"""Pydantic DTO —— 这就是 OpenAPI 契约(前端据此生成类型化客户端)。 契约先于业务锁定:字段一旦定下,前端可并行开发,后端实现改动不影响前端。 """ from __future__ import annotations from datetime import datetime from typing import Literal from pydantic import BaseModel, Field # ---------- Candle ---------- class CandleOut(BaseModel): ts: datetime open: float high: float low: float close: float volume: float amount: float | None = None # 成交额(元);无数据为 null turnover: float | None = None # 换手率 %;无数据为 null model_config = {"from_attributes": True} # ---------- Backtest ---------- class BacktestRequest(BaseModel): symbol: str = "000001" timeframe: str = "1d" strategy: str = "macd_cross" # macd_cross | ma_cross | single_ma params: dict[str, float] = Field(default_factory=dict) # 各策略参数 initial_cash: float = 1000000.0 fast_mode: bool = False # True => 关闭 T+1/费用,交互试探 start: datetime | None = None end: datetime | None = None class SignalOut(BaseModel): ts: datetime side: str # "buy" | "sell" price: float qty: float class EquityPoint(BaseModel): ts: datetime value: float class IndicatorOut(BaseModel): strategy: str data: dict[str, list[float | None]] = {} # 列名 -> 序列(MACD: macd/signal/hist;均线: fast/slow 或 ma) class MetricsOut(BaseModel): total_return: float max_drawdown: float sharpe: float volatility: float num_trades: int = 0 win_rate: float = 0.0 class BacktestResponse(BaseModel): symbol: str timeframe: str strategy: str candles: list[CandleOut] indicators: IndicatorOut signals: list[SignalOut] equity: list[EquityPoint] metrics: MetricsOut final_cash: float final_position: float initial_cash: float class SyncRequest(BaseModel): symbol: str start: str | None = None # YYYYMMDD end: str | None = None source: str = "auto" # auto | tushare | akshare force: bool = False # True => 忽略缓存重新拉取 # ---------- Event Backtest(自然语言事件回测) ---------- class EventBacktestSpec(BaseModel): """事件回测参数:entry 条件在信号日 D 收盘确认 -> D+1 买入 -> 持有 N 日卖出。""" entry: ScreenConditions entry_timing: Literal["next_open", "next_close"] = "next_open" # 次日开盘/收盘买入 holding_days: int = Field(default=3, ge=1, le=250) # 买入后再持有 N 个交易日 exit_timing: Literal["close", "open"] = "close" # 到期按收盘/开盘卖出 class EventBacktestRequest(BaseModel): text: str = Field(min_length=2, max_length=500) spec: EventBacktestSpec | None = None # 直传则跳过 LLM 解析(调参重跑) ts_code: str | None = None # 指定则只回测该股;空则全市场 start: datetime | None = None end: datetime | None = None class EventTradeOut(BaseModel): ts_code: str name: str | None = None entry_date: datetime entry_price: float exit_date: datetime exit_price: float ret_pct: float # 区间收益率 %(复权校正) class EventYearStatOut(BaseModel): year: int samples: int mean_pct: float median_pct: float win_rate: float class EventStatsOut(BaseModel): samples: int stocks: int mean_pct: float median_pct: float win_rate: float # % std_pct: float = 0.0 p10_pct: float = 0.0 p25_pct: float = 0.0 p75_pct: float = 0.0 p90_pct: float = 0.0 max_pct: float = 0.0 min_pct: float = 0.0 by_year: list[EventYearStatOut] = Field(default_factory=list) class EventBacktestResponse(BaseModel): text: str spec: EventBacktestSpec universe: str # "all" 或 ts_code start: datetime end: datetime stats: EventStatsOut trades: list[EventTradeOut] = Field(default_factory=list) # 最好+最差样本(各 100) total: int class SyncResponse(BaseModel): symbol: str bars: int source: str # ---------- Screener(智能选股) ---------- Op = Literal["gt", "ge", "lt", "le", "between"] class IndicatorCondition(BaseModel): """技术指标条件(在最近 lookback 个交易日窗口内判定)。 indicator 白名单见 screener/llm.py 的 SYSTEM_PROMPT(kdj_j / rsi / macd_dif…)。 设置 value_indicator 时为指标间比较(如 DIF > DEA、close < boll_lower),value 填 0 占位。 """ indicator: str params: dict[str, float] = Field(default_factory=dict) # 如 {"n": 9, "m1": 3, "m2": 3} op: Op value: float value2: float | None = None # between 上界 value_indicator: str | None = None # 比较对象为另一指标(同白名单)时使用 value_params: dict[str, float] = Field(default_factory=dict) # 比较对象指标参数(默认沿用 params/默认值) lookback: int = 1 # 检查最近 N 个交易日 match: Literal["all", "any"] = "all" # all=连续满足;any=任一满足 class SnapshotCondition(BaseModel): """每日快照条件(最新交易日截面)。市值单位亿元;换手率为百分数(5 表示 5%)。""" field: str # total_mv|circ_mv|pe_ttm|pb|turnover_rate|close op: Op value: float value2: float | None = None class ScreenConditions(BaseModel): indicator: list[IndicatorCondition] = Field(default_factory=list) snapshot: list[SnapshotCondition] = Field(default_factory=list) exclude_st: bool = True exclude_delisted: bool = True exclude_bj: bool = True # 排除北交所 class ScreenerRunRequest(BaseModel): text: str = Field(min_length=2, max_length=500) # 直传条件则跳过 LLM 解析(预留给"微调再跑") conditions: ScreenConditions | None = None class ScreenerItemOut(BaseModel): ts_code: str name: str close: float | None = None # 最新收盘价(元) pct_chg: float | None = None # 日涨跌幅 % total_mv: float | None = None # 总市值(亿元) circ_mv: float | None = None # 流通市值(亿元) pe_ttm: float | None = None pb: float | None = None turnover_rate: float | None = None indicators: dict[str, float | None] = Field(default_factory=dict) # 引用到的指标最新值 class ScreenerRunResponse(BaseModel): conditions: ScreenConditions trade_date: datetime | None # 数据基准交易日 total: int # 命中总数(items 可能被截断) items: list[ScreenerItemOut] indicator_labels: dict[str, str] = Field(default_factory=dict) # "kdj_j" -> "KDJ J(9,3,3)" class ScreenerSyncRequest(BaseModel): days: int = Field(default=90, ge=10, le=250) # 同步最近 N 个交易日 force: bool = False # True => 全量重拉(幂等) class ScreenerSyncStatus(BaseModel): running: bool step: str | None = None # 进行中步骤文案 total_days: int = 0 done_days: int = 0 error: str | None = None ready: bool = False # 至少 1 个交易日数据可用于选股 last_trade_date: datetime | None = None last_synced_at: datetime | None = None stats: dict[str, int] = Field(default_factory=dict) # stocks/daily_rows/snapshot_rows/dates # ---------- 个股详情预览(选股结果点入,全屏同花顺/通达信式) ---------- class PreviewInfoOut(BaseModel): ts_code: str symbol: str name: str industry: str | None = None area: str | None = None market: str | None = None # 主板/创业板/科创板/北交所 list_date: str | None = None trade_date: datetime | None = None # 行情/信息卡数据基准交易日 open: float | None = None high: float | None = None low: float | None = None close: float | None = None pre_close: float | None = None pct_chg: float | None = None # 日涨跌幅 % volume_hand: float | None = None # 成交量(手) amount_yi: float | None = None # 成交额(亿元) turnover_rate: float | None = None # 换手率 % pe_ttm: float | None = None pb: float | None = None total_mv: float | None = None # 总市值(亿元) circ_mv: float | None = None # 流通市值(亿元) class PreviewResponse(BaseModel): ts_code: str symbol: str source: str # bfq|qfq|hfq=实际复权口径(本地 adj_factor 换算) | market=近段未复权兜底 info: PreviewInfoOut candles: list[CandleOut] indicators: dict[str, dict[str, list[float | None]]] = Field(default_factory=dict) # indicators 形如 {"ma": {"ma5": [...], ...}, "macd": {"dif": ...}, "kdj": {...}, "rsi": {...}, "boll": {...}} has_more: bool = False # 返回窗口之前是否还有更早历史(前端向左滚动翻页用) # ---------- Auth ---------- class LoginRequest(BaseModel): username: str = Field(min_length=1, max_length=64) password: str = Field(min_length=1, max_length=1024) class CurrentUserOut(BaseModel): id: int username: str model_config = {"from_attributes": True} class LoginResponse(BaseModel): user: CurrentUserOut expires_at: datetime # ---------- 股票列表(全市场浏览) ---------- class StockListItemOut(BaseModel): ts_code: str symbol: str name: str industry: str | None = None market: str | None = None close: float | None = None # 最新收盘(candles 未复权) prev_close: float | None = None pct_chg: float | None = None # 最新两根日线计算 last_ts: datetime | None = None bar_count: int | None = None # 本地缓存日线条数 watched: bool = False # 是否自选(当前用户) class StockListResponse(BaseModel): total: int items: list[StockListItemOut] # ---------- 看股页筛选项 ---------- class FacetItemOut(BaseModel): name: str count: int class StockFacetsResponse(BaseModel): industries: list[FacetItemOut] = Field(default_factory=list) areas: list[FacetItemOut] = Field(default_factory=list) # ---------- 用户偏好 / 自选股 / 提问历史 ---------- class PreferencesOut(BaseModel): prefs: dict[str, object] = Field(default_factory=dict) # key -> JSON 值 class PreferencesUpdate(BaseModel): prefs: dict[str, object] # 部分更新:只覆盖出现的 key(值为 null 表示删除) class WatchlistOp(BaseModel): ts_code: str = Field(min_length=6, max_length=12) class ScreenerQueryOut(BaseModel): id: int text: str conditions: ScreenConditions | None = None hit_count: int | None = None created_at: datetime model_config = {"from_attributes": True} class ScreenerQueryListResponse(BaseModel): items: list[ScreenerQueryOut]