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