VeighNa量化社区
你的开源社区量化交易平台 | vn.py | vnpy
置顶主题

VeighNa发布v4.5.0 - 多因子策略模块性能优化

发布于VeighNa社区公众号【vnpy-community】
原文作者:用Python的交易员 | 发布时间:2026-10-11
本周发布了VeighNa的4.5.0版本,本次更新的主要内容是大幅优化多因子策略模块中时序算子的计算性能,同时重构了因子和信号的绩效分析实现。

对于已经安装了VeighNa Studio 4.0版本的用户,可以使用快速更新功能完成自动升级。对于没有安装的用户,请下载VeighNa Studio-4.5.0,体验一键安装的量化交易Python发行版,下载链接:

https://download.vnpy.com/veighna_studio-4.5.0.exe

 

时序算子性能优化

 

4.5.0重写了多因子策略模块vnpy.alpha中的6个时序算子:ts_mean、ts_std、ts_rank、ts_argmax、ts_argmin和ts_quantile。旧版统一基于Polars的rolling_map实现,每个窗口都要回调一次Python,导致了显著的数据来回复制开销。

新版中,ts_mean、ts_std和ts_rank改为按合约向量化计算,ts_quantile改用Polars原生的rolling_quantile,ts_argmax和ts_argmin改为按合约拆分后直接对数据切片计算(不再经过rolling_map的逐窗口调用),从而尽可能发挥Polars底层计算引擎的性能优势。

Alpha 158中有70个因子用到这些算子(ma、std、rank、qtlu、qtld、imax、imin等),新版本优化后prepare_data的特征计算会快很多。

在300只股票、每只2000根K线(共60万行)的数据上,窗口为20时单个算子的耗时对比如下:

description

窗口取60时,ts_rank和ts_quantile仍有百倍以上的提升,ts_mean和ts_std在40多倍,ts_argmax和ts_argmin保持在15倍左右。

注意:cs_rank改为返回当日截面上的百分位排名,取值在0到1之间,4.4.0返回的是从1开始的名次。Alpha 101已实现的82个因子中有62个用到cs_rank,升级后需要重新计算数据集。

 

因子和信号绩效分析

此前因子和信号绩效分析依赖alphalens-reloaded,许多用户容易根据报错提示直接安装alphalens(不兼容的老版本)导致出错,且matplotlib绘制的图表效果也不够理想。

4.5.0中去掉了这个依赖,改为参考Alphalens分析框架的计算逻辑在vnpy.alpha内部重新实现,图表改用Plotly绘制。调用方式不变:

dataset.show_feature_performance("ma_20")
dataset.show_signal_performance(signal)

完整页面如下图所示:

description

 

CHANGELOG

 

新增

  1. vnpy_ctabacktester的回测配置按策略名分组保存,切换策略时自动恢复;没有历史配置时回到界面初始值

调整

  1. 交易接口、功能模块、数据库和数据服务补充公开接口文档与类型注解,增加离线测试并纳入持续集成
  2. vnpy.alpha的cs_rank改为横截面百分位排名
  3. 重构vnpy.alpha的因子和信号绩效分析,移除alphalens-reloaded
  4. vnpy.alpha回测增加fill_price参数,新增vwap撮合模式支持
  5. 标签周期解析支持ts_delay的任意价格字段
  6. 重构vnpy.alpha的ts_mean、ts_std、ts_rank、ts_argmax、ts_argmin、ts_quantile函数,大幅优化性能
  7. vnpy_polygon改名为vnpy_massive,通过Massive REST获取美股K线
  8. vnpy_mini更新API至1.7.7、交易API至1.7.7
  9. vnpy_tap更新API至9.3.1.11、交易API至9.3.9.18
  10. vnpy_ctp增加登录时的ProductInfo产品信息传参支持
  11. vnpy_ctp的MdApi.subscribeMarketData支持一次传入合约列表
  12. vnpy_ctp的纯ASCII字符串的编码转换不再走GBK
  13. vnpy_ib更新API至10.45.1

修复

  1. 修复界面时间列点击无法排序的问题(TimeCell写入时间戳)
  2. 修复vnpy.alpha的ta_rsi、ta_atr在多合约数据上串算的问题,改为按合约分别计算
  3. 修复BarGenerator.update_tick,最高价和最低价只反映相对上一笔Tick的变化
  4. 修复RPC的主题过滤,并保持心跳,避免发布套接字卡住
  5. vnpy_ctp在init、join、exit、release期间释放GIL,避免退出死锁
  6. vnpy_ctp行情毫秒不足三位时补零
  7. vnpy_ctp确保本地SUBMITTING先于该笔柜台回报入队
  8. vnpy_femas修正委托时间字段名
  9. vnpy_rohon补上Windows运行库,并修正Linux rpath
  10. vnpy_sqlite读取时把带时区的查询起止时间转成朴素时间
  11. vnpy_questdb将空的database.host和localhost改为127.0.0.1,避免Windows上连接超时
  12. vnpy_tushare日线下载使用trade_date
  13. vnpy_tushare分钟和小时数据满页后按最早时间再向前翻页,避免重复请求同一页
  14. vnpy_xt将深圳创业板合约识别为股票
  15. vnpy_portfoliostrategy的分钟K线按完整分钟切片

 



CTA量化通关系列3 - 连续合约没搞对,回测结论靠不住

发布于VeighNa社区公众号【vnpy-community】
原文作者:Fusion小助手 | 发布时间:2026-07-31
上一篇我们把交易想法整理成了策略逻辑说明书。规则清楚之后,下一步是拿历史行情做检验。股票有一条相对完整的价格历史;期货则不同——每一张合约都会到期退市。

要回测某个品种过去三年,通常不能只用某一个交割月。研究者会把不同月份的合约,按规则接成一条更长的序列,行业里常叫它「主力连续」。它方便研究,却容易让人产生错觉:以为屏幕上那根 K 线,就是市场上某一张真实挂牌的合约。

你回测用的那根「主力连续」K 线,在真实市场上从来没有存在过。

本篇只讲清这件事:为什么要连续合约、指数 / 前复权 / 后复权各自适合什么场景、为什么不同软件的数据常对不上。

 

换月一接,跳空可能是假的

 

主力合约,指同一品种里市场参与最集中、通常也更适合作为研究与交易参照的那张合约。持仓量是未平仓合约的数量,常用来衡量资金集中在哪一张合约上。随着交割临近,持仓与成交会迁到更远的月份,研究用的「当前主力」也要跟着换——这一步叫换月。

问题在于:旧主力与新主力在交接前后,价格往往不相等。若把两段行情直接首尾相接,换月处会出现一根「跳空」。这根缺口来自合约切换,不一定来自品种本身的供需冲击。

description

对趋势类规则来说,这种假跳空特别麻烦。突破跌破、均线穿越,本就对相邻两根 K 线的相对位置敏感;换月缺口一旦被当成真波动,信号可能在「不该下单的日子」被触发。若回测引擎再按跳空幅度记账,权益曲线还会多出一笔现实里对不上的盈亏。

我们在社区答疑里遇到过这类困惑:回测曲线在某个交易日突然多出一块,对着分月合约检查下来,那天并没有对应的真实波动,只是连续序列在换月处没处理好。

所以连续合约要同时回答两件事:月份如何拼出足够长的样本,以及换月缺口如何处理、处理后的价格还能不能用来发信号。

 

三类连续序列:用途不同,别混用

 

常见的连续序列可以分为三类:指数连续、前复权主力连续和后复权主力连续。

初学时不必急着记住这些名称。三类序列其实回答两个不同的问题:指数连续回答「怎样概括整个品种的价格走势」;前复权和后复权回答「换月产生价差后,保留今天的价格,还是保留过去的价格」。

指数连续:观察整个品种

指数连续不是依次拼接某一张主力合约,而是把同一品种的多个可交易合约,通常按照持仓量加权,合成一条价格序列。

