httptrading 1.0.1__py3-none-any.whl → 1.0.3__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 +3 -2
- httptrading/broker/base.py +24 -1
- httptrading/broker/{futu.py → futu_sec.py} +106 -21
- httptrading/broker/interactive_brokers.py +50 -26
- httptrading/broker/longbridge.py +55 -9
- httptrading/broker/tiger.py +63 -3
- httptrading/http_server.py +268 -297
- httptrading/model.py +87 -0
- {httptrading-1.0.1.dist-info → httptrading-1.0.3.dist-info}/METADATA +93 -25
- httptrading-1.0.3.dist-info/RECORD +18 -0
- httptrading-1.0.1.dist-info/RECORD +0 -18
- {httptrading-1.0.1.dist-info → httptrading-1.0.3.dist-info}/WHEEL +0 -0
- {httptrading-1.0.1.dist-info → httptrading-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {httptrading-1.0.1.dist-info → httptrading-1.0.3.dist-info}/top_level.txt +0 -0
httptrading/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from httptrading.broker.base import *
|
2
|
-
from httptrading.broker.
|
2
|
+
from httptrading.broker.futu_sec import *
|
3
3
|
from httptrading.broker.longbridge import *
|
4
4
|
from httptrading.broker.tiger import *
|
5
5
|
from httptrading.broker.interactive_brokers import *
|
6
|
-
from httptrading.http_server import
|
6
|
+
from httptrading.http_server import *
|
7
|
+
from httptrading.model import HtGlobalConfig
|
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)
|
@@ -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]
|
@@ -337,7 +383,7 @@ class Futu(SecuritiesBroker):
|
|
337
383
|
)
|
338
384
|
if ret != RET_OK:
|
339
385
|
raise Exception(f'下单失败: {data}')
|
340
|
-
orders = self.
|
386
|
+
orders = self._df_to_list(data)
|
341
387
|
assert len(orders) == 1
|
342
388
|
order_id = orders[0]['order_id']
|
343
389
|
assert order_id
|
@@ -365,10 +411,60 @@ class Futu(SecuritiesBroker):
|
|
365
411
|
**kwargs
|
366
412
|
))
|
367
413
|
|
414
|
+
@classmethod
|
415
|
+
def _build_order(cls, futu_order: dict) -> Order:
|
416
|
+
from futu import OrderStatus
|
417
|
+
"""
|
418
|
+
富途证券的状态定义
|
419
|
+
NONE = "N/A" # 未知状态
|
420
|
+
UNSUBMITTED = "UNSUBMITTED" # 未提交
|
421
|
+
WAITING_SUBMIT = "WAITING_SUBMIT" # 等待提交
|
422
|
+
SUBMITTING = "SUBMITTING" # 提交中
|
423
|
+
SUBMIT_FAILED = "SUBMIT_FAILED" # 提交失败,下单失败
|
424
|
+
TIMEOUT = "TIMEOUT" # 处理超时,结果未知
|
425
|
+
SUBMITTED = "SUBMITTED" # 已提交,等待成交
|
426
|
+
FILLED_PART = "FILLED_PART" # 部分成交
|
427
|
+
FILLED_ALL = "FILLED_ALL" # 全部已成
|
428
|
+
CANCELLING_PART = "CANCELLING_PART" # 正在撤单_部分(部分已成交,正在撤销剩余部分)
|
429
|
+
CANCELLING_ALL = "CANCELLING_ALL" # 正在撤单_全部
|
430
|
+
CANCELLED_PART = "CANCELLED_PART" # 部分成交,剩余部分已撤单
|
431
|
+
CANCELLED_ALL = "CANCELLED_ALL" # 全部已撤单,无成交
|
432
|
+
FAILED = "FAILED" # 下单失败,服务拒绝
|
433
|
+
DISABLED = "DISABLED" # 已失效
|
434
|
+
DELETED = "DELETED" # 已删除,无成交的订单才能删除
|
435
|
+
FILL_CANCELLED = "FILL_CANCELLED" # 成交被撤销,一般遇不到,意思是已经成交的订单被回滚撤销,成交无效变为废单
|
436
|
+
"""
|
437
|
+
canceled_endings = {OrderStatus.CANCELLED_ALL, OrderStatus.CANCELLED_PART, }
|
438
|
+
bad_endings = {
|
439
|
+
OrderStatus.SUBMIT_FAILED,
|
440
|
+
OrderStatus.FAILED,
|
441
|
+
OrderStatus.DISABLED,
|
442
|
+
OrderStatus.DELETED,
|
443
|
+
OrderStatus.FILL_CANCELLED, # 不清楚对于成交数量有何影响.
|
444
|
+
}
|
445
|
+
pending_cancel_sets = {OrderStatus.CANCELLING_PART, OrderStatus.CANCELLING_ALL, }
|
446
|
+
|
447
|
+
order_id = futu_order['order_id']
|
448
|
+
reason = ''
|
449
|
+
order_status: str = futu_order['order_status']
|
450
|
+
if order_status in bad_endings:
|
451
|
+
reason = order_status
|
452
|
+
is_canceled = order_status in canceled_endings
|
453
|
+
is_pending_cancel = order_status in pending_cancel_sets
|
454
|
+
return Order(
|
455
|
+
order_id=order_id,
|
456
|
+
currency=futu_order['currency'],
|
457
|
+
qty=int(futu_order['qty']),
|
458
|
+
filled_qty=int(futu_order['dealt_qty']),
|
459
|
+
avg_price=futu_order['dealt_avg_price'] or 0.0,
|
460
|
+
error_reason=reason,
|
461
|
+
is_canceled=is_canceled,
|
462
|
+
is_pending_cancel=is_pending_cancel,
|
463
|
+
)
|
464
|
+
|
368
465
|
def _order(self, order_id: str) -> Order:
|
369
|
-
from futu import RET_OK
|
370
|
-
|
371
|
-
cancel_set = {OrderStatus.CANCELLED_PART, OrderStatus.CANCELLED_ALL, }
|
466
|
+
from futu import RET_OK
|
467
|
+
|
372
468
|
with self._refresh_order_bucket:
|
373
469
|
ret, data = self._trade_client.order_list_query(
|
374
470
|
order_id=order_id,
|
@@ -377,22 +473,11 @@ class Futu(SecuritiesBroker):
|
|
377
473
|
)
|
378
474
|
if ret != RET_OK:
|
379
475
|
raise Exception(f'调用获取订单失败, 订单: {order_id}')
|
380
|
-
orders = self.
|
476
|
+
orders = self._df_to_list(data)
|
381
477
|
if len(orders) != 1:
|
382
478
|
raise Exception(f'找不到订单(未完成), 订单: {order_id}')
|
383
479
|
futu_order = orders[0]
|
384
|
-
|
385
|
-
if futu_order['order_status'] in error_set:
|
386
|
-
reason = futu_order['order_status']
|
387
|
-
return Order(
|
388
|
-
order_id=order_id,
|
389
|
-
currency=futu_order['currency'],
|
390
|
-
qty=int(futu_order['qty']),
|
391
|
-
filled_qty=int(futu_order['dealt_qty']),
|
392
|
-
avg_price=futu_order['dealt_avg_price'] or 0.0,
|
393
|
-
error_reason=reason,
|
394
|
-
is_canceled=futu_order['order_status'] in cancel_set,
|
395
|
-
)
|
480
|
+
return self._build_order(futu_order)
|
396
481
|
|
397
482
|
async def order(self, order_id: str) -> Order:
|
398
483
|
return await self.call_sync(lambda : self._order(order_id=order_id))
|
@@ -4,8 +4,8 @@ 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
|
-
from collections import defaultdict
|
9
9
|
from httptrading.tool.leaky_bucket import *
|
10
10
|
from httptrading.tool.time import *
|
11
11
|
from httptrading.broker.base import *
|
@@ -30,9 +30,19 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
30
30
|
def _on_init(self):
|
31
31
|
self._account_id = self.broker_args.get('account_id')
|
32
32
|
self._client_id = self.broker_args.get('client_id')
|
33
|
+
nest_asyncio.apply()
|
33
34
|
|
34
35
|
async def start(self):
|
35
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}')
|
36
46
|
|
37
47
|
async def shutdown(self):
|
38
48
|
ib_socket = self._client
|
@@ -87,7 +97,11 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
87
97
|
client: ib_insync.IB = client
|
88
98
|
|
89
99
|
def _order_status_changed(trade: ib_insync.Trade):
|
90
|
-
|
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}')
|
91
105
|
|
92
106
|
client.orderStatusEvent += _order_status_changed
|
93
107
|
|
@@ -194,7 +208,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
194
208
|
**kwargs
|
195
209
|
) -> str:
|
196
210
|
import ib_insync
|
197
|
-
with self._order_bucket:
|
211
|
+
async with self._order_bucket:
|
198
212
|
client = self._client
|
199
213
|
ib_contract = await self.contract_to_ib_contract(contract)
|
200
214
|
|
@@ -275,7 +289,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
275
289
|
|
276
290
|
async def _cancel_order(self, order_id: str):
|
277
291
|
order_id_int = int(order_id)
|
278
|
-
with self._order_bucket:
|
292
|
+
async with self._order_bucket:
|
279
293
|
client = self._client
|
280
294
|
trades = client.trades()
|
281
295
|
for ib_trade in trades:
|
@@ -288,8 +302,12 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
288
302
|
async def cancel_order(self, order_id: str):
|
289
303
|
await self.call_async(self._cancel_order(order_id=order_id))
|
290
304
|
|
291
|
-
|
305
|
+
@classmethod
|
306
|
+
def _build_order(cls, ib_trade):
|
292
307
|
import ib_insync
|
308
|
+
canceled_endings = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
|
309
|
+
bad_endings = {ib_insync.OrderStatus.Inactive, }
|
310
|
+
pending_cancel_sets = {ib_insync.OrderStatus.PendingCancel, }
|
293
311
|
|
294
312
|
def _total_fills(trade) -> int:
|
295
313
|
return int(trade.filled())
|
@@ -301,7 +319,31 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
301
319
|
cap = sum([fill.execution.shares * fill.execution.avgPrice for fill in trade.fills], 0.0)
|
302
320
|
return round(cap / total_fills, 5)
|
303
321
|
|
304
|
-
|
322
|
+
qty = int(_total_fills(ib_trade) + ib_trade.remaining())
|
323
|
+
filled_qty = _total_fills(ib_trade)
|
324
|
+
qty = qty or filled_qty
|
325
|
+
assert qty >= filled_qty
|
326
|
+
avg_fill_price = _avg_price(ib_trade)
|
327
|
+
reason = ''
|
328
|
+
if ib_trade.orderStatus.status in bad_endings:
|
329
|
+
reason = ib_trade.orderStatus.status
|
330
|
+
is_canceled = ib_trade.orderStatus.status in canceled_endings
|
331
|
+
is_pending_cancel = ib_trade.orderStatus.status in pending_cancel_sets
|
332
|
+
order_id = str(ib_trade.order.permId)
|
333
|
+
order = Order(
|
334
|
+
order_id=order_id,
|
335
|
+
currency=ib_trade.contract.currency,
|
336
|
+
qty=qty,
|
337
|
+
filled_qty=filled_qty,
|
338
|
+
avg_price=avg_fill_price,
|
339
|
+
error_reason=reason,
|
340
|
+
is_canceled=is_canceled,
|
341
|
+
is_pending_cancel=is_pending_cancel,
|
342
|
+
)
|
343
|
+
return order
|
344
|
+
|
345
|
+
async def _order(self, order_id: str) -> Order:
|
346
|
+
async with self._order_bucket:
|
305
347
|
client = self._client
|
306
348
|
trades = client.trades()
|
307
349
|
order_id_int = int(order_id)
|
@@ -309,26 +351,8 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
309
351
|
ib_order = ib_trade.order
|
310
352
|
if ib_order.permId != order_id_int:
|
311
353
|
continue
|
312
|
-
|
313
|
-
|
314
|
-
qty = qty or filled_qty
|
315
|
-
assert qty >= filled_qty
|
316
|
-
avg_fill_price = _avg_price(ib_trade)
|
317
|
-
reason = ''
|
318
|
-
if ib_trade.orderStatus.status == ib_insync.OrderStatus.Inactive:
|
319
|
-
reason = 'Inactive'
|
320
|
-
|
321
|
-
cancel_status = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
|
322
|
-
is_cancelled = ib_trade.orderStatus.status in cancel_status
|
323
|
-
return Order(
|
324
|
-
order_id=order_id,
|
325
|
-
currency=ib_trade.contract.currency,
|
326
|
-
qty=qty,
|
327
|
-
filled_qty=filled_qty,
|
328
|
-
avg_price=avg_fill_price,
|
329
|
-
error_reason=reason,
|
330
|
-
is_canceled=is_cancelled,
|
331
|
-
)
|
354
|
+
order = self._build_order(ib_trade)
|
355
|
+
return order
|
332
356
|
raise Exception(f'查询不到订单{order_id}')
|
333
357
|
|
334
358
|
async def order(self, order_id: str) -> Order:
|
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:
|
@@ -344,17 +396,10 @@ class LongBridge(SecuritiesBroker):
|
|
344
396
|
))
|
345
397
|
|
346
398
|
def _order(self, order_id: str) -> Order:
|
347
|
-
from longport.openapi import OrderStatus
|
348
399
|
with self._assets_bucket:
|
349
400
|
self._try_refresh()
|
350
401
|
resp = self._trade_client.order_detail(order_id=order_id)
|
351
|
-
reason =
|
352
|
-
if resp.status == OrderStatus.Rejected:
|
353
|
-
reason = '已拒绝'
|
354
|
-
if resp.status == OrderStatus.Expired:
|
355
|
-
reason = '已过期'
|
356
|
-
if resp.status == OrderStatus.PartialWithdrawal:
|
357
|
-
reason = '部分撤单'
|
402
|
+
reason, is_canceled, is_pending_cancel = self._order_status(resp)
|
358
403
|
return Order(
|
359
404
|
order_id=order_id,
|
360
405
|
currency=resp.currency,
|
@@ -362,7 +407,8 @@ class LongBridge(SecuritiesBroker):
|
|
362
407
|
filled_qty=int(resp.executed_quantity),
|
363
408
|
avg_price=float(resp.executed_price) if resp.executed_price else 0.0,
|
364
409
|
error_reason=reason,
|
365
|
-
is_canceled=
|
410
|
+
is_canceled=is_canceled,
|
411
|
+
is_pending_cancel=is_pending_cancel,
|
366
412
|
)
|
367
413
|
|
368
414
|
async def order(self, order_id: str) -> Order:
|
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:
|
@@ -318,19 +374,23 @@ class Tiger(SecuritiesBroker):
|
|
318
374
|
))
|
319
375
|
|
320
376
|
def _order(self, order_id: str) -> Order:
|
321
|
-
from tigeropen.trade.domain.order import Order as TigerOrder
|
377
|
+
from tigeropen.trade.domain.order import Order as TigerOrder
|
378
|
+
|
322
379
|
with self._order_bucket:
|
323
380
|
tiger_order: TigerOrder = self._trade_client.get_order(id=int(order_id))
|
324
381
|
if tiger_order is None:
|
325
382
|
raise Exception(f'查询不到订单{order_id}')
|
383
|
+
|
384
|
+
reason, is_canceled, is_pending_cancel = self._order_status(tiger_order)
|
326
385
|
return Order(
|
327
386
|
order_id=order_id,
|
328
387
|
currency=tiger_order.contract.currency,
|
329
388
|
qty=tiger_order.quantity or 0,
|
330
389
|
filled_qty=tiger_order.filled or 0,
|
331
390
|
avg_price=tiger_order.avg_fill_price or 0.0,
|
332
|
-
error_reason=
|
333
|
-
is_canceled=
|
391
|
+
error_reason=reason,
|
392
|
+
is_canceled=is_canceled,
|
393
|
+
is_pending_cancel=is_pending_cancel,
|
334
394
|
)
|
335
395
|
|
336
396
|
async def order(self, order_id: str) -> Order:
|