httptrading 1.0.2__py3-none-any.whl → 1.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- httptrading/__init__.py +1 -0
- httptrading/broker/base.py +25 -1
- httptrading/broker/futu_sec.py +76 -21
- httptrading/broker/interactive_brokers.py +49 -26
- httptrading/broker/longbridge.py +56 -17
- httptrading/broker/tiger.py +61 -15
- httptrading/http_server.py +275 -325
- httptrading/model.py +79 -0
- {httptrading-1.0.2.dist-info → httptrading-1.0.5.dist-info}/METADATA +16 -7
- httptrading-1.0.5.dist-info/RECORD +18 -0
- httptrading-1.0.2.dist-info/RECORD +0 -18
- {httptrading-1.0.2.dist-info → httptrading-1.0.5.dist-info}/WHEEL +0 -0
- {httptrading-1.0.2.dist-info → httptrading-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {httptrading-1.0.2.dist-info → httptrading-1.0.5.dist-info}/top_level.txt +0 -0
httptrading/__init__.py
CHANGED
httptrading/broker/base.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
1
3
|
import asyncio
|
2
4
|
import importlib
|
3
5
|
from abc import ABC
|
4
6
|
from typing import Type, Callable, Any
|
5
7
|
from httptrading.model import *
|
8
|
+
from httptrading.tool.locate import *
|
6
9
|
|
7
10
|
|
8
11
|
class BaseBroker(ABC):
|
@@ -22,7 +25,10 @@ class BaseBroker(ABC):
|
|
22
25
|
return BrokerRegister.get_meta(type(self)).display
|
23
26
|
|
24
27
|
def detect_package(self):
|
25
|
-
|
28
|
+
meta = BrokerRegister.get_meta(type(self))
|
29
|
+
if not meta:
|
30
|
+
return
|
31
|
+
pkg_info = meta.detect_package
|
26
32
|
if pkg_info is None:
|
27
33
|
return
|
28
34
|
try:
|
@@ -36,6 +42,23 @@ class BaseBroker(ABC):
|
|
36
42
|
async def shutdown(self):
|
37
43
|
pass
|
38
44
|
|
45
|
+
def dump_order(self, order: Order):
|
46
|
+
if not isinstance(order, Order):
|
47
|
+
return
|
48
|
+
folder = HtGlobalConfig.STREAM_DUMP_FOLDER
|
49
|
+
if not folder:
|
50
|
+
return
|
51
|
+
if not os.path.isdir(folder):
|
52
|
+
return
|
53
|
+
json_str = json.dumps(
|
54
|
+
order,
|
55
|
+
indent=2,
|
56
|
+
default=HtGlobalConfig.JSON_DEFAULT.json_default,
|
57
|
+
)
|
58
|
+
filename = f'{self.instance_id}-{order.order_id}.json'
|
59
|
+
full_path = os.path.join(folder, filename)
|
60
|
+
LocateTools.write_file(full_path, json_str)
|
61
|
+
|
39
62
|
async def call_sync(self, f: Callable[[], Any]):
|
40
63
|
try:
|
41
64
|
r = await asyncio.get_running_loop().run_in_executor(None, f)
|
@@ -59,6 +82,7 @@ class BaseBroker(ABC):
|
|
59
82
|
direction: str,
|
60
83
|
qty: int,
|
61
84
|
price: float = None,
|
85
|
+
full_args: dict = None,
|
62
86
|
**kwargs
|
63
87
|
) -> str:
|
64
88
|
raise NotImplementedError
|
httptrading/broker/futu_sec.py
CHANGED
@@ -30,6 +30,7 @@ class Futu(SecuritiesBroker):
|
|
30
30
|
|
31
31
|
def _on_init(self):
|
32
32
|
from futu import SysConfig, OpenQuoteContext, OpenSecTradeContext, SecurityFirm, TrdMarket, TrdEnv
|
33
|
+
|
33
34
|
config_dict = self.broker_args
|
34
35
|
self._trd_env: str = config_dict.get('trade_env', TrdEnv.REAL) or TrdEnv.REAL
|
35
36
|
pk_path = config_dict.get('pk_path', '')
|
@@ -49,6 +50,7 @@ class Futu(SecuritiesBroker):
|
|
49
50
|
)
|
50
51
|
trade_ctx.set_sync_query_connect_timeout(6.0)
|
51
52
|
self._trade_client = trade_ctx
|
53
|
+
self._when_create_client()
|
52
54
|
if self._quote_client is None:
|
53
55
|
SysConfig.set_all_thread_daemon(True)
|
54
56
|
host = config_dict.get('host', '127.0.0.1')
|
@@ -57,6 +59,50 @@ class Futu(SecuritiesBroker):
|
|
57
59
|
quote_ctx.set_sync_query_connect_timeout(6.0)
|
58
60
|
self._quote_client = quote_ctx
|
59
61
|
|
62
|
+
def _when_create_client(self):
|
63
|
+
from futu import TradeOrderHandlerBase, RET_OK, OpenSecTradeContext
|
64
|
+
|
65
|
+
client: OpenSecTradeContext = self._trade_client
|
66
|
+
|
67
|
+
def _on_recv_rsp(content):
|
68
|
+
for _futu_order in self._df_to_list(content):
|
69
|
+
try:
|
70
|
+
_order = self._build_order(_futu_order)
|
71
|
+
self.dump_order(_order)
|
72
|
+
except Exception as _ex:
|
73
|
+
print(f'[{self.__class__.__name__}]_on_recv_rsp: {_ex}\norder: {_futu_order}')
|
74
|
+
|
75
|
+
class TradeOrderHandler(TradeOrderHandlerBase):
|
76
|
+
def on_recv_rsp(self, rsp_pb):
|
77
|
+
ret, content = super().on_recv_rsp(rsp_pb)
|
78
|
+
if ret == RET_OK:
|
79
|
+
_on_recv_rsp(content)
|
80
|
+
return ret, content
|
81
|
+
|
82
|
+
client.set_handler(TradeOrderHandler())
|
83
|
+
|
84
|
+
async def start(self):
|
85
|
+
from futu import RET_OK, OpenSecTradeContext
|
86
|
+
|
87
|
+
client: OpenSecTradeContext = self._trade_client
|
88
|
+
if HtGlobalConfig.DUMP_ACTIVE_ORDERS:
|
89
|
+
try:
|
90
|
+
ret, data = client.order_list_query(
|
91
|
+
refresh_cache=True,
|
92
|
+
trd_env=self._trd_env,
|
93
|
+
)
|
94
|
+
except Exception as e:
|
95
|
+
print(f'[{self.__class__.__name__}]DUMP_ACTIVE_ORDERS: {e}')
|
96
|
+
else:
|
97
|
+
if ret == RET_OK:
|
98
|
+
futu_orders = self._df_to_list(data)
|
99
|
+
for futu_order in futu_orders:
|
100
|
+
try:
|
101
|
+
order = self._build_order(futu_order)
|
102
|
+
await self.call_sync(lambda : self.dump_order(order))
|
103
|
+
except Exception as ex:
|
104
|
+
print(f'[{self.__class__.__name__}]DUMP_ACTIVE_ORDERS: {ex}\norder: {futu_order}')
|
105
|
+
|
60
106
|
@classmethod
|
61
107
|
def code_to_contract(cls, code) -> Contract | None:
|
62
108
|
region = ''
|
@@ -98,7 +144,7 @@ class Futu(SecuritiesBroker):
|
|
98
144
|
return code
|
99
145
|
|
100
146
|
@classmethod
|
101
|
-
def
|
147
|
+
def _df_to_list(cls, df) -> list[dict]:
|
102
148
|
return df.to_dict(orient='records')
|
103
149
|
|
104
150
|
def _positions(self):
|
@@ -111,7 +157,7 @@ class Futu(SecuritiesBroker):
|
|
111
157
|
)
|
112
158
|
if resp != RET_OK:
|
113
159
|
raise Exception(f'返回失败: {resp}')
|
114
|
-
positions = self.
|
160
|
+
positions = self._df_to_list(data)
|
115
161
|
for d in positions:
|
116
162
|
code = d.get('code')
|
117
163
|
currency = d.get('currency')
|
@@ -153,7 +199,7 @@ class Futu(SecuritiesBroker):
|
|
153
199
|
)
|
154
200
|
if resp != RET_OK:
|
155
201
|
raise Exception(f'可用资金信息获取失败: {data}')
|
156
|
-
assets = self.
|
202
|
+
assets = self._df_to_list(data)
|
157
203
|
if len(assets) == 1:
|
158
204
|
cash = Cash(
|
159
205
|
currency='USD',
|
@@ -219,7 +265,7 @@ class Futu(SecuritiesBroker):
|
|
219
265
|
ret, data = self._quote_client.get_market_snapshot([code, ])
|
220
266
|
if ret != RET_OK:
|
221
267
|
raise ValueError(f'快照接口调用失败: {data}')
|
222
|
-
table = self.
|
268
|
+
table = self._df_to_list(data)
|
223
269
|
if len(table) != 1:
|
224
270
|
raise ValueError(f'快照接口调用无数据: {data}')
|
225
271
|
d = table[0]
|
@@ -274,6 +320,7 @@ class Futu(SecuritiesBroker):
|
|
274
320
|
direction: str,
|
275
321
|
qty: int,
|
276
322
|
price: float = None,
|
323
|
+
full_args: dict = None,
|
277
324
|
**kwargs
|
278
325
|
) -> str:
|
279
326
|
from futu import RET_OK, TrdSide, OrderType as FutuOrderType, TimeInForce as FutuTimeInForce, Session
|
@@ -337,7 +384,7 @@ class Futu(SecuritiesBroker):
|
|
337
384
|
)
|
338
385
|
if ret != RET_OK:
|
339
386
|
raise Exception(f'下单失败: {data}')
|
340
|
-
orders = self.
|
387
|
+
orders = self._df_to_list(data)
|
341
388
|
assert len(orders) == 1
|
342
389
|
order_id = orders[0]['order_id']
|
343
390
|
assert order_id
|
@@ -352,6 +399,7 @@ class Futu(SecuritiesBroker):
|
|
352
399
|
direction: str,
|
353
400
|
qty: int,
|
354
401
|
price: float = None,
|
402
|
+
full_args: dict = None,
|
355
403
|
**kwargs
|
356
404
|
) -> str:
|
357
405
|
return await self.call_sync(lambda : self._place_order(
|
@@ -362,12 +410,13 @@ class Futu(SecuritiesBroker):
|
|
362
410
|
direction=direction,
|
363
411
|
qty=qty,
|
364
412
|
price=price,
|
413
|
+
full_args=full_args,
|
365
414
|
**kwargs
|
366
415
|
))
|
367
416
|
|
368
|
-
|
369
|
-
|
370
|
-
|
417
|
+
@classmethod
|
418
|
+
def _build_order(cls, futu_order: dict) -> Order:
|
419
|
+
from futu import OrderStatus
|
371
420
|
"""
|
372
421
|
富途证券的状态定义
|
373
422
|
NONE = "N/A" # 未知状态
|
@@ -394,22 +443,11 @@ class Futu(SecuritiesBroker):
|
|
394
443
|
OrderStatus.FAILED,
|
395
444
|
OrderStatus.DISABLED,
|
396
445
|
OrderStatus.DELETED,
|
397
|
-
OrderStatus.FILL_CANCELLED,
|
446
|
+
OrderStatus.FILL_CANCELLED, # 不清楚对于成交数量有何影响.
|
398
447
|
}
|
399
448
|
pending_cancel_sets = {OrderStatus.CANCELLING_PART, OrderStatus.CANCELLING_ALL, }
|
400
449
|
|
401
|
-
|
402
|
-
ret, data = self._trade_client.order_list_query(
|
403
|
-
order_id=order_id,
|
404
|
-
refresh_cache=True,
|
405
|
-
trd_env=self._trd_env,
|
406
|
-
)
|
407
|
-
if ret != RET_OK:
|
408
|
-
raise Exception(f'调用获取订单失败, 订单: {order_id}')
|
409
|
-
orders = self.df_to_dict(data)
|
410
|
-
if len(orders) != 1:
|
411
|
-
raise Exception(f'找不到订单(未完成), 订单: {order_id}')
|
412
|
-
futu_order = orders[0]
|
450
|
+
order_id = futu_order['order_id']
|
413
451
|
reason = ''
|
414
452
|
order_status: str = futu_order['order_status']
|
415
453
|
if order_status in bad_endings:
|
@@ -427,6 +465,23 @@ class Futu(SecuritiesBroker):
|
|
427
465
|
is_pending_cancel=is_pending_cancel,
|
428
466
|
)
|
429
467
|
|
468
|
+
def _order(self, order_id: str) -> Order:
|
469
|
+
from futu import RET_OK
|
470
|
+
|
471
|
+
with self._refresh_order_bucket:
|
472
|
+
ret, data = self._trade_client.order_list_query(
|
473
|
+
order_id=order_id,
|
474
|
+
refresh_cache=True,
|
475
|
+
trd_env=self._trd_env,
|
476
|
+
)
|
477
|
+
if ret != RET_OK:
|
478
|
+
raise Exception(f'调用获取订单失败, 订单: {order_id}')
|
479
|
+
orders = self._df_to_list(data)
|
480
|
+
if len(orders) != 1:
|
481
|
+
raise Exception(f'找不到订单(未完成), 订单: {order_id}')
|
482
|
+
futu_order = orders[0]
|
483
|
+
return self._build_order(futu_order)
|
484
|
+
|
430
485
|
async def order(self, order_id: str) -> Order:
|
431
486
|
return await self.call_sync(lambda : self._order(order_id=order_id))
|
432
487
|
|
@@ -4,6 +4,7 @@ https://ib-insync.readthedocs.io/readme.html
|
|
4
4
|
"""
|
5
5
|
import re
|
6
6
|
import asyncio
|
7
|
+
import nest_asyncio
|
7
8
|
from typing import Any
|
8
9
|
from httptrading.tool.leaky_bucket import *
|
9
10
|
from httptrading.tool.time import *
|
@@ -29,9 +30,19 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
29
30
|
def _on_init(self):
|
30
31
|
self._account_id = self.broker_args.get('account_id')
|
31
32
|
self._client_id = self.broker_args.get('client_id')
|
33
|
+
nest_asyncio.apply()
|
32
34
|
|
33
35
|
async def start(self):
|
34
36
|
await self._try_create_client()
|
37
|
+
client = self._client
|
38
|
+
if HtGlobalConfig.DUMP_ACTIVE_ORDERS:
|
39
|
+
trades = client.trades()
|
40
|
+
for trade in trades:
|
41
|
+
try:
|
42
|
+
order = self._build_order(trade)
|
43
|
+
await self.call_sync(lambda: self.dump_order(order))
|
44
|
+
except Exception as ex:
|
45
|
+
print(f'[{self.__class__.__name__}]DUMP_ACTIVE_ORDERS: {ex}\norder: {trade}')
|
35
46
|
|
36
47
|
async def shutdown(self):
|
37
48
|
ib_socket = self._client
|
@@ -86,7 +97,11 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
86
97
|
client: ib_insync.IB = client
|
87
98
|
|
88
99
|
def _order_status_changed(trade: ib_insync.Trade):
|
89
|
-
|
100
|
+
try:
|
101
|
+
order = self._build_order(trade)
|
102
|
+
self.dump_order(order)
|
103
|
+
except Exception as e:
|
104
|
+
print(f'[{self.__class__.__name__}]_order_status_changed: {e}\ntrade: {trade}')
|
90
105
|
|
91
106
|
client.orderStatusEvent += _order_status_changed
|
92
107
|
|
@@ -190,10 +205,11 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
190
205
|
direction: str,
|
191
206
|
qty: int,
|
192
207
|
price: float = None,
|
208
|
+
full_args: dict = None,
|
193
209
|
**kwargs
|
194
210
|
) -> str:
|
195
211
|
import ib_insync
|
196
|
-
with self._order_bucket:
|
212
|
+
async with self._order_bucket:
|
197
213
|
client = self._client
|
198
214
|
ib_contract = await self.contract_to_ib_contract(contract)
|
199
215
|
|
@@ -259,6 +275,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
259
275
|
direction: str,
|
260
276
|
qty: int,
|
261
277
|
price: float = None,
|
278
|
+
full_args: dict = None,
|
262
279
|
**kwargs
|
263
280
|
) -> str:
|
264
281
|
return await self.call_async(self._place_order(
|
@@ -269,12 +286,13 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
269
286
|
direction=direction,
|
270
287
|
qty=qty,
|
271
288
|
price=price,
|
289
|
+
full_args=full_args,
|
272
290
|
**kwargs
|
273
291
|
))
|
274
292
|
|
275
293
|
async def _cancel_order(self, order_id: str):
|
276
294
|
order_id_int = int(order_id)
|
277
|
-
with self._order_bucket:
|
295
|
+
async with self._order_bucket:
|
278
296
|
client = self._client
|
279
297
|
trades = client.trades()
|
280
298
|
for ib_trade in trades:
|
@@ -287,9 +305,9 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
287
305
|
async def cancel_order(self, order_id: str):
|
288
306
|
await self.call_async(self._cancel_order(order_id=order_id))
|
289
307
|
|
290
|
-
|
308
|
+
@classmethod
|
309
|
+
def _build_order(cls, ib_trade):
|
291
310
|
import ib_insync
|
292
|
-
|
293
311
|
canceled_endings = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
|
294
312
|
bad_endings = {ib_insync.OrderStatus.Inactive, }
|
295
313
|
pending_cancel_sets = {ib_insync.OrderStatus.PendingCancel, }
|
@@ -304,7 +322,31 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
304
322
|
cap = sum([fill.execution.shares * fill.execution.avgPrice for fill in trade.fills], 0.0)
|
305
323
|
return round(cap / total_fills, 5)
|
306
324
|
|
307
|
-
|
325
|
+
qty = int(_total_fills(ib_trade) + ib_trade.remaining())
|
326
|
+
filled_qty = _total_fills(ib_trade)
|
327
|
+
qty = qty or filled_qty
|
328
|
+
assert qty >= filled_qty
|
329
|
+
avg_fill_price = _avg_price(ib_trade)
|
330
|
+
reason = ''
|
331
|
+
if ib_trade.orderStatus.status in bad_endings:
|
332
|
+
reason = ib_trade.orderStatus.status
|
333
|
+
is_canceled = ib_trade.orderStatus.status in canceled_endings
|
334
|
+
is_pending_cancel = ib_trade.orderStatus.status in pending_cancel_sets
|
335
|
+
order_id = str(ib_trade.order.permId)
|
336
|
+
order = Order(
|
337
|
+
order_id=order_id,
|
338
|
+
currency=ib_trade.contract.currency,
|
339
|
+
qty=qty,
|
340
|
+
filled_qty=filled_qty,
|
341
|
+
avg_price=avg_fill_price,
|
342
|
+
error_reason=reason,
|
343
|
+
is_canceled=is_canceled,
|
344
|
+
is_pending_cancel=is_pending_cancel,
|
345
|
+
)
|
346
|
+
return order
|
347
|
+
|
348
|
+
async def _order(self, order_id: str) -> Order:
|
349
|
+
async with self._order_bucket:
|
308
350
|
client = self._client
|
309
351
|
trades = client.trades()
|
310
352
|
order_id_int = int(order_id)
|
@@ -312,26 +354,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
312
354
|
ib_order = ib_trade.order
|
313
355
|
if ib_order.permId != order_id_int:
|
314
356
|
continue
|
315
|
-
|
316
|
-
filled_qty = _total_fills(ib_trade)
|
317
|
-
qty = qty or filled_qty
|
318
|
-
assert qty >= filled_qty
|
319
|
-
avg_fill_price = _avg_price(ib_trade)
|
320
|
-
reason = ''
|
321
|
-
if ib_trade.orderStatus.status in bad_endings:
|
322
|
-
reason = ib_trade.orderStatus.status
|
323
|
-
is_canceled = ib_trade.orderStatus.status in canceled_endings
|
324
|
-
is_pending_cancel = ib_trade.orderStatus.status in pending_cancel_sets
|
325
|
-
order = Order(
|
326
|
-
order_id=order_id,
|
327
|
-
currency=ib_trade.contract.currency,
|
328
|
-
qty=qty,
|
329
|
-
filled_qty=filled_qty,
|
330
|
-
avg_price=avg_fill_price,
|
331
|
-
error_reason=reason,
|
332
|
-
is_canceled=is_canceled,
|
333
|
-
is_pending_cancel=is_pending_cancel,
|
334
|
-
)
|
357
|
+
order = self._build_order(ib_trade)
|
335
358
|
return order
|
336
359
|
raise Exception(f'查询不到订单{order_id}')
|
337
360
|
|
httptrading/broker/longbridge.py
CHANGED
@@ -116,6 +116,58 @@ class LongBridge(SecuritiesBroker):
|
|
116
116
|
self._lp_config = config
|
117
117
|
self._quote_client = quote_ctx
|
118
118
|
self._trade_client = trade_ctx
|
119
|
+
self._when_create_client()
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
def _order_status(cls, lp_order):
|
123
|
+
# 订单状态定义见
|
124
|
+
# https://open.longportapp.com/zh-CN/docs/trade/trade-definition#orderstatus
|
125
|
+
from longport.openapi import OrderStatus, PushOrderChanged, OrderDetail
|
126
|
+
|
127
|
+
canceled_endings = {OrderStatus.Canceled, }
|
128
|
+
bad_endings = {
|
129
|
+
OrderStatus.Rejected,
|
130
|
+
OrderStatus.Expired,
|
131
|
+
OrderStatus.PartialWithdrawal,
|
132
|
+
}
|
133
|
+
pending_cancel_sets = {OrderStatus.PendingCancel, }
|
134
|
+
|
135
|
+
if isinstance(lp_order, PushOrderChanged):
|
136
|
+
status = lp_order.status
|
137
|
+
elif isinstance(lp_order, OrderDetail):
|
138
|
+
status = lp_order.status
|
139
|
+
else:
|
140
|
+
raise Exception(f'{lp_order}对象不是已知可解析订单状态的类型')
|
141
|
+
reason = ''
|
142
|
+
if status in bad_endings:
|
143
|
+
reason = str(status)
|
144
|
+
is_canceled = status in canceled_endings
|
145
|
+
is_pending_cancel = status in pending_cancel_sets
|
146
|
+
return reason, is_canceled, is_pending_cancel
|
147
|
+
|
148
|
+
def _when_create_client(self):
|
149
|
+
from longport.openapi import PushOrderChanged, TopicType, OrderStatus
|
150
|
+
|
151
|
+
def _on_order_changed(event: PushOrderChanged):
|
152
|
+
reason, is_canceled, is_pending_cancel = self._order_status(event)
|
153
|
+
try:
|
154
|
+
order = Order(
|
155
|
+
order_id=event.order_id,
|
156
|
+
currency=event.currency,
|
157
|
+
qty=int(event.executed_quantity),
|
158
|
+
filled_qty=int(event.executed_quantity),
|
159
|
+
avg_price=float(event.executed_price) if event.executed_price else 0.0,
|
160
|
+
error_reason=reason,
|
161
|
+
is_canceled=is_canceled,
|
162
|
+
is_pending_cancel=is_pending_cancel,
|
163
|
+
)
|
164
|
+
self.dump_order(order)
|
165
|
+
except Exception as e:
|
166
|
+
print(f'[{self.__class__.__name__}]_on_order_changed: {e}\norder: {event}')
|
167
|
+
|
168
|
+
trade_client = self._trade_client
|
169
|
+
trade_client.set_on_order_changed(_on_order_changed)
|
170
|
+
trade_client.subscribe([TopicType.Private])
|
119
171
|
|
120
172
|
def _try_refresh(self):
|
121
173
|
if not self._auto_refresh_token:
|
@@ -258,6 +310,7 @@ class LongBridge(SecuritiesBroker):
|
|
258
310
|
direction: str,
|
259
311
|
qty: int,
|
260
312
|
price: float = None,
|
313
|
+
full_args: dict = None,
|
261
314
|
**kwargs
|
262
315
|
) -> str:
|
263
316
|
from longport.openapi import OrderType as LbOrderType, OrderSide, TimeInForceType, OutsideRTH
|
@@ -330,6 +383,7 @@ class LongBridge(SecuritiesBroker):
|
|
330
383
|
direction: str,
|
331
384
|
qty: int,
|
332
385
|
price: float = None,
|
386
|
+
full_args: dict = None,
|
333
387
|
**kwargs
|
334
388
|
) -> str:
|
335
389
|
return await self.call_sync(lambda : self._place_order(
|
@@ -340,30 +394,15 @@ class LongBridge(SecuritiesBroker):
|
|
340
394
|
direction=direction,
|
341
395
|
qty=qty,
|
342
396
|
price=price,
|
397
|
+
full_args=full_args,
|
343
398
|
**kwargs
|
344
399
|
))
|
345
400
|
|
346
401
|
def _order(self, order_id: str) -> Order:
|
347
|
-
# 订单状态定义见
|
348
|
-
# https://open.longportapp.com/zh-CN/docs/trade/trade-definition#orderstatus
|
349
|
-
from longport.openapi import OrderStatus
|
350
|
-
|
351
|
-
canceled_endings = {OrderStatus.Canceled, }
|
352
|
-
bad_endings = {
|
353
|
-
OrderStatus.Rejected,
|
354
|
-
OrderStatus.Expired,
|
355
|
-
OrderStatus.PartialWithdrawal,
|
356
|
-
}
|
357
|
-
pending_cancel_sets = {OrderStatus.PendingCancel, }
|
358
|
-
|
359
402
|
with self._assets_bucket:
|
360
403
|
self._try_refresh()
|
361
404
|
resp = self._trade_client.order_detail(order_id=order_id)
|
362
|
-
reason =
|
363
|
-
if resp.status in bad_endings:
|
364
|
-
reason = str(resp.status)
|
365
|
-
is_canceled = resp.status in canceled_endings
|
366
|
-
is_pending_cancel = resp.status in pending_cancel_sets
|
405
|
+
reason, is_canceled, is_pending_cancel = self._order_status(resp)
|
367
406
|
return Order(
|
368
407
|
order_id=order_id,
|
369
408
|
currency=resp.currency,
|
httptrading/broker/tiger.py
CHANGED
@@ -46,6 +46,62 @@ class Tiger(SecuritiesBroker):
|
|
46
46
|
self._trade_client = self._trade_client or TradeClient(client_config)
|
47
47
|
protocol, host, port = client_config.socket_host_port
|
48
48
|
self._push_client = self._push_client or PushClient(host, port, use_ssl=(protocol == 'ssl'))
|
49
|
+
self._when_create_client()
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def _order_status(cls, tiger_order):
|
53
|
+
# 订单状态定义见
|
54
|
+
# https://quant.itigerup.com/openapi/zh/python/appendix2/overview.html#%E8%AE%A2%E5%8D%95%E7%8A%B6%E6%80%81
|
55
|
+
# 注意文档比 SDK 定义要少
|
56
|
+
from tigeropen.trade.domain.order import Order as TigerOrder, OrderStatus
|
57
|
+
from tigeropen.push.pb.OrderStatusData_pb2 import OrderStatusData
|
58
|
+
from tigeropen.common.util.order_utils import get_order_status
|
59
|
+
|
60
|
+
canceled_endings = {OrderStatus.CANCELLED, }
|
61
|
+
bad_endings = {
|
62
|
+
OrderStatus.REJECTED,
|
63
|
+
OrderStatus.EXPIRED,
|
64
|
+
}
|
65
|
+
pending_cancel_sets = {OrderStatus.PENDING_CANCEL, }
|
66
|
+
|
67
|
+
if isinstance(tiger_order, TigerOrder):
|
68
|
+
status = tiger_order.status
|
69
|
+
elif isinstance(tiger_order, OrderStatusData):
|
70
|
+
status = get_order_status(tiger_order.status)
|
71
|
+
else:
|
72
|
+
raise Exception(f'{tiger_order}对象不是已知可解析订单状态的类型')
|
73
|
+
reason = ''
|
74
|
+
if status in bad_endings:
|
75
|
+
reason = str(status)
|
76
|
+
is_canceled = status in canceled_endings
|
77
|
+
is_pending_cancel = status in pending_cancel_sets
|
78
|
+
return reason, is_canceled, is_pending_cancel
|
79
|
+
|
80
|
+
def _when_create_client(self):
|
81
|
+
from tigeropen.push.pb.OrderStatusData_pb2 import OrderStatusData
|
82
|
+
|
83
|
+
def _on_order_changed(frame: OrderStatusData):
|
84
|
+
try:
|
85
|
+
reason, is_canceled, is_pending_cancel = self._order_status(frame)
|
86
|
+
order = Order(
|
87
|
+
order_id=str(frame.id),
|
88
|
+
currency=frame.currency,
|
89
|
+
qty=frame.totalQuantity or 0,
|
90
|
+
filled_qty=frame.filledQuantity or 0,
|
91
|
+
avg_price=frame.avgFillPrice or 0.0,
|
92
|
+
error_reason=reason,
|
93
|
+
is_canceled=is_canceled,
|
94
|
+
is_pending_cancel=is_pending_cancel,
|
95
|
+
)
|
96
|
+
self.dump_order(order)
|
97
|
+
except Exception as e:
|
98
|
+
print(f'[{self.__class__.__name__}]_on_order_changed: {e}\norder: {frame}')
|
99
|
+
|
100
|
+
client_config = self._config
|
101
|
+
push_client = self._push_client
|
102
|
+
push_client.order_changed = _on_order_changed
|
103
|
+
push_client.connect(client_config.tiger_id, client_config.private_key)
|
104
|
+
push_client.subscribe_order(account=client_config.account)
|
49
105
|
|
50
106
|
def _grab_quote(self):
|
51
107
|
with self._grab_lock:
|
@@ -225,6 +281,7 @@ class Tiger(SecuritiesBroker):
|
|
225
281
|
direction: str,
|
226
282
|
qty: int,
|
227
283
|
price: float = None,
|
284
|
+
full_args: dict = None,
|
228
285
|
**kwargs
|
229
286
|
) -> str:
|
230
287
|
if contract.trade_type != TradeType.Securities:
|
@@ -304,6 +361,7 @@ class Tiger(SecuritiesBroker):
|
|
304
361
|
direction: str,
|
305
362
|
qty: int,
|
306
363
|
price: float = None,
|
364
|
+
full_args: dict = None,
|
307
365
|
**kwargs
|
308
366
|
) -> str:
|
309
367
|
return await self.call_sync(lambda: self._place_order(
|
@@ -314,31 +372,19 @@ class Tiger(SecuritiesBroker):
|
|
314
372
|
direction=direction,
|
315
373
|
qty=qty,
|
316
374
|
price=price,
|
375
|
+
full_args=full_args,
|
317
376
|
**kwargs
|
318
377
|
))
|
319
378
|
|
320
379
|
def _order(self, order_id: str) -> Order:
|
321
|
-
|
322
|
-
# https://quant.itigerup.com/openapi/zh/python/appendix2/overview.html#%E8%AE%A2%E5%8D%95%E7%8A%B6%E6%80%81
|
323
|
-
# 注意文档比 SDK 定义要少
|
324
|
-
from tigeropen.trade.domain.order import Order as TigerOrder, OrderStatus
|
325
|
-
|
326
|
-
canceled_endings = {OrderStatus.CANCELLED, }
|
327
|
-
bad_endings = {
|
328
|
-
OrderStatus.REJECTED,
|
329
|
-
OrderStatus.EXPIRED,
|
330
|
-
}
|
331
|
-
pending_cancel_sets = {OrderStatus.PENDING_CANCEL, }
|
380
|
+
from tigeropen.trade.domain.order import Order as TigerOrder
|
332
381
|
|
333
382
|
with self._order_bucket:
|
334
383
|
tiger_order: TigerOrder = self._trade_client.get_order(id=int(order_id))
|
335
384
|
if tiger_order is None:
|
336
385
|
raise Exception(f'查询不到订单{order_id}')
|
337
386
|
|
338
|
-
|
339
|
-
reason = tiger_order.reason or (order_status.name if order_status in bad_endings else None)
|
340
|
-
is_canceled = order_status in canceled_endings
|
341
|
-
is_pending_cancel = order_status in pending_cancel_sets
|
387
|
+
reason, is_canceled, is_pending_cancel = self._order_status(tiger_order)
|
342
388
|
return Order(
|
343
389
|
order_id=order_id,
|
344
390
|
currency=tiger_order.contract.currency,
|