它反映的是整个品种的大致价格水位,通常比较平滑,适合观察长期趋势和进行宏观研究。但这个价格是计算出来的,可能不等于任何一张实际合约的价格,因此不能直接把它当成可以挂单成交的价格。

可以把它理解为:

指数连续回答「这个品种整体在涨还是在跌」,但不回答「今天应该按什么价格下单」。

前复权主力连续:保留今天的真实价格

前复权主力连续始终跟踪主力合约。换月时,它保留新主力当前的真实价格,同时平移换月前的历史价格,把合约切换产生的假跳空消除(这种用价差做整体平移的做法,常称 Panama 加法复权)。

例如,旧主力换月前为 3500 点,新主力为 3600 点,两者相差 100 点。直接拼接会出现一个 100 点的跳空。前复权会把换月前的历史价格统一加上 100 点,使换月前后的走势衔接起来,而今天的 3600 点仍然对应真实可交易的主力合约。

因此,前复权适合需要让历史信号与当前盘面价格保持一致的场景。CTA 策略回测多数会优先使用这一类序列。

后复权主力连续:保留过去的真实价格

后复权主力连续同样跟踪主力合约,但选择保留较早的历史价格,把换月后的较新价格向历史价格平移。

仍以上面的换月为例,旧主力为 3500 点,新主力为 3600 点。后复权会保留旧主力的 3500 点,并把换月后的价格减去 100 点,使两段走势连续。

这种序列更适合观察长期路径、比较长区间的净值变化。但它最新端的价格经过了调整,未必等于当前主力合约的真实盘面价格,因此不适合直接拿来判断今天应该在哪个价位下单。

三者的区别可以归纳为:

description

如果需要进一步理解它们的数学关系,记 (t) 为当前观察日,(t_k) 为第 (k) 次换月的生效日,换月价差定义为:

description

新主力价格旧主力价格

各家采用开盘价还是收盘价计算,规则可能不同。

前复权:

description

后复权:

description

也就是说,前复权把观察日之后才发生的换月价差加到历史价格上;后复权则从较新价格中减去观察日当时已经发生的换月价差。

公式并不是重点。实际使用时,只需先记住:

  • 前复权保留当前价格,调整历史价格;
  • 后复权保留历史价格,调整后续价格;
  • 指数连续反映品种整体,但不是任何一张真实合约。

前复权还有两个需要注意的特点。

第一,加法复权会让历次换月价差在远端历史中不断累积。如果价差长期同向,早期价格会被持续抬高或压低;当累计调整为负时,极端情况下可能逼近零,甚至变成负数。使用依赖绝对价格水平的指标时,需要特别留意。

第二,前复权历史并不是永久固定的。如果重新构建数据时,换月日期或换月价差发生变化,整段历史价格也会随之改变。因此,上周导出的前复权序列与本周重新构建的序列,同一天的收盘价可能不同。这是复权规则带来的正常结果,并不意味着数据库出了问题。

做 CTA 策略回测时,多数情况下会选择前复权主力连续,因为它回答的是:

按照今天的价格口径,历史上的策略信号是否应该触发?

选错序列,后面所有回测结论都会建立在错误的价格口径上。

 

主力怎么切换,决定连续长什么样

 

「谁算主力」本身就是一条规则。Fusion 数据中心采用双轨口径(与常见行业实践一致):

  • 商品期货:以日终持仓量为主要依据——持仓量最大,且达到原主力的 1.1 倍时,才切换;切换在次日生效,日内不换。
  • 金融期货:以成交量、持仓量双第一为常见条件,同样次日生效。
  • 滚动方向:只向更远的月份切换,避免在近月之间来回跳。

把倍数、生效时点、是否要求成交量同步最大写进规则之后,同一天「谁是主力」才会有唯一答案。规则改一点,换月日就可能错开,连续序列上的缺口位置与复权幅度也会跟着变。

因此有一条比「哪家数据更准」更管用的判断:

主力判定、换月时点和缺口处理规则不同,不同软件的主力连续自然可能对不上——看到不一致,先查两边的构造规则。

不能只凭价格不同就判断哪一方的数据有误。比较连续序列之前,首先要确认两边是否属于同一类型;如果都是主力连续,再核对主力判定、换月时点和复权规则。

 

在 Fusion 数据中心里准备可回测序列

 

前面把规则和序列类型讲清楚之后,下一步就是在数据中心里操作:先下载分月合约入库,再重建成本地连续序列。VeighNa Fusion 用固定后缀区分上述三类序列——这是平台内约定,不是交易所官方代码:

description

连续序列的代码也是平台内部约定。以螺纹钢为例,Fusion 的指数连续使用 rb90.SHFE,米筐 RQData 使用 RB99;迅投研则将同类序列称为「加权合约」,代码为 rbJQ00.SF,本文将其归入指数连续一类。

三者都通过多个可交易合约反映品种整体价格,但纳入的合约范围、持仓量权重及计算时点可能不同,因此价格不一定完全一致。

本系列后文回测演示,会与螺纹钢同一品种对齐,策略侧常用前复权序列 rb91.SHFE。

第一步,在数据中心选择交易所、品种和日期范围,点击「下载入库」。任务完成后,分月合约数据会写入本地数据库。

description

下载螺纹钢分月合约

第二步,分月数据入库后,点击「重建连续」。系统会识别主力换月日期,并按照对应规则生成三类连续序列。

description

重建螺纹钢连续序列

第三步,重建完成后,在左侧合约列表中检查是否已经生成 rb90、rb91 和 rb92。

description

检查生成的 rb90、rb91 和 rb92

有一条容易漏的操作纪律:追加下载了更新的分月数据之后,需要手动触发重建,连续序列才会按最新合约与换月结果重算。只下载、不重建,回测仍可能跑在旧的连续结果上——分月库已经是新的,91 却还是上一次拼接的产物。

下载与重建拆开,是为了让你看清「原始分月」和「研究用连续」是两层数据。连续序列出问题,优先回到分月合约和换月记录排查,别急着先改策略参数。

 

本篇小结

 

  • 主力连续是按规则构造出来的研究用序列,不等于任何一张在市场上真实挂牌交易的合约。
  • 指数连续、前复权主力连续和后复权主力连续用途不同;CTA 策略回测通常优先选择前复权主力连续。
  • 跨软件价格对不上时,先确认序列类型,再查构造规则,再谈数据对错。
  • 在 Fusion 里用 90 / 91 / 92 区分三类序列,更新分月数据后记得重建。

 

接下来讲什么

 

数据就绪之后,可以把上一篇的策略逻辑说明书交给 AI 生成代码。实现门槛变低了,读懂代码、核对翻译是否忠实,反而成了更关键的一步。

下一篇:不会写代码,也要会读策略。


VeighNa Fusion 是韦纳软件面向期货公司推出的标准化期货量化交易平台,本系列文中演示均基于 Fusion 内置功能完成,如希望了解详情,请扫描下方二维码添加小助手交流:

description

 



《CTA策略》课程中与2.6.0版本不一致的地方

《CTA策略》课程中与现版本2.6.0不符的地方

 

课时2-策略模板

问题(同样的问题还出现在了以下课时:课时24):

在课程03:07附近提到打开cta_strategy目录不在vnpy目录下

解决方案:

大家可以在vnpy的同级目录下寻找。一般在安装veighna_studio的目录下,如路径“C:\veighna_studio\Lib\site-packages\"

课时3-策略开发

问题:

在05:39附近,由于2.6.0版本将cta_strategy等上层应用剥离出去了,因此以前版本的导入cta_strategy的方式有误:

from vnpa.app.cta_strategy import CtaTemplate

 

解决方案:

   from vnpy_ctastrategy import CtaTemplate

 

课时4-历史数据回测

问题:

在课程02:50附近,由于2.6.0版本中增加了其他的数据获取API,因此增加了datafeed全局字段,大家在升级或者初次使用VNStation时需要根据具体情况自行配置。

解决方案:

