astock-agent/frontend/src/lib/api.ts
2026-04-22 22:19:29 +08:00

677 lines
16 KiB
TypeScript

const API_BASE = "";
function getAuthToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem("auth_token");
}
function handleUnauthorized(): void {
if (typeof window === "undefined") return;
localStorage.removeItem("auth_token");
localStorage.removeItem("auth_user");
window.location.href = "/login";
}
export async function fetchAPI<T>(path: string): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${path}`, { headers });
if (res.status === 401) {
handleUnauthorized();
throw new Error("Unauthorized");
}
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function postAPI<T>(path: string, body?: unknown): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${path}`, {
method: "POST",
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 401) {
handleUnauthorized();
throw new Error("Unauthorized");
}
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function deleteAPI<T>(path: string): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${path}`, {
method: "DELETE",
headers,
});
if (res.status === 401) {
handleUnauthorized();
throw new Error("Unauthorized");
}
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || `API error: ${res.status}`);
}
return res.json();
}
export async function patchAPI<T>(path: string, body?: unknown): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}${path}`, {
method: "PATCH",
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 401) {
handleUnauthorized();
throw new Error("Unauthorized");
}
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || `API error: ${res.status}`);
}
return res.json();
}
export interface MarketTemperatureData {
trade_date: string;
temperature: number;
up_count: number;
down_count: number;
limit_up_count: number;
limit_down_count?: number;
max_streak?: number;
broken_rate?: number;
index_above_ma20?: boolean;
}
export interface IndexOverview {
name: string;
code: string;
close: number;
pct_chg: number;
volume: number;
realtime: boolean;
}
export interface RecommendationData {
ts_code: string;
name: string;
sector: string;
score: number;
level: string;
signal: string;
market_temp_score: number;
sector_score: number;
capital_score: number;
technical_score: number;
supply_demand_score?: number;
price_action_score?: number;
position_score?: number;
valuation_score?: number;
entry_price: number | null;
target_price: number | null;
stop_loss: number | null;
reasons: string[];
risk_note: string;
strategy?: "momentum" | "potential" | "trend_breakout";
entry_signal_type?: "breakout" | "pullback" | "launch" | "none";
llm_analysis?: string;
llm_score?: number | null;
recall_tags?: string[];
prefilter_decision?: "priority" | "watch" | "ignore" | "";
prefilter_reason?: string;
focus_points?: string[];
scan_session: string;
created_at: string | null;
entry_timing?: string;
action_plan?: "可操作" | "重点关注" | "观察";
trigger_condition?: string;
invalidation_condition?: string;
suggested_position_pct?: number;
review_after_days?: number;
lifecycle_status?: string;
data_freshness?: string;
tracking?: RecommendationTrackingSummary | null;
}
export interface RecommendationTrackingSummary {
current_price: number | null;
pct_from_entry: number | null;
max_return_pct: number | null;
max_drawdown_pct: number | null;
days_since_recommendation: number | null;
close_reason: string;
review_note: string;
track_date: string;
}
export interface LeadingStock {
ts_code: string;
name: string;
pct_chg: number;
amount: number;
limit_times?: number;
}
export interface SectorData {
sector_code: string;
sector_name: string;
pct_change: number;
trade_date?: string;
capital_inflow: number;
limit_up_count: number;
days_continuous: number;
heat_score: number;
stage?: string;
member_count?: number;
leading_stocks?: LeadingStock[];
leading_stocks_realtime?: LeadingStock[] | null;
pct_trend?: number[];
turnover_avg?: number;
main_force_ratio?: number;
realtime_pct_change?: number | null;
realtime_limit_up_count?: number | null;
realtime_amount?: number | null;
realtime_turnover_rate?: number | null;
realtime_up_count?: number | null;
realtime_down_count?: number | null;
is_realtime?: boolean;
data_mode?: "realtime_overlay" | "daily_snapshot";
structure_trade_date?: string;
}
export interface LatestResult {
market_temperature: MarketTemperatureData | null;
recommendations: RecommendationData[];
strategy_profile?: {
strategy_id: string;
name: string;
description?: string;
buy_threshold?: number;
min_score?: number;
notes?: string[];
} | null;
}
export interface DayGroup {
date: string;
count: number;
buy_count: number;
avg_score: number;
recommendations: RecommendationData[];
}
// ---------- Performance Stats ----------
export interface PerformanceStats {
total_recommendations: number;
tracked: number;
winning: number;
win_rate: number;
avg_return: number;
avg_max_return: number;
avg_max_drawdown: number;
hit_target_count: number;
hit_stop_count: number;
lifecycle_counts: Record<string, number>;
route_breakdown?: Array<{ route: string; count: number; win_rate: number; avg_return: number }>;
prefilter_breakdown?: Array<{ decision: string; count: number; win_rate: number; avg_return: number }>;
details: TrackedRecommendation[];
}
export interface TrackedRecommendation {
recommendation_id: number;
ts_code: string;
name: string;
entry_price: number;
current_price: number;
pct_from_entry: number;
hit_target: boolean;
hit_stop_loss: boolean;
status: string;
action_plan?: string;
lifecycle_status?: string;
recall_tags?: string[];
prefilter_decision?: string;
max_return_pct?: number;
max_drawdown_pct?: number;
days_since_recommendation?: number;
close_reason?: string;
review_note?: string;
track_date: string;
}
// ---------- Daily Review ----------
export interface DailyReview {
trade_date: string;
content: string;
created_at: string;
}
export interface DailyReviewResponse {
reviews: DailyReview[];
}
// ---------- Strategy Board ----------
export interface StrategyFocus {
label: string;
description: string;
}
export interface StrategySectorFocus {
sector_name: string;
stage: string;
heat_score: number;
pct_change: number;
limit_up_count: number;
turnover_rate?: number;
up_count?: number;
down_count?: number;
data_mode?: string;
view: string;
}
export interface StrategyStat {
name: string;
count: number;
win_rate: number;
avg_return: number;
avg_max_return: number;
avg_max_drawdown: number;
hit_target: number;
hit_stop: number;
}
export interface StrategyAdjustment {
target: string;
action: string;
reason: string;
confidence: string;
}
export interface StrategyIterationReport {
generated_at: string;
sample_size: number;
summary: string;
strategy_stats: StrategyStat[];
signal_stats: StrategyStat[];
failure_patterns: string[];
adjustment_suggestions: StrategyAdjustment[];
ai_analysis: string;
generated_by: string;
}
export interface StrategyBoard {
trade_date: string;
data_mode?: string;
market_regime: string;
risk_level: string;
action_bias: string;
position_suggestion: string;
summary: string;
recommended_mode: string;
strategy_focus: StrategyFocus[];
watch_sectors: StrategySectorFocus[];
avoid_rules: string[];
iteration_notes: string[];
iteration_report?: StrategyIterationReport;
metrics: {
temperature?: number;
recommendation_count?: number;
actionable_count?: number;
watch_count?: number;
avg_score?: number;
win_rate?: number;
avg_return?: number;
tracked?: number;
};
ai_review: string;
generated_by: string;
}
export interface StockThesisTracking {
track_date: string;
current_price: number | null;
pct_from_entry: number | null;
max_return_pct: number | null;
max_drawdown_pct: number | null;
days_since_recommendation: number | null;
hit_target: boolean;
hit_stop_loss: boolean;
close_reason: string;
review_note: string;
status: string;
}
export interface StockThesisDiagnosis {
id: number;
diagnosis: string;
created_at: string;
}
export interface StockThesisPoint {
label: string;
value: string;
}
export interface StockThesisResponse {
ts_code: string;
name: string;
has_recommendation: boolean;
recommendation: RecommendationData | null;
latest_tracking: StockThesisTracking | null;
tracking_history: StockThesisTracking[];
diagnoses: StockThesisDiagnosis[];
decision_points: StockThesisPoint[];
data_freshness: {
recommendation_created_at: string;
tracking_date: string;
status: string;
message: string;
};
}
export interface OpsStatusResponse {
scan_running: boolean;
scan_mode: string;
is_trading: boolean;
data_freshness: {
market_trade_date: string;
sector_trade_date: string;
tracking_trade_date: string;
last_recommendation_created_at: string;
last_tracking_created_at: string;
last_market_created_at: string;
last_sector_created_at: string;
last_review_created_at: string;
status: string;
message: string;
generated_at: string;
};
actions: {
key: string;
label: string;
admin_only: boolean;
}[];
}
export interface WatchlistItem {
id: number;
ts_code: string;
name: string;
note: string;
watch_group?: "observe" | "focus" | "candidate" | "holding";
cost_price?: number | null;
created_at: string;
conclusion?: string;
advice?: string;
trigger_condition?: string;
risk_note?: string;
summary?: string;
analysis_created_at?: string;
}
export interface WatchlistHistoryItem {
id: number;
user_id: number;
watchlist_id: number;
ts_code: string;
name: string;
conclusion: string;
advice: string;
trigger_condition: string;
risk_note: string;
summary: string;
full_analysis: string;
score_reference: number;
analysis_mode: string;
created_at: string;
}
// ---------- Sector Rotation ----------
export interface SectorRotationData {
trade_date: string;
dates: string[];
sectors: {
sector_code: string;
sector_name: string;
daily_data: { trade_date: string; pct_change: number; net_amount: number }[];
}[];
}
// ---------- AI Diagnosis ----------
export interface DiagnosisResult {
status: string;
ts_code?: string;
diagnosis?: string;
message?: string;
}
export interface ChatMessage {
role: "user" | "assistant";
content: string;
}
export interface StreamEvent {
type: "content" | "status";
content: string;
}
export async function* streamChat(
messages: ChatMessage[]
): AsyncGenerator<StreamEvent, void, undefined> {
const token = getAuthToken();
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(`${API_BASE}/api/chat/stream`, {
method: "POST",
headers,
body: JSON.stringify({ messages }),
});
if (res.status === 401) {
handleUnauthorized();
throw new Error("Unauthorized");
}
if (!res.ok) throw new Error(`Chat API error: ${res.status}`);
if (!res.body) throw new Error("No response body");
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6).trim();
if (data === "[DONE]") return;
try {
const parsed = JSON.parse(data) as StreamEvent;
yield parsed;
} catch {
// ignore malformed lines
}
}
}
}
}
export interface AuthUser {
id: number;
username: string;
role: "admin" | "user";
}
export interface LoginResponse {
token: string;
user: AuthUser;
}
export async function loginAPI(username: string, password: string): Promise<LoginResponse> {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.detail || `Login failed: ${res.status}`);
}
return res.json();
}
// ---------- User Management ----------
export interface UserItem {
id: number;
username: string;
role: "admin" | "user";
is_active: boolean;
created_at: string | null;
}
export interface CreateUserResult {
username: string;
password: string;
role: string;
message: string;
}
export interface ResetPasswordResult {
username: string;
password: string;
message: string;
}
export async function listUsersAPI(): Promise<UserItem[]> {
return fetchAPI<UserItem[]>("/api/auth/users");
}
export async function createUserAPI(username: string, role: string): Promise<CreateUserResult> {
return postAPI<CreateUserResult>("/api/auth/users", { username, role });
}
export async function disableUserAPI(userId: number): Promise<{ message: string }> {
return deleteAPI<{ message: string }>(`/api/auth/users/${userId}`);
}
export async function resetPasswordAPI(userId: number): Promise<ResetPasswordResult> {
return postAPI<ResetPasswordResult>(`/api/auth/users/${userId}/reset-password`);
}
export async function changePasswordAPI(oldPassword: string, newPassword: string): Promise<{ message: string }> {
return postAPI<{ message: string }>("/api/auth/change-password", {
old_password: oldPassword,
new_password: newPassword,
});
}
// ---------- Data Reset (Admin) ----------
export interface DataStats {
recommendations: number;
tracking: number;
sector_heat: number;
market_temperature: number;
daily_reviews: number;
low_score_count: number;
latest_date: string;
earliest_date: string;
}
export interface DataResetResult {
status: string;
mode: string;
deleted: Record<string, number>;
}
export async function getDataStatsAPI(): Promise<DataStats> {
return fetchAPI<DataStats>("/api/auth/data-stats");
}
export async function dataResetAPI(mode: string, beforeDate?: string, minScore?: number): Promise<DataResetResult> {
return postAPI<DataResetResult>("/api/auth/data-reset", {
mode,
before_date: beforeDate,
min_score: minScore,
});
}
// ---------- Debug Logs (Admin) ----------
export interface ErrorLog {
id: number;
source: string;
level: string;
message: string;
detail: string;
created_at: string;
}
export interface ErrorLogsResult {
total: number;
errors: ErrorLog[];
sources: string[];
levels: string[];
}
export interface SystemStatus {
is_trading: boolean;
scan_running: boolean;
scan_locked: boolean;
recent_errors: number;
last_errors: { source: string; message: string; created_at: string }[];
tables_counts: Record<string, number>;
db_size_mb: number;
}
export async function getErrorLogsAPI(limit: number = 50, source?: string, level?: string, days: number = 7): Promise<ErrorLogsResult> {
const params = new URLSearchParams({ limit: String(limit), days: String(days) });
if (source) params.set("source", source);
if (level) params.set("level", level);
return fetchAPI<ErrorLogsResult>(`/api/debug/errors?${params}`);
}
export async function clearErrorLogsAPI(days: number = 30): Promise<{ status: string; deleted: number }> {
return deleteAPI<{ status: string; deleted: number }>(`/api/debug/errors?days=${days}`);
}
export async function getSystemStatusAPI(): Promise<SystemStatus> {
return fetchAPI<SystemStatus>("/api/debug/system");
}