Files
tick-stock-panel/frontend/src/lib/api.ts
T
shy3130 4ee55e40c6 feat(regime): 新增市场状态识别系统并接入叠加策略回测过滤器
后端
- 新增 services/regime_builder.py:5 档日级市场状态分级(strong/lean_strong/range/lean_weak/weak)
- 新增 api/regime.py:5 个 regime 查询接口
- daily_pipeline 接入 regime 构建(step 2.6,soft-fail + 双检测自愈)
- 回测 strategy.py 新增 regime_filter 过滤:采用 T-1 防未来函数,仅作用于入场信号
  在 composite / matrix_native / prepared 三处注入 entry_time_mask
- backtest API 透传 regime_filter(REST + SSE);_make_job_key 纳入 regime_filter 以隔离缓存
- 兼容 _RepoStub 测试夹具(store 属性缺失场景)

前端
- 新增 Regime 页面、路由、导航入口、api 类型与 queryKeys
- 叠加策略回测页新增 regime 过滤器控件

测试
- 新增 tests/test_regime_builder.py(18 项),覆盖分级逻辑、T-1 防未来函数、空值降级、三处 mask 注入
- 全量后端测试 582 passed;前端 pnpm build 通过;git diff --check 无空白错误
2026-08-02 12:05:20 +08:00