具体如何配置请参考官方论坛https://www.vnpy.com/forum/topic/7705-vn-pyfa-bu-v2-6-0-gao-xing-neng-jin-rong-shu-ju-ku

课时8-K线的自定义合成

问题:

2.6.0版本对部分源代码进行了重构,其中就包括了该课程中的BarGenerator,所以课程具体的源代码与课程中展示的不完全一致。

解决方案:

2.6.0版本对BarGenerator进行了重构,对于update_tick函数做了一些改进,同时对于update_bar函数更新分钟数据和小时数据的逻辑分离成了两个独立的函数,分别为update_bar_minute_window函数和update_bar_hour_window函数,虽然与课程中展示的不完全一致,但是功能都是一致的。因此如果大家想要按照课程中所展示的代码自定义一个可以生成类似7分钟这种分钟数据,应该对update_bar_minute_window函数进行复制后再做修改。可以尝试修改如下:

def update_bar_minute_window(self, bar: BarData) -> None:
        """"""
        # If not inited, create window bar object
        if not self.window_bar:
            dt = bar.datetime.replace(second=0, microsecond=0)
            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                gateway_name=bar.gateway_name,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price
            )
        # Otherwise, update high/low price into window bar
        else:
            self.window_bar.high_price = max(
                self.window_bar.high_price,
                bar.high_price
            )
            self.window_bar.low_price = min(
                self.window_bar.low_price,
                bar.low_price
            )

        # Update close price/volume/turnover into window bar
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += bar.volume
        self.window_bar.turnover += bar.turnover
        self.window_bar.open_interest = bar.open_interest
        
        # Check if window bar completed
        if self.window_bar.datetime.minute != bar.datetime.minute:
            self.interval_count += 1
            if not (bar.datetime.minute + 1) % self.window:
                self.on_window_bar(self.window_bar)
                self.window_bar = None
                self.interval_count = 0

 

课时14-回测工作流程

提示1:

在03:59附近展示的目录结构为:

description

2.6.0版本的目录结构为:

description

因此,应该打开backtesting_demo.ipynb文件。

提示2:

在05:06附近,backtesting_demo.ipynb文件第二个单元格输入的vt_symbol与课程不一致,课程中为IF88.CFFEX, 现在为IF888.CFFEX,因此若出现无法加载历史数据的情况请检查该合约数据是否下载过。

问题1:

11:46左右,同课时2问题,cta_strategy目录不在vnpy目录下,而是在vnpy同级目录下。

课时20-固定止损

问题**(同样有导入模块路径错误的课时还有:课时21,22,23,29,44, 49, 51):**

boll_demo_Strategy_20.py代码中模块导入路径错误

解决方案:

from vnpy_ctastrategy import .....

 

提示:

在课程14:10左右说改变代码后需要重启VNtrader,在现版本不需要了,只需要点击CTA回测界面底下的策略重载按钮即可

课时27-曲线绘制

提示:

课程中展示的代码show_chart在现版本中已经通过plotly重构了。

课时36-遗传算法(下)

提示:

课程08:20左右展示的遗传算法的代码在2.6.0版本上做了修改,主体代码在vnpy\trader\optimize\run_ga_optimization()下。



CTA量化通关系列4 - 不会写代码,也要会读策略

发布于VeighNa社区公众号【vnpy-community】
原文作者:Fusion小助手 | 发布时间:2026-08-31
上一篇我们把回测要用的连续序列讲清楚了。规则清楚、数据就绪之后,可以把策略逻辑说明书交给 AI 生成一份 CTA 策略代码。实现门槛低了,核对有没有完整还原说明书,还是要人来做。

你不需要会写代码,但你必须看得懂自己的资金按什么规则在交易。

本篇只做一件事:带着第 2 篇的布林带、Aroon 和 ATR 案例,对着说明书把落点找齐。

 

规则落在代码的哪五处

 

第 1 篇说过,阅读一份结构固定的 CTA 策略,通常比从零编写要容易。读的时候可以先记住一句:代码是逻辑说明书的翻译。

落点就是说明书某一条在代码里的对应位置。代码里有五个固定部件,对照时通常对到这五处,但是要注意:说明书里的某一条,不一定只落在一个部件里。

这五个部件具体是:参数、K 线合成、指标计算、信号判断和持仓管理。on_tick、on_order、on_stop_order 在本案例里只有空的函数壳,里面没有交易逻辑判断,可以先跳过——但要确认它们本来就是空的。

说明书里跟代码直接相关的内容,摘成六条放在这里,后面每一段都回到这张单子:

  • 信号周期:1 分钟合成 15 分钟,指标和信号都在 15 分钟收盘后算。
  • 方向过滤:Aroon 上升指标大于下降指标才做多,反过来才做空。
  • 入场触发:无持仓时,以布林带上轨为多头触发价、下轨为空头触发价,价格触及即入场。
  • 出场:1.5 倍 ATR 移动止损,开仓后极值只朝有利方向更新。
  • 仓位:每次 1 手,持仓期间不加仓。
  • 边界:方向不明不交易;已有持仓只管出场。

后两条尤其容易散落在好几处:手数既在参数里,也在下单数量上。下图只标主要落点,箭头不是一一对应。

description

生成出来的策略基于标准 CtaTemplate(CTA 策略模板),核心逻辑拆进了几个辅助函数。下面按这五处逐个对照。

 

参数:说明书里的数字,先出现在这里

布林带周期 20、宽度 1.7,Aroon 周期 20,ATR 周期 14,止损 1.5 倍,每次 1 手,合成周期 15 分钟,都写在策略类开头。

# 策略参数
boll_window: int = 20       # 布林带周期
boll_dev: float = 1.7       # 布林带宽度
aroon_window: int = 20      # Aroon 周期
atr_window: int = 14        # ATR 周期
sl_multiplier: float = 1.5  # 止损的 ATR 倍数
fixed_size: int = 1         # 每次下单手数
interval: int = 15          # K 线合成周期(分钟)

先核对这几个数字和说明书是否一致。这一步花不了多少时间,也不该省。

紧跟着是 parameters 列表,收进去的参数可以在添加策略时由外部配置,后面选参时也会动到这些名字。

# 参数名称列表:收进这里的参数可由外部配置
parameters = [
    "boll_window",
    "boll_dev",
    "aroon_window",
    "atr_window",
    "sl_multiplier",
    "interval",
]

列表里只收进六个参数名,比上面声明的参数少一个。

第一,fixed_size 不在其中。手数属于仓位管理,实盘由风控模块控制,不放进这份名单,后面选参也不会动它。

第二,interval 列在里面,但它决定的是合成周期,选参时也应固定。

除了参数,策略类开头还有 boll_up、intra_trade_high、long_stop 这类列在 variables 里的变量。它们跟参数不是一类,运行中会随行情和持仓变化,记住实时轨道、开仓后极值和当前止损价。

 

K 线合成:15 分钟决策写进了哪里

第一条要求用 1 分钟合成 15 分钟。对应位置有两处:on_init 里创建的 BarGenerator(K 线合成器),以及 on_bar 回调。

# on_init 中创建 K 线生成器:将 1 分钟 K 线合成 15 分钟 K 线
self.bg = BarGenerator(
    self.on_bar,
    window=int(self.interval),
    on_window_bar=self.on_15min_bar,
    interval=Interval.MINUTE,
)

def on_bar(self, bar: BarData) -> None:
    # 1 分钟 K 线只负责推入合成器,不在此处下单
    self.bg.update_bar(bar)

window 指定合成周期,on_window_bar 指定每走完 15 分钟调用哪个函数。on_bar 里只做 update_bar,说明 1 分钟线只负责送进合成器;若指标计算或发单写在这里,决策频率就和说明书对不上了。

 

指标计算:布林、Aroon、ATR 的数值从哪来

15 分钟 K 线回调 on_15min_bar 是策略主干:先把 K 线收进 ArrayManager(K 线序列与指标计算),算完指标,再按持仓状态分别处理。

