从零搭建量化分析系统之热点追踪-01-技术方案
版本:v0.1(MVP)
日期:2026-07-13
目标周期:4 周 MVP → 2 周优化 → 持续迭代
一、项目概述
1.1 系统定位
从通达信盘口/逐笔数据中,实时捕获市场热点股票及板块资金异动,输出量化的"热点指数"(HotScore),辅助交易决策。
1.2 核心目标
| 层级 | 目标 | 说明 |
|---|---|---|
| 近期 | 未来 5–30 分钟是否会成为市场热点 | 可验证、有交易价值 |
| 中期 | 板块联动与龙头识别 | 提供板块维度的资金流向视图 |
| 远期 | AI 预测热度提升 | 在规则引擎基础上叠加模型 |
不做:预测涨停、预测未来一周股价。
最重要的原则:Evaluation 比 AI 更重要。
整个系统的价值取决于一个问题:HotScore 选出来的股票,到底准不准?
评估层是这个问题的唯一裁判。它从第 2 周就开始运行,并且永不停止。
AI 模型只有在评估达标后才有资格启动。
1.3 整体架构
┌──────────────────────────────────────────────────────────────────┐
│ 前端展示层 │
│ 实时热点排行 │ 板块热力图 │ 个股资金面板 │ 异动告警 │ 评估看板 │
└──────────────────────────┬───────────────────────────────────────┘
│ WebSocket / SSE
┌──────────────────────────┴───────────────────────────────────────┐
│ API 服务层 (FastAPI) │
│ /hotspot/* /sector/* /alert/* /eval/* /model/* │
└──────────────────────────┬───────────────────────────────────────┘
│
┌──────────────────────────┴───────────────────────────────────────┐
│ 计算引擎层 │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ FeatureEng │ │ HotScore │ │ SectorEng │ │ AI Predictor │ │
│ │ 特征计算 │ │ 热点评分 │ │ 板块联动 │ │ 模型推理 │ │
│ └────────────┘ └────────────┘ └────────────┘ └──────────────┘ │
└──────────────────────────┬───────────────────────────────────────┘
│
┌──────────────────────────┴───────────────────────────────────────┐
│ 数据管道层 │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ TickCollector│ │ OrderBookCol │ │ SectorCollector │ │
│ │ 逐笔成交采集 │ │ 五档盘口采集 │ │ 板块/涨停/情绪数据采集 │ │
│ └──────┬───────┘ └──────┬───────┘ └────────────┬───────────┘ │
│ └────────────────┴──────────────────────┘ │
│ DataBus (内存队列 / Redis Stream) │
└──────────────────────────┬───────────────────────────────────────┘
│
┌──────────────────────────┴───────────────────────────────────────┐
│ 存储层 │
│ Redis (实时状态/排行) │ DuckDB (历史特征/回测) │ MySQL (配置/映射) │
└─────────────────────────────────────────────────────────────────┘
1.4 项目目录结构
market_hotspot/
├── config/
│ ├── settings.yaml # 全局配置
│ ├── sectors.yaml # 板块-个股映射
│ └── hotscore_weights.yaml # HotScore 权重配置(可热更新)
├── collector/ # 数据采集层
│ ├── __init__.py
│ ├── base.py # 采集器基类
│ ├── tick_collector.py # 逐笔成交采集
│ ├── orderbook_collector.py # 五档盘口采集
│ ├── sector_collector.py # 板块行情采集
│ ├── limitup_collector.py # 涨停/炸板数据采集
│ └── manager.py # 采集调度管理器
├── engine/ # 计算引擎层
│ ├── __init__.py
│ ├── feature_engine.py # 特征计算引擎
│ ├── hotscore_engine.py # HotScore 评分引擎
│ ├── sector_engine.py # 板块联动引擎
│ └── alert_engine.py # 异动告警引擎
├── model/ # AI 预测层
│ ├── __init__.py
│ ├── label_builder.py # 标签构建(严防未来函数)
│ ├── trainer.py # 离线训练
│ ├── predictor.py # 在线推理
│ └── saved/ # 模型文件目录
├── evaluator/ # 评估层(核心模块)
│ ├── __init__.py
│ ├── eval_engine.py # 评估引擎(快照 + 回填 + 汇总)
│ ├── precision.py # 多阈值 Precision / Recall 计算
│ ├── baseline.py # 基线对比(随机/成交额Top/涨幅Top)
│ ├── session_analyzer.py # 分时段分析
│ ├── ic_analyzer.py # 特征分量 IC 分析
│ ├── weight_optimizer.py # 基于 IC 的权重自动调优
│ ├── reporter.py # 每日评估报告生成
│ └── eval_storage.py # 评估数据读写(DuckDB)
├── api/ # API 层
│ ├── __init__.py
│ ├── routes_hotspot.py # 热点排行相关路由
│ ├── routes_sector.py # 板块联动相关路由
│ ├── routes_alert.py # 告警相关路由
│ ├── routes_eval.py # 评估数据路由
│ └── ws.py # WebSocket 管理
├── storage/ # 存储层
│ ├── __init__.py
│ ├── redis_store.py # Redis 读写
│ ├── duckdb_store.py # DuckDB 历史数据
│ └── cache.py # 内存缓存(LRU / 时序环形缓冲)
├── common/ # 公共工具
│ ├── __init__.py
│ ├── trading_calendar.py # 交易日历
│ ├── symbol_utils.py # 代码转换(SH600519 ↔ 600519 等)
│ └── logger.py # 统一日志
├── frontend/
│ └── hotspot_dashboard.html # 实时热点监控面板
├── tests/
│ ├── test_feature_engine.py
│ ├── test_hotscore.py
│ └── test_collector.py
├── docs/
│ ├── 热点侦测系统.pdf # 原始规划书
│ └── 技术方案.md # 本文档
├── main.py # 启动入口
└── requirements.txt
1.5 技术栈总览
| 类别 | 选型 | 版本要求 | 说明 |
|---|---|---|---|
| 语言 | Python | ≥3.10 | asyncio 原生支持 |
| Web 框架 | FastAPI | ≥0.100 | 异步、WebSocket 原生支持 |
| 行情数据 | pytdx | ≥1.72 | 逐笔成交 + 盘口(通达信协议) |
| 板块/涨停 | akshare | ≥1.12 | 东方财富数据源 |
| 实时存储 | Redis | ≥7.0 | Sorted Set + Stream |
| 历史存储 | DuckDB | ≥0.10 | 列式分析,复用现有基础设施 |
| 关系存储 | MySQL | ≥8.0 | 配置、映射表,复用现有库 |
| 定时调度 | APScheduler | ≥3.10 | 多频率任务调度 |
| 数据处理 | pandas + numpy | pandas≥2.0 | 滚动窗口特征计算 |
| ML 模型 | LightGBM | ≥4.0 | 盘中实时推理,速度优于 XGBoost |
| 前端 | 原生 HTML/JS | — | 与 stock_pool_app 风格统一 |
二、数据采集层详细设计
2.1 采集器基类
所有采集器继承统一基类,确保一致的生命周期管理、错误处理和监控。
class BaseCollector(ABC):
"""采集器基类"""
def __init__(self, name: str, interval_seconds: float):
self.name = name
self.interval_seconds = interval_seconds
self.is_running = False
self.stats = CollectorStats() # 采集统计
@abstractmethod
async def connect(self) -> None: ...
@abstractmethod
async def disconnect(self) -> None: ...
@abstractmethod
async def fetch_once(self) -> list[dict]: ...
async def run_loop(self):
"""主循环:fetch → publish → sleep"""
while self.is_running:
try:
t0 = time.monotonic()
records = await self.fetch_once()
await self.publish(records)
self.stats.record_success(len(records), time.monotonic() - t0)
except Exception as e:
self.stats.record_error(e)
await self.handle_error(e)
await asyncio.sleep(self.interval_seconds)
async def publish(self, records: list[dict]):
"""发布到 DataBus(内存队列 + 可选 Redis Stream)"""
await DataBus.publish(self.name, records)
2.2 逐笔成交采集器 (TickCollector)
数据源:pytdx 的 get_transaction_data 接口。
采集策略:
初始化阶段(开盘前):
1. 从 Redis/配置 加载今日关注列表(默认:昨日成交额 Top300 + 股票池标的)
2. 建立 pytdx 连接池(3~5 个连接)
盘中循环(每 3 秒):
1. 将关注列表分片,分配给连接池中各连接并发拉取
2. 对每只股票记录上次拉取的最后一条索引,增量拉取新成交
3. 原始逐笔 → 解析方向(B/S)→ 发布到 DataBus
盘中动态扩展(每 1 分钟):
1. 检查是否有新股票触发"粗筛条件"(量比>2 或涨幅>3%)
2. 将新标的加入关注列表
逐笔数据结构:
@dataclass
class TickRecord:
symbol: str # 股票代码 (如 "600519")
market: int # 市场 (0=深圳, 1=上海)
timestamp: datetime # 成交时间
price: float # 成交价
volume: int # 成交量(手)
amount: float # 成交额(元)
direction: int # 方向:1=主动买, -1=主动卖, 0=中性
order_kind: str # 订单类型
连接池管理:
class TdxConnectionPool:
"""pytdx 连接池,支持自动重连和服务器切换"""
def __init__(self, pool_size: int = 5):
self.pool_size = pool_size
self.connections: list[TdxHq_API] = []
self.server_list = [
("119.147.212.81", 7709),
("14.17.75.71", 7709),
("14.215.128.18", 7709),
("59.173.18.140", 7709),
("218.75.126.9", 7709),
]
self.current_server_idx = 0
async def get_connection(self) -> TdxHq_API:
"""从池中获取可用连接,不可用时自动重建"""
...
async def execute_batch(
self, symbols: list[str], fetch_fn: Callable
) -> dict[str, Any]:
"""将 symbols 分片,分配给各连接并发执行"""
chunk_size = math.ceil(len(symbols) / self.pool_size)
chunks = [symbols[i:i+chunk_size]
for i in range(0, len(symbols), chunk_size)]
tasks = [
self._fetch_chunk(conn, chunk, fetch_fn)
for conn, chunk in zip(self.connections, chunks)
]
results = await asyncio.gather(*tasks, return_exceptions=True)
return self._merge_results(results)
pytdx 同步转异步的处理:
pytdx 本身是同步阻塞的,需要用 asyncio.to_thread 或 loop.run_in_executor 包装:
async def fetch_once(self) -> list[TickRecord]:
raw_data = await asyncio.to_thread(
self._sync_fetch_all, self.watch_list
)
return self._parse_ticks(raw_data)
def _sync_fetch_all(self, symbols: list[str]) -> dict:
"""同步批量拉取(在线程池中执行)"""
result = {}
for symbol in symbols:
market = 1 if symbol.startswith("6") else 0
data = self.api.get_transaction_data(
market, symbol, start=self.last_index.get(symbol, 0), count=1000
)
if data:
result[symbol] = data
self.last_index[symbol] = (
self.last_index.get(symbol, 0) + len(data)
)
return result
2.3 五档盘口采集器 (OrderBookCollector)
数据源:pytdx 的 get_security_quotes 接口(单次最多可查 80 只)。
采集策略:
每 3 秒:
1. 将关注列表按 80 只一组分批
2. 并发拉取各批次五档行情
3. 解析买1~买5、卖1~卖5 的价格和挂单量
4. 计算快照级衍生字段(买卖盘总量、中间价等)
5. 发布到 DataBus
连续快照差分(每分钟聚合):
1. 对比相邻两个快照的挂单量变化,估算撤单量
2. 统计买盘增量 vs 卖盘增量
盘口数据结构:
@dataclass
class OrderBookSnapshot:
symbol: str
timestamp: datetime
last_price: float # 最新价
bid_prices: list[float] # 买1~买5价格
bid_volumes: list[int] # 买1~买5挂单量(手)
ask_prices: list[float] # 卖1~卖5价格
ask_volumes: list[int] # 卖1~卖5挂单量(手)
total_volume: int # 当日总成交量
total_amount: float # 当日总成交额
open_price: float
high_price: float
low_price: float
pre_close: float
@property
def bid_total(self) -> int:
return sum(self.bid_volumes)
@property
def ask_total(self) -> int:
return sum(self.ask_volumes)
@property
def bid_ask_imbalance(self) -> float:
"""买卖盘失衡度:>1 买方强势,<1 卖方强势"""
if self.ask_total == 0:
return float('inf')
return self.bid_total / self.ask_total
2.4 板块行情采集器 (SectorCollector)
数据源:akshare 的 stock_board_concept_name_em(概念板块)和 stock_board_industry_name_em(行业板块)。
采集策略:
盘前(09:15):
1. 拉取全量板块列表,更新板块-个股映射缓存
每 30 秒:
1. 拉取概念板块实时涨幅排行
2. 拉取行业板块实时涨幅排行
3. 发布板块级聚合数据到 DataBus
每 1 分钟:
1. 拉取涨停数据(东方财富涨停板接口)
2. 统计各板块涨停家数、炸板家数
板块数据结构:
@dataclass
class SectorSnapshot:
sector_name: str # 板块名称
sector_code: str # 板块代码
sector_type: str # "concept" | "industry"
change_pct: float # 涨跌幅 %
total_amount: float # 板块总成交额(亿)
up_count: int # 上涨家数
down_count: int # 下跌家数
limit_up_count: int # 涨停家数
limit_down_count: int # 跌停家数
leader_symbol: str # 领涨股代码
leader_change_pct: float # 领涨股涨幅
timestamp: datetime
2.5 涨停/炸板采集器 (LimitUpCollector)
数据源:akshare 的 stock_zt_pool_em(涨停池)、stock_zt_pool_zbgc_em(炸板池)。
@dataclass
class LimitUpRecord:
symbol: str
name: str
limit_up_time: str # 首次封板时间 "09:35:21"
last_seal_time: str # 最新封板时间
open_count: int # 打开次数(炸板次数)
seal_amount: float # 封单金额(万)
seal_ratio: float # 封单占流通市值比
continuous_days: int # 连板天数
sectors: list[str] # 所属概念板块
timestamp: datetime
2.6 采集调度管理器 (CollectorManager)
统一管理所有采集器的生命周期,根据交易时段自动启停。
class CollectorManager:
"""采集调度管理器"""
def __init__(self, config: dict):
self.collectors: dict[str, BaseCollector] = {}
self.config = config
def register(self, collector: BaseCollector):
self.collectors[collector.name] = collector
async def start_all(self):
"""根据交易日历启动采集"""
if not is_trading_day(date.today()):
logger.info("非交易日,跳过采集")
return
for collector in self.collectors.values():
await collector.connect()
# 按各自频率启动采集循环
tasks = [
asyncio.create_task(c.run_loop())
for c in self.collectors.values()
]
# 等待交易时段结束
await self._wait_until_market_close()
for c in self.collectors.values():
c.is_running = False
await asyncio.gather(*tasks)
# 收盘后持久化当日数据
await self._persist_daily_data()
async def _wait_until_market_close(self):
"""等待至 15:05(留 5 分钟缓冲收尾数据)"""
...
交易时段控制:
09:15 - 09:25 集合竞价阶段:仅采集盘口,不采集逐笔
09:25 - 09:30 等待开盘
09:30 - 11:30 上午连续竞价:全采集器启动
11:30 - 13:00 午休:暂停采集
13:00 - 15:00 下午连续竞价:全采集器启动
15:00 - 15:05 收盘缓冲:完成最后一批数据落盘
2.7 DataBus(数据总线)
内存中的发布-订阅总线,解耦采集器与计算引擎。
class DataBus:
"""进程内数据总线,基于 asyncio.Queue"""
_channels: dict[str, list[asyncio.Queue]] = {}
@classmethod
def subscribe(cls, channel: str) -> asyncio.Queue:
queue = asyncio.Queue(maxsize=10000)
cls._channels.setdefault(channel, []).append(queue)
return queue
@classmethod
async def publish(cls, channel: str, records: list[dict]):
for queue in cls._channels.get(channel, []):
try:
queue.put_nowait(records)
except asyncio.QueueFull:
# 消费者太慢,丢弃最旧的数据
queue.get_nowait()
queue.put_nowait(records)
@classmethod
async def publish_to_redis(cls, channel: str, records: list[dict]):
"""可选:同时写入 Redis Stream 做持久化和跨进程共享"""
...
频道定义:
| 频道名 | 生产者 | 数据频率 | 消费者 |
|---|---|---|---|
tick |
TickCollector | ~3秒 | FeatureEngine |
orderbook |
OrderBookCollector | ~3秒 | FeatureEngine |
sector |
SectorCollector | ~30秒 | SectorEngine |
limitup |
LimitUpCollector | ~1分钟 | SectorEngine, AlertEngine |
2.8 采集监控指标
每个采集器实时维护以下统计:
@dataclass
class CollectorStats:
total_fetches: int = 0 # 总采集次数
total_records: int = 0 # 总记录数
error_count: int = 0 # 错误次数
last_fetch_time: float = 0 # 上次采集耗时(秒)
avg_fetch_time: float = 0 # 平均采集耗时
last_error: str = "" # 最近一次错误信息
last_success_at: datetime = None
reconnect_count: int = 0 # 重连次数
通过 GET /api/collector/stats 暴露为监控端点。
三、特征计算引擎详细设计
3.1 整体架构
FeatureEngine 订阅 DataBus 的 tick 和 orderbook 频道,维护每只股票的时序缓冲区,实时计算 4 大类特征。
DataBus (tick/orderbook)
│
▼
┌─────────────────────────────────────────┐
│ FeatureEngine │
│ │
│ ┌──────────────────────────────┐ │
│ │ TimeSeriesBuffer (per stock)│ │
│ │ - tick_buffer (环形, 保留30分钟)│ │
│ │ - ob_buffer (环形, 保留30分钟) │ │
│ │ - minute_bars (当日分钟K) │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌──────────────┴───────────────┐ │
│ │ 特征计算管道 │ │
│ │ 1. 成交特征 (VolumeFeatures) │ │
│ │ 2. 资金主动性 (FlowFeatures) │ │
│ │ 3. 盘口结构 (OrderBookFeats) │ │
│ │ 4. 联动特征 (LinkageFeatures) │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌──────────────┴───────────────┐ │
│ │ FeatureSnapshot (per stock) │ │
│ │ → 发布到 DataBus "features" │ │
│ │ → 写入 Redis (实时查询) │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────┘
3.2 时序缓冲区
为每只股票维护高效的环形缓冲区,避免无限增长:
class TimeSeriesBuffer:
"""单只股票的时序数据缓冲区"""
def __init__(self, symbol: str, max_minutes: int = 30):
self.symbol = symbol
self.max_minutes = max_minutes
# 逐笔成交缓冲(原始粒度,保留最近 max_minutes 分钟)
self.ticks: deque[TickRecord] = deque()
# 盘口快照缓冲(每 3 秒一条)
self.orderbooks: deque[OrderBookSnapshot] = deque()
# 分钟 K 线(当日全部保留)
self.minute_bars: list[MinuteBar] = []
# 预聚合:每分钟统计值(减少重复计算)
self.minute_agg: dict[str, MinuteAggregate] = {}
def append_tick(self, tick: TickRecord):
self.ticks.append(tick)
self._evict_old(self.ticks)
self._update_minute_agg(tick)
def append_orderbook(self, ob: OrderBookSnapshot):
self.orderbooks.append(ob)
self._evict_old(self.orderbooks)
def _evict_old(self, buf: deque):
cutoff = datetime.now() - timedelta(minutes=self.max_minutes)
while buf and buf[0].timestamp < cutoff:
buf.popleft()
def get_recent_ticks(self, minutes: int) -> list[TickRecord]:
cutoff = datetime.now() - timedelta(minutes=minutes)
return [t for t in self.ticks if t.timestamp >= cutoff]
分钟预聚合结构(减少实时计算开销):
@dataclass
class MinuteAggregate:
"""每分钟预聚合统计"""
minute_key: str # "09:35"
total_volume: int = 0 # 总成交量(手)
total_amount: float = 0 # 总成交额(元)
buy_volume: int = 0 # 主动买成交量
sell_volume: int = 0 # 主动卖成交量
buy_amount: float = 0 # 主动买成交额
sell_amount: float = 0 # 主动卖成交额
trade_count: int = 0 # 成交笔数
large_order_amount: float = 0 # 大单成交额(单笔>50万)
max_price: float = 0
min_price: float = 0
vwap: float = 0 # 成交量加权均价
3.3 特征定义与计算
3.3.1 成交特征 (VolumeFeatures)
| 特征名 | 计算方式 | 窗口 | 说明 |
|---|---|---|---|
volume_surge_5m |
最近5分钟成交额 / 前5分钟成交额 | 10min | 5分钟成交额增速(倍数) |
volume_surge_1m |
最近1分钟成交额 / 前1分钟成交额 | 2min | 1分钟成交额增速 |
volume_ratio |
当前累计成交量 / (过去5日同时段平均成交量) | 当日 | 量比 |
turnover_rate |
当日成交量 / 流通股本 | 当日 | 换手率 % |
large_order_ratio |
大单成交额 / 总成交额 | 5min | 大单占比(阈值:单笔>50万元) |
trade_density |
成交笔数 / 时间窗口分钟数 | 5min | 成交密度 |
class VolumeFeatureCalculator:
LARGE_ORDER_THRESHOLD = 500_000 # 50万元
def calculate(self, buf: TimeSeriesBuffer) -> dict:
aggs = buf.minute_agg
now_minute = self._current_minute()
recent_5m = self._sum_amount(aggs, now_minute, 5)
prev_5m = self._sum_amount(aggs, now_minute - 5, 5)
recent_1m = self._sum_amount(aggs, now_minute, 1)
prev_1m = self._sum_amount(aggs, now_minute - 1, 1)
return {
"volume_surge_5m": recent_5m / max(prev_5m, 1),
"volume_surge_1m": recent_1m / max(prev_1m, 1),
"volume_ratio": self._calc_volume_ratio(buf),
"turnover_rate": self._calc_turnover(buf),
"large_order_ratio": self._calc_large_ratio(buf, minutes=5),
"trade_density": self._calc_density(buf, minutes=5),
}
3.3.2 资金主动性特征 (FlowFeatures)
| 特征名 | 计算方式 | 窗口 | 说明 |
|---|---|---|---|
active_buy_amount |
Σ(主动买成交额) | 5min | 主动买入总额 |
active_sell_amount |
Σ(主动卖成交额) | 5min | 主动卖出总额 |
net_active_buy |
主动买 - 主动卖 | 5min | 净主动买入额 |
active_buy_ratio |
主动买额 / 总成交额 | 5min | 主动买占比 |
consecutive_buy_count |
连续主动买单笔数 | 实时 | 当前连续买单计数 |
buy_momentum |
最近5分钟主动买占比 - 前5分钟主动买占比 | 10min | 买入动量变化 |
class FlowFeatureCalculator:
def calculate(self, buf: TimeSeriesBuffer) -> dict:
recent_ticks = buf.get_recent_ticks(minutes=5)
buy_amount = sum(t.amount for t in recent_ticks if t.direction == 1)
sell_amount = sum(t.amount for t in recent_ticks if t.direction == -1)
total = buy_amount + sell_amount
return {
"active_buy_amount": buy_amount,
"active_sell_amount": sell_amount,
"net_active_buy": buy_amount - sell_amount,
"active_buy_ratio": buy_amount / max(total, 1),
"consecutive_buy_count": self._count_consecutive_buys(buf),
"buy_momentum": self._calc_momentum(buf),
}
def _count_consecutive_buys(self, buf: TimeSeriesBuffer) -> int:
"""从最新一笔往回数,连续主动买的笔数"""
count = 0
for tick in reversed(buf.ticks):
if tick.direction == 1:
count += 1
else:
break
return count
3.3.3 盘口结构特征 (OrderBookFeatures)
| 特征名 | 计算方式 | 说明 |
|---|---|---|
bid_ask_imbalance |
买盘总量 / 卖盘总量 | 买卖盘失衡度 |
seal_amount |
涨停价位的买一挂单金额 | 封单金额(仅涨停时有意义) |
seal_duration |
封单持续时间 | 封单稳定时间(分钟) |
cancel_rate |
(t-1挂单量 - t挂单量 + t成交量) / t-1挂单量 | 撤单率(快照差分估算) |
ask_decay_speed |
卖盘总量变化斜率(最近5个快照) | 卖压衰减速度 |
spread |
(卖一 - 买一) / 买一 | 买卖价差 |
depth_ratio |
买一量 / (买一量 + 卖一量) | 盘口深度比 |
class OrderBookFeatureCalculator:
def calculate(self, buf: TimeSeriesBuffer) -> dict:
if not buf.orderbooks:
return self._empty_features()
latest = buf.orderbooks[-1]
features = {
"bid_ask_imbalance": latest.bid_ask_imbalance,
"spread": self._calc_spread(latest),
"depth_ratio": self._calc_depth_ratio(latest),
}
# 需要连续快照的特征
if len(buf.orderbooks) >= 2:
prev = buf.orderbooks[-2]
features["cancel_rate"] = self._estimate_cancel_rate(prev, latest)
features["ask_decay_speed"] = self._calc_ask_decay(buf.orderbooks)
# 涨停相关
if self._is_at_limit_up(latest):
features["seal_amount"] = latest.bid_volumes[0] * latest.bid_prices[0]
features["seal_duration"] = self._calc_seal_duration(buf.orderbooks)
else:
features["seal_amount"] = 0
features["seal_duration"] = 0
return features
3.3.4 联动特征 (LinkageFeatures)
| 特征名 | 计算方式 | 说明 |
|---|---|---|
sector_change_pct |
所属板块涨跌幅 | 板块涨幅 |
sector_up_ratio |
板块内上涨家数 / 总家数 | 板块上涨比例 |
sector_limitup_count |
板块内涨停家数 | — |
sector_volume_surge |
板块成交额增速 | — |
market_sentiment |
全市场涨跌家数比 × 涨停家数权重 | 市场情绪指数 (0-100) |
sector_rank |
该股所属板块在全市场板块涨幅排名 | 板块强度排名 |
联动特征的计算依赖 SectorEngine 的输出,通过 DataBus 的 sector_features 频道获取。
class LinkageFeatureCalculator:
def __init__(self, sector_mapping: dict[str, list[str]]):
self.symbol_to_sectors = self._build_reverse_map(sector_mapping)
def calculate(self, symbol: str, sector_data: dict) -> dict:
sectors = self.symbol_to_sectors.get(symbol, [])
if not sectors:
return self._empty_features()
# 取该股所属的最强板块
best_sector = max(
sectors,
key=lambda s: sector_data.get(s, {}).get("change_pct", 0)
)
sd = sector_data.get(best_sector, {})
return {
"sector_change_pct": sd.get("change_pct", 0),
"sector_up_ratio": sd.get("up_ratio", 0),
"sector_limitup_count": sd.get("limitup_count", 0),
"sector_volume_surge": sd.get("volume_surge", 1),
"market_sentiment": sector_data.get("_market_sentiment", 50),
"sector_rank": sd.get("rank", 999),
}
3.4 特征快照与输出
每只股票的所有特征汇总为一个 FeatureSnapshot,每分钟更新一次:
@dataclass
class FeatureSnapshot:
symbol: str
timestamp: datetime
# 成交特征
volume_surge_5m: float
volume_surge_1m: float
volume_ratio: float
turnover_rate: float
large_order_ratio: float
trade_density: float
# 资金主动性
active_buy_amount: float
active_sell_amount: float
net_active_buy: float
active_buy_ratio: float
consecutive_buy_count: int
buy_momentum: float
# 盘口结构
bid_ask_imbalance: float
seal_amount: float
seal_duration: float
cancel_rate: float
ask_decay_speed: float
spread: float
depth_ratio: float
# 联动特征
sector_change_pct: float
sector_up_ratio: float
sector_limitup_count: int
sector_volume_surge: float
market_sentiment: float
sector_rank: int
def to_dict(self) -> dict:
return asdict(self)
def to_normalize_vector(self) -> np.ndarray:
"""转为归一化向量,供 HotScore / ML 使用"""
...
3.5 性能预算
| 步骤 | 目标耗时 | 说明 |
|---|---|---|
| 单只股票特征计算 | <2ms | 主要是分钟聚合查表 |
| 全部关注列表 (300只) | <600ms | 可并行 |
| 特征发布到 DataBus | <10ms | 内存操作 |
| 特征写入 Redis | <50ms | pipeline 批量写入 |
| 单轮总耗时 | <1秒 | 满足"每分钟计算一次"的要求 |
四、信号层(HotScore 引擎)详细设计
4.1 HotScore 公式
采用规划书推荐的加权评分公式,所有输入特征先做 0-100 截面归一化:
HotScore = w1 × Norm(成交额增速)
+ w2 × Norm(主动买占比)
+ w3 × Norm(板块联动强度)
+ w4 × Norm(换手率)
+ w5 × Norm(封单质量)
默认权重(可通过 hotscore_weights.yaml 热更新):
| 分量 | 权重 | 归一化输入 | 说明 |
|---|---|---|---|
| w1 成交额增速 | 0.35 | volume_surge_5m |
放量是热点最直接的信号 |
| w2 主动买占比 | 0.25 | active_buy_ratio |
区分真买还是对倒 |
| w3 板块联动强度 | 0.20 | sector_up_ratio × sector_change_pct |
独涨不如板块共振 |
| w4 换手率 | 0.10 | turnover_rate |
参与度 |
| w5 封单质量 | 0.10 | seal_amount × seal_duration |
涨停股的封单质量 |
4.2 截面归一化
对全市场同时刻的特征值做截面排名百分位,消除绝对值量纲差异:
class CrossSectionalNormalizer:
"""截面归一化:将特征转为 0-100 的百分位排名"""
def normalize(
self, feature_name: str, values: dict[str, float]
) -> dict[str, float]:
"""
values: {symbol: raw_value}
returns: {symbol: percentile_score (0-100)}
"""
sorted_items = sorted(values.items(), key=lambda x: x[1])
n = len(sorted_items)
result = {}
for rank, (symbol, _) in enumerate(sorted_items):
result[symbol] = (rank / max(n - 1, 1)) * 100
return result
4.3 HotScore 引擎核心
class HotScoreEngine:
def __init__(self, weights_path: str = "config/hotscore_weights.yaml"):
self.weights = self._load_weights(weights_path)
self.normalizer = CrossSectionalNormalizer()
self._last_reload_time = 0
def compute_all(
self, features: dict[str, FeatureSnapshot]
) -> dict[str, HotScoreResult]:
"""对全部股票计算 HotScore 并返回排序结果"""
self._maybe_reload_weights()
# 提取各维度原始值
raw = {
"volume_surge": {s: f.volume_surge_5m for s, f in features.items()},
"active_buy": {s: f.active_buy_ratio for s, f in features.items()},
"sector_linkage": {
s: f.sector_up_ratio * f.sector_change_pct
for s, f in features.items()
},
"turnover": {s: f.turnover_rate for s, f in features.items()},
"seal_quality": {
s: f.seal_amount * f.seal_duration
for s, f in features.items()
},
}
# 截面归一化
normed = {
dim: self.normalizer.normalize(dim, vals)
for dim, vals in raw.items()
}
# 加权求和
results = {}
for symbol in features:
score = (
self.weights["volume_surge"] * normed["volume_surge"][symbol]
+ self.weights["active_buy"] * normed["active_buy"][symbol]
+ self.weights["sector_linkage"] * normed["sector_linkage"][symbol]
+ self.weights["turnover"] * normed["turnover"][symbol]
+ self.weights["seal_quality"] * normed["seal_quality"][symbol]
)
results[symbol] = HotScoreResult(
symbol=symbol,
score=round(score, 2),
level=self._classify(score),
components={dim: normed[dim][symbol] for dim in normed},
timestamp=features[symbol].timestamp,
)
return results
@staticmethod
def _classify(score: float) -> str:
if score >= 85:
return "strong_hot" # 强热点 → 重点观察
elif score >= 70:
return "potential_hot" # 潜在热点 → 进入观察池
else:
return "normal" # 普通异动 → 暂不跟踪
HotScore 结果结构:
@dataclass
class HotScoreResult:
symbol: str
score: float # 0-100
level: str # "strong_hot" | "potential_hot" | "normal"
components: dict[str, float] # 各维度得分明细
timestamp: datetime
rank: int = 0 # 在全市场的排名(后续填充)
4.4 实时排行榜
class HotRanking:
"""实时热点排行榜,基于 Redis Sorted Set"""
REDIS_KEY = "hotspot:ranking:{date}"
TOP_N = 50
async def update(self, results: dict[str, HotScoreResult]):
"""更新排行榜"""
pipe = self.redis.pipeline()
key = self.REDIS_KEY.format(date=date.today().isoformat())
# 写入 Sorted Set
for symbol, r in results.items():
pipe.zadd(key, {symbol: r.score})
# 设置过期时间(当日有效)
pipe.expire(key, 86400)
await pipe.execute()
async def get_top(self, n: int = 50) -> list[dict]:
"""获取 Top N 热点股"""
key = self.REDIS_KEY.format(date=date.today().isoformat())
items = await self.redis.zrevrange(key, 0, n - 1, withscores=True)
return [{"symbol": s, "score": score, "rank": i + 1}
for i, (s, score) in enumerate(items)]
4.5 HotScore 分数变化追踪
追踪每只股票的 HotScore 历史,识别"急速升温"模式:
class HotScoreTracker:
"""追踪 HotScore 时序变化"""
def __init__(self):
self.history: dict[str, deque[tuple[datetime, float]]] = {}
def record(self, symbol: str, score: float, ts: datetime):
buf = self.history.setdefault(symbol, deque(maxlen=60))
buf.append((ts, score))
def score_delta(self, symbol: str, minutes: int = 5) -> float:
"""最近 N 分钟的分数变化"""
buf = self.history.get(symbol)
if not buf or len(buf) < 2:
return 0
cutoff = datetime.now() - timedelta(minutes=minutes)
old_scores = [s for t, s in buf if t <= cutoff]
if not old_scores:
return 0
return buf[-1][1] - old_scores[-1]
def is_surging(self, symbol: str, threshold: float = 20) -> bool:
"""5分钟内分数上升超过 threshold,视为"急速升温" """
return self.score_delta(symbol, minutes=5) > threshold
五、板块联动引擎详细设计
5.1 板块-个股映射管理
class SectorMapping:
"""板块-个股映射管理器"""
def __init__(self, config_path: str = "config/sectors.yaml"):
self.concept_map: dict[str, list[str]] = {} # 概念→股票列表
self.industry_map: dict[str, list[str]] = {} # 行业→股票列表
self.symbol_concepts: dict[str, list[str]] = {} # 股票→所属概念
self.symbol_industry: dict[str, str] = {} # 股票→所属行业
async def refresh_from_eastmoney(self):
"""从东方财富刷新板块成分股(每日开盘前执行)"""
# akshare: stock_board_concept_cons_em / stock_board_industry_cons_em
...
def get_sector_members(self, sector_name: str) -> list[str]:
"""获取板块成分股列表"""
...
def get_stock_sectors(self, symbol: str) -> list[str]:
"""获取股票所属的全部板块"""
...
5.2 板块级指标聚合
class SectorEngine:
"""板块联动分析引擎"""
def compute_sector_metrics(
self,
sector_name: str,
member_features: dict[str, FeatureSnapshot],
sector_snapshot: SectorSnapshot,
) -> SectorMetrics:
members = list(member_features.values())
up_count = sum(1 for m in members if m.volume_surge_5m > 1)
total = len(members)
return SectorMetrics(
sector_name=sector_name,
change_pct=sector_snapshot.change_pct,
up_ratio=up_count / max(total, 1),
avg_volume_surge=np.mean([m.volume_surge_5m for m in members]),
limitup_count=sector_snapshot.limit_up_count,
total_net_buy=sum(m.net_active_buy for m in members),
heat_score=self._calc_sector_heat(sector_snapshot, members),
leader=self._identify_leader(member_features),
timestamp=datetime.now(),
)
5.3 龙头股识别
龙头股定义为板块内"最先异动 + 资金最强 + 市场辨识度最高"的个股:
def _identify_leader(
self, member_features: dict[str, FeatureSnapshot]
) -> str | None:
"""识别板块龙头"""
if not member_features:
return None
candidates = []
for symbol, feat in member_features.items():
leader_score = (
0.4 * feat.volume_surge_5m # 放量程度
+ 0.3 * feat.active_buy_ratio # 资金强度
+ 0.2 * (feat.seal_amount > 0) # 是否涨停
+ 0.1 * feat.turnover_rate # 活跃度
)
candidates.append((symbol, leader_score))
candidates.sort(key=lambda x: x[1], reverse=True)
return candidates[0][0]
5.4 板块联动检测
def detect_sector_resonance(
self, sector_metrics: dict[str, SectorMetrics]
) -> list[ResonanceEvent]:
"""检测板块共振事件:多个相关板块同时异动"""
events = []
hot_sectors = [
(name, m) for name, m in sector_metrics.items()
if m.heat_score > 70
]
# 两两检查是否存在成分股重叠
for i, (name_a, m_a) in enumerate(hot_sectors):
for name_b, m_b in hot_sectors[i+1:]:
overlap = self._member_overlap(name_a, name_b)
if overlap > 0.3: # 成分股重叠度超过 30%
events.append(ResonanceEvent(
sectors=[name_a, name_b],
overlap_ratio=overlap,
combined_heat=(m_a.heat_score + m_b.heat_score) / 2,
timestamp=datetime.now(),
))
return events
六、AI 预测层详细设计
6.1 设计原则
- 先规则,再模型:HotScore 规则引擎是基础,AI 层在其上做增量预测
- 预测热度而非涨跌:标签是"未来 HotScore 是否提升"而非"涨停"
- 严防未来函数:标签构建代码中加时间戳断言
6.2 标签设计
class LabelBuilder:
"""构建训练标签,严格避免未来函数"""
# 推荐的 3 种标签
LABEL_CONFIGS = {
"hotscore_surge_10m": {
"desc": "未来10分钟 HotScore 是否 > 85",
"horizon_minutes": 10,
"target_fn": lambda future_scores: max(future_scores) > 85,
},
"top20_15m": {
"desc": "未来15分钟是否进入成交额 Top20",
"horizon_minutes": 15,
"target_fn": lambda future_ranks: min(future_ranks) <= 20,
},
"sector_leader_20m": {
"desc": "未来20分钟是否成为板块龙头",
"horizon_minutes": 20,
"target_fn": lambda future_is_leader: any(future_is_leader),
},
}
def build_label(
self,
feature_time: datetime,
label_config: str,
historical_data: pd.DataFrame,
) -> int:
"""
feature_time: 特征计算时间点 T
返回: 1(正样本)/ 0(负样本)
断言保障:只使用 T 之后的数据构建标签
"""
cfg = self.LABEL_CONFIGS[label_config]
horizon = cfg["horizon_minutes"]
future_start = feature_time + timedelta(minutes=1)
future_end = feature_time + timedelta(minutes=horizon)
# 严格断言:标签数据的时间范围
future_data = historical_data[
(historical_data["timestamp"] >= future_start) &
(historical_data["timestamp"] <= future_end)
]
assert future_data["timestamp"].min() > feature_time, \
f"未来函数泄漏!标签数据时间 {future_data['timestamp'].min()} <= 特征时间 {feature_time}"
if future_data.empty:
return -1 # 数据不足,标记为无效
return int(cfg["target_fn"](future_data["value"].tolist()))
6.3 特征工程(用于模型训练的扩展特征)
在实时特征基础上增加时序衍生特征:
class ModelFeatureBuilder:
"""为 ML 模型构建扩展特征"""
def build(self, snapshot: FeatureSnapshot, history: list[FeatureSnapshot]) -> dict:
base = snapshot.to_dict()
# 时序衍生特征
if len(history) >= 5:
scores_5 = [h.volume_surge_5m for h in history[-5:]]
base["volume_surge_trend_5m"] = np.polyfit(range(5), scores_5, 1)[0]
base["volume_surge_std_5m"] = np.std(scores_5)
buy_ratios = [h.active_buy_ratio for h in history[-5:]]
base["buy_ratio_trend_5m"] = np.polyfit(range(5), buy_ratios, 1)[0]
if len(history) >= 10:
scores_10 = [h.volume_surge_5m for h in history[-10:]]
base["volume_surge_trend_10m"] = np.polyfit(range(10), scores_10, 1)[0]
base["volume_surge_max_10m"] = max(scores_10)
base["volume_surge_min_10m"] = min(scores_10)
# 时段特征
minutes_from_open = self._minutes_from_open(snapshot.timestamp)
base["minutes_from_open"] = minutes_from_open
base["is_morning"] = int(minutes_from_open <= 120)
base["is_last_30min"] = int(minutes_from_open >= 210)
# 当前 HotScore(如果有)
base["current_hotscore"] = getattr(snapshot, "hotscore", 0)
return base
6.4 模型训练
class HotspotTrainer:
"""离线模型训练"""
def __init__(self, label_config: str = "hotscore_surge_10m"):
self.label_config = label_config
self.feature_builder = ModelFeatureBuilder()
self.label_builder = LabelBuilder()
def prepare_dataset(
self, start_date: str, end_date: str
) -> tuple[pd.DataFrame, pd.Series]:
"""从 DuckDB 历史数据构建训练集"""
# 1. 加载历史特征快照
features_df = self.storage.load_features(start_date, end_date)
# 2. 构建标签
labels = []
for _, row in features_df.iterrows():
label = self.label_builder.build_label(
feature_time=row["timestamp"],
label_config=self.label_config,
historical_data=features_df,
)
labels.append(label)
features_df["label"] = labels
# 3. 过滤无效标签
valid = features_df[features_df["label"] >= 0]
X = valid.drop(columns=["label", "timestamp", "symbol"])
y = valid["label"]
return X, y
def train(self, X: pd.DataFrame, y: pd.Series) -> lgb.Booster:
"""训练 LightGBM 模型"""
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, shuffle=False # 时序数据不 shuffle
)
train_data = lgb.Dataset(X_train, label=y_train)
val_data = lgb.Dataset(X_val, label=y_val, reference=train_data)
params = {
"objective": "binary",
"metric": ["auc", "binary_logloss"],
"learning_rate": 0.05,
"num_leaves": 31,
"max_depth": 6,
"min_child_samples": 50,
"feature_fraction": 0.8,
"bagging_fraction": 0.8,
"bagging_freq": 5,
"verbose": -1,
}
model = lgb.train(
params,
train_data,
num_boost_round=500,
valid_sets=[val_data],
callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)],
)
model.save_model(f"model/saved/{self.label_config}_latest.txt")
return model
6.5 在线推理
class HotspotPredictor:
"""盘中在线推理"""
def __init__(self, model_path: str):
self.model = lgb.Booster(model_file=model_path)
self.feature_builder = ModelFeatureBuilder()
def predict_batch(
self,
current_features: dict[str, FeatureSnapshot],
history: dict[str, list[FeatureSnapshot]],
) -> dict[str, PredictionResult]:
"""批量预测"""
rows = []
symbols = []
for symbol, snap in current_features.items():
hist = history.get(symbol, [])
row = self.feature_builder.build(snap, hist)
rows.append(row)
symbols.append(symbol)
if not rows:
return {}
X = pd.DataFrame(rows)
probs = self.model.predict(X)
results = {}
for symbol, prob in zip(symbols, probs):
results[symbol] = PredictionResult(
symbol=symbol,
probability=round(float(prob), 4),
prediction=int(prob > 0.5),
confidence=round(abs(prob - 0.5) * 2, 4),
timestamp=datetime.now(),
)
return results
@dataclass
class PredictionResult:
symbol: str
probability: float # 0-1,成为热点的概率
prediction: int # 0 或 1
confidence: float # 0-1,置信度
timestamp: datetime
6.6 模型更新策略
每日收盘后(15:30):
1. 将当日盘中数据持久化到 DuckDB
2. 用最近 N 个交易日数据增量训练模型
3. 在最近 3 个交易日数据上验证 AUC,不低于阈值才替换线上模型
每周末:
1. 用全量历史数据重新训练
2. 生成评估报告(特征重要性、标签分布、时段表现等)
七、告警引擎详细设计
7.1 告警规则
class AlertEngine:
"""异动告警引擎"""
ALERT_RULES = [
{
"name": "hot_surge",
"desc": "HotScore 急速升温",
"condition": lambda ctx: ctx["score_delta_5m"] > 25,
"level": "high",
},
{
"name": "volume_explosion",
"desc": "成交额爆发",
"condition": lambda ctx: ctx["volume_surge_5m"] > 5,
"level": "high",
},
{
"name": "sector_resonance",
"desc": "板块共振启动",
"condition": lambda ctx: ctx["sector_heat"] > 80 and ctx["sector_limitup"] >= 3,
"level": "medium",
},
{
"name": "seal_break",
"desc": "涨停板炸板",
"condition": lambda ctx: ctx["was_sealed"] and not ctx["is_sealed"],
"level": "medium",
},
{
"name": "continuous_buy",
"desc": "连续大单买入",
"condition": lambda ctx: ctx["consecutive_buy_count"] > 20 and ctx["active_buy_ratio"] > 0.75,
"level": "low",
},
]
async def check(
self,
features: dict[str, FeatureSnapshot],
scores: dict[str, HotScoreResult],
tracker: HotScoreTracker,
) -> list[Alert]:
alerts = []
for symbol, feat in features.items():
ctx = self._build_context(symbol, feat, scores, tracker)
for rule in self.ALERT_RULES:
if rule["condition"](ctx):
alerts.append(Alert(
symbol=symbol,
rule_name=rule["name"],
description=rule["desc"],
level=rule["level"],
context=ctx,
timestamp=datetime.now(),
))
return alerts
7.2 告警去重与限流
class AlertThrottler:
"""告警去重:同一股票同一规则 N 分钟内不重复告警"""
def __init__(self, cooldown_minutes: int = 5):
self.cooldown = timedelta(minutes=cooldown_minutes)
self.last_alert: dict[str, datetime] = {} # "symbol:rule" → last_time
def should_fire(self, alert: Alert) -> bool:
key = f"{alert.symbol}:{alert.rule_name}"
last = self.last_alert.get(key)
if last and datetime.now() - last < self.cooldown:
return False
self.last_alert[key] = datetime.now()
return True
八、评估层详细设计(核心模块)
设计哲学:Evaluation 比 AI 更重要。
HotScore 的权重是"拍脑袋"定的,AI 模型再花哨也只是锦上添花。
如果没有一套严格的评估体系,我们永远不知道 HotScore 到底准不准。
评估层是整个系统的审判官——它决定这个系统有没有价值、权重该怎么调、
什么时候可以上 AI、什么时候该推倒重来。
8.1 核心问题:HotScore 到底准不准?
回答这个问题的方式只有一个:用未来的事实说话。
每一分钟 → 记录 HotScore Top 20
│
▼
等 10 分钟
│
▼
回头统计这 20 只:
├─ 上涨了几只?
├─ 平均涨了多少?
├─ 几只涨停了?
├─ 几只进了龙虎榜?
├─ 几只进了成交额 Top 20?
└─ Precision / Recall 各多少?
这就是评估层的核心循环。每分钟做一次快照,N 分钟后用事实回填。
不依赖任何主观判断,只看数字。
8.2 评估数据流
T 时刻 T+N 时刻
┌──────────────────────────┐ ┌──────────────────────────┐
│ HotScore 快照 (Snapshot) │ │ 市场事实 (Outcome) │
│ │ │ │
│ Top20 股票列表 │ ───────→ │ 这 20 只的真实表现 │
│ 每只的 HotScore 分数 │ 等 N 分钟 │ 涨跌 / 涨停 / 龙虎榜 │
│ 每只的特征分量 │ │ 成交额排名 / 板块地位 │
│ 当前市场状态 │ │ │
└──────────────────────────┘ └──────────────────────────┘
│ │
└────────────────┬─────────────────────────┘
▼
┌─────────────────────┐
│ EvalRecord 评估记录 │
│ 快照 + 结果 = 一条 │
│ 完整的评估样本 │
└──────────┬──────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
实时仪表盘 每日报告 权重优化输入
8.3 数据结构定义
8.3.1 快照记录(T 时刻拍摄)
@dataclass
class EvalSnapshot:
"""每分钟的 HotScore 快照,用于未来回填评估"""
snapshot_id: str # "{date}_{HHmm}" 如 "2026-07-14_0935"
timestamp: datetime # 快照时间
market_state: MarketState # 当时市场状态
# Top N 列表(默认 Top20,可配置)
top_symbols: list[str] # 进入 Top N 的股票代码
top_scores: dict[str, float] # {symbol: hotscore}
top_components: dict[str, dict] # {symbol: {维度: 分量得分}}
# 全市场基线
total_scored: int # 参与评分的股票总数
score_mean: float # 全市场 HotScore 均值
score_std: float # 全市场 HotScore 标准差
score_threshold_85_count: int # HotScore ≥ 85 的股票数
score_threshold_70_count: int # HotScore ≥ 70 的股票数
@dataclass
class MarketState:
"""快照时刻的市场状态(用于分段分析)"""
minutes_from_open: int # 距开盘几分钟
session: str # "morning" | "afternoon"
market_up_ratio: float # 全市场上涨比例
market_limit_up_count: int # 全市场涨停家数
market_sentiment: float # 情绪指数 0-100
8.3.2 结果回填(T+N 时刻回填)
@dataclass
class OutcomeRecord:
"""针对快照中每只股票,N 分钟后的真实结果"""
symbol: str
snapshot_id: str # 关联到哪个快照
hotscore_at_snapshot: float # 快照时的 HotScore
# ---- 价格表现 ----
price_at_snapshot: float # 快照时价格
price_after_5m: float # 5 分钟后价格
price_after_10m: float # 10 分钟后价格
price_after_15m: float # 15 分钟后价格
price_after_30m: float # 30 分钟后价格
return_5m: float # 5 分钟收益率 %
return_10m: float # 10 分钟收益率 %
return_15m: float # 15 分钟收益率 %
return_30m: float # 30 分钟收益率 %
max_return_30m: float # 30 分钟内最高收益 %
max_drawdown_30m: float # 30 分钟内最大回撤 %
# ---- 事件标记(bool)----
went_up_10m: bool # 10 分钟后是否上涨
hit_limit_up_30m: bool # 30 分钟内是否涨停
entered_amount_top20_30m: bool # 30 分钟内是否进入成交额 Top20
entered_longhu_today: bool # 当日是否进入龙虎榜
became_sector_leader_30m: bool # 30 分钟内是否成为板块龙头
# ---- HotScore 后续变化 ----
hotscore_after_10m: float # 10 分钟后 HotScore
hotscore_delta_10m: float # HotScore 变化量
stayed_in_top20_10m: bool # 10 分钟后是否仍在 Top20
8.3.3 评估记录(汇总一个快照的整体表现)
@dataclass
class EvalRecord:
"""一个快照的评估汇总(快照 + 全部结果 → 一条评估记录)"""
snapshot_id: str
timestamp: datetime
horizon_minutes: int # 评估时间窗口(5/10/15/30)
top_n: int # Top N 大小(默认 20)
# ---- 涨跌统计 ----
up_count: int # 上涨股票数
down_count: int # 下跌股票数
flat_count: int # 平盘股票数
up_ratio: float # 上涨比例 = up_count / top_n
# ---- 收益统计 ----
avg_return: float # 平均收益率 %
median_return: float # 中位数收益率 %
max_return: float # 最大收益率 %
min_return: float # 最小收益率 %
return_std: float # 收益率标准差
positive_avg_return: float # 上涨股票的平均涨幅 %
negative_avg_return: float # 下跌股票的平均跌幅 %
# ---- 事件统计 ----
limit_up_count: int # 涨停数量
amount_top20_count: int # 进入成交额 Top20 的数量
longhu_count: int # 进入龙虎榜的数量
sector_leader_count: int # 成为板块龙头的数量
# ---- Precision / Recall ----
precision_up: float # 精确率(选出的里面,上涨的比例)
recall_up: float # 召回率(所有上涨的里面,被选出的比例)
f1_up: float # F1 分数
precision_limit_up: float # 涨停精确率
recall_limit_up: float # 涨停召回率
# ---- 对比基线 ----
baseline_avg_return: float # 基线(全市场平均收益)
excess_return: float # 超额收益 = avg_return - baseline
baseline_up_ratio: float # 基线上涨比例
excess_up_ratio: float # 超额上涨比例
# ---- 市场状态 ----
market_state: MarketState
8.4 评估引擎核心逻辑
class EvalEngine:
"""评估引擎 —— 系统的审判官"""
HORIZONS = [5, 10, 15, 30] # 评估时间窗口(分钟)
TOP_N_LIST = [10, 20, 50] # 不同 Top N 粒度
def __init__(self, storage: "EvalStorage"):
self.storage = storage
# 待回填的快照队列:{snapshot_id: (EvalSnapshot, scheduled_time)}
self.pending_snapshots: dict[str, tuple[EvalSnapshot, datetime]] = {}
# =====================================================
# 第一步:每分钟拍快照
# =====================================================
async def take_snapshot(
self,
scores: dict[str, HotScoreResult],
features: dict[str, FeatureSnapshot],
market_state: MarketState,
):
"""
每分钟由主循环调用。
拍下当前 HotScore 排行,存入待回填队列。
"""
now = datetime.now()
snapshot_id = now.strftime("%Y-%m-%d_%H%M")
sorted_scores = sorted(
scores.values(), key=lambda r: r.score, reverse=True
)
for top_n in self.TOP_N_LIST:
top_items = sorted_scores[:top_n]
snapshot = EvalSnapshot(
snapshot_id=f"{snapshot_id}_top{top_n}",
timestamp=now,
market_state=market_state,
top_symbols=[r.symbol for r in top_items],
top_scores={r.symbol: r.score for r in top_items},
top_components={r.symbol: r.components for r in top_items},
total_scored=len(scores),
score_mean=np.mean([r.score for r in scores.values()]),
score_std=np.std([r.score for r in scores.values()]),
score_threshold_85_count=sum(
1 for r in scores.values() if r.score >= 85
),
score_threshold_70_count=sum(
1 for r in scores.values() if r.score >= 70
),
)
# 持久化快照
await self.storage.save_snapshot(snapshot)
# 对每个时间窗口,注册回填任务
for horizon in self.HORIZONS:
backfill_time = now + timedelta(minutes=horizon)
task_id = f"{snapshot.snapshot_id}_h{horizon}"
self.pending_snapshots[task_id] = (snapshot, backfill_time)
# =====================================================
# 第二步:N 分钟后回填结果
# =====================================================
async def backfill_outcomes(
self,
current_prices: dict[str, float],
current_amounts: dict[str, float],
current_scores: dict[str, HotScoreResult],
limit_up_set: set[str],
longhu_set: set[str],
sector_leaders: dict[str, str],
amount_top20: set[str],
):
"""
每分钟由主循环调用。
检查哪些快照已到回填时间,回填真实结果并生成 EvalRecord。
"""
now = datetime.now()
completed = []
for task_id, (snapshot, backfill_time) in self.pending_snapshots.items():
if now < backfill_time:
continue
horizon = int(task_id.split("_h")[-1])
# 为快照中的每只股票计算 OutcomeRecord
outcomes = []
for symbol in snapshot.top_symbols:
outcome = self._build_outcome(
symbol=symbol,
snapshot=snapshot,
horizon=horizon,
current_price=current_prices.get(symbol),
current_amount=current_amounts.get(symbol, 0),
current_score=current_scores.get(symbol),
is_limit_up=symbol in limit_up_set,
is_longhu=symbol in longhu_set,
is_sector_leader=symbol in [
v for v in sector_leaders.values()
],
is_amount_top20=symbol in amount_top20,
)
outcomes.append(outcome)
# 汇总为 EvalRecord
eval_record = self._aggregate_eval(
snapshot, outcomes, horizon, current_prices
)
await self.storage.save_eval_record(eval_record)
await self.storage.save_outcomes(outcomes)
completed.append(task_id)
# 实时推送评估结果到前端
await self._broadcast_eval(eval_record)
for task_id in completed:
del self.pending_snapshots[task_id]
# =====================================================
# 第三步:汇总计算
# =====================================================
def _aggregate_eval(
self,
snapshot: EvalSnapshot,
outcomes: list[OutcomeRecord],
horizon: int,
all_prices: dict[str, float],
) -> EvalRecord:
"""将 N 个 OutcomeRecord 汇总为一条 EvalRecord"""
returns = [self._get_return(o, horizon) for o in outcomes]
top_n = len(outcomes)
up_count = sum(1 for r in returns if r > 0)
down_count = sum(1 for r in returns if r < 0)
flat_count = top_n - up_count - down_count
limit_up_count = sum(1 for o in outcomes if o.hit_limit_up_30m)
amount_top20_count = sum(
1 for o in outcomes if o.entered_amount_top20_30m
)
longhu_count = sum(1 for o in outcomes if o.entered_longhu_today)
sector_leader_count = sum(
1 for o in outcomes if o.became_sector_leader_30m
)
# ---- Precision / Recall ----
precision_up = up_count / max(top_n, 1)
# Recall: 全市场同期上涨股票中,被 Top N 选中的比例
# 需要全市场涨跌数据
market_up_symbols = self._get_market_up_symbols(all_prices, horizon)
recall_up = (
up_count / max(len(market_up_symbols), 1)
if market_up_symbols else 0
)
f1_up = (
2 * precision_up * recall_up / max(precision_up + recall_up, 1e-9)
)
# 涨停的 Precision / Recall
total_limit_up = len(self._get_market_limit_up(horizon))
precision_limit_up = limit_up_count / max(top_n, 1)
recall_limit_up = (
limit_up_count / max(total_limit_up, 1)
if total_limit_up else 0
)
# ---- 基线对比 ----
baseline_returns = self._get_market_avg_return(all_prices, horizon)
baseline_avg = np.mean(baseline_returns) if baseline_returns else 0
baseline_up_ratio = (
sum(1 for r in baseline_returns if r > 0)
/ max(len(baseline_returns), 1)
)
avg_return = np.mean(returns) if returns else 0
positive_returns = [r for r in returns if r > 0]
negative_returns = [r for r in returns if r < 0]
return EvalRecord(
snapshot_id=snapshot.snapshot_id,
timestamp=snapshot.timestamp,
horizon_minutes=horizon,
top_n=top_n,
up_count=up_count,
down_count=down_count,
flat_count=flat_count,
up_ratio=up_count / max(top_n, 1),
avg_return=avg_return,
median_return=float(np.median(returns)) if returns else 0,
max_return=max(returns) if returns else 0,
min_return=min(returns) if returns else 0,
return_std=float(np.std(returns)) if returns else 0,
positive_avg_return=(
np.mean(positive_returns) if positive_returns else 0
),
negative_avg_return=(
np.mean(negative_returns) if negative_returns else 0
),
limit_up_count=limit_up_count,
amount_top20_count=amount_top20_count,
longhu_count=longhu_count,
sector_leader_count=sector_leader_count,
precision_up=precision_up,
recall_up=recall_up,
f1_up=f1_up,
precision_limit_up=precision_limit_up,
recall_limit_up=recall_limit_up,
baseline_avg_return=baseline_avg,
excess_return=avg_return - baseline_avg,
baseline_up_ratio=baseline_up_ratio,
excess_up_ratio=up_count / max(top_n, 1) - baseline_up_ratio,
market_state=snapshot.market_state,
)
8.5 Precision / Recall 详解
传统的 Precision/Recall 框架需要明确"正样本"定义。在这个系统中有多个维度:
维度一:上涨 Precision(最基本)
实际上涨 实际未涨
HotScore Top20 选中 TP FP ← Precision = TP/(TP+FP)
HotScore Top20 未选 FN TN ← Recall = TP/(TP+FN)
- Precision(精确率):我选出的 20 只里,真正上涨的比例。越高说明"选出来的靠谱"。
- Recall(召回率):全市场上涨的股票里,被我选中的比例。越高说明"没漏掉好的"。
对于热点侦测系统,Precision 远比 Recall 重要:
我们不需要找到所有上涨的股票(那是全市场选股的事),
我们只需要确保选出来的大概率是对的。
维度二:涨停 Precision
问题:Top20 中有几只后来涨停了?
Precision_涨停 = (Top20中后来涨停的) / 20
Recall_涨停 = (Top20中后来涨停的) / (全市场当日涨停总数)
维度三:成交额 Top20 Precision
问题:HotScore Top20 与成交额 Top20 的重合度?
Overlap = |HotScore_Top20 ∩ Amount_Top20| / 20
维度四:多阈值 Precision
不只看 Top20,还要看不同 HotScore 阈值下的精确率:
| 信号条件 | 选出数量 | 10分钟后上涨数 | Precision | Avg Return |
|---|---|---|---|---|
| Score ≥ 90 | ~5 | ? | ? | ? |
| Score ≥ 85 | ~15 | ? | ? | ? |
| Score ≥ 80 | ~30 | ? | ? | ? |
| Score ≥ 70 | ~80 | ? | ? | ? |
| Top 10 | 10 | ? | ? | ? |
| Top 20 | 20 | ? | ? | ? |
| Top 50 | 50 | ? | ? | ? |
class PrecisionByThreshold:
"""按不同阈值计算 Precision,找到最优 threshold"""
SCORE_THRESHOLDS = [90, 85, 80, 75, 70]
TOPN_THRESHOLDS = [10, 20, 50]
def compute(
self,
all_scores: dict[str, float],
future_returns: dict[str, float],
horizon: int,
) -> list[ThresholdResult]:
results = []
# 按分数阈值
for threshold in self.SCORE_THRESHOLDS:
selected = {s for s, score in all_scores.items() if score >= threshold}
results.append(self._evaluate_set(
name=f"score≥{threshold}",
selected=selected,
future_returns=future_returns,
horizon=horizon,
))
# 按 Top N
sorted_symbols = sorted(
all_scores, key=all_scores.get, reverse=True
)
for n in self.TOPN_THRESHOLDS:
selected = set(sorted_symbols[:n])
results.append(self._evaluate_set(
name=f"top{n}",
selected=selected,
future_returns=future_returns,
horizon=horizon,
))
return results
def _evaluate_set(
self, name: str, selected: set, future_returns: dict, horizon: int
) -> "ThresholdResult":
returns = [future_returns.get(s, 0) for s in selected]
up_count = sum(1 for r in returns if r > 0)
return ThresholdResult(
name=name,
selected_count=len(selected),
up_count=up_count,
precision=up_count / max(len(selected), 1),
avg_return=np.mean(returns) if returns else 0,
median_return=float(np.median(returns)) if returns else 0,
horizon=horizon,
)
@dataclass
class ThresholdResult:
name: str
selected_count: int
up_count: int
precision: float
avg_return: float
median_return: float
horizon: int
8.6 对比基线
HotScore 的价值必须通过对比基线来证明。设计 3 种基线:
class BaselineComparator:
"""
基线对比:如果 HotScore Top20 跑不赢基线,那整个系统没有价值。
"""
def compute_baselines(
self,
all_prices: dict[str, PriceHistory],
hotscore_top20: list[str],
horizon: int,
) -> dict[str, BaselineResult]:
# 基线1:随机选 20 只
random_20 = random.sample(list(all_prices.keys()), 20)
baseline_random = self._eval_portfolio(random_20, all_prices, horizon)
# 基线2:成交额 Top20(市场公认的活跃股)
amount_sorted = sorted(
all_prices, key=lambda s: all_prices[s].current_amount, reverse=True
)
baseline_amount = self._eval_portfolio(
amount_sorted[:20], all_prices, horizon
)
# 基线3:涨幅 Top20(追涨策略)
return_sorted = sorted(
all_prices,
key=lambda s: all_prices[s].intraday_return, reverse=True,
)
baseline_return = self._eval_portfolio(
return_sorted[:20], all_prices, horizon
)
# HotScore Top20 自身
hotscore_result = self._eval_portfolio(
hotscore_top20, all_prices, horizon
)
return {
"hotscore_top20": hotscore_result,
"random_20": baseline_random,
"amount_top20": baseline_amount,
"return_top20": baseline_return,
}
def _eval_portfolio(
self, symbols: list[str], prices: dict, horizon: int
) -> "BaselineResult":
returns = []
for s in symbols:
p = prices.get(s)
if p:
future_price = p.get_price_after(horizon)
ret = (future_price - p.current_price) / p.current_price * 100
returns.append(ret)
up_count = sum(1 for r in returns if r > 0)
return BaselineResult(
avg_return=np.mean(returns) if returns else 0,
median_return=float(np.median(returns)) if returns else 0,
up_ratio=up_count / max(len(returns), 1),
up_count=up_count,
total=len(returns),
)
@dataclass
class BaselineResult:
avg_return: float
median_return: float
up_ratio: float
up_count: int
total: int
每日报告输出示例:
═══════════════════════════════════════════════════════
2026-07-14 评估报告 | 10分钟窗口 | Top20
═══════════════════════════════════════════════════════
策略 平均收益 上涨比例 中位数收益
─────────────────────────────────────────────────
HotScore Top20 +1.23% 75.0% +0.87% ★
成交额 Top20 +0.45% 55.0% +0.21%
涨幅 Top20 +0.12% 50.0% -0.05%
随机 20只 +0.18% 48.0% +0.03%
超额收益(vs 随机): +1.05% ✓ 有效
超额收益(vs 成交额): +0.78% ✓ 有效
如果 HotScore Top20 跑不赢"随机选 20 只",说明整个评分体系无效,需要重新审视权重。
8.7 分时段分析
市场不同时段的特征截然不同,必须分段评估:
class SessionAnalyzer:
"""按交易时段分段评估 HotScore 表现"""
SESSIONS = {
"open_15min": (0, 15), # 开盘 15 分钟(最混乱)
"morning_mid": (15, 60), # 上午中段
"morning_late": (60, 120), # 上午尾段
"afternoon_early": (120, 180), # 下午前段
"afternoon_late": (180, 210), # 下午中段
"close_30min": (210, 240), # 尾盘 30 分钟
}
def analyze_by_session(
self, eval_records: list[EvalRecord]
) -> dict[str, SessionStats]:
"""按时段聚合评估指标"""
session_records: dict[str, list] = {s: [] for s in self.SESSIONS}
for record in eval_records:
minutes = record.market_state.minutes_from_open
for session_name, (start, end) in self.SESSIONS.items():
if start <= minutes < end:
session_records[session_name].append(record)
break
return {
name: self._aggregate_session(records)
for name, records in session_records.items()
if records
}
def _aggregate_session(self, records: list[EvalRecord]) -> "SessionStats":
return SessionStats(
sample_count=len(records),
avg_precision=np.mean([r.precision_up for r in records]),
avg_return=np.mean([r.avg_return for r in records]),
avg_excess_return=np.mean([r.excess_return for r in records]),
avg_up_ratio=np.mean([r.up_ratio for r in records]),
avg_limit_up_count=np.mean([r.limit_up_count for r in records]),
best_precision=max(r.precision_up for r in records),
worst_precision=min(r.precision_up for r in records),
)
@dataclass
class SessionStats:
sample_count: int
avg_precision: float
avg_return: float
avg_excess_return: float
avg_up_ratio: float
avg_limit_up_count: float
best_precision: float
worst_precision: float
输出示例:
时段 样本数 Precision 平均收益 超额收益 涨停命中
──────────────────────────────────────────────────────────
开盘15分钟 15 82% +1.8% +1.5% 2.1
上午中段 45 71% +1.2% +0.9% 1.3
上午尾段 60 65% +0.8% +0.5% 0.8
下午前段 60 68% +0.9% +0.6% 0.9
下午中段 30 58% +0.4% +0.1% 0.4
尾盘30分钟 30 52% +0.2% -0.1% 0.2
→ 结论:HotScore 在开盘 15 分钟效果最好,尾盘几乎失效
→ 行动:考虑尾盘降低 HotScore 权重或停止推送
8.8 HotScore 各分量的 IC 分析
IC(Information Coefficient)衡量每个特征分量与未来收益的相关性,是调整权重的定量依据:
class FeatureICAnalyzer:
"""计算每个 HotScore 分量与未来收益的 IC"""
COMPONENTS = [
"volume_surge", "active_buy", "sector_linkage",
"turnover", "seal_quality",
]
def compute_daily_ic(
self,
snapshots: list[EvalSnapshot],
outcomes: list[OutcomeRecord],
horizon: int = 10,
) -> dict[str, float]:
"""
计算每个分量的截面 IC(Spearman Rank Correlation)
IC > 0.05 → 该分量有预测价值
IC > 0.10 → 该分量是强信号
IC < 0.02 → 该分量基本无用,可降低权重
"""
ic_results = {}
for component in self.COMPONENTS:
scores_list = []
returns_list = []
for snapshot in snapshots:
for symbol in snapshot.top_symbols:
comp_score = snapshot.top_components.get(
symbol, {}
).get(component, 0)
outcome = self._find_outcome(symbol, snapshot.snapshot_id, outcomes)
if outcome:
ret = self._get_return(outcome, horizon)
scores_list.append(comp_score)
returns_list.append(ret)
if len(scores_list) >= 30:
ic, _ = spearmanr(scores_list, returns_list)
ic_results[component] = round(ic, 4)
else:
ic_results[component] = None
return ic_results
def suggest_weight_adjustment(
self, ic_values: dict[str, float]
) -> dict[str, float]:
"""基于 IC 建议权重调整"""
valid = {k: v for k, v in ic_values.items() if v is not None and v > 0}
if not valid:
return {}
total_ic = sum(valid.values())
suggested = {k: round(v / total_ic, 2) for k, v in valid.items()}
return suggested
输出示例:
HotScore 分量 IC 分析(10分钟窗口,最近 5 个交易日)
─────────────────────────────────────────────────
分量 IC值 当前权重 建议权重 结论
volume_surge 0.128 0.35 0.32 ✓ 有效,核心信号
active_buy 0.095 0.25 0.24 ✓ 有效
sector_linkage 0.082 0.20 0.21 ✓ 有效
turnover 0.018 0.10 0.05 ⚠ 偏弱,降低权重
seal_quality 0.071 0.10 0.18 ↑ 被低估,提高权重
→ 建议:turnover 权重从 0.10 降至 0.05,seal_quality 从 0.10 升至 0.18
8.9 每日评估报告
@dataclass
class DailyEvalReport:
"""每日收盘后生成的完整评估报告"""
date: str
trading_minutes: int # 当日交易分钟数
# ---- 信号统计 ----
total_snapshots: int # 拍了多少个快照
total_unique_symbols: int # 总共出现过多少只不同的股票
# ---- 按时间窗口的核心指标 ----
metrics_by_horizon: dict[int, HorizonMetrics] # {5: ..., 10: ..., 15: ..., 30: ...}
# ---- 基线对比 ----
baseline_comparison: dict[int, dict[str, BaselineResult]]
# ---- 分时段分析 ----
session_analysis: dict[str, SessionStats]
# ---- IC 分析 ----
component_ic: dict[str, float]
weight_suggestions: dict[str, float]
# ---- 最佳/最差案例 ----
best_snapshots: list[str] # 表现最好的 5 个快照 ID
worst_snapshots: list[str] # 表现最差的 5 个快照 ID
# ---- 趋势(与过去 N 日对比)----
precision_trend_7d: list[float] # 最近 7 日 Precision 趋势
excess_return_trend_7d: list[float] # 最近 7 日超额收益趋势
@dataclass
class HorizonMetrics:
"""单个时间窗口的汇总指标"""
horizon: int
# Precision / Recall 均值
avg_precision: float
avg_recall: float
avg_f1: float
# 收益
avg_return: float
median_return: float
avg_excess_return: float
# 涨跌统计
avg_up_ratio: float
total_limit_up_hits: int
total_amount_top20_hits: int
total_longhu_hits: int
# 最好 / 最差快照
best_precision: float
worst_precision: float
precision_std: float
# 多阈值分析
threshold_results: list[ThresholdResult]
8.10 评估数据存储
-- 评估快照表(每分钟一条 × 每个 Top N 粒度)
-- 路径: data/hotspot/eval/snapshots/date={yyyy-mm-dd}/data.parquet
CREATE TABLE eval_snapshots (
snapshot_id VARCHAR PRIMARY KEY,
timestamp TIMESTAMP,
top_n INTEGER,
top_symbols JSON, -- ["600519", "300750", ...]
top_scores JSON, -- {"600519": 92.5, ...}
top_components JSON, -- {"600519": {"volume_surge": 88, ...}, ...}
total_scored INTEGER,
score_mean DOUBLE,
score_std DOUBLE,
-- 市场状态
minutes_from_open INTEGER,
session VARCHAR,
market_up_ratio DOUBLE,
market_limit_up_count INTEGER,
market_sentiment DOUBLE
);
-- 结果回填表(每只股票 × 每个快照 × 每个时间窗口一条)
-- 路径: data/hotspot/eval/outcomes/date={yyyy-mm-dd}/data.parquet
CREATE TABLE eval_outcomes (
snapshot_id VARCHAR,
symbol VARCHAR,
horizon_minutes INTEGER,
hotscore_at_snapshot DOUBLE,
price_at_snapshot DOUBLE,
price_after DOUBLE,
return_pct DOUBLE,
max_return_pct DOUBLE,
max_drawdown_pct DOUBLE,
went_up BOOLEAN,
hit_limit_up BOOLEAN,
entered_amount_top20 BOOLEAN,
entered_longhu BOOLEAN,
became_sector_leader BOOLEAN,
hotscore_after DOUBLE,
hotscore_delta DOUBLE,
stayed_in_topn BOOLEAN,
PRIMARY KEY (snapshot_id, symbol, horizon_minutes)
);
-- 评估汇总表(每个快照 × 每个时间窗口一条)
-- 路径: data/hotspot/eval/records/date={yyyy-mm-dd}/data.parquet
CREATE TABLE eval_records (
snapshot_id VARCHAR,
horizon_minutes INTEGER,
top_n INTEGER,
timestamp TIMESTAMP,
-- 涨跌
up_count INTEGER,
down_count INTEGER,
up_ratio DOUBLE,
-- 收益
avg_return DOUBLE,
median_return DOUBLE,
max_return DOUBLE,
min_return DOUBLE,
return_std DOUBLE,
-- 事件
limit_up_count INTEGER,
amount_top20_count INTEGER,
longhu_count INTEGER,
sector_leader_count INTEGER,
-- Precision / Recall
precision_up DOUBLE,
recall_up DOUBLE,
f1_up DOUBLE,
precision_limit_up DOUBLE,
recall_limit_up DOUBLE,
-- 基线对比
baseline_avg_return DOUBLE,
excess_return DOUBLE,
-- 市场状态
minutes_from_open INTEGER,
session VARCHAR,
PRIMARY KEY (snapshot_id, horizon_minutes)
);
-- 每日评估报告表
CREATE TABLE eval_daily_report (
date DATE,
horizon_minutes INTEGER,
top_n INTEGER,
total_snapshots INTEGER,
avg_precision DOUBLE,
avg_recall DOUBLE,
avg_return DOUBLE,
avg_excess_return DOUBLE,
avg_up_ratio DOUBLE,
total_limit_up_hits INTEGER,
total_longhu_hits INTEGER,
best_precision DOUBLE,
worst_precision DOUBLE,
component_ic JSON, -- {"volume_surge": 0.128, ...}
weight_suggestions JSON,
precision_trend_7d JSON, -- [0.72, 0.68, 0.71, ...]
PRIMARY KEY (date, horizon_minutes, top_n)
);
8.11 评估驱动的权重自动调优
评估层不只是"看看数字",它直接驱动 HotScore 权重的迭代:
Week 1: 默认权重(规划书推荐)
↓ 收集 5 个交易日的评估数据
↓
Week 2: IC 分析 → 发现 turnover IC 只有 0.018,seal_quality IC 有 0.071
↓ 权重调整建议:降 turnover,升 seal_quality
↓
Week 3: 应用新权重,继续评估
↓ 对比 Week 2 vs Week 3 的 Precision 和超额收益
↓
Week 4: 如果改善 → 固化;如果恶化 → 回滚
每周循环,永不停止。
class WeightOptimizer:
"""基于评估数据的权重自动调优"""
def optimize(
self,
recent_eval: list[DailyEvalReport],
current_weights: dict[str, float],
min_ic_threshold: float = 0.02,
) -> WeightUpdate:
# 1. 计算近 5 日平均 IC
avg_ic = self._average_ic(recent_eval)
# 2. IC 过低的分量降权
adjusted = {}
for comp, ic in avg_ic.items():
if ic is None or ic < min_ic_threshold:
adjusted[comp] = current_weights[comp] * 0.5 # 砍半
else:
adjusted[comp] = ic # IC 加权
# 3. 归一化使权重和为 1
total = sum(adjusted.values())
new_weights = {k: round(v / total, 3) for k, v in adjusted.items()}
# 4. 与当前权重对比,变化过大则限制步长
for comp in new_weights:
delta = new_weights[comp] - current_weights[comp]
if abs(delta) > 0.10: # 单次调整不超过 10%
new_weights[comp] = current_weights[comp] + 0.10 * np.sign(delta)
return WeightUpdate(
old_weights=current_weights,
new_weights=new_weights,
ic_values=avg_ic,
reason="weekly_ic_optimization",
)
8.12 评估层在架构中的位置提升
原架构将评估放在最末层。现在把它提升到与 HotScore 引擎同级的核心位置:
采集层 → 特征层 → HotScore 引擎 ←──→ 评估引擎(审判官)
↑ │
│ │
└── 权重调整 ←────────┘
在主循环中,评估引擎不是"事后总结",而是伴随运行:
async def main_loop():
"""主循环(每分钟执行一次)"""
while is_trading_time():
# 1. 特征计算
features = await feature_engine.compute()
# 2. HotScore 计算
scores = hotscore_engine.compute_all(features)
# 3. 评估引擎:拍快照(记录当前 Top20)
await eval_engine.take_snapshot(scores, features, market_state)
# 4. 评估引擎:回填之前的快照(用当前事实)
await eval_engine.backfill_outcomes(
current_prices, current_amounts, scores,
limit_up_set, longhu_set, sector_leaders, amount_top20,
)
# 5. 实时推送排行 + 评估数据到前端
await ws_manager.broadcast_ranking(scores)
await ws_manager.broadcast_eval(eval_engine.latest_eval)
await asyncio.sleep(60)
# 收盘后
daily_report = eval_engine.generate_daily_report()
await eval_storage.save_daily_report(daily_report)
# 每周五自动运行权重优化建议
if is_friday():
suggestion = weight_optimizer.optimize(
recent_eval=eval_storage.load_recent(days=5),
current_weights=hotscore_engine.weights,
)
logger.info(f"权重优化建议: {suggestion}")
8.13 评估看板设计
在前端增加独立的评估看板 Tab:
┌──────────────────────────────────────────────────────────────────┐
│ [热点排行] [板块联动] [告警] [★ 评估看板] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ 实时 Precision 仪表盘 ──────────────────────────────────┐ │
│ │ │ │
│ │ 当前 Precision (10min) [====████████====] 72% │ │
│ │ 当前超额收益 (10min) +0.85% vs 随机 +0.12% │ │
│ │ 今日涨停命中 3 / 18 (16.7%) │ │
│ │ 今日龙虎榜命中 1 / 8 (12.5%) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Precision 时序图 ─────────┐ ┌─ 超额收益时序图 ──────────┐ │
│ │ Y: Precision % │ │ Y: 超额收益 % │ │
│ │ X: 09:30 ────── 15:00 │ │ X: 09:30 ────── 15:00 │ │
│ │ ···· 70% 基准线 │ │ ···· 0% 基准线 │ │
│ │ ▁▃▅█▇▆▅▃▂▁▃▅▇█▆▅▃ │ │ ▂▅█▇▅▃▁▂▃▅▇█▆▅▃▂ │ │
│ └────────────────────────────┘ └───────────────────────────┘ │
│ │
│ ┌─ 多阈值 Precision 对比 ───────────────────────────────────┐ │
│ │ Score≥90 ████████████████████████████ 85% (avg +2.1%) │ │
│ │ Score≥85 ██████████████████████████ 78% (avg +1.5%) │ │
│ │ Top 10 █████████████████████████ 75% (avg +1.3%) │ │
│ │ Top 20 ████████████████████████ 72% (avg +1.1%) │ │
│ │ Score≥70 ██████████████████ 62% (avg +0.6%) │ │
│ │ Top 50 ████████████████ 58% (avg +0.3%) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 基线对比 ──────────────────┐ ┌─ 分时段 Precision ───────┐ │
│ │ HotScore Top20 ████ 1.2% │ │ 开盘15min ████████ 82% │ │
│ │ 成交额 Top20 ██ 0.5% │ │ 上午中段 ██████ 71% │ │
│ │ 涨幅 Top20 █ 0.1% │ │ 上午尾段 █████ 65% │ │
│ │ 随机 20 只 █ 0.2% │ │ 下午前段 ██████ 68% │ │
│ └────────────────────────────┘ │ 下午中段 ████ 58% │ │
│ │ 尾盘30min ███ 52% │ │
│ └──────────────────────────┘ │
│ │
│ ┌─ IC 分析(最近5日)─────────────────────────────────────────┐ │
│ │ volume_surge ▓▓▓▓▓▓▓▓▓▓▓▓▓ 0.128 (权重 0.35 → 0.32) │ │
│ │ active_buy ▓▓▓▓▓▓▓▓▓▓ 0.095 (权重 0.25 → 0.24) │ │
│ │ sector_link ▓▓▓▓▓▓▓▓ 0.082 (权重 0.20 → 0.21) │ │
│ │ seal_quality ▓▓▓▓▓▓▓ 0.071 (权重 0.10 → 0.18) │ │
│ │ turnover ▓▓ 0.018 (权重 0.10 → 0.05) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
8.14 评估层验收标准
| 阶段 | 指标 | 及格线 | 优秀线 | 失败线(需要停下来反思) |
|---|---|---|---|---|
| MVP | Top20 10min Precision | ≥55% | ≥70% | <45%(不如随机) |
| MVP | Top20 10min 超额收益 | >0% | >+0.5% | <0%(跑输随机) |
| MVP | Top20 10min 上涨比例 | >全市场均值 | >均值+15% | <全市场均值 |
| 成熟 | 涨停 Recall (30min) | ≥10% | ≥20% | <5% |
| 成熟 | IC (volume_surge) | >0.05 | >0.10 | <0.02 |
| 成熟 | 连续5日 Precision 稳定性 | std<10% | std<5% | std>20% |
关键决策点:
- 如果 Precision 连续 3 个交易日 < 45% → 暂停系统,检查数据质量和特征计算
- 如果超额收益连续 5 个交易日 < 0 → 回滚到上一版权重,排查原因
- 只有 Precision 稳定 ≥ 55% 且超额 > 0 至少 10 个交易日 → 才有资格开始训练 AI 模型
九、API 与前端展示层详细设计
9.1 API 路由设计
热点排行 API
| 方法 | 路径 | 说明 | 响应频率 |
|---|---|---|---|
| GET | /api/hotspot/ranking |
当前热点排行 Top N | 实时 |
| GET | /api/hotspot/detail/{symbol} |
单只股票的热点详情(特征 + 评分明细) | 实时 |
| GET | /api/hotspot/history/{symbol} |
单只股票的 HotScore 历史曲线 | 分钟级 |
| GET | /api/hotspot/surging |
急速升温股票列表 | 实时 |
板块联动 API
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/sector/ranking |
板块热度排行 |
| GET | /api/sector/detail/{sector} |
板块详情(成分股 + 龙头 + 指标) |
| GET | /api/sector/resonance |
当前板块共振事件 |
告警 API
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/alert/recent |
最近告警列表 |
| WS | /ws/alerts |
实时告警推送 |
评估 API(核心)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/eval/realtime |
当前实时 Precision / 超额收益 / 涨停命中 |
| GET | /api/eval/precision_series/{date} |
当日 Precision 时序数据(绘图用) |
| GET | /api/eval/threshold_analysis |
多阈值 Precision 分析(Score≥90/85/80/70, Top10/20/50) |
| GET | /api/eval/baseline/{date} |
基线对比(HotScore vs 随机/成交额Top/涨幅Top) |
| GET | /api/eval/session/{date} |
分时段分析(开盘/上午/下午/尾盘) |
| GET | /api/eval/ic/{date} |
各分量 IC 分析 + 权重调整建议 |
| GET | /api/eval/daily/{date} |
指定日期完整评估报告 |
| GET | /api/eval/trend |
最近 N 日 Precision / 超额收益趋势 |
| GET | /api/eval/snapshots/{snapshot_id} |
查看某个快照的详细结果 |
系统状态 API
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/system/collectors |
采集器状态(运行 / 延迟 / 错误) |
| GET | /api/system/config |
当前配置(权重等) |
| PUT | /api/system/config/weights |
热更新 HotScore 权重 |
9.2 WebSocket 实时推送
class WebSocketManager:
"""WebSocket 连接管理器"""
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
async def broadcast_ranking(self, ranking: list[dict]):
"""广播热点排行更新(每分钟)"""
msg = {"type": "ranking_update", "data": ranking, "ts": now_str()}
await self._broadcast(msg)
async def broadcast_alert(self, alert: Alert):
"""广播告警(实时)"""
msg = {"type": "alert", "data": asdict(alert), "ts": now_str()}
await self._broadcast(msg)
async def _broadcast(self, message: dict):
dead = []
for conn in self.active_connections:
try:
await conn.send_json(message)
except WebSocketDisconnect:
dead.append(conn)
for conn in dead:
self.active_connections.remove(conn)
9.3 前端面板设计(hotspot_dashboard.html)
布局规划:
┌──────────────────────────────────────────────────────────────────┐
│ 顶部状态栏:系统状态 │ 采集延迟 │ 时间 │ 市场情绪指数 │
├────────────────────────────┬─────────────────────────────────────┤
│ │ │
│ 热点排行 Top 20 │ 选中股票详情 │
│ ┌────┬───────┬─────┐ │ ┌─────────────────────────────┐ │
│ │排名│ 代码 │ 分数 │ │ │ HotScore 走势图 │ │
│ ├────┼───────┼─────┤ │ │ (折线图,5秒刷新) │ │
│ │ 1 │600519 │ 92 │ │ ├─────────────────────────────┤ │
│ │ 2 │300750 │ 88 │ │ │ 特征雷达图 │ │
│ │ ...│ ... │ ... │ │ │ (成交/资金/盘口/联动 4维) │ │
│ └────┴───────┴─────┘ │ ├─────────────────────────────┤ │
│ (点击行 → 右侧显示详情) │ │ 分钟成交额柱状图 │ │
│ │ │ 主动买卖比例 │ │
│ │ └─────────────────────────────┘ │
├────────────────────────────┼─────────────────────────────────────┤
│ │ │
│ 板块热度排行 │ 告警流 │
│ ┌──────────┬─────┐ │ ┌─────────────────────────────┐ │
│ │ 板块名 │ 热度 │ │ │ 09:35 600519 成交额爆发 │ │
│ │ 人工智能 │ 85 │ │ │ 09:37 板块共振: AI+算力 │ │
│ │ 半导体 │ 78 │ │ │ 09:42 300750 急速升温 │ │
│ └──────────┴─────┘ │ │ ... │ │
│ │ └─────────────────────────────┘ │
└────────────────────────────┴─────────────────────────────────────┘
十、存储层详细设计
10.1 Redis 数据结构
| Key 模式 | 类型 | 说明 | TTL |
|---|---|---|---|
hotspot:ranking:{date} |
Sorted Set | 热点排行榜 (score=HotScore) | 24h |
hotspot:features:{symbol} |
Hash | 单只股票最新特征快照 | 盘中更新 |
hotspot:score_history:{symbol}:{date} |
List | HotScore 分钟级历史 | 7d |
sector:metrics:{sector} |
Hash | 板块最新指标 | 盘中更新 |
sector:ranking:{date} |
Sorted Set | 板块热度排行 | 24h |
alert:recent |
List (capped) | 最近 100 条告警 | 24h |
system:collector_stats |
Hash | 采集器状态 | 盘中更新 |
config:hotscore_weights |
Hash | HotScore 权重(热更新) | 永久 |
eval:latest:{horizon} |
Hash | 最新一次评估结果 (precision/return等) | 盘中更新 |
eval:precision_series:{date} |
List | 当日 Precision 时序(绘图用) | 7d |
eval:pending_snapshots |
Hash | 待回填的快照队列 | 盘中更新 |
10.2 DuckDB 表设计(历史数据)
-- 特征历史表(按日期 Hive 分区)
-- 路径: data/hotspot/features/date={yyyy-mm-dd}/data.parquet
CREATE TABLE feature_history (
symbol VARCHAR,
timestamp TIMESTAMP,
-- 成交特征
volume_surge_5m DOUBLE,
volume_surge_1m DOUBLE,
volume_ratio DOUBLE,
turnover_rate DOUBLE,
large_order_ratio DOUBLE,
trade_density DOUBLE,
-- 资金主动性
active_buy_amount DOUBLE,
active_sell_amount DOUBLE,
net_active_buy DOUBLE,
active_buy_ratio DOUBLE,
consecutive_buy_count INTEGER,
buy_momentum DOUBLE,
-- 盘口结构
bid_ask_imbalance DOUBLE,
seal_amount DOUBLE,
seal_duration DOUBLE,
cancel_rate DOUBLE,
ask_decay_speed DOUBLE,
spread DOUBLE,
depth_ratio DOUBLE,
-- 联动特征
sector_change_pct DOUBLE,
sector_up_ratio DOUBLE,
sector_limitup_count INTEGER,
sector_volume_surge DOUBLE,
market_sentiment DOUBLE,
sector_rank INTEGER,
-- HotScore
hotscore DOUBLE,
hotscore_level VARCHAR
);
-- 评估相关表见第八章 8.10 节(eval_snapshots / eval_outcomes /
-- eval_records / eval_daily_report 共 4 张表)
-- 告警历史表
CREATE TABLE alert_history (
id INTEGER,
symbol VARCHAR,
rule_name VARCHAR,
level VARCHAR,
description VARCHAR,
timestamp TIMESTAMP,
context JSON
);
10.3 MySQL 表设计(配置与映射)
-- 板块-个股映射表
CREATE TABLE sector_mapping (
id INT AUTO_INCREMENT PRIMARY KEY,
sector_name VARCHAR(50) NOT NULL COMMENT '板块名称',
sector_code VARCHAR(20) NOT NULL COMMENT '板块代码',
sector_type ENUM('concept', 'industry') NOT NULL,
symbol VARCHAR(10) NOT NULL COMMENT '股票代码',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_sector_symbol (sector_code, symbol),
KEY idx_symbol (symbol),
KEY idx_sector (sector_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 关注列表(动态管理)
CREATE TABLE watch_list (
symbol VARCHAR(10) PRIMARY KEY,
source VARCHAR(20) NOT NULL COMMENT '来源: pool/scan/manual',
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active TINYINT(1) DEFAULT 1,
reason VARCHAR(200) COMMENT '加入原因'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
十一、配置文件设计
11.1 settings.yaml
system:
name: "market_hotspot"
log_level: "INFO"
timezone: "Asia/Shanghai"
collector:
tick:
interval_seconds: 3
max_symbols: 500
orderbook:
interval_seconds: 3
batch_size: 80
sector:
interval_seconds: 30
limitup:
interval_seconds: 60
connection_pool:
size: 5
reconnect_delay: 5
max_retries: 3
feature:
buffer_minutes: 30
compute_interval_seconds: 60
large_order_threshold: 500000
hotscore:
weights:
volume_surge: 0.35
active_buy: 0.25
sector_linkage: 0.20
turnover: 0.10
seal_quality: 0.10
thresholds:
strong_hot: 85
potential_hot: 70
alert:
cooldown_minutes: 5
max_alerts_per_minute: 20
model:
enabled: false # MVP 阶段先关闭
label_config: "hotscore_surge_10m"
retrain_schedule: "daily"
min_auc_threshold: 0.65
storage:
redis:
host: "127.0.0.1"
port: 6379
db: 1
duckdb:
data_root: "data/hotspot"
mysql:
host: "192.168.129.1"
port: 3306
database: "quant_data"
username: "claw"
password: "claw"
api:
host: "0.0.0.0"
port: 8001 # 与 stock_pool_app(8000) 错开
ws_ping_interval: 30
trading_hours:
pre_market: "09:15"
morning_open: "09:30"
morning_close: "11:30"
afternoon_open: "13:00"
afternoon_close: "15:00"
post_market: "15:05"
11.2 hotscore_weights.yaml(支持热更新)
version: 1
updated_at: "2026-07-13"
note: "初始权重,来自规划书推荐值"
weights:
volume_surge: 0.35
active_buy: 0.25
sector_linkage: 0.20
turnover: 0.10
seal_quality: 0.10
thresholds:
strong_hot: 85
potential_hot: 70
normalization:
method: "percentile_rank" # percentile_rank | min_max | z_score
十二、主程序启动流程
# main.py 启动流程伪代码
async def main():
# 1. 加载配置
config = load_config("config/settings.yaml")
# 2. 初始化存储
redis = await init_redis(config["storage"]["redis"])
duckdb = init_duckdb(config["storage"]["duckdb"])
# 3. 初始化板块映射
sector_mapping = SectorMapping()
await sector_mapping.refresh_from_eastmoney()
# 4. 初始化各引擎
feature_engine = FeatureEngine(config["feature"])
hotscore_engine = HotScoreEngine()
sector_engine = SectorEngine(sector_mapping)
alert_engine = AlertEngine(config["alert"])
# 5. 初始化采集器
manager = CollectorManager(config["collector"])
manager.register(TickCollector(...))
manager.register(OrderBookCollector(...))
manager.register(SectorCollector(...))
manager.register(LimitUpCollector(...))
# 6. 启动 FastAPI + WebSocket
app = create_fastapi_app(config, engines={...})
server = uvicorn.Server(uvicorn.Config(app, host="0.0.0.0", port=8001))
# 7. 启动主循环
await asyncio.gather(
manager.start_all(), # 数据采集
feature_engine.run(), # 特征计算
hotscore_engine.run(), # HotScore 计算
alert_engine.run(), # 告警检测
server.serve(), # API 服务
)
十三、开发路线图
Phase 1(第1周):数据基础
| 任务 | 优先级 | 预计工时 |
|---|---|---|
| 项目骨架 + 配置管理 | P0 | 0.5d |
| pytdx 连接池 + 逐笔采集 | P0 | 1.5d |
| 五档盘口采集 | P0 | 1d |
| DataBus 数据总线 | P0 | 0.5d |
| 采集监控 + 日志 | P1 | 0.5d |
| 验收标准 | 盘中稳定采集 300 只股票,丢包率<1%,连续运行 2 个交易日无崩溃 |
Phase 2(第2周):特征 + HotScore + 评估骨架
| 任务 | 优先级 | 预计工时 |
|---|---|---|
| 时序缓冲区 + 分钟预聚合 | P0 | 1d |
| 成交特征 + 资金特征计算 | P0 | 1d |
| 盘口结构特征计算 | P0 | 0.5d |
| HotScore 引擎 + 截面归一化 | P0 | 1d |
| 评估引擎:快照 + 回填 + EvalRecord | P0 | 1d |
| Redis 排行榜 + API | P1 | 0.5d |
| 验收标准 | HotScore 每分钟刷新 + 每分钟自动拍快照 + 10 分钟后自动回填结果 |
第 2 周结束时必须能回答:Top20 的 10 分钟 Precision 是多少?超额收益是多少?
如果不能回答,不要进入第 3 周。
Phase 3(第3周):板块联动 + 评估体系完善
| 任务 | 优先级 | 预计工时 |
|---|---|---|
| 板块行情 + 涨停数据采集 | P0 | 1d |
| 板块-个股映射管理 | P0 | 0.5d |
| 板块联动引擎 + 龙头识别 | P0 | 1d |
| 联动特征注入 HotScore | P0 | 0.5d |
| 多阈值 Precision + 基线对比 + 分时段分析 | P0 | 1d |
| IC 分析 + 权重调整建议 | P0 | 0.5d |
| 告警引擎 + WebSocket 推送 | P1 | 0.5d |
| 验收标准 | Precision ≥ 55% 且超额收益 > 0(连续 3 个交易日),否则调整权重后重新验证 |
Phase 4(第4周):评估看板 + AI(仅当评估达标时)
| 任务 | 优先级 | 预计工时 |
|---|---|---|
| 评估看板前端(Precision 曲线、基线对比、IC 图) | P0 | 1.5d |
| 每日评估报告自动生成 | P0 | 0.5d |
| 权重自动调优(基于 IC) | P1 | 1d |
| 历史数据持久化管道 | P0 | 0.5d |
| LightGBM 训练 + 在线推理(仅当 Precision 稳定 ≥55%) | P2 | 1.5d |
| 验收标准 | 评估看板可用 + 10 个交易日 Precision 趋势稳定 |
Phase 4 的 AI 部分是可选的。 如果 Phase 3 结束时 Precision < 55%,
第 4 周应全力优化 HotScore 权重和特征,而不是上 AI。
AI 在一个"不准"的 HotScore 上训练,只会放大错误。
Phase 5(第5-6周):优化迭代
- 权重滚动优化(每周基于 IC 微调)
- 如果 Precision 稳定,启动 AI 模型训练
- 评估 AI 模型 vs 纯 HotScore 的增量价值
- 性能优化(采集并发、计算向量化)
- 与 stock_pool_app 集成(观察池打通)
- 移动端告警推送(可选)
十四、依赖清单
# requirements.txt
# Web 框架
fastapi>=0.100.0
uvicorn[standard]>=0.23.0
websockets>=12.0
# 数据源
pytdx>=1.72
akshare>=1.12.0
# 数据处理
pandas>=2.0.0
numpy>=1.24.0
# 存储
redis[hiredis]>=5.0.0
duckdb>=0.10.0
sqlalchemy>=2.0.0
pymysql>=1.1.0
# ML(Phase 4 启用)
lightgbm>=4.0.0
scikit-learn>=1.3.0
# 调度与工具
apscheduler>=3.10.0
pyyaml>=6.0
pydantic>=2.0.0
# 日志
loguru>=0.7.0
暂无评论,快来抢沙发吧~