涡旋指标期货量化策略
一、交易策略解释
核心思想
涡旋指标(Vortex Indicator,VI)量化交易策略的核心思想是利用价格运动中的"涡旋"模式来识别和跟踪市场趋势。 该策略基于价格运动在形成趋势时展现的特定方向性运动模式,通过比较正向涡旋指标(+VI)和负向涡旋指标(-VI)的相对强度来确定趋势的方向和力度。
当+VI线上穿-VI线时,表明上升动量增强,可能形成上升趋势,产生做多信号;当-VI线上穿+VI线时,表明下降动量增强,可能形成下降趋势,产生做空信号。 该策略的本质是抓住市场趋势的起始阶段并伴随趋势发展获利,同时通过涡旋指标值的大小判断趋势强度,协助进行动态仓位管理。
理论基础
涡旋指标由Etienne Botes和Douglas Siepman于2010年首次引入交易社区,其理论基础源于自然界中涡旋流动的现象(如水流绕过障碍物时形成的涡流模式)。 该指标理论受到J. Welles Wilder方向运动概念的影响,认为连续价格柱之间的关系可以提供关于市场方向的有价值见解。学术研究表明,价格运动在形成趋势时确实表现出类似涡旋的特性:
-
价格波动的方向性集聚:当市场形成趋势时,价格波动会在某一方向上集聚,表现为连续的高点比前一个高点更高,低点比前一个低点更高(上升趋势);或连续的低点比前一个低点更低,高点比前一个高点更低(下降趋势)。
-
多周期相关性:趋势形成通常跨越多个时间周期,形成自相似的价格模式,这与自然界中的涡旋结构类似。
-
动量累积效应:市场趋势往往伴随着动量的积累过程,这可以通过涡旋指标中+VI和-VI的相对变化捕捉到。
策略适用场景
-
明显趋势市场:策略在明显趋势形成时表现最佳,特别是在大宗商品、外汇和期货等波动性较大的市场中。
-
中长期交易:虽然涡旋指标可用于不同时间框架,但通常在日线、4小时线等中长期时间框架上表现更佳,可以过滤掉短期噪音。
-
高波动性品种:指标在波动较大的期货品种中能更好地捕捉趋势起点,如能源期货、金属期货和农产品期货等。
-
宏观经济转折期:在宏观经济出现明显转折,导致商品价格趋势发生改变时,涡旋指标能较早捕捉到这种变化。
-
季节性波动市场:对于具有季节性波动特征的商品期货,涡旋指标能够有效识别季节性价格趋势的开始。
二、天勤介绍
天勤平台概述
天勤(TqSdk)是一个由信易科技开发的开源量化交易系统,为期货、期权等衍生品交易提供专业的量化交易解决方案。平台具有以下特 点:
- 丰富的行情数据: 提供所有可交易合约的全部Tick和K线数据,基于内存数据库实现零延迟访问。
- 一站式的解决方案: 从历史数据分析到实盘交易的完整工具链,打通开发、回测、模拟到实盘的全流程。
- 专业的技术支持: 近百个技术指标源码,深度集成pandas和numpy,采用单线程异步模型保证性能。
策略开发流程
- 环境准备
- 安装Python环境(推荐Python 3.6或以上版本)
- 安装tqsdk包:pip install tqsdk
- 注册天勤账户获取访问密钥
- 数据准备
- 订阅近月与远月合约行情
- 获取历史K线或者Tick数据(用于分析与行情推进)
- 策略编写
- 设计信号生成逻辑(基于价差、均值和标准差)
- 编写交易执行模块(开仓、平仓逻辑)
- 实现风险控制措施(止损、资金管理)
- 回测验证
- 设置回测时间区间和初始资金
- 运行策略获取回测结果
- 分析绩效指标(胜率、收益率、夏普率等)
- 策略优化
- 调整参数(标准差倍数、窗口大小等)
- 添加过滤条件(成交量、波动率等)
- 完善风险控制机制
三、天勤实现策略
策略原理
涡旋指标(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()