def on_15min_bar(self, bar: BarData) -> None:
    self.update_am(bar)
    if not self.am.inited:
        return

    self.calculate_indicators()

    if self.pos == 0:
        self.handle_no_position()
    elif self.pos > 0:
        self.update_long_position(bar)
    elif self.pos < 0:
        self.update_short_position(bar)

    self.put_event()    # 通知界面刷新策略状态

pos 是 CTA 引擎为该策略实例维护的逻辑净持仓:大于 0 为多,小于 0 为空,等于 0 为无仓,需要注意它和底层账户的实际持仓不一定一致。

这三个分支是仓位和边界这两条规则的主要落点:只有无持仓才会走到开仓函数,有仓时只更新止损。「不加仓」靠的就是这个结构,代码里没有专门写它的那一行。

if not self.am.inited: return 常被读成「K 线不够算指标」。其实是 ArrayManager 的缓存还没填满,填满之前不算指标也不下单。

这里容量设为 100,比最长的指标窗口大得多。on_init 里的 load_bar(10) 就是为此预加载历史 K 线。

def calculate_indicators(self) -> None:
    # 布林带上下轨
    self.boll_up, self.boll_down = self.am.boll(
        int(self.boll_window), self.boll_dev
    )
    # Aroon 指标
    self.aroon_up, self.aroon_down = self.am.aroon(int(self.aroon_window))
    # ATR 指标
    self.atr_value = self.am.atr(int(self.atr_window))

要核对的是计算函数有没有传入前面声明的参数,以及 Aroon 的上、下两条有没有接反。

 

信号判断:方向过滤与入场触发

说明书把入场拆成方向过滤和入场触发。无持仓时,主干会调用 handle_no_position。

def handle_no_position(self) -> None:
    # 取消上一周期可能残留的旧委托
    self.cancel_all()

    # 上升趋势更强,以布林带上轨为触发价开多
    if self.aroon_up > self.aroon_down:
        self.buy(self.boll_up, self.fixed_size, stop=True)

    # 下降趋势更强,以布林带下轨为触发价开空
    if self.aroon_down > self.aroon_up:
        self.short(self.boll_down, self.fixed_size, stop=True)

只有无持仓且 Aroon 给出明确方向时,策略才会往对应轨道挂单。两个指标数值相等时,多空两个分支都不会执行,这就是「方向不明不交易」。

参数 stop=True 表示发的是停止单:先在本地记着一个触发价,等价格真的触及才报出去。所以这里有两层时点——决策在 15 分钟收盘完成,成交可能落在下一根 15 分钟的盘中。

每根 K 线的轨道都在变,所以函数开头先 cancel_all() 撤掉旧单,再按新轨道挂出。

 

持仓管理:移动止损与成交极值

说明书只保留一条离场路径:1.5 倍 ATR 移动止损。多头持仓时调用 update_long_position。

def update_long_position(self, bar: BarData) -> None:
    # 更新持仓期间最高价(只升不降)
    self.intra_trade_high = max(self.intra_trade_high, bar.high_price)

    # 按最新极值与 ATR 重算止损价,发送平仓停止单
    self.long_stop = self.intra_trade_high - self.sl_multiplier * self.atr_value
    self.cancel_all()
    self.sell(self.long_stop, self.fixed_size, stop=True)

max 让多头跟踪的最高价只升不降。空头一侧在 update_short_position 里用 min 跟踪最低价,平仓同样发停止单。止损价每根 K 线按最新极值和 ATR 重算一次,下一节把这一条完整对一遍。

有一条容易漏的细节:intra_trade_high(开仓后最高价)从哪里开始记?看成交回调 on_trade:

def on_trade(self, trade: TradeData) -> None:
    # 开仓成交,把极值起点设为真实成交价
    if trade.direction == Direction.LONG and trade.offset == Offset.OPEN:
        self.intra_trade_high = trade.price
    elif trade.direction == Direction.SHORT and trade.offset == Offset.OPEN:
        self.intra_trade_low = trade.price
    # 平仓成交,重置极值记录
    elif trade.offset in (Offset.CLOSE, Offset.CLOSETODAY):
        self.intra_trade_high = 0.0
        self.intra_trade_low = 0.0

    self.put_event()

只有收到开仓成交回报时,才把成交价作为跟踪起点,平仓后归零。如果过早用发单前的 K 线高点初始化,或者平仓后不重置,上一笔的极值就会带到新一笔止损里。

 

把移动止损完整对一遍

 

部件找齐以后,用一条容易对不上的,把核对做完。第 1 篇举过例子:策略说明要求 1.5 倍 ATR 移动止损,代码里可能变成固定止损,两种写法都能跑完回测,交易行为却已经不是一回事。

说明书这一条是:持仓后启用移动止损,距离为 1.5 × ATR(周期 14),多头跟踪开仓后最高价、空头跟踪最低价,价格触及则平仓。

对着代码按顺序检查:

  • ATR 周期是不是 14、倍数是不是 1.5——看参数声明与 calculate_indicators。
  • 极值起点是不是从开仓成交算起——看 on_trade 用没用 trade.price。
  • 多头用最高价、空头用最低价——看 max 与 min。
  • 平仓用的是不是停止单,多头 sell、空头 cover。
  • 每根新 K 线更新时,有没有先撤掉旧止损单——看 cancel_all()。

这几处对上了,说明书这一条就算落到代码里了。先看极值有没有顺势更新;止损价按「极值 − 1.5 倍 ATR」每根重算,就够了。

对不上时,常见的走样就是第 1 篇那个例子:极值不再更新,或者开仓后只算一次、后面不再改,虽然看起来仍然有止损,但跑的已经是固定止损。

 

在 Fusion 里打开生成代码

 

前面找落点的读法,换一个编辑器也一样。下一步就是在智策投研里打开生成出来的文件。确认逻辑之后,流程会生成一份和开源版同一套结构的 CtaTemplate 策略文件。

第一步,在智策工作台打开智策投研。确认逻辑之后,流程会接着生成代码,并可能自动往下走;打开「策略代码」页,确认文件已经出来即可。

description

智策投研策略代码页

第二步,点「打开策略文件」,不要从文件顶上逐行读。先找参数与 parameters,再看 on_15min_bar 的主干,最后看 handle_no_position、update_long_position 与 on_trade。

止损倍数这个参数,正文摘录里叫 sl_multiplier,有的生成稿会写成 atr_stop_multiplier。对的是数字 1.5,不是名字本身。

description

打开策略文件后从参数读起

 

本篇小结

 

  • 读 CTA 策略,核心是核对说明书的翻译。对之前先把准备核对的规则摘成一张单子。
  • 五个部件给出找落点的地图;边界规则按单子逐条找,不要按部件凑条目。
  • 移动止损至少要核周期、倍数、成交初始化、极值方向,以及更新前有没有撤旧单。
  • 智策投研能生成标准 CtaTemplate 文件;翻译有没有走样,仍要人来看。

 

接下来讲什么

 

说明书对完了,代码写得出来,回测也能跑。接下来要当心的,是那些不会报错的坑——曲线好看,实盘却对不上。

下一篇:AI 写的策略,能运行不等于能信。

 


VeighNa Fusion 是韦纳软件面向期货公司推出的标准化期货量化交易平台,本系列文中演示均基于 Fusion 内置功能完成,如希望了解详情,请扫描下方二维码添加小助手交流:

description

 



vnpy 的启动流程总结

作为初学者,面对 vnpy 无所不包、博大精深的丰富内容,试图用图形对 vnpy 的运行流程做一个归纳。
不到之处,还请各位多多指正

description



欠了大家几个月,这篇把 VeighNa Fusion 一次讲清楚

发布于VeighNa社区公众号【vnpy-community】
原文作者:用Python的交易员 | 发布时间:2026-08-06
VeighNa Fusion 已于 2026 年一季度发布。过去几个月,我们围绕它写了不少 CTA 量化文章,介绍过策略逻辑、连续合约和 AI 辅助投研,却一直欠大家一篇完整的产品介绍。