2513 lines
82 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 后端 API 客户端 — 全项目统一入口
//
// Dev:Vite 代理 /api 到 :3018
// Prod:同源(FastAPI 托管前端 dist)
import { toast } from '@/components/Toast'
const BASE = ''
type RequestOptions = RequestInit & {
/** 为 true 时不弹错误 toast(由调用方自行汇总提示,如多图串行队列) */
quiet?: boolean
}
async function request<T>(path: string, init?: RequestOptions): Promise<T> {
const { quiet, ...fetchInit } = init ?? {}
const isFormData = fetchInit.body instanceof FormData
const headers: Record<string, string> = {}
if (!isFormData) headers['Content-Type'] = 'application/json'
// 合并调用方传入的 headers (此前会被整体覆盖丢弃)
Object.assign(headers, fetchInit.headers as Record<string, string> | undefined)
const res = await fetch(`${BASE}${path}`, { ...fetchInit, headers })
if (!res.ok) {
let detail = ''
try {
const j = JSON.parse(await res.text())
const raw = j.detail ?? j.message ?? ''
if (Array.isArray(raw)) {
// FastAPI 422 校验错误: [{type, loc, msg, input}, ...] → 取 msg 拼接
detail = raw.map((e: any) => e?.msg || String(e)).join('; ')
} else if (typeof raw === 'string') {
detail = raw
} else if (raw && typeof raw === 'object') {
detail = JSON.stringify(raw)
}
} catch { /* ignore */ }
const msg = detail || `${res.status} ${res.statusText}`
// 401 (未登录/会话过期) 不弹 toast — 由全局认证拦截器统一跳登录页, 避免刷屏
if (res.status !== 401 && !quiet) 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>
/** 服务端是否正在同步(手动触发)——驱动"同步中"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
}
export interface FinancialSharesRecord {
symbol?: string
period_end: string
announce_date?: string | null
total_shares?: number | null
float_shares?: number | null
[key: string]: any
}
/** AI 财务分析历史报告 */
export interface AiFinancialReport {
id: string
symbol: string
name: string
focus: string
content: string
periods?: number
summary?: string
created_at: string
}
// ===== 个股分析 =====
export type LevelType = 'sr' | 'pivot' | 'extreme' | 'boll' | 'keltner_s' | 'keltner_m' | 'keltner_l' | 'atr_stop' | 'gap' | 'fib' | 'round'
export interface PriceLevel {
value: number
label: string
type: LevelType
side: 'resistance' | 'support' | 'neutral'
strength?: 'strong' | 'medium' | 'weak'
/** 档位(仅 pivot 有):0=P, 1=R1/S1, 2=R2/S2, 3=R3/S3。前端按"显示到第几档"过滤。 */
rank?: number
}
/** 带状曲线指标(布林带/Keltner/ATR)的每日时间序列,与 dates 对齐。 */
export interface LevelSeries {
boll?: { upper: (number | null)[]; lower: (number | null)[]; mid?: (number | null)[] }
keltner_s?: { upper: (number | null)[]; lower: (number | null)[] }
keltner_m?: { upper: (number | null)[]; lower: (number | null)[] }
keltner_l?: { upper: (number | null)[]; lower: (number | null)[] }
atr?: { stop_loss: (number | null)[]; take_profit: (number | null)[] }
}
export interface StockLevels {
levels: Record<LevelType, PriceLevel[]>
close: number | null
summary: string
symbol: string
/** dates 与 series 对齐;前端按自身 rows 的日期映射,缺失填 null */
dates?: string[]
series?: LevelSeries
}
export interface AiStockReport {
id: string
symbol: string
name: string
focus: string
content: string
summary?: string
close?: number | null
levels?: Record<LevelType, PriceLevel[]>
created_at: string
}
// ===== Kline =====
export interface MinuteKlineRow {
datetime: string
open: number
high: number
low: number
close: number
volume: number
amount: number
}
export interface PriceLimitInfo {
rate: number
limit_up: number | null
limit_down: number | null
source: 'rule' | 'instrument'
}
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
name?: string | null
}
export interface WatchlistImportCandidate {
code: string
symbol: string | null
name: string | null
matched: boolean
already_in_watchlist: boolean
}
export interface WatchlistImportResult {
provider: string
codes: string[]
candidates: WatchlistImportCandidate[]
matched_count: number
unmatched_count: number
}
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 StrategyLoadError {
file: string
error: string
}
export interface ScreenerResult {
as_of: string
strategy: string | null
rows: any[]
total: number
elapsed_ms: number
}
export interface ScreenerResultSummary {
total: number
as_of: string
}
export interface ScreenerCachedSummary {
as_of: string | null
results: Record<string, ScreenerResultSummary>
today_ever_counts: Record<string, number>
updated_at: number | null
}
export interface ScreenerCachedResult {
result: ScreenerResult | null
today_ever_rows: Record<string, any> | null
strategy_ids_by_symbol: Record<string, string[]>
updated_at: number | null
}
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; stocks?: { symbol: string; name?: string; amount?: 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[] }
}
// ===== 概念涨幅轮动矩阵 =====
// dates: 日期字符串列表(最新在最前); columns: {日期: [[概念名, 涨幅小数], ...]} 每列各自降序
export interface RpsRotationData {
dates: string[]
columns: Record<string, [string, number][]>
concept_count: number
}
// ===== 市场环境(Regime) =====
export type RegimeState = 'strong' | 'lean_strong' | 'range' | 'lean_weak' | 'weak'
export const REGIME_STATE_LABELS: Record<RegimeState, string> = {
strong: '强势',
lean_strong: '偏强',
range: '震荡',
lean_weak: '偏弱',
weak: '弱势',
}
export const REGIME_STATE_COLORS: Record<RegimeState, string> = {
strong: '#ef4444', // 红(强)
lean_strong: '#f97316', // 橙
range: '#6b7280', // 灰
lean_weak: '#3b82f6', // 蓝
weak: '#10b981', // 绿(弱)
}
export interface RegimeRow {
date: string
state: RegimeState
score: number
limit_up: number
limit_down: number
broken_limit: number
max_consecutive: number
seal_rate: number
up_count: number
down_count: number
up_ratio: number
index_pct: number
above_ma20_pct: number
total_amount: number
avg_turnover: number
}
export interface RegimeHistory {
rows: RegimeRow[]
total: number
}
export interface RegimeStateItem {
state: RegimeState
label: string
count: number
pct: number
}
export interface RegimeStates {
distribution: RegimeStateItem[]
days: number
}
export interface RegimeCoverage {
rows: number
earliest_date: string | null
latest_date: string | null
}
// ===== 大盘复盘 =====
export interface AiReviewReport {
id: string
as_of: string
focus?: string
content: string
summary?: string
emotion_score?: number | null
emotion_label?: string
created_at: string
}
// ===== 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 CompositeChildInfo {
id: string
name: string
source: string
weight: number
}
export interface StrategyDetail {
id: string
name: string
description: string
tags: string[]
source: 'builtin' | 'custom' | 'ai' | 'composite'
execution_backend: 'polars_expr' | 'matrix_native' | 'python_history_legacy' | 'composite'
asset_types: string[]
timeframes: string[]
version: string
basic_filter: Record<string, any>
params: StrategyParamDef[]
params_defaults: Record<string, any>
scoring: Record<string, number>
entry_signals: string[]
exit_signals: string[]
minute_exit_trigger_supported_signals: string[]
stop_loss: number | null
take_profit: 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
// 叠加策略(composite)专属: 子策略列表与合并模式。非 composite 时为 null。
composite_children?: CompositeChildInfo[] | null
}
export interface StrategyBuildResult {
code: string
meta: Record<string, any>
valid: boolean
error: string | null
}
export type StrategyBuildStreamEvent =
| { type: 'meta'; strategy_id?: string; step?: number }
| { type: 'delta'; content: string }
| ({ type: 'result' } & StrategyBuildResult)
| { type: 'error'; message: string }
export interface StrategyCodeSaveResult {
ok: boolean
strategy_id: string
source: 'ai' | 'custom' | 'composite'
path: string
meta: Record<string, any>
}
// ===== Custom Signals (自定义信号) =====
export interface CustomSignalCondition {
left: string // 字段名
op: string // > >= < <= == !=
right: string // "field:xxx" 或数字字符串
leftDays?: number // 左字段取几日前 (0=当日, 默认)
rightDays?: number // 右字段取几日前 (仅 right 为字段时有意义)
}
export interface CustomSignal {
id: string
name: string
kind: 'entry' | 'exit' | 'both'
conditions: CustomSignalCondition[]
enabled: boolean
}
export interface CustomSignalFieldGroup {
key: string
label: string
fields: { key: string; label: string }[]
}
export interface CustomSignalOptions {
fields: { key: string; label: string }[]
groups?: CustomSignalFieldGroup[]
maxDays?: number
operators: string[]
kinds: { key: string; label: string }[]
}
// ===== Monitor (监控规则 + 触发记录) =====
export interface MonitorCondition {
field: string
op: string // truth | > >= < <= == !=
value?: number | null // op 非 truth 时必填
}
export type StrategyNotifyEvent = 'buy_signal' | 'sell_signal' | 'pool_entry' | 'pool_exit'
export interface MonitorRule {
id: string
name: string
enabled: boolean
type: 'strategy' | 'signal' | 'price' | 'market' | 'ladder'
asset_type?: 'stock' | 'etf' | 'index'
scope: 'symbols' | 'all' | 'sector'
symbols: string[]
sector?: string | null
strategy_id?: string | null
direction: 'entry' | 'exit' | 'both' | 'up' | 'down'
notify_events?: StrategyNotifyEvent[]
conditions: MonitorCondition[]
logic: 'and' | 'or'
cooldown_seconds: number
severity: 'info' | 'warn' | 'critical'
message: string
webhook_url?: string
webhook_enabled?: boolean // 兼容老规则, 已由 webhook_channels 取代
webhook_channels?: string[] // 命中时推送的外部渠道 (合法值 'feishu' | 'wecom')
created_at?: string
runtime_warning?: string
// ladder 专属: 封单监控
metric?: 'sealed_vol' | 'sealed_amount' // 量(手) / 额(元)
threshold?: number // 封单 <= 此值时报警
}
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 }[]
intraday_signal_support: {
available: boolean
source: string | null
max_symbols: number
reason: 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
conditions?: MonitorCondition[]
logic?: 'and' | 'or'
/** ext 富化字段 (行业/概念等), 键为 "{configId}__{fieldName}" */
[key: string]: unknown
}
/** 生成监控规则 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
/** 最终状态为涨跌停且当天开高低收四价相同 */
is_one_word?: boolean
}
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<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
entry_signal_id?: string | null
exit_signal_id?: string | null
}
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
take_profit: 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
execution_backend?: string
// 叠加策略回测: 子策略构成与权重归因
composite_children?: { id: string; weight: number }[]
}
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: '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_configured?: boolean
ai_model: string
ai_codex_command?: string
ai_codex_reasoning_effort?: string
ai_user_agent: string
}
/** 保存 TickFlow 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 DataSourceItem {
name: string
display_name: string
datasets: string[]
path?: string | null
}
/** 内置可选插件数据源 (plugins/ 目录, 需手动装依赖) */
export interface PluginDataSourceItem {
name: string
display_name: string
datasets: string[]
runtime: string // node | python | none
available: boolean // 依赖是否已安装
status: string // 可用性原因 (供 UI 显示)
description: string
install_hint: string // 未装依赖时显示的安装命令
}
export interface DataSourceLoadError {
name?: string
path: string
errors: string[]
}
export interface DataSourcesResponse {
builtin: DataSourceItem[]
plugins: PluginDataSourceItem[]
custom: DataSourceItem[]
errors: DataSourceLoadError[]
config_dir: string
}
export interface DataSourceTestResult {
provider: string
dataset: string
rows: number
columns: string[]
preview: Record<string, unknown>[]
}
export interface DatasetConfig {
url: string
method: string
batch?: number | null
rpm?: number | null
response_path: string
field_map: Record<string, string>
transforms?: Record<string, string>
symbols_param?: string
start_param?: string
end_param?: string
asset_type_param?: string | null
freq_param?: string | null
timeout?: number | null
}
export interface AuthConfig {
type: string
token_env?: string | null
header?: string
param?: string
}
export interface CustomSourceConfig {
name: string
display_name: string
auth: AuthConfig
datasets: Record<string, DatasetConfig>
}
export interface WecomBotStatus {
enabled: boolean
running: boolean
connected: boolean
bot_id_configured: boolean
secret_configured: boolean
last_error: string
}
export interface Preferences {
realtime_quotes_enabled: boolean
indices_nav_pinned: boolean
minute_sync_enabled: boolean
minute_sync_days: number
minute_sync_segment_days: number
daily_data_provider?: string
adj_factor_provider?: string
minute_data_provider?: string
realtime_data_provider?: string
financial_data_provider?: string
realtime_watchlist_symbols?: string[]
realtime_pull_stock?: boolean
realtime_pull_etf?: boolean
realtime_pull_index?: boolean
realtime_index_mode?: 'core' | 'all'
realtime_index_symbols?: string[]
pipeline_pull_a_share: boolean
pipeline_pull_etf: boolean
pipeline_pull_index: boolean
pipeline_index_symbols: string
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 }
review_schedule: { enabled: boolean; hour: number; minute: number }
review_push_channels: string[]
sse_refresh_pages: Record<string, boolean>
strategy_monitor_enabled: boolean
strategy_monitor_ids: string[]
system_notify_enabled: boolean
feishu_webhook_url?: string
feishu_webhook_secret?: string
wecom_webhook_url?: string
wecom_bot_id?: string
wecom_bot_secret?: string
wecom_bot_enabled?: boolean
webhook_enabled_default?: boolean
webhook_default_channels?: string[]
sidebar_index_symbols: string[]
nav_order: string[]
nav_hidden: string[]
screener_auto_run: boolean
minute_intraday_refresh: boolean
minute_intraday_refresh_interval: number
monitor_ext_fields: { concept: MonitorExtFieldItem | null; industry: MonitorExtFieldItem | null }
}
/** 监控中心 ext 字段单项配置 (行业/概念标签的来源 + 显示裁剪) */
export interface MonitorExtFieldItem {
/** "configId.fieldName" */
field: string
/** 显示前N个标签, 0=不限制 */
maxTags?: number
/** 隐藏的位置 (0-based), 如 [0] 表示隐藏第一个 */
hiddenIndices?: number[]
}
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[]
/** ext 富化字段 (行业/概念等), 键为 "{configId}__{fieldName}" */
[key: string]: unknown
}
// ===== API surface =====
export const api = {
health: () => request<{ status: string; version: string; mode: string }>('/health'),
// ===== Auth (访问认证) =====
authStatus: () =>
request<{ configured: boolean; authenticated: boolean }>('/api/auth/status'),
authSetup: (password: string) =>
request<{ ok: boolean }>('/api/auth/setup', {
method: 'POST',
body: JSON.stringify({ password }),
}),
authLogin: (password: string) =>
request<{ ok: boolean }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ password }),
}),
authLogout: () =>
request<{ ok: boolean }>('/api/auth/logout', { method: 'POST' }),
authChangePassword: (oldPassword: string, newPassword: string) =>
request<{ ok: boolean }>('/api/auth/change-password', {
method: 'POST',
body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
}),
settings: () => request<SettingsState>('/api/settings'),
saveTickflowKey: (api_key: string) =>
request<SaveTickflowKeyResult>('/api/settings/tickflow-key', {
method: 'POST',
body: JSON.stringify({ api_key }),
}),
clearTickflowKey: () =>
request<any>('/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; codex_command?: string; codex_reasoning_effort?: string; user_agent?: string }) =>
request<{ ok: boolean; ai_provider?: string; ai_model?: string; ai_codex_command?: string; ai_codex_reasoning_effort?: string; ai_configured?: boolean }>('/api/settings/ai', {
method: 'POST',
body: JSON.stringify(ai),
}),
/** 一键清空 AI 配置(保留自定义 UA) */
clearAiSettings: () =>
request<{ ok: boolean }>('/api/settings/ai', { method: 'DELETE' }),
preferences: () => request<Preferences>('/api/settings/preferences'),
dataSources: () => request<DataSourcesResponse>('/api/settings/data-sources'),
dataSource: (name: string) => request<CustomSourceConfig>(`/api/settings/data-sources/${encodeURIComponent(name)}`),
saveDataSource: (config: CustomSourceConfig) =>
request<DataSourcesResponse>('/api/settings/data-sources', {
method: 'POST',
body: JSON.stringify(config),
}),
deleteDataSource: (name: string) =>
request<DataSourcesResponse>(`/api/settings/data-sources/${encodeURIComponent(name)}`, { method: 'DELETE' }),
reloadDataSources: () => request<DataSourcesResponse>('/api/settings/data-sources/reload', { method: 'POST' }),
installPlugin: (name: string) => {
// npm install 可能耗时较长, 用 6 分钟超时
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 360_000)
return request<DataSourcesResponse & { install_ok: boolean; install_message: string }>(
`/api/settings/plugins/${encodeURIComponent(name)}/install`,
{ method: 'POST', signal: controller.signal },
).finally(() => clearTimeout(timer))
},
uninstallPlugin: (name: string) =>
request<DataSourcesResponse & { uninstall_ok: boolean; uninstall_message: string }>(
`/api/settings/plugins/${encodeURIComponent(name)}/install`,
{ method: 'DELETE' },
),
testDataSource: (
provider: string,
dataset: string,
symbols?: string[],
config?: CustomSourceConfig,
) =>
request<DataSourceTestResult>('/api/settings/data-sources/test', {
method: 'POST',
body: JSON.stringify({ provider, dataset, symbols, config }),
}),
updateDataProviders: (cfg: Partial<Pick<Preferences, 'daily_data_provider' | 'adj_factor_provider' | 'minute_data_provider' | 'realtime_data_provider' | 'financial_data_provider'>>) =>
request<Pick<Preferences, 'daily_data_provider' | 'adj_factor_provider' | 'minute_data_provider' | 'realtime_data_provider'>>(
'/api/settings/preferences/data-providers',
{ method: 'PUT', body: JSON.stringify(cfg) },
),
updateMinuteSync: (enabled: boolean, days: number, segmentDays?: number) =>
request<Preferences>('/api/settings/preferences/minute-sync', {
method: 'PUT',
body: JSON.stringify({
minute_sync_enabled: enabled,
minute_sync_days: days,
...(segmentDays != null ? { minute_sync_segment_days: segmentDays } : {}),
}),
}),
updatePipelinePullTypes: (cfg: Partial<Pick<Preferences, 'pipeline_pull_a_share' | 'pipeline_pull_etf' | 'pipeline_pull_index'>>) =>
request<{
pipeline_pull_a_share: boolean
pipeline_pull_etf: boolean
pipeline_pull_index: boolean
}>('/api/settings/preferences/pipeline-pull-types', {
method: 'PUT',
body: JSON.stringify(cfg),
}),
updatePipelineIndexSymbols: (symbols: string) =>
request<{ pipeline_index_symbols: string }>('/api/settings/preferences/pipeline-index-symbols', {
method: 'PUT',
body: JSON.stringify({ symbols }),
}),
updateRealtimeQuotes: (enabled: boolean) =>
request<{ realtime_quotes_enabled: boolean; realtime_allowed?: boolean; mode?: string; error?: string }>('/api/settings/preferences/realtime-quotes', {
method: 'PUT',
body: JSON.stringify({ realtime_quotes_enabled: enabled }),
}),
updateRealtimeQuoteScope: (cfg: Partial<Pick<Preferences, 'realtime_pull_stock' | 'realtime_pull_etf' | 'realtime_pull_index' | 'realtime_index_mode' | 'realtime_index_symbols'>>) =>
request<Partial<Preferences>>('/api/settings/preferences/realtime-quote-scope', {
method: 'PUT',
body: JSON.stringify(cfg),
}),
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
paused?: boolean
mode?: 'none' | 'watchlist' | 'full_market'
realtime_allowed?: boolean
interval_s: number
symbol_count: number
watchlist_symbol_count?: number
index_symbol_count?: number
etf_symbol_count?: number
quote_age_ms: number | null
is_trading_hours: boolean
is_polling_window?: boolean
market_phase?: string
final_sync_done?: boolean
final_sync_failed?: string | null
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
minute_intraday_refresh?: boolean
minute_intraday_refresh_interval?: number
monitor_ext_fields?: { concept: MonitorExtFieldItem | null; industry: MonitorExtFieldItem | null }
}) =>
request<{
sse_refresh_pages: Record<string, boolean>
strategy_monitor_enabled: boolean
strategy_monitor_ids: string[]
sidebar_index_symbols: string[]
screener_auto_run: boolean
minute_intraday_refresh: boolean
minute_intraday_refresh_interval: number
monitor_ext_fields: { concept: MonitorExtFieldItem | null; industry: MonitorExtFieldItem | null }
}>('/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 }),
}),
updateFeishuWebhook: (url: string, secret: string = '') =>
request<{ feishu_webhook_url: string; feishu_webhook_secret: string }>('/api/settings/preferences/feishu-webhook', {
method: 'PUT',
body: JSON.stringify({ url, secret }),
}),
updateWecomWebhook: (url: string) =>
request<{ wecom_webhook_url: string }>('/api/settings/preferences/wecom-webhook', {
method: 'PUT',
body: JSON.stringify({ url }),
}),
updateWecomBot: (botId: string, secret: string, enabled: boolean = true) =>
request<{
wecom_bot_id: string
wecom_bot_secret: string
wecom_bot_enabled: boolean
wecom_bot_status: WecomBotStatus
}>('/api/settings/preferences/wecom-bot', {
method: 'PUT',
body: JSON.stringify({ bot_id: botId, secret, enabled }),
}),
toggleWecomBot: (enabled: boolean) =>
request<{ wecom_bot_enabled: boolean; wecom_bot_status: WecomBotStatus }>('/api/settings/preferences/wecom-bot-toggle', {
method: 'PUT',
body: JSON.stringify({ enabled }),
}),
updateWebhookDefault: (enabled: boolean) =>
request<{ webhook_enabled_default: boolean }>('/api/settings/preferences/webhook-enabled-default', {
method: 'PUT',
body: JSON.stringify({ enabled }),
}),
updateWebhookDefaultChannels: (channels: string[]) =>
request<{ webhook_default_channels: string[] }>('/api/settings/preferences/webhook-default-channels', {
method: 'PUT',
body: JSON.stringify({ channels }),
}),
updatePipelineSchedule: (hour: number, minute: number) =>
request<{ hour: number; minute: number }>('/api/settings/preferences/pipeline-schedule', {
method: 'PUT',
body: JSON.stringify({ hour, minute }),
}),
updateReviewSchedule: (enabled: boolean, hour: number, minute: number) =>
request<{ enabled: boolean; hour: number; minute: number }>('/api/settings/preferences/review-schedule', {
method: 'PUT',
body: JSON.stringify({ enabled, hour, minute }),
}),
updateReviewPush: (channels: string[]) =>
request<{ review_push_channels: string[] }>('/api/settings/preferences/review-push', {
method: 'PUT',
body: JSON.stringify({ channels }),
}),
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<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 }, extColumns?: string) =>
request<{
symbol: string
name?: string
stock_info?: { name?: string; total_shares?: number; float_shares?: number; ext?: Record<string, unknown> }
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<string, KlineRow[]> }>('/api/kline/daily-batch', {
method: 'POST',
body: JSON.stringify({ symbols, days }),
}),
klineMinuteBatch: (symbols: string[], date?: string) =>
request<{ data: Record<string, MinuteKlineRow[]> }>('/api/kline/minute-batch', {
method: 'POST',
body: JSON.stringify({ symbols, date }),
}),
instrumentSearch: (q: string, limit = 20, assetTypes?: string) =>
request<{ results: { symbol: string; name: string; code: string; asset_type?: string }[] }>(
`/api/kline/instruments/search?q=${encodeURIComponent(q)}&limit=${limit}${assetTypes ? `&asset_types=${encodeURIComponent(assetTypes)}` : ''}`,
),
/** 批量查股票名称 (传入 symbol 列表, 返回 {symbol: name}) */
instrumentNames: (symbols: string[]) =>
request<{ names: Record<string, string> }>('/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'
asset_type?: 'stock' | 'etf' | 'index'
price_limit?: PriceLimitInfo | null
}>(
`/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: (days?: number, extend?: boolean) =>
request<{ status: string; job_id: string }>('/api/kline/sync_minute', {
method: 'POST',
body: JSON.stringify({ ...(days ? { days } : {}), ...(extend ? { extend: true } : {}) }),
}),
syncMinuteSingle: (symbol: string) =>
request<{ status: string; symbol: string; rows: number }>('/api/kline/sync_minute_single', {
method: 'POST',
body: JSON.stringify({ symbol }),
}),
clearMinute: () =>
request<{ status: string; removed: number }>('/api/kline/clear_minute', {
method: 'POST',
body: JSON.stringify({ confirm: true }),
}),
extendHistory: (value: number, unit: 'day' | 'month' | 'year') =>
request<{ status: string; job_id: string }>('/api/kline/extend_history', {
method: 'POST',
body: JSON.stringify({ value, unit }),
}),
repairDaily: (startDate: string) =>
request<{ status: string; job_id: string }>('/api/kline/repair_daily', {
method: 'POST',
body: JSON.stringify({ start_date: startDate }),
}),
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 }),
}),
watchlistOcrStatus: () =>
request<{ provider: string; available: boolean }>('/api/watchlist/ocr-status'),
watchlistImportImage: (file: File, signal?: AbortSignal, quiet = false) => {
const fd = new FormData()
fd.append('file', file)
return request<WatchlistImportResult>('/api/watchlist/import-image', {
method: 'POST',
body: fd,
signal,
quiet,
})
},
watchlistRemove: (symbol: string) =>
request<{ symbols: WatchlistEntry[] }>(
`/api/watchlist/${encodeURIComponent(symbol)}`,
{ method: 'DELETE' },
),
watchlistMoveToTop: (symbol: string) =>
request<{ symbols: WatchlistEntry[] }>(
`/api/watchlist/${encodeURIComponent(symbol)}/top`,
{ method: 'POST' },
),
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: async (assetType?: 'stock' | 'etf' | 'index') => {
const data = await request<{ strategies: StrategyDetail[]; load_errors?: StrategyLoadError[] }>(
`/api/strategies?${assetType ? `asset_type=${assetType}&` : ''}timeframe=1d`,
)
return { presets: data.strategies, load_errors: data.load_errors }
},
screenerRunPreset: (strategy_id: string, pool?: string[], asOf?: string, extColumns?: string, assetType: 'stock' | 'etf' = 'stock') =>
request<ScreenerResult>('/api/screener/run_preset', {
method: 'POST',
body: JSON.stringify({ strategy_id, pool, as_of: asOf ?? null, ext_columns: extColumns || null, asset_type: assetType }),
}),
screenerRunCustom: (conditions: string[], orderBy?: string, limit = 30, pool?: string[], extColumns?: string, assetType: 'stock' | 'etf' = 'stock') =>
request<ScreenerResult>('/api/screener/run', {
method: 'POST',
body: JSON.stringify({ conditions, order_by: orderBy, limit, pool, ext_columns: extColumns || null, asset_type: assetType }),
}),
screenerRunAll: (asOf?: string, strategyIds?: string[], assetType: 'stock' | 'etf' = 'stock') =>
request<{ as_of: string | null; results: Record<string, ScreenerResultSummary> }>(
'/api/screener/run_all', { method: 'POST', body: JSON.stringify({ as_of: asOf ?? null, strategy_ids: strategyIds ?? null, asset_type: assetType, timeframe: '1d', summary_only: true }) },
),
screenerCachedSummary: () =>
request<ScreenerCachedSummary>('/api/screener/cached-summary'),
screenerCachedResult: (strategyId: string, extColumns?: string) =>
request<ScreenerCachedResult>(
extColumns
? `/api/screener/cached-result/${encodeURIComponent(strategyId)}?ext_columns=${encodeURIComponent(extColumns)}`
: `/api/screener/cached-result/${encodeURIComponent(strategyId)}`,
),
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}` : ''}`),
// 概念涨幅轮动矩阵: 每列(日期)各自把所有概念按当天涨幅从高到低排序
rpsRotation: (days: number) =>
request<RpsRotationData>(`/api/rps/rotation?days=${days}`),
// 市场环境(Regime)
regimeHistory: (start?: string, end?: string, limit?: number) => {
const params = new URLSearchParams()
if (start) params.set('start', start)
if (end) params.set('end', end)
if (limit) params.set('limit', String(limit))
const qs = params.toString()
return request<RegimeHistory>(`/api/regime/history${qs ? `?${qs}` : ''}`)
},
regimeLatest: () => request<{ row: RegimeRow | null }>('/api/regime/latest'),
regimeStates: (days = 60) => request<RegimeStates>(`/api/regime/states?days=${days}`),
regimeCoverage: () => request<RegimeCoverage>('/api/regime/coverage'),
regimeRecompute: (start?: string, end?: string) => {
const params = new URLSearchParams()
if (start) params.set('start', start)
if (end) params.set('end', end)
const qs = params.toString()
return request<{ ok: boolean; computed: number }>(`/api/regime/recompute${qs ? `?${qs}` : ''}`, { method: 'POST' })
},
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<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'
asset_type?: 'stock' | 'etf' | 'index'
}) =>
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
asset_type?: 'stock' | 'etf' | 'index'
}) =>
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'
entry_fill?: 'close_t' | 'open_t+1' | null
exit_fill?: 'close_t' | 'open_t+1' | 'signal_next_minute' | null
fees_pct?: number
commission_pct?: number
stamp_tax_pct?: number
slippage_bps?: number
max_positions?: number
initial_capital?: number
position_sizing?: 'equal' | 'score_weight'
asset_type?: 'stock' | 'etf' | 'index'
minute_fill?: boolean
}) =>
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' }),
refreshCache: () => request<{ ok: boolean }>('/api/data/refresh-cache', { 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}` : ''}`)
},
dimensionMembers: (id: string, opts: { field: string; value: string; date?: string; limit?: number }) => {
const qs = new URLSearchParams({ field: opts.field, value: opts.value })
if (opts.date) qs.set('date', opts.date)
if (opts.limit) qs.set('limit', String(opts.limit))
return request<DimensionMembersResult>(`/api/ext-data/${encodeURIComponent(id)}/dimension-members?${qs.toString()}`)
},
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' },
),
// 内置预设 (概念/行业) 手动获取数据: 走结构转换, 保证 schema 一致
extDataPresetFetch: (id: string) =>
request<{ status: string; rows: number }>(
`/api/ext-data/presets/${id}/fetch`,
{ 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 },
)
},
extDataDetectUrl: (body: ExtDataDetectUrlRequest) =>
request<ExtDataDetectUrlResult>('/api/ext-data/detect-url', {
method: 'POST',
body: JSON.stringify(body),
}),
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)}` : ''}`,
),
financialShares: (symbol?: string) =>
request<{ data: FinancialSharesRecord[] }>(
`/api/financials/shares${symbol ? `?symbol=${encodeURIComponent(symbol)}` : ''}`,
),
/** 触发财务数据同步(后台异步执行,接口立即返回 started 状态) */
financialSync: (table: string) =>
request<{ status: string; synced: { started: boolean; reason?: string } }>(
`/api/financials/sync/${table}`, { method: 'POST' },
),
/** AI 分析报告 CRUD */
financialReportsList: () =>
request<{ reports: AiFinancialReport[] }>('/api/financials/reports'),
financialReportSave: (r: {
symbol: string; name?: string; focus?: string; content: string
periods?: number; summary?: string
}) =>
request<{ ok: boolean; report: AiFinancialReport }>('/api/financials/reports', {
method: 'POST', body: JSON.stringify(r),
}),
financialReportDelete: (reportId: string) =>
request<{ ok: boolean }>(`/api/financials/reports/${encodeURIComponent(reportId)}`, { method: 'DELETE' }),
/**
* AI 财务分析 — 流式调用。
*
* 返回一个可逐行读取的 async generator,每行是 JSON:
* {type:"meta",symbol,summary,periods}
* {type:"delta",content:"..."} ← 文本片段,逐个累加
* {type:"error",message:"..."}
* {type:"done"}
*
* 用 ReadableStream 解析(而非 SSE EventSource),支持 POST body 且更简单。
*/
async *financialAnalyzeStream(symbol: string, focus?: string): AsyncGenerator<{
type: 'meta' | 'delta' | 'error' | 'done'
symbol?: string
summary?: string
periods?: number
content?: string
message?: string
}> {
const res = await fetch('/api/financials/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol, focus: focus ?? '' }),
})
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)
}
if (!res.body) throw new Error('响应无 body')
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buf = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
// 按行分割(保留最后不完整的行在 buf)
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
const s = line.trim()
if (!s) continue
try {
yield JSON.parse(s)
} catch {
// 忽略无法解析的行
}
}
}
// 处理残余
if (buf.trim()) {
try { yield JSON.parse(buf.trim()) } catch { /* ignore */ }
}
},
// ===== 个股分析 =====
stockAnalysisLevels: (symbol: string, days = 120) =>
request<StockLevels>(`/api/stock-analysis/levels?symbol=${encodeURIComponent(symbol)}&days=${days}`),
stockAnalysisReportsList: () =>
request<{ reports: AiStockReport[] }>('/api/stock-analysis/reports'),
stockAnalysisReportSave: (r: {
symbol: string; name?: string; focus?: string; content: string
summary?: string; close?: number | null
levels?: Record<LevelType, PriceLevel[]>
}) =>
request<{ ok: boolean; report: AiStockReport }>('/api/stock-analysis/reports', {
method: 'POST', body: JSON.stringify(r),
}),
stockAnalysisReportDelete: (reportId: string) =>
request<{ ok: boolean }>(`/api/stock-analysis/reports/${encodeURIComponent(reportId)}`, { method: 'DELETE' }),
/**
* AI 个股四维分析 — 流式调用(NDJSON,与财务分析同协议)。
* meta 里额外带 levels(关键价位)供图表回放。
*/
async *stockAnalyzeStream(symbol: string, focus?: string): AsyncGenerator<{
type: 'meta' | 'delta' | 'error' | 'done'
symbol?: string
summary?: string
levels?: Record<LevelType, PriceLevel[]>
close?: number | null
content?: string
message?: string
}> {
const res = await fetch('/api/stock-analysis/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol, focus: focus ?? '' }),
})
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)
}
if (!res.body) throw new Error('响应无 body')
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buf = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
const s = line.trim()
if (!s) continue
try { yield JSON.parse(s) } catch { /* ignore */ }
}
}
if (buf.trim()) {
try { yield JSON.parse(buf.trim()) } catch { /* ignore */ }
}
},
// ===== 大盘复盘 =====
reviewReportsList: () =>
request<{ reports: AiReviewReport[] }>('/api/market-recap/reports'),
reviewReportSave: (r: {
as_of: string; focus?: string; content: string
summary?: string; emotion_score?: number | null; emotion_label?: string
}) =>
request<{ ok: boolean; report: AiReviewReport }>('/api/market-recap/reports', {
method: 'POST', body: JSON.stringify(r),
}),
reviewReportDelete: (reportId: string) =>
request<{ ok: boolean }>(`/api/market-recap/reports/${encodeURIComponent(reportId)}`, { method: 'DELETE' }),
/**
* AI 大盘复盘 — 流式调用(NDJSON,与个股/财务分析同协议)。
* meta 里带 as_of / emotion_score / emotion_label / summary,供前端先渲染信号灯。
*/
async *reviewStream(asOf?: string, focus?: string): AsyncGenerator<{
type: 'meta' | 'delta' | 'error' | 'done'
as_of?: string
emotion_score?: number
emotion_label?: string
summary?: string
content?: string
message?: string
}> {
const res = await fetch('/api/market-recap/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ as_of: asOf ?? null, focus: focus ?? '' }),
})
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)
}
if (!res.body) throw new Error('响应无 body')
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buf = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
const s = line.trim()
if (!s) continue
try { yield JSON.parse(s) } catch { /* ignore */ }
}
}
if (buf.trim()) {
try { yield JSON.parse(buf.trim()) } catch { /* ignore */ }
}
},
/** AI 概念轮动分析 — 流式 NDJSON。 */
async *rotationAnalyzeStream(days: number, focus?: string): AsyncGenerator<{
type: 'meta' | 'delta' | 'error' | 'done'
days?: number
summary?: string
content?: string
message?: string
}> {
const res = await fetch('/api/rps/rotation-analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ days, focus: focus ?? '' }),
})
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)
}
if (!res.body) throw new Error('响应无 body')
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buf = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
const s = line.trim()
if (!s) continue
try { yield JSON.parse(s) } catch { /* ignore */ }
}
}
if (buf.trim()) {
try { yield JSON.parse(buf.trim()) } catch { /* ignore */ }
}
},
// ===== Strategy Engine =====
strategyList: (assetType?: 'stock' | 'etf', timeframe = '1d') => {
const params = new URLSearchParams()
if (assetType) params.set('asset_type', assetType)
if (timeframe) params.set('timeframe', timeframe)
const qs = params.toString()
return request<{ strategies: StrategyDetail[]; load_errors?: StrategyLoadError[] }>(
`/api/strategies${qs ? `?${qs}` : ''}`,
)
},
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' }),
// ===== Monitor Rules (监控规则) =====
monitorRulesList: () =>
request<{ rules: MonitorRule[] }>('/api/monitor-rules'),
monitorRuleOptions: () =>
request<MonitorRuleOptions>('/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' }),
/** 模拟触发 ladder 封单监控 (Dev 调试, 不落盘不推送) */
monitorRuleTestLadder: () =>
request<{
ok: boolean
as_of: string
sealed_count: number
triggered: Array<{
rule_id: string; rule_name: string; symbol: string; name?: string
type: string; message: string; severity: string
sealed_value: number; sealed_metric: string
current_sealed_vol?: number; current_sealed_amount?: number
}>
not_triggered: Array<{
rule_id: string; rule_name: string; symbol: string
metric: string; threshold: number; current_value: number | null
current_sealed_vol?: number; current_sealed_amount?: number | null
reason: string
}>
}>('/api/monitor-rules/test-ladder', { method: 'POST' }),
/** 真实触发 ladder 预警 (落盘+飞书+SSE), Dev 调试用 */
monitorRuleTriggerLadder: () =>
request<{
ok: boolean
triggered: number
events: Array<{ symbol: string; name: string; message: string }>
}>('/api/monitor-rules/trigger-ladder', { method: 'POST' }),
/** 生成演示监控规则 (Dev 页用) */
monitorRuleSeed: () =>
request<{ ok: boolean; generated: number }>('/api/monitor-rules/seed', { method: 'POST' }),
// ===== Alerts (触发记录) =====
alertsList: (params?: { days?: number; limit?: number; source?: string; type?: string; extColumns?: 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)
if (params?.extColumns) qs.set('ext_columns', params.extColumns)
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; provider?: string }>('/api/strategies/ai/status'),
/** 测试 AI 连通性 */
strategyAiTest: () =>
request<{ ok: boolean; error?: string; model?: string; response?: 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<StrategyBuildResult>(
'/api/strategies/build',
{ method: 'POST', body: JSON.stringify({ step, ...payload }) },
),
async *strategyBuildStream(step: number, payload: Record<string, any>): AsyncGenerator<StrategyBuildStreamEvent> {
const res = await fetch('/api/strategies/build/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ step, ...payload }),
})
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)
}
if (!res.body) throw new Error('响应无 body')
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buf = ''
for (;;) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
const s = line.trim()
if (!s) continue
try { yield JSON.parse(s) } catch { /* ignore */ }
}
}
if (buf.trim()) {
try { yield JSON.parse(buf.trim()) } catch { /* ignore */ }
}
},
strategyValidateCode: (payload: { code: string; strategy_id?: string; name?: string; description?: string }) =>
request<StrategyBuildResult>('/api/strategies/code/validate', {
method: 'POST',
body: JSON.stringify(payload),
}),
strategySaveCodeV2: (payload: {
strategy_id: string
code: string
target_source: 'ai' | 'custom'
mode: 'create' | 'update'
name?: string
description?: string
}) =>
request<StrategyCodeSaveResult>('/api/strategies/code/save', {
method: 'POST',
body: JSON.stringify(payload),
}),
/** 创建/更新叠加策略(composite): 声明式引用多个子策略 */
strategySaveComposite: (payload: {
strategy_id: string
name: string
description?: string
children: { strategy_id: string; weight: number }[]
merge_mode: 'union' | 'intersect'
min_confirm?: number
mode: 'create' | 'update'
}) =>
request<StrategyCodeSaveResult>('/api/strategies/composite/save', {
method: 'POST',
body: JSON.stringify(payload),
}),
/** 保存 AI 生成的策略文件 */
strategySaveCode: (strategyId: string, code: string, meta?: { name?: string; description?: string }) =>
request<{ ok: boolean; path: string }>('/api/strategies/ai/save', {
method: 'POST',
body: JSON.stringify({ strategy_id: strategyId, code, name: meta?.name ?? '', description: meta?.description ?? '' }),
}),
}
// ===== 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
etf_daily: TableStats | null
etf_enriched: TableStats | null
etf_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
etf_daily_files?: number
etf_daily_size_mb?: number
etf_enriched_files?: number
etf_enriched_size_mb?: number
etf_instruments_files?: number
etf_instruments_size_mb?: number
etf_adj_factor_files?: number
etf_adj_factor_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
indicators_ready?: boolean
}
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
next_run?: string | null
}
export interface ExtDataDetectUrlRequest {
url: string
method?: string
headers?: Record<string, string>
body?: string
response_path?: string
field_map?: Record<string, string>
}
export interface ExtDataDetectUrlResult {
status: string
total_rows: number
response_path: string
response_path_candidates: string[]
fields: ExtDataField[]
symbol_candidates: string[]
code_candidates: string[]
preview: Record<string, unknown>[]
}
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 DimensionMembersResult {
id: string
label: string
date: string | null
field: string
value: string
total: number
limit: number
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
}