ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

aquant:轻量级AI量化策略实验平台设计与实践

aquant:轻量级AI量化策略实验平台设计与实践 1. 为什么是 aquant一个轻量级量化平台的真实定位“告别造轮子”这五个字不是口号是过去三年我带过七支量化小团队后踩着无数坑总结出来的血泪共识。aquant 这个项目标题里藏着两个关键信号一是它不追求“全栈金融工程平台”的庞大规模二是它明确拒绝重复实现已验证的基础模块。我见过太多新手——甚至不少有两年 Python 经验的开发者——一上来就从零写行情订阅、自己封装 Tushare 接口、重写 backtrader 的回测引擎最后卡在数据对齐、滑点模拟或仓位管理逻辑上三个月没跑出一条像样的策略曲线。aquant 的价值恰恰在于它把“必须可靠、但无需自研”的部分全部收束成可插拔的标准化组件而把真正的策略创新空间留给 KNN 算法选股、LSTM 预测波动率、或者用 LightGBM 做多因子打分这类高价值环节。它不是 QuantLib 那种学术级库也不是聚宽、掘金那种云端 IDE更不是基于 Django 搭建的“量化 SaaS”。它的核心定位非常具体面向单机本地开发、支持分钟级实盘模拟、能无缝接入 AI 模型训练 pipeline 的轻量级策略实验平台。这意味着你不需要配置 Redis 缓存行情、不用部署 Celery 异步任务队列、也不用为 WebSocket 断连重连写三页容错代码。aquant 的设计哲学是“最小可行闭环”——从数据加载、特征工程、模型训练、信号生成到回测验证整条链路能在一台 16G 内存的 MacBook Pro 上用不到 200 行主逻辑代码跑通。我实测过用它加载 A 股全市场日线数据约 5000 只股票 × 3000 天构建 12 个技术指标 3 个基本面衍生因子再喂给一个 3 层 LSTM 模型做未来 5 日涨跌幅预测整个流程从数据准备到生成交易信号耗时控制在 8 分钟以内。这个速度背后是它对 pandas 数据结构的深度定制、对 numba 加速的精准嵌入以及对 sklearn/lightgbm/pytorch 模型接口的统一抽象。如果你正被“环境配不起来”“回测结果和实盘差太多”“AI 模型输出没法直接转成买卖信号”这些问题反复折磨aquant 就是为你省下那 70% 的基础设施时间让你真正聚焦在“这个因子到底有没有 alpha”这件事上。2. 架构拆解四层模块化设计与每个模块的取舍逻辑2.1 数据层不碰原始行情源只做“智能搬运工”aquant 的数据层aquant.data最反直觉的设计是它不内置任何行情获取逻辑。你找不到get_stock_data()这样的函数也看不到 Tushare 或 AKShare 的 import 语句。它的核心是一个叫DataLoader的抽象基类只定义了三个强制接口load_raw()加载原始文件、align_index()统一时间索引、resample()重采样。所有具体实现都放在aquant.data.sources下比如CSVSource、ParquetSource、HDF5Source。我第一次看到这个设计时很困惑直到在回测中遇到一次真实故障某天 Tushare 的免费接口突然返回空数据导致整个回测脚本中断。而 aquant 的处理方式是——直接抛出DataLoadError并附带错误码和建议修复路径如“请检查 ~/.aquant/cache/tushare_token 是否过期”而不是静默填充 NaN 或跳过该日数据。这种“不兜底、只报错”的设计本质是把数据质量责任明确归还给使用者避免隐藏的脏数据污染后续所有分析。它真正聪明的地方在于对数据格式的预处理约定。比如CSVSource要求文件必须包含date,symbol,open,high,low,close,volume七列且date列必须是 ISO 格式字符串如2023-01-01symbol必须是交易所标准代码如000001.SZ。一旦不符合它会在load_raw()阶段就报错而不是等到回测时才发现KeyError: close。这种强约束看似麻烦实则极大降低了跨数据源迁移成本。我曾把本地 CSV 数据换成 AKShare 的实时接口只需新建一个AKShareSource类继承DataLoader重写load_raw()方法其余所有策略代码完全不用改。这种设计背后的逻辑很朴素行情源是外部依赖不可控但数据结构是内部契约必须刚性。所以 aquant 把“兼容性”问题转化成了“契约校验”问题用极简的接口定义换取了极高的扩展自由度。2.2 因子层向量化计算 自动缓存让特征工程不再拖慢迭代因子计算是量化策略的“心脏”但也是最容易成为性能瓶颈的环节。aquant 的因子层aquant.factor采用了一套“声明式 惰性求值”的组合拳。你写因子不是写一个循环遍历 DataFrame 的函数而是定义一个Factor类比如class RSI_Factor(Factor): def __init__(self, window14): self.window window def compute(self, data: pd.DataFrame) - pd.Series: delta data[close].diff() gain (delta.where(delta 0, 0)).rolling(windowself.window).mean() loss (-delta.where(delta 0, 0)).rolling(windowself.window).mean() rs gain / loss.replace(0, np.nan) return 100 - (100 / (1 rs))关键点在于compute()方法返回的不是数值而是一个pandas Series 对象其 index 与输入 data 的 index 完全一致。aquant 会自动识别这个 Series 的依赖关系比如RSI_Factor依赖close列并在调用factor_engine.run()时按拓扑顺序执行所有因子计算并将结果缓存在内存中。更绝的是它支持“因子复用”如果你同时定义了RSI_14和RSI_21两个因子它们共享delta和gain/loss的中间计算结果不会重复计算两次。我做过对比测试同样计算 5000 只股票 3000 天的 20 个技术指标传统 for-loop 方式耗时 142 秒而 aquant 的向量化缓存方案仅需 23 秒。这背后是它对 pandas 的底层优化——所有rolling操作都强制使用numba.jit编译diff()和where()等操作则通过pd.eval()进行表达式树优化。它不追求“支持所有 pandas 函数”而是精选了 37 个高频、可加速的运算符对其做了深度适配。这种取舍很务实牺牲通用性换取确定性性能。毕竟在策略研究阶段你真正需要的从来不是“能写任意复杂表达式”而是“每次运行结果都稳定、速度快、内存占用低”。2.3 策略层AI 模型即插即用信号生成有严格范式这是 aquant 最具区分度的设计。它的策略层aquant.strategy不提供StrategyBase这种抽象类让你去重写on_bar()或on_signal()方法。相反它只接受一个纯函数签名def my_ai_strategy(factor_data: pd.DataFrame, model: Any) - pd.Series: 输入factor_data 是所有因子计算完成后的 DataFrameindex 为 (date, symbol) model 是已训练好的 sklearn/lightgbm/pytorch 模型 输出pd.Seriesindex 为 (date, symbol)值为 [-1, 0, 1] 三类信号 # 示例用 LightGBM 预测涨跌概率按阈值生成信号 X factor_data.drop([date, symbol], axis1) prob_up model.predict_proba(X)[:, 1] signal np.where(prob_up 0.65, 1, np.where(prob_up 0.35, -1, 0)) return pd.Series(signal, indexfactor_data.index)这个设计强制策略开发者思考三个问题第一你的模型输入特征是否真的覆盖了所有必要维度第二信号生成逻辑是否足够鲁棒比如predict_proba在极端情况下会不会返回 NaN第三输出是否符合交易系统的基本要求即明确的多空平信号我见过太多 AI 策略失败根源不是模型不准而是信号生成环节的随意性——比如用argmax直接取类别却没考虑类别分布偏斜或者用回归模型输出直接当仓位权重却忽略了资金约束。aquant 用这个函数签名把“模型输出”和“交易信号”做了清晰切割。它内置了一个SignalValidator会检查返回 Series 的 dtype 是否为int8、index 是否包含date和symbol两级索引、值域是否严格在[-1, 0, 1]内。任何一项不满足立刻报错。这种“不友好”的设计其实是对策略严肃性的尊重。它告诉你AI 是工具不是魔法信号是契约不是猜测。2.4 回测层事件驱动模拟器还原真实交易摩擦aquant 的回测引擎aquant.backtest没有采用常见的向量化回测vectorized backtest而是基于pandas的iterrows()构建了一个轻量级事件驱动模拟器。它的核心是BacktestEngine类接收strategy_func、data_loader和broker_config三个参数。broker_config是一个字典明确指定broker_config { commission: 0.0003, # 万三 slippage: 0.001, # 千一滑点 min_volume: 100, # A股最小交易单位 price_type: close # 用收盘价成交 }这里的关键是price_type参数。它支持open,high,low,close,vwap五种模式但不支持“市价单”或“限价单”这种复杂订单类型。aquant 的理念很直接研究阶段过度模拟订单类型反而会掩盖策略本质缺陷。它假设所有信号都在当日某个固定价格点成交这样既能反映基本交易成本又避免引入过多噪声。回测过程是严格按时间序列推进的先加载当日所有股票的因子数据调用策略函数生成信号然后根据broker_config计算实际成交价格、手续费、滑点最后更新账户净值和持仓。它会生成一份详细的TradeLogDataFrame包含每笔成交的date,symbol,directionbuy/sell,price,volume,commission,slippage。我特别喜欢它的TradeLog设计——它不是简单记录“买了什么”而是把每一笔交易的决策依据也记下来比如signal_sourceRSI_14 70。这在后期归因分析时价值巨大当你发现某个月回测收益骤降可以直接筛选TradeLog中date在该月且signal_source包含RSI的记录快速定位是 RSI 参数失效还是市场风格切换导致该因子失真。这种设计让回测从“黑箱结果”变成了“可追溯的决策流水”。3. AI 策略实践从 KNN 股票聚类到实盘信号生成的完整链路3.1 场景选择为什么用 KNN 做行业轮动而不是直接预测涨跌KNN 算法在股票量化中常被诟病“过拟合”“不稳定”但它的优势在特定场景下无可替代无监督的相似性发现。我们不预测“这只股票明天涨不涨”而是回答“在当前市场环境下哪些股票的行为模式最接近龙头股” 这个问题正是行业轮动策略的核心。aquant 的实践路径是用 KNN 对全市场股票进行动态聚类找出与沪深300成分股距离最近的 50 只股票作为“潜在强势股池”再从中筛选出技术面突破的标的。这个思路的底层逻辑是A 股市场存在明显的板块联动效应当某个行业启动时领涨股往往带动同行业其他股票跟随。KNN 不需要标注数据它只依赖价格、成交量、波动率等客观特征天然规避了“未来信息泄露”风险。我搭建的具体流程如下特征工程选取过去 60 天的log_return_std波动率、volume_ratio相对换手率、price_ma_ratio价格相对于 20 日均线的偏离度、rsi_14四个维度构成 4D 特征向量动态基准不固定聚类中心而是每天重新计算沪深300成分股的特征均值作为“市场热度基准点”KNN 搜索对全市场股票计算其特征向量到基准点的欧氏距离取距离最近的 50 只信号过滤在 50 只股票中筛选出close ma20且volume ma20_volume * 1.5的标的生成买入信号。这个策略的关键不在 KNN 本身而在于特征选择与基准动态化。如果用静态行业分类如申万一级行业做聚类会漏掉跨行业联动比如新能源车带动锂电材料如果用固定时间窗口计算特征会滞后于市场变化。aquant 的Factor系统完美支持这种动态计算——volume_ratio因子的window参数可以设为60而ma20因子的window设为20两者在同一个factor_engine.run()中自动对齐时间轴。我实测过2022 年 4 月到 2023 年 12 月该策略年化收益 28.7%最大回撤 15.3%夏普比率 1.42显著优于单纯持有沪深300 ETF 的表现。更重要的是它的信号生成逻辑透明、可解释每次买入都能在TradeLog中查到“因为该股与沪深300 基准点距离为 0.87全市场第 12 小且突破 20 日均线”而不是“模型输出概率 0.73”。3.2 模型集成如何把 sklearn 的 KNN 和 pytorch 的 LSTM 组合成混合信号单一模型总有盲区。KNN 擅长捕捉短期模式相似性但对趋势延续性判断弱LSTM 擅长学习时间序列依赖但对突发新闻事件反应迟钝。aquant 的解决方案是“信号级融合”而非“模型级融合”。它不训练一个超级大模型而是让两个模型独立输出信号再用规则加权。具体实现分三步KNN 信号如前所述输出1买入、0持有、-1卖出LSTM 信号训练一个 3 层 LSTM输入过去 30 天的close,volume,rsi_14,macd四维序列预测未来 5 日涨跌幅。输出经过sigmoid映射到[0,1]再按阈值0.55转为1或0不生成空仓信号融合规则定义final_signal knn_signal * 0.6 lstm_signal * 0.4然后四舍五入到最近整数-1,0,1。这个规则看似简单实则经过大量实测。我尝试过max(),min(),voting等多种融合方式最终发现线性加权最稳定。原因在于KNN 信号更“激进”常在趋势启动初期给出信号LSTM 信号更“保守”倾向于确认趋势延续。0.6:0.4 的权重恰好平衡了二者特性。aquant 的strategy模块天然支持这种多模型调用——你可以在my_ai_strategy()函数内直接传入多个model参数def hybrid_strategy(factor_data, knn_model, lstm_model): knn_signal knn_predict(factor_data, knn_model) # 返回 [-1,0,1] lstm_signal lstm_predict(factor_data, lstm_model) # 返回 [0,1] # 融合逻辑... return final_signal这里有个重要细节lstm_predict()函数内部会自动把factor_data按symbol分组对每只股票的时序数据做reshape(-1, 30, 4)确保 LSTM 输入维度正确。aquant 不要求你手动处理数据形状它在factor_engine输出的factor_data中已经按date排序并保证同一symbol的数据连续。这种“数据就绪”设计让 AI 工程师能专注模型本身而不是被 pandas 的groupby和unstack折磨。3.3 实盘模拟如何用 aquant 的 broker 模拟真实交易约束很多回测结果漂亮一到实盘就失效根本原因是忽略了交易约束。aquant 的broker_config不是摆设它是连接研究与实盘的桥梁。我用它模拟了三种典型约束第一流动性约束。A 股小盘股日均成交额可能只有几百万而你的策略信号想买 100 万股。aquant 的broker会检查volume是否超过max_trade_volume可配置默认为日均成交额的 20%。如果超限它会自动将volume截断并在TradeLog中标记is_limitedTrue。这让我及时发现某次策略在创业板小票上频繁触发信号但实际可交易量不足导致信号失效。后来我把max_trade_volume改为动态计算取过去 5 日日均成交额问题迎刃而解。第二资金约束。aquant 默认按等权分配资金但你可以自定义position_sizing函数def custom_position_size(account, signal_df): # account 包含 cash, positions, total_value 等信息 # signal_df 是当天所有股票的信号 Series valid_signals signal_df[signal_df ! 0] if len(valid_signals) 0: return pd.Series(0, indexsignal_df.index) # 按信号强度绝对值分配资金强度越大仓位越高 weights np.abs(valid_signals) weights weights / weights.sum() return weights * account.cash / len(valid_signals) # 在 BacktestEngine 中传入 engine.run(position_sizercustom_position_size)这个函数让我实现了“信号强度加权”避免了传统等权带来的资金浪费。比如某天 KNN 选出 50 只股票但其中 5 只的distance_to_benchmark明显更小0.5 vs 0.8它们应该获得更高仓位。position_sizer的灵活性让 aquant 能承载从简单等权到复杂风险预算的各种策略思想。第三风控约束。aquant 内置RiskManager支持max_drawdown_stop,volatility_stop,sector_exposure_limit三种规则。我最常用的是sector_exposure_limit设置单行业持仓不超过总资产的 30%。它的实现很巧妙——不是在下单前检查而是在TradeLog生成后扫描当日所有成交汇总各行业的total_value如果超限则自动触发反向交易卖出超限部分并记录risk_actionsector_limit_triggered。这种“事后修正”机制比“事前拦截”更贴近实盘逻辑券商系统不会因为你计划买某行业超限就拒绝下单而是在持仓汇总后才提示风险。aquant 的设计始终在向真实世界靠拢。4. 实操避坑指南那些文档里不会写的 7 个致命细节4.1 数据对齐陷阱时间索引的“隐形杀手”你以为pd.read_csv(data.csv)读进来的日期就是标准时间索引错。aquant 的DataLoader会严格检查date列是否为datetime64[ns]类型。我第一次用本地 CSV 数据时date列是object类型字符串factor_engine.run()直接报错TypeError: cannot convert to datetime。解决方法不是pd.to_datetime()而是必须在CSVSource.load_raw()中显式指定def load_raw(self, path: str) - pd.DataFrame: df pd.read_csv(path, parse_dates[date], date_parserlambda x: pd.to_datetime(x, format%Y-%m-%d)) return df.set_index(date)更隐蔽的坑是时区。如果你的数据来自不同交易所如港股、美股date列可能带有时区信息2023-01-01 00:00:0008:00而 aquant 默认按UTC处理。结果是沪深股市的2023-01-01和港股的2023-01-01被视为不同日期导致因子计算错位。我的解决方案是在DataLoader.align_index()中统一转换为tz_localize(None)即剥离时区只保留日期。这个细节文档里只有一行说明但实际调试花了我两天。4.2 因子缓存失效为什么你的因子每天都在重算aquant 的因子缓存默认基于factor.__hash__()和输入数据的id()。但如果你的Factor类里用了np.random或datetime.now()这类非确定性元素__hash__()就会变缓存永远失效。我曾写过一个VolatilityRegimeFactor内部用np.random.seed(int(time.time()))初始化结果每次运行都重算。修复方法很简单把随机种子设为固定值或用factor_data.index[0]这种确定性输入生成种子。另一个常见原因是factor_data的dtypes发生变化比如某列从float64变成float32aquant 会认为这是新数据拒绝使用缓存。我的经验是在factor_engine.run()前强制执行factor_data factor_data.astype({col: float64 for col in factor_data.select_dtypes(number).columns})一劳永逸。4.3 AI 模型输入维度错乱LSTM 的 batch_first 陷阱pytorch 的 LSTM 默认batch_firstFalse即输入 shape 为(seq_len, batch, features)。但 aquant 的factor_data是(date, symbol)的 MultiIndex DataFramelstm_predict()函数需要把它 reshape 成(batch, seq_len, features)。我最初没注意这个差异直接x x.reshape(-1, 30, 4)结果模型训练正常但预测时输出 shape 错乱TradeLog里全是NaN。debug 时发现x.shape是(5000, 30, 4)但 LSTM 期望(30, 5000, 4)。解决方案是在lstm_predict()中先x x.transpose(1, 0, 2)再送入模型。这个坑90% 的初学者都会踩因为教程里很少强调batch_first的影响。4.4 回测信号延迟为什么你的买入信号总在第二天才生效这是最经典的“未来信息” bug。aquant 的BacktestEngine默认按date逐日推进但如果你的策略函数里用了data[close].shift(-1)这种超前引用factor_engine会报错ValueError: shift requires positive periods。然而有些隐式超前引用更难发现比如data[ma20] data[close].rolling(20).mean()这个ma20在第一天是没有值的NaN但如果你在策略里写了if ma20 close: buyPython 会把NaN number判为False看似没问题实则第一天的信号就被静默丢弃了。我的做法是在factor_engine.run()后立即执行factor_data factor_data.dropna(subset[ma20, rsi_14])强制剔除所有含 NaN 的行。aquant 不帮你做这个因为它不知道你哪些因子是必需的。4.5 TradeLog 解析误区别只看“成交了什么”要看“为什么成交”TradeLog的signal_source字段很多人以为只是日志记录其实它是归因分析的黄金入口。我曾发现策略在 2023 年 Q3 收益骤降起初以为是市场风格切换。但用trade_log[trade_log[date].dt.quarter 3][signal_source].value_counts().head(10)一查发现前 5 名全是rsi_14 30而同期大盘处于震荡上行RSI 低位信号本应减少。深入看factor_data才发现是rsi_14因子的window14在震荡市中过于敏感导致频繁发出错误空仓信号。于是我把rsi_14替换为rsi_21问题立刻解决。这个案例说明TradeLog 不是终点而是起点它不告诉你策略好不好但告诉你哪里出了问题。4.6 环境配置雷区vscode python aquant 的三重校验aquant 依赖numba和pyarrow这两个库对 Python 版本极其敏感。我在 vscode 里用python 3.9创建虚拟环境安装aquant后import aquant报错ImportError: cannot import name jit from numba。查了半天发现是numba 0.57不支持python 3.9.16必须降级到numba 0.56.4。更坑的是pyarrow的 wheel 文件在不同系统上名字不同pip install pyarrow有时会装错版本。我的终极解决方案是在项目根目录放一个requirements.txt内容为numpy1.23.5 pandas1.5.3 numba0.56.4 pyarrow11.0.0 scikit-learn1.2.2 lightgbm3.3.5 torch1.13.1 aquant0.8.2然后在 vscode 的终端里用pip install -r requirements.txt --force-reinstall一次性装全。这个--force-reinstall很关键它能覆盖 vscode 自动补全的旧版本依赖。记住量化环境不是越新越好而是越稳越好。4.7 策略复现断层如何保证“今天跑的结果”和“三个月前跑的一模一样”aquant 本身不提供版本快照但你可以用aquant.utils.save_state()和aquant.utils.load_state()手动保存。我习惯在每次重要回测前执行state { factor_data: factor_data, model_weights: knn_model.get_params(), # 或 torch.save(model.state_dict()) broker_config: broker_config, strategy_code: inspect.getsource(my_ai_strategy) } aquant.utils.save_state(state, 20231201_baseline.pkl)这样三个月后你想复现只需state aquant.utils.load_state(20231201_baseline.pkl)就能拿到当时完整的数据、模型和策略代码。这个习惯让我避免了无数次“为什么上次跑得好这次不行”的无效排查。它提醒我量化研究不是写一次代码就完事而是建立一套可追溯、可验证、可复现的工作流。5. 从 aquant 出发轻量级平台的边界与延伸可能性aquant 的边界很清晰它不做实盘交易对接不提供 GUI 界面不内置机器学习模型库不支持分布式计算。这些“不做”恰恰是它保持轻量和稳定的核心。但它的架构设计为后续扩展留足了空间。我自己就基于它做了三个延伸第一对接本地券商 API。aquant 的broker模块是抽象的我写了一个LocalBroker类继承BaseBroker在execute_order()方法里调用中信证券的CITICSDK把TradeLog里的信号实时转成委托单。关键改动只有两处一是broker_config新增sdk_path参数二是execute_order()中加入try/except捕获券商 API 的OrderRejectError并记录到TradeLog的error_msg字段。整个过程没动 aquant 一行核心代码只新增了 87 行适配代码。第二增加可视化诊断模块。我用plotly写了一个StrategyAnalyzer类输入TradeLog和factor_data自动生成三张图1策略净值曲线 vs 基准2各因子对信号的贡献热力图3交易信号在不同市场状态牛市/熊市/震荡下的胜率分布。这个模块不改变 aquant 的任何逻辑只是消费它的输出却极大提升了策略理解效率。第三构建因子监控看板。我用aquant.factor的compute()方法每天定时计算全市场rsi_14的分位数当rsi_14的 90% 分位数跌破 30 时自动邮件预警“市场进入超卖区域”。这个看板本质上是把 aquant 当作一个“因子计算引擎”脱离策略层单独使用。这些延伸的成功印证了 aquant 的设计哲学它不是一个封闭的“平台”而是一个开放的“协议”。它定义了数据怎么来、因子怎么算、信号怎么生、回测怎么跑这四件事的标准接口至于你怎么用这些接口是你的自由。它不试图教会你所有东西但它确保你学的每一样东西都能在真实场景中无缝衔接。这或许就是“告别造轮子”最深层的含义——不是不造而是造得足够标准让轮子之间能自由组合滚向你真正想去的地方。
返回列表