一、交易策略解释
核心思想
涡旋指标(Vortex Indicator,VI)量化交易策略的核心思想是利用价格运动中的"涡旋"模式来识别和跟踪市场趋势。 该策略基于价格运动在形成趋势时展现的特定方向性运动模式,通过比较正向涡旋指标(+VI)和负向涡旋指标(-VI)的相对强度来确定趋势的方向和力度。
当+VI线上穿-VI线时,表明上升动量增强,可能形成上升趋势,产生做多信号;当-VI线上穿+VI线时,表明下降动量增强,可能形成下降趋势,产生做空信号。 该策略的本质是抓住市场趋势的起始阶段并伴随趋势发展获利,同时通过涡旋指标值的大小判断趋势强度,协助进行动态仓位管理。
理论基础
涡旋指标由Etienne Botes和Douglas Siepman于2010年首次引入交易社区,其理论基础源于自然界中涡旋流动的现象(如水流绕过障碍物时形成的涡流模式)。 该指标理论受到J. Welles Wilder方向运动概念的影响,认为连续价格柱之间的关系可以提供关于市场方向的有价值见解。学术研究表明,价格运动在形成趋势时确实表现出类似涡旋的特性:
价格波动的方向性集聚:当市场形成趋势时,价格波动会在某一方向上集聚,表现为连续的高点比前一个高点更高,低点比前一个低点更高(上升趋势);或连续的低点比前一个低点更低,高点比前一个高点更低(下降趋势)。
多周期相关性:趋势形成通常跨越多个时间周期,形成自相似的价格模式,这与自然界中的涡旋结构类似。
动量累积效应:市场趋势往往伴随着动量的积累过程,这可以通过涡旋指标中+VI和-VI的相对变化捕捉到。
策略适用场景
明显趋势市场:策略在明显趋势形成时表现最佳,特别是在大宗商品、外汇和期货等波动性较大的市场中。
中长期交易:虽然涡旋指标可用于不同时间框架,但通常在日线、4小时线等中长期时间框架上表现更佳,可以过滤掉短期噪音。
高波动性品种:指标在波动较大的期货品种中能更好地捕捉趋势起点,如能源期货、金属期货和农产品期货等。
宏观经济转折期:在宏观经济出现明显转折,导致商品价格趋势发生改变时,涡旋指标能较早捕捉到这种变化。
季节性波动市场:对于具有季节性波动特征的商品期货,涡旋指标能够有效识别季节性价格趋势的开始。
策略原理
涡旋指标(VI)的计算涉及以下几个步骤:
1. 计算真实范围(TR): TR = max(当前高点 - 当前低点, |当前高点 - 前一收盘价|, |当前低点 - 前一收盘价|)
2. 计算正向涡旋运动(+VM): +VM = |当前高点 - 前一低点|
3. 计算负向涡旋运动(-VM): -VM = |当前低点 - 前一高点|
4. 计算N周期内TR、+VM和-VM的总和: 通常N=14,但可根据不同市场条件优化
5. 计算正向涡旋指标(+VI): +VI = (N周期+VM总和) / (N周期TR总和)
6. 计算负向涡旋指标(-VI): -VI = (N周期-VM总和) / (N周期TR总和)
交易逻辑
入场信号:
当+VI上穿-VI时,产生做多信号
当-VI上穿+VI时,产生做空信号
为减少假信号,加入条件:只有当穿越线的VI值大于1时,信号才有效
出场信号:
当持有多头仓位时,如果-VI上穿+VI,平仓离场
当持有空头仓位时,如果+VI上穿-VI,平仓离场
止损设置:
基于ATR(平均真实范围)设置动态止损
多头止损位 = 入场价 - 2 * ATR
空头止损位 = 入场价 + 2 * ATR
回测
回测初始设置
- 测试周期: 2022 年 11 月 1 日至 2023 年 4 月 19 日
- 交易品种: CFFEX.IC2306
- 初始资金: 1000万元
回测结果

上述回测累计收益走势图

