httptrading 1.0.2__tar.gz → 1.0.3__tar.gz
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-1.0.2/httptrading.egg-info → httptrading-1.0.3}/PKG-INFO +16 -7
- httptrading-1.0.2/PKG-INFO → httptrading-1.0.3/README.md +14 -20
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/__init__.py +1 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/base.py +24 -1
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/futu_sec.py +73 -21
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/interactive_brokers.py +46 -26
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/longbridge.py +53 -17
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/tiger.py +58 -15
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/http_server.py +267 -325
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/model.py +79 -0
- httptrading-1.0.2/README.md → httptrading-1.0.3/httptrading.egg-info/PKG-INFO +29 -6
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/requires.txt +1 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/pyproject.toml +2 -2
- {httptrading-1.0.2 → httptrading-1.0.3}/LICENSE +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/__init__.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/__init__.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/leaky_bucket.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/locate.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/time.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/SOURCES.txt +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/dependency_links.txt +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/top_level.txt +0 -0
- {httptrading-1.0.2 → httptrading-1.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: httptrading
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.3
|
4
4
|
Summary: 统一交易通道的接口服务
|
5
5
|
Author-email: songwei <github@songwei.name>
|
6
6
|
License: MIT
|
@@ -10,6 +10,7 @@ License-File: LICENSE
|
|
10
10
|
Requires-Dist: aiohttp>=3.12.6
|
11
11
|
Requires-Dist: humanize>=4.12.3
|
12
12
|
Requires-Dist: tomlkit>=0.13.2
|
13
|
+
Requires-Dist: nest-asyncio>=1.6.0
|
13
14
|
Dynamic: license-file
|
14
15
|
|
15
16
|
# httptrading
|
@@ -397,6 +398,10 @@ POST /httptrading/api/{instanceId}/order/cancel
|
|
397
398
|
}
|
398
399
|
```
|
399
400
|
|
401
|
+
```
|
402
|
+
⚠️ 撤单接口仅完成对交易通道的撤单接口调用, 不代表在较短时间后订单可以进入撤销的状态. 一个例子是假日发起撤单, 通道不一定执行撤单而是进入已请求撤单的状态.
|
403
|
+
```
|
404
|
+
|
400
405
|
|
401
406
|
### 查询单个订单
|
402
407
|
|
@@ -423,15 +428,19 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
|
|
423
428
|
"errorReason": "", // 如果订单异常, 这里记录错误信息
|
424
429
|
"isCanceled": false, // 是否已撤销
|
425
430
|
"isFilled": false, // 是否全部成交
|
426
|
-
"isCompleted": false, // 全部成交 或者
|
427
|
-
"isCancelable": true //
|
431
|
+
"isCompleted": false, // 全部成交 或者 有订单异常 或者 已撤销
|
432
|
+
"isCancelable": true // 是否可撤的标志, 等价于 not isCompleted and not isPendingCancel
|
428
433
|
}
|
429
434
|
}
|
430
435
|
```
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
436
|
+
⚠️ 有些交易通道不支持查询单个订单状态, 而是查询活动订单的列表来实现, 意味着订单在结束周期的交易日之后, 将查不到订单.
|
437
|
+
|
438
|
+
| 交易通道 | 支持查单个订单 |
|
439
|
+
|------|---------|
|
440
|
+
| 盈透证券 | ❌ |
|
441
|
+
| 富途证券 | ❌ |
|
442
|
+
| 长桥证券 | ✅ |
|
443
|
+
| 老虎证券 | ✅ |
|
435
444
|
|
436
445
|
交易通道的参数
|
437
446
|
------------
|
@@ -1,17 +1,3 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: httptrading
|
3
|
-
Version: 1.0.2
|
4
|
-
Summary: 统一交易通道的接口服务
|
5
|
-
Author-email: songwei <github@songwei.name>
|
6
|
-
License: MIT
|
7
|
-
Requires-Python: >=3.13
|
8
|
-
Description-Content-Type: text/markdown
|
9
|
-
License-File: LICENSE
|
10
|
-
Requires-Dist: aiohttp>=3.12.6
|
11
|
-
Requires-Dist: humanize>=4.12.3
|
12
|
-
Requires-Dist: tomlkit>=0.13.2
|
13
|
-
Dynamic: license-file
|
14
|
-
|
15
1
|
# httptrading
|
16
2
|
|
17
3
|
```shell
|
@@ -397,6 +383,10 @@ POST /httptrading/api/{instanceId}/order/cancel
|
|
397
383
|
}
|
398
384
|
```
|
399
385
|
|
386
|
+
```
|
387
|
+
⚠️ 撤单接口仅完成对交易通道的撤单接口调用, 不代表在较短时间后订单可以进入撤销的状态. 一个例子是假日发起撤单, 通道不一定执行撤单而是进入已请求撤单的状态.
|
388
|
+
```
|
389
|
+
|
400
390
|
|
401
391
|
### 查询单个订单
|
402
392
|
|
@@ -423,15 +413,19 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
|
|
423
413
|
"errorReason": "", // 如果订单异常, 这里记录错误信息
|
424
414
|
"isCanceled": false, // 是否已撤销
|
425
415
|
"isFilled": false, // 是否全部成交
|
426
|
-
"isCompleted": false, // 全部成交 或者
|
427
|
-
"isCancelable": true //
|
416
|
+
"isCompleted": false, // 全部成交 或者 有订单异常 或者 已撤销
|
417
|
+
"isCancelable": true // 是否可撤的标志, 等价于 not isCompleted and not isPendingCancel
|
428
418
|
}
|
429
419
|
}
|
430
420
|
```
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
421
|
+
⚠️ 有些交易通道不支持查询单个订单状态, 而是查询活动订单的列表来实现, 意味着订单在结束周期的交易日之后, 将查不到订单.
|
422
|
+
|
423
|
+
| 交易通道 | 支持查单个订单 |
|
424
|
+
|------|---------|
|
425
|
+
| 盈透证券 | ❌ |
|
426
|
+
| 富途证券 | ❌ |
|
427
|
+
| 长桥证券 | ✅ |
|
428
|
+
| 老虎证券 | ✅ |
|
435
429
|
|
436
430
|
交易通道的参数
|
437
431
|
------------
|
@@ -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,9 +411,9 @@ class Futu(SecuritiesBroker):
|
|
365
411
|
**kwargs
|
366
412
|
))
|
367
413
|
|
368
|
-
|
369
|
-
|
370
|
-
|
414
|
+
@classmethod
|
415
|
+
def _build_order(cls, futu_order: dict) -> Order:
|
416
|
+
from futu import OrderStatus
|
371
417
|
"""
|
372
418
|
富途证券的状态定义
|
373
419
|
NONE = "N/A" # 未知状态
|
@@ -394,22 +440,11 @@ class Futu(SecuritiesBroker):
|
|
394
440
|
OrderStatus.FAILED,
|
395
441
|
OrderStatus.DISABLED,
|
396
442
|
OrderStatus.DELETED,
|
397
|
-
OrderStatus.FILL_CANCELLED,
|
443
|
+
OrderStatus.FILL_CANCELLED, # 不清楚对于成交数量有何影响.
|
398
444
|
}
|
399
445
|
pending_cancel_sets = {OrderStatus.CANCELLING_PART, OrderStatus.CANCELLING_ALL, }
|
400
446
|
|
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]
|
447
|
+
order_id = futu_order['order_id']
|
413
448
|
reason = ''
|
414
449
|
order_status: str = futu_order['order_status']
|
415
450
|
if order_status in bad_endings:
|
@@ -427,6 +462,23 @@ class Futu(SecuritiesBroker):
|
|
427
462
|
is_pending_cancel=is_pending_cancel,
|
428
463
|
)
|
429
464
|
|
465
|
+
def _order(self, order_id: str) -> Order:
|
466
|
+
from futu import RET_OK
|
467
|
+
|
468
|
+
with self._refresh_order_bucket:
|
469
|
+
ret, data = self._trade_client.order_list_query(
|
470
|
+
order_id=order_id,
|
471
|
+
refresh_cache=True,
|
472
|
+
trd_env=self._trd_env,
|
473
|
+
)
|
474
|
+
if ret != RET_OK:
|
475
|
+
raise Exception(f'调用获取订单失败, 订单: {order_id}')
|
476
|
+
orders = self._df_to_list(data)
|
477
|
+
if len(orders) != 1:
|
478
|
+
raise Exception(f'找不到订单(未完成), 订单: {order_id}')
|
479
|
+
futu_order = orders[0]
|
480
|
+
return self._build_order(futu_order)
|
481
|
+
|
430
482
|
async def order(self, order_id: str) -> Order:
|
431
483
|
return await self.call_sync(lambda : self._order(order_id=order_id))
|
432
484
|
|
@@ -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
|
|
@@ -193,7 +208,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
193
208
|
**kwargs
|
194
209
|
) -> str:
|
195
210
|
import ib_insync
|
196
|
-
with self._order_bucket:
|
211
|
+
async with self._order_bucket:
|
197
212
|
client = self._client
|
198
213
|
ib_contract = await self.contract_to_ib_contract(contract)
|
199
214
|
|
@@ -274,7 +289,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
274
289
|
|
275
290
|
async def _cancel_order(self, order_id: str):
|
276
291
|
order_id_int = int(order_id)
|
277
|
-
with self._order_bucket:
|
292
|
+
async with self._order_bucket:
|
278
293
|
client = self._client
|
279
294
|
trades = client.trades()
|
280
295
|
for ib_trade in trades:
|
@@ -287,9 +302,9 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
287
302
|
async def cancel_order(self, order_id: str):
|
288
303
|
await self.call_async(self._cancel_order(order_id=order_id))
|
289
304
|
|
290
|
-
|
305
|
+
@classmethod
|
306
|
+
def _build_order(cls, ib_trade):
|
291
307
|
import ib_insync
|
292
|
-
|
293
308
|
canceled_endings = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
|
294
309
|
bad_endings = {ib_insync.OrderStatus.Inactive, }
|
295
310
|
pending_cancel_sets = {ib_insync.OrderStatus.PendingCancel, }
|
@@ -304,7 +319,31 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
304
319
|
cap = sum([fill.execution.shares * fill.execution.avgPrice for fill in trade.fills], 0.0)
|
305
320
|
return round(cap / total_fills, 5)
|
306
321
|
|
307
|
-
|
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:
|
308
347
|
client = self._client
|
309
348
|
trades = client.trades()
|
310
349
|
order_id_int = int(order_id)
|
@@ -312,26 +351,7 @@ class InteractiveBrokers(SecuritiesBroker):
|
|
312
351
|
ib_order = ib_trade.order
|
313
352
|
if ib_order.permId != order_id_int:
|
314
353
|
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
|
-
)
|
354
|
+
order = self._build_order(ib_trade)
|
335
355
|
return order
|
336
356
|
raise Exception(f'查询不到订单{order_id}')
|
337
357
|
|
@@ -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,26 +396,10 @@ class LongBridge(SecuritiesBroker):
|
|
344
396
|
))
|
345
397
|
|
346
398
|
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
399
|
with self._assets_bucket:
|
360
400
|
self._try_refresh()
|
361
401
|
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
|
402
|
+
reason, is_canceled, is_pending_cancel = self._order_status(resp)
|
367
403
|
return Order(
|
368
404
|
order_id=order_id,
|
369
405
|
currency=resp.currency,
|
@@ -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,27 +374,14 @@ class Tiger(SecuritiesBroker):
|
|
318
374
|
))
|
319
375
|
|
320
376
|
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, }
|
377
|
+
from tigeropen.trade.domain.order import Order as TigerOrder
|
332
378
|
|
333
379
|
with self._order_bucket:
|
334
380
|
tiger_order: TigerOrder = self._trade_client.get_order(id=int(order_id))
|
335
381
|
if tiger_order is None:
|
336
382
|
raise Exception(f'查询不到订单{order_id}')
|
337
383
|
|
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
|
384
|
+
reason, is_canceled, is_pending_cancel = self._order_status(tiger_order)
|
342
385
|
return Order(
|
343
386
|
order_id=order_id,
|
344
387
|
currency=tiger_order.contract.currency,
|