httptrading 1.0.1__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.1 → httptrading-1.0.3}/PKG-INFO +93 -25
- {httptrading-1.0.1 → httptrading-1.0.3}/README.md +92 -25
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/__init__.py +3 -2
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/base.py +24 -1
- httptrading-1.0.1/httptrading/broker/futu.py → httptrading-1.0.3/httptrading/broker/futu_sec.py +106 -21
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/interactive_brokers.py +50 -26
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/longbridge.py +55 -9
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/tiger.py +63 -3
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/http_server.py +268 -297
- httptrading-1.0.3/httptrading/model.py +261 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/PKG-INFO +93 -25
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/SOURCES.txt +1 -1
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/requires.txt +1 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/pyproject.toml +2 -2
- httptrading-1.0.1/httptrading/model.py +0 -174
- {httptrading-1.0.1 → httptrading-1.0.3}/LICENSE +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/__init__.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/__init__.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/leaky_bucket.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/locate.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/time.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/dependency_links.txt +0 -0
- {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/top_level.txt +0 -0
- {httptrading-1.0.1 → 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
|
|
@@ -407,30 +412,35 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
|
|
407
412
|
|
408
413
|
```json lines
|
409
414
|
{
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
415
|
+
"type": "apiResponse",
|
416
|
+
"instanceId": "ggUqPZbSKuQ7Ewsk",
|
417
|
+
"broker": "futu",
|
418
|
+
"brokerDisplay": "富途证券",
|
419
|
+
"time": "2025-05-28T05:59:29.984021+00:00",
|
420
|
+
"ex": null,
|
421
|
+
"order": {
|
422
|
+
"type": "order",
|
423
|
+
"orderId": "6278888",
|
424
|
+
"currency": "USD",
|
425
|
+
"qty": 12, // 订单数量
|
426
|
+
"filledQty": 0, // 已成交数量
|
427
|
+
"avgPrice": 0, // 成交价
|
428
|
+
"errorReason": "", // 如果订单异常, 这里记录错误信息
|
429
|
+
"isCanceled": false, // 是否已撤销
|
430
|
+
"isFilled": false, // 是否全部成交
|
431
|
+
"isCompleted": false, // 全部成交 或者 有订单异常 或者 已撤销
|
432
|
+
"isCancelable": true // 是否可撤的标志, 等价于 not isCompleted and not isPendingCancel
|
433
|
+
}
|
428
434
|
}
|
429
435
|
```
|
436
|
+
⚠️ 有些交易通道不支持查询单个订单状态, 而是查询活动订单的列表来实现, 意味着订单在结束周期的交易日之后, 将查不到订单.
|
430
437
|
|
431
|
-
|
432
|
-
|
433
|
-
|
438
|
+
| 交易通道 | 支持查单个订单 |
|
439
|
+
|------|---------|
|
440
|
+
| 盈透证券 | ❌ |
|
441
|
+
| 富途证券 | ❌ |
|
442
|
+
| 长桥证券 | ✅ |
|
443
|
+
| 老虎证券 | ✅ |
|
434
444
|
|
435
445
|
交易通道的参数
|
436
446
|
------------
|
@@ -503,8 +513,8 @@ from httptrading import *
|
|
503
513
|
from httptrading.model import *
|
504
514
|
|
505
515
|
|
506
|
-
@broker_register('
|
507
|
-
class
|
516
|
+
@broker_register('myBroker', 'XX证券')
|
517
|
+
class MyBroker(BaseBroker):
|
508
518
|
# 根据需要的功能实现接口
|
509
519
|
# 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
|
510
520
|
async def place_order(
|
@@ -538,6 +548,64 @@ class MyTradingApi(BaseBroker):
|
|
538
548
|
async def quote(self, contract: Contract) -> Quote:
|
539
549
|
raise NotImplementedError
|
540
550
|
|
541
|
-
async def market_status(self) -> dict[
|
551
|
+
async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
|
542
552
|
raise NotImplementedError
|
543
553
|
```
|
554
|
+
|
555
|
+
|
556
|
+
编写新接口
|
557
|
+
--------
|
558
|
+
|
559
|
+
```python
|
560
|
+
import aiohttp.web
|
561
|
+
import httptrading
|
562
|
+
|
563
|
+
class MyApi(httptrading.HttpTradingView):
|
564
|
+
async def get(self):
|
565
|
+
broker = self.current_broker()
|
566
|
+
return self.response_api(
|
567
|
+
broker=broker,
|
568
|
+
args={
|
569
|
+
'method': 'GET',
|
570
|
+
'hello': 'world',
|
571
|
+
},
|
572
|
+
)
|
573
|
+
|
574
|
+
async def post(self):
|
575
|
+
body_d: dict = await self.request.json()
|
576
|
+
broker = self.current_broker()
|
577
|
+
return self.response_api(
|
578
|
+
broker=broker,
|
579
|
+
args={
|
580
|
+
'method': 'POST',
|
581
|
+
'hello': 'world',
|
582
|
+
'body': body_d,
|
583
|
+
},
|
584
|
+
)
|
585
|
+
|
586
|
+
httptrading.run(
|
587
|
+
host='127.0.0.1',
|
588
|
+
port=8080,
|
589
|
+
brokers=list(),
|
590
|
+
extend_apis=[aiohttp.web.view(r'/httptrading/api/{instance_id:\w{16,32}}/hello/world', MyApi), ],
|
591
|
+
)
|
592
|
+
```
|
593
|
+
|
594
|
+
改变默认的接口
|
595
|
+
-----------
|
596
|
+
|
597
|
+
```python
|
598
|
+
import httptrading
|
599
|
+
|
600
|
+
def my_std_apis():
|
601
|
+
std_apis = httptrading.std_api_factory()
|
602
|
+
apis = [api for api in std_apis if 1 == 1]
|
603
|
+
return apis
|
604
|
+
|
605
|
+
httptrading.run(
|
606
|
+
host='127.0.0.1',
|
607
|
+
port=8080,
|
608
|
+
brokers=list(),
|
609
|
+
std_apis=my_std_apis,
|
610
|
+
)
|
611
|
+
```
|
@@ -383,6 +383,10 @@ POST /httptrading/api/{instanceId}/order/cancel
|
|
383
383
|
}
|
384
384
|
```
|
385
385
|
|
386
|
+
```
|
387
|
+
⚠️ 撤单接口仅完成对交易通道的撤单接口调用, 不代表在较短时间后订单可以进入撤销的状态. 一个例子是假日发起撤单, 通道不一定执行撤单而是进入已请求撤单的状态.
|
388
|
+
```
|
389
|
+
|
386
390
|
|
387
391
|
### 查询单个订单
|
388
392
|
|
@@ -393,30 +397,35 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
|
|
393
397
|
|
394
398
|
```json lines
|
395
399
|
{
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
400
|
+
"type": "apiResponse",
|
401
|
+
"instanceId": "ggUqPZbSKuQ7Ewsk",
|
402
|
+
"broker": "futu",
|
403
|
+
"brokerDisplay": "富途证券",
|
404
|
+
"time": "2025-05-28T05:59:29.984021+00:00",
|
405
|
+
"ex": null,
|
406
|
+
"order": {
|
407
|
+
"type": "order",
|
408
|
+
"orderId": "6278888",
|
409
|
+
"currency": "USD",
|
410
|
+
"qty": 12, // 订单数量
|
411
|
+
"filledQty": 0, // 已成交数量
|
412
|
+
"avgPrice": 0, // 成交价
|
413
|
+
"errorReason": "", // 如果订单异常, 这里记录错误信息
|
414
|
+
"isCanceled": false, // 是否已撤销
|
415
|
+
"isFilled": false, // 是否全部成交
|
416
|
+
"isCompleted": false, // 全部成交 或者 有订单异常 或者 已撤销
|
417
|
+
"isCancelable": true // 是否可撤的标志, 等价于 not isCompleted and not isPendingCancel
|
418
|
+
}
|
414
419
|
}
|
415
420
|
```
|
421
|
+
⚠️ 有些交易通道不支持查询单个订单状态, 而是查询活动订单的列表来实现, 意味着订单在结束周期的交易日之后, 将查不到订单.
|
416
422
|
|
417
|
-
|
418
|
-
|
419
|
-
|
423
|
+
| 交易通道 | 支持查单个订单 |
|
424
|
+
|------|---------|
|
425
|
+
| 盈透证券 | ❌ |
|
426
|
+
| 富途证券 | ❌ |
|
427
|
+
| 长桥证券 | ✅ |
|
428
|
+
| 老虎证券 | ✅ |
|
420
429
|
|
421
430
|
交易通道的参数
|
422
431
|
------------
|
@@ -489,8 +498,8 @@ from httptrading import *
|
|
489
498
|
from httptrading.model import *
|
490
499
|
|
491
500
|
|
492
|
-
@broker_register('
|
493
|
-
class
|
501
|
+
@broker_register('myBroker', 'XX证券')
|
502
|
+
class MyBroker(BaseBroker):
|
494
503
|
# 根据需要的功能实现接口
|
495
504
|
# 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
|
496
505
|
async def place_order(
|
@@ -524,6 +533,64 @@ class MyTradingApi(BaseBroker):
|
|
524
533
|
async def quote(self, contract: Contract) -> Quote:
|
525
534
|
raise NotImplementedError
|
526
535
|
|
527
|
-
async def market_status(self) -> dict[
|
536
|
+
async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
|
528
537
|
raise NotImplementedError
|
529
|
-
```
|
538
|
+
```
|
539
|
+
|
540
|
+
|
541
|
+
编写新接口
|
542
|
+
--------
|
543
|
+
|
544
|
+
```python
|
545
|
+
import aiohttp.web
|
546
|
+
import httptrading
|
547
|
+
|
548
|
+
class MyApi(httptrading.HttpTradingView):
|
549
|
+
async def get(self):
|
550
|
+
broker = self.current_broker()
|
551
|
+
return self.response_api(
|
552
|
+
broker=broker,
|
553
|
+
args={
|
554
|
+
'method': 'GET',
|
555
|
+
'hello': 'world',
|
556
|
+
},
|
557
|
+
)
|
558
|
+
|
559
|
+
async def post(self):
|
560
|
+
body_d: dict = await self.request.json()
|
561
|
+
broker = self.current_broker()
|
562
|
+
return self.response_api(
|
563
|
+
broker=broker,
|
564
|
+
args={
|
565
|
+
'method': 'POST',
|
566
|
+
'hello': 'world',
|
567
|
+
'body': body_d,
|
568
|
+
},
|
569
|
+
)
|
570
|
+
|
571
|
+
httptrading.run(
|
572
|
+
host='127.0.0.1',
|
573
|
+
port=8080,
|
574
|
+
brokers=list(),
|
575
|
+
extend_apis=[aiohttp.web.view(r'/httptrading/api/{instance_id:\w{16,32}}/hello/world', MyApi), ],
|
576
|
+
)
|
577
|
+
```
|
578
|
+
|
579
|
+
改变默认的接口
|
580
|
+
-----------
|
581
|
+
|
582
|
+
```python
|
583
|
+
import httptrading
|
584
|
+
|
585
|
+
def my_std_apis():
|
586
|
+
std_apis = httptrading.std_api_factory()
|
587
|
+
apis = [api for api in std_apis if 1 == 1]
|
588
|
+
return apis
|
589
|
+
|
590
|
+
httptrading.run(
|
591
|
+
host='127.0.0.1',
|
592
|
+
port=8080,
|
593
|
+
brokers=list(),
|
594
|
+
std_apis=my_std_apis,
|
595
|
+
)
|
596
|
+
```
|
@@ -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
|
@@ -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)
|
httptrading-1.0.1/httptrading/broker/futu.py → httptrading-1.0.3/httptrading/broker/futu_sec.py
RENAMED
@@ -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))
|