代码
#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = "Chaos" from datetime import date import numpy as np import pandas as pd from tqsdk import TqApi, TqAuth, TqBacktest, TargetPosTask, BacktestFinished from tqsdk.ta import ATR # ===== 全局参数设置 ===== SYMBOL = "CFFEX.IC2306" # 螺纹钢期货合约 POSITION_SIZE = 30 # 固定交易手数 START_DATE = date(2022, 11, 1) # 回测开始日期 END_DATE = date(2023, 4, 19) # 回测结束日期 # 涡旋指标参数 VI_PERIOD = 14 # 涡旋指标周期 ATR_PERIOD = 14 # ATR指标周期 ATR_MULTIPLIER = 2.0 # 止损倍数 VI_THRESHOLD = 1.0 # VI值阈值,筛选强度较大的信号 # ===== 全局变量 ===== current_direction = 0 # 当前持仓方向:1=多头,-1=空头,0=空仓 entry_price = 0 # 开仓价格 stop_loss_price = 0 # 止损价格 # ===== 涡旋指标计算函数 ===== def calculate_vortex(df, period=14): """计算涡旋指标""" # 计算真实范围(TR) df['tr'] = np.maximum( np.maximum( df['high'] - df['low'], np.abs(df['high'] - df['close'].shift(1)) ), np.abs(df['low'] - df['close'].shift(1)) ) # 计算正向涡旋运动(+VM) df['plus_vm'] = np.abs(df['high'] - df['low'].shift(1)) # 计算负向涡旋运动(-VM) df['minus_vm'] = np.abs(df['low'] - df['high'].shift(1)) # 计算N周期内的总和 df['tr_sum'] = df['tr'].rolling(window=period).sum() df['plus_vm_sum'] = df['plus_vm'].rolling(window=period).sum() df['minus_vm_sum'] = df['minus_vm'].rolling(window=period).sum() # 计算涡旋指标 df['plus_vi'] = df['plus_vm_sum'] / df['tr_sum'] df['minus_vi'] = df['minus_vm_sum'] / df['tr_sum'] return df # ===== 策略开始 ===== print("开始运行涡旋指标(VI)期货策略...") # 创建API实例 api = TqApi(backtest=TqBacktest(start_dt=START_DATE, end_dt=END_DATE), auth=TqAuth("快期账号", "快期密码")) # 订阅合约的日K线数据 klines = api.get_kline_serial(SYMBOL, 60 * 60 * 24) # 日线数据 # 创建目标持仓任务 target_pos = TargetPosTask(api, SYMBOL) try: while True: # 等待更新 api.wait_update() # 如果K线有更新 if api.is_changing(klines.iloc[-1], "datetime"): # 确保有足够的数据计算指标 if len(klines) < max(VI_PERIOD, ATR_PERIOD) + 5: continue # 计算涡旋指标 df = pd.DataFrame(klines) df = calculate_vortex(df, VI_PERIOD) # 计算ATR atr_data = ATR(klines, ATR_PERIOD) current_atr = float(atr_data.atr.iloc[-1]) # 获取最新和前一个周期的数据 current_price = float(klines.close.iloc[-1]) current_plus_vi = float(df.plus_vi.iloc[-1]) current_minus_vi = float(df.minus_vi.iloc[-1]) prev_plus_vi = float(df.plus_vi.iloc[-2]) prev_minus_vi = float(df.minus_vi.iloc[-2]) # 获取当前日期 current_datetime = pd.to_datetime(klines.datetime.iloc[-1], unit='ns') date_str = current_datetime.strftime('%Y-%m-%d') # 输出调试信息 print(f"日期: {date_str}, 价格: {current_price}, +VI: {current_plus_vi:.4f}, -VI: {current_minus_vi:.4f}, ATR: {current_atr:.2f}") # ===== 交易逻辑 ===== # 空仓状态 - 寻找开仓机会 if current_direction == 0: # 多头信号: +VI上穿-VI且+VI > 阈值 if prev_plus_vi <= prev_minus_vi and current_plus_vi > current_minus_vi and current_plus_vi > VI_THRESHOLD: # 设置入场价格 entry_price = current_price # 设置止损价格 stop_loss_price = entry_price - ATR_MULTIPLIER * current_atr # 设置持仓方向和目标持仓 current_direction = 1 target_pos.set_target_volume(POSITION_SIZE) print(f"多头开仓: 价格={entry_price}, 手数={POSITION_SIZE}, 止损价={stop_loss_price:.2f}") # 空头信号: -VI上穿+VI且-VI > 阈值 elif prev_minus_vi <= prev_plus_vi and current_minus_vi > current_plus_vi and current_minus_vi > VI_THRESHOLD: # 设置入场价格 entry_price = current_price # 设置止损价格 stop_loss_price = entry_price + ATR_MULTIPLIER * current_atr # 设置持仓方向和目标持仓 current_direction = -1 target_pos.set_target_volume(-POSITION_SIZE) print(f"空头开仓: 价格={entry_price}, 手数={POSITION_SIZE}, 止损价={stop_loss_price:.2f}") # 多头持仓 - 检查平仓条件 elif current_direction == 1: # 条件1: 止损触发 if current_price <= stop_loss_price: profit_pct = (current_price - entry_price) / entry_price * 100 target_pos.set_target_volume(0) current_direction = 0 print(f"多头止损平仓: 价格={current_price}, 盈亏={profit_pct:.2f}%") # 条件2: 信号反转 (-VI上穿+VI) elif prev_minus_vi <= prev_plus_vi and current_minus_vi > current_plus_vi: profit_pct = (current_price - entry_price) / entry_price * 100 target_pos.set_target_volume(0) current_direction = 0 print(f"多头信号平仓: 价格={current_price}, 盈亏={profit_pct:.2f}%") # 空头持仓 - 检查平仓条件 elif current_direction == -1: # 条件1: 止损触发 if current_price >= stop_loss_price: profit_pct = (entry_price - current_price) / entry_price * 100 target_pos.set_target_volume(0) current_direction = 0 print(f"空头止损平仓: 价格={current_price}, 盈亏={profit_pct:.2f}%") # 条件2: 信号反转 (+VI上穿-VI) elif prev_plus_vi <= prev_minus_vi and current_plus_vi > current_minus_vi: profit_pct = (entry_price - current_price) / entry_price * 100 target_pos.set_target_volume(0) current_direction = 0 print(f"空头信号平仓: 价格={current_price}, 盈亏={profit_pct:.2f}%") except BacktestFinished as e: print(f"策略运行异常: {e}") api.close()