简介:这是一套面向量化入门者与Python爱好者的轻量级AI炒股系统源码,聚焦实战场景下的择时分析与周度复盘能力,解决初学者难以落地多模型协同与Agent模块化设计的痛点。资源共11个文件,含4个核心Python脚本(如app.py、main.py、train.py)、2个预训练模型pkl文件(分类与回归双模型各一)、2个示例数据csv(data.csv、trades.csv)及requirements.txt等,总大小仅598KB,普通笔记本即可快速运行调试。已有440人学习下载,适合用于课程设计、个人量化项目孵化或Agent架构学习实践。读者可直接运行Streamlit交互界面完成单票择时推演,调用多Agent模块解耦的选股、风控、择时与复盘流程,并自动生成结构化周报;代码结构清晰,utils.py封装通用工具,__pycache__已包含编译缓存,便于二次开发与模型替换。
1. 这不是“AI选股App”,而是一套可审计、可回溯、可周度迭代的交易决策流水线:用多Agent解耦信号生成、仓位管理与复盘归因,LGBM不直接预测涨跌,而是建模“当前市场状态是否适合执行某类择时策略”
你见过太多“AI炒股”项目——模型一跑,准确率92%,回测曲线光滑如镜,但实盘第一天就爆仓。问题不在算法,而在系统设计:把价格预测、仓位计算、止损逻辑、复盘归因全塞进一个黑匣子,出问题时连哪一行代码在哪个市场环境下失效都查不到。本方案彻底放弃“端到端预测股价”的玄学路径,转而构建一条可拆解、可验证、可周度快修的决策流水线:用多Agent架构将“什么时候该看什么信号”(择时分析)和“看到信号后怎么下注、怎么盯盘、怎么收手”(仓位执行)物理隔离;LGBM不预测明天涨还是跌,而是训练成一个市场状态分类器——它判断的是“当前波动率+量能+板块轮动强度是否处于历史前15%的高风险区”,这个输出直接驱动Agent的决策开关;所有中间结果(信号触发时间、仓位变化轨迹、盈亏归因到具体因子)自动存入本地SQLite,支撑后续周度复盘。适合有Python工程能力、熟悉pandas和lightgbm、手头已有日频行情数据(A股/港股/美股均可)的量化实践者,目标不是替代人做决策,而是让每次交易都有据可查、每次复盘都有迹可循。
2. 多Agent架构设计:为什么不用单模型硬编码?三个Agent的职责边界与通信契约
2.1 择时Agent:只负责“发信号”,不碰仓位,不关心盈亏
择时Agent是整个系统的哨兵。它的唯一输入是过去60个交易日的OHLCV、行业资金流、北向持仓变化(可选),输出是结构化信号:{'signal_type': 'breakout', 'confidence': 0.82, 'valid_until': '2024-06-28', 'target_stocks': ['600519.SH', '000858.SZ']}。关键设计点在于信号必须带有效期和置信度——避免“过期信号”被误执行。我们不用强化学习动态调整阈值,而是用LGBM输出的原始分数(model.predict_proba(X)[:, 1])直接作为置信度,因为LGBM的输出概率在跨周期校准后具备统计意义(后文详述校准方法)。信号类型严格限定为预定义枚举:['breakout', 'mean_reversion', 'trend_follow', 'volatility_squeeze'],禁止自由文本,确保下游Agent能无歧义解析。
2.2 执行Agent:只响应信号,按规则算仓位,不修改信号
执行Agent收到择时Agent的信号后,启动三步确定性流程:
- 过滤:剔除ST、停牌、流通市值<50亿的股票(参数可配置);
- 仓位分配:对剩余标的按
signal_confidence * (1 / rank_in_signal)加权,例如3只股票置信度分别为0.82/0.75/0.68,则权重为0.82/1 : 0.75/2 : 0.68/3 ≈ 0.41 : 0.375 : 0.227; - 风控截断:单票仓位上限15%,单日总开仓不超过可用资金的30%。
注意:执行Agent从不调用任何预测模型,所有计算基于信号字段和本地风控参数。它的输出是标准订单字典:
{'order_list': [{'code': '600519.SH', 'action': 'buy', 'amount': 1200, 'price_limit': 185.2}, ...], 'total_risk_exposure': 0.28}。这份订单会被写入orders_pending.csv,等待模拟/实盘引擎读取。
2.3 复盘Agent:不参与实时决策,只做周度归因与策略衰减诊断
复盘Agent在每周日晚22:00自动触发,读取本周所有已成交订单、对应持仓盈亏、市场状态快照(波动率分位数、行业轮动强度指数),生成三份报告:
weekly_signal_effectiveness.csv:统计每类信号的成功率(盈利订单占比)、平均持有天数、最大回撤;factor_contribution.xlsx:用SHAP值分解本周盈亏中各因子(如“北向增持强度”“小盘股溢价”)的贡献度;strategy_decay_report.md:对比当前信号触发频率与过去12周均值,若breakout信号本周触发次数下降40%,则标记“趋势跟踪类策略可能进入低效期”。
这个Agent的存在,让“周度复盘”不再是人工翻Excel,而是自动生成可执行的策略调整建议。
3. LGBM双模型设计:一个判状态,一个校准置信度,拒绝“预测即真理”
3.1 状态分类模型(StateClassifier):把市场切成4个可操作象限
我们不训练模型预测“明天涨1.5%”,而是定义四个市场状态:
| 状态ID | 名称 | 判定逻辑(示例) |
|---|---|---|
| S1 | 高波动+高流动性 | 20日ATR > 历史90%分位 & 日均成交额 > 500亿 |
| S2 | 低波动+趋势初现 | ATR < 历史30%分位 & MACD柱状图连续3日扩大 |
| S3 | 高波动+流动性枯竭 | ATR > 90%分位 & 成交额 < 200亿 |
| S4 | 低波动+横盘震荡 | ATR < 30%分位 & BOLL带宽 < 5% |
StateClassifier的训练目标是:给定当日特征(ATR、成交额、MACD、RSI、行业轮动强度等12维),输出四分类概率。关键技巧在于特征工程必须包含跨周期统计量:例如'atr_20d_ratio_to_1y_mean'(当前ATR除以一年均值),而非原始ATR值——这能让模型理解“当前波动是常态还是异动”。模型用lightgbm.LGBMClassifier,num_class=4,损失函数选multiclass,重点调参项:
params = { 'objective': 'multiclass', 'num_class': 4, 'learning_rate': 0.05, 'num_leaves': 31, 'min_data_in_leaf': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.9, 'bagging_freq': 5, 'lambda_l1': 0.1, 'lambda_l2': 0.1, 'verbose': -1 }逻辑说明:
min_data_in_leaf=20防止过拟合小样本状态(如S3状态全年仅出现12次);bagging_freq=5引入随机性,避免模型对特定极端行情过拟合;lambda_l1/l2双正则控制树结构复杂度。训练集必须覆盖至少3个完整牛熊周期(建议2019-2024年数据)。
3.2 置信度校准模型(ConfidenceCalibrator):让LGBM输出真正可信
StateClassifier输出的概率(如[0.1, 0.65, 0.2, 0.05])不能直接当置信度用——原始LGBM概率常呈“尖峰厚尾”,在低概率区间严重失真。我们训练第二个轻量级LGBM,输入是StateClassifier的4维输出概率+原始特征,输出是标量calibrated_confidence(0~1)。训练目标是让calibrated_confidence与实际胜率高度相关:
# 构造校准标签:以未来5日收益率>2%为正样本 y_calibrate = (future_returns > 0.02).astype(int) # 特征:state_probs + 原始12维特征 X_calibrate = np.hstack([state_probs, raw_features]) # 训练二分类模型 calibrator = lgb.LGBMClassifier( objective='binary', learning_rate=0.1, num_leaves=15, min_data_in_leaf=10 ) calibrator.fit(X_calibrate, y_calibrate)参数说明:
num_leaves=15限制模型复杂度,避免校准过拟合;min_data_in_leaf=10保证每个叶子节点有足够样本支撑概率估计。校准后,当模型输出calibrated_confidence=0.78,意味着历史上同类信号后续5日盈利超2%的概率真实值为76.3%~79.1%(用Bootstrap法验证)。
4. 从数据到信号:本地化部署全流程与关键参数配置
4.1 数据准备:三类数据源的清洗与对齐规范
系统依赖三类数据,必须按统一时间戳对齐:
- 行情数据:日频OHLCV,要求
open/high/low/close/volume字段齐全,缺失用前值填充(ffill),但需记录缺失标记列volume_is_estimated; - 资金流数据:北向资金日净流入(单位:亿元),来源可选聚源/东方财富,要求与行情日期严格对齐;
- 行业数据:申万一级行业指数收盘价,用于计算行业轮动强度(
abs(行业涨跌幅 - 全市场涨跌幅)的均值)。
所有数据存入data/raw/下按日期分区(如data/raw/20240621/),清洗脚本核心逻辑:
# clean_data.py import pandas as pd import numpy as np def align_and_clean(date_str): # 读取当日行情 df_price = pd.read_parquet(f'data/raw/{date_str}/price.parquet') # 读取资金流 df_fund = pd.read_csv(f'data/raw/{date_str}/fund_flow.csv') # 合并,按股票代码索引对齐 merged = df_price.join(df_fund.set_index('code'), on='code', rsuffix='_fund') # 填充缺失值并标记 merged['volume'] = merged['volume'].fillna(method='ffill') merged['volume_is_estimated'] = merged['volume'].isna().astype(int) # 保存清洗后数据 merged.to_parquet(f'data/cleaned/{date_str}.parquet') return merged if __name__ == '__main__': for date in get_trading_days('20240101', '20240621'): align_and_clean(date)逻辑说明:
join操作确保股票维度对齐;volume_is_estimated标记列后续会作为特征输入StateClassifier,让模型知道“当前成交量是否可靠”;get_trading_days函数需自行实现(推荐用akshare获取A股交易日历)。
4.2 信号生成:择时Agent的最小可运行命令
假设已清洗好2024年6月全部数据,运行择时Agent只需一条命令:
python agent_timing.py --start_date 20240617 --end_date 20240621 --model_path models/state_classifier.txtagent_timing.py核心逻辑:
# agent_timing.py import lightgbm as lgb import pandas as pd from utils.feature_engineer import build_features def generate_signals(start_date, end_date, model_path): model = lgb.Booster(model_file=model_path) signals = [] for date in pd.date_range(start_date, end_date, freq='D'): if not is_trading_day(date): continue # 构建当日特征(含跨周期统计量) X = build_features(date) # 返回12维numpy数组 # 预测市场状态 state_probs = model.predict(X.reshape(1, -1))[0] # 用校准模型转换置信度 calibrator = lgb.Booster(model_file='models/confidence_calibrator.txt') calibrated_conf = calibrator.predict( np.hstack([state_probs, X]).reshape(1, -1) )[0] # 根据状态映射择时信号 signal_type = map_state_to_signal(np.argmax(state_probs)) if calibrated_conf > 0.7: # 置信度阈值 signals.append({ 'date': date.strftime('%Y%m%d'), 'signal_type': signal_type, 'confidence': float(calibrated_conf), 'valid_until': (date + pd.Timedelta(days=5)).strftime('%Y%m%d'), 'target_stocks': select_stocks_by_signal(signal_type, date) }) pd.DataFrame(signals).to_csv('output/timing_signals.csv', index=False) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('--start_date') parser.add_argument('--end_date') parser.add_argument('--model_path') args = parser.parse_args() generate_signals(args.start_date, args.end_date, args.model_path)参数说明:
--start_date/--end_date指定信号生成区间;--model_path指向训练好的StateClassifier模型;map_state_to_signal函数需按业务规则实现(如S1状态触发volatility_squeeze信号);select_stocks_by_signal根据信号类型筛选标的(breakout选近期放量突破年线个股)。
5. 避坑指南:血泪经验总结的5个高频翻车点与根治方案
5.1 现象:择时信号频繁触发但实盘亏损,回溯发现90%信号在S3状态(高波动+流动性枯竭)下发出
原因:StateClassifier在S3状态样本极少(2024年前5个月仅出现7次),模型将其误判为S1(高波动+高流动性),导致在流动性枯竭时仍按“高流动性”逻辑执行突破策略。
解决:在训练StateClassifier时,对S3状态样本做SMOTE过采样,并在损失函数中为S3类别设置3倍权重:class_weight={0:1, 1:1, 2:3, 3:1}。同时,在择时Agent中增加硬性熔断:当volume_is_estimated==1且atr_20d_ratio_to_1y_mean>1.5时,直接屏蔽所有信号。
5.2 现象:LGBM输出的置信度为0.85,但后续5日盈利概率仅52%
原因:未对StateClassifier输出做概率校准,原始LGBM概率在0.7~0.9区间存在系统性高估。
解决:必须部署ConfidenceCalibrator模型,且校准标签y_calibrate不能简单用“涨跌”,而要用多空双阈值:正样本定义为future_return > 0.02,负样本定义为future_return < -0.015,中间区域(-1.5%~2%)不参与训练,避免模型学习模糊地带。
5.3 现象:周度复盘报告中“信号成功率”指标忽高忽低,无法归因
原因:复盘Agent计算成功率时,未排除“信号有效期内停牌”或“信号发出后次日跌停无法买入”的订单,导致分母失真。
解决:在订单生成阶段(执行Agent)就标记order_status:'pending'(待执行)、'filled'(已成交)、'canceled'(因停牌/跌停取消)。复盘时只统计order_status=='filled'的订单,分母=已成交订单数,分子=已成交且盈利订单数。
5.4 现象:多Agent间信号传递延迟,导致执行Agent处理的是3天前的信号
原因:择时Agent输出CSV文件,执行Agent定时扫描文件修改时间,但Linux系统文件时间戳精度为秒级,且网络存储可能存在缓存。
解决:改用SQLite数据库作为Agent间通信媒介。择时Agent插入记录到signals表,执行Agent查询WHERE status='pending' AND valid_until >= ?,处理完后更新status='processed'。SQL语句自带事务保证,杜绝竞态。
5.5 现象:更换股票池(如从沪深300切换到中证1000)后,所有信号失效
原因:特征工程中使用了全局统计量(如“全市场成交额均值”),当股票池变更,这些统计量分布偏移,导致特征失真。
解决:所有跨周期统计量必须按股票池重新计算。在build_features函数中,传入当前股票池列表,atr_20d_ratio_to_1y_mean改为“该股票池内个股ATR均值的1年分位数”,而非全市场均值。
6. 周度复盘的进阶技巧:用SHAP值定位策略失效根源与动态调整信号阈值
6.1 为什么不用简单的“胜率排序”,而要用SHAP做归因?
胜率只能告诉你“breakout信号最近不好用”,但无法回答“是因为突破位置选错了?还是因为突破后量能跟不上?或是因为大盘处于S3状态却强行执行?”。SHAP值能分解每个特征对单笔交易盈亏的贡献:
# 在复盘Agent中计算SHAP import shap explainer = shap.TreeExplainer(state_classifier_model) shap_values = explainer.shap_values(X_weekly) # X_weekly: 本周所有成交订单的特征矩阵 # 按特征聚合SHAP绝对值均值 feature_importance = np.abs(shap_values).mean(axis=0).mean(axis=0) # shape=(12,)输出示例:
{'atr_20d_ratio_to_1y_mean': 0.42, 'rsi_14d': 0.31, 'industry_rotation_strength': 0.18, ...}。若发现atr_20d_ratio_to_1y_mean贡献度骤降(从0.42→0.15),说明近期信号有效性不再由波动率主导,需检查是否进入低波动周期。
6.2 动态阈值调整:让系统自己学会“什么时候该保守”
固定置信度阈值(如0.7)在不同市场状态下效果差异巨大。我们设计动态阈值机制:
| 当前市场状态 | 推荐置信度阈值 | 调整逻辑 |
|---|---|---|
| S1(高波动+高流动性) | 0.65 | 流动性充足,允许更高容错 |
| S2(低波动+趋势初现) | 0.75 | 趋势脆弱,需更高确定性 |
| S3(高波动+流动性枯竭) | 0.0(熔断) | 直接禁用所有信号 |
| S4(低波动+横盘震荡) | 0.70 | 震荡市需耐心,避免频繁交易 |
该阈值表存于config/dynamic_thresholds.json,复盘Agent每周根据state_distribution_last_4w(过去4周各状态出现频率)微调阈值:若S2状态占比连续2周<10%,则将其阈值下调0.05。 |
6.3 复盘报告的落地动作清单:把分析转化为下周操作
一份合格的周度复盘报告,必须包含可执行动作,而非仅描述现象。我们在strategy_decay_report.md中强制包含:
- ✅已确认问题:
breakout信号在S2状态下胜率下降至41%(历史均值58%) - 🛠️根因定位:
SHAP显示'rsi_14d'贡献度从0.31→0.12,说明突破时RSI超买程度不足 - 📝下周动作:
调整breakout信号触发条件:要求RSI(14) > 65(原为>55),并在信号字典中新增字段'rsi_requirement': 65 - 📊验证指标:
下周监控:满足新RSI条件的breakout信号数量、其中S2状态下信号占比、平均持有天数
这套机制让复盘不再是“总结过去”,而是“设计下周”。我坚持了18个月,最大的教训是:不要相信任何未经周度复盘验证的信号逻辑,哪怕它在回测中表现完美——市场状态永远在变,而你的系统必须比它变得更快。希望帮到你。
本文还有配套的精品资源,点击获取