From be25b684a09dc3a60f3d582c3277cf8ebb9179af Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Wed, 9 Jan 2019 16:41:51 +0800 Subject: [PATCH] [Add] Start ib gateway development --- {test => tests}/trader/run.py | 0 vnpy/gateway/ib/ib_gateway.py | 271 ++++++++++++++++++++++++++++++++++ vnpy/trader/constant.py | 96 ++++++------ vnpy/trader/engine.py | 50 ++++--- 4 files changed, 355 insertions(+), 62 deletions(-) rename {test => tests}/trader/run.py (100%) create mode 100644 vnpy/gateway/ib/ib_gateway.py diff --git a/test/trader/run.py b/tests/trader/run.py similarity index 100% rename from test/trader/run.py rename to tests/trader/run.py diff --git a/vnpy/gateway/ib/ib_gateway.py b/vnpy/gateway/ib/ib_gateway.py new file mode 100644 index 00000000..6d2074b1 --- /dev/null +++ b/vnpy/gateway/ib/ib_gateway.py @@ -0,0 +1,271 @@ +""" +Please install ibapi from Interactive Brokers github page. +""" +from datetime import datetime +from copy import copy + +from ibapi.wrapper import EWrapper +from ibapi.client import EClient + +from vnpy.trader.gateway import Gateway +from vnpy.trader.object import ( + TickData, + OrderData, + TradeData, + ContractData, + PositionData, + AccountData, + LogData, + SubscribeRequest, + OrderRequest, + CancelRequest +) +from vnpy.trader.constant import ( + PRODUCT_EQUITY, + PRODUCT_FOREX, + PRODUCT_SPOT, + PRODUCT_FUTURES, + PRODUCT_OPTION, + PRICETYPE_LIMIT, + PRICETYPE_MARKET, + DIRECTION_LONG, + DIRECTION_SHORT, + EXCHANGE_SMART, + EXCHANGE_NYMEX, + EXCHANGE_GLOBEX, + EXCHANGE_IDEALPRO, + EXCHANGE_HKEX, + EXCHANGE_HKFE, + EXCHANGE_CME, + EXCHANGE_ICE, + CURRENCY_CNY, + CURRENCY_HKD, + CURRENCY_USD, + STATUS_SUBMITTING, + STATUS_NOTTRADED, + STATUS_PARTTRADED, + STATUS_ALLTRADED, + STATUS_CANCELLED, + STATUS_REJECTED, + OPTION_CALL, + OPTION_PUT +) + +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()} + +EXCHANGE_VT2IB = { + EXCHANGE_SMART: "SMART", + EXCHANGE_NYMEX: "NYMEX", + EXCHANGE_GLOBEX: "GLOBEX", + EXCHANGE_IDEALPRO: "IDEALPRO", + EXCHANGE_CME: "CME", + EXCHANGE_ICE: "ICE", + EXCHANGE_HKEX: "HKEX", + EXCHANGE_HKFE: "HKFE" +} +EXCHANGE_IB2VT = {v: k for k, v in EXCHANGE_VT2IB.items()} + +STATUS_IB2VT = { + "Submitted": STATUS_NOTTRADED, + "Filled": STATUS_ALLTRADED, + "Cancelled": STATUS_CANCELLED, + "PendingSubmit": STATUS_SUBMITTING, + "PreSubmitted": STATUS_NOTTRADED +} + +PRODUCT_VT2IB = { + PRODUCT_EQUITY: "STK", + PRODUCT_FOREX: "CASH", + PRODUCT_SPOT: "CMDTY", + PRODUCT_OPTION: "OPT", + PRODUCT_FUTURES: "FUT" +} + +OPTION_VT2IB = {OPTION_CALL: "CALL", OPTION_PUT: "PUT"} + +CURRENCY_VT2IB = {CURRENCY_USD: "USD", CURRENCY_CNY: "CNY", CURRENCY_HKD: "HKD"} + +TICKFIELD_IB2VT = { + 0: "bid_volume_1", + 1: "bid_price_1", + 2: "ask_price_1", + 3: "ask_volume_1", + 4: "last_price", + 5: "last_volume", + 6: "high_price", + 7: "low_price", + 8: "volume", + 9: "pre_close", + 14: "open_price" +} + +ACCOUNTFIELD_IB2VT = { + "NetLiquidationByCurrency": "balance", + "NetLiquidation": "balance", + "UnrealizedPnL": "positionProfit", + "AvailableFunds": "available", + "MaintMarginReq": "margin", +} + + +class IbGateway(Gateway): + """""" + + def __init__(self, event_engine): + """""" + super(Gateway, self).__init__(event_engine, "IB") + + def connect(self): + """ + Start gateway connection. + """ + pass + + def close(self): + """ + Close gateway connection. + """ + pass + + def subscribe(self, req: SubscribeRequest): + """ + Subscribe tick data update. + """ + pass + + def send_order(self, req: OrderRequest): + """ + Send a new order. + """ + pass + + def cancel_order(self, req: CancelRequest): + """ + Cancel an existing order. + """ + pass + + def query_account(self): + """ + Query account balance. + """ + pass + + def query_position(self): + """ + Query holding positions. + """ + pass + + +class IbApi(EWrapper): + """""" + + def __init__(self, gateway: Gateway): + """""" + super(IbApi, self).__init__() + + self.gateway = gateway + self.gateway_name = gateway.gateway_name + + self.status = False + self.orderid = 0 + self.ticks = {} + self.orders = {} + self.accounts = {} + self.contracts = {} + + def nextValidId(self, orderId: int): + """ + Callback of next valid orderid. + """ + super(IbApi, self).nextValidId(orderId) + + self.orderid = orderId + + def currentTime(self, time: int): + """ + Callback of current server time of IB. + """ + 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}", + gateway_name=self.gateway_name + ) + self.gateway.on_log(log) + + def error(self, reqId: TickerId, errorCode: int, errorString: str): + """ + Callback of error caused by specific request. + """ + super(IbApi, self).error(reqId, errorCode, errorString) + log = LogData( + msg=f"发生错误,错误代码:{errorCode},错误信息: {errorString}", + gateway_name=self.gateway_name + ) + self.gateway.on_log(log) + + def tickPrice( + self, + reqId: TickerId, + tickType: TickType, + price: float, + attrib: TickAttrib + ): + """ + Callback of tick price update. + """ + super(IbApi, self).tickPrice(reqId, tickType, price, attrib) + + if tickType not in TICKFIELD_IB2VT: + return + + tick = self.ticks[reqId] + name = TICKFIELD_IB2VT[tickType] + setattr(tick, name, price) + + # Forex and spot product of IB has no tick time and last price. + # We need to calculate locally. + contract = self.contracts[reqId] + if contract.product in (PRODUCT_FOREX, PRODUCT_SPOT): + tick.last_price = (tick.bid_price_1 + tick.ask_price_1) / 2 + tick.datetime = datetime.now() + self.gateway.on_tick(copy(tick)) + + def tickSize(self, reqId: TickerId, tickType: TickType, size: int): + """ + Callback of tick volume update. + """ + super(IbApi, self).tickSize(reqId, tickType, size) + + if tickType not in TICKFIELD_IB2VT: + return + + tick = self.ticks[reqId] + name = TICKFIELD_IB2VT[tickType] + setattr(tick, name, size) + + self.gateway.on_tick(copy(tick)) + + def tickString(self, reqId: TickerId, tickType: TickType, value: str): + """ + Callback of tick string update. + """ + super().tickString(reqId, tickType, value) + + if tickType != "45": + return + + tick = self.ticks[reqId] + tick.datetime = datetime.fromtimestamp(value) + + self.gateway.on_tick(copy(tick)) diff --git a/vnpy/trader/constant.py b/vnpy/trader/constant.py index cd1e53e3..7726a09f 100644 --- a/vnpy/trader/constant.py +++ b/vnpy/trader/constant.py @@ -2,54 +2,64 @@ General constant string used in VN Trader. """ -DIRECTION_LONG = '多' -DIRECTION_SHORT = '空' -DIRECTION_NET = '净' +DIRECTION_LONG = "多" +DIRECTION_SHORT = "空" +DIRECTION_NET = "净" -OFFSET_OPEN = '开' -OFFSET_CLOSE = '平' -OFFSET_CLOSETODAY = '平今' -OFFSET_CLOSEYESTERDAY = '平昨' +OFFSET_OPEN = "开" +OFFSET_CLOSE = "平" +OFFSET_CLOSETODAY = "平今" +OFFSET_CLOSEYESTERDAY = "平昨" -STATUS_SUBMITTING = '提交中' -STATUS_NOTTRADED = '未成交' -STATUS_PARTTRADED = '部分成交' -STATUS_ALLTRADED = '全部成交' -STATUS_CANCELLED = '已撤销' -STATUS_REJECTED = '拒单' +STATUS_SUBMITTING = "提交中" +STATUS_NOTTRADED = "未成交" +STATUS_PARTTRADED = "部分成交" +STATUS_ALLTRADED = "全部成交" +STATUS_CANCELLED = "已撤销" +STATUS_REJECTED = "拒单" -PRODUCT_EQUITY = '股票' -PRODUCT_FUTURES = '期货' -PRODUCT_OPTION = '期权' -PRODUCT_INDEX = '指数' -PRODUCT_FOREX = '外汇' -PRODUCT_SPOT = '现货' +PRODUCT_EQUITY = "股票" +PRODUCT_FUTURES = "期货" +PRODUCT_OPTION = "期权" +PRODUCT_INDEX = "指数" +PRODUCT_FOREX = "外汇" +PRODUCT_SPOT = "现货" -PRICETYPE_LIMIT = '限价' -PRICETYPE_MARKET = '市价' -PRICETYPE_FAK = 'FAK' -PRICETYPE_FOK = 'FOK' +PRICETYPE_LIMIT = "限价" +PRICETYPE_MARKET = "市价" +PRICETYPE_FAK = "FAK" +PRICETYPE_FOK = "FOK" -OPTION_CALL = '看涨期权' -OPTION_PUT = '看跌期权' +OPTION_CALL = "看涨期权" +OPTION_PUT = "看跌期权" -EXCHANGE_SSE = 'SSE' -EXCHANGE_SZSE = 'SZSE' -EXCHANGE_CFFEX = 'CFFEX' -EXCHANGE_SHFE = 'SHFE' -EXCHANGE_CZCE = 'CZCE' -EXCHANGE_DCE = 'DCE' -EXCHANGE_INE = 'INE' -EXCHANGE_SGE = 'SGE' +EXCHANGE_SSE = "SSE" +EXCHANGE_SZSE = "SZSE" +EXCHANGE_CFFEX = "CFFEX" +EXCHANGE_SHFE = "SHFE" +EXCHANGE_CZCE = "CZCE" +EXCHANGE_DCE = "DCE" +EXCHANGE_INE = "INE" +EXCHANGE_SGE = "SGE" -CURRENCY_USD = 'USD' -CURRENCY_HKD = 'HKD' +EXCHANGE_SMART = "SMART" +EXCHANGE_NYMEX = "NYMEX" +EXCHANGE_GLOBEX = "GLOBEX" +EXCHANGE_IDEALPRO = "IDEALPRO" +EXCHANGE_CME = "CME" +EXCHANGE_ICE = "ICE" +EXCHANGE_HKEX = "HKEX" +EXCHANGE_HKFE = "HKFE" -INTERVAL_1M = '1分钟' -INTERVAL_5M = '5分钟' -INTERVAL_15M = '15分钟' -INTERVAL_30M = '30分钟' -INTERVAL_1H = '1小时' -INTERVAL_4H = '4小时' -INTERVAL_DAILY = '日线' -INTERVAL_WEEKLY = '周线' \ No newline at end of file +CURRENCY_USD = "USD" +CURRENCY_HKD = "HKD" +CURRENCY_CNY = "CNY" + +INTERVAL_1M = "1分钟" +INTERVAL_5M = "5分钟" +INTERVAL_15M = "15分钟" +INTERVAL_30M = "30分钟" +INTERVAL_1H = "1小时" +INTERVAL_4H = "4小时" +INTERVAL_DAILY = "日线" +INTERVAL_WEEKLY = "周线" \ No newline at end of file diff --git a/vnpy/trader/engine.py b/vnpy/trader/engine.py index 0163c9c3..bc2c2119 100644 --- a/vnpy/trader/engine.py +++ b/vnpy/trader/engine.py @@ -6,7 +6,15 @@ from datetime import datetime from vnpy.event import EventEngine, Event -from .event import EVENT_LOG +from .event import ( + EVENT_LOG, + EVENT_TICK, + EVENT_ORDER, + EVENT_TRADE, + EVENT_POSITION, + EVENT_ACCOUNT, + EVENT_CONTRACT +) from .object import LogData, SubscribeRequest, OrderRequest, CancelRequest from .utility import Singleton, get_temp_path, check_order_active from .setting import SETTINGS @@ -31,7 +39,7 @@ class MainEngine: self.init_engines() - def init_engines: + def init_engines(self): """ Init all engines. """ @@ -40,20 +48,21 @@ class MainEngine: # OMS Engine self.engines["oms"] = OmsEngine(self, self.event_engine) - + oms_engine = self.engines["oms"] self.get_tick = oms_engine.get_tick self.get_order = oms_engine.get_order self.get_position = oms_engine.get_position self.get_account = oms_engine.get_account self.get_contract = oms_engine.get_contract - self.get_all_ticks = oms.get_all_ticks - self.get_all_orders = oms.get_all_orders - self.get_all_trades = oms.get_all_trades - self.get_all_positions = oms.get_all_positions - self.get_all_accounts = oms.get_all_accounts - self.get_all_active_orders = oms.get_all_active_orders - + self.get_contract = oms_engine.get_contract + self.get_all_ticks = oms_engine.get_all_ticks + self.get_all_orders = oms_engine.get_all_orders + self.get_all_trades = oms_engine.get_all_trades + self.get_all_positions = oms_engine.get_all_positions + self.get_all_accounts = oms_engine.get_all_accounts + self.get_all_active_orders = oms_engine.get_all_active_orders + def write_log(self, msg: str): """ Put log event with specific message. @@ -172,7 +181,7 @@ class LogEngine: file_handler = logging.FileHandler(file_path, mode='w', encoding='utf8') file_handler.setLevel(self.level) file_handler.setFormatter(self.formatter) - self.logger.StreamHandler(file_handler) + self.logger.addHandler(file_handler) def register_event(self): """""" @@ -232,7 +241,7 @@ class OmsEngine: # Otherwise, pop inactive order from in dict elif order.vt_orderid in self.active_orders: self.active_orders.pop(order.vt_orderid) - + def process_trade_event(self, event: Event): """""" trade = event.data @@ -251,8 +260,8 @@ class OmsEngine: def process_contract_event(self, event: Event): """""" contract = event.data - self.contracts[contract.vt_symbol]] = contract - + self.contracts[contract.vt_symbol] = contract + def get_tick(self, vt_symbol): """ Get latest market tick data by vt_symbol. @@ -264,7 +273,7 @@ class OmsEngine: Get latest order data by vt_orderid. """ return self.orders.get(vt_orderid, None) - + def get_trade(self, vt_tradeid): """ Get trade data by vt_tradeid. @@ -288,7 +297,7 @@ class OmsEngine: Get contract data by vt_symbol. """ return self.contracts.get(vt_symbol, None) - + def get_all_ticks(self): """ Get all tick data. @@ -324,8 +333,8 @@ class OmsEngine: Get all contract data. """ return list(self.contracts.values()) - - def get_all_active_orders(self, vt_symbol: str=''): + + def get_all_active_orders(self, vt_symbol: str = ''): """ Get all active orders by vt_symbol. @@ -334,5 +343,8 @@ class OmsEngine: if not vt_symbol: return list(self.active_orders.values()) else: - active_orders = [order for order in self.active_orders.values() if order.vt_symbol == vt_symbol] + active_orders = [ + order for order in self.active_orders.values() + if order.vt_symbol == vt_symbol + ] return active_orders \ No newline at end of file