// 后端 API 客户端 — 全项目统一入口 // // Dev:Vite 代理 /api 到 :3018 // Prod:同源(FastAPI 托管前端 dist) import { toast } from '@/components/Toast' const BASE = '' function getAccessToken(): string | null { try { return localStorage.getItem('access_token') } catch { return null } } function clearAccessToken(): void { try { localStorage.removeItem('access_token') } catch { /* ignore */ } } async function request(path: string, init?: RequestInit): Promise { const isFormData = init?.body instanceof FormData const headers: Record = {} if (!isFormData) headers['Content-Type'] = 'application/json' const token = getAccessToken() if (token) headers['X-Access-Token'] = token // 合并调用方传入的 headers(如显式覆盖 Content-Type) if (init?.headers) { const incoming = init.headers as Record for (const [k, v] of Object.entries(incoming)) { if (v != null) headers[k] = v } } const res = await fetch(`${BASE}${path}`, { ...init, headers }) if (!res.ok) { // 401 时清除令牌并跳转登录页,避免无限弹 toast if (res.status === 401) { clearAccessToken() if (typeof window !== 'undefined' && window.location.pathname !== '/verify') { window.location.href = '/verify' } } let detail = '' try { const j = JSON.parse(await res.text()); detail = j.detail ?? j.message ?? '' } catch { /* ignore */ } const msg = detail || `${res.status} ${res.statusText}` toast(msg, 'error') throw new Error(msg) } return res.json() as Promise } // ===== Access UUID Gate ===== export interface VerifyAccessUuidResponse { valid: boolean role?: 'admin' | 'user' token?: string } export interface AccessAuthStatusResponse { enabled: boolean verified: boolean role?: 'admin' | 'user' } export interface UuidRecord { uuid: string label: string enabled: boolean created_at: number } // ===== Capabilities ===== export interface CapabilityLimits { rpm: number | null batch: number | null subscribe: number | null } export interface CapabilitiesResponse { label: string capabilities: Record } // ===== Financials ===== export interface FinancialStatus { available: boolean tables: Record last_sync: Record /** 服务端是否正在同步(手动触发)——驱动"同步中"UI 并防重复点击 */ syncing?: boolean } export interface FinancialMetricRecord { symbol?: string period_end: string announce_date?: string | null eps_basic?: number | null eps_diluted?: number | null bps?: number | null ocfps?: number | null roe?: number | null roe_diluted?: number | null roa?: number | null gross_margin?: number | null net_margin?: number | null debt_to_asset_ratio?: number | null revenue_yoy?: number | null net_income_yoy?: number | null operating_cash_to_revenue?: number | null inventory_turnover?: number | null [key: string]: any } export interface FinancialIncomeRecord { symbol?: string period_end: string announce_date?: string | null revenue?: number | null operating_cost?: number | null operating_profit?: number | null total_profit?: number | null net_income?: number | null net_income_attributable?: number | null basic_eps?: number | null diluted_eps?: number | null [key: string]: any } export interface FinancialBalanceSheetRecord { symbol?: string period_end: string announce_date?: string | null total_assets?: number | null total_current_assets?: number | null cash_and_equivalents?: number | null total_liabilities?: number | null total_equity?: number | null equity_attributable?: number | null [key: string]: any } export interface FinancialCashFlowRecord { symbol?: string period_end: string announce_date?: string | null net_operating_cash_flow?: number | null net_investing_cash_flow?: number | null net_financing_cash_flow?: number | null capex?: number | null net_cash_change?: number | null [key: string]: any } // ===== Kline ===== export interface MinuteKlineRow { datetime: string open: number high: number low: number close: number volume: number amount: number } export interface KlineRow { symbol?: string date: string open: number high: number low: number close: number volume?: number change_pct?: number ma5?: number | null ma20?: number | null ma60?: number | null macd_dif?: number | null macd_dea?: number | null macd_hist?: number | null rsi_14?: number | null vol_ratio_5d?: number | null [key: string]: any } // ===== Watchlist ===== export interface WatchlistEntry { symbol: string added_at: string note?: string } export interface Quote { symbol: string price?: number pct?: number close?: number change_pct?: number [key: string]: any } export interface IndexInstrument { symbol: string name?: string | null code?: string | null asset_type?: 'index' [key: string]: any } export interface IndexQuote { symbol: string name?: string | null last_price?: number | null close?: number | null prev_close?: number | null change_pct?: number | null change_amount?: number | null open?: number | null high?: number | null low?: number | null volume?: number | null amount?: number | null timestamp?: number | null [key: string]: any } // ===== Screener ===== export interface ScreenerStrategy { id: string name: string description: string source?: string } export interface ScreenerResult { as_of: string strategy: string | null rows: any[] total: number elapsed_ms: number } export interface MarketSnapshotRow { symbol: string name?: string | null close?: number | null change_pct?: number | null amount?: number | null volume?: number | null turnover_rate?: number | null vol_ratio_5d?: number | null total_shares?: number | null float_shares?: number | null market_cap?: number | null float_market_cap?: number | null consecutive_limit_ups?: number | null [key: string]: any } export interface OverviewDimensionRankItem { name: string count: number avg_pct: number up_count: number down_count: number amount: number leader?: { symbol?: string | null name?: string | null change_pct?: number | null } | null } export interface OverviewMarket { as_of: string | null quote_status: { enabled?: boolean running?: boolean quote_age_ms?: number | null is_trading_hours?: boolean [key: string]: any } indices: IndexQuote[] breadth: { total: number up: number down: number flat: number up_pct: number down_pct: number avg_pct?: number | null median_pct?: number | null strong_up?: number strong_down?: number } amount: { total: number; avg: number } boards: { board: string; count: number; up: number; down: number; up_pct: number; amount: number }[] limit: { limit_up: number; broken: number; failed: number; limit_down: number; max_boards: number; seal_rate?: number; tiers: { boards: number; count: number }[]; sealed_ready?: boolean; fake_up?: number; fake_down?: number } distribution: { label: string; count: number; pct: number }[] trend: { above_ma5: number; above_ma20: number; above_ma60: number; above_ma5_pct: number; above_ma20_pct: number; above_ma60_pct: number; new_high: number; new_low: number } activity: { avg_turnover: number; high_turnover: number; high_vol_ratio: number; vol_ratio: number } radar: { key: string; label: string; value: number }[] emotion: { score: number; label: string } top_gainers: MarketSnapshotRow[] top_losers: MarketSnapshotRow[] turnover_leaders: MarketSnapshotRow[] active_leaders: MarketSnapshotRow[] concept_rank: { leading: OverviewDimensionRankItem[]; lagging: OverviewDimensionRankItem[] } industry_rank: { leading: OverviewDimensionRankItem[]; lagging: OverviewDimensionRankItem[] } } // ===== Strategy Engine ===== export interface StrategyParamDef { id: string label: string type: 'float' | 'int' | 'select' | 'bool' default: number | string | boolean min?: number max?: number step?: number options?: string[] } export interface StrategyDetail { id: string name: string description: string tags: string[] source: 'builtin' | 'custom' | 'ai' version: string basic_filter: Record params: StrategyParamDef[] params_defaults: Record scoring: Record entry_signals: string[] exit_signals: string[] stop_loss: number | null trailing_stop: number | null trailing_take_profit_activate: number | null trailing_take_profit_drawdown: number | null max_hold_days: number | null display_limit?: number alerts: { field: string; op?: string; value?: number; message: string }[] order_by: string descending: boolean limit: number } // ===== Custom Signals (自定义信号) ===== export interface CustomSignalCondition { left: string // 字段名 op: string // > >= < <= == != right: string // "field:xxx" 或数字字符串 } export interface CustomSignal { id: string name: string kind: 'entry' | 'exit' | 'both' conditions: CustomSignalCondition[] enabled: boolean } export interface CustomSignalOptions { fields: { key: string; label: string }[] operators: string[] kinds: { key: string; label: string }[] } // ===== Monitor (监控规则 + 触发记录) ===== export interface MonitorCondition { field: string op: string // truth | > >= < <= == != value?: number | null // op 非 truth 时必填 } export interface MonitorRule { id: string name: string enabled: boolean type: 'strategy' | 'signal' | 'price' | 'market' scope: 'symbols' | 'all' | 'sector' symbols: string[] sector?: string | null strategy_id?: string | null direction: 'entry' | 'exit' | 'both' conditions: MonitorCondition[] logic: 'and' | 'or' cooldown_seconds: number severity: 'info' | 'warn' | 'critical' message: string webhook_url?: string webhook_enabled?: boolean created_at?: string } export interface MonitorRuleOptions { threshold_fields: { key: string; label: string }[] builtin_signals: { key: string; label: string }[] custom_signals: { key: string; label: string }[] operators: string[] types: { key: string; label: string }[] scopes: { key: string; label: string }[] logics: { key: string; label: string }[] severities: { key: string; label: string }[] directions: { key: string; label: string }[] } export interface AlertEvent { ts: number rule_id?: string rule_name?: string source: string type: string symbol?: string name?: string | null message: string price?: number | null change_pct?: number | null signals?: string[] severity?: string strategy_id?: string } /** 生成监控规则 id (时间戳 + 随机后缀), 用户无需手动填写。 */ export function genRuleId(): string { const ts = Date.now().toString(36) const rand = Math.random().toString(36).slice(2, 6) return `mr_${ts}_${rand}` } // ===== Limit Ladder ===== export interface LimitLadderStock { symbol: string name?: string | null close?: number | null change_pct?: number | null consecutive_limit_ups?: number | null consecutive_limit_downs?: number | null status?: 'limit_up' | 'broken' | 'failed' | 'limit_down' | 'recovery' | null /** 五档 sealed: real=真封板, fake=假涨停(已归炸板), pending=待确认, null=降级/无能力 */ sealed_status?: 'real' | 'fake' | 'pending' | null /** 封单量(买一/卖一量), 仅真封板有值 */ sealed_vol?: number | null } export interface LimitLadderTier { boards: number count: number stocks: LimitLadderStock[] } export interface LimitLadderResult { as_of: string tiers: LimitLadderTier[] /** 双方向涨跌停计数(修正后, 不论当前 direction) */ counts?: { up: number; down: number } /** 双方向涨跌停原始计数(修正前, 供弹窗对比) */ counts_raw?: { up: number; down: number } /** sealed 数据是否就绪(false→前端显示降级标识) */ sealed_ready?: boolean /** sealed 数据 age(秒), null=盘后定版或无数据 */ sealed_age?: number | null /** sealed 修正统计: real=真封板, fake=假涨停(归炸板), pending=待确认 */ sealed_counts?: { real: number; fake: number; pending: number } /** 涨停侧 sealed 明细 */ sealed_counts_up?: { real: number; fake: number; pending: number } /** 跌停侧 sealed 明细 */ sealed_counts_down?: { real: number; fake: number; pending: number } } // ===== Backtest ===== export interface BacktestResult { run_id: string config: any stats: Record equity_curve: { date: string; value: number }[] trades: any[] per_symbol_stats: { symbol: string; total_return: number }[] } // ===== Factor Backtest ===== export interface FactorColumn { id: string label: string group: string desc: string } export interface GroupStat { group: number label: string total_return: number annual_return: number max_drawdown: number sharpe: number win_rate: number } export interface FactorBacktestResult { run_id: string config: Record ic_mean: number | null ic_std: number | null ir: number | null ic_win_rate: number | null ic_series: { date: string; ic: number }[] group_stats: GroupStat[] group_nav: Record[] long_short_stats: Record long_short_nav: { date: string; value: number }[] elapsed_ms: number n_symbols: number n_dates: number error: string | null } // ===== Strategy Backtest ===== export interface StrategyBacktestTrade { symbol: string name?: string entry_date: string exit_date: string entry_price: number exit_price: number pnl_pct: number duration: number exit_reason: string shares?: number lots?: number position_pct?: number entry_value?: number exit_value?: number pnl_amount?: number entry_score?: number | null entry_signal_date?: string | null exit_signal_date?: string | null blocked_exit_days?: number } export interface StrategyBacktestResult { run_id: string config: Record stats: Record equity_curve: { date: string; value: number; cash?: number; positions?: number; exposure?: number }[] drawdown_curve: { date: string; value: number }[] benchmark_curve?: { date: string; value: number; close?: number; name?: string; symbol?: string }[] trades: StrategyBacktestTrade[] per_symbol_stats: { symbol: string n_trades: number total_return: number win_rate: number best: number worst: number }[] strategy_info: { id: string name: string description: string entry_signals: string[] exit_signals: string[] stop_loss: number | null trailing_stop: number | null trailing_take_profit_activate: number | null trailing_take_profit_drawdown: number | null score_min: number | null score_max: number | null max_hold_days: number | null source: string } elapsed_ms: number error: string | null } // ===== Settings ===== /** 端点发现清单 —— 对应数据源官网 /endpoints.json */ export interface EndpointItem { id: string url: string label: string region?: string description?: string premium?: boolean } export interface EndpointManifest { version?: number description?: string healthPath?: string /** 每端点测试轮数,用于 /health 多轮探测取中位数 */ testRounds?: number endpoints: EndpointItem[] /** 数据来源:remote=远程拉取 / fallback=内置回退列表 */ source?: 'remote' | 'fallback' } export interface SettingsState { mode: 'none' | 'free' | 'api_key' tickflow_api_key_masked: string has_tickflow_key: boolean tier_label: string current_endpoint: string probe_log: string[] missing_caps: string[] extras_caps: string[] // 首次使用引导 onboarding_completed: boolean // AI 配置 ai_provider: string ai_base_url: string ai_api_key_masked: string has_ai_key: boolean ai_model: string ai_daily_token_budget: number } /** 保存数据源 Key 的响应(先探后存) */ export interface SaveTickflowKeyResult { ok: boolean /** ok=false 且 key 无效时的原因标识,前端据此提示「Key 无效」 */ reason?: 'invalid' error?: string mode?: 'none' | 'free' | 'api_key' tier_label?: string current_endpoint?: string tickflow_api_key_masked?: string capabilities_count?: number } export interface Preferences { realtime_quotes_enabled: boolean indices_nav_pinned: boolean minute_sync_enabled: boolean minute_sync_days: number pipeline_schedule: { hour: number; minute: number } instruments_schedule: { hour: number; minute: number } enriched_batch_size: number index_daily_batch_size: number limit_ladder_monitor_enabled: boolean depth_polling_interval: number depth_finalize_time: { hour: number; minute: number } sse_refresh_pages: Record strategy_monitor_enabled: boolean strategy_monitor_ids: string[] system_notify_enabled: boolean sidebar_index_symbols: string[] nav_order: string[] nav_hidden: string[] screener_auto_run: boolean } // ===== Strategy Alert ===== export interface StrategyAlertEvent { source: 'strategy' | 'depth' type: string strategy_id?: string symbol?: string name?: string | null message: string price?: number | null change_pct?: number | null signals?: string[] } // ===== API surface ===== export const api = { health: () => request<{ status: string; version: string; mode: string }>('/health'), verifyAccessUuid: (credential: string) => request('/api/auth/verify', { method: 'POST', body: JSON.stringify({ credential }), }), accessAuthStatus: () => request('/api/auth/status'), // ===== Admin UUID management ===== listUuids: () => request('/api/admin/uuids'), createUuid: (label = '') => request('/api/admin/uuids', { method: 'POST', body: JSON.stringify({ label }), }), deleteUuid: (uuid: string) => request<{ ok: boolean }>(`/api/admin/uuids/${encodeURIComponent(uuid)}`, { method: 'DELETE' }), toggleUuid: (uuid: string, enabled: boolean) => request<{ ok: boolean }>( `/api/admin/uuids/${encodeURIComponent(uuid)}/toggle?enabled=${enabled}`, { method: 'PUT' }, ), settings: () => request('/api/settings'), saveTickflowKey: (api_key: string) => request('/api/settings/tickflow-key', { method: 'POST', body: JSON.stringify({ api_key }), }), clearTickflowKey: () => request('/api/settings/tickflow-key', { method: 'DELETE' }), /** 标记首次使用向导完成(持久化到后端 preferences) */ completeOnboarding: () => request<{ ok: boolean; onboarding_completed: boolean }>( '/api/settings/onboarding/complete', { method: 'POST' }, ), /** 保存 AI 配置 */ saveAiSettings: (ai: { provider?: string; base_url?: string; api_key?: string; model?: string; daily_token_budget?: number }) => request<{ ok: boolean }>('/api/settings/ai', { method: 'POST', body: JSON.stringify(ai), }), preferences: () => request('/api/settings/preferences'), updateMinuteSync: (enabled: boolean, days: number) => request('/api/settings/preferences/minute-sync', { method: 'PUT', body: JSON.stringify({ minute_sync_enabled: enabled, minute_sync_days: days }), }), updateRealtimeQuotes: (enabled: boolean) => request<{ realtime_quotes_enabled: boolean }>('/api/settings/preferences/realtime-quotes', { method: 'PUT', body: JSON.stringify({ realtime_quotes_enabled: enabled }), }), updateIndicesNavPinned: (pinned: boolean) => request<{ indices_nav_pinned: boolean }>('/api/settings/preferences/indices-nav-pinned', { method: 'PUT', body: JSON.stringify({ indices_nav_pinned: pinned }), }), quoteStatus: () => request<{ enabled: boolean running: boolean interval_s: number symbol_count: number index_symbol_count?: number quote_age_ms: number | null is_trading_hours: boolean last_fetch_ms: number | null }>('/api/intraday/status'), quoteInterval: () => request<{ interval: number; min_interval: number; max_interval: number }>( '/api/settings/preferences/quote-interval', ), updateQuoteInterval: (interval: number) => request<{ interval: number; min_interval: number; max_interval: number }>( '/api/settings/preferences/quote-interval', { method: 'PUT', body: JSON.stringify({ interval }) }, ), intradayRefresh: () => request<{ status: string }>('/api/intraday/refresh', { method: 'POST' }), indexQuotes: (symbols?: string[]) => request<{ rows: IndexQuote[]; count: number }>( `/api/intraday/indices${symbols?.length ? `?symbols=${encodeURIComponent(symbols.join(','))}` : ''}`, ), updateRealtimeMonitorConfig: (cfg: { sse_refresh_pages?: Record strategy_monitor_enabled?: boolean strategy_monitor_ids?: string[] sidebar_index_symbols?: string[] screener_auto_run?: boolean }) => request<{ sse_refresh_pages: Record strategy_monitor_enabled: boolean strategy_monitor_ids: string[] sidebar_index_symbols: string[] screener_auto_run: boolean }>('/api/settings/preferences/realtime-monitor', { method: 'PUT', body: JSON.stringify(cfg), }), updateSystemNotify: (enabled: boolean) => request<{ system_notify_enabled: boolean }>('/api/settings/preferences/system-notify', { method: 'PUT', body: JSON.stringify({ enabled }), }), updatePipelineSchedule: (hour: number, minute: number) => request<{ hour: number; minute: number }>('/api/settings/preferences/pipeline-schedule', { method: 'PUT', body: JSON.stringify({ hour, minute }), }), updateDepthPollingInterval: (interval: number) => request<{ depth_polling_interval: number }>('/api/settings/preferences/depth-polling-interval', { method: 'PUT', body: JSON.stringify({ interval }), }), updateLimitLadderMonitor: (enabled: boolean) => request<{ limit_ladder_monitor_enabled: boolean }>('/api/settings/preferences/limit-ladder-monitor', { method: 'PUT', body: JSON.stringify({ enabled }), }), runLimitLadderFix: () => request<{ ok: boolean; count: number; msg: string }>('/api/settings/preferences/limit-ladder-monitor/run', { method: 'POST', }), updateDepthFinalizeTime: (hour: number, minute: number) => request<{ hour: number; minute: number }>('/api/settings/preferences/depth-finalize-time', { method: 'PUT', body: JSON.stringify({ hour, minute }), }), saveNavOrder: (nav_order: string[]) => request<{ nav_order: string[] }>('/api/settings/preferences/nav-order', { method: 'PUT', body: JSON.stringify({ nav_order }), }), saveNavHidden: (nav_hidden: string[]) => request<{ nav_hidden: string[] }>('/api/settings/preferences/nav-hidden', { method: 'PUT', body: JSON.stringify({ nav_hidden }), }), updateInstrumentsSchedule: (hour: number, minute: number) => request<{ hour: number; minute: number }>('/api/settings/preferences/instruments-schedule', { method: 'PUT', body: JSON.stringify({ hour, minute }), }), updateEnrichedBatchSize: (size: number) => request<{ enriched_batch_size: number }>('/api/settings/preferences/enriched-batch-size', { method: 'PUT', body: JSON.stringify({ size }), }), updateIndexDailyBatchSize: (size: number) => request<{ index_daily_batch_size: number }>('/api/settings/preferences/index-daily-batch-size', { method: 'PUT', body: JSON.stringify({ size }), }), // 自选列表列配置 watchlistColumns: () => request<{ columns: any[] | null }>('/api/settings/preferences/watchlist-columns'), updateWatchlistColumns: (columns: any[]) => request<{ columns: any[] }>('/api/settings/preferences/watchlist-columns', { method: 'PUT', body: JSON.stringify({ columns }), }), // 策略结果列表列配置 screenerResultColumns: () => request<{ columns: any[] | null }>('/api/settings/preferences/screener-result-columns'), updateScreenerResultColumns: (columns: any[]) => request<{ columns: any[] }>('/api/settings/preferences/screener-result-columns', { method: 'PUT', body: JSON.stringify({ columns }), }), capabilities: () => request('/api/capabilities'), version: () => request<{ version: string }>('/api/data/version'), redetectCapabilities: () => request('/api/capabilities/redetect', { method: 'POST' }), klineDaily: (symbol: string, days = 120, dateRange?: { start: string; end: string }, extColumns?: string) => request<{ symbol: string name?: string stock_info?: { name?: string; total_shares?: number; float_shares?: number; ext?: Record } rows: KlineRow[] source?: string }>( (dateRange ? `/api/kline/daily?symbol=${encodeURIComponent(symbol)}&start_date=${dateRange.start}&end_date=${dateRange.end}` : `/api/kline/daily?symbol=${encodeURIComponent(symbol)}&days=${days}`) + (extColumns ? `&ext_columns=${encodeURIComponent(extColumns)}` : ''), ), klineDailyBatch: (symbols: string[], days = 12) => request<{ data: Record }>('/api/kline/daily-batch', { method: 'POST', body: JSON.stringify({ symbols, days }), }), instrumentSearch: (q: string, limit = 20) => request<{ results: { symbol: string; name: string; code: string }[] }>( `/api/kline/instruments/search?q=${encodeURIComponent(q)}&limit=${limit}`, ), /** 批量查股票名称 (传入 symbol 列表, 返回 {symbol: name}) */ instrumentNames: (symbols: string[]) => request<{ names: Record }>('/api/kline/instruments/names', { method: 'POST', body: JSON.stringify(symbols), }), klineMinute: (symbol: string, date?: string) => request<{ symbol: string name?: string stock_info?: { name?: string; total_shares?: number; float_shares?: number } date: string | null rows: MinuteKlineRow[] source?: 'local' | 'live' | 'none' }>( `/api/kline/minute?symbol=${encodeURIComponent(symbol)}${date ? `&date=${date}` : ''}`, ), indexList: () => request<{ results: IndexInstrument[]; count: number }>('/api/index/list'), indexSearch: (q: string, limit = 20) => request<{ results: IndexInstrument[] }>( `/api/index/search?q=${encodeURIComponent(q)}&limit=${limit}`, ), indexDaily: (symbol: string, days = 120, dateRange?: { start: string; end: string }) => request<{ symbol: string name?: string index_info?: IndexInstrument rows: KlineRow[] source?: string }>( dateRange ? `/api/index/daily?symbol=${encodeURIComponent(symbol)}&start_date=${dateRange.start}&end_date=${dateRange.end}` : `/api/index/daily?symbol=${encodeURIComponent(symbol)}&days=${days}`, ), indexMinute: (symbol: string, date?: string) => request<{ symbol: string name?: string index_info?: IndexInstrument date: string | null rows: MinuteKlineRow[] source?: string }>( `/api/index/minute?symbol=${encodeURIComponent(symbol)}${date ? `&date=${date}` : ''}`, ), syncIndexInstruments: () => request<{ status: string; count: number }>('/api/index/sync_instruments', { method: 'POST' }), syncIndexDaily: (days = 365) => request<{ status: string; index_count: number; rows_written: number }>( `/api/index/sync_daily?days=${days}`, { method: 'POST' }, ), syncSymbol: (symbol: string, days = 250) => request<{ symbol: string; rows_written: number }>( `/api/kline/sync?symbol=${encodeURIComponent(symbol)}&days=${days}`, { method: 'POST' }, ), syncMinute: () => request<{ status: string; job_id: string }>('/api/kline/sync_minute', { method: 'POST' }), extendHistory: (value: number, unit: 'day' | 'month' | 'year') => request<{ status: string; job_id: string }>('/api/kline/extend_history', { method: 'POST', body: JSON.stringify({ value, unit }), }), extendMinuteHistory: (value: number, unit: 'day' | 'month') => request<{ status: string; job_id: string }>('/api/kline/extend_minute_history', { method: 'POST', body: JSON.stringify({ value, unit }), }), rebuildEnriched: () => request<{ status: string; job_id: string }>('/api/kline/rebuild_enriched', { method: 'POST', }), watchlistList: () => request<{ symbols: WatchlistEntry[] }>('/api/watchlist'), watchlistAdd: (symbol: string, note = '') => request<{ symbols: WatchlistEntry[] }>('/api/watchlist', { method: 'POST', body: JSON.stringify({ symbol, note }), }), watchlistBatchAdd: (symbols: string[], note = '') => request<{ symbols: WatchlistEntry[]; added: number }>('/api/watchlist/batch', { method: 'POST', body: JSON.stringify({ symbols, note }), }), watchlistRemove: (symbol: string) => request<{ symbols: WatchlistEntry[] }>( `/api/watchlist/${encodeURIComponent(symbol)}`, { method: 'DELETE' }, ), watchlistClear: () => request<{ removed: number }>('/api/watchlist', { method: 'DELETE' }), watchlistQuotes: () => request<{ quotes: Quote[] }>('/api/watchlist/quotes'), watchlistEnriched: (extColumns?: string) => request<{ rows: any[]; as_of: string | null; elapsed_ms: number }>( extColumns ? `/api/watchlist/enriched?ext_columns=${encodeURIComponent(extColumns)}` : '/api/watchlist/enriched', ), screenerStrategies: () => request<{ presets: ScreenerStrategy[] }>('/api/screener/strategies'), screenerRunPreset: (strategy_id: string, pool?: string[], asOf?: string, extColumns?: string) => request('/api/screener/run_preset', { method: 'POST', body: JSON.stringify({ strategy_id, pool, as_of: asOf ?? null, ext_columns: extColumns || null }), }), screenerRunCustom: (conditions: string[], orderBy?: string, limit = 30, pool?: string[], extColumns?: string) => request('/api/screener/run', { method: 'POST', body: JSON.stringify({ conditions, order_by: orderBy, limit, pool, ext_columns: extColumns || null }), }), screenerRunAll: (asOf?: string, strategyIds?: string[], extColumns?: string) => request<{ as_of: string | null; results: Record }>( '/api/screener/run_all', { method: 'POST', body: JSON.stringify({ as_of: asOf ?? null, strategy_ids: strategyIds ?? null, ext_columns: extColumns || null }) }, ), screenerCached: (extColumns?: string) => request<{ as_of: string | null; results: Record; today_ever_matched: Record | null; today_ever_rows: Record> | null; updated_at: number | null }>( extColumns ? `/api/screener/cached?ext_columns=${encodeURIComponent(extColumns)}` : '/api/screener/cached', ), marketSnapshot: () => request<{ as_of: string | null; rows: MarketSnapshotRow[] }>('/api/screener/market-snapshot'), overviewMarket: (asOf?: string) => request(`/api/overview/market${asOf ? `?as_of=${asOf}` : ''}`), limitLadder: (asOf?: string, extColumns?: string, direction?: 'up' | 'down') => { const params = new URLSearchParams() if (asOf) params.set('as_of', asOf) if (extColumns) params.set('ext_columns', extColumns) if (direction === 'down') params.set('direction', 'down') const qs = params.toString() return request( `/api/screener/limit-ladder${qs ? `?${qs}` : ''}`, ) }, backtestStatus: () => request<{ available: boolean }>('/api/backtest/status'), backtestRun: (payload: { symbols: string[] entries: string[] exits: string[] start?: string end?: string stop_loss_pct?: number max_hold_days?: number matching?: 'close_t' | 'open_t+1' }) => request('/api/backtest/run', { method: 'POST', body: JSON.stringify(payload), }), factorColumns: () => request<{ columns: FactorColumn[] }>('/api/backtest/factor/columns'), factorRun: (payload: { factor_name: string symbols?: string[] | null start?: string | null end?: string | null n_groups?: number rebalance?: 'daily' | 'weekly' | 'monthly' weight?: 'equal' | 'factor_weight' fees_pct?: number slippage_bps?: number }) => request('/api/backtest/factor/run', { method: 'POST', body: JSON.stringify(payload), }), strategyBacktestRun: (payload: { strategy_id: string symbols?: string[] | null start?: string | null end?: string | null params?: Record | null overrides?: Record | null matching?: 'close_t' | 'open_t+1' entry_fill?: 'close_t' | 'open_t+1' | null exit_fill?: 'close_t' | 'open_t+1' | null fees_pct?: number slippage_bps?: number max_positions?: number initial_capital?: number position_sizing?: 'equal' | 'score_weight' }) => request('/api/backtest/strategy/run', { method: 'POST', body: JSON.stringify(payload), }), pipelineRun: () => request<{ job_id: string; reused: boolean }>( '/api/pipeline/run', { method: 'POST' }, ), pipelineJob: (id: string) => request(`/api/pipeline/jobs/${id}`), pipelineJobs: (limit = 20) => request<{ active_id: string | null; jobs: PipelineJobSummary[] }>( `/api/pipeline/jobs?limit=${limit}`, ), dataStatus: () => request('/api/data/status'), dataClear: () => request<{ deleted_files: number }>('/api/data/clear', { method: 'POST' }), enrichedSchema: (table: string) => request(`/api/data/schema/${table}`), testEndpoint: (url: string, rounds?: number) => request<{ ok: boolean url: string rounds: number success: number median_ms: number | null min_ms?: number | null max_ms?: number | null /** 兼容旧字段,等于 median_ms */ latency_ms?: number | null error?: string }>( '/api/settings/test_endpoint', { method: 'POST', body: JSON.stringify({ url, rounds }), }, ), // 端点发现 —— 后端代理拉取数据源官网 /endpoints.json(前端无法跨域直连) listEndpoints: () => request('/api/settings/endpoints'), switchEndpoint: (url: string) => request<{ ok: boolean; current_endpoint: string; error?: string }>( '/api/settings/switch_endpoint', { method: 'POST', body: JSON.stringify({ url }), }, ), // ===== 扩展数据 ===== extDataList: () => request<{ items: ExtDataConfig[] }>('/api/ext-data'), extDataRows: (id: string, opts?: { date?: string; limit?: number; columns?: string[] }) => { const qs = new URLSearchParams() if (opts?.date) qs.set('date', opts.date) if (opts?.limit) qs.set('limit', String(opts.limit)) if (opts?.columns?.length) qs.set('columns', opts.columns.join(',')) const suffix = qs.toString() return request(`/api/ext-data/${encodeURIComponent(id)}/rows${suffix ? `?${suffix}` : ''}`) }, analysisMenus: () => request<{ items: AnalysisMenu[] }>('/api/analysis-menus'), analysisMenu: (id: string) => request(`/api/analysis-menus/${encodeURIComponent(id)}`), analysisMenuSave: (id: string, body: Omit) => request(`/api/analysis-menus/${encodeURIComponent(id)}`, { method: 'POST', body: JSON.stringify(body), }), analysisMenuReorder: (ids: string[]) => request<{ items: AnalysisMenu[] }>('/api/analysis-menus/reorder', { method: 'POST', body: JSON.stringify({ ids }), }), analysisMenuDelete: (id: string) => request<{ status: string }>(`/api/analysis-menus/${encodeURIComponent(id)}`, { method: 'DELETE' }), extDataCreate: (body: { id: string; label: string; mode: 'snapshot' | 'timeseries'; fields: { name: string; dtype: string; label: string }[]; description?: string; symbol_map?: Record; code_map?: Record }) => request('/api/ext-data', { method: 'POST', body: JSON.stringify(body), }), extDataUpdate: (id: string, body: { label?: string; fields?: { name: string; dtype: string; label: string }[]; description?: string }) => request(`/api/ext-data/${id}`, { method: 'PUT', body: JSON.stringify(body), }), extDataDelete: (id: string) => request<{ status: string }>(`/api/ext-data/${id}`, { method: 'DELETE' }), extDataUpload: (id: string, file: File, snapshotDate?: string) => { const fd = new FormData() fd.append('file', file) return request<{ status: string; rows: number; date: string }>( `/api/ext-data/${id}/upload${snapshotDate ? `?snapshot_date=${snapshotDate}` : ''}`, { method: 'POST', body: fd }, ) }, extDataIngest: (id: string, body: { date?: string; rows: Record[] }) => request<{ status: string; rows: number; date: string }>( `/api/ext-data/${id}/ingest`, { method: 'POST', body: JSON.stringify(body) }, ), extDataSchemaAll: () => request<{ items: { id: string; label: string; mode: string; columns: { name: string; type: string; label: string }[] }[] }>('/api/ext-data/schema-all'), extDataPullConfig: (id: string, body: { url: string; method?: string; headers?: Record; body?: string; response_path?: string; field_map?: Record; schedule_minutes?: number; enabled?: boolean; }) => request<{ status: string; pull: PullConfig }>( `/api/ext-data/${id}/pull`, { method: 'PUT', body: JSON.stringify(body) }, ), extDataPullTest: (id: string) => request<{ status: string; total_rows: number; preview: Record[]; has_symbol: boolean }>( `/api/ext-data/${id}/pull/test`, { method: 'POST' }, ), extDataPullRun: (id: string) => request<{ status: string; rows: number; date: string }>( `/api/ext-data/${id}/pull/run`, { method: 'POST' }, ), extDataDetectFields: (file: File) => { const fd = new FormData() fd.append('file', file) return request<{ fields: { name: string; dtype: string; label: string }[]; rows: number; symbol_candidates: string[]; code_candidates: string[] }>( '/api/ext-data/detect-fields', { method: 'POST', body: fd }, ) }, extDataFixSymbol: (id: string) => request<{ status: string; fixed_files: number }>( `/api/ext-data/${id}/fix-symbol`, { method: 'POST' }, ), // ===== Financials ===== financialStatus: () => request('/api/financials/status'), financialMetrics: (symbol?: string) => request<{ data: FinancialMetricRecord[] }>( `/api/financials/metrics${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ''}`, ), financialIncome: (symbol?: string) => request<{ data: FinancialIncomeRecord[] }>( `/api/financials/income${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ''}`, ), financialBalanceSheet: (symbol?: string) => request<{ data: FinancialBalanceSheetRecord[] }>( `/api/financials/balance-sheet${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ''}`, ), financialCashFlow: (symbol?: string) => request<{ data: FinancialCashFlowRecord[] }>( `/api/financials/cash-flow${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ''}`, ), financialSync: (table: string) => request<{ status: string; synced: Record }>( `/api/financials/sync/${table}`, { method: 'POST' }, ), // ===== Strategy Engine ===== strategyList: () => request<{ strategies: StrategyDetail[] }>('/api/strategies'), strategyGet: (id: string) => request(`/api/strategies/${id}`), strategyRun: (strategyId: string, params?: Record, asOf?: string, pool?: string[]) => request('/api/strategies/run', { method: 'POST', body: JSON.stringify({ strategy_id: strategyId, params, as_of: asOf ?? null, pool }), }), strategyRunAll: (asOf?: string) => request<{ as_of: string | null; results: Record }>( '/api/strategies/run-all', { method: 'POST', body: JSON.stringify({ as_of: asOf ?? null }) }, ), strategySaveConfig: (strategyId: string, overrides: Record) => request<{ ok: boolean }>('/api/strategies/config', { method: 'POST', body: JSON.stringify({ strategy_id: strategyId, overrides }), }), strategyResetConfig: (strategyId: string) => request<{ ok: boolean }>(`/api/strategies/config/${strategyId}`, { method: 'DELETE' }), /** 删除自定义策略(内置策略不可删除) */ strategyDelete: (strategyId: string) => request<{ ok: boolean }>(`/api/strategies/${strategyId}`, { method: 'DELETE' }), strategyReload: () => request<{ ok: boolean; count: number }>('/api/strategies/reload', { method: 'POST' }), // ===== Custom Signals (自定义信号) ===== customSignalsList: () => request<{ signals: CustomSignal[] }>('/api/custom-signals'), customSignalsOptions: () => request('/api/custom-signals/options'), customSignalSave: (signal: CustomSignal) => request<{ ok: boolean; signal: CustomSignal }>('/api/custom-signals', { method: 'POST', body: JSON.stringify(signal), }), customSignalDelete: (id: string) => request<{ ok: boolean }>(`/api/custom-signals/${encodeURIComponent(id)}`, { method: 'DELETE' }), // ===== Monitor Rules (监控规则) ===== monitorRulesList: () => request<{ rules: MonitorRule[] }>('/api/monitor-rules'), monitorRuleOptions: () => request('/api/monitor-rules/options'), monitorRuleSave: (rule: MonitorRule) => request<{ ok: boolean; rule: MonitorRule }>('/api/monitor-rules', { method: 'POST', body: JSON.stringify(rule), }), monitorRuleDelete: (id: string) => request<{ ok: boolean }>(`/api/monitor-rules/${encodeURIComponent(id)}`, { method: 'DELETE' }), /** 生成演示监控规则 (Dev 页用) */ monitorRuleSeed: () => request<{ ok: boolean; generated: number }>('/api/monitor-rules/seed', { method: 'POST' }), // ===== Alerts (触发记录) ===== alertsList: (params?: { days?: number; limit?: number; source?: string; type?: string }) => { const qs = new URLSearchParams() if (params?.days) qs.set('days', String(params.days)) if (params?.limit) qs.set('limit', String(params.limit)) if (params?.source) qs.set('source', params.source) if (params?.type) qs.set('type', params.type) const s = qs.toString() return request<{ alerts: AlertEvent[]; total: number }>(`/api/alerts${s ? `?${s}` : ''}`) }, alertsClear: () => request<{ ok: boolean; cleared: number }>('/api/alerts', { method: 'DELETE' }), alertDelete: (ts: number) => request<{ ok: boolean }>(`/api/alerts/${ts}`, { method: 'DELETE' }), /** 生成演示触发记录 (Dev 页用) */ alertSeed: (count = 12, recent = true) => request<{ ok: boolean; generated: number }>(`/api/alerts/seed?count=${count}&recent=${recent}`, { method: 'POST' }), /** 检查 AI 配置状态 */ strategyAiStatus: () => request<{ configured: boolean; has_key: boolean; has_model: boolean }>('/api/strategies/ai/status'), /** 测试 AI 连通性 */ strategyAiTest: () => request<{ ok: boolean; error?: string; model?: string; usage?: { prompt: number; completion: number } }>( '/api/strategies/ai/test', { method: 'POST' }, ), /** 获取策略源文件内容 */ strategyGetSource: (id: string) => request<{ code: string; source: string }>(`/api/strategies/${id}/source`), strategyBuild: (step: number, payload: Record) => request<{ code: string; meta: Record; valid: boolean; error: string | null }>( '/api/strategies/build', { method: 'POST', body: JSON.stringify({ step, ...payload }) }, ), /** 保存 AI 生成的策略文件 */ strategySaveCode: (strategyId: string, code: string) => request<{ ok: boolean; path: string }>('/api/strategies/ai/save', { method: 'POST', body: JSON.stringify({ strategy_id: strategyId, code }), }), } // ===== Pipeline ===== export interface PipelineJob { id: string status: 'pending' | 'running' | 'succeeded' | 'failed' stage: string progress: number // 0-100 整体进度 stage_pct: number // 0-100 当前阶段内进度 log: { ts: string; stage: string; msg: string }[] started_at: string | null finished_at: string | null duration_s: number | null result: { universe_size: number daily_days: number adj_factor_symbols: number enriched_days: number index_count?: number index_daily_rows?: number minute_rows: number skipped_stages?: string[] } | null error: string | null } export type PipelineJobSummary = Omit // ===== Data status ===== interface TableStats { rows: number earliest_date: string | null latest_date: string | null symbols_covered: number trading_days: number } interface InstrumentsStats { rows: number symbols_covered: number latest_as_of: string | null named: number } export interface DataStatus { daily: TableStats | null enriched: TableStats | null index_daily: TableStats | null index_enriched: TableStats | null index_instruments: InstrumentsStats | null minute: TableStats | null adj_factor: TableStats | null instruments: InstrumentsStats | null financials: { rows: number; tables: Record } | null storage: { daily_files: number daily_size_mb: number enriched_files: number enriched_size_mb: number index_daily_files?: number index_daily_size_mb?: number index_enriched_files?: number index_enriched_size_mb?: number index_instruments_files?: number index_instruments_size_mb?: number minute_files: number minute_size_mb: number adj_factor_files: number adj_factor_size_mb: number instruments_files: number instruments_size_mb: number financials_files?: number financials_size_mb?: number ext_data_files?: number ext_data_size_mb?: number total_size_mb: number } next_pipeline_run: string | null next_instruments_run: string | null last_pipeline_run: string | null last_instruments_run: string | null checked_at: string } export interface EnrichedField { name: string type: string desc: string } // ===== 扩展数据 ===== export interface ExtDataField { name: string dtype: string label: string } export interface PullConfig { url: string method: string headers?: Record body?: string | null response_path: string field_map?: Record schedule_minutes: number enabled: boolean last_run?: string | null last_status?: string | null last_message?: string | null last_rows?: number | null } export interface ExtDataConfig { id: string label: string mode: 'snapshot' | 'timeseries' fields: ExtDataField[] description?: string symbol_map?: Record code_map?: Record created_at: string updated_at: string latest_sync_date?: string | null date_range?: string[] | null pull?: PullConfig | null } export interface ExtDataRowsResult { id: string label: string mode: 'snapshot' | 'timeseries' date: string | null total: number limit: number fields: ExtDataField[] rows: Record[] } export interface AnalysisColumn { field: string label?: string type?: 'string' | 'number' | 'percent' | 'amount' | 'date' width?: number | null sortable?: boolean precision?: number | null format?: string | null aggregate?: 'count' | 'avg' | 'sum' | 'min' | 'max' | null visible?: boolean } export interface AnalysisMenu { id: string label: string icon: string data_source: string template: 'dimension_rank' | 'ranking' | 'table' dimension_field?: string | null rank_field?: string | null group_columns: AnalysisColumn[] detail_columns: AnalysisColumn[] default_sort?: { field: string; order: 'asc' | 'desc' } | null visible: boolean order: number created_at?: string | null updated_at?: string | null builtin?: boolean }