diff --git a/tests/trader/run.py b/tests/trader/run.py index 0b7fb501..1ab073c5 100644 --- a/tests/trader/run.py +++ b/tests/trader/run.py @@ -15,6 +15,7 @@ from vnpy.gateway.huobi import HuobiGateway from vnpy.app.cta_strategy import CtaStrategyApp from vnpy.app.csv_loader import CsvLoaderApp +from vnpy.app.algo_trading import AlgoTradingApp def main(): @@ -35,6 +36,7 @@ def main(): main_engine.add_app(CtaStrategyApp) main_engine.add_app(CsvLoaderApp) + main_engine.add_app(AlgoTradingApp) main_window = MainWindow(main_engine, event_engine) main_window.showMaximized() diff --git a/vnpy/app/algo_trading/__init__.py b/vnpy/app/algo_trading/__init__.py index e5be3d37..dba58a66 100644 --- a/vnpy/app/algo_trading/__init__.py +++ b/vnpy/app/algo_trading/__init__.py @@ -3,9 +3,10 @@ from pathlib import Path from vnpy.trader.app import BaseApp from .engine import AlgoEngine, APP_NAME +from .template import AlgoTemplate -class CtaStrategyApp(BaseApp): +class AlgoTradingApp(BaseApp): """""" app_name = APP_NAME diff --git a/vnpy/app/algo_trading/algos/__init__.py b/vnpy/app/algo_trading/algos/__init__.py index d6fa977a..e69de29b 100644 --- a/vnpy/app/algo_trading/algos/__init__.py +++ b/vnpy/app/algo_trading/algos/__init__.py @@ -1,59 +0,0 @@ -# encoding: UTF-8 - -''' -动态载入所有的策略类 -''' -from __future__ import print_function - -import os -import importlib -import traceback - - -# 用来保存算法类和控件类的字典 -ALGO_DICT = {} -WIDGET_DICT = {} - - -#---------------------------------------------------------------------- -def loadAlgoModule(path, prefix): - """使用importlib动态载入算法""" - for root, subdirs, files in os.walk(path): - for name in files: - # 只有文件名以Algo.py结尾的才是算法文件 - if len(name)>7 and name[-7:] == 'Algo.py': - try: - # 模块名称需要模块路径前缀 - moduleName = prefix + name.replace('.py', '') - module = importlib.import_module(moduleName) - - # 获取算法类和控件类 - algo = None - widget = None - - for k in dir(module): - # 以Algo结尾的类,是算法 - if k[-4:] == 'Algo': - algo = module.__getattribute__(k) - - # 以Widget结尾的类,是控件 - if k[-6:] == 'Widget': - widget = module.__getattribute__(k) - - # 保存到字典中 - if algo and widget: - ALGO_DICT[algo.templateName] = algo - WIDGET_DICT[algo.templateName] = widget - except: - print ('-' * 20) - print ('Failed to import strategy file %s:' %moduleName) - traceback.print_exc() - - -# 遍历algo目录下的文件 -path1 = os.path.abspath(os.path.dirname(__file__)) -loadAlgoModule(path1, 'vnpy.trader.app.algoTrading.algo.') - -# 遍历工作目录下的文件 -path2 = os.getcwd() -loadAlgoModule(path2, '') \ No newline at end of file diff --git a/vnpy/app/algo_trading/algos/twap_algo.py b/vnpy/app/algo_trading/algos/twap_algo.py index e69de29b..32fc6a65 100644 --- a/vnpy/app/algo_trading/algos/twap_algo.py +++ b/vnpy/app/algo_trading/algos/twap_algo.py @@ -0,0 +1,112 @@ +from vnpy.trader.constant import Offset, Direction +from vnpy.trader.object import TradeData +from vnpy.trader.engine import BaseEngine + +from vnpy.app.algo_trading import AlgoTemplate + + +class TwapAlgo(AlgoTemplate): + """""" + + display_name = "TWAP 时间加权平均" + + default_setting = { + "vt_symbol": "", + "direction": [Direction.LONG.value, Direction.SHORT.value], + "price": 0.0, + "volume": 0.0, + "time": 600, + "interval": 60, + "offset": [ + Offset.NONE.value, + Offset.OPEN.value, + Offset.CLOSE.value, + Offset.CLOSETODAY.value, + Offset.CLOSEYESTERDAY.value + ] + } + + def __init__( + self, + algo_engine: BaseEngine, + algo_name: str, + setting: dict + ): + """""" + super().__init__(algo_engine, algo_name, setting) + + # Parameters + self.vt_symbol = setting["vt_symbol"] + self.direction = Direction(setting["direction"]) + self.price = setting["price"] + self.volume = setting["volume"] + self.time = setting["time"] + self.interval = setting["interval"] + self.offset = Offset(setting["offset"]) + + # Variables + self.order_volume = self.volume / (self.time / self.interval) + self.timer_count = 0 + self.total_count = 0 + self.traded = 0 + + self.variables.extend([ + "traded", + "order_volume", + "timer_count", + "total_count" + ]) + + self.subscribe(self.vt_symbol) + self.put_parameters_event() + self.put_variables_event() + + def on_trade(self, trade: TradeData): + """""" + self.traded += trade.volume + + if self.traded >= self.volume: + self.stop() + else: + self.put_variables_event() + + def on_timer(self): + """""" + self.timer_count += 1 + self.total_count += 1 + self.put_variables_event() + + if self.total_count >= self.time: + self.write_log("执行时间已结束,停止算法") + self.stop() + return + + if self.timer_count < self.interval: + return + self.timer_count = 0 + + tick = self.get_tick(self.vt_symbol) + if not tick: + return + + self.cancel_all() + + left_volume = self.volume - self.traded + order_volume = min(self.order_volume, left_volume) + + if self.direction == Direction.LONG: + if tick.ask_price_1 <= self.price: + self.buy(self.vt_symbol, self.price, + order_volume, offset=self.offset) + self.write_log( + f"委托买入{self.vt_symbol}:{order_volume}@{self.price}") + else: + if tick.bid_price_1 >= self.price: + self.sell(self.vt_symbol, self.price, + order_volume, offset=self.offset) + self.write_log( + f"委托卖出{self.vt_symbol}:{order_volume}@{self.price}") + + def get_default_setting(self): + """""" + return self.default_setting diff --git a/vnpy/app/algo_trading/engine.py b/vnpy/app/algo_trading/engine.py index 3c5224dd..fe551043 100644 --- a/vnpy/app/algo_trading/engine.py +++ b/vnpy/app/algo_trading/engine.py @@ -1,65 +1,257 @@ -from vnpy.event import EventEngine +from vnpy.event import EventEngine, Event from vnpy.trader.engine import BaseEngine, MainEngine -from vnpy.trader.event import (EVENT_TICK, EVENT_TIMER, EVENT_ORDER, EVENT_TRADE) +from vnpy.trader.event import ( + EVENT_TICK, EVENT_TIMER, EVENT_ORDER, EVENT_TRADE) +from vnpy.trader.constant import (Direction, Offset, OrderType) +from vnpy.trader.object import (SubscribeRequest, OrderRequest) +from vnpy.trader.utility import load_json, save_json + +from .template import AlgoTemplate + + +APP_NAME = "AlgoTrading" + +EVENT_ALGO_LOG = "eAlgoLog" +EVENT_ALGO_SETTING = "eAlgoSetting" +EVENT_ALGO_VARIABLES = "eAlgoVariables" +EVENT_ALGO_PARAMETERS = "eAlgoParameters" class AlgoEngine(BaseEngine): """""" + setting_filename = "algo_trading_setting.json" def __init__(self, main_engine: MainEngine, event_engine: EventEngine): """Constructor""" - super().__init__(main_engine, event_engine) - + super().__init__(main_engine, event_engine, APP_NAME) + self.algos = {} self.symbol_algo_map = {} self.orderid_algo_map = {} - + + self.algo_templates = {} + self.algo_settings = {} + + self.load_algo_template() self.register_event() - + + def init_engine(self): + """""" + self.write_log("算法交易引擎启动") + self.load_algo_setting() + + def load_algo_template(self): + """""" + from .algos.twap_algo import TwapAlgo + + self.algo_templates[TwapAlgo.__name__] = TwapAlgo + + def load_algo_setting(self): + """""" + self.algo_settings = load_json(self.setting_filename) + + for setting_name, setting in self.algo_settings.items(): + self.put_setting_event(setting_name, setting) + + self.write_log("算法配置载入成功") + + def save_algo_setting(self): + """""" + save_json(self.setting_filename, self.algo_settings) + def register_event(self): """""" self.event_engine.register(EVENT_TICK, self.process_tick_event) self.event_engine.register(EVENT_TIMER, self.process_timer_event) self.event_engine.register(EVENT_ORDER, self.process_order_event) self.event_engine.register(EVENT_TRADE, self.process_trade_event) - - def process_tick_event(self): + + def process_tick_event(self, event: Event): """""" - pass - - def process_timer_event(self): + tick = event.data + + algos = self.symbol_algo_map.get(tick.vt_symbol, None) + if algos: + for algo in algos: + algo.update_tick(tick) + + def process_timer_event(self, event: Event): """""" - pass - - def process_trade_event(self): + for algo in self.algos.values(): + algo.update_timer() + + def process_trade_event(self, event: Event): """""" - pass - - def process_order_event(self): + trade = event.data + + algo = self.orderid_algo_map.get(trade.vt_orderid, None) + if algo: + algo.update_trade(trade) + + def process_order_event(self, event: Event): """""" - pass - + order = event.data + + algo = self.orderid_algo_map.get(order.vt_orderid, None) + if algo: + algo.update_order(order) + def start_algo(self, setting: dict): """""" - pass - - def stop_algo(self, algo_name: dict): + template_name = setting["template_name"] + algo_template = self.algo_templates[template_name] + + algo = algo_template.new(self, setting) + algo.start() + + self.algos[algo.algo_name] = algo + return algo.algo_name + + def stop_algo(self, algo_name: str): """""" - pass - + algo = self.algos.get(algo_name, None) + if algo: + algo.stop() + self.algos.pop(algo_name) + def stop_all(self): """""" - pass - - def subscribe(self, algo, vt_symbol): + for algo_name in list(self.algos.keys()): + self.stop_algo(algo_name) + + def subscribe(self, algo: AlgoTemplate, vt_symbol: str): """""" - pass - + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + self.write_log(f'订阅行情失败,找不到合约:{vt_symbol}', algo) + return + + algos = self.symbol_algo_map.setdefault(vt_symbol, set()) + + if not algos: + req = SubscribeRequest( + symbol=contract.symbol, + exchange=contract.exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) + + algos.add(algo) + def send_order( - self, - algo, - vt_symbol + self, + algo: AlgoTemplate, + vt_symbol: str, + direction: Direction, + price: float, + volume: float, + order_type: OrderType, + offset: Offset ): """""" - pass + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + self.write_log(f'委托下单失败,找不到合约:{vt_symbol}', algo) + return + + req = OrderRequest( + symbol=contract.symbol, + exchange=contract.exchange, + direction=direction, + type=order_type, + volume=volume, + price=price, + offset=offset + ) + vt_orderid = self.main_engine.send_order(req, contract.gateway_name) + + self.orderid_algo_map[vt_orderid] = algo + return vt_orderid + + def cancel_order(self, algo: AlgoTemplate, vt_orderid: str): + """""" + order = self.main_engine.get_order(vt_orderid) + + if not order: + self.write_log(f"委托撤单失败,找不到委托:{vt_orderid}", algo) + return + + req = order.create_cancel_request() + self.main_engine.cancel_order(req, order.gateway_name) + + def get_tick(self, algo: AlgoTemplate, vt_symbol: str): + """""" + tick = self.main_engine.get_tick(vt_symbol) + + if not tick: + self.write_log(f"查询行情失败,找不到行情:{vt_symbol}", algo) + + return tick + + def get_contract(self, algo: AlgoTemplate, vt_symbol: str): + """""" + contract = self.main_engine.get_contract(vt_symbol) + + if not contract: + self.write_log(f"查询合约失败,找不到合约:{vt_symbol}", algo) + + return contract + + def write_log(self, msg: str, algo: AlgoTemplate = None): + """""" + if algo: + msg = f"{algo.algo_name}:{msg}" + + event = Event(EVENT_ALGO_LOG) + event.data = msg + self.event_engine.put(event) + + def put_setting_event(self, setting_name: str, setting: dict): + """""" + event = Event(EVENT_ALGO_SETTING) + event.data = { + "setting_name": setting_name, + "setting": setting + } + self.event_engine.put(event) + + def update_algo_setting(self, setting_name: str, setting: dict): + """""" + self.algo_settings[setting_name] = setting + + self.save_algo_setting() + + self.put_setting_event(setting_name, setting) + + def remove_algo_setting(self, setting_name: str): + """""" + if setting_name not in self.algo_settings: + return + self.algo_settings.pop(setting_name) + + event = Event(EVENT_ALGO_SETTING) + event.data = { + "setting_name": setting_name, + "setting": None + } + self.event_engine.put(event) + + self.save_algo_setting() + + def put_parameters_event(self, algo: AlgoTemplate, parameters: dict): + """""" + event = Event(EVENT_ALGO_PARAMETERS) + event.data = { + "algo_name": algo.algo_name, + "parameters": parameters + } + self.event_engine.put(event) + + def put_variables_event(self, algo: AlgoTemplate, variables: dict): + """""" + event = Event(EVENT_ALGO_VARIABLES) + event.data = { + "algo_name": algo.algo_name, + "variables": variables + } + self.event_engine.put(event) diff --git a/vnpy/app/algo_trading/template.py b/vnpy/app/algo_trading/template.py index 1ba0c5a4..758e526f 100644 --- a/vnpy/app/algo_trading/template.py +++ b/vnpy/app/algo_trading/template.py @@ -1,30 +1,38 @@ from vnpy.trader.engine import BaseEngine from vnpy.trader.object import TickData, OrderData, TradeData -from vnpy.trader.constant import OrderType, Offset +from vnpy.trader.constant import OrderType, Offset, Direction + class AlgoTemplate: """""" - count = 0 + + _count = 0 + display_name = "" + default_setting = {} + variables = [] def __init__( - self, - algo_engine: BaseEngine, + self, + algo_engine: BaseEngine, algo_name: str, setting: dict ): """Constructor""" self.algo_engine = algo_engine self.algo_name = algo_name - - self.active = False - self.active_orders = {} # vt_orderid:order - @staticmethod - def new(cls, algo_engine:BaseEngine, setting: dict): + self.active = False + self.active_orders = {} # vt_orderid:order + + self.variables.insert(0, "active") + + @classmethod + def new(cls, algo_engine: BaseEngine, setting: dict): """Create new algo instance""" - cls.count += 1 - algo_name = f"{cls.__name__}_{cls.count}" + cls._count += 1 + algo_name = f"{cls.__name__}_{cls._count}" algo = cls(algo_engine, algo_name, setting) + return algo def update_tick(self, tick: TickData): """""" @@ -38,27 +46,27 @@ class AlgoTemplate: self.active_orders[order.vt_orderid] = order elif order.vt_orderid in self.active_orders: self.active_orders.pop(order.vt_orderid) - + self.on_order(order) - + def update_trade(self, trade: TradeData): """""" if self.active: self.on_trade(trade) - + def update_timer(self): """""" if self.active: self.on_timer() - + def on_start(self): """""" pass - + def on_stop(self): """""" pass - + def on_tick(self, tick: TickData): """""" pass @@ -66,58 +74,106 @@ class AlgoTemplate: def on_order(self, order: OrderData): """""" pass - + def on_trade(self, trade: TradeData): """""" pass - + def on_timer(self): """""" - pass - + pass + def start(self): """""" - pass - + self.active = True + self.on_start() + self.put_variables_event() + def stop(self): """""" - pass - + self.active = False + self.cancel_all() + self.on_stop() + self.put_variables_event() + + def subscribe(self, vt_symbol): + """""" + self.algo_engine.subscribe(self, vt_symbol) + def buy( - self, - vt_symbol, - price, - volume, + self, + vt_symbol, + price, + volume, order_type: OrderType = OrderType.LIMIT, offset: Offset = Offset.NONE ): """""" - return self.algo_engine.buy( + return self.algo_engine.send_order( + self, vt_symbol, + Direction.LONG, price, volume, order_type, offset ) - + def sell( - self, - vt_symbol, - price, - volume, + self, + vt_symbol, + price, + volume, order_type: OrderType = OrderType.LIMIT, offset: Offset = Offset.NONE ): """""" - return self.algo_engine.buy( + return self.algo_engine.send_order( + self, vt_symbol, + Direction.SHORT, price, volume, order_type, offset - ) - - - - - \ No newline at end of file + ) + + def cancel_order(self, vt_orderid: str): + """""" + self.algo_engine.cancel_order(self, vt_orderid) + + def cancel_all(self): + """""" + if not self.active_orders: + return + + for vt_orderid in self.active_orders.keys(): + self.cancel_order(vt_orderid) + + def get_tick(self, vt_symbol: str): + """""" + return self.algo_engine.get_tick(self, vt_symbol) + + def get_contract(self, vt_symbol: str): + """""" + return self.algo_engine.get_contract(self, vt_symbol) + + def write_log(self, msg: str): + """""" + self.algo_engine.write_log(msg, self) + + def put_parameters_event(self): + """""" + parameters = {} + for name in self.default_setting.keys(): + parameters[name] = getattr(self, name) + + self.algo_engine.put_parameters_event(self, parameters) + + def put_variables_event(self): + """""" + variables = {} + for name in self.variables: + variables[name] = getattr(self, name) + + self.algo_engine.put_variables_event(self, variables) diff --git a/vnpy/app/algo_trading/ui/__init__.py b/vnpy/app/algo_trading/ui/__init__.py new file mode 100644 index 00000000..9ac801bb --- /dev/null +++ b/vnpy/app/algo_trading/ui/__init__.py @@ -0,0 +1 @@ +from .widget import AlgoManager diff --git a/vnpy/app/algo_trading/ui/display.py b/vnpy/app/algo_trading/ui/display.py new file mode 100644 index 00000000..8d6b991b --- /dev/null +++ b/vnpy/app/algo_trading/ui/display.py @@ -0,0 +1,15 @@ +NAME_DISPLAY_MAP = { + "vt_symbol": "本地代码", + "direction": "方向", + "price": "价格", + "volume": "数量", + "time": "执行时间(秒)", + "interval": "每轮间隔(秒)", + "offset": "开平", + "active": "算法状态", + "traded": "成交数量", + "order_volume": "单笔委托", + "timer_count": "本轮读秒", + "total_count": "累计读秒", + "template_name": "算法模板" +} diff --git a/vnpy/app/algo_trading/ui/widget.py b/vnpy/app/algo_trading/ui/widget.py index e69de29b..57976d92 100644 --- a/vnpy/app/algo_trading/ui/widget.py +++ b/vnpy/app/algo_trading/ui/widget.py @@ -0,0 +1,571 @@ +""" +Widget for algo trading. +""" + +from functools import partial +from datetime import datetime + +from vnpy.event import EventEngine, Event +from vnpy.trader.engine import MainEngine +from vnpy.trader.ui import QtWidgets, QtCore + +from ..engine import ( + AlgoEngine, + AlgoTemplate, + APP_NAME, + EVENT_ALGO_LOG, + EVENT_ALGO_PARAMETERS, + EVENT_ALGO_VARIABLES, + EVENT_ALGO_SETTING +) +from .display import NAME_DISPLAY_MAP + + +class AlgoWidget(QtWidgets.QWidget): + """ + Start connection of a certain gateway. + """ + + def __init__( + self, + algo_engine: AlgoEngine, + algo_template: AlgoTemplate + ): + """""" + super().__init__() + + self.algo_engine = algo_engine + self.template_name = algo_template.__name__ + self.default_setting = algo_template.default_setting + + self.widgets = {} + + self.init_ui() + + def init_ui(self): + """ + Initialize line edits and form layout based on setting. + """ + self.setMaximumWidth(400) + + form = QtWidgets.QFormLayout() + + for field_name, field_value in self.default_setting.items(): + field_type = type(field_value) + + if field_type == list: + widget = QtWidgets.QComboBox() + widget.addItems(field_value) + else: + widget = QtWidgets.QLineEdit() + + display_name = NAME_DISPLAY_MAP.get(field_name, field_name) + + form.addRow(display_name, widget) + self.widgets[field_name] = (widget, field_type) + + start_algo_button = QtWidgets.QPushButton("启动算法") + start_algo_button.clicked.connect(self.start_algo) + form.addRow(start_algo_button) + + form.addRow(QtWidgets.QLabel("")) + + self.setting_name_line = QtWidgets.QLineEdit() + form.addRow("配置名称", self.setting_name_line) + + save_setting_button = QtWidgets.QPushButton("保存配置") + save_setting_button.clicked.connect(self.save_setting) + form.addRow(save_setting_button) + + self.setLayout(form) + + def get_setting(self): + """ + Get setting value from line edits. + """ + setting = {"template_name": self.template_name} + + for field_name, tp in self.widgets.items(): + widget, field_type = tp + if field_type == list: + field_value = str(widget.currentText()) + else: + try: + field_value = field_type(widget.text()) + except ValueError: + display_name = NAME_DISPLAY_MAP.get(field_name, field_name) + QtWidgets.QMessageBox.warning( + self, + "参数错误", + f"{display_name}参数类型应为{field_type},请检查!" + ) + return None + + setting[field_name] = field_value + + return setting + + def start_algo(self): + """ + Start algo trading. + """ + setting = self.get_setting() + if setting: + self.algo_engine.start_algo(setting) + + def update_setting(self, setting_name: str, setting: dict): + """ + Update setting into widgets. + """ + self.setting_name_line.setText(setting_name) + + for name, tp in self.widgets.items(): + widget, _ = tp + value = setting[name] + + if isinstance(widget, QtWidgets.QLineEdit): + widget.setText(str(value)) + elif isinstance(widget, QtWidgets.QComboBox): + ix = widget.findText(value) + widget.setCurrentIndex(ix) + + def save_setting(self): + """ + Save algo setting + """ + setting_name = self.setting_name_line.text() + if not setting_name: + return + + setting = self.get_setting() + if setting: + self.algo_engine.update_algo_setting(setting_name, setting) + + +class AlgoMonitor(QtWidgets.QTableWidget): + """""" + parameters_signal = QtCore.pyqtSignal(Event) + variables_signal = QtCore.pyqtSignal(Event) + + def __init__( + self, + algo_engine: AlgoEngine, + event_engine: EventEngine, + mode_active: bool + ): + """""" + super().__init__() + + self.algo_engine = algo_engine + self.event_engine = event_engine + self.mode_active = mode_active + + self.algo_cells = {} + + self.init_ui() + self.register_event() + + def init_ui(self): + """""" + labels = [ + "", + "算法", + "参数", + "状态" + ] + self.setColumnCount(len(labels)) + self.setHorizontalHeaderLabels(labels) + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + self.verticalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + + for column in range(2, 4): + self.horizontalHeader().setSectionResizeMode( + column, + QtWidgets.QHeaderView.Stretch + ) + self.setWordWrap(True) + + if not self.mode_active: + self.hideColumn(0) + + def register_event(self): + """""" + self.parameters_signal.connect(self.process_parameters_event) + self.variables_signal.connect(self.process_variables_event) + + self.event_engine.register( + EVENT_ALGO_PARAMETERS, self.parameters_signal.emit) + self.event_engine.register( + EVENT_ALGO_VARIABLES, self.variables_signal.emit) + + def process_parameters_event(self, event): + """""" + data = event.data + algo_name = data["algo_name"] + parameters = data["parameters"] + + cells = self.get_algo_cells(algo_name) + text = to_text(parameters) + cells["parameters"].setText(text) + + def process_variables_event(self, event): + """""" + data = event.data + algo_name = data["algo_name"] + variables = data["variables"] + + cells = self.get_algo_cells(algo_name) + variables_cell = cells["variables"] + text = to_text(variables) + variables_cell.setText(text) + + row = self.row(variables_cell) + active = variables["active"] + + if self.mode_active: + if active: + self.showRow(row) + else: + self.hideRow(row) + else: + if active: + self.hideRow(row) + else: + self.showRow(row) + + def stop_algo(self, algo_name: str): + """""" + self.algo_engine.stop_algo(algo_name) + + def get_algo_cells(self, algo_name: str): + """""" + cells = self.algo_cells.get(algo_name, None) + + if not cells: + stop_func = partial(self.stop_algo, algo_name=algo_name) + stop_button = QtWidgets.QPushButton("停止") + stop_button.clicked.connect(stop_func) + + name_cell = QtWidgets.QTableWidgetItem(algo_name) + parameters_cell = QtWidgets.QTableWidgetItem() + variables_cell = QtWidgets.QTableWidgetItem() + + self.insertRow(0) + self.setCellWidget(0, 0, stop_button) + self.setItem(0, 1, name_cell) + self.setItem(0, 2, parameters_cell) + self.setItem(0, 3, variables_cell) + + cells = { + "name": name_cell, + "parameters": parameters_cell, + "variables": variables_cell + } + self.algo_cells[algo_name] = cells + + return cells + + +class ActiveAlgoMonitor(AlgoMonitor): + """ + Monitor for active algos. + """ + + def __init__(self, algo_engine: AlgoEngine, event_engine: EventEngine): + """""" + super().__init__(algo_engine, event_engine, True) + + +class InactiveAlgoMonitor(AlgoMonitor): + """ + Monitor for inactive algos. + """ + + def __init__(self, algo_engine: AlgoEngine, event_engine: EventEngine): + """""" + super().__init__(algo_engine, event_engine, False) + + +class SettingMonitor(QtWidgets.QTableWidget): + """""" + setting_signal = QtCore.pyqtSignal(Event) + use_signal = QtCore.pyqtSignal(dict) + + def __init__(self, algo_engine: AlgoEngine, event_engine: EventEngine): + """""" + super().__init__() + + self.algo_engine = algo_engine + self.event_engine = event_engine + + self.settings = {} + self.setting_cells = {} + + self.init_ui() + self.register_event() + + def init_ui(self): + """""" + labels = [ + "", + "", + "名称", + "配置" + ] + self.setColumnCount(len(labels)) + self.setHorizontalHeaderLabels(labels) + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + self.verticalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + + self.horizontalHeader().setSectionResizeMode( + 3, + QtWidgets.QHeaderView.Stretch + ) + self.setWordWrap(True) + + def register_event(self): + """""" + self.setting_signal.connect(self.process_setting_event) + + self.event_engine.register( + EVENT_ALGO_SETTING, self.setting_signal.emit) + + def process_setting_event(self, event): + """""" + data = event.data + setting_name = data["setting_name"] + setting = data["setting"] + cells = self.get_setting_cells(setting_name) + + if setting: + self.settings[setting_name] = setting + + cells["setting"].setText(to_text(setting)) + else: + if setting_name in self.settings: + self.settings.pop(setting_name) + + row = self.row(cells["setting"]) + self.removeRow(row) + + self.setting_cells.pop(setting_name) + + def get_setting_cells(self, setting_name: str): + """""" + cells = self.setting_cells.get(setting_name, None) + + if not cells: + use_func = partial(self.use_setting, setting_name=setting_name) + use_button = QtWidgets.QPushButton("使用") + use_button.clicked.connect(use_func) + + remove_func = partial(self.remove_setting, + setting_name=setting_name) + remove_button = QtWidgets.QPushButton("移除") + remove_button.clicked.connect(remove_func) + + name_cell = QtWidgets.QTableWidgetItem(setting_name) + setting_cell = QtWidgets.QTableWidgetItem() + + self.insertRow(0) + self.setCellWidget(0, 0, use_button) + self.setCellWidget(0, 1, remove_button) + self.setItem(0, 2, name_cell) + self.setItem(0, 3, setting_cell) + + cells = { + "name": name_cell, + "setting": setting_cell + } + self.setting_cells[setting_name] = cells + + return cells + + def use_setting(self, setting_name: str): + """""" + setting = self.settings[setting_name] + setting["setting_name"] = setting_name + self.use_signal.emit(setting) + + def remove_setting(self, setting_name: str): + """""" + self.algo_engine.remove_algo_setting(setting_name) + + +class LogMonitor(QtWidgets.QTableWidget): + """""" + signal = QtCore.pyqtSignal(Event) + + def __init__(self, event_engine: EventEngine): + """""" + super().__init__() + + self.event_engine = event_engine + + self.init_ui() + self.register_event() + + def init_ui(self): + """""" + labels = [ + "时间", + "信息" + ] + self.setColumnCount(len(labels)) + self.setHorizontalHeaderLabels(labels) + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + self.verticalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + + self.horizontalHeader().setSectionResizeMode( + 1, + QtWidgets.QHeaderView.Stretch + ) + self.setWordWrap(True) + + def register_event(self): + """""" + self.signal.connect(self.process_log_event) + + self.event_engine.register(EVENT_ALGO_LOG, self.signal.emit) + + def process_log_event(self, event): + """""" + msg = event.data + timestamp = datetime.now().strftime("%H:%M:%S") + + timestamp_cell = QtWidgets.QTableWidgetItem(timestamp) + msg_cell = QtWidgets.QTableWidgetItem(msg) + + self.insertRow(0) + self.setItem(0, 0, timestamp_cell) + self.setItem(0, 1, msg_cell) + + +class AlgoManager(QtWidgets.QWidget): + """""" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + self.algo_engine = main_engine.get_engine(APP_NAME) + + self.algo_widgets = {} + + self.init_ui() + self.algo_engine.init_engine() + + def init_ui(self): + """""" + self.setWindowTitle("算法交易") + + # Left side control widgets + self.template_combo = QtWidgets.QComboBox() + self.template_combo.currentIndexChanged.connect(self.show_algo_widget) + + form = QtWidgets.QFormLayout() + form.addRow("算法", self.template_combo) + widget = QtWidgets.QWidget() + widget.setLayout(form) + + vbox = QtWidgets.QVBoxLayout() + vbox.addWidget(widget) + + for algo_template in self.algo_engine.algo_templates.values(): + widget = AlgoWidget(self.algo_engine, algo_template) + vbox.addWidget(widget) + + template_name = algo_template.__name__ + display_name = algo_template.display_name + + self.algo_widgets[template_name] = widget + self.template_combo.addItem(display_name, template_name) + + vbox.addStretch() + + stop_all_button = QtWidgets.QPushButton("全部停止") + stop_all_button.setFixedHeight(stop_all_button.sizeHint().height() * 2) + stop_all_button.clicked.connect(self.algo_engine.stop_all) + + vbox.addWidget(stop_all_button) + + # Right side monitor widgets + active_algo_monitor = ActiveAlgoMonitor( + self.algo_engine, self.event_engine + ) + inactive_algo_monitor = InactiveAlgoMonitor( + self.algo_engine, self.event_engine + ) + tab1 = QtWidgets.QTabWidget() + tab1.addTab(active_algo_monitor, "执行中") + tab1.addTab(inactive_algo_monitor, "已结束") + + log_monitor = LogMonitor(self.event_engine) + tab2 = QtWidgets.QTabWidget() + tab2.addTab(log_monitor, "日志") + + setting_monitor = SettingMonitor(self.algo_engine, self.event_engine) + setting_monitor.use_signal.connect(self.use_setting) + tab3 = QtWidgets.QTabWidget() + tab3.addTab(setting_monitor, "配置") + + grid = QtWidgets.QGridLayout() + grid.addWidget(tab1, 0, 0, 1, 2) + grid.addWidget(tab2, 1, 0) + grid.addWidget(tab3, 1, 1) + + hbox2 = QtWidgets.QHBoxLayout() + hbox2.addLayout(vbox) + hbox2.addLayout(grid) + self.setLayout(hbox2) + + self.show_algo_widget() + + def show_algo_widget(self): + """""" + ix = self.template_combo.currentIndex() + current_name = self.template_combo.itemData(ix) + + for template_name, widget in self.algo_widgets.items(): + if template_name == current_name: + widget.show() + else: + widget.hide() + + def use_setting(self, setting: dict): + """""" + setting_name = setting["setting_name"] + template_name = setting["template_name"] + + widget = self.algo_widgets[template_name] + widget.update_setting(setting_name, setting) + + def show(self): + """""" + self.showMaximized() + + +def to_text(data: dict): + """ + Convert dict data into string. + """ + buf = [] + for key, value in data.items(): + key = NAME_DISPLAY_MAP.get(key, key) + buf.append(f"{key}:{value}") + text = ",".join(buf) + return text diff --git a/vnpy/app/cta_strategy/engine.py b/vnpy/app/cta_strategy/engine.py index 9bff635b..37e5973a 100644 --- a/vnpy/app/cta_strategy/engine.py +++ b/vnpy/app/cta_strategy/engine.py @@ -40,6 +40,7 @@ from vnpy.trader.database import DbTickData, DbBarData from vnpy.trader.setting import SETTINGS from .base import ( + APP_NAME, EVENT_CTA_LOG, EVENT_CTA_STRATEGY, EVENT_CTA_STOPORDER, @@ -73,7 +74,7 @@ class CtaEngine(BaseEngine): def __init__(self, main_engine: MainEngine, event_engine: EventEngine): """""" super(CtaEngine, self).__init__( - main_engine, event_engine, "CtaStrategy") + main_engine, event_engine, APP_NAME) self.strategy_setting = {} # strategy_name: dict self.strategy_data = {} # strategy_name: dict