Files
stock/backend/app/schemas.py
2026-09-09 15:07:58 +08:00

713 lines
27 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 date, 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 # 返回窗口之前是否还有更早历史(前端向左滚动翻页用)
# ---------- 公司简介tushare stock_company详情页按需懒加载 ----------
class StockCompanyOut(BaseModel):
ts_code: str
com_name: str | None = None # 公司全称
com_id: str | None = None # 统一社会信用代码
chairman: str | None = None # 法人代表
manager: str | None = None # 总经理
secretary: str | None = None # 董秘
reg_capital: float | None = None # 注册资本(万元)
setup_date: str | None = None # 注册日期 YYYYMMDD展示层换算
province: str | None = None # 所在省份
city: str | None = None # 所在城市
introduction: str | None = None # 公司介绍
website: str | None = None # 公司主页
email: str | None = None # 电子邮件
office: str | None = None # 办公地址
employees: int | None = None # 员工人数
main_business: str | None = None # 主要业务及产品
business_scope: str | None = None # 经营范围
# ---------- 财务数据fina_indicator + 三大报表关键值,详情页按需懒加载) ----------
class StockFinanceRecordOut(BaseModel):
"""一行 = 一个报告期。金额单位元(展示层换算亿/万),比率与同比为百分数。"""
end_date: str # 报告期 YYYYMMDD
ann_date: str | None = None # 公告日 YYYYMMDD
eps: float | None = None # 基本每股收益(元)
bps: float | None = None # 每股净资产(元)
ocfps: float | None = None # 每股经营现金流净额(元)
roe: float | None = None # 净资产收益率 %
roe_dt: float | None = None # 扣非净资产收益率 %
grossprofit_margin: float | None = None # 销售毛利率 %
netprofit_margin: float | None = None # 销售净利率 %
debt_to_assets: float | None = None # 资产负债率 %
or_yoy: float | None = None # 营业收入同比 %
netprofit_yoy: float | None = None # 归母净利润同比 %
dt_netprofit_yoy: float | None = None # 扣非净利润同比 %
profit_dedt: float | None = None # 扣非净利润(元)
rd_exp: float | None = None # 研发投入(元)
total_revenue: float | None = None # 营业总收入(元)
operate_profit: float | None = None # 营业利润(元)
n_income_attr_p: float | None = None # 归母净利润(元)
total_assets: float | None = None # 总资产(元)
total_hldr_eqy: float | None = None # 归母股东权益(元)
n_cashflow_act: float | None = None # 经营现金流净额(元)
class StockFinanceOut(BaseModel):
ts_code: str
records: list[StockFinanceRecordOut] # 按报告期倒序records[0] = 最新报告期)
# ---------- 分红送股tushare dividend详情页按需懒加载 ----------
class StockDividendRecordOut(BaseModel):
end_date: str | None = None # 分红年度 YYYYMMDD
ann_date: str | None = None # 预案公告日
div_proc: str | None = None # 实施进度(预案/实施)
stk_div: float | None = None # 每股送转
stk_bo_rate: float | None = None # 每股送股比例
stk_co_rate: float | None = None # 每股转增比例
cash_div: float | None = None # 每股分红(税后,元)
cash_div_tax: float | None = None # 每股分红(税前,元)
base_share: float | None = None # 基准股本(万股)
record_date: str | None = None # 股权登记日
ex_date: str | None = None # 除权除息日K线标记锚点
pay_date: str | None = None # 派息日
div_listdate: str | None = None # 红股上市日
imp_ann_date: str | None = None # 实施公告日
class StockDividendOut(BaseModel):
ts_code: str
records: list[StockDividendRecordOut] # 按分红年度倒序;空列表 = 确认无分红
# ---------- 参考数据tushare 参考数据版块,详情页按需懒加载) ----------
class StockReferenceOut(BaseModel):
"""rows_json 快照直出records 为清洗后的 tushare 原始行,字段随 kind 而异。"""
ts_code: str
kind: str # 白名单见 app/data/reference.py REFERENCE_KINDS
records: list[dict[str, str | float | None]] = Field(default_factory=list)
# ---------- 打板专题(主页,同花顺口径) ----------
class LimitStockOut(BaseModel):
"""涨跌停榜单行:三池共用,涨停池字段最全(炸板/跌停池仅价格类字段)。"""
ts_code: str
name: str | None = None
price: float | None = None # 收盘价(元)
pct_chg: float | None = None # 涨跌幅 %
tag: str | None = None # 涨停标签:首板 / 2天2板仅涨停池
status: str | None = None # 涨停状态:一字板 / 换手板 / N连板仅涨停池
lu_desc: str | None = None # 涨停原因(仅涨停池)
open_num: float | None = None # 打开次数
limit_amount_yi: float | None = None # 封单额(亿元,仅涨停池)
turnover_yi: float | None = None # 成交额(亿元,仅涨停池)
first_lu_time: str | None = None # 首次涨停时间
last_lu_time: str | None = None # 最后涨停时间(仅炸板池)
limit_up_suc_rate: float | None = None # 近一年封板率 %(仅涨停池)
class LimitLadderOut(BaseModel):
ts_code: str
name: str | None = None
nums: int # 连板数
class LimitBlockOut(BaseModel):
name: str | None = None # 同花顺概念板块名
days: float | None = None # 板块连涨天数
up_stat: str | None = None # 如「6天3板」
cons_nums: float | None = None # 连板家数
up_nums: float | None = None # 涨停家数
pct_chg: float | None = None # 板块涨跌 %
class LimitSummaryOut(BaseModel):
up_count: int = 0
broken_count: int = 0
down_count: int = 0
first_board_count: int = 0
max_ladder: LimitLadderOut | None = None
ladder_dist: list[dict[str, int]] = Field(default_factory=list) # [{nums, count}] 升序2板起
class LimitBoardResponse(BaseModel):
trade_date: str # YYYY-MM-DD
updated_at: str
summary: LimitSummaryOut
up: list[LimitStockOut] = Field(default_factory=list) # 涨停池(按封单额降序)
broken: list[LimitStockOut] = Field(default_factory=list) # 炸板池
down: list[LimitStockOut] = Field(default_factory=list) # 跌停池
ladder: list[LimitLadderOut] = Field(default_factory=list) # 连板天梯(连板数降序)
blocks: list[LimitBlockOut] = Field(default_factory=list) # 涨停最强板块
errors: list[str] = Field(default_factory=list)
# ---------- 概念板块THSths_index 列表 + ths_daily 行情 + ths_member 成分) ----------
class ThsBoardOut(BaseModel):
ts_code: str # 885835.TI / 700001.TI
name: str | None = None
type: str | None = None # N概念 I行业 TH主题 S特色 R地域 BB宽基 ST风格
count: float | None = None # 成分个数
list_date: str | None = None # YYYYMMDD
close: float | None = None # 板块指数收盘(当日快照)
pct_change: float | None = None # 涨跌幅 %
vol: float | None = None # 成交量(手)
turnover_rate: float | None = None # 换手率 %
class ThsBoardListResponse(BaseModel):
trade_date: str | None = None
updated_at: str | None = None
boards: list[ThsBoardOut] = Field(default_factory=list)
errors: list[str] = Field(default_factory=list)
class ThsMemberOut(BaseModel):
con_code: str # 成分股代码 000016.SZ
con_name: str | None = None
close: float | None = None # 现价candles 最新,北交所等无底座为空)
pct_chg: float | None = None # 涨跌幅 %(最新收盘 / 前收 - 1
class ThsBoardMembersResponse(BaseModel):
code: str
name: str | None = None
members: list[ThsMemberOut] = Field(default_factory=list)
# ---------- 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
turnover_rate: float | None = None # 换手率 %daily_snapshot
pe_ttm: float | None = None # 市盈率 TTM
pb: float | None = None # 市净率
total_mv: float | None = None # 总市值(亿元)
circ_mv: float | None = None # 流通市值(亿元)
watched: bool = False # 是否自选(当前用户)
held: 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)
# ---------- ETF 列表(全市场浏览;行情走 candles 底座,规模走东财快照) ----------
class EtfListItemOut(BaseModel):
ts_code: str # 510300.SH
symbol: str # 510300
name: str
exchange: str # SH/SZ
close: float | None = None # 最新收盘candles 未复权)
prev_close: float | None = None
pct_chg: float | None = None # 最新两根日线计算
amount: float | None = None # 最新成交额亿元candles 最新 bar
last_ts: datetime | None = None
turnover_rate: float | None = None # 换手率 %(东财快照)
total_mv: float | None = None # 总市值(亿元,东财快照)
circ_mv: float | None = None # 流通市值(亿元)
list_date: str | None = None # 上市日 YYYYMMDD首根K线日
watched: bool = False # 是否自选(当前用户)
class EtfListResponse(BaseModel):
total: int
items: list[EtfListItemOut]
class EtfSyncRequest(BaseModel):
full: bool = False # true = 忽略增量起点全量重拉(修数据用)
class EtfSyncStatus(BaseModel):
running: bool
step: str | None = None
total: int = 0 # 本次需拉 K 线的 ETF 数
done: int = 0
error: str | None = None
ready: bool = False # etf_basic 非空且已有日线
last_trade_date: datetime | None = None
last_synced_at: datetime | None = None
stats: dict[str, int] = Field(default_factory=dict) # etfs
# ---------- 用户偏好 / 自选股 / 提问历史 ----------
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 HoldingOp(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]
# ---------- 交割单(个人实盘买卖点) ----------
class UserTradeOut(BaseModel):
id: int
ts_code: str
name: str | None = None
trade_date: date # 成交日期ISO YYYY-MM-DD
direction: str # buy | sell
price: float | None = None # 成交价(券商原始价,不复权)
qty: float # 股数
amount: float | None = None
fee: float | None = None
class TradesImportResponse(BaseModel):
inserted: int # 新入库成交笔数
skipped_dup: int # 与库内完全一致(重复上传同文件)跳过
skipped_other: int # 非买卖行(转账/配号/利息等)
stocks: int # 涉及股票数
bad: list[str] = Field(default_factory=list) # 解析失败样例(前 5 条)
sample: list[UserTradeOut] = Field(default_factory=list) # 本次入库的前几笔(核对用)
class TradesClearResponse(BaseModel):
deleted: int
# ---------- 大盘总览(主页) ----------
class IndexQuoteOut(BaseModel):
code: str # 000001.SH / HKTECH / DJI
name: str # 上证指数 / 恒生科技 / 道琼斯
region: str # cn | hk | us前端分组展示
close: float | None = None
change: float | None = None
pct_chg: float | None = None
trade_date: date | None = None # 行情归属交易日(实时时为当天)
spark: list[float] = [] # 近 N 日收盘(旧 -> 新),迷你走势用
spark_dates: list[str] = [] # 与 spark 对齐的交易日YYYYMMDDhover 提示用
realtime: bool = False # True=盘中实时腾讯False=最近收盘tushare EOD
class MarketStatsOut(BaseModel):
"""沪深两市汇总daily_info沪 SH_MARKET(主板A+科创+B) + 深 SZ_MARKET不含基金/北交所,亿元)。"""
trade_date: date | None = None
total_mv: float | None = None # 总市值(亿元)
float_mv: float | None = None # 流通市值(亿元)
amount: float | None = None # 两市成交额亿元EOD
turnover: float | None = None # 换手率 %(沪市口径)
amount_today: float | None = None # 当日实时两市成交额(亿元,腾讯;收盘后为当日终值)
class AmountBarOut(BaseModel):
"""两市成交额历史的一根柱(亿元)。"""
date: date
amount: float
intraday: bool = False # True=当日实时口径(尚未并入 EOD
class MarketOverviewResponse(BaseModel):
updated_at: datetime
indexes: list[IndexQuoteOut] = []
stats: MarketStatsOut | None = None
amount_history: list[AmountBarOut] = [] # 近 N 交易日两市成交额(旧 -> 新,末根可能盘中)
errors: list[str] = [] # 部分来源失败的说明(透明但不阻塞展示)
# ---------- 指数专题(国际指数卡片 + 指数详情) ----------
class GlobalIndexQuoteOut(BaseModel):
"""国际指数卡片index_global 最新收盘 + 45 日 spark"""
code: str # DJI / SPX / HSI ...
name: str # 道琼斯工业指数
region: str # americas | europe | asia前端分组
country: str # 美国 / 英国 / 日本 ...
close: float | None = None
change: float | None = None
pct_chg: float | None = None
trade_date: date | None = None
spark: list[float] = []
spark_dates: list[str] = []
class GlobalIndexListResponse(BaseModel):
updated_at: datetime
items: list[GlobalIndexQuoteOut] = []
errors: list[str] = []
class IndexBasicOut(BaseModel):
"""指数基本信息:国内 index_basic市场/发布方/基期基点),国际由静态表合成(国家/地区)。"""
ts_code: str
name: str
market: str | None = None # SSE / CSI / SZSE ...
publisher: str | None = None # 中证指数 / 上交所
category: str | None = None # 规模指数 / 综合指数
base_date: date | None = None # 基期
base_point: float | None = None # 基点
list_date: date | None = None # 发布日期
country: str | None = None # 国际指数:国家/地区
region: str | None = None # 国际指数americas/europe/asia
class IndexValuationPointOut(BaseModel):
"""大盘指数每日指标的一日index_dailybasic仅部分国内指数有数据"""
trade_date: date
pe: float | None = None
pe_ttm: float | None = None
pb: float | None = None
turnover_rate: float | None = None # 换手率 %
total_mv: float | None = None # 总市值tushare 文档标注万元,实测按元才对得上量级)
float_mv: float | None = None # 流通市值(元)
class IndexQuoteBriefOut(BaseModel):
"""详情页头部最新行情(收盘口径)。"""
close: float | None = None
change: float | None = None
pct_chg: float | None = None
open: float | None = None
high: float | None = None
low: float | None = None
pre_close: float | None = None
trade_date: date | None = None
spark: list[float] = []
spark_dates: list[str] = []
class IndexDetailResponse(BaseModel):
code: str
name: str
region: str # cn | americas | europe | asia
quote: IndexQuoteBriefOut
basic: IndexBasicOut | None = None
valuation: IndexValuationPointOut | None = None # 最新一日(接口不覆盖时 None
valuation_history: list[IndexValuationPointOut] = [] # 近 N 日PE 走势小图)
class IndexWeightItemOut(BaseModel):
con_code: str # 成分股 000001.SZ
name: str | None = None # 平安银行(本地 stock_basic 回填,缺则 None
weight: float # 权重 %
class IndexWeightsResponse(BaseModel):
trade_date: date
total: int # 成分股总数(返回 items 为按权重降序的子集)
items: list[IndexWeightItemOut] = []