这篇文章想回答三个最直接的问题:为什么要做 Fusion,Fusion 是什么,以及它能为个人量化投资者减少哪些前期准备。

过去,许多个人投资者会直接使用 VeighNa 开源版连接 CTP 柜台。用户自行配置 Python 环境、安装组件并维护策略,就能搭起一套属于自己的量化交易系统。

随着期货市场程序化交易新规落地,个人使用自建系统时,需要完成的前期准备明显增加。除了编写策略,用户还要准备接入测试、沟通办理流程,并自行解决数据和运行环境问题。对于预算和技术力量有限的个人投资者,这些前期工作可能比写出第一条交易逻辑更难。

过去,接入柜台主要是技术准备;现在,自建系统还要完成接入测试。

 

为什么推出 Fusion

 

新规出台后不久,有期货公司找到我们,希望提供一套由期货公司统一采购、面向客户使用的标准化量化平台。

我们在社区答疑中反复遇到三类问题:系统如何接入,回测数据如何获取,以及如何把想法写成可以检查、回测和迭代的策略代码。

因此,我们基于 VeighNa 已有的 CTA 策略体系开发了 VeighNa Fusion。它保留了大家熟悉的策略开发、历史回测和自动交易功能,同时把运行环境、数据中心与智策投研整合到同一个桌面终端中。

 

Fusion 是什么

 

VeighNa Fusion 是韦纳软件面向期货公司推出的标准化期货量化交易平台,由合作期货公司采购后提供给其客户使用。

策略这块,Fusion 还是沿用标准 CtaTemplate 策略体系。已经使用 VeighNa 开源版开发常规 CTA 策略的用户,可以继续沿用原有的代码结构和开发经验。

Fusion 会帮你管好运行环境和版本。装好后可以直接开始写策略,把时间更多花在策略逻辑和风险控制上。

description

下面依次介绍接入、数据和策略实现这三类前期门槛。

 

第一类门槛:系统如何接入实盘

 

希望使用 Fusion 的客户需要在合作期货公司开通期货账户,并申请相关权限,权限开通后即可使用。

Fusion 平台由期货公司统一提供,客户可以省去单独准备系统接入测试的工作,减少重复的技术准备和沟通。

对客户来说,使用 Fusion 软件本身没有额外收费。期货交易手续费以期货公司业务规则为准。使用智策相关功能时,需自行准备大模型厂商的 API Key,并支付相应的 token 费用。

 

第二类门槛:回测数据从哪里来

 

CTA 策略回测离不开历史行情,而数据的获取、清洗、入库和持续更新,常常是初学者遇到的第二道门槛。

作为期货公司统一采购的标准平台,Fusion 内置了 数据中心 模块,目前提供 2020 年至今的国内期货 1 分钟历史行情。用户可以按交易所和品种选择合约,将分月数据下载到本地数据库。

分月数据在每个交易日收盘后更新,用户可在数据中心执行增量同步。同步分月合约数据后,还需要重新构建连续序列,连续合约才会包含刚同步交易日的数据。

数据中心可以在本地重建三类连续序列:

  • 90:指数连续,按各合约持仓量加权计算;
  • 91:前复权主力连续,保留当前真实价格,CTA 策略回测通常优先选用;
  • 92:后复权主力连续,保留较早的历史价格,更适合观察长期路径和区间对比。

description

下载分月数据和重建连续序列是两个步骤。连续合约的构造公式以及 91 和 92 的选择方法,可以看看《CTA 量化通关》里讲连续合约的那一篇。

 

第三类门槛:把想法变成可验证的策略

 

有了环境和数据,接下来才进入策略开发。对于编程经验有限的用户,从一句交易想法走到可运行代码,中间还隔着规则补全、代码实现、回测配置和参数优化等多步流程。

Fusion 集成的智策Agent会把这段工作组织成四个阶段:

  1. 明确策略想法,生成逻辑说明并等待用户确认;
  2. 生成策略代码,执行格式、语法、代码规范和类型检查;
  3. 按用户创建任务时填写的手续费、滑点等配置执行历史回测;
  4. 规划参数优化范围,执行优化,并说明参数结果的选择理由。

在智策工作台中,这四个阶段还会再拆成八步:生成逻辑、确认逻辑、生成代码、代码检查、执行回测、优化规划、执行优化和选择参数。

智策Agent整理完开平仓、止损和参数规则后会暂停,等待用户检查和修改,确认后才继续生成代码。

在《CTA 量化通关》的策略逻辑示例中,起初的策略想法只写了“1.5 倍 ATR 移动止损”,止损价的更新方向需要在确认逻辑时补充完整,随后才能生成代码。

代码检查覆盖格式、语法、代码规范和类型问题。用户负责检查策略逻辑中的指标计算和条件判断,并评估实盘风险。手续费、滑点等回测假设由用户创建任务时配置,回测结果好不好用,还得看数据、参数和策略本身。

参数优化完成后,智策Agent会比较结果,优先考虑相对稳健的参数区域并给出理由,最终参数会结合策略逻辑和风险要求选择。

智策Agent能帮你省下不少重复劳动。生成的策略仍是标准 CtaTemplate 结构,方便你继续阅读和修改;不过策略逻辑、风险控制和实盘前测试,还是得自己把关。

 

谁适合使用 Fusion

 

如果你已经有一定期货交易经验,想尝试 CTA 量化,并希望控制前期接入、数据和开发上的成本与精力,Fusion 可以把原本分散的交易、数据管理和策略投研工具集中起来,编程经验有限的用户也可以快速上手。

对个人投资者来说,VeighNa 现在大致有三条路:

  • VeighNa 开源版:适合有一定技术基础、希望自己掌控整套技术栈的开发者;
  • VeighNa Fusion:适合想少花精力在接入、环境和数据准备上的个人交易者;
  • VeighNa Elite:适合策略数量较多、对执行效率和实盘稳定性要求更高的专业交易员。

大家可以根据自己的实际情况,选择合适的路线。

 

如何开始

 

如果想使用 VeighNa Fusion,可以扫描下方二维码添加 Fusion 小助手,了解当前的合作期货公司,以及权限开通的具体办理方式,完成这些步骤后,就可以开始使用了。

description

后续我们还会继续在《CTA 量化通关》系列里,围绕策略逻辑怎么写清楚、连续合约怎么选、回测结果怎么看、实盘前要注意什么,一件一件往下拆,帮大家把 Fusion 用得更扎实一些。

 



CTA量化通关系列2 - 你的交易想法,机器看不懂

发布于VeighNa社区公众号【vnpy-community】
原文作者:Fusion小助手 | 发布时间:2026-07-29
上一篇我们说到,做 CTA 量化的第一道坎,是把交易想法翻译成机器能执行的规则。不少人其实已经有一条口头纪律,听起来也够具体——「均线金叉就做多」。

主观交易时,这句话背后往往还藏着一串默认前提:用哪根周期的均线、金叉当下进还是等收盘、进几手、什么条件离场。盘中靠经验,这些空白会被随手补上。程序不会补。把同样一句话交给机器,它未必当场报错,却可能用你从未同意过的默认假设继续往下跑。

AI 时代交易员的核心工作不是写代码,而是确认逻辑。

本篇只做一件事:给出一套五要素框架,并用一个完整案例,演示如何把大白话整理成一份可执行、可核对的策略逻辑说明书。代码怎么写、这份策略能不能赚钱、布林带和 Aroon 的公式细节,分别留给后面的文章。

 

规则化的五要素

 

上一篇留下的那串问题——周期、确认时机、手数、离场、加仓、方向不明——归纳成五类。缺任何一类,机器仍然无法稳定执行。

description

信号周期回答的是:用什么级别的 K 线做判断。交易所公开推送的行情里,1 分钟 K 线往往是研究者能稳定拿到的更细行情;策略若按 15 分钟决策,常见做法是用 1 分钟合成 15 分钟,再在合成后的序列上算指标、出信号。

