diff --git a/tests/api/base/WebSocketClientTest.py b/tests/api/base/WebSocketClientTest.py index 8d931ecf..dc9651dd 100644 --- a/tests/api/base/WebSocketClientTest.py +++ b/tests/api/base/WebSocketClientTest.py @@ -2,10 +2,10 @@ import unittest from Promise import Promise -from vnpy.api.websocket import WebSocketClient +from vnpy.api.websocket import WebsocketClient -class TestWebsocketClient(WebSocketClient): +class TestWebsocketClient(WebsocketClient): def __init__(self): host = 'wss://echo.websocket.org' diff --git a/vnpy/api/okexfutures/OkexFuturesApi.py b/vnpy/api/okexfutures/OkexFuturesApi.py deleted file mode 100644 index 6974dc1d..00000000 --- a/vnpy/api/okexfutures/OkexFuturesApi.py +++ /dev/null @@ -1,1157 +0,0 @@ -# encoding: UTF-8 -""" -# Okex Futures API V3 坑记: -* https://www.okex.com/api/futures/v3/instruments 返回值中的trade_increment有时候会变成quote_increment -* /api/futures/v3/order 如果下单时不提供client_id,返回值将不会有client_id字段 -* websocket居然还没升级好就把API放出来了?! - -""" - -from enum import Enum -from typing import Any, Callable, List, Union - -from vnpy.api.okexfutures.OkexFuturesBase import OkexFuturesRestBaseV1, OkexFuturesRestBaseV3, \ - OkexFuturesWebSocketBase -from vnpy.api.rest import Request - - -######################################################################## -class _OkexFuturesCustomExtra(object): - - #---------------------------------------------------------------------- - def __init__(self, onSuccess, onFailed, extra): - self.onFailed = onFailed - self.onSuccess = onSuccess - self.extra = extra - - -######################################################################## -class OkexFuturesEasySymbol(object): - BTC = 'btc' - LTC = 'ltc' - ETH = 'eth' - ETC = 'etc' - BCH = 'bch' - EOS = 'eos' - XRP = 'xrp' - BTG = 'btg' - - -######################################################################## -class OkexFuturesSymbol(object): - BTC = 'btc_usd' - LTC = 'ltc_usd' - ETH = 'eth_usd' - ETC = 'etc_usd' - BCH = 'bch_usd' - - -######################################################################## -class OkexFuturesPriceType(object): - Buy = 'buy' - Sell = 'sell' - - -######################################################################## -class OkexFuturesContractType(object): - ThisWeek = 'this_week' - NextWeek = 'next_week' - Quarter = 'quarter' - - -######################################################################## -class OkexFuturesOrderType(object): - OpenLong = '1' - OpenShort = '2' - CloseLong = '3' - CloseShort = '4' - - -######################################################################## -class OkexFuturesOrderStatus(object): - NotFinished = '1' - Finished = '2' - - -######################################################################## -class OkexFuturesOrder(object): - - #---------------------------------------------------------------------- - def __init__(self): - self.volume = None - self.contractName = None - self.createDate = None - self.tradedVolume = None - self.fee = None - self.leverRate = None - self.remoteId = None - self.price = None - self.priceAvg = None - self.status = None - self.symbol = None - self.orderType = None - self.unitAmount = None - - -######################################################################## -class OkexFuturesUserInfo(object): - - #---------------------------------------------------------------------- - def __init__(self): - self.easySymbol = None # 'etc', 'btc', 'eth', etc. - self.accountRights = None - self.keepDeposit = None - self.profitReal = None - self.profitUnreal = None - self.riskRate = None - - -######################################################################## -class OkexFuturesPosition(object): - - #---------------------------------------------------------------------- - def __init__(self, ): - self.forceLiquidatePrice = None - self.holding = [] # type: List[OkexFuturesPositionDetail] - - -######################################################################## -class OkexFuturesPositionDetail(object): - - #---------------------------------------------------------------------- - def __init__(self, ): - self.buyAmount = None - self.buyAvailable = None - self.buyPriceAvg = None - self.buyPriceCost = None - self.buyProfitReal = None - self.contractId = None - self.createDate = None - self.leverRate = None - self.sellAmount = None - self.sellAvailable = None - self.sellPriceAvg = None - self.sellPriceCost = None - self.sellProfitReal = None - self.symbol = None - self.contractType = None - - -######################################################################## -class OkexFuturesTickInfo(object): - - #---------------------------------------------------------------------- - def __init__(self, symbol, remoteContractType, last, limitHigh, limitLow, vol, sell, buy, - unitAmount, holdAmount, - contractId, high, low): - self.symbol = symbol - self.remoteContractType = remoteContractType - self.last = last - self.limitHigh = limitHigh # type: str # 最高买入限制价格 - self.limitLow = limitLow # type: str # 最低卖出限制价格 - self.vol = vol # type: float # 24 小时成交量 - self.sell = sell # type: float # 卖一价格 - self.buy = buy # type: float # 买一价格 - self.unitAmount = unitAmount # type: float # 合约价值 - self.holdAmount = holdAmount # type: float # 当前持仓量 - self.contractId = contractId # type: long # 合约ID - self.high = high # type: float # 24 小时最高价格 - self.low = low # type: float # 24 小时最低价格 - - -######################################################################## -class OkexFuturesTradeInfo(object): - - #---------------------------------------------------------------------- - def __init__(self, symbol, remoteContractType, index, price, volume, time, direction, coinVolume): - self.symbol = symbol - self.remoteContractType = remoteContractType - self.index = index - self.price = price - self.volume = volume - self.time = time - self.direction = direction - self.coinVolume = coinVolume - - -######################################################################## -class OkexFuturesUserTradeInfo(object): - - #---------------------------------------------------------------------- - def __init__(self, symbol, remoteContractType, amount, - contractName, createdDate, createDateStr, dealAmount, fee, - orderId, price, priceAvg, status, type, unitAmount, leverRate, systemType - ): - self.symbol = symbol # type: str # btcUsd ltcUsd ethUsd etcUsd bchUsd - self.remoteContractType = remoteContractType - self.amount = amount # type: float # 委托数量 - self.contractName = contractName # type: str # 合约名称 - self.createdDate = createdDate # type: long # 委托时间 - self.createDateStr = createDateStr # type: str # 委托时间字符串 - self.dealAmount = dealAmount # type: float # 成交数量 - self.fee = fee # type: float # 手续费 - self.remoteId = orderId # type: long # 订单ID - self.price = price # type: float # 订单价格 - self.priceAvg = priceAvg # type: float # 平均价格 - self.status = status # type: int # 订单状态(0等待成交 1部分成交 2全部成交 -1撤单 4撤单处理中) - self.type = type # type: int # 订单类型 1:开多 2:开空 3:平多 4:平空 - self.unitAmount = unitAmount # type: float # 合约面值 - self.leverRate = leverRate # type: float # 杠杆倍数 value:10/20 默认10 - self.systemType = systemType # type: int # 订单类型 0:普通 1:交割 2:强平 4:全平 5:系统反单 - - -######################################################################## -class OkexFuturesContractsInfoV3(object): - - #---------------------------------------------------------------------- - def __init__(self, - instrumentId, - underlyingIndex, - quoteCurrency, - quote_increment, - contractVal, - listing, - delivery, - tickSize, - ): - self.symbol = instrumentId # String # 合约ID,如BTC-USD-180213 - self.underlyingIndex = underlyingIndex # String # 交易货币币种,如:btc-usdt中的btc - self.quoteCurrency = quoteCurrency # String # 计价货币币种,如:btc-usdt中的usdt - self.quoteIncrement = quote_increment # Number # 下单数量精度 - self.contractVal = contractVal # Number # 合约面值(美元) - self.listing = listing # Date # 上线日期 - self.delivery = delivery # Date # 交割日期 - self.tickSize = tickSize # Number # 下单价格精度 - - -######################################################################## -class OkexFuturesAccountInfoV3(object): - - #---------------------------------------------------------------------- - def __init__(self, currency, balance, hold, available): - self.currency = currency # String # 币种 - self.balance = balance # number # 余额 - self.hold = hold # number # 冻结(不可用) - self.available = available # number # 可用于提现或资金划转的数量 - - -######################################################################## -class OkexFuturesPositionInfoV3(object): - - #---------------------------------------------------------------------- - def __init__(self, marginMode, liquidationPrice, longQty, longAvailQty, longAvgCost, - longSettlementPrice, - realizedPnl, shortQty, shortAvailQty, shortAvgCost, shortSettlementPrice, - instrumentId, - leverage, createAt, updatAt, - ): - self.marginMode = marginMode # String # 账户类型:全仓 crossed - self.liquidationPrice = liquidationPrice # Price # 预估爆仓价 - self.longQty = longQty # Number # 多仓数量 - self.longAvailQty = longAvailQty # Number # 多仓可平仓数量 - self.longAvgCost = longAvgCost # Price # 开仓平均价 - self.longSettlementPrice = longSettlementPrice # Price # 多仓结算基准价 - self.realizedPnl = realizedPnl # Number # 已实现盈余 - self.shortQty = shortQty # Number # 空仓数量 - self.shortAvailQty = shortAvailQty # Number # 空仓可平仓数量 - self.shortAvgCost = shortAvgCost # Price # 开仓平均价 - self.shortSettlementPrice = shortSettlementPrice # String # 空仓结算基准价 - self.symbol = instrumentId # Number # 合约ID,如BTC-USD-180213 - self.leverage = leverage # Date # 杠杆倍数 - self.createAt = createAt # Date # 创建时间 - self.updatAt = updatAt # Date # 更新时间 - - -######################################################################## -class OkexFuturesOrderSentInfoV3(object): - - #---------------------------------------------------------------------- - def __init__(self, orderId, clientOid, errorCode, errorMessage): - self.orderId = orderId # String # 订单ID,下单失败时,此字段值为-1 - self.clientOid = clientOid # String # 由您设置的订单ID来识别您的订单 - self.errorCode = errorCode # Number # 错误码,下单成功时为0,下单失败时会显示相应错误码 - self.errorMessage = errorMessage # String # 错误信息,下单成功时为空,下单失败时会显示错误信息 - - -######################################################################## -class OkexFuturesOrderDetailV3(object): - - #---------------------------------------------------------------------- - def __init__(self, instrumentId, size, timestamp, filledQty, fee, orderId, price, priceAvg, - status, orderType, contractVal, leverage, ): - self.symbol = instrumentId # String # 合约ID,如BTC-USD-180213 - self.volume = size # Number # 数量 - self.timestamp = timestamp # Date # 委托时间 - self.tradedVolume = filledQty # Number # 成交数量 - self.fee = fee # Price # 手续费 - self.remoteId = orderId # String # 订单ID - self.price = price # Price # 订单价格 - self.priceAvg = priceAvg # Price # 平均价格 - self.status = status # Number # 订单状态(0:等待成交 1:部分成交 2:已完成) - self.orderType = orderType # Number # 订单类型(1:开多 2:开空 3:开多 4:平空) - self.contractVal = contractVal # Price # 合约面值 - self.leverage = leverage # Number # 杠杆倍数 value:10/20 默认10 - - -######################################################################## -class OkexFuturesRestClientV3(OkexFuturesRestBaseV3): - """ - Okex新出了V3版本的API,这里用的是V3的版本 - """ - #---------------------------------------------------------------------- - def __init__(self): - """Constructor""" - super(OkexFuturesRestClientV3, self).__init__() - - #---------------------------------------------------------------------- - def sendOrder(self, - symbol, - orderType, - price, - volume, - leverRate, # type: int # 档杆倍数,10或者20 - onSuccess, # type: Callable[[OkexFuturesOrderSentInfoV3, Any], Any] - onFailed=None, # type: Callable[[OkexFuturesOrderSentInfoV3, Any], Any] - matchPrice=False, # type: bool # 是否为市价单 - clientOid=None, # type: str # OkexAPI提供的的用户自定义字段 - extra=None - ): - """ 下单 """ - data = { - 'client_oid': clientOid, - 'instrument_id': symbol, - 'type': orderType, - 'size': volume, - 'leverage': leverRate, - - 'price': price, - 'match_price': '0' - } - # if matchPrice: - # data['match_price'] = '1' - # else: - # data['price'] = price - - return self.addRequest('POST', '/api/futures/v3/order', - callback=self._onOrderSent, - data=data, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra) - ) - - #---------------------------------------------------------------------- - def cancelOrder(self, - symbol, # type: str - remoteId, # type: str - onSuccess, # type: Callable[[Any], Any] - onFailed=None, # type: Callable[[Any], Any] - extra=None - ): # type: (...)->Request - """撤单""" - path = '/api/futures/v3/cancel_order/' + symbol + '/' + remoteId - return self.addRequest('POST', path, - callback=self._onOrderCanceled, - data={ - 'instrument_id' : symbol, - 'order_id': remoteId - }, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra) - ) - - #---------------------------------------------------------------------- - def queryAccount(self, - onSuccess, # type: Callable[[List[OkexFuturesAccountInfoV3], Any], Any] - extra=None - ): # type: (...)->Request - """ - 查询全部账户资金 - """ - return self.addRequest('GET', '/api/account/v3/wallet', - callback=self._onAccounts, - extra=_OkexFuturesCustomExtra(onSuccess, None, extra) - ) - - #---------------------------------------------------------------------- - def queryOrders(self, - symbol, - status, # type: OkexFuturesOrderStatus - onSuccess, # type: Callable[[List[OkexFuturesOrderDetailV3], Any], Any] - onFailed=None, # type: Callable[[Any], Any] - startPage=0, # type: int # 取回的页数区间为:(start, end) - endPage=2, # type: int # 取回的页数区间为:(start, end) - numberPerPage=100, # type: int - extra=None - ): # type: (...)->Request - """查询账户订单""" - path = '/api/futures/v3/orders/' + symbol - return self.addRequest("POST", path, - data={ - 'status': status, - 'instrument_id': symbol, - 'from': startPage, - 'to': endPage, - 'limit': numberPerPage, - }, - callback=self._onOrders, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra) - ) - - #---------------------------------------------------------------------- - def queryPositions(self, - onSuccess, - extra=None): - """ 获取全部持仓 """ - return self.addRequest('GET', '/api/account/v3/position', - callback=self._onPositions, - extra=_OkexFuturesCustomExtra(onSuccess, None, extra) - ) - - #---------------------------------------------------------------------- - def queryContracts(self, - onSuccess, # type: Callable[[List[OkexFuturesContractsInfoV3], Any], Any] - extra=None): - """ 获取全部合约信息 """ - return self.addRequest('GET', '/api/futures/v3/instruments', - callback=self._onContracts, - extra=_OkexFuturesCustomExtra(onSuccess, None, extra) - ) - - #---------------------------------------------------------------------- - @staticmethod - def _onOrderSent(data, request): #type: (dict, Request)->None - """下单回调""" - extra = request.extra # type: _OkexFuturesCustomExtra - order = OkexFuturesOrderSentInfoV3( - data['order_id'], - data['client_oid'] if 'client_oid' in data else None, - data['error_code'], - data['error_message'], - ) - if order.orderId != '-1': - extra.onSuccess(order, extra.extra) - else: - if extra.onFailed: - extra.onFailed(order, extra.extra) - - #---------------------------------------------------------------------- - def _onOrderCanceled(self, data, request): #type: (dict, Request)->None - """撤单回调""" - extra = request.extra # type: _OkexFuturesCustomExtra - result = data['result'] - if result is True: - extra.onSuccess(extra.extra) - else: - if extra.onFailed: - extra.onFailed(extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def _onAccounts(data, request): #type: (dict, Request)->None - """账户资金回调""" - extra = request.extra # type: _OkexFuturesCustomExtra - accs = [] - for acc in data: - accs.append(OkexFuturesAccountInfoV3( - acc['currency'], - acc['balance'], - acc['hold'], - acc['available'], - )) - extra.onSuccess(accs, extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def _onOrders(data, request): #type: (dict, Request)->None - """ - 查询订单回调 - https://www.okex.com/docs/zh/#futures-list - """ - extra = request.extra # type: _OkexFuturesCustomExtra - if data['result'] is True: - os = [] - for info in data['orders']: - os.append(OkexFuturesOrderDetailV3( - info['instrument_id'], - info['size'], - info['timestamp'], - info['filled_qty'], - info['fee'], - info['order_id'], - info['price'], - info['price_avg'], - info['status'], - info['type'], - info['contract_val'], - info['leverage'], - )) - extra.onSuccess(os, extra.extra) - else: - if extra.onFailed: - extra.onFailed(extra.extra) - - - #---------------------------------------------------------------------- - @staticmethod - def _onPositions(data, request): #type: (dict, Request)->None - extra = request.extra # type: _OkexFuturesCustomExtra - accs = [] - for acc in data: - accs.append(OkexFuturesPositionInfoV3( - acc['margin_mode'], - acc['liquidation_price'], - acc['long_qty'], - acc['long_avail_qty'], - acc['long_avg_cost'], - acc['long_settlement_price'], - acc['realized_pnl'], - acc['short_qty'], - acc['short_avail_qty'], - acc['short_avg_cost'], - acc['short_settlement_price'], - acc['instrument_id'], - acc['leverage'], - acc['create_at'], - acc['updat_at'], - )) - extra.onSuccess(accs, extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def _onContracts(data, request): #type: (dict, Request)->None - """ - 合约信息回调 - https://www.okex.com/docs/zh/#futures-contract_information - """ - extra = request.extra # type: _OkexFuturesCustomExtra - ins = [] - for instrument in data: - ins.append(OkexFuturesContractsInfoV3( - instrument['instrument_id'], - instrument['underlying_index'], - instrument['quote_currency'], - instrument['quote_increment'] if 'quote_increment' in instrument else instrument['trade_increment'], - instrument['contract_val'], - instrument['listing'], - instrument['delivery'], - instrument['tick_size'], - )) - extra.onSuccess(ins, extra.extra) - - -######################################################################## -class OkexFuturesRestClientV1(OkexFuturesRestBaseV1): - """ - 这里用的是旧的v1版本的OkexAPI - """ - - #---------------------------------------------------------------------- - def __init__(self): - """Constructor""" - super(OkexFuturesRestClientV1, self).__init__() - - #---------------------------------------------------------------------- - def sendOrder(self, symbol, contractType, orderType, volume, - onSuccess, onFailed=None, - price=None, useMarketPrice=False, leverRate=None, - extra=None): # type:(str, OkexFuturesContractType, OkexFuturesOrderType, float, Callable[[str, Any], Any], Callable[[int, Any], Any], float, bool, Union[int, None], Any)->Request - """ - :param symbol: str - :param contractType: OkexFuturesContractType - :param orderType: OkexFuturesOrderType - :param volume: float - :param onSuccess: (orderId: int)->Any - :param onFailed: ()->Any - :param price: float - :param useMarketPrice: bool - :param leverRate: int | None - :param extra: Any - :return: Request - """ - - data = {} - if useMarketPrice: - data['match_price'] = 1 - else: - data['price'] = price - data.update({ - 'symbol': symbol, - 'contract_typ': contractType, # 合约类型:当周/下周/季度 - 'amount': volume, - 'type': orderType, - }) - if leverRate: - data['lever_rate'] = leverRate # 杠杆倍数 - - request = self.addRequest('POST', - '/future_trade.do', - callback=self.onOrderSent, - data=data, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra)) - return request - - #---------------------------------------------------------------------- - def cancelOrder(self, symbol, contractType, orderId, onSuccess, onFailed=None, - extra=None): # type: (str, OkexFuturesContractType, str, Callable[[object], Any], Callable[[int, Any], Any], Any)->Request - """ - :param symbol: str - :param contractType: OkexFuturesContractType - :param orderId: str - :param onSuccess: ()->Any - :param onFailed: ()->Any - :param extra: Any - :return: Request - """ - data = { - 'symbol': symbol, - 'contractType': contractType, - 'order_id': orderId - } - return self.addRequest('POST', - '/future_cancel.do', - callback=self.onOrderCanceled, - data=data, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra)) - - #---------------------------------------------------------------------- - def queryOrder(self, symbol, contractType, orderId, onSuccess, onFailed=None, - extra=None): # type: (str, OkexFuturesContractType, str, Callable[[List[OkexFuturesOrder], Any], Any], Callable[[int, Any], Any], Any)->Request - """ - @note onSuccess接收的第一个参数是列表,并且有可能为空 - - :param symbol: str - :param contractType: OkexFuturesContractType - :param orderId: str - :param onSuccess: (orders: List[OkexFuturesOrder], extra:Any)->Any - :param onFailed: (extra: Any)->Any - :param extra: Any - :return: Request - """ - data = { - 'symbol': symbol, - 'contractType': contractType, - 'order_id': orderId - } - return self.addRequest('POST', - '/future_order_info.do', - callback=self.onOrder, - data=data, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra)) - - #---------------------------------------------------------------------- - def queryOrders(self, symbol, contractType, status, - onSuccess, onFailed=None, - pageIndex=0, pageLength=50, - extra=None): # type: (str, OkexFuturesContractType, OkexFuturesOrderStatus, Callable[[List[OkexFuturesOrder], Any], Any], Callable[[int, Any], Any], int, int, Any)->Request - """ - @note onSuccess接收的第一个参数是列表,并且有可能为空 - - :param symbol: str - :param contractType: OkexFuturesContractType - :param onSuccess: (List[OkexFuturesOrder], extra:Any)->Any - :param onFailed: (extra: Any)->Any - :param pageIndex: 页码 - :param pageLength: 最大显示数量(最大值50) - :param extra: Any - :return: Request - """ - data = { - 'symbol': symbol, - 'contract_type': contractType, - 'status': status, - 'order_id': -1, - 'current_page': pageIndex, - 'page_length': pageLength - } - - return self.addRequest('POST', - '/future_order_info.do', - callback=self.onOrder, - data=data, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra)) - - #---------------------------------------------------------------------- - def queryUserInfo(self, onSuccess, onFailed=None, - extra=None): # type: (Callable[[List[OkexFuturesUserInfo], Any], Any], Callable[[int, Any], Any], Any)->Request - """ - 查询用户信息 - :param onSuccess: (userInfos: List[OkexFuturesUserInfo], extra: Any)->Any - :param onFailed: (extra: Any)->Any - :param extra: Any - :return: Request - """ - return self.addRequest('POST', - '/future_userinfo.do', - callback=self.onOrder, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra)) - - #---------------------------------------------------------------------- - def queryPosition(self, symbol, contractType, - onSuccess, onFailed=None, - extra=None): # type: (str, OkexFuturesContractType, Callable[[OkexFuturesPosition, Any], Any], Callable[[int, Any], Any], Any)->Request - """ - :param symbol: OkexFuturesSymbol - :param contractType: OkexFuturesContractType - :param onSuccess: (pos:OkexFuturesPosition, extra: any)->Any - :param onFailed: (errorCode: int, extra: any)->Any - :param extra: - :return: - """ - data = { - 'symbol': symbol, - 'contractType': contractType - } - return self.addRequest('POST', - '/future_position.do', - data=data, - callback=self.onPosition, - extra=_OkexFuturesCustomExtra(onSuccess, onFailed, extra)) - - #---------------------------------------------------------------------- - @staticmethod - def onOrderSent(data, request): # type: (dict, Request)->None - """ - 下单回执,一般用来保存sysId - """ - extra = request.extra # type: _OkexFuturesCustomExtra - if data['result'] is True: - remoteId = data['order_id'] - extra.onSuccess(remoteId, extra.extra) - else: - if extra.onFailed: - code = 0 - if 'error_code' in data: - code = data['error_code'] - extra.onFailed(code, extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def onOrderCanceled(data, request): # type: (dict, Request)->None - """ - 取消订单回执 - """ - success = data['result'] - extra = request.extra # type: _OkexFuturesCustomExtra - if success: - extra.onSuccess(extra.extra) - else: - if extra.onFailed: - code = 0 - if 'error_code' in data: - code = data['error_code'] - extra.onFailed(code, extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def onOrder(data, request): # type: (dict, Request)->None - success = data['result'] - extra = request.extra # type: _OkexFuturesCustomExtra - if success: - orders = [] - for order in data['orders']: - okexOrder = OkexFuturesOrder() - - okexOrder.volume = order['amount'] - okexOrder.contractName = order['contract_name'] - okexOrder.createDate = order['create_date'] - okexOrder.tradedVolume = order['deal_amount'] - okexOrder.fee = order['fee'] - okexOrder.leverRate = order['lever_rate'] - okexOrder.remoteId = str(order['orderId']) - okexOrder.price = order['price'] - okexOrder.priceAvg = order['price_avg'] - okexOrder.status = order['status'] - okexOrder.orderType = order['type'] - okexOrder.unitAmount = order['unit_amount'] - okexOrder.symbol = order['symbol'] - orders.append(okexOrder) - extra.onSuccess(orders, extra.extra) - else: - if extra.onFailed: - code = 0 - if 'error_code' in data: - code = data['error_code'] - extra.onFailed(code, extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def onUserInfo(data, request): # type: (dict, Request)->None - success = data['result'] - extra = request.extra # type: _OkexFuturesCustomExtra - if success: - infos = data['info'] - uis = [] - for easySymbol, info in infos.items(): # type: str, dict - ui = OkexFuturesUserInfo() - ui.easySymbol = easySymbol - ui.accountRights = info['account_rights'] - ui.keepDeposit = info['keep_deposit'] - ui.profitReal = info['profit_real'] - ui.profitUnreal = info['profit_unreal'] - ui.riskRate = info['risk_rate'] - uis.append(ui) - extra.onSuccess(uis, extra.extra) - else: - if extra.onFailed: - code = 0 - if 'error_code' in data: - code = data['error_code'] - extra.onFailed(code, extra.extra) - - #---------------------------------------------------------------------- - @staticmethod - def onPosition(data, request): # type: (dict, Request)->None - success = data['result'] - extra = request.extra # type: _OkexFuturesCustomExtra - if success: - pos = OkexFuturesPosition() - pos.forceLiquidatePrice = data['force_liqu_price'] - for item in data['holding']: - posDetail = OkexFuturesPositionDetail() - posDetail.buyAmount = item['buy_amount'] - posDetail.buyAvailable = item['buy_available'] - posDetail.buyPriceAvg = item['buy_price_avg'] - posDetail.buyPriceCost = item['buy_price_cost'] - posDetail.buyProfitReal = item['buy_profit_real'] - posDetail.contractId = item['contract_id'] - posDetail.contractType = item['contract_type'] - posDetail.createDate = item['create_date'] - posDetail.leverRate = item['lever_rate'] - posDetail.sellAmount = item['sell_amount'] - posDetail.sellAvailable = item['sell_available'] - posDetail.sellPriceAvg = item['sell_price_avg'] - posDetail.sellPriceCost = item['sell_price_cost'] - posDetail.sellProfitReal = item['sell_profit_real'] - posDetail.symbol = item['symbol'] - pos.holding.append(posDetail) - extra.onSuccess(pos, extra.extra) - else: - if extra.onFailed: - code = 0 - if 'error_code' in data: - code = data['error_code'] - extra.onFailed(code, extra.extra) - - #--------------------------------------------------------------------- - @staticmethod - def errorCodeToString(code): - assert code in restErrorCodeMap - return restErrorCodeMap[code] - - -######################################################################## -class OkexFuturesWebSocketClient(OkexFuturesWebSocketBase): - - #---------------------------------------------------------------------- - def __init__(self): - super(OkexFuturesWebSocketClient, self).__init__() - self.onTick = self.defaultOnTick - self.onUserTrade = self.defaultOnUserTrade - - #---------------------------------------------------------------------- - def subscribe(self, easySymbol, contractType): # type: (OkexFuturesEasySymbol, OkexFuturesContractType)->None - self.sendPacket({ - 'event': 'addChannel', - 'channel': 'ok_sub_futureusd_' + easySymbol + '_ticker_' + contractType - }) - - #---------------------------------------------------------------------- - def subscribeUserTrade(self): - self.sendPacket({ - 'event': 'addChannel', - 'channel': 'ok_sub_futureusd_trades' - }) - - #---------------------------------------------------------------------- - def onPacket(self, packets): - - for packet in packets: - channelName = None - if 'channel' in packet: - channelName = packet['channel'] - if not channelName or channelName == 'addChannel': - return - - packet = packet['data'] - channel = parseChannel(channelName) # type: ExtraSymbolChannel - - if channel.type == ChannelType.Tick: - self.onTick(OkexFuturesTickInfo( - symbol=channel.symbol, - remoteContractType=channel.remoteContractType, - last=packet['last'], - limitHigh=packet['limitHigh'], - limitLow=packet['limitLow'], - vol=packet['vol'], - sell=packet['sell'], - buy=packet['buy'], - unitAmount=packet['unitAmount'], - holdAmount=packet['hold_amount'], - contractId=packet['contractId'], - high=packet['high'], - low=packet['low'], - )) - # elif channel.type == ChannelType.Trade: - # trades = [] - # for tradeInfo in packet: - # trades.append(OkexFuturesTradeInfo( - # channel.symbol, channel.remoteContractType, *tradeInfo - # )) - # self.onTrades(trades) - - elif channel.type == ChannelType.UserTrade: - self.onUserTrade(OkexFuturesUserTradeInfo( - symbol=packet['symbol'], - remoteContractType=packet['contract_type'], - amount=packet['amount'], - contractName=packet['contract_name'], - createdDate=packet['created_date'], - createDateStr=packet['create_date_str'], - dealAmount=packet['deal_amount'], - fee=packet['fee'], - orderId=packet['order_id'], - price=packet['price'], - priceAvg=packet['price_avg'], - status=packet['status'], - type=packet['type'], - unitAmount=packet['unit_amount'], - leverRate=packet['lever_rate'], - systemType=packet['system_type'], - )) - - #---------------------------------------------------------------------- - def defaultOnTick(self, tick): # type: (OkexFuturesTickInfo)->None - pass - - #---------------------------------------------------------------------- - def defaultOnUserTrade(self, tick): # type: (OkexFuturesUserTradeInfo)->None - pass - - -restErrorCodeMap = { - 0: '远程服务器并未给出错误代码', - - 20001: '用户不存在', - 20002: '用户被冻结', - 20003: '用户被爆仓冻结', - 20004: '合约账户被冻结', - 20005: '用户合约账户不存在', - 20006: '必填参数为空', - 20007: '参数错误', - 20008: '合约账户余额为空', - 20009: '虚拟合约状态错误', - 20010: '合约风险率信息不存在', - 20011: '10倍/20倍杠杆开BTC前保证金率低于90%/80%,10倍/20倍杠杆开LTC前保证金率低于80%/60%', - 20012: '10倍/20倍杠杆开BTC后保证金率低于90%/80%,10倍/20倍杠杆开LTC后保证金率低于80%/60%', - 20013: '暂无对手价', - 20014: '系统错误', - 20015: '订单信息不存在', - 20016: '平仓数量是否大于同方向可用持仓数量', - 20017: '非本人操作', - 20018: '下单价格高于前一分钟的103%或低于97%', - 20019: '该IP限制不能请求该资源', - 20020: '密钥不存在', - 20021: '指数信息不存在', - 20022: '接口调用错误(全仓模式调用全仓接口,逐仓模式调用逐仓接口)', - 20023: '逐仓用户', - 20024: 'sign签名不匹配', - 20025: '杠杆比率错误', - 20026: 'API鉴权错误', - 20027: '无交易记录', - 20028: '合约不存在', - 20029: '转出金额大于可转金额', - 20030: '账户存在借款', - 20038: '根据相关法律,您所在的国家或地区不能使用该功能。', - 20049: '用户请求接口过于频繁', - 20061: '合约相同方向只支持一个杠杆,若有10倍多单,就不能再下20倍多单', - 21005: '请求接口失败,请您重试', - 21020: '合约交割中,无法下单', - 21021: '合约清算中,无法下单', - 21023: '当前全仓方向仓位已超过最大可开张数', - 21024: '当前逐仓方向仓位已超过最大可开张数', - 21025: '下单后保证金率小于对应档位要求的最低保证金率', - 21026: '您的账户已被限制开仓操作', - 20119: '接口已下线或无法使用', -} - -webSocketErrorCodeMap = { - 10000: '必填参数为空', - 10001: '参数错误', - 10002: '验证失败', - 10003: '该连接已经请求了其他用户的实时交易数据', - 10004: '该连接没有请求此用户的实时交易数据', - 10005: 'api_key或者sign不合法', - 10008: '非法参数', - 10009: '订单不存在', - 10010: '余额不足', - 10011: '卖的数量小于BTC/LTC最小买卖额度', - 10012: '当前网站暂时只支持btc_usd ltc_usd', - 10014: '下单价格不得≤0或≥1000000', - 10015: '暂不支持此channel订阅', - 10016: '币数量不足', - 10017: 'WebSocket鉴权失败', - 10100: '用户被冻结', - 10049: '小额委托(<0.15BTC)的未成交委托数量不得大于50个', - 10216: '非开放API', - 20001: '用户不存在', - 20002: '用户被冻结', - 20003: '用户被爆仓冻结', - 20004: '合约账户被冻结', - 20005: '用户合约账户不存在', - 20006: '必填参数为空', - 20007: '参数错误', - 20008: '合约账户余额为空', - 20009: '虚拟合约状态错误', - 20010: '合约风险率信息不存在', - 20011: '开仓前保证金率超过90%', - 20012: '开仓后保证金率超过90%', - 20013: '暂无对手价', - 20014: '系统错误', - 20015: '订单信息不存在', - 20016: '平仓数量是否大于同方向可用持仓数量', - 20017: '非本人操作', - 20018: '下单价格高于前一分钟的105%或低于95%', - 20019: '该IP限制不能请求该资源', - 20020: '密钥不存在', - 20021: '指数信息不存在', - 20022: '接口调用错误', - 20023: '逐仓用户', - 20024: 'sign签名不匹配', - 20025: '杠杆比率错误', - 20100: '请求超时', - 20101: '数据格式无效', - 20102: '登录无效', - 20103: '数据事件类型无效', - 20104: '数据订阅类型无效', - 20107: 'JSON格式错误', - 20115: 'quote参数未匹配到', - 20116: '参数不匹配', - 1002: '交易金额大于余额', - 1003: '交易金额小于最小交易值', - 1004: '交易金额小于0', - 1007: '没有交易市场信息', - 1008: '没有最新行情信息', - 1009: '没有订单', - 1010: '撤销订单与原订单用户不一致', - 1011: '没有查询到该用户', - 1013: '没有订单类型', - 1014: '没有登录', - 1015: '没有获取到行情深度信息', - 1017: '日期参数错误', - 1018: '下单失败', - 1019: '撤销订单失败', - 1024: '币种不存在', - 1025: '没有K线类型', - 1026: '没有基准币数量', - 1027: '参数不合法可能超出限制', - 1028: '保留小数位失败', - 1029: '正在准备中', - 1030: '有融资融币无法进行交易', - 1031: '转账余额不足', - 1032: '该币种不能转账', - 1035: '密码不合法', - 1036: '谷歌验证码不合法', - 1037: '谷歌验证码不正确', - 1038: '谷歌验证码重复使用', - 1039: '短信验证码输错限制', - 1040: '短信验证码不合法', - 1041: '短信验证码不正确', - 1042: '谷歌验证码输错限制', - 1043: '登陆密码不允许与交易密码一致', - 1044: '原密码错误', - 1045: '未设置二次验证', - 1046: '原密码未输入', - 1048: '用户被冻结', - 1050: '订单已撤销或者撤销中', - 1051: '订单已完成交易', - 1201: '账号零时删除', - 1202: '账号不存在', - 1203: '转账金额大于余额', - 1204: '不同种币种不能转账', - 1205: '账号不存在主从关系', - 1206: '提现用户被冻结', - 1207: '不支持转账', - 1208: '没有该转账用户', - 1209: '当前api不可用', -} - - -######################################################################## -class ChannelType(Enum): - Login = 1 - ForecastPrice = 2 - Tick = 3 - Depth = 4 - Trade = 5 - Index = 6 - UserTrade = 7 - UserInfo = 8 - - -######################################################################## -class Channel(object): - - #---------------------------------------------------------------------- - def __init__(self, type): - self.type = type - - -######################################################################## -class SymbolChannel(Channel): - - #---------------------------------------------------------------------- - def __init__(self, type, symbol): - super(SymbolChannel, self).__init__(type) - self.symbol = symbol - - -######################################################################## -class FutureSymbolChannel(SymbolChannel): - - #---------------------------------------------------------------------- - def __init__(self, type, symbol, remoteContractType): - super(FutureSymbolChannel, self).__init__(type, symbol) - self.remoteContractType = remoteContractType - - -######################################################################## -class ExtraSymbolChannel(FutureSymbolChannel): - - #---------------------------------------------------------------------- - def __init__(self, type, symbol, remoteContractType, extra): - super(ExtraSymbolChannel, self).__init__(type, symbol, remoteContractType) - self.extra = extra - - -#---------------------------------------------------------------------- -def parseChannel(channel): # type: (str)->Channel - if channel == 'login': - return Channel(ChannelType.Login) - - # 还未提供订阅的channel都注释掉 - # elif channel[4:12] == 'forecast': # eg: 'btc_forecast_price' - # return SymbolChannel(ChannelType.ForecastPrice, channel[:3]) - - sp = channel.split('_') - if sp[-1] == 'trades': # eg: 'ok_sub_futureusd_trades' - return Channel(ChannelType.UserTrade) - # if sp[-1] == 'userinfo': # eg: 'ok_sub_futureusd_btc_userinfo' - # return Channel(ChannelType.UserInfo) - # if sp[-1] == 'index': # eg: 'ok_sub_futureusd_btc_index' - # return SymbolChannel(ChannelType.Index, channel[17:20]) - - # if len(sp) == 9: - # _, _, _, easySymbol, crash, typeName, contractTypePrefix, _, depth = sp - # return ExtraSymbolChannel(ChannelType.Depth, easySymbol + '_' + crash, - # remotePrefixToRemoteContractType(contractTypePrefix), - # depth) - _, _, _, easySymbol, crash, typeName, contractTypePrefix, _ = sp - return FutureSymbolChannel(ChannelType.Tick, easySymbol + '_' + crash, - remotePrefixToRemoteContractType(contractTypePrefix)) - - -#---------------------------------------------------------------------- -def remotePrefixToRemoteContractType(prefix): - return _prefixForRemoteContractType[prefix] - - -_prefixForRemoteContractType = {v.split('_')[0]: v for k, v in OkexFuturesContractType.__dict__.items() if - not k.startswith('_')} diff --git a/vnpy/api/okexfutures/__init__.py b/vnpy/api/okexfutures/__init__.py deleted file mode 100644 index 0b3fb37f..00000000 --- a/vnpy/api/okexfutures/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .OkexFuturesApi import OkexFuturesAccountInfoV3, OkexFuturesContractType, \ - OkexFuturesContractsInfoV3, OkexFuturesOrder, OkexFuturesOrderDetailV3, \ - OkexFuturesOrderSentInfoV3, OkexFuturesOrderStatus, OkexFuturesPosition, \ - OkexFuturesPositionDetail, OkexFuturesPositionInfoV3, OkexFuturesPriceType, \ - OkexFuturesRestClientV1, OkexFuturesRestClientV3, OkexFuturesSymbol, OkexFuturesUserInfo, \ - OkexFuturesWebSocketClient diff --git a/vnpy/api/rest/RestClient.py b/vnpy/api/rest/RestClient.py index a013668d..031c6214 100644 --- a/vnpy/api/rest/RestClient.py +++ b/vnpy/api/rest/RestClient.py @@ -88,7 +88,7 @@ class RestClient(object): self.urlBase = urlBase #---------------------------------------------------------------------- - def _generateSession(self): + def _createSession(self): """""" return requests.session() @@ -127,7 +127,7 @@ class RestClient(object): params=None, # type: dict data=None, # type: dict headers=None, # type: dict - onFailed=None, # type: Callable[[dict, Request], Any] + onFailed=None, # type: Callable[[int, Request], Any] extra=None # type: Any ): # type: (...)->Request """ @@ -151,7 +151,7 @@ class RestClient(object): #---------------------------------------------------------------------- def _run(self): - session = self._generateSession() + session = self._createSession() while self._active: try: request = self._queue.get(timeout=1) diff --git a/vnpy/api/websocket/WebSocketClient.py b/vnpy/api/websocket/WebsocketClient.py similarity index 95% rename from vnpy/api/websocket/WebSocketClient.py rename to vnpy/api/websocket/WebsocketClient.py index 7576d5c0..a2e73382 100644 --- a/vnpy/api/websocket/WebSocketClient.py +++ b/vnpy/api/websocket/WebsocketClient.py @@ -11,7 +11,7 @@ import websocket from threading import Lock, Thread -class WebSocketClient(object): +class WebsocketClient(object): """ Websocket API @@ -36,8 +36,6 @@ class WebSocketClient(object): def __init__(self): """Constructor""" self.host = None # type: str - - self._createConnection = websocket.create_connection self._ws_lock = Lock() self._ws = None # type: websocket.WebSocket @@ -70,7 +68,8 @@ class WebSocketClient(object): """ self._active = False self._disconnect() - + + #---------------------------------------------------------------------- def join(self): """ 等待所有工作线程退出 @@ -100,6 +99,10 @@ class WebSocketClient(object): if self._active: self._disconnect() self._connect() + + #---------------------------------------------------------------------- + def _createConnection(self, *args, **kwargs): + return websocket.create_connection(*args, **kwargs) #---------------------------------------------------------------------- def _connect(self): diff --git a/vnpy/api/websocket/__init__.py b/vnpy/api/websocket/__init__.py index 5c44c43f..3accc1ac 100644 --- a/vnpy/api/websocket/__init__.py +++ b/vnpy/api/websocket/__init__.py @@ -1 +1 @@ -from .WebSocketClient import WebSocketClient +from .WebsocketClient import WebsocketClient diff --git a/vnpy/trader/gateway/okexFuturesGateway/OkexFuturesApi.py b/vnpy/trader/gateway/okexFuturesGateway/OkexFuturesApi.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/vnpy/trader/gateway/okexFuturesGateway/OkexFuturesApi.py @@ -0,0 +1 @@ + diff --git a/vnpy/api/okexfutures/OkexFuturesBase.py b/vnpy/trader/gateway/okexFuturesGateway/OkexFuturesBase.py similarity index 98% rename from vnpy/api/okexfutures/OkexFuturesBase.py rename to vnpy/trader/gateway/okexFuturesGateway/OkexFuturesBase.py index 4a0ab12d..9192cfd0 100644 --- a/vnpy/api/okexfutures/OkexFuturesBase.py +++ b/vnpy/trader/gateway/okexFuturesGateway/OkexFuturesBase.py @@ -8,7 +8,7 @@ import urllib import time from vnpy.api.rest import Request, RestClient -from vnpy.api.websocket import WebSocketClient +from vnpy.api.websocket import WebsocketClient #---------------------------------------------------------------------- @@ -36,6 +36,7 @@ def signV1(dataWithApiKey, apiSecret): def signV3(dataToSign, apiSecret): return base64.b64encode( hmac.new(apiSecret, dataToSign.encode(), hashlib.sha256).digest()) + ######################################################################## class OkexFuturesRestBaseV1(RestClient): host = 'https://www.okex.com/api/v1' @@ -115,7 +116,7 @@ class OkexFuturesRestBaseV3(RestClient): ######################################################################## -class OkexFuturesWebSocketBase(WebSocketClient): +class OkexFuturesWebSocketBase(WebsocketClient): """ Okex期货websocket客户端 实例化后使用init设置apiKey和secretKey(apiSecret) diff --git a/vnpy/trader/gateway/okexFuturesGateway/__init__.py b/vnpy/trader/gateway/okexFuturesGateway/__init__.py index e69de29b..3d841ee1 100644 --- a/vnpy/trader/gateway/okexFuturesGateway/__init__.py +++ b/vnpy/trader/gateway/okexFuturesGateway/__init__.py @@ -0,0 +1,11 @@ +# encoding: UTF-8 + +from __future__ import absolute_import +from vnpy.trader import vtConstant +from .okexFuturesGateway import OkexFuturesGateway + +gatewayClass = OkexFuturesGateway +gatewayName = 'OKEXFUTURES' +gatewayDisplayName = 'OKEXFUTURES' +gatewayType = vtConstant.GATEWAYTYPE_BTC +gatewayQryEnabled = True diff --git a/vnpy/trader/gateway/okexFuturesGateway/okexFutureGateway.py b/vnpy/trader/gateway/okexFuturesGateway/okexFutureGateway.py deleted file mode 100644 index f998b601..00000000 --- a/vnpy/trader/gateway/okexFuturesGateway/okexFutureGateway.py +++ /dev/null @@ -1,420 +0,0 @@ -# encoding: UTF-8 - -from __future__ import print_function - -import json - -from typing import Dict - -from vnpy.api.okexfutures.OkexFuturesApi import * -from vnpy.trader.vtFunction import getJsonPath -from vnpy.trader.vtGateway import * - - -######################################################################## -class _Order(object): - _lastLocalId = 0 - - #---------------------------------------------------------------------- - def __init__(self): - _Order._lastLocalId += 1 - self.localId = str(_Order._lastLocalId) - self.remoteId = None - self.vtOrder = None # type: VtOrderData - - -######################################################################## -class OkexFuturesGateway(VtGateway): - """OKEX期货交易接口""" - - #---------------------------------------------------------------------- - def __init__(self, eventEngine, *_, **__): # args, kwargs is needed for compatibility - """Constructor""" - super(OkexFuturesGateway, self).__init__(eventEngine, 'OkexFuturesGateway') - self.exchange = constant.EXCHANGE_OKEXFUTURE - self.apiKey = None # type: str - self.apiSecret = None # type: str - self.apiPassphrase = None # type: str - - self.restApi = OkexFuturesRestClientV3() - - self.webSocket = OkexFuturesWebSocketClient() - self.webSocket.onTick = self._onTick - self.webSocket.onUserTrade = self._onUserTrade - - self.leverRate = 1 - self.symbols = [] - - self.tradeID = 0 - self._orders = {} # type: Dict[str, _Order] - self._remoteIds = {} # type: Dict[str, _Order] - - #---------------------------------------------------------------------- - def readConfig(self): - """ - 从json文件中读取设置,并将其内容返回为一个dict - :一个一个return: - """ - fileName = self.gatewayName + '_connect.json' - filePath = getJsonPath(fileName, __file__) - - try: - with open(filePath, 'rt') as f: - return json.load(f) - except IOError: - log = VtLogData() - log.gatewayName = self.gatewayName - log.logContent = u'读取连接配置出错,请检查' - # todo: pop a message box is better - self.onLog(log) - return None - - #---------------------------------------------------------------------- - def loadSetting(self): - """载入设置""" - setting = self.readConfig() - if setting: - """连接""" - # 载入json文件 - try: - # todo: check by predefined settings names and types - # or check by validator - self.apiKey = str(setting['apiKey']) - self.apiSecret = str(setting['secretKey']) - self.apiPassphrase = str(setting['passphrase']) - self.leverRate = setting['leverRate'] - self.symbols = setting['symbols'] - except KeyError: - log = VtLogData() - log.gatewayName = self.gatewayName - log.logContent = u'连接配置缺少字段,请检查' - self.onLog(log) - return - - #---------------------------------------------------------------------- - def connect(self): - """连接""" - self.loadSetting() - self.restApi.init(self.apiKey, self.apiSecret, self.apiPassphrase) - self.webSocket.init(self.apiKey, self.apiSecret, self.apiPassphrase) - self.restApi.start() - self.webSocket.start() - - #---------------------------------------------------------------------- - def subscribe(self, subscribeReq): # type: (VtSubscribeReq)->None - """订阅行情""" - remoteSymbol, remoteContractType = localSymbolToRemote(subscribeReq.symbol) - return self.webSocket.subscribe(remoteSymbol, remoteContractType) - - #---------------------------------------------------------------------- - def _getOrderByLocalId(self, localId): - """从本地Id获取对应的内部Order对象""" - if localId in self._orders: - return self._orders[localId] - return None - - #---------------------------------------------------------------------- - def _getOrderByRemoteId(self, remoteId): - """从Api的OrderId获取对应的内部Order对象""" - if remoteId in self._remoteIds: - return self._remoteIds[remoteId] - return None - - #---------------------------------------------------------------------- - def _saveRemoteId(self, remoteId, myorder): - """将remoteId和队友的""" - myorder.remoteId = remoteId - self._remoteIds[remoteId] = myorder - - #---------------------------------------------------------------------- - def _generateLocalOrder(self, symbol, price, volume, direction, offset): - myorder = _Order() - localId = myorder.localId - self._orders[localId] = myorder - myorder.vtOrder = VtOrderData.createFromGateway(self, - self.exchange, - localId, - symbol, - price, - volume, - direction, - offset) - return myorder - - #---------------------------------------------------------------------- - def sendOrder(self, vtRequest): # type: (VtOrderReq)->str - """发单""" - myorder = self._generateLocalOrder(vtRequest.symbol, - vtRequest.price, - vtRequest.volume, - vtRequest.direction, - vtRequest.offset) - - orderType = _orderTypeMap[(vtRequest.direction, vtRequest.offset)] # 开多、开空、平多、平空 - userMarketPrice = False - - if vtRequest.priceType == constant.PRICETYPE_MARKETPRICE: - userMarketPrice = True - - self.restApi.sendOrder(symbol=vtRequest.symbol, - orderType=orderType, - volume=vtRequest.volume, - price=vtRequest.price, - matchPrice=userMarketPrice, - leverRate=self.leverRate, - onSuccess=self._onOrderSent, - onFailed=self._onSendOrderFailed, - extra=myorder, - ) - return myorder.localId - - #---------------------------------------------------------------------- - def cancelOrder(self, vtCancel): # type: (VtCancelOrderReq)->None - """撤单""" - myorder = self._getOrderByLocalId(vtCancel.orderID) - assert myorder is not None, u"理论上是无法取消一个不存在的本地单的" - - self.restApi.cancelOrder(vtCancel.symbol, - myorder.remoteId, - onSuccess=self._onOrderCanceled, - extra=myorder - ) - - #---------------------------------------------------------------------- - def queryContracts(self): - self.restApi.queryContracts(onSuccess=self._onQueryContracts) - - #---------------------------------------------------------------------- - def queryOrders(self, symbol, status): # type: (str, OkexFuturesOrderStatus)->None - """ - :param symbol: - :param status: OkexFuturesOrderStatus - :return: - """ - - self.restApi.queryOrders(symbol=symbol, - status=status, - onSuccess=self._onQueryOrders, - ) - - #---------------------------------------------------------------------- - def qryAccount(self): - self.restApi.queryAccount(onSuccess=self._onQueryAccount) - """查询账户资金""" - pass - - #---------------------------------------------------------------------- - def qryPosition(self): - """查询持仓""" - self.restApi.queryPositions(onSuccess=self._onQueryPosition) - - #---------------------------------------------------------------------- - def close(self): - """关闭""" - self.restApi.stop() - self.webSocket.stop() - - #---------------------------------------------------------------------- - def _onOrderSent(self, order, myorder): #type: (OkexFuturesOrderSentInfoV3, _Order)->None - myorder.remoteId = order.orderId - myorder.vtOrder.status = constant.STATUS_NOTTRADED - self._saveRemoteId(myorder.remoteId, myorder) - self.onOrder(myorder.vtOrder) - - #---------------------------------------------------------------------- - @staticmethod - def _onSendOrderFailed(order, myorder): #type: (OkexFuturesOrderSentInfoV3, _Order)->None - myorder.vtOrder.status = constant.STATUS_REJECTED - - #---------------------------------------------------------------------- - @staticmethod - def _onOrderCanceled(myorder): # type: (_Order)->Any - myorder.vtOrder.status = constant.STATUS_CANCELLED - - #---------------------------------------------------------------------- - def _onQueryContracts(self, contracts, extra): # type: (List[OkexFuturesContractsInfoV3], Any)->None - for contract in contracts: - vtContract = VtContractData.createFromGateway( - gateway=self, - exchange=self.exchange, - symbol=contract.symbol, - productClass=constant.PRODUCT_FUTURES, - priceTick=contract.tickSize, - size=contract.quoteIncrement, - name=contract.symbol, - expiryDate=contract.delivery, - underlyingSymbol=contract.underlyingIndex - ) - self.onContract(vtContract) - - #---------------------------------------------------------------------- - def _onQueryOrders(self, orders, extra): # type: (List[OkexFuturesOrderDetailV3], Any)->None - localContractType = extra - for order in orders: - remoteId = order.remoteId - - myorder = self._getOrderByRemoteId(remoteId) - if myorder: - # 如果订单已经缓存在本地,则尝试更新订单状态 - - # 有新交易才推送更新 - if order.tradedVolume != myorder.vtOrder.tradedVolume: - myorder.vtOrder.tradedVolume = order.tradedVolume - myorder.vtOrder.status = constant.STATUS_PARTTRADED - self.onOrder(myorder.vtOrder) - else: - # 本地无此订单的缓存(例如,用其他工具的下单) - # 缓存该订单,并推送 - symbol = remoteSymbolToLocal(order.symbol, localContractType) - direction, offset = remoteOrderTypeToLocal(order.orderType) - myorder = self._generateLocalOrder(symbol, order.price, order.volume, direction, offset) - myorder.vtOrder.tradedVolume = order.tradedVolume - myorder.remoteId = order.remoteId - self._saveRemoteId(myorder.remoteId, myorder) - self.onOrder(myorder.vtOrder) - - #---------------------------------------------------------------------- - def _onQueryAccount(self, infos, _): # type: (List[OkexFuturesAccountInfoV3], Any)->None - for info in infos: - vtAccount = VtAccountData() - vtAccount.accountID = info.currency - vtAccount.vtAccountID = self.gatewayName + '.' + vtAccount.accountID - vtAccount.balance = info.balance - vtAccount.available = info.available - vtAccount.margin = info.hold # todo: is this right? - self.onAccount(vtAccount) - - #---------------------------------------------------------------------- - def _onQueryPosition(self, posex, extra): # type: (List[OkexFuturesPositionInfoV3], Any)->None - localContractType = extra - for pos in posex: - # 多头持仓 - posex = VtPositionData.createFromGateway( - gateway=self, - exchange=self.exchange, - symbol=remoteSymbolToLocal(pos.symbol, localContractType), - direction=constant.DIRECTION_NET, - position=float(pos.longQty), - price=pos.longAvgCost, - ) - - self.onPosition(posex) - - # 空头持仓 - posex = VtPositionData.createFromGateway( - gateway=self, - exchange=self.exchange, - symbol=remoteSymbolToLocal(pos.symbol, localContractType), - direction=constant.DIRECTION_SHORT, - position=float(pos.shortQty), - price=pos.shortAvgCost, - ) - - self.onPosition(posex) - - #---------------------------------------------------------------------- - def _onTick(self, info): # type: (OkexFuturesTickInfo)->None - uiSymbol = remoteSymbolToLocal(info.symbol, remoteContractTypeToLocal(info.remoteContractType)) - self.onTick(VtTickData.createFromGateway( - gateway=self, - symbol=uiSymbol, - exchange=self.exchange, - lastPrice=info.last, - lastVolume=info.vol, - highPrice=info.high, - lowPrice=info.low, - openInterest=info.holdAmount, - lowerLimit=info.limitLow, - upperLimit=info.limitHigh, - )) - - def _onUserTrade(self, info): # type: (OkexFuturesUserTradeInfo)->None - tradeID = str(self.tradeID) - self.tradeID += 1 - order = self._getOrderByRemoteId(info.remoteId) - if order: - self.onTrade(VtTradeData.createFromOrderData( - order=order.vtOrder, - tradeID=tradeID, - tradePrice=info.price, - tradeVolume=info.dealAmount # todo: 这里应该填写的到底是order总共成交了的数量,还是该次trade成交的数量 - )) - else: - # todo: 与order无关联的trade该如何处理? - # uiSymbol = remoteSymbolToLocal(info.symbol, remoteContractTypeToLocal(info.remoteContractType)) - pass - return - - -#---------------------------------------------------------------------- -def localOrderTypeToRemote(direction, offset): # type: (str, str)->str - return _orderTypeMap[(direction, offset)] - - -#---------------------------------------------------------------------- -def remoteOrderTypeToLocal(orderType): # type: (str)->(str, str) - """ - :param orderType: - :return: direction, offset - """ - return _orderTypeMapReverse[orderType] - - -#---------------------------------------------------------------------- -def localContractTypeToRemote(localContractType): - return _contractTypeMap[localContractType] - - -#---------------------------------------------------------------------- -def remoteContractTypeToLocal(remoteContractType): - return _contractTypeMapReverse[remoteContractType] - - -#---------------------------------------------------------------------- -def localSymbolToRemote(symbol): # type: (str)->(OkexFuturesSymbol, OkexFuturesContractType) - """ - :return: remoteSymbol, remoteContractType - """ - return _symbolsForUi[symbol] - - -#---------------------------------------------------------------------- -def remoteSymbolToLocal(remoteSymbol, localContractType): - return remoteSymbol.upper() + '_' + localContractType - - -_orderTypeMap = { - (constant.DIRECTION_LONG, constant.OFFSET_OPEN): OkexFuturesOrderType.OpenLong, - (constant.DIRECTION_SHORT, constant.OFFSET_OPEN): OkexFuturesOrderType.OpenShort, - (constant.DIRECTION_LONG, constant.OFFSET_CLOSE): OkexFuturesOrderType.CloseLong, - (constant.DIRECTION_SHORT, constant.OFFSET_CLOSE): OkexFuturesOrderType.CloseShort, -} -_orderTypeMapReverse = {v: k for k, v in _orderTypeMap.items()} - -_contractTypeMap = { - k.upper(): v for k, v in OkexFuturesContractType.__dict__.items() if not k.startswith('_') -} -_contractTypeMapReverse = {v: k for k, v in _contractTypeMap.items()} - -_easySymbols = { - v for k, v in OkexFuturesEasySymbol.__dict__.items() if not k.startswith('_') -} - -_remoteSymbols = { - v for k, v in OkexFuturesSymbol.__dict__.items() if not k.startswith('_') -} - -# symbols for ui, -# keys:给用户看的symbols : f"{internalSymbol}_{contractType}" -# values: API接口使用的symbol和contractType字段 -_symbolsForUi = {(remoteSymbol.upper() + '_' + upperContractType.upper()): (remoteSymbol, remoteContractType) - for remoteSymbol in _remoteSymbols - for upperContractType, remoteContractType in - _contractTypeMap.items()} # type: Dict[str, List[str, str]] -_symbolsForUiReverse = {v: k for k, v in _symbolsForUi.items()} - -_channel_for_subscribe = { - 'ok_sub_futureusd_' + easySymbol + '_ticker_' + remoteContractType: (easySymbol, remoteContractType) - for easySymbol in _easySymbols - for remoteContractType in _contractTypeMap.values() -} diff --git a/vnpy/trader/gateway/okexFuturesGateway/okexFuturesGateway.py b/vnpy/trader/gateway/okexFuturesGateway/okexFuturesGateway.py new file mode 100644 index 00000000..6fd22958 --- /dev/null +++ b/vnpy/trader/gateway/okexFuturesGateway/okexFuturesGateway.py @@ -0,0 +1,836 @@ +# encoding: UTF-8 + +from __future__ import print_function + +import json +import sys +import traceback +from collections import defaultdict + +from enum import Enum +from typing import Dict, List + +from vnpy.api.rest import Request +from vnpy.trader.gateway.okexFuturesGateway.OkexFuturesBase import \ + OkexFuturesRestBaseV3, \ + OkexFuturesWebSocketBase +from vnpy.trader.vtFunction import getJsonPath +from vnpy.trader.vtGateway import * + + +######################################################################## +class ApiError(Exception): + """Okex的API常常变动,当API发生奇葩的变动的时候,会发升这个异常""" + pass + + +######################################################################## +class OkexFuturesEasySymbol(object): + BTC = 'btc' + LTC = 'ltc' + ETH = 'eth' + ETC = 'etc' + BCH = 'bch' + EOS = 'eos' + XRP = 'xrp' + BTG = 'btg' + + +######################################################################## +class OkexFuturesSymbol(object): + BTC = 'btc_usd' + LTC = 'ltc_usd' + ETH = 'eth_usd' + ETC = 'etc_usd' + BCH = 'bch_usd' + + +######################################################################## +class OkexFuturesPriceType(object): + Buy = 'buy' + Sell = 'sell' + + +######################################################################## +class OkexFuturesContractType(object): + ThisWeek = 'this_week' + NextWeek = 'next_week' + Quarter = 'quarter' + + +######################################################################## +class OkexFuturesOrderType(object): + OpenLong = '1' + OpenShort = '2' + CloseLong = '3' + CloseShort = '4' + + +######################################################################## +class OkexFuturesOrderStatus(object): + NotTraded = 0 + PartialTraded = 1 + Finished = 2 + + +######################################################################## +class Order(object): + _lastLocalId = 0 + + #---------------------------------------------------------------------- + def __init__(self): + Order._lastLocalId += 1 + self.localId = str(Order._lastLocalId) + self.remoteId = None + self.vtOrder = None # type: VtOrderData + + +######################################################################## +class Symbol(object): + + #---------------------------------------------------------------------- + def __init__(self): + self.v3 = None # type: str # BTC_USD_1891201 + self.ui = None # type: str # BTC_USD_NEXTWEEK + self.v1Symbol = None # type: str # btc_usd + self.uiSymbol = None # type: str # btc_usd + self.easySymbol = None # type: str # btc + self.localContractTYpe = None # type: str # THISWEEK + self.remoteContractType = None # type: str # this_week + + +######################################################################## +class ChannelType(Enum): + Login = 1 + ForecastPrice = 2 + Tick = 3 + Depth = 4 + Trade = 5 + Index = 6 + UserTrade = 7 + UserInfo = 8 + Position = 9 + Order = 10 + + +######################################################################## +class Channel(object): + + #---------------------------------------------------------------------- + def __init__(self, type_, symbol=None, remoteContractType=None, extra_=None): + self.type = type_ + self.symbol = symbol + self.remoteContractType = remoteContractType + self.extra = extra_ + + +######################################################################## +class OkexFuturesGateway(VtGateway): + """OKEX期货交易接口""" + + #---------------------------------------------------------------------- + def __init__(self, eventEngine, gatewayName='OKEXFUTURES'): + """Constructor""" + super(OkexFuturesGateway, self).__init__(eventEngine, gatewayName) + self.exchange = constant.EXCHANGE_OKEXFUTURE + self.apiKey = None # type: str + self.apiSecret = None # type: str + self.apiPassphrase = None # type: str + + self.restClient = OkexFuturesRestBaseV3() + + self.webSocket = OkexFuturesWebSocketBase() + self.webSocket.onPacket = self.onWebSocketPacket + + self.leverRate = 1 + self.symbols = [] + + self._symbolDict = {} # type: Dict[str, Symbol] + + self.tradeID = 0 + self._orders = {} # type: Dict[str, Order] + self._remoteIds = {} # type: Dict[str, Order] + + #---------------------------------------------------------------------- + def readConfig(self): + """ + 从json文件中读取设置,并将其内容返回为一个dict + :一个一个return: + """ + fileName = self.gatewayName + '_connect.json' + filePath = getJsonPath(fileName, __file__) + + try: + with open(filePath, 'rt') as f: + return json.load(f) + except IOError: + log = VtLogData() + log.gatewayName = self.gatewayName + log.logContent = u'读取连接配置出错,请检查' + # todo: pop a message box is better + self.onLog(log) + return None + + #---------------------------------------------------------------------- + def loadSetting(self): + """载入设置""" + setting = self.readConfig() + if setting: + """连接""" + # 载入json文件 + try: + # todo: check by predefined settings names and types + # or check by validator + self.apiKey = str(setting['apiKey']) + self.apiSecret = str(setting['secretKey']) + self.apiPassphrase = str(setting['passphrase']) + self.leverRate = setting['leverRate'] + self.symbols = setting['symbols'] + except KeyError: + log = VtLogData() + log.gatewayName = self.gatewayName + log.logContent = u'连接配置缺少字段,请检查' + self.onLog(log) + return + + #---------------------------------------------------------------------- + def connect(self): + """连接""" + self.loadSetting() + self.restClient.init(self.apiKey, self.apiSecret, self.apiPassphrase) + self.webSocket.init(self.apiKey, self.apiSecret, self.apiPassphrase) + self.restClient.start() + self.webSocket.start() + + self.queryContracts() + + #---------------------------------------------------------------------- + def subscribe(self, subscribeReq): # type: (VtSubscribeReq)->None + """订阅行情""" + s = self.parseSymbol(subscribeReq.symbol) + remoteSymbol = s.v1Symbol.lower() + remoteContractType = s.remoteContractType + + self.webSocket.sendPacket({ + 'event': 'addChannel', + 'channel': 'ok_sub_futureusd_' + remoteSymbol.lower() + '_ticker_' + remoteContractType + }) + + #---------------------------------------------------------------------- + def subscribeUserTrade(self): + self.webSocket.sendPacket({ + 'event': 'addChannel', + 'channel': 'ok_sub_futureusd_trades' + }) + + #---------------------------------------------------------------------- + def _writeError(self, msg): # type: (str)->None + e = VtErrorData() + e.gatewayName = self.gatewayName + e.errorMsg = msg + self.onError(e) + + #---------------------------------------------------------------------- + def _getOrderByLocalId(self, localId): + """从本地Id获取对应的内部Order对象""" + if localId in self._orders: + return self._orders[localId] + return None + + #---------------------------------------------------------------------- + def _getOrderByRemoteId(self, remoteId): + """从Api的OrderId获取对应的内部Order对象""" + if remoteId in self._remoteIds: + return self._remoteIds[remoteId] + return None + + #---------------------------------------------------------------------- + def _saveRemoteId(self, remoteId, myorder): + """将remoteId和队友的""" + myorder.remoteId = remoteId + self._remoteIds[remoteId] = myorder + + #---------------------------------------------------------------------- + def _generateLocalOrder(self, symbol, price, volume, direction, offset): + myorder = Order() + localId = myorder.localId + self._orders[localId] = myorder + myorder.vtOrder = VtOrderData.createFromGateway(self, + localId, + symbol, + self.exchange, + price, + volume, + direction, + offset) + return myorder + + #---------------------------------------------------------------------- + def parseSymbol(self, symbol): + return self._symbolDict[symbol] + + #---------------------------------------------------------------------- + def sendOrder(self, vtRequest): # type: (VtOrderReq)->str + """发单""" + symbol = self.parseSymbol(vtRequest.symbol).v3 + myorder = self._generateLocalOrder(symbol, + vtRequest.price, + vtRequest.volume, + vtRequest.direction, + vtRequest.offset) + + orderType = _orderTypeMap[(vtRequest.direction, vtRequest.offset)] # 开多、开空、平多、平空 + + data = { + 'client_oid': None, + 'instrument_id': symbol, + 'type': orderType, + 'size': vtRequest.volume, + 'leverage': self.leverRate, + } + if vtRequest.priceType == constant.PRICETYPE_MARKETPRICE: + data['match_price'] = '1' + else: + data['price'] = vtRequest.price + + self.restClient.addRequest('POST', '/api/futures/v3/order', + callback=self._onOrderSent, + onFailed=self._onSendOrderFailed, + data=data, + extra=myorder + ) + return myorder.vtOrder.vtOrderID + + #---------------------------------------------------------------------- + def cancelOrder(self, vtCancel): # type: (VtCancelOrderReq)->None + """撤单""" + myorder = self._getOrderByLocalId(vtCancel.orderID) + assert myorder is not None, u"理论上是无法取消一个不存在的本地单的" + + symbol = vtCancel.symbol + remoteId = myorder.remoteId + + path = '/api/futures/v3/cancel_order/' + symbol + '/' + remoteId + self.restClient.addRequest('POST', path, + callback=self._onOrderCanceled, + onFailed=self._onCancelOrderFailed, + data={ + 'instrument_id': symbol, + 'order_id': remoteId + }, + extra=myorder + ) + + #---------------------------------------------------------------------- + def queryContracts(self): + return self.restClient.addRequest('GET', '/api/futures/v3/instruments', + callback=self._onQueryContracts, + ) + + #---------------------------------------------------------------------- + def queryOrders(self, symbol, status): # type: (str, OkexFuturesOrderStatus)->None + """ + :param symbol: + :param status: OkexFuturesOrderStatus + :return: + """ + symbol = self.parseSymbol(symbol).v3 + path = '/api/futures/v3/orders/' + symbol + self.restClient.addRequest("POST", path, + data={ + 'status': status, + 'instrument_id': symbol, + # 'from': 0, + # 'to': 2, + # 'limit': 100, + }, + callback=self._onQueryOrders, + ) + + #---------------------------------------------------------------------- + def qryAccount(self): + """查询账户资金 + Okex 的API变化太大,不单独实现API了,所有东西都封装在这里面 + """ + return self.restClient.addRequest('GET', '/api/futures/v3/accounts', + callback=self._onQryAccounts, + ) + + #---------------------------------------------------------------------- + def _onQryAccounts(self, data, _): + if 'info' not in data: + raise ApiError("unable to parse account data") + + for easySymbol, detail in data['info'].items(): # type: str, dict + acc = VtAccountData() + acc.gatewayName = self.gatewayName + acc.accountID = easySymbol + acc.vtAccountID = acc.gatewayName + '.' + acc.accountID + + acc.balance = detail.get('equity', 0) + acc.available = detail['total_avail_balance'] + if 'contracts' in detail: + keys = {'available_qty': 'available_qty', + 'fixed_balance': 'fixed_balance', + 'margin_for_unfilled': 'margin', + 'margin_frozen': 'margin', + 'realized_pnl': 'realized_pnl', + 'unrealized_pnl': 'unrealized_pnl'} + for v in keys.values(): + detail[v] = 0.0 + for c in detail['contracts']: + for k, v in keys.items(): + detail[v] += float(c[k]) + acc.margin = detail['margin'] + acc.positionProfit = data.get('unrealized_pnl', 0) + acc.closeProfit = data.get('realized_pnl', 0) + self.onAccount(acc) + + #---------------------------------------------------------------------- + def qryPosition(self): + """查询持仓""" + return self.restClient.addRequest('GET', '/api/futures/v3/position', + callback=self._onQueryPosition, + ) + + #---------------------------------------------------------------------- + def close(self): + """关闭""" + self.restClient.stop() + self.webSocket.stop() + + #---------------------------------------------------------------------- + def _onOrderSent(self, data, request): #type: (dict, Request)->None + """下单回调""" + # errorCode = data['error_code'], + # errorMessage = data['error_message'], + myorder = request.extra # type: Order + remoteId = data['order_id'] + if remoteId != '-1': + myorder.remoteId = remoteId + myorder.vtOrder.status = constant.STATUS_NOTTRADED + self._saveRemoteId(myorder.remoteId, myorder) + else: + myorder.vtOrder.status = constant.STATUS_REJECTED + self.onOrder(myorder.vtOrder) + + #---------------------------------------------------------------------- + def _onSendOrderFailed(self, _, request): # type:(int, Request)->None + myorder = request.extra # type: Order + myorder.vtOrder.status = constant.STATUS_REJECTED + self.onOrder(myorder.vtOrder) + + #---------------------------------------------------------------------- + def _onOrderCanceled(self, data, request): #type: (dict, Request)->None + myorder = request.extra # type: Order + result = data['result'] + if result is True: + myorder.vtOrder.status = constant.STATUS_CANCELLED + self.onOrder(myorder.vtOrder) + else: + # todo: more detail about error + print('failed to cancel order: ' + json.dumps(data)) + self._writeError('Failed to cancel order {}'.format(myorder.localId)) + + #---------------------------------------------------------------------- + def _onCancelOrderFailed(self, _, request): # type:(int, Request)->None + myorder = request.extra # type: Order + self._writeError(u'Failed to cancel order {}'.format(myorder.localId)) + + #---------------------------------------------------------------------- + def _onQueryContracts(self, data, _): #type: (dict, Request)->None + + # 首先建立THISWEEK, NEXTWEEK, QUARTER和相应Contract的映射 + symbols = set() + for contract in data: + symbol = contract['instrument_id'] + symbols.add(symbol) + + # 一般来说,一个币种对有三种到期日期不同的symbol。 + # 将这三种symbol按顺序排列,就能依次得到ThisWeek, NextWeek和Quarter三种symbol + s2 = defaultdict(list) + for symbol in sorted(symbols): + easySymbol = symbol[:3] + s2[easySymbol].append(symbol) + + # 按顺序取出排列好的symbols,对应上ThisWeek, NextWeek和Quarter + # 然后记录下来他们的几种symbols形式和相应的一些常量: + # v1Symbol: BTC_USD_THISWEEK + # v3Symbol: BTC_USD_181222 + # easySymbol: btc, eth, ... + # remoteContractType: this_week, next_week, ... + # localContractType: THISWEEK, NEXTWEEK, ... + + symbolDict = {} + for easySymbol, sortedSymbols in s2.items(): + if len(sortedSymbols) == 3: + for contractType, v3symbol in zip(_contractTypeMap.keys(), sortedSymbols): + uiSymbol = '{}_USD_{}'.format(easySymbol, contractType) # ETC_USD_THISWEEK + remoteContractName = '{}{}'.format(easySymbol, v3symbol[-4:]) # ETC1201 + + s = Symbol() + s.v1Symbol = '{}_{}'.format(easySymbol.lower(), "usd") + s.v3 = v3symbol + s.easySymbol = easySymbol + s.remoteContractType = _contractTypeMap[contractType] + s.localContractTYpe = contractType + s.uiSymbol = uiSymbol + + # normal map + symbolDict[uiSymbol.upper()] = s + symbolDict[uiSymbol.lower()] = s + symbolDict[uiSymbol] = s + symbolDict[v3symbol] = s + + # switch between '-' and '_' + symbolDict[uiSymbol.upper().replace('_', '-')] = s + symbolDict[uiSymbol.lower().replace('_', '-')] = s + symbolDict[uiSymbol.replace('_', '-')] = s + symbolDict[v3symbol.replace('-', '_')] = s + + # BTCUSD181228 BTCUSDTHISWEEK, btcusdthisweek + symbolDict[v3symbol.upper().replace('-', '')] = s + symbolDict[uiSymbol.upper().replace('_', '')] = s + symbolDict[uiSymbol.lower().replace('_', '')] = s + + symbolDict[remoteContractName.upper()] = s + symbolDict[remoteContractName.lower()] = s + + # unicode and str + for k, v in symbolDict.items(): + self._symbolDict[str(k)] = v + self._symbolDict[unicode(k)] = v + + # 其次响应onContract,也是该函数的本职工作 + for contract in data: + symbol = contract['instrument_id'] + size = contract['quote_increment'] if 'quote_increment' in contract else contract[ + 'trade_increment'], + vtContract = VtContractData.createFromGateway( + gateway=self, + exchange=self.exchange, + symbol=symbol, + productClass=constant.PRODUCT_FUTURES, + priceTick=contract['tick_size'], + size=size, + name=symbol, + expiryDate=contract['delivery'], + underlyingSymbol=contract['underlying_index'], + ) + self.onContract(vtContract) + + # 最后订阅symbols,还有查询其他东西 + for symbol in self.symbols: + s = self.parseSymbol(symbol) + # noinspection PyTypeChecker + req = VtSubscribeReq() + req.symbol = s.v3 + self.subscribe(req) + + # 查询账户啊,持仓啊,委托单啊之类的东西 + self.qryAccount() + self.qryPosition() + + # 查询所有未成交的委托 + # v3 API尚未支持该操作 + # for symbol in symbols: + # # noinspection PyTypeChecker + # self.queryOrders(symbol, OkexFuturesOrderStatus.NotTraded) + + #---------------------------------------------------------------------- + def _onQueryOrders(self, data, _): #type: (dict, Request)->None + if data['result'] is True: + for info in data['orders']: + remoteId = info['order_id'] + tradedVolume = info['filled_qty'] + + myorder = self._getOrderByRemoteId(remoteId) + if myorder: + # 如果订单已经缓存在本地,则尝试更新订单状态 + + # 有新交易才推送更新 + if tradedVolume != myorder.vtOrder.tradedVolume: + myorder.vtOrder.tradedVolume = tradedVolume + myorder.vtOrder.status = constant.STATUS_PARTTRADED + self.onOrder(myorder.vtOrder) + else: + # 本地无此订单的缓存(例如,用其他工具的下单) + # 缓存该订单,并推送 + symbol = info['instrument_id'] + direction, offset = remoteOrderTypeToLocal(info['type']) + myorder = self._generateLocalOrder(symbol, + info['price'], + info['size'], + direction, + offset) + myorder.vtOrder.tradedVolume = tradedVolume + myorder.remoteId = remoteId + self._saveRemoteId(myorder.remoteId, myorder) + self.onOrder(myorder.vtOrder) + + #---------------------------------------------------------------------- + def _onQueryPosition(self, data, _): #type: (dict, Request)->None + if 'holding' in data: + posex = data['holding'] + elif 'position' in data: + posex = data['position'] + else: + raise ApiError("Failed to parse position data") + for pos in posex: + symbol = self.parseSymbol(pos['instrument_id']).uiSymbol + + # 多头持仓 + vtPos = VtPositionData.createFromGateway( + gateway=self, + exchange=self.exchange, + symbol=symbol, + direction=constant.DIRECTION_NET, + position=pos['long_qty'], + price=pos['long_avg_cost'], + ) + self.onPosition(vtPos) + + # 多头持仓 + vtPos = VtPositionData.createFromGateway( + gateway=self, + exchange=self.exchange, + symbol=symbol, + direction=constant.DIRECTION_NET, + position=pos['short_qty'], + price=pos['short_avg_cost'], + ) + self.onPosition(vtPos) + + #---------------------------------------------------------------------- + def onWebSocketPacket(self, packets): + for packet in packets: + channelName = None + if 'channel' in packet: + channelName = packet['channel'] + if not channelName or channelName == 'addChannel': + return + + data = packet['data'] + channel = parseChannel(channelName) # type: Channel + if not channel: + print("unknown websocket channel : ", json.dumps(packet, indent=2)) + return + try: + if channel.type == ChannelType.Tick: + uiSymbol = remoteSymbolToLocal(channel.symbol, + remoteContractTypeToLocal( + channel.remoteContractType)) + self.onTick(VtTickData.createFromGateway( + gateway=self, + symbol=uiSymbol, + exchange=self.exchange, + lastPrice=data['last'], + lastVolume=data['vol'], + highPrice=data['high'], + lowPrice=data['low'], + openInterest=data['hold_amount'], + lowerLimit=data['limitLow'], + upperLimit=data['limitHigh'], + )) + elif channel.type == ChannelType.Position: + symbol = data['symbol'] + positions = data['positions'] + for pos in positions: + if pos['position'] == '1': + direction = constant.DIRECTION_LONG + else: + direction = constant.DIRECTION_SHORT + total = pos['hold_amount'] + usable = pos['eveningup'] + # margin = _tryGetValue(pos, 'margin', 'fixmargin') + profit = _tryGetValue(pos, 'profitreal', 'realized') + symbol = self.parseSymbol(pos['contract_name']).uiSymbol + self.onPosition(VtPositionData.createFromGateway( + gateway=self, + exchange=self.exchange, + symbol=symbol, + direction=direction, + position=total, + frozen=total - usable, + price=pos['avgprice'], + profit=profit, + )) + elif channel.type == ChannelType.UserInfo: + # ws 的acc没有分货币,没法用 + pass + elif channel.type == ChannelType.UserTrade: + tradeID = str(self.tradeID) + self.tradeID += 1 + order = self._getOrderByRemoteId(data['orderid']) + if order: + self.onTrade(VtTradeData.createFromOrderData( + order=order.vtOrder, + tradeID=tradeID, + tradePrice=data['price'], + + # todo: 这里应该填写的到底是order总共成交了的数量,还是该次trade成交的数量 + tradeVolume=data['deal_amount'], + )) + else: + # todo: 与order无关联的trade该如何处理? + # uiSymbol = remoteSymbolToLocal(info.symbol, + # remoteContractTypeToLocal(info.remoteContractType)) + pass + except KeyError: + print("WebSocket error: parsing {}:\n{}".format(channelName, data)) + traceback.print_exception(*sys.exc_info()) + + #---------------------------------------------------------------------- + # noinspection PyUnusedLocal + def onApiError(self, exceptionType, exceptionValue, tb, + request=None # type: Request + ): + msg = traceback.format_exception(exceptionType, exceptionValue, tb) + self._writeError(msg) + + #---------------------------------------------------------------------- + def onApiFailed(self, _, request): # type:(int, Request)->None + self._writeError(str(request)) + pass + + #---------------------------------------------------------------------- + def setQryEnabled(self, _): + """dummy function""" + pass + + +#---------------------------------------------------------------------- +def localOrderTypeToRemote(direction, offset): # type: (str, str)->str + return _orderTypeMap[(direction, offset)] + + +#---------------------------------------------------------------------- +def remoteOrderTypeToLocal(orderType): # type: (str)->(str, str) + """ + :param orderType: + :return: direction, offset + """ + return _orderTypeMapReverse[orderType] + + +#---------------------------------------------------------------------- +def localContractTypeToRemote(localContractType): + return _contractTypeMap[localContractType] + + +#---------------------------------------------------------------------- +def remoteContractTypeToLocal(remoteContractType): + return _contractTypeMapReverse[remoteContractType] + + +#---------------------------------------------------------------------- +def localSymbolToRemote(symbol): # type: (str)->(OkexFuturesSymbol, OkexFuturesContractType) + """ + :return: remoteSymbol, remoteContractType + """ + return _symbolsForUi[symbol] + + +#---------------------------------------------------------------------- +def remoteSymbolToLocal(remoteSymbol, localContractType): + return remoteSymbol.upper() + '_' + localContractType + + +#---------------------------------------------------------------------- +def remotePrefixToRemoteContractType(prefix): + return _prefixForRemoteContractType[prefix] + + +#---------------------------------------------------------------------- +def parseChannel(channel): # type: (str)->Channel + if channel == 'login': + return Channel(ChannelType.Login) + + # 还未提供订阅的channel都注释掉 + # elif channel[4:12] == 'forecast': # eg: 'btc_forecast_price' + # return SymbolChannel(ChannelType.ForecastPrice, channel[:3]) + + sp = channel.split('_') + if sp[-1] == 'trades': # eg: 'ok_sub_futureusd_trades' + return Channel(ChannelType.UserTrade) + # if sp[-1] == 'userinfo': # eg: 'ok_sub_futureusd_btc_userinfo' + # return Channel(ChannelType.UserInfo) + # if sp[-1] == 'index': # eg: 'ok_sub_futureusd_btc_index' + # return SymbolChannel(ChannelType.Index, channel[17:20]) + if sp[-1] == 'positions': # eg: 'ok_sub_futureusd_positions' + return Channel(ChannelType.Position) + + if sp[-1] == 'userinfo': # eg: 'ok_sub_futureusd_positions' + return Channel(ChannelType.UserInfo) + + lsp = len(sp) + if sp[-1] == 'quarter': + if lsp == 7: + _, _, _, easySymbol, crash, typeName, contractTypePrefix = sp + return Channel(ChannelType.Tick, + easySymbol + '_' + crash, + remotePrefixToRemoteContractType(contractTypePrefix)) + elif sp[-1] == 'week': + # if lsp == 9: + # _, _, _, easySymbol, crash, typeName, contractTypePrefix, _, depth = sp + # return ExtraSymbolChannel(ChannelType.Depth, easySymbol + '_' + crash, + # remotePrefixToRemoteContractType(contractTypePrefix), + # depth) + if lsp == 8: + _, _, _, easySymbol, crash, typeName, contractTypePrefix, _ = sp + return Channel(ChannelType.Tick, + easySymbol + '_' + crash, + remotePrefixToRemoteContractType(contractTypePrefix)) + + +#---------------------------------------------------------------------- +def _tryGetValue(dict, *keys): + """尝试从字典中获取某些键中的一个""" + for k in keys: + if k in dict: + return dict[k] + return None + + +_prefixForRemoteContractType = {v.split('_')[0]: v for k, v in + OkexFuturesContractType.__dict__.items() if + not k.startswith('_')} + +_orderTypeMap = { + (constant.DIRECTION_LONG, constant.OFFSET_OPEN): OkexFuturesOrderType.OpenLong, + (constant.DIRECTION_SHORT, constant.OFFSET_OPEN): OkexFuturesOrderType.OpenShort, + (constant.DIRECTION_LONG, constant.OFFSET_CLOSE): OkexFuturesOrderType.CloseLong, + (constant.DIRECTION_SHORT, constant.OFFSET_CLOSE): OkexFuturesOrderType.CloseShort, +} +_orderTypeMapReverse = {v: k for k, v in _orderTypeMap.items()} + +_contractTypeMap = { + k.upper(): v for k, v in OkexFuturesContractType.__dict__.items() if not k.startswith('_') +} +_contractTypeMapReverse = {v: k for k, v in _contractTypeMap.items()} + +_easySymbols = { + v for k, v in OkexFuturesEasySymbol.__dict__.items() if not k.startswith('_') +} + +_remoteSymbols = { + v for k, v in OkexFuturesSymbol.__dict__.items() if not k.startswith('_') +} + +# symbols for ui, +# keys:给用户看的symbols : f"{internalSymbol}_{contractType}" +# values: API接口使用的symbol和contractType字段 +_symbolsForUi = {(remoteSymbol.upper() + '_' + upperContractType.upper()) + : (remoteSymbol, remoteContractType) + for remoteSymbol in _remoteSymbols + for upperContractType, remoteContractType in + _contractTypeMap.items() + } # type: Dict[str, List[str, str]] +_symbolsForUiReverse = {v: k for k, v in _symbolsForUi.items()} + +_channel_for_subscribe = { + 'ok_sub_futureusd_' + easySymbol + '_ticker_' + remoteContractType + : (easySymbol, remoteContractType) + for easySymbol in _easySymbols + for remoteContractType in _contractTypeMap.values() +} diff --git a/vnpy/trader/vtObject.py b/vnpy/trader/vtObject.py index c8a79ed0..b8774805 100644 --- a/vnpy/trader/vtObject.py +++ b/vnpy/trader/vtObject.py @@ -184,6 +184,7 @@ class VtTradeData(VtBaseData): trade.volume = tradeVolume trade.tradeTime = datetime.now().strftime('%H:%M:%S') return trade + #---------------------------------------------------------------------- @staticmethod def createFromOrderData(order, @@ -240,13 +241,19 @@ class VtOrderData(VtBaseData): #---------------------------------------------------------------------- @staticmethod - def createFromGateway(gateway, orderId, symbol, exchange, price, volume, direction, - offset=EMPTY_UNICODE, - tradedVolume=EMPTY_INT, - status=constant.STATUS_UNKNOWN, - orderTime=EMPTY_UNICODE, - cancelTime=EMPTY_UNICODE, - ): # type: (VtGateway, str, str, str, float, float, str, str, int, str, str, str)->VtOrderData + def createFromGateway(gateway, # type: VtGateway + orderId, # type: str + symbol, # type: str + exchange, # type: str + price, # type: float + volume, # type: int + direction, # type: str + offset=EMPTY_UNICODE, # type: str + tradedVolume=EMPTY_INT, # type: int + status=constant.STATUS_UNKNOWN, # type: str + orderTime=EMPTY_UNICODE, # type: str + cancelTime=EMPTY_UNICODE, # type: str + ): # type: (...)->VtOrderData vtOrder = VtOrderData() vtOrder.gatewayName = gateway.gatewayName vtOrder.symbol = symbol @@ -291,12 +298,16 @@ class VtPositionData(VtBaseData): #---------------------------------------------------------------------- @staticmethod - def createFromGateway(gateway, exchange, symbol, direction, position, - frozen=EMPTY_INT, - price=EMPTY_FLOAT, - yestordayPosition=EMPTY_INT, - profit=EMPTY_FLOAT - ): # type: (VtGateway, str, str, str, float, int, float, int, float)->VtPositionData + def createFromGateway(gateway, # type: VtGateway + exchange, # type: str + symbol, # type: str + direction, # type: str + position, # type: int + frozen=EMPTY_INT, # type: int + price=EMPTY_FLOAT, # type: float + yestordayPosition=EMPTY_INT, # type: int + profit=EMPTY_FLOAT # type: float + ): # type: (...)->VtPositionData vtPosition = VtPositionData() vtPosition.gatewayName = gateway.gatewayName vtPosition.symbol = symbol @@ -421,7 +432,6 @@ class VtContractData(VtBaseData): return d - ######################################################################## class VtHistoryData(object): """K线时间序列数据""" @@ -531,6 +541,3 @@ class VtSingleton(type): cls._instances[cls] = super(VtSingleton, cls).__call__(*args, **kwargs) return cls._instances[cls] - - -