"""ORM 模型。 Candle 表设计与 TimescaleDB hypertable 完全兼容:将来在目标 PG 库执行 SELECT create_hypertable('candles', 'ts'); 即可升级为时序表 + Continuous Aggregates 多周期预聚合,无需改表结构。 智能选股三表(stock_basic / market_daily / daily_snapshot)与回测 candles(qfq) 完全隔离:选股用未复权日线按 trade_date 全市场批量落地,避免污染回测复权缓存。 """ from datetime import datetime from sqlalchemy import DateTime, Float, Integer, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from .db import Base def _utcnow() -> datetime: # naive UTC,避免 SQLite 存储时区带来的麻烦 from datetime import timezone return datetime.now(timezone.utc).replace(tzinfo=None) class Candle(Base): __tablename__ = "candles" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) symbol: Mapped[str] = mapped_column(String(16), index=True) timeframe: Mapped[str] = mapped_column(String(4), default="1d", index=True) ts: Mapped[datetime] = mapped_column(DateTime, index=True) # bar 开始时间 open: Mapped[float] = mapped_column(Float) high: Mapped[float] = mapped_column(Float) low: Mapped[float] = mapped_column(Float) close: Mapped[float] = mapped_column(Float) volume: Mapped[float] = mapped_column(Float) __table_args__ = ( UniqueConstraint("symbol", "timeframe", "ts", name="uq_candle_sym_tf_ts"), ) class BacktestRun(Base): """回测运行注册表(可复现/可审计/可回归对比的基础)。 完整版应记录 策略版本 + 参数快照 + 数据快照(复权/数据源/库版本)+ 环境指纹 + 结果指纹。 MVP 先落关键字段,结构就位。 """ __tablename__ = "backtest_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) symbol: Mapped[str] = mapped_column(String(16)) strategy: Mapped[str] = mapped_column(String(64)) timeframe: Mapped[str] = mapped_column(String(4), default="1d") params_json: Mapped[str] = mapped_column(String, default="{}") initial_cash: Mapped[float] = mapped_column(Float, default=100000.0) total_return: Mapped[float] = mapped_column(Float, default=0.0) max_drawdown: Mapped[float] = mapped_column(Float, default=0.0) sharpe: Mapped[float] = mapped_column(Float, default=0.0) num_trades: Mapped[int] = mapped_column(Integer, default=0) class StockBasic(Base): """A股股票列表(stock_basic 快照;选股展示名称、排除 ST/退市/北交所的依据)。""" __tablename__ = "stock_basic" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) ts_code: Mapped[str] = mapped_column(String(12), unique=True, index=True) # 000001.SZ symbol: Mapped[str] = mapped_column(String(10), index=True) # 000001 name: Mapped[str] = mapped_column(String(32)) area: Mapped[str | None] = mapped_column(String(32)) industry: Mapped[str | None] = mapped_column(String(32)) market: Mapped[str | None] = mapped_column(String(32)) # 主板/创业板/科创板/北交所 exchange: Mapped[str] = mapped_column(String(8)) # SSE/SZSE/BSE list_status: Mapped[str] = mapped_column(String(2), index=True) # L上市 D退市 P暂停 list_date: Mapped[str] = mapped_column(String(8), default="") delist_date: Mapped[str | None] = mapped_column(String(8)) class MarketDaily(Base): """全市场未复权日线(选股专用,与回测 candles(qfq) 隔离)。 单位沿用 Tushare 原始:vol 手、amount 千元。 """ __tablename__ = "market_daily" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) trade_date: Mapped[datetime] = mapped_column(DateTime, index=True) ts_code: Mapped[str] = mapped_column(String(12), index=True) open: Mapped[float] = mapped_column(Float) high: Mapped[float] = mapped_column(Float) low: Mapped[float] = mapped_column(Float) close: Mapped[float] = mapped_column(Float) pre_close: Mapped[float] = mapped_column(Float) change: Mapped[float | None] = mapped_column(Float) pct_chg: Mapped[float | None] = mapped_column(Float) # 日涨跌幅 % vol: Mapped[float] = mapped_column(Float) # 手 amount: Mapped[float] = mapped_column(Float) # 千元 __table_args__ = ( UniqueConstraint("ts_code", "trade_date", name="uq_mkt_code_date"), ) class DailySnapshot(Base): """每日指标快照(daily_basic)。total_mv/circ_mv 单位万元(Tushare 原始),API 层换算亿元。""" __tablename__ = "daily_snapshot" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) trade_date: Mapped[datetime] = mapped_column(DateTime, index=True) ts_code: Mapped[str] = mapped_column(String(12), index=True) close: Mapped[float | None] = mapped_column(Float) turnover_rate: Mapped[float | None] = mapped_column(Float) # 换手率 % turnover_rate_f: Mapped[float | None] = mapped_column(Float) # 自由流通换手率 % volume_ratio: Mapped[float | None] = mapped_column(Float) # 量比 pe: Mapped[float | None] = mapped_column(Float) pe_ttm: Mapped[float | None] = mapped_column(Float) pb: Mapped[float | None] = mapped_column(Float) total_mv: Mapped[float | None] = mapped_column(Float) # 总市值(万元) circ_mv: Mapped[float | None] = mapped_column(Float) # 流通市值(万元) __table_args__ = ( UniqueConstraint("ts_code", "trade_date", name="uq_snap_code_date"), ) class TradeCalendar(Base): """交易日历缓存(trade_cal 拉取一次宽范围后本地维护,低积分 token 限频 1 次/小时)。""" __tablename__ = "trade_calendar" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) trade_date: Mapped[str] = mapped_column(String(8), unique=True, index=True) # YYYYMMDD