上一篇文章里,我作为一个量化小白,花了不少篇幅拆解了社区大佬的小市值策略——五道风控防线、九项年报排雷、动态持仓调节……拆完之后我有一个很直观的感受:这套策略的内核是"活得久",靠的是极致风控 + 小市值弹性。 但最近两个月盯盘下来,我发现一个问题:ETF行情太好了。 纳指ETF、黄金ETF、港股互联网ETF……这些标的动辄月涨10%+,而且波动远小于小市值个股。我手里的小市值策略虽然在A股震荡行情里能吃到肉,但碰到「市场整体偏弱、ETF板块性行情轮动」的阶段,就会显得力不从心——小市值在休息,ETF在狂飙,两边接不上。 于是我开始在社区里大量翻阅ETF轮动相关的帖子和策略,学习各位大佬的思路。看了十几篇帖子之后,我萌生了一个想法:能不能把小市值和ETF轮动组合起来,搞一个"双核引擎"? 说实话,ETF轮动这块我是完完全全的新手。下面文章里涉及的ETF池子构建、动量打分、滤波器选择等,都是我在社区里反复学习、参考了多位大佬的策略和文章后,慢慢拼凑理解出来的。如果有理解不到位的地方,还请各位前辈多多指教? 小市值负责在A股弹性行情里捕捉超额;ETF轮动负责在板块行情里追趋势。两个引擎交替发力,互相补位——我给它起了个名字:「双龙出海」。 一、为什么要加ETF?先看数据说话 单跑小市值策略,5年30倍,年化收益极高,但有一个问题:回撤集中在大盘系统性下跌的阶段。当大盘暴跌时,小市值股票的跌幅往往比大盘还狠。 而ETF轮动策略有一个天然优势:它可以在全球资产中切换。A股不行就切港股ETF,港股不行就切纳指ETF,纳指也不行就直接买货币基金(银华日利 511880)躺平。 加上ETF之后效果怎么样?我跑了2021年至今的回测,结果直接把我看傻了: 指标 双龙出海(小市值+ETF) 基准(沪深300) 总收益 4632.93%(约47倍) -9.11% 年化收益 112.53% — 最大回撤 15.50% — 夏普比率 4.589 — 索提诺比率 7.535 — 胜率 56.7% — 盈亏比 2.505 — 阿尔法 1.115 — 贝塔 0.502 — 5年47倍,年化112%,最大回撤才15.5%——对比上一篇纯小市值的"5年30倍17回撤",收益从30倍提升到47倍,回撤反而从17%降到了15.5%。 说白了就是:加了ETF引擎之后,不仅赚得更多了,还更稳了。这不就是"既要又要"的最佳答案么? 维度 纯小市值 小市值 + ETF轮动 进攻性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ 防守性 ⭐⭐⭐ ⭐⭐⭐⭐⭐ 行情适应性 A股弹性行情 全市场+全球资产 空仓时的机会成本 高(只能买货币ETF) 低(自动切到强势ETF) 核心逻辑:小市值是矛,ETF轮动是盾——双核协同,攻守兼备。 二、架构设计:两个子账户,各管各的 双核策略的架构其实不复杂,核心就是子账户隔离: 总资金 100% ├── 子账户0:小市值策略(50%) │ └── 每周二调仓,选中证1000最小市值股票 └── 子账户1:五福ETF轮动(50%) └── 每日午后轮动,从100+只ETF中挑最强的1只 聚宽提供了 set_subportfolios 的功能,可以把一个账户拆成两个独立子账户,各自有独立的持仓和资金,互不干扰: set_subportfolios([ SubPortfolioConfig(cash=小市值资金, type='stock'), # 子账户0 SubPortfolioConfig(cash=ETF资金, type='stock'), # 子账户1 ]) 这样做的好处是: 策略之间完全隔离:小市值止损不会影响ETF持仓,反之亦然 资金分配清晰:各占50%,不会出现一边吃掉另一边资金的情况 独立记录收益:可以分别看两个策略各自的表现,方便归因分析 三、ETF轮动引擎拆解(站在大佬肩膀上) 小市值部分的逻辑我在上一篇已经拆过了,这里重点讲ETF轮动部分。 先说声感谢:ETF轮动这块我参考了社区里很多大佬的策略和思路,包括ETF池子的构建方式、动量打分的数学方法、震荡期切换机制等。我做的更多是「学习→理解→整合」的工作,算不上什么原创,更像是一个学习笔记。如果哪位大佬看到觉得某个模块眼熟,那大概率就是从您那学来的? 3.1 ETF池子:固定池 + 动态池,双池合并 ETF轮动的第一个难题是:从哪些ETF里选? 我学习到的一个很聪明的做法——固定池 + 动态池合并: 固定池(108只):手工精选的优质ETF,覆盖黄金、白银、纳指、恒生、各行业板块等 动态池(全市场扫描):每天从全市场ETF中自动筛选流动性达标的头部标的,去重后取行业最强的那一只 关键过滤逻辑: 剔除宽基指数ETF(沪深300、中证500等——这些不是行业轮动标的) 剔除债券/货币类ETF(短融、国债、可转债等——这些不是趋势标的) 流动性门槛:日均成交额低于全市场ETF均值/20000的,直接淘汰 行业去重:同行业多只ETF时,只保留成交额最高的那一只 最终合并出约100-150只ETF的"作战池"。 3.2 动量打分:拉普拉斯滤波 + 高斯滤波 ETF轮动最核心的问题是:怎么判断哪只ETF最强? 策略用的是加权线性回归 + 数学滤波器的组合方案: 动量得分计算(25日回看): 1. 对收盘价取对数 2. 用加权最小二乘法拟合趋势线(近期数据权重更高) 3. 斜率年化 → 年化收益率 4. 计算R²(拟合优度)→ 趋势稳定度 5. 动量得分 = 年化收益率 × R² 说人话就是:涨得快还涨得稳的ETF,得分最高。 但这还不够。策略额外加了两层数学滤波器,用来判断"该不该现在买": 滤波器 用途 触发条件 拉普拉斯滤波(正常期使用) 平滑价格,识别趋势 价格 > 滤波值 且 斜率 > 0.002 高斯滤波(震荡期使用) 更强的降噪,更保守 价格 > 滤波值 且 斜率 > 0.002 正常行情用拉普拉斯(灵敏一点,抓趋势),震荡行情切高斯(保守一点,少挨打)。两个滤波器自动切换,这个设计很巧妙。 3.3 震荡期自动切换:市场的"红绿灯" 策略有一套完整的"红绿灯"机制来判断当前是正常期还是震荡期: 进入震荡期(亮红灯)——满足任一条件: 沪深300的乖离率(BIAS)> 8%(涨太多了,有回调风险) RSI从70以上回落到65以下(超买后开始回落) 当天触发了止损(市场可能在变差) 退出震荡期(亮绿灯)——满足任一条件: 从近20日低点反弹超过4% 回撤收窄 + 多个复苏信号连续出现 震荡期持续超过20个交易日(强制退出,不能永远观望) 还有一个冷却期设计:每次红绿灯切换后,3个交易日内不允许再次切换,防止频繁反复。 3.4 多层过滤漏斗 从作战池到最终买入,要过七道关: 100+只ETF → 动量得分过滤(0 ≤ 得分 ≤ 5) → R²过滤(> 0.4,趋势不稳的排除) → 成交量过滤(量比 <1.8,异常放量的排除) → 短期风控(近3天没有单日跌 > 3%) → 溢价率过滤(可选,防止QDII高溢价陷阱) → 动态滤波过滤(正常期/震荡期分别用不同滤波器) → 最终只选1只! 没错,最终只持有1只ETF。这是一个非常激进但也非常纯粹的设计——全仓轮动,不分散。因为分散在ETF轮动里反而是稀释收益,不如集中火力追最强的那个。 3.5 分钟级止损:最后的保险丝 ETF策略还有一个分钟级的止损机制(every_bar 频率运行): 固定止损:当前价格跌破成本价的95%时,立刻卖出 日内跌幅止损(可选):如果当天相对昨收跌超过5%,也立刻卖出 触发止损后会标记 stop_loss_triggered_today,在午后13:10的检查中自动触发进入震荡期——止损不仅是保护本金,还会触发策略整体进入防守姿态。 四、双核协同的几个关键细节 4.1 资金分配 当前是简单的50:50平分。但我在想,未来可以根据市场状态动态调整——比如A股弹性好的时候多给小市值,ETF板块轮动强的时候多给ETF。这是一个可以继续优化的方向。 4.2 独立收益记录 策略每日收盘后会分别记录两个子策略的累计收益率,方便做归因分析。用 record() 函数输出到回测图表上,可以直观看到两条收益曲线。 def record_daily_performance(context): for i, strategy_key in enumerate(['strategy1', 'strategy2']): sub_portfolio = context.subportfolios[i] cumulative_return = (sub_portfolio.total_value / initial_cash - 1) * 100 # 记录到图表 record(小市值=小市值收益率, 五福ETF=ETF收益率) 4.3 滑点和佣金的差异化设置 很多人忽略的一个细节——股票和ETF的交易成本是不一样的: set_slippage(FixedSlippage(0.002), type="stock") # 股票:固定滑点 set_slippage(PriceRelatedSlippage(0.0001), type="fund") # ETF:比例滑点 # 股票佣金:万0.85,卖出还有千分之0.5的印花税 # ETF佣金:万0.5,无印花税 ETF的交易成本天然比股票低很多,这也是ETF轮动策略能频繁调仓的基础。 五、一个小白的学习感悟 不要只盯一个赛道。小市值再好,碰到风格切换也会歇菜。加一个ETF轮动引擎,相当于给自己多开了一个全球化的战场。 站在巨人肩膀上学得更快。ETF轮动这一整套逻辑,如果让我从零开始写,可能半年都写不出来。但社区里有那么多大佬无私分享代码和思路,我做的只是把不同的模块学懂、拼到一起。聚宽社区的开源氛围真的很好,感谢每一位分享策略的前辈。 数学滤波器很有意思。拉普拉斯、高斯这些信号处理领域的工具,用到金融数据上效果很好。作为小白第一次接触这些概念,说实话还没有完全吃透,后面还要继续啃论文和代码。 震荡期切换是ETF轮动的灵魂。没有这个机制,ETF轮动在震荡行情里会被反复打脸。有了红绿灯+冷却期,策略才能在"追趋势"和"认怂"之间优雅切换。 子账户隔离是个好设计。让两个策略各管各的,不争抢资金,不相互干扰。简单粗暴但有效。 最后还是那句话:风控是一切的基础。不管是小市值的五道防线,还是ETF的分钟级止损,核心都是同一个信仰——先活着,再赚钱。 六、实战检验 说得再好不如跑起来看。我已经把这套「小市值+ETF轮动_双龙出海」策略上传到了 9db量化竞技场,用真实模拟盘每天跑。 欢迎围观、拍砖。在9db上可以看到每天的交易记录、持仓变化、收益曲线。比回测更真实,因为是每天实时跑的——好不好,跑几周就知道了。 也欢迎大家去看看其他大佬的策略表现,和自己的策略做个对比。量化这条路,闭门造车不如多看多学。 作为一个刚入门的量化小白,文中很多理解可能不够深入甚至有偏差,欢迎各位大佬在评论区指正。也特别感谢社区里那些无私分享策略和思路的前辈们,没有你们的开源精神,就没有这篇学习笔记。 免责声明:以上内容仅为个人学习记录,不构成任何投资建议。股市有风险,投资需谨慎。 最近的股市行情,让不少散户朋友感到“苦不堪言”。我也知道你们难,每天盯盘复盘,甚至焦虑到夜不能寐。但我要泼一盆冷水:在交易的世界里,盲目的努力往往是负债。如果交易逻辑不对,越努力,可能亏损得越快。 今天我要分享的这十条交易口诀,当年只在极小的圈子里传阅。它们是老手们用血和泪换来的看家本事,是真金白银和数轮牛熊周期冲刷出的“生存底牌”。 我曾在券商工作多年,亲眼见过无数人破产,也见过少数人把小资金做成天文数字。最让我印象深刻的是“老臣”——早年一个挖煤矿发家的爆发户。他没有任何高超的技术背景,学历也一般,但他却成了圈内有名的顶级游资。他的秘诀很简单:不再自己瞎琢磨,而是站在成功者的肩膀上,将交易打磨成一套机械执行的系统。 如果你还在迷茫,请把这套口诀刻在心里。如果你觉得不好用,我随时接受你的“灵魂拷问”。 1.高位时的“反直觉”:别在风险转移时接棒 在股价高位时,普通散户的直觉往往是错的。看到放量拉升就觉得“人气爆棚”想冲进去,看到缩量横盘就觉得“没戏了”赶紧下车。这恰恰落入了主力转移风险的圈套。 “高位无量要拿,拿错也要拿;高位放量要跑,跑错也要跑。” 深度分析: 高位“无量”说明主力筹码高度锁定(锁仓),行情大概率还没走完,此时下车多半会拍断大腿。而高位一旦出现“巨量炸开”,别以为是承接力强,那其实是主力在制造虚假繁荣,利用流动性完成筹码向散户的换手出货。散户眼中所谓的“机会”,往往是主力换手出逃的掩护。记住,高位巨量不跑,等待你的可能就是长达数年的“高位站岗”。 2.低位布局:识别主力亮牌前的暗流 低位博弈,拼的是耐心,读的是“量在价先”的真理。 “低位放量要跟,量在价先。” 深度分析: **●**低位无量要等: 没人气、没成交并不代表这只票没戏,只是主力还没到亮牌的时候。除了那些毫无生气的“杂毛股”,此时需要静观其变,不必急于入场。 **●**低位放量要跟: 股价在底部突然放量,意味着筹码开始从散户手中向主力聚集。这是主升浪爆发的前兆,是资金入场的明确信号。在这个阶段,读懂成交量这个“先行指标”,比盯着波动看更有意义。 3.量价的共振与背离:识破“对倒”的骗局 学会识别主力“对倒”制造的假象,是你保护本金的关键。 **●**量增价平(撤退信号): 钱在大笔砸入,股价却原地踏步。这往往是主力在通过对倒制造虚假成交量,实则在偷偷撤退。 **●量增价升/**量平价升(上车信号): ♦****量增价升是典型的多头号角,必须果断跟进。 ♦****量平价升则说明量能极其稳定,上方几乎没有抛压,上升趋势已经稳固形成。 **●**量平价跌(出局信号): 当量能枯竭伴随股价阴跌,这是趋势转弱的先兆。此时别谈什么“信仰”,离场是保命的唯一选择。 4.趋势与缺口:资金强度的“质量测试” “淹死的都是会游泳的”,这句话送给那些总想在弱势行情里秀操作的技术派。在趋势面前,个人技术微不足道。 “大盘破位,空仓为王。” 深度分析: **●**顺势而为: 交易的第一条铁律永远是顺势。大盘趋势一旦破坏,再牛的高手也要收手。保住本金永远是第一优先级。 **●**缺口的价值: 带有跳空缺口的上攻,尤其是突破性缺口,如果回踩而不补,这就是最强的质量测试。它说明资金根本不想给任何人拿便宜筹码的机会,这是强弩之势。 **●**年线的生命力: 年线是股市的“生死分界线”。年线走平要高度警惕,说明趋势在激烈拉扯;而当年线上翘后的回踩,往往就是所谓的“黄金坑”,是难得的布局机会。 5.交易的底层哲学:系统胜过勤奋 市场从不讲道理,它只讲筹码交换。真正的赢家,从来不是最勤奋的那批人,而是能将逻辑打磨成系统并严格执行的人。 正如当年的老臣,他明白悟道的本质其实就是:放下一边倒的消息和基本面,回归纪律和心态。交易的第一条是纪律,第二条是心态。所谓的成功,就是不再受盘口波动的诱惑,只在系统发出信号时扣动扳机。 结语 股市不是比谁做得多,而是比谁活得久。当你能够冷静地对照口诀,剥离贪婪与恐惧的直觉,识别出主力的进退路线时,你就已经站在了成功者的肩膀上。 在下一次面对盘中“巨量拉升”的诱惑时,你会选择追随那种让你“血脉偾张”的直觉,还是选择坚守这套用血泪换来的交易系统?点个关注,我陪你一起在这个残酷的市场中,打磨出属于你自己的生存体系。 在港股数据归集、策略回测与实盘建模过程中,港股通标的定期、临时变动是造成回测失真、历史统计出错的高频诱因。不少研究人员直接将标的清单固化在代码配置中,忽略标的增减对应的生效时点,最终出现历史回测与实盘口径不一致问题。结合项目落地经验,从监管规则、日期判定、数据获取、工程优化几个维度做技术分享。 一、标的调整规则与生效时间逻辑 港股通标的调整由沪深交易所、港交所协同划定,分为季度定期调整与突发事件临时调整两类,所有变更细则以交易所官方公告为基准。公告关键要素包含调整类型、证券代码、基准参考日、正式生效日四项内容。 公告披露日期和实际生效日期并不统一,监管预留过渡期用于券商、清算机构完成系统参数配置: 季度定期调整:常规在公告发布后的第二个交易日正式生效; 临时异动调整:受退市、长期停牌等因素触发,生效区间多为公告后 1~3 个交易日。 实操举例:标的剔除公告于周三披露,个股通常延后数个交易日起移出港股通可交易标的池。 二、生效日期的两类获取方案 1. 人工查阅公告 从三大交易所官方披露平台调取公告文本,手动提取标的变更及生效时间,适合小范围标的抽样核验,难以满足批量自动化数据运维需求。 2. API 动态拉取(量化工程优选方案) 自动化数据架构下,通过接口动态同步标的范围与生效日期是标准化方案。多数通用行情接口仅返回当日有效标的,如需回溯历史变更时点,需接口支持变更明细与生效字段输出。 日常研究工作中借助 AllTick API 获取港股盘口与标的归属信息,接口同步附带港股通变更生效时间字段,以下为 WebSocket 订阅参考代码: import websocket import json ws_url = "wss://ws.alltick.co/stock" def on_message(ws, message): data = json.loads(message) # 数据包包含tick行情、港股通准入状态、标的调整生效日期 print("收到数据:", data) def on_open(ws): subscribe_msg = { "action": "subscribe", "stocks": ["00001.HK", "00700.HK"] } ws.send(json.dumps(subscribe_msg)) def on_error(ws, error): print("WebSocket错误:", error) def on_close(ws): print("WebSocket关闭") ws = websocket.WebSocketApp( ws_url, on_message=on_message, on_open=on_open, on_error=on_error, on_close=on_close ) ws.run_forever() 依托接口实时同步,系统可按任意历史日期精准判别个股是否隶属于港股通,有效支撑历史数据拼接、多周期回测建模。 三、量化建模与数据处理关键要点 杜绝代码内硬编码标的清单:季度常态化调样持续变更成分范围,静态清单会随时间逐步偏离真实市场结构; 以生效日期作为数据拆分边界:新增标的自生效日纳入统计样本,剔除标的从生效日起剔除,统一回测统计口径; 持续监控临时非标调整:突发风险事件带来的临时增减无固定周期,依托接口动态更新能够规避非常规变动带来的数据漏洞。 结语 港股通标的生效时间依托成熟的监管规则,小体量基本面研究可依托公告人工整理,系统化量化项目建议接入行情接口实现标的与生效日期动态管理。围绕生效日期搭建数据筛选逻辑,是提升数据源可信度、优化回测与实盘拟合度的重要一环。 在长期的量化策略研究与模型迭代过程中,我发现多数短线策略回测失真的核心原因,往往不在于算法逻辑,而在于行情数据颗粒度不足。标准行情数据源仅保留固定周期的K线与逐笔明细,盘中瞬时的盘口状态、小幅价格波动、量能微妙切换等高频细节都会被抹平。 这就导致很多短线模型在回测阶段表现优异,落地实盘却频繁失效。为了解决高频策略的数据偏差问题,我搭建了一套定时行情快照采集工具,通过实时行情接口持续抓取盘面状态,以5秒为固定周期完成全域标的数据存档,为短周期策略复盘、盘口规律统计、模型精度优化提供精细化的原始数据支撑。 这套工具的整体实现逻辑十分务实:持续订阅实时行情数据流,将动态刷新的盘口信息进行内存缓存,再通过定时任务完成数据切片落地。整体开发门槛较低,程序运行稳定性强,能够完美适配个人量化研究者的日常数据积累需求。 一、实时行情数据流订阅与缓存搭建 在高频数据采集场景中,轮询式数据请求存在延迟不稳定、接口请求冗余、容易丢失瞬时数据等问题,因此行业内主流方案均采用WebSocket长连接推送模式。建立稳定连接后,只需提前配置需要监控的标的列表,即可被动、持续接收全量Tick行情数据,最大程度贴合实时盘面变化。 在我的量化工具迭代过程中,会采用AllTick API稳定的推送服务作为行情数据源,保障高频采集场景下的数据完整性与低延迟特性。完整的行情订阅与本地缓存实现代码如下: import websocket import json current_snapshot = {} def on_message(ws, message): data = json.loads(message) symbol = data.get("symbol") if symbol: current_snapshot[symbol] = data def on_open(ws): req = { "action": "subscribe", "symbols": ["AAPL", "TSLA", "MSFT"] } ws.send(json.dumps(req)) ws = websocket.WebSocketApp( "wss://api.alltick.co/stock", on_message=on_message, on_open=on_open ) ws.run_forever() 代码中的 current_snapshot 字典是整套采集系统的核心枢纽,它会实时覆盖更新每一个订阅标的的最新盘口数据,始终保存当前最新的盘面状态,为后续定时快照存储提供完整、实时的数据基底。 二、基于多线程的5秒周期快照落地方案 市场行情是连续不间断波动的,但量化建模与数据分析需要结构化、离散化的时序数据。为了实现连续数据流的精准切片存储,我采用多线程分离的架构设计,将行情接收与数据存储逻辑解耦。 单独开启独立守护线程,以5秒为固定时间间隔,周期性读取内存中的最新缓存数据,批量写入本地文件或数据库,实现自动化、无人值守的数据采集。具体实现代码如下: import time import threading def save_snapshot(): while True: time.sleep(5) ts = time.strftime("%Y-%m-%d %H:%M:%S") for symbol, data in current_snapshot.items(): price = data.get("price") volume = data.get("volume") with open("snapshot.csv", "a") as f: f.write(f"{ts},{symbol},{price},{volume}\n") threading.Thread(target=save_snapshot, daemon=True).start() 该本地化CSV存储方案结构简洁、无需复杂配置,适合中小规模标的监控场景。若研究过程中需要同时订阅数十乃至上百只标的,可将存储逻辑升级为批量异步写入或数据库持久化,有效规避高频读写带来的性能瓶颈,保障系统平稳运行。 三、高频采集场景的数据优化策略 5秒级的高频采样模式,在多标的监控场景下会持续生成海量时序数据,若不做优化,容易出现文件冗余、读写卡顿、数据检索低效等问题。结合长期量化数据积累的实战经验,分享三套适配个人研究场景的轻量化优化方案: 第一,精简数据存储维度。舍弃行情数据中冗余的非核心字段,仅保留时间戳、标的代码、最新价格、成交量等模型回测必需的核心参数,在不影响研究精度的前提下压缩数据体积。 第二,优化IO读写逻辑。摒弃单条数据逐行写入的低效模式,采用批量攒批、异步写入的方式,降低频繁磁盘操作带来的性能损耗,提升程序运行效率。 第三,建立历史数据归档机制。定期对过期的历史快照数据进行分档存储与压缩处理,避免单一文件体积过大,保障长期数据采集与后续数据检索、分析的效率。 需要明确的是,行情快照数据与逐笔成交明细属于两类不同的研究数据源。快照数据记录的是特定时间节点的全局盘口静态状态,更适配短周期趋势研判、高频策略回测、盘口状态相关性分析等场景,是短线量化研究的核心数据之一。 四、5秒采样周期的量化逻辑解析 不少量化研究者在搭建快照系统时,都会纠结采样周期的参数选择。这个参数的核心取舍逻辑,是平衡数据信噪比与行情捕捉完整性。 1秒级的超高频采样会收录大量市场随机噪音,无效数据占比极高,不仅增加存储压力,还会干扰模型的特征提取;而10秒及以上的长间隔采样,时间颗粒度过于粗放,会丢失短线快速异动、瞬时量能拐点等关键交易信号,无法支撑高频策略的精细化回测。 经过多轮实盘测试与数据对比,5秒是适配短线量化研究的最优折中参数,既能完整捕捉有效短期行情波动,又能合理控制数据体量,适配绝大多数个人高频策略的研究需求。 五、实战运行效果与应用总结 我曾基于这套架构,长期对数十只标的进行不间断数据采集,实测系统延迟稳定维持在数十毫秒级别,数据同步精度较高。相较于传统手动抓数、平台导出历史数据等方式,自动化快照采集体系实现了时序数据的连续性与可控性,极大方便了后续的数据统计、可视化分析与策略迭代优化。 从量化应用角度来说,这套工具的落地门槛较低,核心稳定运行的关键仅有三点:可靠的行情长连接通道、精准的标的订阅配置、稳定的数据持久化逻辑。满足这三个条件,即可长期稳定完成数十至上百只标的的高频快照采集,为策略回测、模型优化、盘口规律研究提供高质量的精细化行情数据集,有效解决短线量化模型回测失真、拟合不准的行业常见问题。 我在投顾团队服务量化类客户时,发现一个高频痛点:客户用多路外汇行情源做策略回测,历史绩效优秀,一到模拟盘或实盘就频繁误触发。有一次核查 EUR/USD 网格策略,明明回测中价差套利机会清晰,实盘却总在欧美重叠时段出现反向开仓。逐笔核对后,问题出在伦敦和纽约两路 tick 数据的时间戳差了 5 小时,策略把不同时间点的价格当成了同一时刻处理。 对量化交易来说,行情数据的时间语义一致性是策略鲁棒性的基石。尤其是在外汇市场,跨时区、跨数据源的时间对齐如果没处理好,回测结果就是海市蜃楼。下面我把在实践中沉淀下来的一套方案分享出来,供各位朋友参考。 精准识别 API 时间字段 不同外汇行情 API 的时间载体差异很大。可能是秒/毫秒时间戳、ISO 8601 字符串,也可能是不带时区的本地时间。上手新接口时,我必定确认三件事:返回的是 UTC 还是本地时间;精度是秒还是毫秒;是否内置夏令时信息。曾有一个 API 返回 "2026-06-03T15:30:00" 却不带时区,实际上在夏季是 CEST,导致我的触发时间集体偏移一小时。解决方案就是强制将所有入站时间在入口处转为 UTC 时间戳,从源头建立单一标准。 标准时区转换流程 Python 生态下,使用 pytz 或 zoneinfo 可以稳定完成时区切换。核心步骤是解析字符串为 aware datetime,再调用 astimezone 转至目标时区。下例将 UTC 报价时间转为北京本地时间,方便结合国内交易时段分析: from datetime import datetime import pytz utc_time = datetime.strptime("2026-06-03T07:30:00Z", "%Y-%m-%dT%H:%M:%SZ") utc_time = utc_time.replace(tzinfo=pytz.UTC) # 转换为北京时间,校验策略在亚洲时段的触发逻辑 local_time = utc_time.astimezone(pytz.timezone("Asia/Shanghai")) print(local_time) 开发规范上,如果 API 同时提供时间戳和字符串,择其一作为系统内唯一标准,是避免隐式转换错误的重要纪律。 多源报价的时间对齐策略 实际生产环境中,我通常需要融合多个流动性提供方的 tick。例如 EUR/USD 同时从伦敦和纽约接入,直接将带不同时区语义的时间合并,会把未来信息带入回测。我的标准工序是: 全部报价转为 UTC; 按时序排序; 按策略特性选择对齐策略。 常用的三种策略中,“最近有效”对实盘延迟最友好;“均值填充”能在回测中平滑噪声、还原理论价;“线性插值”适合高频信号对时间连续性的要求。我会让客户根据策略的持仓周期和滑点容忍度选择最合适的方案,并在回测报告中注明所用的对齐方法,确保可复现。 数据库存储的最佳实践 落地存储时,我强制使用 UTC 时间戳作为主键。PostgreSQL 的 timestamptz 字段原生支持时区,可以杜绝多源数据的时间键冲突。分析端按需要转换成本地时间,无需修改底层。对于 tick 级别数据,只保留时间、价格和成交量三个核心字段,必要时对时间字段建立分区或索引,保证查询效率。 工程验证:以实时外汇接口为例 我在实验环境中使用过一些直连市场的实时数据接口,其中 AllTick 推送的 tick 直接采用 UTC 毫秒时间戳。通过 WebSocket 接入后,可以毫秒级完成时区对齐,交付给策略引擎的时间线完全统一。示例代码如下: import websocket import json from datetime import datetime import pytz def on_message(ws, message): data = json.loads(message) utc_ts = data['timestamp'] # 毫秒 utc_time = datetime.utcfromtimestamp(utc_ts / 1000).replace(tzinfo=pytz.UTC) local_time = utc_time.astimezone(pytz.timezone("Asia/Shanghai")) print(local_time, data['symbol'], data['price']) ws = websocket.WebSocketApp("wss://api.alltick.co/realtime", on_message=on_message) ws.run_forever() 通过这种方式,每一笔 tick 都携带了准确的 UTC 基准和本地映射,便于策略引擎和监控面板共同使用。 我的核心判断 外汇行情 API 的时间对齐问题,本质上是为策略建立一条可靠的时间基准轴。统一采用 UTC 作为内部标准,再根据业务需求转换时区、对齐多源数据,能规避绝大多数因时间语义差异引发的回测偏差和实盘误判。量化投顾的价值不仅在于策略开发,更在于帮客户把控这些数据层面的细节。时间统一了,数据可靠了,策略的稳定性才能经得起市场的检验。 摘要 用 Python 接国内期货行情,脚本跑通只是第一步。真正让监控误判、回测跑偏的,是三个更隐蔽的问题:volume_24h 和 volume 被当成同一个东西、last_price 和 close 被混用、异步处理只写了 async 没做去重和背压。本文基于 MCP 实测字段结构与审核事实库核验,用一张接口对照表、一段字段归一化示例和异步处理边界说明,帮你把这几个坑一次踩实。 一、脚本跑通了,但判断总是错 你写了一个 Python 脚本,接上国内期货行情,打算监控黄金期货的成交活跃度。逻辑很简单:成交量突然放大就触发提醒。 脚本跑通了。接口返回了数据。屏幕上跳出了数字。 但跑了一周,你发现两件事不对劲: 第一:同样的品种,同一时刻,ticker 接口返回的成交量和 K 线接口返回的成交量,数值差异很大。 第二:偶尔出现的“成交量飙升”告警,事后对照交易软件一看,根本没那回事。 排查之后发现两个问题: 问题一:你把 ticker 快照的 volume_24h 当成了单根 K 线的 volume。前者是过去 24 小时的累计成交量,后者是单根 K 线周期内的成交量。两者的字段口径不同,不能直接比较,数值会随测试时点变化。在你的监控逻辑里,阈值是按 K 线成交量设的,但数字用的是 ticker 的 24 小时累计值。误判是必然的。 问题二:你的异步回调函数收到推送后直接更新了界面,没有做去重和乱序处理。同一条 ticker 推送被处理了两次,界面上出现了一个瞬时尖峰,然后消失。 期货行情接入最隐蔽的问题,不是接口调不通,而是字段语义没分清、异步边界没处理好——脚本跑得越快,错得越稳定。 二、先把四类接口搞清楚 在写代码之前,先分清四类期货行情接口。它们的字段命名、数据口径和适用场景完全不同,但很多教程混着写。 接口类型 用途 关键字段 不能用来做什么 示例端点(以 TickDB 为例) ticker 快照 当前时刻快照:最新价、24h 成交量 last_price、volume_24h、timestamp 历史回测(无历史序列);volume_24h ≠ 单根 K 线成交量 GET /v1/market/ticker kline K 线 历史 K 线:开/收/高/低/量 close、volume、open、high、low 查当前实时价(最近一根可能未收盘);close ≠ last_price GET /v1/market/kline recent_trades 最近成交明细 price、quantity、side、timestamp 替代 ticker 快照(返回序列而非单一价格);部分品种可能无数据 GET /v1/market/trades WS trade 推送 实时成交推送 取决于具体频道和品种 需自行处理去重/乱序/断连/背压;不能假设所有品种都支持 — 一句话规则: 要当前最新价 → ticker + last_price 要历史 K 线或单根成交量 → kline + volume 不要把 kline 的 close 当 ticker 用,也不要把 ticker 的 volume_24h 当 kline 的 volume 用 三、同一个“成交量”,两种不同的口径 这是期货行情接入中最容易踩的坑,没有之一。 字段 来源 含义 你用错的表现 volume_24h ticker 快照 过去 24 小时的累计成交量 用它和单日成交量阈值比较,误报率飙升 volume kline 单根 K 线周期内的成交量 用它判断当日活跃度,口径完全不对 本次 MCP 实测字段结构确认:以黄金期货 au2608 为例,ticker 快照返回 volume_24h 字段,1d K 线返回 volume 字段。两者的字段口径不同,不能直接比较,数值会随测试时点变化。 如果你用的是同一个行情 API(本文以 TickDB 为例),ticker 端点的字段叫 volume_24h,kline 端点的字段叫 volume——两个字段名不重叠,本身就是一种设计上的字段语义隔离。如果你用的数据源两个字段都叫 volume,你就必须在代码层面自己区分。 正确做法:在字段映射层里显式区分。不要用同一个变量名 volume 同时接收 ticker 和 kline 的返回值。 四、合约代码和时间戳,不能凭直觉 合约代码格式 国内期货的合约代码没有交易所后缀。以上海期货交易所黄金期货为例: ✅ 正确 ❌ 错误 au2608 AU2608.SHF / au2608.SHFE 本次 MCP 实测确认 au2608、ag2608、a2607 等合约代码存在于期货品种列表中,ticker 和 kline 均可正常返回数据。 一个值得注意的设计:国内期货合约代码无交易所后缀(如 au2608),而 A 股代码带 .SH/.SZ(如 600519.SH),港股代码带 .HK(如 700.HK),美股代码带 .US(如 AAPL.US)。这种跨市场品种代码格式的差异,在单一数据源内统一管理,比在业务代码里硬编码映射更可持续——否则每加一个新市场,就要改一次正则表达式。 时间戳单位 ticker 的 timestamp 常见为 13 位毫秒 UTC,K 线的 time 也是毫秒 UTC。但不同接口的时间戳单位可能不同——recent_trades 的 timestamp 可能为秒级,WebSocket 推送的时间戳也可能因频道和品种而存在差异。 不要在代码里写死 ts / 1000 全局转换。逐接口核验后再做归一化。 数值精度 ticker 的 last_price 和 kline 的 close、volume 等数值字段,行情 API 通常以字符串返回。参与计算和比较时必须使用 Decimal,不要用 float。 五、字段归一化:把规范写进代码,而不是文档里 以下示例展示如何对 ticker 和 kline 的返回字段做显式归一化,避免字段语义混淆。示例中的价格和成交量数值为脱敏教学值,不对应当前行情。 """ 期货行情字段归一化教学示例 以 TickDB ticker 端点和 kline 端点的返回结构为例 注意:这是逻辑演示,非生产级代码。价格和成交量为脱敏教学值,不对应当前行情。 """ from decimal import Decimal, InvalidOperation # --- 结构化示例 payload(脱敏教学值,不对应当前行情) --- ticker_payload = { "symbol": "au2608", "last_price": "480.50", # 脱敏教学值 "volume_24h": "125000", # 脱敏教学值 "timestamp": 1779825600000 # 毫秒 UTC } kline_payload = { "symbol": "au2608", "open": "478.20", "high": "482.10", # 脱敏教学值 "low": "477.80", "close": "480.50", # 脱敏教学值 "volume": "15000", # 脱敏教学值,单根 K 线成交量 ≠ volume_24h "quote_volume": "7185000", # 脱敏教学值 "time": 1779782400000 # 毫秒 UTC } def normalize_ticker(raw) -> dict: """将 ticker 快照字段归一化。 以 TickDB ticker 端点 (GET /v1/market/ticker) 返回结构为例: - 价格字段:last_price - 成交量字段:volume_24h(24 小时累计) - 数据路径:data["data"] 为数组,每元素为一个品种快照 """ try: return { "symbol": raw["symbol"], "last_price": Decimal(str(raw["last_price"])), "volume_24h": Decimal(str(raw["volume_24h"])), "timestamp_ms": int(raw["timestamp"]), "_note": "volume_24h 为 24 小时累计,不等于单根 K 线 volume" } except (KeyError, InvalidOperation, ValueError) as e: return {"error": f"ticker 字段解析失败: {e}", "symbol": raw.get("symbol", "unknown")} def normalize_kline(raw) -> dict: """将 K 线字段归一化。 以 TickDB kline 端点 (GET /v1/market/kline) 返回结构为例: - 价格字段:close(收盘价),不等于 ticker 的 last_price - 成交量字段:volume(单根 K 线周期内),不等于 ticker 的 volume_24h - 数据路径:data["data"]["klines"] 为 K 线数组 """ try: return { "symbol": raw["symbol"], "open": Decimal(str(raw["open"])), "high": Decimal(str(raw["high"])), "low": Decimal(str(raw["low"])), "close": Decimal(str(raw["close"])), "volume": Decimal(str(raw["volume"])), "quote_volume": Decimal(str(raw["quote_volume"])), "time_ms": int(raw["time"]), "_note": "close 为 K 线收盘价 ≠ ticker last_price;volume 为单根 K 线成交量" } except (KeyError, InvalidOperation, ValueError) as e: return {"error": f"kline 字段解析失败: {e}", "symbol": raw.get("symbol", "unknown")} if __name__ == "__main__": t = normalize_ticker(ticker_payload) k = normalize_kline(kline_payload) print("ticker:", t) print("kline:", k) # ⚠️ t["volume_24h"] 和 k["volume"] 口径不同,不能直接比较 核心逻辑:normalize_ticker 和 normalize_kline 是两个独立函数,返回字段名不重叠。上游业务代码拿到归一化后的 dict,不会把两个字段混用。 六、异步处理:async 只是开始,不是终点 很多开发者的流程:用 asyncio 写 WebSocket 客户端 → 收到推送就回调 → 回调里更新界面。Demo 阶段跑几天没问题,就认为“异步处理搞定了”。 但 Demo 阶段的特点是:网络稳定、推送频率低、回调逻辑简单、无并发竞争。系统化运行后: 问题 Demo 为什么没发现 不处理的后果 消息去重 推送少,重复概率低 同一条 tick 被处理两次,出现瞬时尖峰 乱序到达 网络稳定时基本有序 “最新价”被旧数据覆盖,趋势判断出错 背压 回调简单,消费快于生产 波动剧烈时推送暴增,内存持续增长 超时与重连 网络稳定 断连期间数据真空,重连后无补全 失败关闭 手动 Ctrl+C 缓冲区数据丢失、文件未关闭 异步处理不等于高频交易能力。 本文讨论的去重、背压、乱序、超时、失败关闭,是数据接入层面的工程问题,与交易执行层面的低延迟是完全不同的概念。 一个基本的异步处理框架至少需要:回调只入队列不做阻塞操作、消费者线程做去重和排序、队列满时降级而非无限堆积、断连自动重连并通过 REST 补全缺口。 本次 MCP 实测字段结构确认:get_ticker 和 get_kline 对 au2608 等期货品种返回了有效数据;get_recent_trades("au2608", type="futures") 返回空结果。因此本文的异步处理讨论仅针对 ticker 和 kline 场景。 七、以 TickDB 为例:从字段映射到异步边界,接口设计如何落地 前面六节分别讲了四类接口区分、成交量口径、代码格式规范、字段归一化和异步处理边界。这一节把这些点串起来,以 TickDB 为样本,展示一个行情 API 如何在接口设计层面回应这些工程约束。选择它作为样本,不是因为“它最好”,而是因为它的设计在以下几个维度上提供了可核验的对照——你可以用同样的维度去检查你实际使用的数据源。 字段映射层(对应第二、三、五节的问题): 接口 端点 价格字段 成交量字段 数据路径 ticker 快照 GET /v1/market/ticker last_price volume_24h(24h 累计) data["data"] 数组 kline K 线 GET /v1/market/kline close volume(单根周期内) data["data"]["klines"] 数组 异步处理边界层(对应第六节的问题): WebSocket 心跳:客户端每 1 秒发送 {"cmd":"ping"},服务端回复 {"cmd":"pong"} 错误码语义:3001 限流 → 读取 Retry-After 头 → 退避重试;1001 鉴权失败 → 直接阻断,不重试 断连补全:WebSocket 不保证补齐断连期间数据,需自行通过 REST 接口补全 这些设计把字段映射和异步边界变成了可验证的 API 行为,而不是文档里的抽象描述。如果你用其他数据源,用同样的维度去核验即可。 八、结尾 期货行情接入的难点,从来不是“能不能跑通”。Demo 跑通只需要十几行代码。 真正花时间的,是跑通之后的事:字段映射有没有显式区分?成交量口径有没有搞清楚?异步回调里有没有做去重和背压? 这三个问题排查起来比写代码耗时得多,但也比写代码更决定系统能稳定运行多久。 你用 Python 接过期货行情吗?有没有踩过 volume_24h 和 volume 混用的坑,或者异步回调里发现过重复推送?欢迎分享你的排查经历。 在加密币种策略回测研究中,不少研究者将大量精力投入指标优化与参数调优,但回测结论稳定性较差,和实盘表现偏离度偏高。复盘后发现,数据规范性不足是关键诱因。结合实盘研究经验,围绕数据标准、常见隐患、调取方法及模型落地展开技术交流。 一、量化回测的数据规格要求 标准 K 线包含时间戳、开盘价、最高价、最低价、收盘价、成交量六大核心字段。收盘价用于趋势类模型演算,高低价格作为突破型策略判定基准,成交量可筛选流动性薄弱时段,规避虚假开仓信号。 数据获取分 REST、WebSocket 两类接口,历史回溯场景优先采用 REST 请求;跨年度大范围历史数据建议分段分片调取,规避接口限流引发的数据缺失。 二、数据源接入三类典型数据隐患 时间基准不统一,各服务商时区规则有差异,多标的汇总后时序错位; 小众币种存在行情间断,空值干扰指标连续运算; K 线周期与模型预设周期不一致,造成开平仓信号测算错误。 正式回测前需完成时区校准、空缺数据修补、周期匹配三项预处理工作。 三、行情拉取参考代码 import requests import pandas as pd 规范接口输出结构化数据,快速转化为 DataFrame 格式,简易清洗后即可对接回测框架。 四、规整数据对量化建模的实用价值 经过标准化处理的数据集,能够保障各类量化模型回测结果可复现,便于区分策略逻辑缺陷与数据问题,减少无意义的参数迭代,提升模型研发效率。 经过多轮策略回溯验证,AllTick API加密 K 线字段规范、历史数据连续性优异,适配量化建模与历史回测工作。 亲测最好用的AI编写量化策略工具,可以让 AI 直接写各个平台的策略代码,直接生成可运行的策略代码,代码质量远高于直接使用 DeepSeek、Trae 等平台。 大家可以直接用描述策略,然后一键生成可运行的完整策略代码,也可以把它当做一个API 查询工具。 最新消息,已经支持SuperMind等主流量化平台啦,并且实盘亲测过了,很适合小白用户,上线之后获得了非常多朋友的好评。 **🚀️ AI工具平台:https://iris.findtruman.io/ai/tool/ai-quantitative-trading/** 第一次来这,交互做得很漂亮,就是不知道实力怎么样😄 在数据平台中,没看到AH溢价率相关数据,好像也没法通过计算得到?