diff --git a/vnpy/app/cta_strategy_pro/back_testing.py b/vnpy/app/cta_strategy_pro/back_testing.py index d33b33bc..fabfed11 100644 --- a/vnpy/app/cta_strategy_pro/back_testing.py +++ b/vnpy/app/cta_strategy_pro/back_testing.py @@ -2273,6 +2273,8 @@ class BackTestingEngine(object): self.trade_dict.clear() self.trades.clear() self.trade_pnl_list = [] + self.last_bar.clear() + self.last_dt = None def append_trade(self, trade: TradeData): """ diff --git a/vnpy/app/cta_strategy_pro/engine.py b/vnpy/app/cta_strategy_pro/engine.py index ad9d8c47..e2eacc4d 100644 --- a/vnpy/app/cta_strategy_pro/engine.py +++ b/vnpy/app/cta_strategy_pro/engine.py @@ -868,6 +868,9 @@ class CtaEngine(BaseEngine): def get_contract(self, vt_symbol): return self.main_engine.get_contract(vt_symbol) + def get_all_contracts(self): + return self.main_engine.get_all_contracts() + def get_account(self, vt_accountid: str = ""): """ 查询账号的资金""" # 如果启动风控,则使用风控中的最大仓位 diff --git a/vnpy/app/cta_strategy_pro/portfolio_testing.py b/vnpy/app/cta_strategy_pro/portfolio_testing.py index 86119f7a..52d51598 100644 --- a/vnpy/app/cta_strategy_pro/portfolio_testing.py +++ b/vnpy/app/cta_strategy_pro/portfolio_testing.py @@ -188,6 +188,7 @@ class PortfolioTestingEngine(BackTestingEngine): self.cur_capital = self.init_capital # 更新设置期初资金 if not self.data_end_date: self.data_end_date = datetime.today() + self.test_end_date = datetime.now().strftime('%Y%m%d') # 保存回测脚本到数据库 self.save_setting_to_mongo() @@ -275,6 +276,7 @@ class PortfolioTestingEngine(BackTestingEngine): bar.high_price = float(bar_data['high']) bar.low_price = float(bar_data['low']) bar.volume = int(bar_data['volume']) + bar.open_interest = int(bar_data.get('open_interest', 0)) bar.date = bar_datetime.strftime('%Y-%m-%d') bar.time = bar_datetime.strftime('%H:%M:%S') str_td = str(bar_data.get('trading_day', '')) diff --git a/vnpy/app/cta_strategy_pro/template.py b/vnpy/app/cta_strategy_pro/template.py index 8351fd2e..3abcdc44 100644 --- a/vnpy/app/cta_strategy_pro/template.py +++ b/vnpy/app/cta_strategy_pro/template.py @@ -1154,7 +1154,12 @@ class CtaProFutureTemplate(CtaProTemplate): self.put_event() def on_trade(self, trade: TradeData): - """交易更新""" + """ + 交易更新 + 支持股指期货的对锁单或者解锁 + :param trade: + :return: + """ self.write_log(u'{},交易更新事件:{},当前持仓:{} ' .format(self.cur_datetime, trade.__dict__, @@ -1168,30 +1173,45 @@ class CtaProFutureTemplate(CtaProTemplate): dist_record['volume'] = trade.volume dist_record['price'] = trade.price dist_record['symbol'] = trade.vt_symbol + if trade.exchange == Exchange.CFFEX: + if trade.direction == Direction.LONG: + if abs(self.position.short_pos) >= trade.volume: + self.position.short_pos += trade.volume + else: + self.position.long_pos += trade.volume + else: + if self.position.long_pos >= trade.volume: + self.position.long_pos -= trade.volume + else: + self.position.short_pos -= trade.volume - if trade.direction == Direction.LONG and trade.offset == Offset.OPEN: - dist_record['operation'] = 'buy' - self.position.open_pos(trade.direction, volume=trade.volume) + self.position.pos = self.position.long_pos + self.position.short_pos dist_record['long_pos'] = self.position.long_pos dist_record['short_pos'] = self.position.short_pos + else: + if trade.direction == Direction.LONG and trade.offset == Offset.OPEN: + dist_record['operation'] = 'buy' + self.position.open_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos - if trade.direction == Direction.SHORT and trade.offset == Offset.OPEN: - dist_record['operation'] = 'short' - self.position.open_pos(trade.direction, volume=trade.volume) - dist_record['long_pos'] = self.position.long_pos - dist_record['short_pos'] = self.position.short_pos + if trade.direction == Direction.SHORT and trade.offset == Offset.OPEN: + dist_record['operation'] = 'short' + self.position.open_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos - if trade.direction == Direction.LONG and trade.offset != Offset.OPEN: - dist_record['operation'] = 'cover' - self.position.close_pos(trade.direction, volume=trade.volume) - dist_record['long_pos'] = self.position.long_pos - dist_record['short_pos'] = self.position.short_pos + if trade.direction == Direction.LONG and trade.offset != Offset.OPEN: + dist_record['operation'] = 'cover' + self.position.close_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos - if trade.direction == Direction.SHORT and trade.offset != Offset.OPEN: - dist_record['operation'] = 'sell' - self.position.close_pos(trade.direction, volume=trade.volume) - dist_record['long_pos'] = self.position.long_pos - dist_record['short_pos'] = self.position.short_pos + if trade.direction == Direction.SHORT and trade.offset != Offset.OPEN: + dist_record['operation'] = 'sell' + self.position.close_pos(trade.direction, volume=trade.volume) + dist_record['long_pos'] = self.position.long_pos + dist_record['short_pos'] = self.position.short_pos self.save_dist(dist_record) self.pos = self.position.pos diff --git a/vnpy/app/cta_strategy_pro/template_spread.py b/vnpy/app/cta_strategy_pro/template_spread.py index 111e097a..13e17741 100644 --- a/vnpy/app/cta_strategy_pro/template_spread.py +++ b/vnpy/app/cta_strategy_pro/template_spread.py @@ -71,12 +71,21 @@ class CtaSpreadTemplate(CtaTemplate): self.act_price_tick = None # 主动合约价格跳动 self.pas_price_tick = None # 被动合约价格跳动 + self.act_symbol_size = None + self.pas_symbol_size = None + self.act_margin_rate = None + self.pas_margin_rate = None self.act_pos = None # 主动合约得holding pos self.pas_pos = None # 被动合约得holding pos self.last_minute = None # 最后的分钟,用于on_tick内每分钟处理的逻辑 + # 资金相关 + self.max_invest_rate = 0.1 # 最大仓位(0~1) + self.max_invest_margin = 0 # 资金上限 0,不限制 + self.max_invest_pos = 0 # 单向头寸数量上限 0,不限制 + def update_setting(self, setting: dict): """更新配置参数""" super().update_setting(setting) @@ -85,6 +94,10 @@ class CtaSpreadTemplate(CtaTemplate): self.pas_symbol, self.pas_exchange = extract_vt_symbol(self.pas_vt_symbol) self.act_price_tick = self.cta_engine.get_price_tick(self.act_vt_symbol) self.pas_price_tick = self.cta_engine.get_price_tick(self.pas_vt_symbol) + self.act_symbol_size = self.cta_engine.get_size(self.act_vt_symbol) + self.pas_symbol_size = self.cta_engine.get_size(self.pas_vt_symbol) + self.act_margin_rate = self.cta_engine.get_margin_rate(self.act_vt_symbol) + self.pas_margin_rate = self.cta_engine.get_margin_rate(self.pas_vt_symbol) # 实盘采用FAK if not self.backtesting and self.activate_fak: @@ -467,6 +480,11 @@ class CtaSpreadTemplate(CtaTemplate): except Exception as ex: self.write_error(u'save_tns 异常:{} {}'.format(str(ex), traceback.format_exc())) + def save_data(self): + """保存过程数据""" + if not self.backtesting: + return + def send_wechat(self, msg: str): """实盘时才发送微信""" if self.backtesting: @@ -529,7 +547,7 @@ class CtaSpreadTemplate(CtaTemplate): if trade.offset == Offset.OPEN: # 更新开仓均价/数量 if trade.vt_symbol == self.act_vt_symbol: - opened_price = grid.snapshot.get('act_open_price', grid.volume * self.act_vol_ratio) + opened_price = grid.snapshot.get('act_open_price', 0) opened_volume = grid.snapshot.get('act_open_volume', grid.volume * self.act_vol_ratio) act_open_volume = opened_volume + trade.volume act_open_price = (opened_price * opened_volume + trade.price * trade.volume) / act_open_volume @@ -632,6 +650,9 @@ class CtaSpreadTemplate(CtaTemplate): # 在策略得活动订单中,移除 self.active_orders.pop(order.vt_orderid, None) self.gt.save() + if len(self.active_orders) < 1: + self.entrust = 0 + return def on_order_open_canceled(self, order: OrderData): """ @@ -1202,12 +1223,16 @@ class CtaSpreadTemplate(CtaTemplate): self.write_error(f'spd_short,{self.pas_vt_symbol}开多仓{grid.volume * self.pas_vol_ratio}手失败,' f'委托价:{self.cur_pas_tick.ask_price_1}') return [] - grid.snapshot.update({"act_vt_symbol": self.act_vt_symbol, "act_open_volume": grid.volume * self.act_vol_ratio, - "pas_vt_symbol": self.pas_vt_symbol, "pas_open_volume": grid.volume * self.pas_vol_ratio}) + # WJ: update_grid_trade() 中会根据实际交易的数目更新 act_open_volume & pas_open_volume, + # 所以这里必须设置为初始值0,否则grid中的 open_volume会是实际持仓的2倍,导致spd_sell & spd_cover时失败 + # grid.snapshot.update({"act_vt_symbol": self.act_vt_symbol, "act_open_volume": grid.volume * self.act_vol_ratio, + # "pas_vt_symbol": self.pas_vt_symbol, "pas_open_volume": grid.volume * self.pas_vol_ratio}) + grid.snapshot.update({"act_vt_symbol": self.act_vt_symbol, "act_open_volume": 0, + "pas_vt_symbol": self.pas_vt_symbol, "pas_open_volume": 0}) grid.order_status = True grid.order_datetime = self.cur_datetime - vt_orderids = act_vt_orderids.extend(pas_vt_orderids) + vt_orderids = act_vt_orderids + pas_vt_orderids # 不能用act_vt_orderids.extend(pas_vt_orderids),它的返回值为 None,会导致没有vt_orderids self.write_log(u'spd short vt_order_ids:{0}'.format(vt_orderids)) return vt_orderids @@ -1264,11 +1289,11 @@ class CtaSpreadTemplate(CtaTemplate): self.write_error(f'spd_short,{self.pas_vt_symbol}开空仓{grid.volume * self.pas_vol_ratio}手失败,' f'委托价:{self.cur_pas_tick.bid_price_1}') return [] - grid.snapshot.update({"act_vt_symbol": self.act_vt_symbol, "act_open_volume": grid.volume * self.act_vol_ratio, - "pas_vt_symbol": self.pas_vt_symbol, "pas_open_volume": grid.volume * self.pas_vol_ratio}) + grid.snapshot.update({"act_vt_symbol": self.act_vt_symbol, "act_open_volume": 0, + "pas_vt_symbol": self.pas_vt_symbol, "pas_open_volume": 0}) grid.order_status = True grid.order_datetime = self.cur_datetime - vt_orderids = act_vt_orderids.extend(pas_vt_orderids) + vt_orderids = act_vt_orderids + pas_vt_orderids self.write_log(u'spd buy vt_ordderids:{}'.format(vt_orderids)) return vt_orderids @@ -1342,7 +1367,7 @@ class CtaSpreadTemplate(CtaTemplate): grid.order_status = True grid.order_datetime = self.cur_datetime - vt_orderids = act_vt_orderids.extend(pas_vt_orderids) + vt_orderids = act_vt_orderids + pas_vt_orderids self.write_log(f'spd sell vt_orderids:{vt_orderids}') return vt_orderids @@ -1415,6 +1440,6 @@ class CtaSpreadTemplate(CtaTemplate): grid.order_status = True grid.order_datetime = self.cur_datetime - vt_orderids = act_vt_orderids.extend(pas_vt_orderids) + vt_orderids = act_vt_orderids + pas_vt_orderids self.write_log(f'spd cover vt_orderids:{vt_orderids}') return vt_orderids diff --git a/vnpy/gateway/binancef/binancef_gateway.py b/vnpy/gateway/binancef/binancef_gateway.py index ef28538d..99b3dda1 100644 --- a/vnpy/gateway/binancef/binancef_gateway.py +++ b/vnpy/gateway/binancef/binancef_gateway.py @@ -214,6 +214,8 @@ class BinancefRestApi(RestClient): self.orders = {} + self.cache_position_symbols = {} + self.accountid = "" def sign(self, request: Request) -> Request: @@ -561,6 +563,15 @@ class BinancefRestApi(RestClient): pnl=float(d["unRealizedProfit"]), gateway_name=self.gateway_name, ) + + # 如果持仓数量为0,且不在之前缓存过的合约信息中,不做on_position + if position.volume == 0: + if position.symbol not in self.cache_position_symbols: + continue + else: + if position.symbol not in self.cache_position_symbols: + self.cache_position_symbols.update({position.symbol: position.volume}) + self.gateway.on_position(position) #if position.symbol == 'BTCUSDT': # self.gateway.write_log(f'{position.__dict__}\n {d}') diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py index 02240dbf..c5f8049f 100644 --- a/vnpy/gateway/ctp/ctp_gateway.py +++ b/vnpy/gateway/ctp/ctp_gateway.py @@ -154,6 +154,7 @@ OPTIONTYPE_CTP2VT = { MAX_FLOAT = sys.float_info.max symbol_exchange_map = {} +option_name_map = {} symbol_name_map = {} symbol_size_map = {} index_contracts = {} @@ -764,6 +765,9 @@ class CtpTdApi(TdApi): self.accountid = self.userid + self.long_option_cost = None # 多头期权动态市值 + self.short_option_cost = None # 空头期权动态市值 + def onFrontConnected(self): """""" self.gateway.write_log("交易服务器连接成功") @@ -903,7 +907,10 @@ class CtpTdApi(TdApi): # Update new position volume position.volume += data["Position"] - position.pnl += data["PositionProfit"] + if data["PositionProfit"] == 0 and position.symbol in option_name_map: + position.pnl += data["PositionCost"] - data["OpenCost"] + else: + position.pnl += data["PositionProfit"] # Calculate average position price if position.volume and size: @@ -916,8 +923,34 @@ class CtpTdApi(TdApi): else: position.frozen += data["LongFrozen"] + position.cur_price = self.gateway.prices.get(position.vt_symbol, None) + if position.cur_price is None: + position.cur_price = position.price + self.gateway.subscribe(SubscribeRequest(symbol=position.symbol, exchange=position.exchange)) + if last: + self.long_option_cost = None + self.short_option_cost = None for position in self.positions.values(): + if position.symbol in option_name_map: + # 重新累计多头期权动态权益 + if position.direction == Direction.LONG: + if self.long_option_cost is None: + self.long_option_cost = position.cur_price * position.volume * symbol_size_map.get( + position.symbol, 0) + else: + self.long_option_cost += position.cur_price * position.volume * symbol_size_map.get( + position.symbol, 0) + + # 重新累计空头期权动态权益 + if position.direction == Direction.SHORT: + if self.short_option_cost is None: + self.short_option_cost = position.cur_price * position.volume * symbol_size_map.get( + position.symbol, 0) + else: + self.short_option_cost += position.cur_price * position.volume * symbol_size_map.get( + position.symbol, 0) + self.gateway.on_position(position) self.positions.clear() @@ -929,10 +962,16 @@ class CtpTdApi(TdApi): if len(self.accountid)== 0: self.accountid = data['AccountID'] + balance = float(data["Balance"]) + if self.long_option_cost is not None: + balance += self.long_option_cost + if self.short_option_cost is not None: + balance -= self.short_option_cost + account = AccountData( accountid=data["AccountID"], pre_balance=round(float(data['PreBalance']), 7), - balance=round(float(data["Balance"]), 7), + balance=round(balance, 7), frozen=round(data["FrozenMargin"] + data["FrozenCash"] + data["FrozenCommission"], 7), gateway_name=self.gateway_name ) @@ -986,6 +1025,7 @@ class CtpTdApi(TdApi): contract.option_strike = data["StrikePrice"] contract.option_index = str(data["StrikePrice"]) contract.option_expiry = datetime.strptime(data["ExpireDate"], "%Y%m%d") + option_name_map[contract.symbol] = contract.name self.gateway.on_contract(contract) diff --git a/vnpy/gateway/gj/gj_gateway.py b/vnpy/gateway/gj/gj_gateway.py index c7b3e075..371e727e 100644 --- a/vnpy/gateway/gj/gj_gateway.py +++ b/vnpy/gateway/gj/gj_gateway.py @@ -1167,7 +1167,7 @@ class TqMdApi(): return try: from tqsdk import TqApi - self.api = TqApi(_stock=True) + self.api = TqApi(_stock=True,url="wss://u.shinnytech.com/t/nfmd/front/mobile") except Exception as e: self.gateway.write_log(f'天勤股票行情API接入异常:'.format(str(e))) self.gateway.write_log(traceback.format_exc()) diff --git a/vnpy/gateway/sopt/sopt_gateway.py b/vnpy/gateway/sopt/sopt_gateway.py index 8bcd3593..9099041a 100644 --- a/vnpy/gateway/sopt/sopt_gateway.py +++ b/vnpy/gateway/sopt/sopt_gateway.py @@ -125,7 +125,7 @@ CHINA_TZ = pytz.timezone("Asia/Shanghai") symbol_exchange_map = {} symbol_name_map = {} symbol_size_map = {} - +option_name_map = {} class SoptGateway(BaseGateway): """ @@ -388,7 +388,7 @@ class SoptMdApi(MdApi): return timestamp = f"{data['TradingDay']} {data['UpdateTime']}.{int(data['UpdateMillisec']/100)}" dt = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f") - dt = CHINA_TZ.localize(dt) + #dt = CHINA_TZ.localize(dt) tick = TickData( symbol=symbol, @@ -523,6 +523,9 @@ class SoptTdApi(TdApi): self.positions = {} self.sysid_orderid_map = {} + self.long_option_cost = None # 多头期权动态市值 + self.short_option_cost = None # 空头期权动态市值 + def onFrontConnected(self): """""" self.gateway.write_log("交易服务器连接成功") @@ -618,11 +621,14 @@ class SoptTdApi(TdApi): if not data: return + #self.gateway.write_log(print_dict(data)) + # Get buffered position object key = f"{data['InstrumentID'], data['PosiDirection']}" position = self.positions.get(key, None) if not position: position = PositionData( + accountid=self.userid, symbol=data["InstrumentID"], exchange=symbol_exchange_map[data["InstrumentID"]], direction=DIRECTION_SOPT2VT[data["PosiDirection"]], @@ -646,7 +652,10 @@ class SoptTdApi(TdApi): # Update new position volume position.volume += data["Position"] - position.pnl += data["PositionProfit"] + if data["PositionProfit"] == 0: + position.pnl += data["PositionCost"] - data["OpenCost"] + else: + position.pnl += data["PositionProfit"] # Calculate average position price if position.volume and size: @@ -659,27 +668,63 @@ class SoptTdApi(TdApi): else: position.frozen += data["LongFrozen"] + position.cur_price = self.gateway.prices.get(position.vt_symbol, None) + if position.cur_price is None: + position.cur_price = position.price + self.gateway.subscribe(SubscribeRequest(symbol=position.symbol, exchange=position.exchange)) + if last: + self.long_option_cost = None + self.short_option_cost = None for position in self.positions.values(): + if position.symbol in option_name_map: + # 重新累计多头期权动态权益 + if position.direction == Direction.LONG: + if self.long_option_cost is None: + self.long_option_cost = position.cur_price * position.volume * symbol_size_map.get(position.symbol, 0) + else: + self.long_option_cost += position.cur_price * position.volume * symbol_size_map.get(position.symbol, 0) + + # 重新累计空头期权动态权益 + if position.direction == Direction.SHORT: + if self.short_option_cost is None: + self.short_option_cost = position.cur_price * position.volume * symbol_size_map.get(position.symbol, 0) + else: + self.short_option_cost += position.cur_price * position.volume * symbol_size_map.get(position.symbol, 0) + self.gateway.on_position(position) self.positions.clear() def onRspQryTradingAccount(self, data: dict, error: dict, reqid: int, last: bool): """""" + + balance = float(data["Balance"]) + + # 资金差额(权利金,正数,是卖call或卖put,收入权利金; 负数,是买call、买put,付出权利金) + cash_in = data.get('CashIn') + #balance -= cash_in + + if self.long_option_cost is not None: + balance += self.long_option_cost + if self.short_option_cost is not None: + balance -= self.short_option_cost account = AccountData( accountid=data["AccountID"], - balance=data["Balance"], + balance=balance, frozen=data["FrozenMargin"] + data["FrozenCash"] + data["FrozenCommission"], gateway_name=self.gateway_name ) - account.available = data["Available"] - account.commission = round(float(data['Commission']), 7) - account.margin = round(float(data['CurrMargin']), 7) - account.close_profit = round(float(data['CloseProfit']), 7) - account.holding_profit = round(float(data['PositionProfit']), 7) - account.trading_day = str(data.get('TradingDay',datetime.now().strftime('%Y-%m-%d'))) + #self.gateway.write_log(print_dict(data)) + + account.available = data["Available"] + account.commission = round(float(data['Commission']), 7) + round(float(data['SpecProductCommission']), 7) + account.margin = round(float(data['CurrMargin']), 7) + account.close_profit = round(float(data['CloseProfit']), 7) + round(float(data['SpecProductCloseProfit']), 7) + account.holding_profit = round(float(data['PositionProfit']), 7) + round(float(data['SpecProductPositionProfit']), 7) + + account.trading_day = str(data.get('TradingDay', datetime.now().strftime('%Y-%m-%d'))) if '-' not in account.trading_day and len(account.trading_day) == 8: account.trading_day = '-'.join( [ @@ -701,7 +746,7 @@ class SoptTdApi(TdApi): contract = ContractData( symbol=data["InstrumentID"], exchange=EXCHANGE_SOPT2VT[data["ExchangeID"]], - name=data["InstrumentName"], + name=data["InstrumentName"].strip(), product=product, size=data["VolumeMultiple"], pricetick=data["PriceTick"], @@ -724,6 +769,7 @@ class SoptTdApi(TdApi): contract.option_index = get_option_index( contract.option_strike, data["InstrumentCode"] ) + option_name_map[contract.symbol] = contract.name self.gateway.on_contract(contract) diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py index 1dceabb0..38cee7ba 100644 --- a/vnpy/trader/ui/widget.py +++ b/vnpy/trader/ui/widget.py @@ -467,6 +467,7 @@ class PositionMonitor(BaseMonitor): "volume": {"display": "数量", "cell": BaseCell, "update": True}, "yd_volume": {"display": "昨仓", "cell": BaseCell, "update": True}, "frozen": {"display": "冻结", "cell": BaseCell, "update": True}, + "cur_price": {"display": "当前价", "cell": BaseCell, "update": True}, "price": {"display": "均价", "cell": BaseCell, "update": True}, "pnl": {"display": "盈亏", "cell": PnlCell, "update": True}, "gateway_name": {"display": "接口", "cell": BaseCell, "update": False},