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.
Files changed (24) hide show
  1. {httptrading-1.0.1 → httptrading-1.0.3}/PKG-INFO +93 -25
  2. {httptrading-1.0.1 → httptrading-1.0.3}/README.md +92 -25
  3. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/__init__.py +3 -2
  4. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/base.py +24 -1
  5. httptrading-1.0.1/httptrading/broker/futu.py → httptrading-1.0.3/httptrading/broker/futu_sec.py +106 -21
  6. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/interactive_brokers.py +50 -26
  7. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/longbridge.py +55 -9
  8. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/tiger.py +63 -3
  9. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/http_server.py +268 -297
  10. httptrading-1.0.3/httptrading/model.py +261 -0
  11. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/PKG-INFO +93 -25
  12. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/SOURCES.txt +1 -1
  13. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/requires.txt +1 -0
  14. {httptrading-1.0.1 → httptrading-1.0.3}/pyproject.toml +2 -2
  15. httptrading-1.0.1/httptrading/model.py +0 -174
  16. {httptrading-1.0.1 → httptrading-1.0.3}/LICENSE +0 -0
  17. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/broker/__init__.py +0 -0
  18. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/__init__.py +0 -0
  19. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/leaky_bucket.py +0 -0
  20. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/locate.py +0 -0
  21. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading/tool/time.py +0 -0
  22. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/dependency_links.txt +0 -0
  23. {httptrading-1.0.1 → httptrading-1.0.3}/httptrading.egg-info/top_level.txt +0 -0
  24. {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.1
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
- "type": "apiResponse",
411
- "instanceId": "ggUqPZbSKuQ7Ewsk",
412
- "broker": "futu",
413
- "brokerDisplay": "富途证券",
414
- "time": "2025-05-28T05:59:29.984021+00:00",
415
- "ex": null,
416
- "order": {
417
- "type": "order",
418
- "orderId": "6278888",
419
- "currency": "USD",
420
- "qty": 12, // 订单数量
421
- "filledQty": 0, // 已成交数量
422
- "avgPrice": 0, // 成交价
423
- "errorReason": "", // 如果订单异常, 这里记录错误信息
424
- "isCanceled": false, // 是否已撤销
425
- "isFilled": false, // 是否全部成交
426
- "isCompleted": false // 全部成交 或者 有异常 或者 已撤销, 亦等价于不可撤的标志
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('myApi', 'XX证券')
507
- class MyTradingApi(BaseBroker):
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[str, dict[str, MarketStatus]]:
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
- "type": "apiResponse",
397
- "instanceId": "ggUqPZbSKuQ7Ewsk",
398
- "broker": "futu",
399
- "brokerDisplay": "富途证券",
400
- "time": "2025-05-28T05:59:29.984021+00:00",
401
- "ex": null,
402
- "order": {
403
- "type": "order",
404
- "orderId": "6278888",
405
- "currency": "USD",
406
- "qty": 12, // 订单数量
407
- "filledQty": 0, // 已成交数量
408
- "avgPrice": 0, // 成交价
409
- "errorReason": "", // 如果订单异常, 这里记录错误信息
410
- "isCanceled": false, // 是否已撤销
411
- "isFilled": false, // 是否全部成交
412
- "isCompleted": false // 全部成交 或者 有异常 或者 已撤销, 亦等价于不可撤的标志
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('myApi', 'XX证券')
493
- class MyTradingApi(BaseBroker):
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[str, dict[str, MarketStatus]]:
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.futu import *
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 run
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
- pkg_info = BrokerRegister.get_meta(type(self)).detect_package
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 df_to_dict(cls, df) -> dict:
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.df_to_dict(data)
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.df_to_dict(data)
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.df_to_dict(data)
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.df_to_dict(data)
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, OrderStatus
370
- error_set = {OrderStatus.FAILED, OrderStatus.DISABLED, OrderStatus.DELETED, }
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.df_to_dict(data)
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
- reason = ''
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))