[Add] Complete ib gateway development

This commit is contained in:
vn.py 2019-01-10 23:45:21 +08:00
parent 84eaeb8733
commit 64c1c3ccde
12 changed files with 807 additions and 270 deletions

View File

@ -1,2 +1,2 @@
pyqt
PyQt5
qdarkstyle

View File

@ -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()

0
vnpy/gateway/__init__.py Normal file
View File

View File

@ -1 +1 @@
from ib_gateway import IbGateway
from .ib_gateway import IbGateway

View File

@ -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,7 +468,7 @@ 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
@ -461,14 +484,16 @@ class IbApi(EWrapper):
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)
@ -480,33 +505,44 @@ class IbApi(EWrapper):
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.
"""
self.status = False
self.client.eDisconnect()
if not self.status:
return
def subscribe(self, req:SubscribeRequest):
self.status = False
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]
@ -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

11
vnpy/trader/app.py Normal file
View File

@ -0,0 +1,11 @@
from abc import ABC
class BaseApp(ABC):
"""
Abstract class for app.
"""
app_name = ''
app_engine = None
app_ui = None

View File

@ -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:

View File

@ -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

View File

@ -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():

View File

@ -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,
@ -63,3 +89,35 @@ class MainWindow(QtWidgets.QMainWindow):
dock.setFeatures(dock.DockWidgetFloatable | dock.DockWidgetMovable)
self.addDockWidget(area, dock)
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()

View File

@ -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)

View File

@ -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)