diff --git a/requirements.txt b/requirements.txt index c212511b..9edf05e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pyqt +PyQt5 qdarkstyle \ No newline at end of file diff --git a/tests/trader/run.py b/tests/trader/run.py index 9fa44e5f..fd2df551 100644 --- a/tests/trader/run.py +++ b/tests/trader/run.py @@ -1,8 +1,11 @@ from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy.trader.ui import MainWindow, create_qapp +from vnpy.gateway.ib import IbGateway -from vnpy.trader.ui.widget import TickMonitor +import os +import logging +import time def main(): @@ -10,7 +13,9 @@ def main(): qapp = create_qapp() event_engine = EventEngine() + main_engine = MainEngine(event_engine) + main_engine.add_gateway(IbGateway) main_window = MainWindow(main_engine, event_engine) main_window.showMaximized() diff --git a/vnpy/gateway/__init__.py b/vnpy/gateway/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vnpy/gateway/ib/__init__.py b/vnpy/gateway/ib/__init__.py index e60e89c4..9d915bff 100644 --- a/vnpy/gateway/ib/__init__.py +++ b/vnpy/gateway/ib/__init__.py @@ -1 +1 @@ -from ib_gateway import IbGateway \ No newline at end of file +from .ib_gateway import IbGateway \ No newline at end of file diff --git a/vnpy/gateway/ib/ib_gateway.py b/vnpy/gateway/ib/ib_gateway.py index 43c0edda..e1b4b191 100644 --- a/vnpy/gateway/ib/ib_gateway.py +++ b/vnpy/gateway/ib/ib_gateway.py @@ -3,15 +3,19 @@ Please install ibapi from Interactive Brokers github page. """ from datetime import datetime from copy import copy +from threading import Thread +from queue import Empty from ibapi.wrapper import EWrapper from ibapi.client import EClient from ibapi.contract import Contract, ContractDetails from ibapi.order import Order -from ibapi.common import TickerId, OrderId, TickAttrib +from ibapi.common import TickerId, OrderId, TickAttrib, MAX_MSG_LEN from ibapi.ticktype import TickType from ibapi.order_state import OrderState from ibapi.execution import Execution +from ibapi.utils import BadMessage +from ibapi import comm from vnpy.trader.gateway import BaseGateway from vnpy.trader.object import ( @@ -62,7 +66,9 @@ PRICETYPE_VT2IB = {PRICETYPE_LIMIT: "LMT", PRICETYPE_MARKET: "MKT"} PRICETYPE_IB2VT = {v: k for k, v in PRICETYPE_VT2IB.items()} DIRECTION_VT2IB = {DIRECTION_LONG: "BUY", DIRECTION_SHORT: "SELL"} -DIRECTION_IB2VT = {v: k for k, v in DIRECTION_IB2VT.items()} +DIRECTION_IB2VT = {v: k for k, v in DIRECTION_VT2IB.items()} +DIRECTION_IB2VT["BOT"] = DIRECTION_LONG +DIRECTION_IB2VT["SLD"] = DIRECTION_SHORT EXCHANGE_VT2IB = { EXCHANGE_SMART: "SMART", @@ -91,7 +97,7 @@ PRODUCT_VT2IB = { PRODUCT_OPTION: "OPT", PRODUCT_FUTURES: "FUT" } -PRODUCT_IB2VT = {v: k for k,v in PRODUCT_VT2IB.items()} +PRODUCT_IB2VT = {v: k for k, v in PRODUCT_VT2IB.items()} OPTION_VT2IB = {OPTION_CALL: "CALL", OPTION_PUT: "PUT"} @@ -123,15 +129,11 @@ ACCOUNTFIELD_IB2VT = { class IbGateway(BaseGateway): """""" - default_setting = { - "host": "127.0.0.1", - "port": 7497, - "clientid": 1 - } + default_setting = {"host": "127.0.0.1", "port": 7497, "clientid": 1} def __init__(self, event_engine): """""" - super(Gateway, self).__init__(event_engine, "IB") + super(IbGateway, self).__init__(event_engine, "IB") self.api = IbApi(self) @@ -181,7 +183,7 @@ class IbGateway(BaseGateway): class IbApi(EWrapper): """""" - def __init__(self, gateway: Gateway): + def __init__(self, gateway: BaseGateway): """""" super(IbApi, self).__init__() @@ -189,15 +191,36 @@ class IbApi(EWrapper): self.gateway_name = gateway.gateway_name self.status = False + self.reqid = 0 self.orderid = 0 self.clientid = 0 self.ticks = {} self.orders = {} self.accounts = {} - self.contracts = {} - self.client = EClient(self) + self.tick_exchange = {} + + self.client = IbClient(self) + self.thread = Thread(target=self.client.run) + + def connectAck(self): + """ + Callback when connection is established. + """ + self.status = True + + log = LogData(msg="IB TWS连接成功", gateway_name=self.gateway_name) + self.gateway.on_log(log) + + def connectionClosed(self): + """ + Callback when connection is closed. + """ + self.status = False + + log = LogData(msg="IB TWS连接断开", gateway_name=self.gateway_name) + self.gateway.on_log(log) def nextValidId(self, orderId: int): """ @@ -213,12 +236,10 @@ class IbApi(EWrapper): """ super(IbApi, self).currentTime(time) - self.status = True - dt = datetime.fromtimestamp(time) time_string = dt.strftime("%Y-%m-%d %H:%M:%S.%f") log = LogData( - msg=f"IB TWS连接成功,服务器时间: {time_string}", + msg=f"服务器时间: {time_string}", gateway_name=self.gateway_name ) self.gateway.on_log(log) @@ -229,7 +250,7 @@ class IbApi(EWrapper): """ super(IbApi, self).error(reqId, errorCode, errorString) log = LogData( - msg=f"发生错误,错误代码:{errorCode},错误信息: {errorString}", + msg=f"信息通知,代码:{errorCode},内容: {errorString}", gateway_name=self.gateway_name ) self.gateway.on_log(log) @@ -253,10 +274,10 @@ class IbApi(EWrapper): name = TICKFIELD_IB2VT[tickType] setattr(tick, name, price) - # Forex and spot product of IB has no tick time and last price. + # Forex and spot product of IDEALPRO has no tick time and last price. # We need to calculate locally. - contract = self.contracts[reqId] - if contract.product in (PRODUCT_FOREX, PRODUCT_SPOT): + exchange = self.tick_exchange[reqId] + if exchange == EXCHANGE_IDEALPRO: tick.last_price = (tick.bid_price_1 + tick.ask_price_1) / 2 tick.datetime = datetime.now() self.gateway.on_tick(copy(tick)) @@ -323,7 +344,7 @@ class IbApi(EWrapper): ) orderid = str(orderId) - order = self.orders.get[orderid] + order = self.orders.get(orderid, None) order.status = STATUS_IB2VT[status] order.traded = filled @@ -349,12 +370,13 @@ class IbApi(EWrapper): ib_contract.exchange ), orderid=orderid, - direction=DIRECTION_IB2V[ib_order.action], - price=order.lmtPrice, - volume=order.totalQuantity, + direction=DIRECTION_IB2VT[ib_order.action], + price=ib_order.lmtPrice, + volume=ib_order.totalQuantity, gateway_name=self.gateway_name ) + self.orders[orderid] = order self.gateway.on_order(copy(order)) def updateAccountValue( @@ -379,6 +401,7 @@ class IbApi(EWrapper): accountid=accountid, gateway_name=self.gateway_name ) + self.accounts[accountid] = account name = ACCOUNTFIELD_IB2VT[key] setattr(account, name, float(val)) @@ -426,7 +449,6 @@ class IbApi(EWrapper): Callback of account update time. """ super(IbApi, self).updateAccountTime(timeStamp) - for account in self.accounts.values(): self.gateway.on_account(copy(account)) @@ -435,6 +457,7 @@ class IbApi(EWrapper): Callback of contract data update. """ super(IbApi, self).contractDetails(reqId, contractDetails) + ib_symbol = contractDetails.contract.conId ib_exchange = contractDetails.contract.exchange ib_size = contractDetails.contract.multiplier @@ -445,30 +468,32 @@ class IbApi(EWrapper): exchange=EXCHANGE_IB2VT.get(ib_exchange, ib_exchange), name=contractDetails.longName, - product=PRODUCT_VT2IB[ib_product] + product=PRODUCT_IB2VT[ib_product], size=ib_size, pricetick=contractDetails.minTick, gateway_name=self.gateway_name ) self.gateway.on_contract(contract) - + def execDetails(self, reqId: int, contract: Contract, execution: Execution): """ Callback of trade data update. """ super(IbApi, self).execDetails(reqId, contract, execution) - + today_date = datetime.now().strftime("%Y%m%d") trade = TradeData( symbol=contract.conId, - exchange=EXCHANGE_IB2VT.get(contract.exchange, contract.exchange) - orderid = str(execution.orderId), + exchange=EXCHANGE_IB2VT.get(contract.exchange, + contract.exchange), + orderid=str(execution.orderId), tradeid=str(execution.execId), direction=DIRECTION_IB2VT[execution.side], price=execution.price, volume=execution.shares, - time=datetime.strptime(f"{today_date} {execution.time}", "%Y%m%d %H:%M:%S"), - gateway_name = self.gateway_name + time=datetime.strptime(execution.time, + "%Y%m%d %H:%M:%S"), + gateway_name=self.gateway_name ) self.gateway.on_trade(trade) @@ -478,39 +503,50 @@ class IbApi(EWrapper): Callback of all sub accountid. """ super(IbApi, self).managedAccounts(accountsList) - + for account_code in accountsList.split(","): - self.client.reqAccountUpdates(account_code) - + self.client.reqAccountUpdates(True, account_code) + def connect(self, setting: dict): """ Connect to TWS. """ + if self.status: + return + self.clientid = setting["clientid"] self.client.connect( - setting["host"]. + setting["host"], setting["port"], setting["clientid"] ) - self.client.reqCurrentTime() - + self.thread.start() + + n = self.client.reqCurrentTime() + def close(self): """ Disconnect to TWS. """ + if not self.status: + return + self.status = False - self.client.eDisconnect() - - def subscribe(self, req:SubscribeRequest): + self.client.disconnect() + + def subscribe(self, req: SubscribeRequest): """ Subscribe tick data update. """ + if not self.status: + return + ib_contract = Contract() ib_contract.conId = str(req.symbol) ib_contract.exchange = EXCHANGE_VT2IB[req.exchange] - + # Get contract data from TWS. self.reqid += 1 self.client.reqContractDetails(self.reqid, ib_contract) @@ -526,26 +562,30 @@ class IbApi(EWrapper): gateway_name=self.gateway_name ) self.ticks[self.reqid] = tick - + self.tick_exchange[self.reqid] = req.exchange + def send_order(self, req: OrderRequest): """ Send a new order. """ + if not self.status: + return '' + self.orderid += 1 ib_contract = Contract() ib_contract.conId = str(req.symbol) ib_contract.exchange = EXCHANGE_VT2IB[req.exchange] - order = Order() - order.orderId = self.orderid - order.clientId = self.clientid - order.action = DIRECTION_VT2IB[req.direction] - order.orderType = PRICETYPE_VT2IB[req.price_type] - order.lmtPrice = req.price - order.totalQuantity = req.volume + ib_order = Order() + ib_order.orderId = self.orderid + ib_order.clientId = self.clientid + ib_order.action = DIRECTION_VT2IB[req.direction] + ib_order.orderType = PRICETYPE_VT2IB[req.price_type] + ib_order.lmtPrice = req.price + ib_order.totalQuantity = req.volume - self.client.placeOrder(self.orderid, contract, order) + self.client.placeOrder(self.orderid, ib_contract, ib_order) self.client.reqIds(1) vt_orderid = f"{self.gateway_name}.{self.orderid}" @@ -555,7 +595,32 @@ class IbApi(EWrapper): """ Cancel an existing order. """ - self.client.cancelOrder(int(req.orderid))) + if not self.status: + return + + self.client.cancelOrder(int(req.orderid)) +class IbClient(EClient): + """""" + def run(self): + """ + Reimplement the original run message loop of eclient. + + Remove all unnecessary try...catch... and allow exceptions to interrupt loop. + """ + while not self.done and self.isConnected(): + try: + text = self.msg_queue.get(block=True, timeout=0.2) + + if len(text) > MAX_MSG_LEN: + errorMsg = "%s:%d:%s" % (BAD_LENGTH.msg(), len(text), text) + self.wrapper.error(NO_VALID_ID, BAD_LENGTH.code(), errorMsg) + self.disconnect() + break + + fields = comm.read_fields(text) + self.decoder.interpret(fields) + except Empty: + pass diff --git a/vnpy/trader/app.py b/vnpy/trader/app.py new file mode 100644 index 00000000..e01bb50c --- /dev/null +++ b/vnpy/trader/app.py @@ -0,0 +1,11 @@ +from abc import ABC + + +class BaseApp(ABC): + """ + Abstract class for app. + """ + + app_name = '' + app_engine = None + app_ui = None \ No newline at end of file diff --git a/vnpy/trader/engine.py b/vnpy/trader/engine.py index 135a3fd6..6d05c96c 100644 --- a/vnpy/trader/engine.py +++ b/vnpy/trader/engine.py @@ -18,7 +18,7 @@ from .event import ( EVENT_CONTRACT ) from .object import LogData, SubscribeRequest, OrderRequest, CancelRequest -from .utility import Singleton, get_temp_path, check_order_active +from .utility import Singleton, get_temp_path from .setting import SETTINGS from .gateway import BaseGateway @@ -54,7 +54,7 @@ class MainEngine: Add gateway. """ gateway = gateway_class(self.event_engine) - self.gateways[gateway.gateway_name] = engine + self.gateways[gateway.gateway_name] = gateway def init_engines(self): """ @@ -78,6 +78,7 @@ class MainEngine: gateway = self.gateways.get(gateway_name, None) if not gateway: self.write_log(f"找不到底层接口:{gateway_name}") + return None return gateway def get_default_setting(self, gateway_name: str): @@ -87,14 +88,21 @@ class MainEngine: gateway = self.get_gateway(gateway_name) if gateway: return gateway.get_default_setting() + return None - def connect(self, gateway_name: str): + def get_all_gateway_names(self): + """ + Get all names of gatewasy added in main engine. + """ + return list(self.gateways.keys()) + + def connect(self, setting: dict, gateway_name: str): """ Start connection of a specific gateway. """ gateway = self.get_gateway(gateway_name) if gateway: - gateway.connect() + gateway.connect(setting) def subscribe(self, req: SubscribeRequest, gateway_name: str): """ @@ -120,7 +128,7 @@ class MainEngine: """ gateway = self.get_gateway(gateway_name) if gateway: - gateway.send_order(req) + gateway.cancel_order(req) def close(self): """ @@ -277,7 +285,7 @@ class OmsEngine(BaseEngine): self.orders[order.vt_orderid] = order # If order is active, then update data in dict. - if check_order_active(order.status): + if order.check_active(): self.active_orders[order.vt_orderid] = order # Otherwise, pop inactive order from in dict elif order.vt_orderid in self.active_orders: diff --git a/vnpy/trader/object.py b/vnpy/trader/object.py index dde8258e..f0e63936 100644 --- a/vnpy/trader/object.py +++ b/vnpy/trader/object.py @@ -6,6 +6,10 @@ from dataclasses import dataclass from datetime import datetime from logging import INFO +from .constant import (STATUS_SUBMITTING, STATUS_NOTTRADED, STATUS_PARTTRADED) + +ACTIVE_STATUSES = set([STATUS_SUBMITTING, STATUS_NOTTRADED, STATUS_PARTTRADED]) + @dataclass class BaseData: @@ -110,6 +114,26 @@ class OrderData(BaseData): self.vt_symbol = f"{self.symbol}.{self.exchange}" self.vt_orderid = f"{self.gateway_name}.{self.orderid}" + def check_active(self): + """ + Check if the order is active. + """ + if self.status in ACTIVE_STATUSES: + return True + else: + return False + + def create_cancel_request(self): + """ + Create cancel request object from order. + """ + req = CancelRequest( + orderid=self.orderid, + symbol=self.symbol, + exchange=self.exchange + ) + return req + @dataclass class TradeData(BaseData): @@ -182,7 +206,7 @@ class LogData(BaseData): def __post_init__(self): """""" - time = datetime + self.time = datetime.now() @dataclass diff --git a/vnpy/trader/ui/__init__.py b/vnpy/trader/ui/__init__.py index 6872f76e..4630b75c 100644 --- a/vnpy/trader/ui/__init__.py +++ b/vnpy/trader/ui/__init__.py @@ -7,6 +7,7 @@ from PyQt5 import QtWidgets, QtGui from .mainwindow import MainWindow from ..setting import SETTINGS +from ..utility import get_icon_path def create_qapp(): @@ -19,9 +20,7 @@ def create_qapp(): font = QtGui.QFont(SETTINGS["font.family"], SETTINGS["font.size"]) qapp.setFont(font) - ui_path = Path(__file__).parent - icon_path = ui_path.joinpath("ico", "vnpy.ico") - icon = QtGui.QIcon(str(icon_path)) + icon = QtGui.QIcon(get_icon_path(__file__, "vnpy.ico")) qapp.setWindowIcon(icon) if 'Windows' in platform.uname(): diff --git a/vnpy/trader/ui/mainwindow.py b/vnpy/trader/ui/mainwindow.py index 28558798..19b30ec2 100644 --- a/vnpy/trader/ui/mainwindow.py +++ b/vnpy/trader/ui/mainwindow.py @@ -1,14 +1,23 @@ -from PyQt5 import QtWidgets, QtCore +""" +Implements main window of VN Trader. +""" + +from functools import partial + +from PyQt5 import QtWidgets, QtCore, QtGui from vnpy.event import EventEngine from ..engine import MainEngine +from ..utility import get_icon_path from .widget import ( TickMonitor, OrderMonitor, TradeMonitor, PositionMonitor, AccountMonitor, - LogMonitor + LogMonitor, + ConnectDialog, + TradingWidget ) @@ -23,6 +32,7 @@ class MainWindow(QtWidgets.QMainWindow): self.main_engine = main_engine self.event_engine = event_engine + self.connect_dialogs = {} self.widgets = {} self.init_ui() @@ -35,6 +45,7 @@ class MainWindow(QtWidgets.QMainWindow): def init_dock(self): """""" + trading_widget, trading_dock = self.create_dock(TradingWidget, "交易", QtCore.Qt.LeftDockWidgetArea) tick_widget, tick_dock = self.create_dock(TickMonitor, "行情", QtCore.Qt.RightDockWidgetArea) order_widget, order_dock = self.create_dock(OrderMonitor, "委托", QtCore.Qt.RightDockWidgetArea) trade_widget, trade_dock = self.create_dock(TradeMonitor, "成交", QtCore.Qt.RightDockWidgetArea) @@ -44,7 +55,22 @@ class MainWindow(QtWidgets.QMainWindow): def init_menu(self): """""" - pass + bar = self.menuBar() + + sys_menu = bar.addMenu("系统") + app_menu = bar.addMenu("功能") + help_menu = bar.addMenu("帮助") + + gateway_names = self.main_engine.get_all_gateway_names() + for name in gateway_names: + func = partial(self.connect, name) + icon = QtGui.QIcon(get_icon_path(__file__, "connect.ico")) + + action = QtWidgets.QAction(f"连接{name}", self) + action.triggered.connect(func) + action.setIcon(icon) + + sys_menu.addAction(action) def create_dock( self, @@ -62,4 +88,36 @@ class MainWindow(QtWidgets.QMainWindow): dock.setObjectName(name) dock.setFeatures(dock.DockWidgetFloatable | dock.DockWidgetMovable) self.addDockWidget(area, dock) - return widget, dock \ No newline at end of file + return widget, dock + + def connect(self, gateway_name: str): + """ + Open connect dialog for gateway connection. + """ + dialog = self.connect_dialogs.get(gateway_name, None) + if not dialog: + dialog = ConnectDialog(self.main_engine, gateway_name) + + dialog.exec() + + def closeEvent(self, event): + """ + Call main engine close function before exit. + """ + reply = QtWidgets.QMessageBox.question( + self, + "退出", + "确认退出?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No + ) + + if reply == QtWidgets.QMessageBox.Yes: + for widget in self.widgets.values(): + widget.close() + + self.main_engine.close() + + event.accept() + else: + event.ignore() diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py index ae802ba2..7e417295 100644 --- a/vnpy/trader/ui/widget.py +++ b/vnpy/trader/ui/widget.py @@ -8,10 +8,16 @@ from typing import Any from PyQt5 import QtWidgets, QtGui, QtCore from vnpy.event import EventEngine, Event -from ..constant import DIRECTION_LONG, DIRECTION_SHORT, DIRECTION_NET +from ..constant import (DIRECTION_LONG, DIRECTION_SHORT, DIRECTION_NET, + OFFSET_OPEN, OFFSET_CLOSE, OFFSET_CLOSETODAY, OFFSET_CLOSEYESTERDAY, + PRICETYPE_LIMIT, PRICETYPE_MARKET, PRICETYPE_FAK, PRICETYPE_FOK, + EXCHANGE_CFFEX, EXCHANGE_SHFE, EXCHANGE_DCE, EXCHANGE_CZCE, EXCHANGE_SSE, + EXCHANGE_SZSE, EXCHANGE_SGE, EXCHANGE_HKEX, EXCHANGE_HKFE, EXCHANGE_SMART, + EXCHANGE_ICE, EXCHANGE_CME, EXCHANGE_NYMEX, EXCHANGE_GLOBEX, EXCHANGE_IDEALPRO) from ..engine import MainEngine from ..event import (EVENT_TICK, EVENT_ORDER, EVENT_TRADE, EVENT_ACCOUNT, EVENT_POSITION, EVENT_CONTRACT, EVENT_LOG) +from ..object import SubscribeRequest, OrderRequest, CancelRequest COLOR_LONG = QtGui.QColor("red") COLOR_SHORT = QtGui.QColor("green") @@ -27,7 +33,7 @@ class BaseCell(QtWidgets.QTableWidgetItem): def __init__(self, content: Any, data: Any): """""" - super(BaseCel, self).__init__() + super(BaseCell, self).__init__() self.setTextAlignment(QtCore.Qt.AlignCenter) self.set_content(content, data) @@ -108,7 +114,7 @@ class PnlCell(BaseCell): """ super(PnlCell, self).set_content(content, data) - if content.startswith("-"): + if str(content).startswith("-"): self.setForeground(COLOR_SHORT) else: self.setForeground(COLOR_LONG) @@ -171,7 +177,7 @@ class BaseMonitor(QtWidgets.QTableWidget): """ self.setColumnCount(len(self.headers)) - labels = [d['display'] for d in self.headers.values()] + labels = [d["display"] for d in self.headers.values()] self.setHorizontalHeaderLabels(labels) self.verticalHeader().setVisible(False) @@ -198,7 +204,7 @@ class BaseMonitor(QtWidgets.QTableWidget): Register event handler into event engine. """ self.signal.connect(self.process_event) - self.event_engine.register(self.event_type, self.process_event) + self.event_engine.register(self.event_type, self.signal.emit) def process_event(self, event): """ @@ -236,10 +242,10 @@ class BaseMonitor(QtWidgets.QTableWidget): setting = self.headers[header] content = data.__getattribute__(header) - cell = setting['cell'](content, data) + cell = setting["cell"](content, data) self.setItem(0, column, cell) - if setting['update']: + if setting["update"]: row_cells[header] = cell if self.data_key: @@ -253,7 +259,7 @@ class BaseMonitor(QtWidgets.QTableWidget): key = data.__getattribute__(self.data_key) row_cells = self.cells[key] - for header, cell in row_cells: + for header, cell in row_cells.items(): content = data.__getattribute__(header) cell.set_content(content, data) @@ -298,65 +304,65 @@ class TickMonitor(BaseMonitor): sorting = True headers = { - 'symbol': { - 'display': '代码', - 'cell': BaseCell, - 'update': False + "symbol": { + "display": "代码", + "cell": BaseCell, + "update": False }, - 'last_price': { - 'display': '最新价', - 'cell': BaseCell, - 'update': True + "last_price": { + "display": "最新价", + "cell": BaseCell, + "update": True }, - 'volume': { - 'display': '成交量', - 'cell': BaseCell, - 'update': True + "volume": { + "display": "成交量", + "cell": BaseCell, + "update": True }, - 'open_price': { - 'display': '开盘价', - 'cell': BaseCell, - 'update': True + "open_price": { + "display": "开盘价", + "cell": BaseCell, + "update": True }, - 'high_price': { - 'display': '最高价', - 'cell': BaseCell, - 'update': True + "high_price": { + "display": "最高价", + "cell": BaseCell, + "update": True }, - 'low_price': { - 'display': '最低价', - 'cell': BaseCell, - 'update': True + "low_price": { + "display": "最低价", + "cell": BaseCell, + "update": True }, - 'bid_price_1': { - 'display': '买1价', - 'cell': BidCell, - 'update': True + "bid_price_1": { + "display": "买1价", + "cell": BidCell, + "update": True }, - 'bid_volume_1': { - 'display': '买1量', - 'cell': BidCell, - 'update': True + "bid_volume_1": { + "display": "买1量", + "cell": BidCell, + "update": True }, - 'ask_price_1': { - 'display': '卖1价', - 'cell': AskCell, - 'update': True + "ask_price_1": { + "display": "卖1价", + "cell": AskCell, + "update": True }, - 'ask_volume_1': { - 'display': '卖1量', - 'cell': AskCell, - 'update': True + "ask_volume_1": { + "display": "卖1量", + "cell": AskCell, + "update": True }, - 'time': { - 'display': '时间', - 'cell': TimeCell, - 'update': True + "datetime": { + "display": "时间", + "cell": TimeCell, + "update": True }, - 'gateway_name': { - 'display': '接口', - 'cell': BaseCell, - 'update': False + "gateway_name": { + "display": "接口", + "cell": BaseCell, + "update": False } } @@ -370,28 +376,23 @@ class LogMonitor(BaseMonitor): sorting = False headers = { - 'time': { - 'display': '时间', - 'cell': BaseCell, - 'update': False + "time": { + "display": "时间", + "cell": TimeCell, + "update": False }, - 'msg': { - 'display': '信息', - 'cell': BaseCell, - 'update': False + "msg": { + "display": "信息", + "cell": BaseCell, + "update": False }, - 'gateway_name': { - 'display': '接口', - 'cell': BaseCell, - 'update': False + "gateway_name": { + "display": "接口", + "cell": BaseCell, + "update": False } } - def process_event(self, event): - """Resize row heights for diplaying long log message.""" - super(LogMonitor, self).process_event(event) - self.resizeRowToContents(0) - class TradeMonitor(BaseMonitor): """ @@ -402,45 +403,45 @@ class TradeMonitor(BaseMonitor): sorting = True headers = { - 'tradeid': { - 'display': "成交号 ", - 'cell': BaseCell, - 'update': False + "tradeid": { + "display": "成交号 ", + "cell": BaseCell, + "update": False }, - 'orderid': { - 'display': '委托号', - 'cell': BaseCell, - 'update': False + "orderid": { + "display": "委托号", + "cell": BaseCell, + "update": False }, - 'symbol': { - 'display': '代码', - 'cell': BaseCell, - 'update': False + "symbol": { + "display": "代码", + "cell": BaseCell, + "update": False }, - 'direction': { - 'display': '方向', - 'cell': DirectionCell, - 'update': False + "direction": { + "display": "方向", + "cell": DirectionCell, + "update": False }, - 'offset': { - 'display': '开平', - 'cell': BaseCell, - 'update': False + "offset": { + "display": "开平", + "cell": BaseCell, + "update": False }, - 'price': { - 'display': '价格', - 'cell': BaseCell, - 'update': False + "price": { + "display": "价格", + "cell": BaseCell, + "update": False }, - 'volume': { - 'display': '数量', - 'cell': BaseCell, - 'update': False + "volume": { + "display": "数量", + "cell": BaseCell, + "update": False }, - 'gateway_name': { - 'display': '接口', - 'cell': BaseCell, - 'update': False + "gateway_name": { + "display": "接口", + "cell": BaseCell, + "update": False } } @@ -454,55 +455,55 @@ class OrderMonitor(BaseMonitor): sorting = True headers = { - 'orderid': { - 'display': '委托号', - 'cell': BaseCell, - 'update': False + "orderid": { + "display": "委托号", + "cell": BaseCell, + "update": False }, - 'symbol': { - 'display': '代码', - 'cell': BaseCell, - 'update': False + "symbol": { + "display": "代码", + "cell": BaseCell, + "update": False }, - 'direction': { - 'display': '方向', - 'cell': DirectionCell, - 'update': False + "direction": { + "display": "方向", + "cell": DirectionCell, + "update": False }, - 'offset': { - 'display': '开平', - 'cell': BaseCell, - 'update': False + "offset": { + "display": "开平", + "cell": BaseCell, + "update": False }, - 'price': { - 'display': '价格', - 'cell': BaseCell, - 'update': False + "price": { + "display": "价格", + "cell": BaseCell, + "update": False }, - 'volume': { - 'display': '总数量', - 'cell': BaseCell, - 'update': True + "volume": { + "display": "总数量", + "cell": BaseCell, + "update": True }, - 'traded': { - 'display': '已成交', - 'cell': BaseCell, - 'update': True + "traded": { + "display": "已成交", + "cell": BaseCell, + "update": True }, - 'status': { - 'display': '状态', - 'cell': BaseCell, - 'update': True + "status": { + "display": "状态", + "cell": BaseCell, + "update": True }, - 'time': { - 'display': '时间', - 'cell': BaseCell, - 'update': True + "time": { + "display": "时间", + "cell": BaseCell, + "update": True }, - 'gateway_name': { - 'display': '接口', - 'cell': BaseCell, - 'update': False + "gateway_name": { + "display": "接口", + "cell": BaseCell, + "update": False } } @@ -515,40 +516,40 @@ class PositionMonitor(BaseMonitor): sorting = True headers = { - 'symbol': { - 'display': '代码', - 'cell': BaseCell, - 'update': False + "symbol": { + "display": "代码", + "cell": BaseCell, + "update": False }, - 'direction': { - 'display': '方向', - 'cell': DirectionCell, - 'update': False + "direction": { + "display": "方向", + "cell": DirectionCell, + "update": False }, - 'volume': { - 'display': '数量', - 'cell': BaseCell, - 'update': True + "volume": { + "display": "数量", + "cell": BaseCell, + "update": True }, - 'frozen': { - 'display': '冻结', - 'cell': BaseCell, - 'update': True + "frozen": { + "display": "冻结", + "cell": BaseCell, + "update": True }, - 'price': { - 'display': '均价', - 'cell': BaseCell, - 'update': False + "price": { + "display": "均价", + "cell": BaseCell, + "update": False }, - 'pnl': { - 'display': '盈亏', - 'cell': PnlCell, - 'update': True + "pnl": { + "display": "盈亏", + "cell": PnlCell, + "update": True }, - 'gateway_name': { - 'display': '接口', - 'cell': BaseCell, - 'update': False + "gateway_name": { + "display": "接口", + "cell": BaseCell, + "update": False } } @@ -562,29 +563,399 @@ class AccountMonitor(BaseMonitor): sorting = True headers = { - 'accountid': { - 'display': '账号', - 'cell': BaseCell, - 'update': False + "accountid": { + "display": "账号", + "cell": BaseCell, + "update": False }, - 'balance': { - 'display': '余额', - 'cell': BaseCell, - 'update': True + "balance": { + "display": "余额", + "cell": BaseCell, + "update": True }, - 'frozen': { - 'display': '冻结', - 'cell': BaseCell, - 'update': True + "frozen": { + "display": "冻结", + "cell": BaseCell, + "update": True }, - 'available': { - 'display': '可用', - 'cell': BaseCell, - 'update': True + "available": { + "display": "可用", + "cell": BaseCell, + "update": True }, - 'gateway_name': { - 'display': '接口', - 'cell': BaseCell, - 'update': False + "gateway_name": { + "display": "接口", + "cell": BaseCell, + "update": False } } + + +class ConnectDialog(QtWidgets.QDialog): + """ + Start connection of a certain gateway. + """ + + def __init__(self, main_engine: MainEngine, gateway_name: str): + """""" + super(ConnectDialog, self).__init__() + + self.main_engine = main_engine + self.gateway_name = gateway_name + + self.line_edits = {} + + self.init_ui() + + def init_ui(self): + """""" + self.setWindowTitle(f"连接{self.gateway_name}") + + form = QtWidgets.QFormLayout() + default_setting = self.main_engine.get_default_setting(self.gateway_name) + + # Initialize line edits and form layout based on default setting. + for field_name, field_value in default_setting.items(): + field_type = type(field_value) + line_edit = QtWidgets.QLineEdit(str(field_value)) + + form.addRow(f"{field_name} <{field_type.__name__}>", line_edit) + self.line_edits[field_name] = (line_edit, field_type) + + button = QtWidgets.QPushButton(u"连接") + button.clicked.connect(self.connect) + form.addRow(button) + + self.setLayout(form) + + def connect(self): + """ + Get setting value from line edits and connect the gateway. + """ + setting = {} + for field_name, tp in self.line_edits.items(): + line_edit, field_type = tp + field_value = field_type(line_edit.text()) + setting[field_name] = field_value + + self.main_engine.connect(setting, self.gateway_name) + self.accept() + + +class TradingWidget(QtWidgets.QWidget): + """ + General manual trading widget. + """ + + signal_tick = QtCore.pyqtSignal(Event) + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super(TradingWidget, self).__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + + self.vt_symbol = "" + + self.init_ui() + self.register_event() + + def init_ui(self): + """""" + self.setFixedWidth(300) + + # Trading function area + self.exchange_combo = QtWidgets.QComboBox() + self.exchange_combo.addItems([ + EXCHANGE_CFFEX, + EXCHANGE_SHFE, + EXCHANGE_DCE, + EXCHANGE_CZCE, + EXCHANGE_SSE, + EXCHANGE_SZSE, + EXCHANGE_HKEX, + EXCHANGE_HKFE, + EXCHANGE_SMART, + EXCHANGE_ICE, + EXCHANGE_CME, + EXCHANGE_NYMEX, + EXCHANGE_GLOBEX, + EXCHANGE_IDEALPRO + ]) + + self.symbol_line = QtWidgets.QLineEdit() + self.symbol_line.returnPressed.connect(self.set_vt_symbol) + + self.name_line = QtWidgets.QLineEdit() + self.name_line.setReadOnly(True) + + self.direction_combo = QtWidgets.QComboBox() + self.direction_combo.addItems([ + DIRECTION_LONG, + DIRECTION_SHORT + ]) + + self.offset_combo = QtWidgets.QComboBox() + self.offset_combo.addItems([ + OFFSET_OPEN, + OFFSET_CLOSE, + OFFSET_CLOSETODAY, + OFFSET_CLOSEYESTERDAY + ]) + + self.pricetype_combo = QtWidgets.QComboBox() + self.pricetype_combo.addItems([ + PRICETYPE_LIMIT, + PRICETYPE_MARKET, + PRICETYPE_FAK, + PRICETYPE_FOK + ]) + + double_validator = QtGui.QDoubleValidator() + double_validator.setBottom(0) + + self.price_line = QtWidgets.QLineEdit() + self.price_line.setValidator(double_validator) + + self.volume_line = QtWidgets.QLineEdit() + self.volume_line.setValidator(double_validator) + + self.gateway_combo = QtWidgets.QComboBox() + self.gateway_combo.addItems(self.main_engine.get_all_gateway_names()) + + send_button = QtWidgets.QPushButton("委托") + send_button.clicked.connect(self.send_order) + + cancel_button = QtWidgets.QPushButton("全撤") + cancel_button.clicked.connect(self.cancel_all) + + form1 = QtWidgets.QFormLayout() + form1.addRow("交易所", self.exchange_combo) + form1.addRow("代码", self.symbol_line) + form1.addRow("名称", self.name_line) + form1.addRow("方向", self.direction_combo) + form1.addRow("开平", self.offset_combo) + form1.addRow("类型", self.pricetype_combo) + form1.addRow("价格", self.price_line) + form1.addRow("数量", self.volume_line) + form1.addRow("接口", self.gateway_combo) + form1.addRow(send_button) + form1.addRow(cancel_button) + + # Market depth display area + bid_color = "rgb(255,174,201)" + ask_color = "rgb(160,255,160)" + + self.bp1_label = self.create_label(bid_color) + self.bp2_label = self.create_label(bid_color) + self.bp3_label = self.create_label(bid_color) + self.bp4_label = self.create_label(bid_color) + self.bp5_label = self.create_label(bid_color) + + self.bv1_label = self.create_label(bid_color, alignment=QtCore.Qt.AlignRight) + self.bv2_label = self.create_label(bid_color, alignment=QtCore.Qt.AlignRight) + self.bv3_label = self.create_label(bid_color, alignment=QtCore.Qt.AlignRight) + self.bv4_label = self.create_label(bid_color, alignment=QtCore.Qt.AlignRight) + self.bv5_label = self.create_label(bid_color, alignment=QtCore.Qt.AlignRight) + + self.ap1_label = self.create_label(ask_color) + self.ap2_label = self.create_label(ask_color) + self.ap3_label = self.create_label(ask_color) + self.ap4_label = self.create_label(ask_color) + self.ap5_label = self.create_label(ask_color) + + self.av1_label = self.create_label(ask_color, alignment=QtCore.Qt.AlignRight) + self.av2_label = self.create_label(ask_color, alignment=QtCore.Qt.AlignRight) + self.av3_label = self.create_label(ask_color, alignment=QtCore.Qt.AlignRight) + self.av4_label = self.create_label(ask_color, alignment=QtCore.Qt.AlignRight) + self.av5_label = self.create_label(ask_color, alignment=QtCore.Qt.AlignRight) + + self.lp_label = self.create_label() + self.return_label = self.create_label(alignment=QtCore.Qt.AlignRight) + + form2 = QtWidgets.QFormLayout() + form2.addRow(self.ap5_label, self.av5_label) + form2.addRow(self.ap4_label, self.av4_label) + form2.addRow(self.ap3_label, self.av3_label) + form2.addRow(self.ap2_label, self.av2_label) + form2.addRow(self.ap1_label, self.av1_label) + form2.addRow(self.lp_label, self.return_label) + form2.addRow(self.bp1_label, self.bv1_label) + form2.addRow(self.bp2_label, self.bv2_label) + form2.addRow(self.bp3_label, self.bv3_label) + form2.addRow(self.bp4_label, self.bv4_label) + form2.addRow(self.bp5_label, self.bv5_label) + + # Overall layout + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(form1) + vbox.addLayout(form2) + self.setLayout(vbox) + + def create_label(self, color: str = "", alignment: int = QtCore.Qt.AlignLeft): + """ + Create label with certain font color. + """ + label = QtWidgets.QLabel() + if color: + label.setStyleSheet(f"color:{color}") + label.setAlignment(alignment) + return label + + def register_event(self): + """""" + self.signal_tick.connect(self.process_tick_event) + self.event_engine.register(EVENT_TICK, self.signal_tick.emit) + + def process_tick_event(self, event: Event): + """""" + tick = event.data + if tick.vt_symbol != self.vt_symbol: + return + + self.lp_label.setText(str(tick.last_price)) + self.bp1_label.setText(str(tick.bid_price_1)) + self.bv1_label.setText(str(tick.bid_volume_1)) + self.ap1_label.setText(str(tick.ask_price_1)) + self.av1_label.setText(str(tick.ask_volume_1)) + + if tick.pre_close: + r = (tick.last_price / tick.pre_close - 1) * 100 + self.return_label.setText(f"{r:.2f}%") + + if tick.bid_price_2: + self.bp2_label.setText(str(tick.bid_price_2)) + self.bv2_label.setText(str(tick.bid_volume_2)) + self.ap2_label.setText(str(tick.ask_price_2)) + self.av2_label.setText(str(tick.ask_volume_2)) + + self.bp3_label.setText(str(tick.bid_price_3)) + self.bv3_label.setText(str(tick.bid_volume_3)) + self.ap3_label.setText(str(tick.ask_price_3)) + self.av3_label.setText(str(tick.ask_volume_3)) + + self.bp4_label.setText(str(tick.bid_price_4)) + self.bv4_label.setText(str(tick.bid_volume_4)) + self.ap4_label.setText(str(tick.ask_price_4)) + self.av4_label.setText(str(tick.ask_volume_4)) + + self.bp5_label.setText(str(tick.bid_price_5)) + self.bv5_label.setText(str(tick.bid_volume_5)) + self.ap5_label.setText(str(tick.ask_price_5)) + self.av5_label.setText(str(tick.ask_volume_5)) + + def set_vt_symbol(self): + """ + Set the tick depth data to monitor by vt_symbol. + """ + symbol = str(self.symbol_line.text()) + if not symbol: + return + + # Generate vt_symbol from symbol and exchange + exchange = str(self.exchange_combo.currentText()) + vt_symbol = f"{symbol}.{exchange}" + + if vt_symbol == self.vt_symbol: + return + self.vt_symbol = vt_symbol + + # Update name line widget and clear all labels + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + self.name_line.setText("") + else: + self.name_line.setText(contract.name) + + self.clear_label_text() + + # Subscribe tick data + req = SubscribeRequest( + symbol=symbol, + exchange=exchange + ) + gateway_name = (self.gateway_combo.currentText()) + + self.main_engine.subscribe(req, gateway_name) + + def clear_label_text(self): + """ + Clear text on all labels. + """ + self.lp_label.setText("") + self.return_label.setText("") + + self.bv1_label.setText("") + self.bv2_label.setText("") + self.bv3_label.setText("") + self.bv4_label.setText("") + self.bv5_label.setText("") + + self.av1_label.setText("") + self.av2_label.setText("") + self.av3_label.setText("") + self.av4_label.setText("") + self.av5_label.setText("") + + self.bp1_label.setText("") + self.bp2_label.setText("") + self.bp3_label.setText("") + self.bp4_label.setText("") + self.bp5_label.setText("") + + self.ap1_label.setText("") + self.ap2_label.setText("") + self.ap3_label.setText("") + self.ap4_label.setText("") + self.ap5_label.setText("") + + def send_order(self): + """ + Send new order manually. + """ + symbol = str(self.symbol_line.text()) + if not symbol: + QtWidgets.QMessageBox.critical( + "委托失败", + "请输入合约代码" + ) + return + + volume_text = str(self.volume_line.text()) + if not volume_text: + QtWidgets.QMessageBox.critical( + "委托失败", + "请输入委托数量" + ) + return + volume = float(volume_text) + + price_text = str(self.price_line.text()) + if not price_text: + price = 0 + else: + price = float(price_text) + + req = OrderRequest( + symbol=symbol, + exchange=str(self.exchange_combo.currentText()), + direction=str(self.direction_combo.currentText()), + price_type=str(self.pricetype_combo.currentText()), + volume=volume, + price=price, + offset=str(self.offset_combo.currentText()) + ) + + gateway_name = str(self.gateway_combo.currentText()) + + self.main_engine.send_order(req, gateway_name) + + def cancel_all(self): + """ + Cancel all active orders. + """ + order_list = self.main_engine.get_all_active_orders() + for order in order_list: + req = order.create_cancel_request() + self.main_engine.cancel_order(req, order.gateway_name) \ No newline at end of file diff --git a/vnpy/trader/utility.py b/vnpy/trader/utility.py index 0cb9a7cf..6a186561 100644 --- a/vnpy/trader/utility.py +++ b/vnpy/trader/utility.py @@ -40,14 +40,10 @@ def get_temp_path(filename: str): return temp_path.joinpath(filename) -ACTIVE_STATUSES = set([STATUS_SUBMITTING, STATUS_NOTTRADED, STATUS_PARTTRADED]) - - -def check_order_active(status: str): +def get_icon_path(file_path: str, ico_name: str): """ - Check if order is active by status. + Get path for icon file with ico name. """ - if status in ACTIVE_STATUSES: - return True - else: - return False + ui_path = Path(file_path).parent + icon_path = ui_path.joinpath("ico", ico_name) + return str(icon_path)