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.
Files changed (23) hide show
  1. {httptrading-1.0.2/httptrading.egg-info → httptrading-1.0.5}/PKG-INFO +16 -7
  2. httptrading-1.0.2/PKG-INFO → httptrading-1.0.5/README.md +14 -20
  3. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/__init__.py +1 -0
  4. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/base.py +25 -1
  5. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/futu_sec.py +76 -21
  6. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/interactive_brokers.py +49 -26
  7. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/longbridge.py +56 -17
  8. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/tiger.py +61 -15
  9. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/http_server.py +275 -325
  10. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/model.py +79 -0
  11. httptrading-1.0.2/README.md → httptrading-1.0.5/httptrading.egg-info/PKG-INFO +29 -6
  12. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/requires.txt +1 -0
  13. {httptrading-1.0.2 → httptrading-1.0.5}/pyproject.toml +2 -2
  14. {httptrading-1.0.2 → httptrading-1.0.5}/LICENSE +0 -0
  15. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/broker/__init__.py +0 -0
  16. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/__init__.py +0 -0
  17. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/leaky_bucket.py +0 -0
  18. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/locate.py +0 -0
  19. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading/tool/time.py +0 -0
  20. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/SOURCES.txt +0 -0
  21. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/dependency_links.txt +0 -0
  22. {httptrading-1.0.2 → httptrading-1.0.5}/httptrading.egg-info/top_level.txt +0 -0
  23. {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.2
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
  ------------
@@ -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)
@@ -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 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]
@@ -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.df_to_dict(data)
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
- def _order(self, order_id: str) -> Order:
369
- from futu import RET_OK, OrderStatus
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
- 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]
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
- 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
 
@@ -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
- async def _order(self, order_id: str) -> Order:
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
- with self._order_bucket:
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
- 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
- )
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,