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(path: string): Promise { const token = getAuthToken(); const headers: Record = {}; 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(path: string, body?: unknown): Promise { const token = getAuthToken(); const headers: Record = { "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(path: string): Promise { const token = getAuthToken(); const headers: Record = {}; 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(path: string, body?: unknown): Promise { const token = getAuthToken(); const headers: Record = { "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; 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 { const token = getAuthToken(); const headers: Record = { "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 { 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 { return fetchAPI("/api/auth/users"); } export async function createUserAPI(username: string, role: string): Promise { return postAPI("/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 { return postAPI(`/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; } export async function getDataStatsAPI(): Promise { return fetchAPI("/api/auth/data-stats"); } export async function dataResetAPI(mode: string, beforeDate?: string, minScore?: number): Promise { return postAPI("/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; db_size_mb: number; } export async function getErrorLogsAPI(limit: number = 50, source?: string, level?: string, days: number = 7): Promise { const params = new URLSearchParams({ limit: String(limit), days: String(days) }); if (source) params.set("source", source); if (level) params.set("level", level); return fetchAPI(`/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 { return fetchAPI("/api/debug/system"); }