[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 qdarkstyle

View File

@ -1,8 +1,11 @@
from vnpy.event import EventEngine from vnpy.event import EventEngine
from vnpy.trader.engine import MainEngine from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import MainWindow, create_qapp 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(): def main():
@ -10,7 +13,9 @@ def main():
qapp = create_qapp() qapp = create_qapp()
event_engine = EventEngine() event_engine = EventEngine()
main_engine = MainEngine(event_engine) main_engine = MainEngine(event_engine)
main_engine.add_gateway(IbGateway)
main_window = MainWindow(main_engine, event_engine) main_window = MainWindow(main_engine, event_engine)
main_window.showMaximized() 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 datetime import datetime
from copy import copy from copy import copy
from threading import Thread
from queue import Empty
from ibapi.wrapper import EWrapper from ibapi.wrapper import EWrapper
from ibapi.client import EClient from ibapi.client import EClient
from ibapi.contract import Contract, ContractDetails from ibapi.contract import Contract, ContractDetails
from ibapi.order import Order 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.ticktype import TickType
from ibapi.order_state import OrderState from ibapi.order_state import OrderState
from ibapi.execution import Execution from ibapi.execution import Execution
from ibapi.utils import BadMessage
from ibapi import comm
from vnpy.trader.gateway import BaseGateway from vnpy.trader.gateway import BaseGateway
from vnpy.trader.object import ( 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()} PRICETYPE_IB2VT = {v: k for k, v in PRICETYPE_VT2IB.items()}
DIRECTION_VT2IB = {DIRECTION_LONG: "BUY", DIRECTION_SHORT: "SELL"} 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_VT2IB = {
EXCHANGE_SMART: "SMART", EXCHANGE_SMART: "SMART",
@ -123,15 +129,11 @@ ACCOUNTFIELD_IB2VT = {
class IbGateway(BaseGateway): class IbGateway(BaseGateway):
"""""" """"""
default_setting = { default_setting = {"host": "127.0.0.1", "port": 7497, "clientid": 1}
"host": "127.0.0.1",
"port": 7497,
"clientid": 1
}
def __init__(self, event_engine): def __init__(self, event_engine):
"""""" """"""
super(Gateway, self).__init__(event_engine, "IB") super(IbGateway, self).__init__(event_engine, "IB")
self.api = IbApi(self) self.api = IbApi(self)
@ -181,7 +183,7 @@ class IbGateway(BaseGateway):
class IbApi(EWrapper): class IbApi(EWrapper):
"""""" """"""
def __init__(self, gateway: Gateway): def __init__(self, gateway: BaseGateway):
"""""" """"""
super(IbApi, self).__init__() super(IbApi, self).__init__()
@ -189,15 +191,36 @@ class IbApi(EWrapper):
self.gateway_name = gateway.gateway_name self.gateway_name = gateway.gateway_name
self.status = False self.status = False
self.reqid = 0 self.reqid = 0
self.orderid = 0 self.orderid = 0
self.clientid = 0 self.clientid = 0
self.ticks = {} self.ticks = {}
self.orders = {} self.orders = {}
self.accounts = {} 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): def nextValidId(self, orderId: int):
""" """
@ -213,12 +236,10 @@ class IbApi(EWrapper):
""" """
super(IbApi, self).currentTime(time) super(IbApi, self).currentTime(time)
self.status = True
dt = datetime.fromtimestamp(time) dt = datetime.fromtimestamp(time)
time_string = dt.strftime("%Y-%m-%d %H:%M:%S.%f") time_string = dt.strftime("%Y-%m-%d %H:%M:%S.%f")
log = LogData( log = LogData(
msg=f"IB TWS连接成功服务器时间: {time_string}", msg=f"服务器时间: {time_string}",
gateway_name=self.gateway_name gateway_name=self.gateway_name
) )
self.gateway.on_log(log) self.gateway.on_log(log)
@ -229,7 +250,7 @@ class IbApi(EWrapper):
""" """
super(IbApi, self).error(reqId, errorCode, errorString) super(IbApi, self).error(reqId, errorCode, errorString)
log = LogData( log = LogData(
msg=f"发生错误,错误代码:{errorCode},错误信息: {errorString}", msg=f"信息通知,代码:{errorCode},内容: {errorString}",
gateway_name=self.gateway_name gateway_name=self.gateway_name
) )
self.gateway.on_log(log) self.gateway.on_log(log)
@ -253,10 +274,10 @@ class IbApi(EWrapper):
name = TICKFIELD_IB2VT[tickType] name = TICKFIELD_IB2VT[tickType]
setattr(tick, name, price) 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. # We need to calculate locally.
contract = self.contracts[reqId] exchange = self.tick_exchange[reqId]
if contract.product in (PRODUCT_FOREX, PRODUCT_SPOT): if exchange == EXCHANGE_IDEALPRO:
tick.last_price = (tick.bid_price_1 + tick.ask_price_1) / 2 tick.last_price = (tick.bid_price_1 + tick.ask_price_1) / 2
tick.datetime = datetime.now() tick.datetime = datetime.now()
self.gateway.on_tick(copy(tick)) self.gateway.on_tick(copy(tick))
@ -323,7 +344,7 @@ class IbApi(EWrapper):
) )
orderid = str(orderId) orderid = str(orderId)
order = self.orders.get[orderid] order = self.orders.get(orderid, None)
order.status = STATUS_IB2VT[status] order.status = STATUS_IB2VT[status]
order.traded = filled order.traded = filled
@ -349,12 +370,13 @@ class IbApi(EWrapper):
ib_contract.exchange ib_contract.exchange
), ),
orderid=orderid, orderid=orderid,
direction=DIRECTION_IB2V[ib_order.action], direction=DIRECTION_IB2VT[ib_order.action],
price=order.lmtPrice, price=ib_order.lmtPrice,
volume=order.totalQuantity, volume=ib_order.totalQuantity,
gateway_name=self.gateway_name gateway_name=self.gateway_name
) )
self.orders[orderid] = order
self.gateway.on_order(copy(order)) self.gateway.on_order(copy(order))
def updateAccountValue( def updateAccountValue(
@ -379,6 +401,7 @@ class IbApi(EWrapper):
accountid=accountid, accountid=accountid,
gateway_name=self.gateway_name gateway_name=self.gateway_name
) )
self.accounts[accountid] = account
name = ACCOUNTFIELD_IB2VT[key] name = ACCOUNTFIELD_IB2VT[key]
setattr(account, name, float(val)) setattr(account, name, float(val))
@ -426,7 +449,6 @@ class IbApi(EWrapper):
Callback of account update time. Callback of account update time.
""" """
super(IbApi, self).updateAccountTime(timeStamp) super(IbApi, self).updateAccountTime(timeStamp)
for account in self.accounts.values(): for account in self.accounts.values():
self.gateway.on_account(copy(account)) self.gateway.on_account(copy(account))
@ -435,6 +457,7 @@ class IbApi(EWrapper):
Callback of contract data update. Callback of contract data update.
""" """
super(IbApi, self).contractDetails(reqId, contractDetails) super(IbApi, self).contractDetails(reqId, contractDetails)
ib_symbol = contractDetails.contract.conId ib_symbol = contractDetails.contract.conId
ib_exchange = contractDetails.contract.exchange ib_exchange = contractDetails.contract.exchange
ib_size = contractDetails.contract.multiplier ib_size = contractDetails.contract.multiplier
@ -445,7 +468,7 @@ class IbApi(EWrapper):
exchange=EXCHANGE_IB2VT.get(ib_exchange, exchange=EXCHANGE_IB2VT.get(ib_exchange,
ib_exchange), ib_exchange),
name=contractDetails.longName, name=contractDetails.longName,
product=PRODUCT_VT2IB[ib_product] product=PRODUCT_IB2VT[ib_product],
size=ib_size, size=ib_size,
pricetick=contractDetails.minTick, pricetick=contractDetails.minTick,
gateway_name=self.gateway_name gateway_name=self.gateway_name
@ -461,13 +484,15 @@ class IbApi(EWrapper):
today_date = datetime.now().strftime("%Y%m%d") today_date = datetime.now().strftime("%Y%m%d")
trade = TradeData( trade = TradeData(
symbol=contract.conId, symbol=contract.conId,
exchange=EXCHANGE_IB2VT.get(contract.exchange, contract.exchange) exchange=EXCHANGE_IB2VT.get(contract.exchange,
contract.exchange),
orderid=str(execution.orderId), orderid=str(execution.orderId),
tradeid=str(execution.execId), tradeid=str(execution.execId),
direction=DIRECTION_IB2VT[execution.side], direction=DIRECTION_IB2VT[execution.side],
price=execution.price, price=execution.price,
volume=execution.shares, volume=execution.shares,
time=datetime.strptime(f"{today_date} {execution.time}", "%Y%m%d %H:%M:%S"), time=datetime.strptime(execution.time,
"%Y%m%d %H:%M:%S"),
gateway_name=self.gateway_name gateway_name=self.gateway_name
) )
@ -480,33 +505,44 @@ class IbApi(EWrapper):
super(IbApi, self).managedAccounts(accountsList) super(IbApi, self).managedAccounts(accountsList)
for account_code in accountsList.split(","): for account_code in accountsList.split(","):
self.client.reqAccountUpdates(account_code) self.client.reqAccountUpdates(True, account_code)
def connect(self, setting: dict): def connect(self, setting: dict):
""" """
Connect to TWS. Connect to TWS.
""" """
if self.status:
return
self.clientid = setting["clientid"] self.clientid = setting["clientid"]
self.client.connect( self.client.connect(
setting["host"]. setting["host"],
setting["port"], setting["port"],
setting["clientid"] setting["clientid"]
) )
self.client.reqCurrentTime() self.thread.start()
n = self.client.reqCurrentTime()
def close(self): def close(self):
""" """
Disconnect to TWS. Disconnect to TWS.
""" """
if not self.status:
return
self.status = False self.status = False
self.client.eDisconnect() self.client.disconnect()
def subscribe(self, req: SubscribeRequest): def subscribe(self, req: SubscribeRequest):
""" """
Subscribe tick data update. Subscribe tick data update.
""" """
if not self.status:
return
ib_contract = Contract() ib_contract = Contract()
ib_contract.conId = str(req.symbol) ib_contract.conId = str(req.symbol)
ib_contract.exchange = EXCHANGE_VT2IB[req.exchange] ib_contract.exchange = EXCHANGE_VT2IB[req.exchange]
@ -526,26 +562,30 @@ class IbApi(EWrapper):
gateway_name=self.gateway_name gateway_name=self.gateway_name
) )
self.ticks[self.reqid] = tick self.ticks[self.reqid] = tick
self.tick_exchange[self.reqid] = req.exchange
def send_order(self, req: OrderRequest): def send_order(self, req: OrderRequest):
""" """
Send a new order. Send a new order.
""" """
if not self.status:
return ''
self.orderid += 1 self.orderid += 1
ib_contract = Contract() ib_contract = Contract()
ib_contract.conId = str(req.symbol) ib_contract.conId = str(req.symbol)
ib_contract.exchange = EXCHANGE_VT2IB[req.exchange] ib_contract.exchange = EXCHANGE_VT2IB[req.exchange]
order = Order() ib_order = Order()
order.orderId = self.orderid ib_order.orderId = self.orderid
order.clientId = self.clientid ib_order.clientId = self.clientid
order.action = DIRECTION_VT2IB[req.direction] ib_order.action = DIRECTION_VT2IB[req.direction]
order.orderType = PRICETYPE_VT2IB[req.price_type] ib_order.orderType = PRICETYPE_VT2IB[req.price_type]
order.lmtPrice = req.price ib_order.lmtPrice = req.price
order.totalQuantity = req.volume 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) self.client.reqIds(1)
vt_orderid = f"{self.gateway_name}.{self.orderid}" vt_orderid = f"{self.gateway_name}.{self.orderid}"
@ -555,7 +595,32 @@ class IbApi(EWrapper):
""" """
Cancel an existing order. 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 EVENT_CONTRACT
) )
from .object import LogData, SubscribeRequest, OrderRequest, CancelRequest 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 .setting import SETTINGS
from .gateway import BaseGateway from .gateway import BaseGateway
@ -54,7 +54,7 @@ class MainEngine:
Add gateway. Add gateway.
""" """
gateway = gateway_class(self.event_engine) gateway = gateway_class(self.event_engine)
self.gateways[gateway.gateway_name] = engine self.gateways[gateway.gateway_name] = gateway
def init_engines(self): def init_engines(self):
""" """
@ -78,6 +78,7 @@ class MainEngine:
gateway = self.gateways.get(gateway_name, None) gateway = self.gateways.get(gateway_name, None)
if not gateway: if not gateway:
self.write_log(f"找不到底层接口:{gateway_name}") self.write_log(f"找不到底层接口:{gateway_name}")
return None
return gateway return gateway
def get_default_setting(self, gateway_name: str): def get_default_setting(self, gateway_name: str):
@ -87,14 +88,21 @@ class MainEngine:
gateway = self.get_gateway(gateway_name) gateway = self.get_gateway(gateway_name)
if gateway: if gateway:
return gateway.get_default_setting() 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. Start connection of a specific gateway.
""" """
gateway = self.get_gateway(gateway_name) gateway = self.get_gateway(gateway_name)
if gateway: if gateway:
gateway.connect() gateway.connect(setting)
def subscribe(self, req: SubscribeRequest, gateway_name: str): def subscribe(self, req: SubscribeRequest, gateway_name: str):
""" """
@ -120,7 +128,7 @@ class MainEngine:
""" """
gateway = self.get_gateway(gateway_name) gateway = self.get_gateway(gateway_name)
if gateway: if gateway:
gateway.send_order(req) gateway.cancel_order(req)
def close(self): def close(self):
""" """
@ -277,7 +285,7 @@ class OmsEngine(BaseEngine):
self.orders[order.vt_orderid] = order self.orders[order.vt_orderid] = order
# If order is active, then update data in dict. # 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 self.active_orders[order.vt_orderid] = order
# Otherwise, pop inactive order from in dict # Otherwise, pop inactive order from in dict
elif order.vt_orderid in self.active_orders: elif order.vt_orderid in self.active_orders:

