mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
1274 lines
39 KiB
TypeScript
1274 lines
39 KiB
TypeScript
// 后端 API 客户端 — 全项目统一入口
|
|||
|
|
//
|
||
|
|
// Dev:Vite 代理 /api 到 :3018
|
||
|
|
// Prod:同源(FastAPI 托管前端 dist)
|
||
|
|
|
||
|
|
import { toast } from '@/components/Toast'
|
||
|
|
|
||
|
|
const BASE = ''
|
||
|
|
|
||
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||
|
|
const isFormData = init?.body instanceof FormData
|
||
|
|
const headers: Record<string, string> = {}
|
||
|
|
if (!isFormData) headers['Content-Type'] = 'application/json'
|
||
|
|
const res = await fetch(`${BASE}${path}`, { ...init, headers })
|
||
|
|
if (!res.ok) {
|
||
|
|
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<T>
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Capabilities =====
|
||
|
|
export interface CapabilityLimits {
|
||
|
|
rpm: number | null
|
||
|
|
batch: number | null
|
||
|
|
subscribe: number | null
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface CapabilitiesResponse {
|
||
|
|
label: string
|
||
|
|
capabilities: Record<string, CapabilityLimits>
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Financials =====
|
||
|
|
export interface FinancialStatus {
|
||
|
|
available: boolean
|
||
|
|
tables: Record<string, { rows: number; symbols: number }>
|
||
|
|
last_sync: Record<string, string>
|
||
|
|
}
|
||
|
|
|
||
|
|
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 }[] }
|
||
|
|
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<string, any>
|
||
|
|
params: StrategyParamDef[]
|
||
|
|
params_defaults: Record<string, any>
|
||
|
|
scoring: Record<string, number>
|
||
|
|
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 }[]
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Limit Ladder =====
|
||
|
|
export interface LimitLadderStock {
|
||
|
|
symbol: string
|
||
|
|
name?: string | null
|
||
|
|
change_pct?: number | null
|
||
|
|
consecutive_limit_ups?: number | null
|
||
|
|
status?: 'limit_up' | 'broken' | 'failed' | null
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface LimitLadderTier {
|
||
|
|
boards: number
|
||
|
|
count: number
|
||
|
|
stocks: LimitLadderStock[]
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface LimitLadderResult {
|
||
|
|
as_of: string
|
||
|
|
tiers: LimitLadderTier[]
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Backtest =====
|
||
|
|
export interface BacktestResult {
|
||
|
|
run_id: string
|
||
|
|
config: any
|
||
|
|
stats: Record<string, any>
|
||
|
|
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<string, any>
|
||
|
|
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<string, any>[]
|
||
|
|
long_short_stats: Record<string, any>
|
||
|
|
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<string, any>
|
||
|
|
stats: Record<string, any>
|
||
|
|
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 =====
|
||
|
|
|
||
|
|
/** 端点发现清单 —— 对应 tickflow.org/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: '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[]
|
||
|
|
// 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
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
sse_refresh_pages: Record<string, boolean>
|
||
|
|
strategy_monitor_enabled: boolean
|
||
|
|
strategy_monitor_ids: string[]
|
||
|
|
sidebar_index_symbols: string[]
|
||
|
|
nav_order: string[]
|
||
|
|
nav_hidden: string[]
|
||
|
|
screener_auto_run: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
// ===== Strategy Alert =====
|
||
|
|
export interface StrategyAlertEvent {
|
||
|
|
source: 'strategy'
|
||
|
|
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'),
|
||
|
|
|
||
|
|
settings: () => request<SettingsState>('/api/settings'),
|
||
|
|
saveTickflowKey: (api_key: string) =>
|
||
|
|
request<any>('/api/settings/tickflow-key', {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify({ api_key }),
|
||
|
|
}),
|
||
|
|
clearTickflowKey: () =>
|
||
|
|
request<any>('/api/settings/tickflow-key', { method: 'DELETE' }),
|
||
|
|
|
||
|
|
/** 保存 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<Preferences>('/api/settings/preferences'),
|
||
|
|
updateMinuteSync: (enabled: boolean, days: number) =>
|
||
|
|
request<Preferences>('/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<string, boolean>
|
||
|
|
strategy_monitor_enabled?: boolean
|
||
|
|
strategy_monitor_ids?: string[]
|
||
|
|
sidebar_index_symbols?: string[]
|
||
|
|
screener_auto_run?: boolean
|
||
|
|
}) =>
|
||
|
|
request<{
|
||
|
|
sse_refresh_pages: Record<string, boolean>
|
||
|
|
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),
|
||
|
|
}),
|
||
|
|
updatePipelineSchedule: (hour: number, minute: number) =>
|
||
|
|
request<{ hour: number; minute: number }>('/api/settings/preferences/pipeline-schedule', {
|
||
|
|
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<CapabilitiesResponse>('/api/capabilities'),
|
||
|
|
version: () => request<{ version: string }>('/api/data/version'),
|
||
|
|
redetectCapabilities: () =>
|
||
|
|
request<CapabilitiesResponse>('/api/capabilities/redetect', { method: 'POST' }),
|
||
|
|
|
||
|
|
klineDaily: (symbol: string, days = 120, dateRange?: { start: string; end: string }) =>
|
||
|
|
request<{
|
||
|
|
symbol: string
|
||
|
|
name?: string
|
||
|
|
stock_info?: { name?: string; total_shares?: number; float_shares?: number }
|
||
|
|
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}`,
|
||
|
|
),
|
||
|
|
klineDailyBatch: (symbols: string[], days = 12) =>
|
||
|
|
request<{ data: Record<string, KlineRow[]> }>('/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}`,
|
||
|
|
),
|
||
|
|
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<ScreenerResult>('/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<ScreenerResult>('/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<string, { total: number; as_of: string; rows: any[] }> }>(
|
||
|
|
'/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<string, { total: number; as_of: string; rows: any[] }>; today_ever_matched: Record<string, string[]> | null; today_ever_rows: Record<string, Record<string, any>> | 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<OverviewMarket>(`/api/overview/market${asOf ? `?as_of=${asOf}` : ''}`),
|
||
|
|
|
||
|
|
limitLadder: (asOf?: string, extColumns?: string) => {
|
||
|
|
const params = new URLSearchParams()
|
||
|
|
if (asOf) params.set('as_of', asOf)
|
||
|
|
if (extColumns) params.set('ext_columns', extColumns)
|
||
|
|
const qs = params.toString()
|
||
|
|
return request<LimitLadderResult>(
|
||
|
|
`/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<BacktestResult>('/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<FactorBacktestResult>('/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<string, any> | null
|
||
|
|
overrides?: Record<string, any> | null
|
||
|
|
matching?: 'close_t' | 'open_t+1'
|
||
|
|
fees_pct?: number
|
||
|
|
slippage_bps?: number
|
||
|
|
max_positions?: number
|
||
|
|
initial_capital?: number
|
||
|
|
position_sizing?: 'equal' | 'score_weight'
|
||
|
|
}) =>
|
||
|
|
request<StrategyBacktestResult>('/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<PipelineJob>(`/api/pipeline/jobs/${id}`),
|
||
|
|
pipelineJobs: (limit = 20) =>
|
||
|
|
request<{ active_id: string | null; jobs: PipelineJobSummary[] }>(
|
||
|
|
`/api/pipeline/jobs?limit=${limit}`,
|
||
|
|
),
|
||
|
|
|
||
|
|
dataStatus: () => request<DataStatus>('/api/data/status'),
|
||
|
|
dataClear: () => request<{ deleted_files: number }>('/api/data/clear', { method: 'POST' }),
|
||
|
|
enrichedSchema: (table: string) => request<EnrichedField[]>(`/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 }),
|
||
|
|
},
|
||
|
|
),
|
||
|
|
|
||
|
|
// 端点发现 —— 后端代理拉取 tickflow.org/endpoints.json(前端无法跨域直连)
|
||
|
|
listEndpoints: () =>
|
||
|
|
request<EndpointManifest>('/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<ExtDataRowsResult>(`/api/ext-data/${encodeURIComponent(id)}/rows${suffix ? `?${suffix}` : ''}`)
|
||
|
|
},
|
||
|
|
|
||
|
|
analysisMenus: () =>
|
||
|
|
request<{ items: AnalysisMenu[] }>('/api/analysis-menus'),
|
||
|
|
|
||
|
|
analysisMenu: (id: string) =>
|
||
|
|
request<AnalysisMenu>(`/api/analysis-menus/${encodeURIComponent(id)}`),
|
||
|
|
|
||
|
|
analysisMenuSave: (id: string, body: Omit<AnalysisMenu, 'id' | 'created_at' | 'updated_at' | 'builtin'>) =>
|
||
|
|
request<AnalysisMenu>(`/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<string, string>; code_map?: Record<string, string> }) =>
|
||
|
|
request<ExtDataConfig>('/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<ExtDataConfig>(`/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<string, unknown>[] }) =>
|
||
|
|
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<string, string>; body?: string;
|
||
|
|
response_path?: string; field_map?: Record<string, string>;
|
||
|
|
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<string, unknown>[]; 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<FinancialStatus>('/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<string, number> }>(
|
||
|
|
`/api/financials/sync/${table}`, { method: 'POST' },
|
||
|
|
),
|
||
|
|
|
||
|
|
// ===== Strategy Engine =====
|
||
|
|
strategyList: () =>
|
||
|
|
request<{ strategies: StrategyDetail[] }>('/api/strategies'),
|
||
|
|
|
||
|
|
strategyGet: (id: string) =>
|
||
|
|
request<StrategyDetail>(`/api/strategies/${id}`),
|
||
|
|
|
||
|
|
strategyRun: (strategyId: string, params?: Record<string, any>, asOf?: string, pool?: string[]) =>
|
||
|
|
request<ScreenerResult>('/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<string, { total: number; as_of: string }> }>(
|
||
|
|
'/api/strategies/run-all',
|
||
|
|
{ method: 'POST', body: JSON.stringify({ as_of: asOf ?? null }) },
|
||
|
|
),
|
||
|
|
|
||
|
|
strategySaveConfig: (strategyId: string, overrides: Record<string, any>) =>
|
||
|
|
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<CustomSignalOptions>('/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' }),
|
||
|
|
|
||
|
|
/** 检查 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<string, any>) =>
|
||
|
|
request<{ code: string; meta: Record<string, any>; 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<PipelineJob, 'log'>
|
||
|
|
|
||
|
|
// ===== 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<string, { rows: number; symbols: number }> } | 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<string, string>
|
||
|
|
body?: string | null
|
||
|
|
response_path: string
|
||
|
|
field_map?: Record<string, string>
|
||
|
|
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<string, string>
|
||
|
|
code_map?: Record<string, string>
|
||
|
|
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<string, any>[]
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|