httptrading 1.0.2__py3-none-any.whl → 1.0.3__py3-none-any.whl

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/__init__.py CHANGED
@@ -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,