View File

@ -6,6 +6,10 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from logging import INFO from logging import INFO
from .constant import (STATUS_SUBMITTING, STATUS_NOTTRADED, STATUS_PARTTRADED)
ACTIVE_STATUSES = set([STATUS_SUBMITTING, STATUS_NOTTRADED, STATUS_PARTTRADED])
@dataclass @dataclass
class BaseData: class BaseData:
@ -110,6 +114,26 @@ class OrderData(BaseData):
self.vt_symbol = f"{self.symbol}.{self.exchange}" self.vt_symbol = f"{self.symbol}.{self.exchange}"
self.vt_orderid = f"{self.gateway_name}.{self.orderid}" 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 @dataclass
class TradeData(BaseData): class TradeData(BaseData):
@ -182,7 +206,7 @@ class LogData(BaseData):
def __post_init__(self): def __post_init__(self):
"""""" """"""
time = datetime self.time = datetime.now()
@dataclass @dataclass

View File

@ -7,6 +7,7 @@ from PyQt5 import QtWidgets, QtGui
from .mainwindow import MainWindow from .mainwindow import MainWindow
from ..setting import SETTINGS from ..setting import SETTINGS
from ..utility import get_icon_path
def create_qapp(): def create_qapp():
@ -19,9 +20,7 @@ def create_qapp():
font = QtGui.QFont(SETTINGS["font.family"], SETTINGS["font.size"]) font = QtGui.QFont(SETTINGS["font.family"], SETTINGS["font.size"])
qapp.setFont(font) qapp.setFont(font)
ui_path = Path(__file__).parent icon = QtGui.QIcon(get_icon_path(__file__, "vnpy.ico"))
icon_path = ui_path.joinpath("ico", "vnpy.ico")
icon = QtGui.QIcon(str(icon_path))
qapp.setWindowIcon(icon) qapp.setWindowIcon(icon)
if 'Windows' in platform.uname(): 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 vnpy.event import EventEngine
from ..engine import MainEngine from ..engine import MainEngine
from ..utility import get_icon_path
from .widget import ( from .widget import (
TickMonitor, TickMonitor,
OrderMonitor, OrderMonitor,
TradeMonitor, TradeMonitor,
PositionMonitor, PositionMonitor,
AccountMonitor, AccountMonitor,
LogMonitor LogMonitor,
ConnectDialog,
TradingWidget
) )
@ -23,6 +32,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.main_engine = main_engine self.main_engine = main_engine
self.event_engine = event_engine self.event_engine = event_engine
self.connect_dialogs = {}
self.widgets = {} self.widgets = {}
self.init_ui() self.init_ui()
@ -35,6 +45,7 @@ class MainWindow(QtWidgets.QMainWindow):
def init_dock(self): 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) tick_widget, tick_dock = self.create_dock(TickMonitor, "行情", QtCore.Qt.RightDockWidgetArea)
order_widget, order_dock = self.create_dock(OrderMonitor, "委托", 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) trade_widget, trade_dock = self.create_dock(TradeMonitor, "成交", QtCore.Qt.RightDockWidgetArea)
@ -44,7 +55,22 @@ class MainWindow(QtWidgets.QMainWindow):
def init_menu(self): 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( def create_dock(
self, self,
@ -63,3 +89,35 @@ class MainWindow(QtWidgets.QMainWindow):
dock.setFeatures(dock.DockWidgetFloatable | dock.DockWidgetMovable) dock.setFeatures(dock.DockWidgetFloatable | dock.DockWidgetMovable)
self.addDockWidget(area, dock) self.addDockWidget(area, dock)
return widget, 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 PyQt5 import QtWidgets, QtGui, QtCore
from vnpy.event import EventEngine, Event 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 ..engine import MainEngine
from ..event import (EVENT_TICK, EVENT_ORDER, EVENT_TRADE, EVENT_ACCOUNT, from ..event import (EVENT_TICK, EVENT_ORDER, EVENT_TRADE, EVENT_ACCOUNT,
EVENT_POSITION, EVENT_CONTRACT, EVENT_LOG) EVENT_POSITION, EVENT_CONTRACT, EVENT_LOG)
from ..object import SubscribeRequest, OrderRequest, CancelRequest
COLOR_LONG = QtGui.QColor("red") COLOR_LONG = QtGui.QColor("red")
COLOR_SHORT = QtGui.QColor("green") COLOR_SHORT = QtGui.QColor("green")
@ -27,7 +33,7 @@ class BaseCell(QtWidgets.QTableWidgetItem):
def __init__(self, content: Any, data: Any): def __init__(self, content: Any, data: Any):
"""""" """"""
super(BaseCel, self).__init__() super(BaseCell, self).__init__()
self.setTextAlignment(QtCore.Qt.AlignCenter) self.setTextAlignment(QtCore.Qt.AlignCenter)
self.set_content(content, data) self.set_content(content, data)
@ -108,7 +114,7 @@ class PnlCell(BaseCell):
""" """
super(PnlCell, self).set_content(content, data) super(PnlCell, self).set_content(content, data)
if content.startswith("-"): if str(content).startswith("-"):
self.setForeground(COLOR_SHORT) self.setForeground(COLOR_SHORT)
else: else:
self.setForeground(COLOR_LONG) self.setForeground(COLOR_LONG)
@ -171,7 +177,7 @@ class BaseMonitor(QtWidgets.QTableWidget):
""" """
self.setColumnCount(len(self.headers)) 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.setHorizontalHeaderLabels(labels)
self.verticalHeader().setVisible(False) self.verticalHeader().setVisible(False)
@ -198,7 +204,7 @@ class BaseMonitor(QtWidgets.QTableWidget):
Register event handler into event engine. Register event handler into event engine.
""" """
self.signal.connect(self.process_event) 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): def process_event(self, event):
""" """
@ -236,10 +242,10 @@ class BaseMonitor(QtWidgets.QTableWidget):
setting = self.headers[header] setting = self.headers[header]
content = data.__getattribute__(header) content = data.__getattribute__(header)
cell = setting['cell'](content, data) cell = setting["cell"](content, data)
self.setItem(0, column, cell) self.setItem(0, column, cell)
if setting['update']: if setting["update"]:
row_cells[header] = cell row_cells[header] = cell
if self.data_key: if self.data_key:
@ -253,7 +259,7 @@ class BaseMonitor(QtWidgets.QTableWidget):
key = data.__getattribute__(self.data_key) key = data.__getattribute__(self.data_key)
row_cells = self.cells[key] row_cells = self.cells[key]
for header, cell in row_cells: for header, cell in row_cells.items():
content = data.__getattribute__(header) content = data.__getattribute__(header)
cell.set_content(content, data) cell.set_content(content, data)
@ -298,65 +304,65 @@ class TickMonitor(BaseMonitor):
sorting = True sorting = True
headers = { headers = {
'symbol': { "symbol": {
'display': '代码', "display": "代码",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'last_price': { "last_price": {
'display': '最新价', "display": "最新价",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'volume': { "volume": {
'display': '成交量', "display": "成交量",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'open_price': { "open_price": {
'display': '开盘价', "display": "开盘价",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'high_price': { "high_price": {
'display': '最高价', "display": "最高价",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'low_price': { "low_price": {
'display': '最低价', "display": "最低价",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'bid_price_1': { "bid_price_1": {
'display': '买1价', "display": "买1价",
'cell': BidCell, "cell": BidCell,
'update': True "update": True
}, },
'bid_volume_1': { "bid_volume_1": {
'display': '买1量', "display": "买1量",
'cell': BidCell, "cell": BidCell,
'update': True "update": True
}, },
'ask_price_1': { "ask_price_1": {
'display': '卖1价', "display": "卖1价",
'cell': AskCell, "cell": AskCell,
'update': True "update": True
}, },
'ask_volume_1': { "ask_volume_1": {
'display': '卖1量', "display": "卖1量",
'cell': AskCell, "cell": AskCell,
'update': True "update": True
}, },
'time': { "datetime": {
'display': '时间', "display": "时间",
'cell': TimeCell, "cell": TimeCell,
'update': True "update": True
}, },
'gateway_name': { "gateway_name": {
'display': '接口', "display": "接口",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
} }
} }
@ -370,28 +376,23 @@ class LogMonitor(BaseMonitor):
sorting = False sorting = False
headers = { headers = {
'time': { "time": {
'display': '时间', "display": "时间",
'cell': BaseCell, "cell": TimeCell,
'update': False "update": False
}, },
'msg': { "msg": {
'display': '信息', "display": "信息",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'gateway_name': { "gateway_name": {
'display': '接口', "display": "接口",
'cell': BaseCell, "cell": BaseCell,
'update': False "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): class TradeMonitor(BaseMonitor):
""" """
@ -402,45 +403,45 @@ class TradeMonitor(BaseMonitor):
sorting = True sorting = True
headers = { headers = {
'tradeid': { "tradeid": {
'display': "成交号 ", "display": "成交号 ",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'orderid': { "orderid": {
'display': '委托号', "display": "委托号",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'symbol': { "symbol": {
'display': '代码', "display": "代码",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'direction': { "direction": {
'display': '方向', "display": "方向",
'cell': DirectionCell, "cell": DirectionCell,
'update': False "update": False
}, },
'offset': { "offset": {
'display': '开平', "display": "开平",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'price': { "price": {
'display': '价格', "display": "价格",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'volume': { "volume": {
'display': '数量', "display": "数量",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'gateway_name': { "gateway_name": {
'display': '接口', "display": "接口",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
} }
} }
@ -454,55 +455,55 @@ class OrderMonitor(BaseMonitor):
sorting = True sorting = True
headers = { headers = {
'orderid': { "orderid": {
'display': '委托号', "display": "委托号",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'symbol': { "symbol": {
'display': '代码', "display": "代码",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'direction': { "direction": {
'display': '方向', "display": "方向",
'cell': DirectionCell, "cell": DirectionCell,
'update': False "update": False
}, },
'offset': { "offset": {
'display': '开平', "display": "开平",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'price': { "price": {
'display': '价格', "display": "价格",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'volume': { "volume": {
'display': '总数量', "display": "总数量",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'traded': { "traded": {
'display': '已成交', "display": "已成交",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'status': { "status": {
'display': '状态', "display": "状态",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'time': { "time": {
'display': '时间', "display": "时间",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'gateway_name': { "gateway_name": {
'display': '接口', "display": "接口",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
} }
} }
@ -515,40 +516,40 @@ class PositionMonitor(BaseMonitor):
sorting = True sorting = True
headers = { headers = {
'symbol': { "symbol": {
'display': '代码', "display": "代码",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'direction': { "direction": {
'display': '方向', "display": "方向",
'cell': DirectionCell, "cell": DirectionCell,
'update': False "update": False
}, },
'volume': { "volume": {
'display': '数量', "display": "数量",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'frozen': { "frozen": {
'display': '冻结', "display": "冻结",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'price': { "price": {
'display': '均价', "display": "均价",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'pnl': { "pnl": {
'display': '盈亏', "display": "盈亏",
'cell': PnlCell, "cell": PnlCell,
'update': True "update": True
}, },
'gateway_name': { "gateway_name": {
'display': '接口', "display": "接口",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
} }
} }
@ -562,29 +563,399 @@ class AccountMonitor(BaseMonitor):
sorting = True sorting = True
headers = { headers = {
'accountid': { "accountid": {
'display': '账号', "display": "账号",
'cell': BaseCell, "cell": BaseCell,
'update': False "update": False
}, },
'balance': { "balance": {
'display': '余额', "display": "余额",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'frozen': { "frozen": {
'display': '冻结', "display": "冻结",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'available': { "available": {
'display': '可用', "display": "可用",
'cell': BaseCell, "cell": BaseCell,
'update': True "update": True
}, },
'gateway_name': { "gateway_name": {
'display': '接口', "display": "接口",
'cell': BaseCell, "cell": BaseCell,
'update': False "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) return temp_path.joinpath(filename)
ACTIVE_STATUSES = set([STATUS_SUBMITTING, STATUS_NOTTRADED, STATUS_PARTTRADED]) def get_icon_path(file_path: str, ico_name: str):
def check_order_active(status: str):
""" """
Check if order is active by status. Get path for icon file with ico name.
""" """
if status in ACTIVE_STATUSES: ui_path = Path(file_path).parent
return True icon_path = ui_path.joinpath("ico", ico_name)
else: return str(icon_path)
return False