这里容易混的一点是:合成周期描述的是「决策频率」,跟数据有多细不是一回事。同一套进出场逻辑,放在 5 分钟和 15 分钟上,交易次数和持仓时间会差不少。先定多久做一次判断,再选合成方式,比先随口定一个周期再倒推要清楚。

入场条件最好拆成两层:一层管过滤,一层管触发。过滤回答「只做多、只做空,还是都可以」;触发回答「什么价格动作才实际下单」。把两层混成一句「看起来该买了」,回测和实盘都很难核对。趋势过滤器加突破触发,是 CTA 里很常见的组合——过滤器负责少做逆势单,触发条件负责给出具体下单的时点。

出场条件至少要有一条离场路径:固定止损、移动止损、止盈、反向信号平仓,都可以,但不能没有。只写进场、不写出场,规则只完成了一半。若同时有多条离场路径,还要写清谁优先,避免程序里两条规则抢着平仓。

仓位规则在入门阶段建议先固定手数。固定 1 手看起来笨,却能把「逻辑对不对」和「仓位大不大」分开。变量少了,后面回测和核对才有参照。等逻辑稳定了,再讨论按波动或资金比例加减仓也不迟。

边界情况专门写「没有信号时怎么办」。方向不明、条件未触发、已有持仓又看到重复信号——这些在主观交易里常被略过,程序却必须有答案。「不交易」「不加仓」本身就是规则,写进说明书,才算完整。

我们在社区答疑里见过一种情况:口头规则只有进场,出场全靠「到时候看」。丢给 AI 也能生成能跑的代码,但离场逻辑往往是生成器自己补的默认值,和交易者心里想的并不是一回事。五要素的作用,就是在生成代码之前,先把这些空白落到纸面上。

 

从大白话到逻辑说明书

 

下面用本系列的固定案例走一遍。后文讲读代码、审查、回测、参数时,都会回到同一份说明书,中途不更换。

先看交易者口头版:

用 1 分钟 K 线合成 15 分钟;布林带周期 20、宽度 1.7;Aroon 周期 20 判断多空——上升趋势强就等突破布林上轨做多,下降趋势强就等跌破下轨做空,方向不明就不做;固定 1 手;持仓后用 1.5 倍 ATR(周期 14)移动止损。

三个指标在本案例里的分工可以记成一句话:布林带用价格相对均值的偏离画通道,突破上下轨作为触发;Aroon 比较近期最高价、最低价出现的先后,用来判断趋势方向;ATR 衡量近期波动幅度,用来设定止损距离。

我们第一次把这段话按五要素逐项填空时,卡在「趋势强」三个字上——到底强到什么程度?说明书里不能停在语感,必须写成可以比较的条件。下面按五要素翻译成一版完整说明书。

策略逻辑说明书(参考)

【信号周期】

  • 数据:1 分钟 K 线,合成 15 分钟 K 线。
  • 计算时点:布林带、Aroon、ATR 均在 15 分钟 K 线收盘后计算与更新。
  • 信号判断:仅在该 15 分钟 K 线收盘后进行,不在分钟内抢跑。

【入场条件】

  • 方向过滤(Aroon,周期 20):
    • Aroon Up > Aroon Down:视为上升趋势较强,只允许考虑做多;
    • Aroon Down > Aroon Up:视为下降趋势较强,只允许考虑做空;
    • 两者相等:视为方向不明,不开仓。
  • 多头触发:当前无持仓,且方向允许做多,且该 15 分钟收盘价突破布林上轨(周期 20、宽度 1.7)。
  • 空头触发:当前无持仓,且方向允许做空,且该 15 分钟收盘价跌破布林下轨(周期 20、宽度 1.7)。

【出场条件】

  • 持仓后启用移动止损,距离为 1.5 × ATR(周期 14)。
  • 多头:止损价随行情上移,不随行情下移;价格触及或跌破止损价则平仓。
  • 空头:止损价随行情下移,不随行情上移;价格触及或升破止损价则平仓。
  • 本策略不设固定止盈,也不另设「反向信号立即平仓」规则;离场以移动止损为准。

【仓位规则】

  • 每次开仓固定 1 手。
  • 不加仓:持仓期间即使再次满足入场条件,也不增加手数。

【边界情况】

  • 无持仓且未同时满足「方向过滤 + 突破触发」:不交易。
  • 方向不明:不交易。
  • 已有持仓:只管理出场,不新开同向或反向仓位。

对照口头版可以看出,大白话里没写、说明书里必须写清楚的,至少有这几处:在 15 分钟收盘后判断、Aroon 用 Up/Down 相对高低定义「趋势强」、突破看收盘价、移动止损只朝有利方向更新、持仓期间不加仓也不对锁。

你也可以拿自己的策略试一次同样的动作:先原样写下口头版,再按五要素逐项填空。填不满的格子,往往就是盘中靠经验临场发挥的地方;那些地方不补上,很难真正程序化。

这份说明书有两个用途。对机器,它是执行规格;对人,它是后面读代码、做审查、看回测时的核对底稿。第 4 篇起我们会拿着它逐项问:代码里的翻译是否忠实。

 

在 Fusion 里走一步:生成逻辑,然后停住

 

把上面的口头想法交给 AI,通常也能得到一份结构化逻辑草稿。能够生成只是起点;生成之后有没有人把关,才决定这份草稿能不能用。

在 VeighNa Fusion 的智策工作台里,打开智策投研,可以用同一段大白话作为任务输入。系统会先走「生成逻辑」——产出的结构,应能和上一节五要素说明书互相对照。

description

智策投研输入大白话想法

description

生成逻辑产物与说明书对照

更关键的是下一步:流程会停在等待你确认,而不会自动写代码、自动回测。

description

等待你确认

description

这一停,对应的正是本篇的钩子:整理和起草可以借助工具,交易规则有没有漏项、和你想的是否一致,确认权在人。若生成稿里「趋势强」仍是模糊说法,或出场只写了止损却没写更新方式,应在确认环节改成可以执行的表述,再进入后续步骤。

 

本篇小结

 

  • 机器只执行写清楚的分支;口头规则里的隐藏前提,要用五要素逐项补全。
  • 策略逻辑说明书是可交付的产出:它既给执行用,也给其他人(包括未来的自己)核对用。
  • AI 能降低「从想法到逻辑草稿」的成本,但确认逻辑仍要人来做。

 

接下来讲什么

 

规则写清楚以后,下一步是拿历史行情做检验。但期货合约会到期退市,回测常用的「主力连续」并不是市场上真实交易过的那张合约——构造方式不同,结论就可能不同。

下一篇:连续合约没搞对,回测结论靠不住。


VeighNa Fusion 是韦纳软件面向期货公司推出的标准化期货量化交易平台,本系列文中演示均基于 Fusion 内置功能完成,如希望了解详情,请扫描下方二维码添加小助手交流:

description

 



CTA量化通关系列1 - 想做量化,到底从哪开始

发布于VeighNa社区公众号【vnpy-community】
原文作者:Fusion小助手 | 发布时间:2026-07-28
夜盘进行到一半,你起身离开了几分钟。回来时,原本等着的突破已经走完。也有反过来的时候:止损条件明明已经触发,你却对着屏幕想“再等等”。一边是错过该做的,一边是拖延该走的——难点都在:执行依赖人,也容易被人改掉。

这两种经历都会引出同一个念头:能不能把交易条件提前写清楚,让程序去盯盘和执行?

带着这个念头往下走,很多人会接触到CTA量化。CTA量化策略在国内通常指这样一类策略:主要研究对象是各个交易品种自身的价格时间序列,趋势跟踪是其中非常典型的一种。随着研究方法演进,如今的CTA量化早已不止简单的规则组合,也会用到各类时序因子和机器学习算法。但对刚上手的人来说,先把输入、判断和执行条件讲清楚仍然是非常合适的入口,所以本系列从规则化方法讲起。

