Files
stock/backend/app/schemas.py
2026-08-15 15:36:11 +08:00

346 lines
11 KiB
Python
Raw Permalink 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.
"""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_PROMPTkdj_j / rsi / macd_dif…
设置 value_indicator 时为指标间比较(如 DIF > DEA、close < boll_lowervalue 填 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]