From b3c5a235635fa88d677a9490408023bfe5247235 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Sat, 11 Sep 2021 21:46:27 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E6=A8=A1=E5=9D=97]=20=E8=B7=9F?= =?UTF-8?q?=E5=8D=95APP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/app/trade_copy/engine.py | 443 +++++++++++++++++++++++++++++ vnpy/app/trade_copy/ui/__init__.py | 1 + vnpy/app/trade_copy/ui/tc.ico | Bin 0 -> 20510 bytes vnpy/app/trade_copy/ui/widget.py | 184 ++++++++++++ 4 files changed, 628 insertions(+) create mode 100644 vnpy/app/trade_copy/engine.py create mode 100644 vnpy/app/trade_copy/ui/__init__.py create mode 100644 vnpy/app/trade_copy/ui/tc.ico create mode 100644 vnpy/app/trade_copy/ui/widget.py diff --git a/vnpy/app/trade_copy/engine.py b/vnpy/app/trade_copy/engine.py new file mode 100644 index 00000000..1799920b --- /dev/null +++ b/vnpy/app/trade_copy/engine.py @@ -0,0 +1,443 @@ +""" +vnpy 2.x版跟单应用 +华富资产,大佳 + +源帐号 => 跟单应用 => 目标帐号 +配置步骤: +1、源帐号需要添加RpcServerApp,并启动。(无界面或有界面都可以) +2、目标帐号交易程序,添加TradeCopyApp,配置源账号的Rep/Pub地址 + +跟单规则:源帐号 仓位 * 倍率 => 目标帐号的目标仓位 +""" +import os +import csv +from threading import Thread +from queue import Queue, Empty +from copy import copy +from collections import defaultdict, namedtuple +from datetime import datetime +import logging +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.constant import Exchange +from vnpy.trader.object import ( + SubscribeRequest, + OrderRequest, + Offset, + Direction, + OrderType, + TickData, + ContractData +) + +from vnpy.rpc import RpcClient +from vnpy.trader.event import EVENT_TICK, EVENT_CONTRACT, EVENT_POSITION, EVENT_TIMER + +from vnpy.trader.utility import load_json, save_json, extract_vt_symbol +from vnpy.app.spread_trading.base import EVENT_SPREAD_DATA, SpreadData + +APP_NAME = "TradeCopy" +EVENT_TRADECOPY_LOG = "eTradeCopyLog" +EVENT_TRADECOPY = 'eTradeCopy' + + +class TradeCopyEngine(BaseEngine): + """ + 跟单交易 + source ==> trade_copy ==> target + """ + setting_filename = "trade_copy_setting.json" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + # 是否激活跟单 + self.active = False + + # 持仓字典 合约+方向: vt_symbol, direction, src_volume, target_volume, cur_volume, count + self.pos_dict = {} + + # 被跟单得rpc server得请求端口、广播端口 + self.source_rpc_rep = 'tcp://localhost:2015' + self.source_rpc_pub = 'tcp://localhost:2018' + + # 跟单比率(1 ~ n) + # target_pos_volume = round(source_pos_volume * copy_ratio) + # 简单得四舍五入 + self.copy_ratio = 1 + + # 跟单检查间隔(缺省5秒) + self.copy_interval = 5 + + # 定时器 + self.timer_count = 0 + + # RPC客户端,用于连接源帐号 + self.client = RpcClient() + + # 回调函数 + self.client.callback = self.client_callback + + # 接受本地position event更新 + self.accept_local = False + + # 加载配置 + self.load_setting() + + # 注册事件 + self.register_event() + + def load_setting(self): + """ + 加载配置 + :return: + """ + try: + setting = load_json(self.setting_filename) + self.source_rpc_rep = setting.get("source_rpc_rep", "") + self.source_rpc_pub = setting.get("source_rpc_pub", "") + self.copy_ratio = setting.get('copy_ratio', 1) + self.copy_interval = setting.get('copy_interval', 5) + + except Exception as ex: + self.write_log(f'{APP_NAME}加载配置文件{self.setting_filename}异常{str(ex)}') + + def save_setting(self): + """ + 保存设置 + :return: + """ + setting = { + "source_rpc_rep": self.source_rpc_rep, + "source_rpc_pub": self.source_rpc_pub, + "copy_ratio": self.copy_ratio, + 'copy_interval': self.copy_interval + } + save_json(self.setting_filename, setting) + self.write_log(f'保存设置完毕:{setting}') + + def register_event(self): + """ + 注册事件处理 + :return: + """ + self.event_engine.register(EVENT_POSITION, self.process_position_event) + self.event_engine.register(EVENT_TIMER, self.process_timer_event) + + def client_callback(self, topic: str, event: Event): + """ + rpc客户端(源帐号)得event回报 + :param topic: + :param event: + :return: + """ + if event is None: + print("none event", topic, event) + return + + # 只处理持仓事件 + if event.type == EVENT_POSITION: + src_pos = event.data + + # 不处理套利合约 + if ' ' in src_pos.symbol or '&' in src_pos.symbol: + return + + # key = 合约+方向 + k = f'{src_pos.vt_symbol}.{src_pos.direction.value}' + pos = self.pos_dict.get(k, {}) + + # 更新持仓中属于source得部分, 计算出目标持仓 + target_volume = int(round(src_pos.volume * self.copy_ratio)) + pos.update({'vt_symbol': src_pos.vt_symbol, + 'direction': src_pos.direction, + 'name': src_pos.name, + 'symbol': src_pos.symbol, + 'src_volume': src_pos.volume, + 'target_volume': target_volume, + 'count': pos.get('count', 0) + 1, + 'volume': pos.get('volume', 0), + 'yd_volume': pos.get('yd_volume', 0), + 'cur_price': pos.get('cur_price', 0), + 'price': pos.get('price', 0), + 'diff': pos.get('diff', target_volume - pos.get('volume', 0)) + }) + + self.pos_dict.update({k: pos}) + + def process_position_event(self, event: Event): + """ + 处理本地帐号得持仓更新事件 + :param event: + :return: + """ + # 必须等到rpc的仓位都到齐了,才接收本地仓位 + if not self.accept_local: + return + + cur_pos = event.data + + # 不处理套利合约 + if ' ' in cur_pos.symbol or '&' in cur_pos.symbol: + return + + # key = 合约+方向 + k = f'{cur_pos.vt_symbol}.{cur_pos.direction.value}' + pos = self.pos_dict.get(k, {}) + + # 更新持仓中属于source得部分 + pos.update({ + 'cur_positionid': cur_pos.vt_positionid, + 'volume': cur_pos.volume, + 'yd_volume': cur_pos.yd_volume, + 'price': cur_pos.price, + 'cur_price': cur_pos.cur_price, + 'diff': pos.get('target_volume', 0) - cur_pos.volume, + 'count': pos.get('count', 0) + 1 + }) + if 'name' not in pos: + pos.update({'name': cur_pos.name}) + if 'vt_symbol' not in pos: + pos.update({'vt_symbol': cur_pos.vt_symbol}) + if 'direction' not in pos: + pos.update({'direction': cur_pos.direction}) + if 'symbol' not in pos: + pos.update({'symbol': cur_pos.symbol}) + if 'src_volume' not in pos: + pos.update({'src_volume': 0}) + if 'target_volume' not in pos: + pos.update({'target_volume': 0}) + + self.pos_dict.update({k: pos}) + + def put_event(self): + """ + 更细监控表 + :return: + """ + for key in self.pos_dict.keys(): + pos = self.pos_dict.get(key, {}) + # 补充key + pos.update({'vt_positionid': key}) + # dict => object + data = namedtuple("TradeCopy", pos.keys())(*pos.values()) + # 推送事件 + event = Event( + EVENT_TRADECOPY, + data + ) + self.event_engine.put(event) + + def process_timer_event(self, event: Event): + """定时执行""" + self.timer_count += 1 + + if self.timer_count % 2 == 0: + self.put_event() + + if self.timer_count < self.copy_interval: + return + + self.timer_count = 0 + + # 未激活,不执行 + if not self.active: + return + + # 执行跟单仓位比较 + for key in self.pos_dict.keys(): + pos = self.pos_dict.get(key) + + # 等到两次rpc pos后,才激活本地持仓更新 + if not self.accept_local and pos.get('count', 0) > 2: + self.accept_local = True + self.write_log(f'激活本地持仓更新') + + # 需要20次更新pos才算有效 + if pos.get('count', 0) <= 20: + continue + + target_volume = pos.get('target_volume', 0) + cur_volume = pos.get('volume', 0) + direction = pos.get('direction') + + # 目标仓位 > 当前仓位, 需要开仓 + if target_volume > cur_volume >= 0: + volume = target_volume - cur_volume + self.open_pos(vt_symbol=pos.get('vt_symbol'), + direction=direction, + volume=volume) + continue + + # 目标仓位 < 当前仓位, 需要减仓 + if 0 <= target_volume < cur_volume: + # 减仓数量 + volume = cur_volume - target_volume + + # 平仓相反方向 + if direction == Direction.LONG: + direction = Direction.SHORT + else: + direction = Direction.LONG + + self.close_pos(vt_symbol=pos.get('vt_symbol'), + direction=direction, + volume=volume, + vt_positionid=pos.get('cur_positionid')) + + def open_pos(self, vt_symbol, direction, volume): + """ + 买入、或做空 + :param vt_symbol: + :param direction: + :param volume: + :return: + """ + cur_tick = self.main_engine.get_tick(vt_symbol) + contract = self.main_engine.get_contract(vt_symbol) + symbol, exchange = extract_vt_symbol(vt_symbol) + if contract is None: + self.write_log(f'异常,{vt_symbol}的合约信息不存在') + return + + if cur_tick is None: + req = SubscribeRequest( + symbol=symbol, + exchange=exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) + self.write_log(f'订阅合约{vt_symbol}') + return + + dt_now = datetime.now() + + # 最新tick的时间,与当前的时间超过间隔,不处理(例如休盘时间) + if (dt_now - cur_tick.datetime).total_seconds() > self.copy_interval: + self.write_log(f'{vt_symbol} 最后tick时间{cur_tick.datetime}不满足开仓要求,当前时间:{dt_now}') + return + + open_price = cur_tick.ask_price_1 if direction == Direction.LONG else cur_tick.bid_price_1 + + order = OrderRequest( + symbol=symbol, + exchange=exchange, + direction=direction, + offset=Offset.OPEN, + volume=volume, + price=open_price, + type=OrderType.FAK + ) + self.write_log(f'发出委托开仓,{vt_symbol}, {direction.value},{volume},{open_price} ') + self.main_engine.send_order(order, contract.gateway_name) + + def close_pos(self, vt_symbol, direction, volume, vt_positionid): + """ + sell or cover + :param vt_symbol: + :param direction: + :param volume: + :return: + """ + cur_tick = self.main_engine.get_tick(vt_symbol) + contract = self.main_engine.get_contract(vt_symbol) + cur_pos = self.main_engine.get_position(vt_positionid) + if contract is None: + self.write_log(f'异常,{vt_symbol}的合约信息不存在') + return + + symbol, exchange = extract_vt_symbol(vt_symbol) + if cur_tick is None: + req = SubscribeRequest( + symbol=symbol, + exchange=exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) + self.write_log(f'订阅合约{vt_symbol}') + return + + if cur_pos is None: + self.write_log(f'异常,{vt_positionid}的持仓信息不存在') + return + + dt_now = datetime.now() + + # 最新tick的时间,与当前的时间超过间隔,不处理(例如休盘时间) + if (dt_now - cur_tick.datetime).total_seconds() > self.copy_interval: + self.write_log(f'{vt_symbol} 最后tick时间{cur_tick.datetime}不满足开仓要求,当前时间:{dt_now}') + return + + close_price = cur_tick.ask_price_1 if direction == Direction.LONG else cur_tick.bid_price_1 + + offset = Offset.CLOSE + if exchange in [Exchange.SHFE, Exchange.CFFEX and Exchange.INE]: + + # 优先平昨仓 + if cur_pos.yd_volume > 0: + # 平昨 + offset = Offset.CLOSEYESTERDAY + + # 如果平昨数量不够,平掉所有昨仓,剩余仓位,下次再平 + if cur_pos.yd_volume < volume: + self.write_log(f'{vt_symbol} 平仓数量:{volume} => {cur_pos.yd_volume}') + volume = cur_pos.yd_volume + else: + offset = Offset.CLOSETODAY + + order = OrderRequest( + symbol=symbol, + exchange=exchange, + direction=direction, + offset=offset, + volume=volume, + price=close_price, + type=OrderType.FAK + ) + self.write_log(f'发出委托开仓,{vt_symbol}, {direction.value},{volume},{close_price} ') + self.main_engine.send_order(order, contract.gateway_name) + + def start_copy(self, source_req_addr, source_pub_addr, copy_ratio, copy_interval): + """ + 开始执行跟单 + :return: + """ + # 订阅事件 + self.client.subscribe_topic("") + + if source_req_addr != self.source_rpc_rep: + self.source_rpc_rep = source_req_addr + if source_pub_addr != self.source_rpc_pub: + self.source_rpc_pub = source_pub_addr + if copy_ratio != self.copy_ratio: + self.copy_ratio = copy_ratio + if copy_interval != self.copy_interval and self.copy_interval >= 1: + self.copy_interval = copy_interval + self.write_log(f'保存设置') + self.save_setting() + + # 连接rpc客户端 + self.write_log(f'开始连接rpc客户端') + self.client.start(self.source_rpc_rep, self.source_rpc_pub) + + # 激活 + self.write_log(f'激活跟单') + self.active = True + + def stop_copy(self): + """ + 停止跟单 + :return: + """ + self.active = False + self.write_log(f'停止跟单') + + def write_log(self, msg: str, source: str = "", level: int = logging.DEBUG): + """ + 更新日志 + :param msg: + :param source: + :param level: + :return: + """ + self.event_engine.put(Event(EVENT_TRADECOPY_LOG, msg)) + super().write_log(msg, source, level) diff --git a/vnpy/app/trade_copy/ui/__init__.py b/vnpy/app/trade_copy/ui/__init__.py new file mode 100644 index 00000000..b6874c15 --- /dev/null +++ b/vnpy/app/trade_copy/ui/__init__.py @@ -0,0 +1 @@ +from .widget import TcManager diff --git a/vnpy/app/trade_copy/ui/tc.ico b/vnpy/app/trade_copy/ui/tc.ico new file mode 100644 index 0000000000000000000000000000000000000000..e7ee47a923dfcb1aa7484622a870f32890db441e GIT binary patch literal 20510 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 S&b^lBGo5?k7Z7i;+ 日志更新函数() + self.signal_log.connect(self.process_log_event) + + self.event_engine.register(EVENT_TRADECOPY_LOG, self.signal_log.emit) + + def process_log_event(self, event: Event): + """处理日志""" + timestamp = datetime.now().strftime("%H:%M:%S") + msg = f"{timestamp}\t{event.data}" + self.log_monitor.append(msg) + + def reset_setting(self): + """ + 重置配置 + :return: + """ + self.line_rep_addr.setText(self.default_source_rpc_pub) + self.line_pub_addr.setText(self.default_source_rpc_pub) + self.line_copy_ratio.setText(self.default_copy_ratio) + self.line_interval.setText(self.default_copy_interval) + + def start_copy(self): + """ + 连接源帐号(RPC) + :return: + """ + + source_req_addr = str(self.line_rep_addr.text()) + source_pub_addr = str(self.line_pub_addr.text()) + copy_ratio = float(self.line_copy_ratio.text()) + copy_interval = float(self.line_interval.text()) + self.tc_engine.start_copy(source_req_addr, source_pub_addr, copy_ratio, copy_interval) + + for widget in self.widgetList: + widget.setEnabled(False) + self.btn_stop_engine.setEnabled(True) + + def stop_copy(self): + + if self.tc_engine: + self.tc_engine.stop_copy()