httptrading 1.0.2__tar.gz → 1.0.5__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.5}/PKG-INFO +16 -7
- httptrading-1.0.2/PKG-INFO → httptrading-1.0.5/README.md +14 -20
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/__init__.py +1 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/base.py +25 -1
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/futu_sec.py +76 -21
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/interactive_brokers.py +49 -26
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/longbridge.py +56 -17
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/tiger.py +61 -15
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/http_server.py +275 -325
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/model.py +79 -0
- httptrading-1.0.2/README.md → httptrading-1.0.5/httptrading.egg-info/PKG-INFO +29 -6
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/requires.txt +1 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/pyproject.toml +2 -2
- {httptrading-1.0.2 → httptrading-1.0.5}/LICENSE +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/__init__.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/__init__.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/leaky_bucket.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/locate.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/time.py +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/SOURCES.txt +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/dependency_links.txt +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/top_level.txt +0 -0
- {httptrading-1.0.2 → httptrading-1.0.5}/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.5
|
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)
|
@@ -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
|
@@ -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
|
|
@@ -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,
|