- 首页双入口(智能选股/策略回测):引入 vue-router,顶部导航 - 智能选股:自然语言 -> LLM 解析结构化条件(智谱 GLM,OpenAI 兼容,/v4 兼容)-> SQL 快照预筛 + pandas 指标过滤(复用 indicators 单一事实源) - 条件模型:指标 vs 常数/指标(value_indicator,如 DIF>DEA、close<布林下轨)、lookback+match 表达连续N天/近N天任一天、市值/PE/PB/换手率快照条件、默认排除 ST/退市/北交所 - 全市场数据同步:按 trade_date 批量拉取未复权日线(与回测 candles qfq 隔离),交易日历/股票列表本地缓存,daily_basic 仅最新截面,Tushare 限频兜底(分钟级重试/小时级降级) - 存储:DATABASE_URL 切远程 PostgreSQL(cirry.cn/stock),本地 SQLite 已移除 - .env 入库(私有仓库);smoke_test 扩展选股链路 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
"""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
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
# ---------- Backtest ----------
|
||
class BacktestRequest(BaseModel):
|
||
symbol: str = "DEMO"
|
||
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 => 忽略缓存重新拉取
|
||
|
||
|
||
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
|