不过,把规则写清楚只是第一步。从“有个想法”走到“程序在实盘里跑起来”,中间还隔着几道具体的坎。我们在社区答疑中见到的问题五花八门,归拢下来却大多落在同样五道坎上:规则、数据、代码、验证和实盘。

本篇只负责把这张地图画清楚——每道坎是什么,难在哪里。连续合约怎么合成、代码怎么审查、回测指标怎么读、实盘流程怎么走,留到后面的文章逐项展开。

做量化的第一道坎不是编程,而是把“我感觉能赚钱”翻译成机器能执行的规则。

description

 

第一道坎:把想法变成规则

 

“均线金叉就做多”听起来已经很像一条规则,但机器仍然无法直接执行。

均线用几分钟的K线计算?金叉出现时立即进场,还是等收盘确认?一次进多少手?满足什么条件离场?信号连续出现时要不要加仓?方向不明确时又该怎么办?

主观交易者会在盘中用经验补全这些空白,程序不会。每一个分支都要提前给出明确答案,连“没有信号就不交易”也要写进规则。

所以策略规则化的关键,是找出交易想法里隐藏的前提,而不只是把一句话换成几行代码。本系列第2篇文章会给出一套五要素框架,把一段大白话整理成可以执行、也可以核对的“策略逻辑说明书”。

 

第二道坎:准备能用的数据

 

规则写好以后,需要放到历史行情中检验。而期货和股票不同:合约会到期退市,要回测一个品种过去三年的表现,通常不能只用某一个交割月份的合约。

研究者需要按照一定规则,把不同月份的合约拼接成连续序列。主力合约什么时候切换、新旧合约之间的价差怎么处理,这些选择都会影响最终得到的K线。

直接拼接还可能在换月处留下虚假跳空。程序如果把这段跳空当作真实行情,就可能生成现实中并不存在的交易信号和盈亏。

这也解释了为什么不同软件的“主力连续”数据有时对不上:差异往往来自构造规则的不同,不能只凭价格不一样就断定哪一方的数据有误。本系列第3篇文章会讲清指数、前复权和后复权连续序列各自适合什么场景。

 

第三道坎:把规则翻译成代码

 

过去,交易者往往要先学习Python、配置开发环境,再熟悉量化框架,才能写出第一份策略。现在,AI辅助策略投研开发已经能根据逻辑说明生成代码初稿,这道坎正在显著变矮。

但实现变得容易,不等于确认的责任也一起交了出去。代码能够运行,只说明它通过了基本的语法和接口要求,并不能证明它忠实实现了原始想法。

例如,用户要求用1.5倍ATR移动止损,AI在代码里却可能写成固定止损。两种写法都能跑完回测,交易行为却已经不是一回事。

想要把这类偏差找出来,不一定必须先学会从零写策略。阅读一份结构固定的CTA策略代码,通常比从零编写要容易得多。本系列第4篇文章会介绍参数、K线合成、指标计算、信号判断和持仓管理这五个固定部件,第5篇继续讨论AI生成代码的审查方法。

 

第四道坎:验证它是否经得起检查

 

策略跑得通、回测曲线向上,仍不足以说明它可以投入实盘。

回测用的是历史数据和简化的成交模型。有三种情况会让报告显得过于乐观:代码引用了决策时还拿不到的信息(未来函数);手续费和滑点设得过低(成本低估);参数被反复调整到恰好贴合某段历史(过度拟合)。

以其中的滑点为例:它指下单时的预期价格与实际成交价格之间的偏差。对交易频率较高的策略来说,即使每次只差一个最小变动价位,累积起来也可能改变回测结论。

在我们看来,验证是整条路线中极为需要耐心的一段。明显的程序错误通常会直接让策略跑不起来,而验证方法有缺陷却可能留下一份完整报告,让人带着错误的信心继续往前走。

因此,本系列第5至第7篇文章会依次讨论怎么审查代码、怎么读回测报告,以及怎么识别参数优化中的过拟合。

 

第五道坎:合规走向实盘

 

策略验证通过后,离真实账户交易仍有一段路:程序化交易权限要单独申请;用外部软件接入时,往往还要先完成系统合规测试,流程更长。接入方式不同,准备工作也不同——回测跑通,不等于可以立刻实盘。

本系列第8篇文章会依据当前期货行业的监管规定,把这条路上的常见步骤与路径差异讲清楚。

 

两条上手路径

 

地图有了,还要选择用什么工具。常见做法可以归纳为两类:自行安装开源框架并维护环境,或者使用期货公司提供的标准平台。

description

这两条路径面向的是不同类型用户的需求:VeighNa 开源版适合希望自行配置环境、扩展功能的开发者;VeighNa Fusion适合希望把精力放在量化投研与交易本身的交易者。

两个版本都采用标准CtaTemplate策略模板,策略结构和开发经验可以延续,区别主要在于环境、数据和运行依赖由谁维护。

本系列后续会用Fusion展示实际操作,但文章中的规则化、数据处理和验证方法与具体工具无关。即使选择其他量化平台,也可以沿着同一张地图开展工作。

下面两张图分别是Fusion主界面,以及内置CTA Agent的工作流全景——后文演示会沿着这条工作流展开。

description

description

本篇小结

 

  • 规则化是理解CTA量化的入门起点;更复杂的时序因子和机器学习策略,同样需要清晰的输入、验证与执行流程。
  • 从想法到实盘,规则、数据、代码、验证和合规流程这五道坎要依次跨过。
  • AI降低了代码实现门槛,没有降低逻辑确认与结果验证的要求。
  • 开源框架和标准平台服务于不同类型用户的需求,可以按自身能力与偏好选择。

 

接下来讲什么

 

五道坎的顺序,也是本系列计划的连载顺序:

  1. 想做量化,到底从哪开始(本篇)
  2. 你的交易想法,机器看不懂
  3. 连续合约没搞对,回测结论靠不住
  4. 不会写代码,也要会读策略
  5. AI写的策略,能运行不等于能信
  6. 回测很好看,先别急着相信
  7. 参数越调越完美,越要警惕过拟合
  8. 策略准备好了,实盘还有合规门槛

description

下一篇,我们从第一道坎动手:用一个具体案例,演示怎么把一段交易想法整理成机器能执行、人能逐项核对的策略逻辑说明书。


VeighNa Fusion 是韦纳软件面向期货公司推出的标准化期货量化交易平台,本系列文中演示均基于 Fusion 内置功能完成,如希望了解详情,请扫描下方二维码添加小助手交流:

description

 



本地部署vnpy$发环境[适用于windows+mac]

安装流程:

  1. 下载 anaconda,并安装
  2. 下载pycharm,并安装
  3. 下载vnpy源码vn.py
    下载源码
  4. 打开 pycharm, file->open,选择下载的vnpy 文件夹
    pycharm打开vnpy文件夹
  5. 配置 python 开发环境, file- settings
    pycharm项目设置
    添加新的anaconda环境
    description
    选择python3.7
    description
    这里我使用的环境名称是 py37_vnpy

打开terminal-注意环境切换成 py37_vnpy,执行以下命令,安装需要的插件

pip install -r requirements.txt -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

description
遇到安装失败的可以单独安装:

pip install PyQt5 -i http://pypi.douban.com/simple --trusted-host pypi.douban.com

description

  1. 创建run.py文件,复制以下代码,来源 README.md

description

  1. 运行 python run.py,注意环境名称是 py37_vnpy

description

备注:
如果需要在cmd 下使用 py37_vnpy 环境。

打开 CMD
运行: conda activate py37_vnpy
会切换到py37_vnpy环境下

description



利用vnpy图表实现多周期复盘的代码

经过好几天的反复,终于完成了。所谓的复盘,就是盘后把行情从新播放一遍,如果使用tick数据,就和真实的盘面一模一样,我这里使用的是1分钟数据复盘,所以简化了很多。

代码如下:

