201 lines
7.2 KiB
Python
201 lines
7.2 KiB
Python
from datetime import time
|
||
from vnpy.trader.constant import Interval
|
||
from vnpy.trader.utility import ArrayManager, BarGenerator, load_json, save_json
|
||
from vnpy.trader.object import TickData, BarData
|
||
|
||
from elite_optionstrategy import (
|
||
StrategyTemplate,
|
||
Variable,
|
||
Parameter,
|
||
PortfolioData,
|
||
ChainData,
|
||
OptionData,
|
||
OptionBarGenerator,
|
||
)
|
||
|
||
|
||
class AdvancedSpreadStrategy(StrategyTemplate):
|
||
"""基于Dual Thrust信号做空符合价差的策略"""
|
||
|
||
author: str = "用Python的交易员"
|
||
|
||
option_portfolio: str = Parameter("MO") # 期权产品代码
|
||
underlying_symbol: str = Parameter("IMJQ00.CFFEX") # 标的合约代码
|
||
k1: float = Parameter(0.3) # 多头系数
|
||
k2: float = Parameter(0.9) # 空头系数
|
||
exit_time: time = Parameter(time(14, 55)) # 平仓时间
|
||
percent_add: float = Parameter(0.002) # 委托超价比例
|
||
otm_level: int = Parameter(0) # 做空期权档位
|
||
leg1_ratio: int = Parameter(4) # 顺势腿的比例
|
||
leg2_ratio: int = Parameter(1) # 逆势腿的比例
|
||
|
||
dual_thrust_signal: int = Variable(0) # 当前信号多空(1多头,-1空头,0无信号)
|
||
atm_strike: float = Variable(0) # 当前平值行权价
|
||
|
||
def on_init(self) -> None:
|
||
"""策略初始化"""
|
||
self.write_log("策略初始化")
|
||
|
||
# K线截面合成器
|
||
self.obg: OptionBarGenerator = OptionBarGenerator(self.on_bars)
|
||
|
||
# 订阅行情
|
||
self.subscribe_options(self.option_portfolio)
|
||
self.subscribe_data(self.underlying_symbol)
|
||
|
||
# 标的信号对象
|
||
self.factor: DualThrustFactor = DualThrustFactor(
|
||
self.underlying_symbol, self.k1, self.k2, self.exit_time
|
||
)
|
||
|
||
# 加载标的历史数据初始化
|
||
bars: list[BarData] = self.load_bars(self.underlying_symbol, 100, Interval.MINUTE)
|
||
for bar in bars:
|
||
self.factor.update_bar(bar)
|
||
|
||
# 缓存文件名称
|
||
self.data_filename: str = f"{self.name}_data.json"
|
||
|
||
def on_start(self) -> None:
|
||
"""策略启动"""
|
||
self.write_log("策略启动")
|
||
data: dict = load_json(self.data_filename)
|
||
self.dual_thrust_signal = data.get("dual_thrust_signal", 0)
|
||
|
||
def on_stop(self) -> None:
|
||
"""策略停止"""
|
||
self.write_log("策略停止")
|
||
data: dict = {"dual_thrust_signal": self.dual_thrust_signal}
|
||
save_json(self.data_filename, data)
|
||
|
||
def on_tick(self, tick: TickData) -> None:
|
||
"""Tick推送"""
|
||
self.obg.update_tick(tick)
|
||
|
||
def on_bars(self, bars: dict[str, BarData]) -> None:
|
||
"""K线推送"""
|
||
# 更新标的信号
|
||
underlying_bar: BarData = bars.pop(self.underlying_symbol, None)
|
||
if underlying_bar:
|
||
self.factor.update_bar(underlying_bar)
|
||
|
||
# 获取期权组合对象
|
||
portfolio: PortfolioData = self.get_portfolio(self.option_portfolio)
|
||
price_data: dict[str, float] = {}
|
||
for bar in bars.values():
|
||
price_data[bar.vt_symbol] = bar.close_price
|
||
portfolio.update_price(price_data)
|
||
|
||
# 获取当月期权链
|
||
front_chain: ChainData = portfolio.get_chain_by_level(0)
|
||
if not front_chain:
|
||
self.write_log("无法获取当月期权链,请检查是否正确添加了期权合约")
|
||
return
|
||
|
||
# 计算平值期权
|
||
front_chain.calculate_atm()
|
||
self.atm_strike = front_chain.atm_strike
|
||
|
||
# 获取Dual Thrust信号
|
||
signal: int = self.factor.get_signal()
|
||
|
||
# 固定交易手数为1
|
||
trading_size: int = 1
|
||
|
||
# 根据信号开仓
|
||
if signal != self.dual_thrust_signal:
|
||
self.clear_targets()
|
||
|
||
call: OptionData = front_chain.get_option_by_level(cp=1, level=self.otm_level)
|
||
put: OptionData = front_chain.get_option_by_level(cp=-1, level=self.otm_level)
|
||
|
||
if call and put:
|
||
if signal == 1: # 多头信号:做空Put(顺势腿)和Call(逆势腿)
|
||
self.set_target(put.vt_symbol, -trading_size * self.leg1_ratio)
|
||
self.set_target(call.vt_symbol, -trading_size * self.leg2_ratio)
|
||
elif signal == -1: # 空头信号:做空Call(顺势腿)和Put(逆势腿)
|
||
self.set_target(call.vt_symbol, -trading_size * self.leg1_ratio)
|
||
self.set_target(put.vt_symbol, -trading_size * self.leg2_ratio)
|
||
|
||
# 平仓逻辑:在exit_time前平仓
|
||
if underlying_bar.datetime.time() >= self.exit_time:
|
||
self.clear_targets()
|
||
|
||
self.dual_thrust_signal = signal
|
||
self.execute_trading(price_data, self.percent_add)
|
||
self.put_event()
|
||
|
||
|
||
class DualThrustFactor:
|
||
"""标的物Dual Thrust因子(基于价格区间输出多空信号)"""
|
||
|
||
def __init__(self, vt_symbol: str, k1: float, k2: float, exit_time: time) -> None:
|
||
self.vt_symbol: str = vt_symbol
|
||
self.k1: float = k1
|
||
self.k2: float = k2
|
||
self.exit_time: time = exit_time
|
||
self.bars = []
|
||
|
||
self.day_open: float = 0
|
||
self.day_high: float = 0
|
||
self.day_low: float = 0
|
||
self.day_range: float = 0
|
||
self.long_entry: float = 0
|
||
self.short_entry: float = 0
|
||
self.signal: int = 0 # 1多头,-1空头,0无信号
|
||
|
||
self.bg: BarGenerator = BarGenerator(self.update_bar, 1, self.update_window_bar)
|
||
self.am: ArrayManager = ArrayManager(40) # 缓存足够的历史数据
|
||
|
||
def update_tick(self, tick: TickData) -> None:
|
||
"""Tick更新"""
|
||
self.bg.update_tick(tick)
|
||
|
||
def update_bar(self, bar: BarData) -> None:
|
||
"""K线更新"""
|
||
self.bg.update_bar(bar)
|
||
|
||
def update_window_bar(self, bar: BarData) -> None:
|
||
"""日级别K线更新"""
|
||
# 检查是否为新交易日
|
||
self.bars.append(bar)
|
||
if len(self.bars) <= 2:
|
||
return
|
||
else:
|
||
self.bars.pop(0)
|
||
last_bar = self.bars[-2]
|
||
if len(self.bars) == 0 or last_bar.datetime.date() != bar.datetime.date():
|
||
if len(self.bars) >= 1:
|
||
# 计算前一交易日的波动范围
|
||
prev_high = self.am.high[-1]
|
||
prev_low = self.am.low[-1]
|
||
prev_close = self.am.close[-1]
|
||
self.day_range = max(prev_high - prev_low, prev_high - prev_close, prev_close - prev_low)
|
||
|
||
# 重置当日参数
|
||
self.day_open = bar.open_price
|
||
self.day_high = bar.high_price
|
||
self.day_low = bar.low_price
|
||
else:
|
||
# 更新当日最高最低价
|
||
self.day_high = max(self.day_high, bar.high_price)
|
||
self.day_low = min(self.day_low, bar.low_price)
|
||
|
||
self.am.update_bar(bar)
|
||
|
||
# 计算入场价
|
||
if self.day_range > 0:
|
||
self.long_entry = self.day_open + self.k1 * self.day_range
|
||
self.short_entry = self.day_open - self.k2 * self.day_range
|
||
|
||
# 生成信号
|
||
if bar.close_price > self.long_entry:
|
||
self.signal = 1
|
||
elif bar.close_price < self.short_entry:
|
||
self.signal = -1
|
||
else:
|
||
self.signal = 0
|
||
|
||
def get_signal(self) -> int:
|
||
"""获取当前多空信号"""
|
||
return self.signal |