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.
Files changed (23) hide show
  1. {httptrading-1.0.2/httptrading.egg-info → httptrading-1.0.3}/PKG-INFO +16 -7
  2. httptrading-1.0.2/PKG-INFO → httptrading-1.0.3/README.md +14 -20
  3. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/__init__.py +1 -0
  4. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/base.py +24 -1
  5. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/futu_sec.py +73 -21
  6. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/interactive_brokers.py +46 -26
  7. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/longbridge.py +53 -17
  8. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/tiger.py +58 -15
  9. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/http_server.py +267 -325
  10. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/model.py +79 -0
  11. httptrading-1.0.2/README.md → httptrading-1.0.3/httptrading.egg-info/PKG-INFO +29 -6
  12. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/requires.txt +1 -0
  13. {httptrading-1.0.2 → httptrading-1.0.3}/pyproject.toml +2 -2
  14. {httptrading-1.0.2 → httptrading-1.0.3}/LICENSE +0 -0
  15. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/broker/__init__.py +0 -0
  16. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/__init__.py +0 -0
  17. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/leaky_bucket.py +0 -0
  18. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/locate.py +0 -0
  19. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading/tool/time.py +0 -0
  20. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/SOURCES.txt +0 -0
  21. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/dependency_links.txt +0 -0
  22. {httptrading-1.0.2 → httptrading-1.0.3}/httptrading.egg-info/top_level.txt +0 -0
  23. {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.2
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
  ------------
@@ -4,3 +4,4 @@ from httptrading.broker.longbridge import *
4
4
  from httptrading.broker.tiger import *
5
5
  from httptrading.broker.interactive_brokers import *
6
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,9 +411,9 @@ class Futu(SecuritiesBroker):
365
411
  **kwargs
366
412
  ))
367
413
 
368
- def _order(self, order_id: str) -> Order:
369
- from futu import RET_OK, OrderStatus
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
- with self._refresh_order_bucket:
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
- pass
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
- async def _order(self, order_id: str) -> Order:
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
- with self._order_bucket:
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
- qty = int(_total_fills(ib_trade) + ib_trade.remaining())
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
- order_status = tiger_order.status
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,