Files
stock/backend/app/schemas.py
cirry 528357c3f5 feat: AI 自然语言选股(GLM)+ 全市场数据管道 + 远程 PostgreSQL
- 首页双入口(智能选股/策略回测):引入 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>
2026-08-14 14:49:53 +08:00

170 lines
5.3 KiB
Python
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.
"""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_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