import multiprocessing
import time
from datetime import datetime
from vnpy.trader.constant import Exchange, Interval
from vnpy.trader.database import database_manager
from vnpy.chart import ChartWidget, VolumeItem, CandleItem
from vnpy.trader.ui import create_qapp, QtCore
from vnpy.trader.object import BarData
import os

bar: BarData

def putbardata(q_1m,q_5m,q_30m,q_4h,su):

#从数据库中读取1分钟数据,你的数据库必须有下载好的数据。
    bars = database_manager.load_bar_data(
        symbol="APEUSDT",
        exchange=Exchange.BINANCE,
        interval=Interval.MINUTE,
        start=datetime(2022, 5, 4),
        end=datetime(2025, 1, 1)
    )
    sudu = 0.055

    i = 0
    for bar in bars:
        q_1m.put(bar)
        q_5m.put(bar)
        q_30m.put(bar)
        q_4h.put(bar)
        if i > 1200:                                          #先快速播放一定数量的一分钟bar
            if not su.empty():
                sudu = int(su.get(True))
                print("速度已经设定为:", sudu)
            if i % 10 == 1 :
                os.system("pause")                  #正常播放以后,每10个一分钟bar暂停一下,按任意键继续,不需要这个功能的可以删掉。
        time.sleep(sudu)
        i = i + 1

def MINUTE_5m(q):
    app = create_qapp()
    widget = ChartWidget()
    widget.add_plot("candle", hide_x_axis=True)
    widget.add_plot("volume", maximum_height=180)
    widget.add_item(CandleItem, "candle", "candle")
    widget.add_item(VolumeItem, "volume", "volume")
    widget.add_cursor()
    history : BarData
    history = []
    global i_5
    i_5 = 0
    global bar_

    def update_bar():
        global i_5
        global bar_
        if not q.empty():
            bar = q.get(True)
            if i_5 == 0  :
                bar_ = bar
                i_5 =  1
                history.append(bar_)
            if i_5 == 5  :
                bar_ = bar
                i_5 = 1
                history.append(bar_)
            else :
                bar_.close_price = bar.close_price
                if bar.high_price > bar_.high_price:
                    bar_.high_price = bar.high_price
                if bar.low_price < bar_.low_price:
                    bar_.low_price = bar.low_price
                bar_.volume = bar_.volume + bar.volume
                i_5 = i_5 + 1
                history[-1] = bar_                            #这一段是把一分钟数据形成5分钟数据

            widget.clear_all()
            widget.update_history(history)            #刷新图形数据

    timer = QtCore.QTimer()
    timer.timeout.connect(update_bar)
    timer.start(50)
    widget.setWindowTitle("五分钟")            #设定五分钟窗口的标题和窗口大小以及位置
    widget.setGeometry(0, 0, 900, 550)
    widget.show()
    app.exec_()

def MINUTE_30m(q):                                     #30分钟和5分钟类似
    app = create_qapp()
    widget = ChartWidget()
    widget.add_plot("candle", hide_x_axis=True)
    widget.add_plot("volume", maximum_height=180)
    widget.add_item(CandleItem, "candle", "candle")
    widget.add_item(VolumeItem, "volume", "volume")
    widget.add_cursor()
    history: BarData
    history = []

    global i_30
    i_30 = 0
    global bar_

    def update_bar():
        global i_30
        global bar_
        if not q.empty():
            bar = q.get(True)
            if i_30 == 0  :
                bar_ = bar
                i_30 =  1
                history.append(bar_)
            if i_30 == 30  :
                bar_ = bar
                i_30 = 1
                history.append(bar_)
            else :
                bar_.close_price = bar.close_price
                if bar.high_price > bar_.high_price:
                    bar_.high_price = bar.high_price
                if bar.low_price < bar_.low_price:
                    bar_.low_price = bar.low_price
                bar_.volume = bar_.volume + bar.volume
                i_30 = i_30 + 1
                history[-1] = bar_

            widget.clear_all()
            widget.update_history(history)

    timer = QtCore.QTimer()
    timer.timeout.connect(update_bar)
    timer.start(50)
    widget.setWindowTitle("三十分钟")
    widget.setGeometry(0, 560, 900, 550)
    widget.show()
    app.exec_()

def MINUTE_4h(q):
    app = create_qapp()
    widget = ChartWidget()
    widget.add_plot("candle", hide_x_axis=True)
    widget.add_plot("volume", maximum_height=180)
    widget.add_item(CandleItem, "candle", "candle")
    widget.add_item(VolumeItem, "volume", "volume")
    widget.add_cursor()
    history: BarData
    history = []

    global i_4h
    i_4h = 0
    global bar_

    def update_bar():
        global i_4h
        global bar_
        if not q.empty():
            bar = q.get(True)
            if i_4h == 0  :
                bar_ = bar
                i_4h =  1
                history.append(bar_)
            if i_4h == 240  :
                bar_ = bar
                i_4h = 1
                history.append(bar_)
            else :
                bar_.close_price = bar.close_price
                if bar.high_price > bar_.high_price:
                    bar_.high_price = bar.high_price
                if bar.low_price < bar_.low_price:
                    bar_.low_price = bar.low_price
                bar_.volume = bar_.volume + bar.volume
                i_4h = i_4h + 1
                history[-1] = bar_

            widget.clear_all()
            widget.update_history(history)

    timer = QtCore.QTimer()
    timer.timeout.connect(update_bar)
    timer.start(50)
    widget.setWindowTitle("四小时")
    widget.setGeometry(860, 560, 1050, 530)

    widget.show()
    app.exec_()

def MINUTE(q):                     #一分钟的是最简单的,直接使用就好。
    app = create_qapp()
    widget = ChartWidget()
    widget.add_plot("candle", hide_x_axis=True)
    widget.add_plot("volume", maximum_height=180)
    widget.add_item(CandleItem, "candle", "candle")
    widget.add_item(VolumeItem, "volume", "volume")
    widget.add_cursor()

    def update_bar():
        if not q.empty():
            bar = q.get(True)
            widget.update_bar(bar)

    timer = QtCore.QTimer()
    timer.timeout.connect(update_bar)
    timer.start(50)
    widget.setWindowTitle("一分钟")
    widget.setGeometry(860, 15, 1050, 550)
    widget.show()
    app.exec_()


if __name__ == '__main__':
    manager = multiprocessing.Manager()
    q_1m = manager.Queue()
    q_5m = manager.Queue()
    q_30m = manager.Queue()
    q_4h = manager.Queue()
    su = manager.Queue()

    pw = multiprocessing.Process(target=putbardata, args=(q_1m,q_5m,q_30m,q_4h,su))
    pr_1m = multiprocessing.Process(target=MINUTE, args=(q_1m,))
    pr_5m = multiprocessing.Process(target=MINUTE_5m, args=(q_5m,))
    pr_30m = multiprocessing.Process(target=MINUTE_30m, args=(q_30m,))
    pr_4h = multiprocessing.Process(target=MINUTE_4h, args=(q_4h,))

    pw.start()
    pr_1m.start()
    pr_5m.start()
    pr_30m.start()
    pr_4h.start()

    sudu = input("请输入速度:")
    su.put(sudu)

    time.sleep(1000000)
    print('任务完成')

大概说一下原理,程序设定了5个进程,通过通道交换数据,其中一个进程发送数据,另外4个进程接受数据,接受数据的四个进程就是4个周期的窗口,把接受的一分钟数据变化成3分钟30分钟等,并用图形展示出来。
只要控制发送数据的节奏,就可以动态的把行情从新演示一遍了。

这是盘后复盘用的,可以回忆一下当天到底发生了什么。

国内期货有一个盘立方软件是可以完美复盘的,数字货币没有这个东西,tradingview有这个功能,但是每月要收费90元,而且tradingview也只能使用1分钟数据复盘。

身为程序员,当然不愿意掏钱,因为自己可以写一个。

感谢vnpy提供的ChartWidget,真的很好用。


最新信息
统计
主题
11287
帖子
42619
已注册用户
92809
最新用户
在线用户
69
在线来宾用户
78449