httptrading 1.0.0__tar.gz → 1.0.2__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.0 → httptrading-1.0.2}/PKG-INFO +114 -50
  2. {httptrading-1.0.0 → httptrading-1.0.2}/README.md +114 -50
  3. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/__init__.py +2 -2
  4. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/broker/base.py +7 -1
  5. httptrading-1.0.0/httptrading/broker/futu.py → httptrading-1.0.2/httptrading/broker/futu_sec.py +41 -8
  6. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/broker/interactive_brokers.py +34 -10
  7. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/broker/longbridge.py +17 -7
  8. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/broker/tiger.py +22 -5
  9. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/http_server.py +45 -14
  10. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/model.py +8 -0
  11. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading.egg-info/PKG-INFO +114 -50
  12. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading.egg-info/SOURCES.txt +1 -1
  13. {httptrading-1.0.0 → httptrading-1.0.2}/pyproject.toml +1 -1
  14. {httptrading-1.0.0 → httptrading-1.0.2}/LICENSE +0 -0
  15. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/broker/__init__.py +0 -0
  16. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/tool/__init__.py +0 -0
  17. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/tool/leaky_bucket.py +0 -0
  18. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/tool/locate.py +0 -0
  19. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading/tool/time.py +0 -0
  20. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading.egg-info/dependency_links.txt +0 -0
  21. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading.egg-info/requires.txt +0 -0
  22. {httptrading-1.0.0 → httptrading-1.0.2}/httptrading.egg-info/top_level.txt +0 -0
  23. {httptrading-1.0.0 → httptrading-1.0.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httptrading
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: 统一交易通道的接口服务
5
5
  Author-email: songwei <github@songwei.name>
6
6
  License: MIT
@@ -14,6 +14,10 @@ Dynamic: license-file
14
14
 
15
15
  # httptrading
16
16
 
17
+ ```shell
18
+ pip install httptrading
19
+ ```
20
+
17
21
  项目的用途
18
22
  --------
19
23
 
@@ -221,34 +225,35 @@ GET /httptrading/api/{instanceId}/market/state
221
225
 
222
226
  ```json lines
223
227
  {
224
- "type": "apiResponse",
225
- "instanceId": "ggUqPZbSKuQ7Ewsk",
226
- "broker": "futu",
227
- "brokerDisplay": "富途证券",
228
- "time": "2025-05-28T05:33:42.543109+00:00",
229
- "ex": null,
230
- "marketStatus": {
231
- "securities": { // 证券类市场状态, 以 region 为键的结构
232
- "US": {
233
- "type": "marketStatus",
234
- "region": "US",
235
- "originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
236
- "unifiedStatus": "CLOSED" // 统一映射的定义
237
- },
238
- "CN": {
239
- "type": "marketStatus",
240
- "region": "CN",
241
- "originStatus": "AFTERNOON",
242
- "unifiedStatus": "RTH"
243
- },
244
- "HK": {
245
- "type": "marketStatus",
246
- "region": "HK",
247
- "originStatus": "AFTERNOON",
248
- "unifiedStatus": "RTH"
249
- }
250
- }
251
- }
228
+ "type": "apiResponse",
229
+ "instanceId": "ggUqPZbSKuQ7Ewsk",
230
+ "broker": "futu",
231
+ "brokerDisplay": "富途证券",
232
+ "time": "2025-05-28T05:33:42.543109+00:00",
233
+ "ex": null,
234
+ "marketStatus": {
235
+ "type": "marketStatusMap",
236
+ "securities": { // 证券类市场状态, 以 region 为键的结构
237
+ "US": {
238
+ "type": "marketStatus",
239
+ "region": "US",
240
+ "originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
241
+ "unifiedStatus": "CLOSED" // 统一映射的定义
242
+ },
243
+ "CN": {
244
+ "type": "marketStatus",
245
+ "region": "CN",
246
+ "originStatus": "AFTERNOON",
247
+ "unifiedStatus": "RTH"
248
+ },
249
+ "HK": {
250
+ "type": "marketStatus",
251
+ "region": "HK",
252
+ "originStatus": "AFTERNOON",
253
+ "unifiedStatus": "RTH"
254
+ }
255
+ }
256
+ }
252
257
  }
253
258
  ```
254
259
 
@@ -402,24 +407,25 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
402
407
 
403
408
  ```json lines
404
409
  {
405
- "type": "apiResponse",
406
- "instanceId": "ggUqPZbSKuQ7Ewsk",
407
- "broker": "futu",
408
- "brokerDisplay": "富途证券",
409
- "time": "2025-05-28T05:59:29.984021+00:00",
410
- "ex": null,
411
- "order": {
412
- "type": "order",
413
- "orderId": "6278888",
414
- "currency": "USD",
415
- "qty": 12, // 订单数量
416
- "filledQty": 0, // 已成交数量
417
- "avgPrice": 0, // 成交价
418
- "errorReason": "", // 如果订单异常, 这里记录错误信息
419
- "isCanceled": false, // 是否已撤销
420
- "isFilled": false, // 是否全部成交
421
- "isCompleted": false // 全部成交 或者 有异常 或者 已撤销, 亦等价于不可撤的标志
422
- }
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
+ "isCancelable": true // 是否可撤的标志
428
+ }
423
429
  }
424
430
  ```
425
431
 
@@ -498,8 +504,8 @@ from httptrading import *
498
504
  from httptrading.model import *
499
505
 
500
506
 
501
- @broker_register('myApi', 'XX证券')
502
- class MyTradingApi(BaseBroker):
507
+ @broker_register('myBroker', 'XX证券')
508
+ class MyBroker(BaseBroker):
503
509
  # 根据需要的功能实现接口
504
510
  # 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
505
511
  async def place_order(
@@ -533,6 +539,64 @@ class MyTradingApi(BaseBroker):
533
539
  async def quote(self, contract: Contract) -> Quote:
534
540
  raise NotImplementedError
535
541
 
536
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
542
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
537
543
  raise NotImplementedError
538
544
  ```
545
+
546
+
547
+ 编写新接口
548
+ --------
549
+
550
+ ```python
551
+ import aiohttp.web
552
+ import httptrading
553
+
554
+ class MyApi(httptrading.HttpTradingView):
555
+ async def get(self):
556
+ broker = self.current_broker()
557
+ return self.response_api(
558
+ broker=broker,
559
+ args={
560
+ 'method': 'GET',
561
+ 'hello': 'world',
562
+ },
563
+ )
564
+
565
+ async def post(self):
566
+ body_d: dict = await self.request.json()
567
+ broker = self.current_broker()
568
+ return self.response_api(
569
+ broker=broker,
570
+ args={
571
+ 'method': 'POST',
572
+ 'hello': 'world',
573
+ 'body': body_d,
574
+ },
575
+ )
576
+
577
+ httptrading.run(
578
+ host='127.0.0.1',
579
+ port=8080,
580
+ brokers=list(),
581
+ extend_apis=[aiohttp.web.view(r'/httptrading/api/{instance_id:\w{16,32}}/hello/world', MyApi), ],
582
+ )
583
+ ```
584
+
585
+ 改变默认的接口
586
+ -----------
587
+
588
+ ```python
589
+ import httptrading
590
+
591
+ def my_std_apis():
592
+ std_apis = httptrading.std_api_factory()
593
+ apis = [api for api in std_apis if 1 == 1]
594
+ return apis
595
+
596
+ httptrading.run(
597
+ host='127.0.0.1',
598
+ port=8080,
599
+ brokers=list(),
600
+ std_apis=my_std_apis,
601
+ )
602
+ ```
@@ -1,5 +1,9 @@
1
1
  # httptrading
2
2
 
3
+ ```shell
4
+ pip install httptrading
5
+ ```
6
+
3
7
  项目的用途
4
8
  --------
5
9
 
@@ -207,34 +211,35 @@ GET /httptrading/api/{instanceId}/market/state
207
211
 
208
212
  ```json lines
209
213
  {
210
- "type": "apiResponse",
211
- "instanceId": "ggUqPZbSKuQ7Ewsk",
212
- "broker": "futu",
213
- "brokerDisplay": "富途证券",
214
- "time": "2025-05-28T05:33:42.543109+00:00",
215
- "ex": null,
216
- "marketStatus": {
217
- "securities": { // 证券类市场状态, 以 region 为键的结构
218
- "US": {
219
- "type": "marketStatus",
220
- "region": "US",
221
- "originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
222
- "unifiedStatus": "CLOSED" // 统一映射的定义
223
- },
224
- "CN": {
225
- "type": "marketStatus",
226
- "region": "CN",
227
- "originStatus": "AFTERNOON",
228
- "unifiedStatus": "RTH"
229
- },
230
- "HK": {
231
- "type": "marketStatus",
232
- "region": "HK",
233
- "originStatus": "AFTERNOON",
234
- "unifiedStatus": "RTH"
235
- }
236
- }
237
- }
214
+ "type": "apiResponse",
215
+ "instanceId": "ggUqPZbSKuQ7Ewsk",
216
+ "broker": "futu",
217
+ "brokerDisplay": "富途证券",
218
+ "time": "2025-05-28T05:33:42.543109+00:00",
219
+ "ex": null,
220
+ "marketStatus": {
221
+ "type": "marketStatusMap",
222
+ "securities": { // 证券类市场状态, 以 region 为键的结构
223
+ "US": {
224
+ "type": "marketStatus",
225
+ "region": "US",
226
+ "originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
227
+ "unifiedStatus": "CLOSED" // 统一映射的定义
228
+ },
229
+ "CN": {
230
+ "type": "marketStatus",
231
+ "region": "CN",
232
+ "originStatus": "AFTERNOON",
233
+ "unifiedStatus": "RTH"
234
+ },
235
+ "HK": {
236
+ "type": "marketStatus",
237
+ "region": "HK",
238
+ "originStatus": "AFTERNOON",
239
+ "unifiedStatus": "RTH"
240
+ }
241
+ }
242
+ }
238
243
  }
239
244
  ```
240
245
 
@@ -388,24 +393,25 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
388
393
 
389
394
  ```json lines
390
395
  {
391
- "type": "apiResponse",
392
- "instanceId": "ggUqPZbSKuQ7Ewsk",
393
- "broker": "futu",
394
- "brokerDisplay": "富途证券",
395
- "time": "2025-05-28T05:59:29.984021+00:00",
396
- "ex": null,
397
- "order": {
398
- "type": "order",
399
- "orderId": "6278888",
400
- "currency": "USD",
401
- "qty": 12, // 订单数量
402
- "filledQty": 0, // 已成交数量
403
- "avgPrice": 0, // 成交价
404
- "errorReason": "", // 如果订单异常, 这里记录错误信息
405
- "isCanceled": false, // 是否已撤销
406
- "isFilled": false, // 是否全部成交
407
- "isCompleted": false // 全部成交 或者 有异常 或者 已撤销, 亦等价于不可撤的标志
408
- }
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
+ "isCancelable": true // 是否可撤的标志
414
+ }
409
415
  }
410
416
  ```
411
417
 
@@ -484,8 +490,8 @@ from httptrading import *
484
490
  from httptrading.model import *
485
491
 
486
492
 
487
- @broker_register('myApi', 'XX证券')
488
- class MyTradingApi(BaseBroker):
493
+ @broker_register('myBroker', 'XX证券')
494
+ class MyBroker(BaseBroker):
489
495
  # 根据需要的功能实现接口
490
496
  # 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
491
497
  async def place_order(
@@ -519,6 +525,64 @@ class MyTradingApi(BaseBroker):
519
525
  async def quote(self, contract: Contract) -> Quote:
520
526
  raise NotImplementedError
521
527
 
522
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
528
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
523
529
  raise NotImplementedError
524
- ```
530
+ ```
531
+
532
+
533
+ 编写新接口
534
+ --------
535
+
536
+ ```python
537
+ import aiohttp.web
538
+ import httptrading
539
+
540
+ class MyApi(httptrading.HttpTradingView):
541
+ async def get(self):
542
+ broker = self.current_broker()
543
+ return self.response_api(
544
+ broker=broker,
545
+ args={
546
+ 'method': 'GET',
547
+ 'hello': 'world',
548
+ },
549
+ )
550
+
551
+ async def post(self):
552
+ body_d: dict = await self.request.json()
553
+ broker = self.current_broker()
554
+ return self.response_api(
555
+ broker=broker,
556
+ args={
557
+ 'method': 'POST',
558
+ 'hello': 'world',
559
+ 'body': body_d,
560
+ },
561
+ )
562
+
563
+ httptrading.run(
564
+ host='127.0.0.1',
565
+ port=8080,
566
+ brokers=list(),
567
+ extend_apis=[aiohttp.web.view(r'/httptrading/api/{instance_id:\w{16,32}}/hello/world', MyApi), ],
568
+ )
569
+ ```
570
+
571
+ 改变默认的接口
572
+ -----------
573
+
574
+ ```python
575
+ import httptrading
576
+
577
+ def my_std_apis():
578
+ std_apis = httptrading.std_api_factory()
579
+ apis = [api for api in std_apis if 1 == 1]
580
+ return apis
581
+
582
+ httptrading.run(
583
+ host='127.0.0.1',
584
+ port=8080,
585
+ brokers=list(),
586
+ std_apis=my_std_apis,
587
+ )
588
+ ```
@@ -1,6 +1,6 @@
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 *
@@ -81,7 +81,13 @@ class BaseBroker(ABC):
81
81
  async def quote(self, contract: Contract) -> Quote:
82
82
  raise NotImplementedError
83
83
 
84
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
84
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
85
+ """
86
+ 报告交易通道提供的市场状态,
87
+ 返回一个双层字典,
88
+ 外层字典是以交易品种分类的结构, 比如 TradeType.Securities,
89
+ 内层的字典是按国家代码区分的各个市场状态的结构, 比如 "US".
90
+ """
85
91
  raise NotImplementedError
86
92
 
87
93
 
@@ -166,7 +166,7 @@ class Futu(SecuritiesBroker):
166
166
  async def cash(self) -> Cash:
167
167
  return await self.call_sync(lambda : self._cash())
168
168
 
169
- def _market_status(self) -> dict[str, dict[str, MarketStatus]]:
169
+ def _market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
170
170
  # 各个市场的状态定义见:
171
171
  # https://openapi.futunn.com/futu-api-doc/qa/quote.html#2090
172
172
  from futu import RET_OK
@@ -204,10 +204,10 @@ class Futu(SecuritiesBroker):
204
204
  unified_status=unified_status,
205
205
  )
206
206
  return {
207
- TradeType.Securities.name.lower(): sec_result,
207
+ TradeType.Securities: sec_result,
208
208
  }
209
209
 
210
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
210
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
211
211
  return await self.call_sync(lambda : self._market_status())
212
212
 
213
213
  def _quote(self, contract: Contract):
@@ -367,8 +367,37 @@ class Futu(SecuritiesBroker):
367
367
 
368
368
  def _order(self, order_id: str) -> Order:
369
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, }
370
+
371
+ """
372
+ 富途证券的状态定义
373
+ NONE = "N/A" # 未知状态
374
+ UNSUBMITTED = "UNSUBMITTED" # 未提交
375
+ WAITING_SUBMIT = "WAITING_SUBMIT" # 等待提交
376
+ SUBMITTING = "SUBMITTING" # 提交中
377
+ SUBMIT_FAILED = "SUBMIT_FAILED" # 提交失败,下单失败
378
+ TIMEOUT = "TIMEOUT" # 处理超时,结果未知
379
+ SUBMITTED = "SUBMITTED" # 已提交,等待成交
380
+ FILLED_PART = "FILLED_PART" # 部分成交
381
+ FILLED_ALL = "FILLED_ALL" # 全部已成
382
+ CANCELLING_PART = "CANCELLING_PART" # 正在撤单_部分(部分已成交,正在撤销剩余部分)
383
+ CANCELLING_ALL = "CANCELLING_ALL" # 正在撤单_全部
384
+ CANCELLED_PART = "CANCELLED_PART" # 部分成交,剩余部分已撤单
385
+ CANCELLED_ALL = "CANCELLED_ALL" # 全部已撤单,无成交
386
+ FAILED = "FAILED" # 下单失败,服务拒绝
387
+ DISABLED = "DISABLED" # 已失效
388
+ DELETED = "DELETED" # 已删除,无成交的订单才能删除
389
+ FILL_CANCELLED = "FILL_CANCELLED" # 成交被撤销,一般遇不到,意思是已经成交的订单被回滚撤销,成交无效变为废单
390
+ """
391
+ canceled_endings = {OrderStatus.CANCELLED_ALL, OrderStatus.CANCELLED_PART, }
392
+ bad_endings = {
393
+ OrderStatus.SUBMIT_FAILED,
394
+ OrderStatus.FAILED,
395
+ OrderStatus.DISABLED,
396
+ OrderStatus.DELETED,
397
+ OrderStatus.FILL_CANCELLED, # 不清楚对于成交数量有何影响.
398
+ }
399
+ pending_cancel_sets = {OrderStatus.CANCELLING_PART, OrderStatus.CANCELLING_ALL, }
400
+
372
401
  with self._refresh_order_bucket:
373
402
  ret, data = self._trade_client.order_list_query(
374
403
  order_id=order_id,
@@ -382,8 +411,11 @@ class Futu(SecuritiesBroker):
382
411
  raise Exception(f'找不到订单(未完成), 订单: {order_id}')
383
412
  futu_order = orders[0]
384
413
  reason = ''
385
- if futu_order['order_status'] in error_set:
386
- reason = futu_order['order_status']
414
+ order_status: str = futu_order['order_status']
415
+ if order_status in bad_endings:
416
+ reason = order_status
417
+ is_canceled = order_status in canceled_endings
418
+ is_pending_cancel = order_status in pending_cancel_sets
387
419
  return Order(
388
420
  order_id=order_id,
389
421
  currency=futu_order['currency'],
@@ -391,7 +423,8 @@ class Futu(SecuritiesBroker):
391
423
  filled_qty=int(futu_order['dealt_qty']),
392
424
  avg_price=futu_order['dealt_avg_price'] or 0.0,
393
425
  error_reason=reason,
394
- is_canceled=futu_order['order_status'] in cancel_set,
426
+ is_canceled=is_canceled,
427
+ is_pending_cancel=is_pending_cancel,
395
428
  )
396
429
 
397
430
  async def order(self, order_id: str) -> Order:
@@ -74,7 +74,6 @@ class InteractiveBrokers(SecuritiesBroker):
74
74
  async with self._lock:
75
75
  if contract in self._ib_contracts:
76
76
  return self._ib_contracts[contract]
77
- print(f'{contract}未命中')
78
77
  currency = self.contract_to_currency(contract)
79
78
  ib_contract = ib_insync.Stock(contract.ticker, 'SMART', currency=currency)
80
79
  client = self._client
@@ -82,6 +81,15 @@ class InteractiveBrokers(SecuritiesBroker):
82
81
  self._ib_contracts[contract] = ib_contract
83
82
  return ib_contract
84
83
 
84
+ def _when_create_client(self, client):
85
+ import ib_insync
86
+ client: ib_insync.IB = client
87
+
88
+ def _order_status_changed(trade: ib_insync.Trade):
89
+ pass
90
+
91
+ client.orderStatusEvent += _order_status_changed
92
+
85
93
  async def _try_create_client(self):
86
94
  import ib_insync
87
95
  async with self._lock:
@@ -101,6 +109,7 @@ class InteractiveBrokers(SecuritiesBroker):
101
109
  ib = ib_socket
102
110
  else:
103
111
  ib = ib_insync.IB()
112
+ self._when_create_client(ib)
104
113
  host = self.broker_args.get('host', '127.0.0.1')
105
114
  port = self.broker_args.get('port', 4000)
106
115
  client_id = self.broker_args.get('client_id', self._client_id)
@@ -222,13 +231,23 @@ class InteractiveBrokers(SecuritiesBroker):
222
231
  case _:
223
232
  raise Exception(f'不支持的订单类型: {order_type}')
224
233
 
234
+ evt = asyncio.Event()
235
+ def _status_evnet(_trade: ib_insync.Trade):
236
+ evt.set()
237
+
225
238
  ib_order = _map_order()
226
239
  ib_order.tif = _map_time_in_force()
227
240
  ib_order.outsideRth = _map_lifecycle()
228
241
  trade: ib_insync.Trade = client.placeOrder(ib_contract, ib_order)
229
- await asyncio.sleep(2.0)
230
- order_id = str(trade.order.permId)
242
+ trade.statusEvent += _status_evnet
243
+ try:
244
+ await asyncio.wait_for(evt.wait(), timeout=2.0)
245
+ except asyncio.TimeoutError:
246
+ pass
247
+ finally:
248
+ order_id = str(trade.order.permId)
231
249
  assert order_id
250
+ order_id = str(trade.order.permId)
232
251
  return order_id
233
252
 
234
253
  async def place_order(
@@ -271,6 +290,10 @@ class InteractiveBrokers(SecuritiesBroker):
271
290
  async def _order(self, order_id: str) -> Order:
272
291
  import ib_insync
273
292
 
293
+ canceled_endings = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
294
+ bad_endings = {ib_insync.OrderStatus.Inactive, }
295
+ pending_cancel_sets = {ib_insync.OrderStatus.PendingCancel, }
296
+
274
297
  def _total_fills(trade) -> int:
275
298
  return int(trade.filled())
276
299
 
@@ -295,20 +318,21 @@ class InteractiveBrokers(SecuritiesBroker):
295
318
  assert qty >= filled_qty
296
319
  avg_fill_price = _avg_price(ib_trade)
297
320
  reason = ''
298
- if ib_trade.orderStatus.status == ib_insync.OrderStatus.Inactive:
299
- reason = 'Inactive'
300
-
301
- cancel_status = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
302
- is_cancelled = ib_trade.orderStatus.status in cancel_status
303
- return Order(
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(
304
326
  order_id=order_id,
305
327
  currency=ib_trade.contract.currency,
306
328
  qty=qty,
307
329
  filled_qty=filled_qty,
308
330
  avg_price=avg_fill_price,
309
331
  error_reason=reason,
310
- is_canceled=is_cancelled,
332
+ is_canceled=is_canceled,
333
+ is_pending_cancel=is_pending_cancel,
311
334
  )
335
+ return order
312
336
  raise Exception(f'查询不到订单{order_id}')
313
337
 
314
338
  async def order(self, order_id: str) -> Order:
@@ -344,17 +344,26 @@ class LongBridge(SecuritiesBroker):
344
344
  ))
345
345
 
346
346
  def _order(self, order_id: str) -> Order:
347
+ # 订单状态定义见
348
+ # https://open.longportapp.com/zh-CN/docs/trade/trade-definition#orderstatus
347
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
+
348
359
  with self._assets_bucket:
349
360
  self._try_refresh()
350
361
  resp = self._trade_client.order_detail(order_id=order_id)
351
362
  reason = ''
352
- if resp.status == OrderStatus.Rejected:
353
- reason = '已拒绝'
354
- if resp.status == OrderStatus.Expired:
355
- reason = '已过期'
356
- if resp.status == OrderStatus.PartialWithdrawal:
357
- 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
358
367
  return Order(
359
368
  order_id=order_id,
360
369
  currency=resp.currency,
@@ -362,7 +371,8 @@ class LongBridge(SecuritiesBroker):
362
371
  filled_qty=int(resp.executed_quantity),
363
372
  avg_price=float(resp.executed_price) if resp.executed_price else 0.0,
364
373
  error_reason=reason,
365
- is_canceled=resp.status == OrderStatus.Canceled,
374
+ is_canceled=is_canceled,
375
+ is_pending_cancel=is_pending_cancel,
366
376
  )
367
377
 
368
378
  async def order(self, order_id: str) -> Order:
@@ -142,7 +142,7 @@ class Tiger(SecuritiesBroker):
142
142
  async def cash(self) -> Cash:
143
143
  return await self.call_sync(lambda: self._cash())
144
144
 
145
- def _market_status(self) -> dict[str, dict[str, MarketStatus]]:
145
+ def _market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
146
146
  from tigeropen.common.consts import Market
147
147
  from tigeropen.quote.domain.market_status import MarketStatus as TigerMarketStatus
148
148
  client = self._quote_client
@@ -172,10 +172,10 @@ class Tiger(SecuritiesBroker):
172
172
  unified_status=unified_status,
173
173
  )
174
174
  return {
175
- TradeType.Securities.name.lower(): sec_result,
175
+ TradeType.Securities: sec_result,
176
176
  }
177
177
 
178
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
178
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
179
179
  return await self.call_sync(lambda: self._market_status())
180
180
 
181
181
  def _quote(self, contract: Contract):
@@ -318,19 +318,36 @@ class Tiger(SecuritiesBroker):
318
318
  ))
319
319
 
320
320
  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 定义要少
321
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, }
332
+
322
333
  with self._order_bucket:
323
334
  tiger_order: TigerOrder = self._trade_client.get_order(id=int(order_id))
324
335
  if tiger_order is None:
325
336
  raise Exception(f'查询不到订单{order_id}')
337
+
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
326
342
  return Order(
327
343
  order_id=order_id,
328
344
  currency=tiger_order.contract.currency,
329
345
  qty=tiger_order.quantity or 0,
330
346
  filled_qty=tiger_order.filled or 0,
331
347
  avg_price=tiger_order.avg_fill_price or 0.0,
332
- error_reason=tiger_order.reason,
333
- is_canceled=tiger_order.status == OrderStatus.CANCELLED,
348
+ error_reason=reason,
349
+ is_canceled=is_canceled,
350
+ is_pending_cancel=is_pending_cancel,
334
351
  )
335
352
 
336
353
  async def order(self, order_id: str) -> Order:
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from datetime import datetime, UTC
3
+ from typing import Callable
3
4
  from aiohttp import web
4
5
  from httptrading.broker.base import *
5
6
  from httptrading.model import *
@@ -19,7 +20,7 @@ class HttpTradingView(web.View):
19
20
  def instance_id(self) -> str:
20
21
  return self.request.match_info.get('instance_id', '')
21
22
 
22
- def current_broker(self):
23
+ def current_broker(self) -> BaseBroker:
23
24
  broker = getattr(self.request, '__current_broker__', None)
24
25
  if broker is None:
25
26
  raise web.HTTPNotFound()
@@ -94,6 +95,7 @@ class HttpTradingView(web.View):
94
95
  'isCanceled': obj.is_canceled,
95
96
  'isFilled': obj.is_filled,
96
97
  'isCompleted': obj.is_completed,
98
+ 'isCancelable': obj.is_cancelable,
97
99
  }
98
100
  raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
99
101
 
@@ -106,7 +108,7 @@ class HttpTradingView(web.View):
106
108
  return web.Response(text=cls.dumps(obj), content_type='application/json')
107
109
 
108
110
  @classmethod
109
- def response_api(cls, broker: BaseBroker, args: dict = None, ex: Exception = None):
111
+ def response_api(cls, broker: BaseBroker = None, args: dict = None, ex: Exception = None):
110
112
  resp = {
111
113
  'type': 'apiResponse',
112
114
  'instanceId': broker.instance_id if broker else None,
@@ -212,6 +214,8 @@ class MarketStatusView(HttpTradingView):
212
214
  async def get(self):
213
215
  broker = self.current_broker()
214
216
  ms_dict = await broker.market_status()
217
+ ms_dict = {t.name.lower(): d for t, d in ms_dict.items()}
218
+ ms_dict['type'] = 'marketStatusMap'
215
219
  return self.response_api(broker, {
216
220
  'marketStatus': ms_dict,
217
221
  })
@@ -253,29 +257,48 @@ async def exception_middleware(request: web.Request, handler):
253
257
  return HttpTradingView.response_api(broker=broker, ex=ex)
254
258
 
255
259
 
260
+ def std_api_factory() -> list[web.RouteDef]:
261
+ apis = [
262
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/place', PlaceOrderView),
263
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/state', OrderStateView),
264
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/cancel', CancelOrderView),
265
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/cash/state', CashView),
266
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/position/state', PositionView),
267
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/ping/state', PlugInView),
268
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/state', MarketStatusView),
269
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/quote', QuoteView),
270
+ ]
271
+ return apis
272
+
256
273
  def run(
257
274
  host: str,
258
275
  port: int,
259
276
  brokers: list[BaseBroker],
277
+ std_apis: Callable[[], list[web.RouteDef]] = None,
278
+ extend_apis: list[web.RouteDef] = None,
279
+ **kwargs
260
280
  ) -> None:
281
+ """
282
+ @param host: 监听地址
283
+ @param port: 监听端口
284
+ @param brokers: 需要控制的交易通道对象列表
285
+ @param std_apis: 如果需要替换默认提供的接口, 这里提供工厂函数的回调
286
+ @param extend_apis: 如果需要增加自定义接口, 这里传入 RouteDef 列表
287
+ @param kwargs: 其他的参数将传给 aiohttp.web.run_app 函数
288
+ """
261
289
  app = web.Application(
262
290
  middlewares=[
263
291
  auth_middleware,
264
292
  exception_middleware,
265
293
  ],
266
294
  )
267
- app.add_routes(
268
- [
269
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/place', PlaceOrderView),
270
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/state', OrderStateView),
271
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/cancel', CancelOrderView),
272
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/cash/state', CashView),
273
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/position/state', PositionView),
274
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/ping/state', PlugInView),
275
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/state', MarketStatusView),
276
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/quote', QuoteView),
277
- ]
278
- )
295
+
296
+ apis = (std_api_factory if std_apis is None else std_apis)()
297
+
298
+ if extend_apis:
299
+ apis.extend(extend_apis)
300
+
301
+ app.add_routes(apis)
279
302
 
280
303
  async def _on_startup(app):
281
304
  HttpTradingView.set_brokers(brokers)
@@ -292,4 +315,12 @@ def run(
292
315
  app,
293
316
  host=host,
294
317
  port=port,
318
+ **kwargs
295
319
  )
320
+
321
+
322
+ __all__ = [
323
+ 'run',
324
+ 'std_api_factory',
325
+ 'HttpTradingView',
326
+ ]
@@ -119,6 +119,9 @@ class Order:
119
119
  avg_price: float = field(default=0.0)
120
120
  error_reason: str = field(default='')
121
121
  is_canceled: bool = field(default=False)
122
+ # 如果交易通道存在"待取消""已提交取消"的订单状态,
123
+ # 这里需要改变默认值为 True
124
+ is_pending_cancel: bool = field(default=False)
122
125
 
123
126
  @property
124
127
  def is_filled(self) -> bool:
@@ -138,6 +141,11 @@ class Order:
138
141
  is_completed = True
139
142
  return is_completed
140
143
 
144
+ @property
145
+ def is_cancelable(self) -> bool:
146
+ is_completed = self.is_completed
147
+ return not is_completed and not self.is_pending_cancel
148
+
141
149
 
142
150
  @dataclass(frozen=True)
143
151
  class DetectPkg:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httptrading
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: 统一交易通道的接口服务
5
5
  Author-email: songwei <github@songwei.name>
6
6
  License: MIT
@@ -14,6 +14,10 @@ Dynamic: license-file
14
14
 
15
15
  # httptrading
16
16
 
17
+ ```shell
18
+ pip install httptrading
19
+ ```
20
+
17
21
  项目的用途
18
22
  --------
19
23
 
@@ -221,34 +225,35 @@ GET /httptrading/api/{instanceId}/market/state
221
225
 
222
226
  ```json lines
223
227
  {
224
- "type": "apiResponse",
225
- "instanceId": "ggUqPZbSKuQ7Ewsk",
226
- "broker": "futu",
227
- "brokerDisplay": "富途证券",
228
- "time": "2025-05-28T05:33:42.543109+00:00",
229
- "ex": null,
230
- "marketStatus": {
231
- "securities": { // 证券类市场状态, 以 region 为键的结构
232
- "US": {
233
- "type": "marketStatus",
234
- "region": "US",
235
- "originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
236
- "unifiedStatus": "CLOSED" // 统一映射的定义
237
- },
238
- "CN": {
239
- "type": "marketStatus",
240
- "region": "CN",
241
- "originStatus": "AFTERNOON",
242
- "unifiedStatus": "RTH"
243
- },
244
- "HK": {
245
- "type": "marketStatus",
246
- "region": "HK",
247
- "originStatus": "AFTERNOON",
248
- "unifiedStatus": "RTH"
249
- }
250
- }
251
- }
228
+ "type": "apiResponse",
229
+ "instanceId": "ggUqPZbSKuQ7Ewsk",
230
+ "broker": "futu",
231
+ "brokerDisplay": "富途证券",
232
+ "time": "2025-05-28T05:33:42.543109+00:00",
233
+ "ex": null,
234
+ "marketStatus": {
235
+ "type": "marketStatusMap",
236
+ "securities": { // 证券类市场状态, 以 region 为键的结构
237
+ "US": {
238
+ "type": "marketStatus",
239
+ "region": "US",
240
+ "originStatus": "AFTER_HOURS_END", // 交易通道原始市场状态
241
+ "unifiedStatus": "CLOSED" // 统一映射的定义
242
+ },
243
+ "CN": {
244
+ "type": "marketStatus",
245
+ "region": "CN",
246
+ "originStatus": "AFTERNOON",
247
+ "unifiedStatus": "RTH"
248
+ },
249
+ "HK": {
250
+ "type": "marketStatus",
251
+ "region": "HK",
252
+ "originStatus": "AFTERNOON",
253
+ "unifiedStatus": "RTH"
254
+ }
255
+ }
256
+ }
252
257
  }
253
258
  ```
254
259
 
@@ -402,24 +407,25 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
402
407
 
403
408
  ```json lines
404
409
  {
405
- "type": "apiResponse",
406
- "instanceId": "ggUqPZbSKuQ7Ewsk",
407
- "broker": "futu",
408
- "brokerDisplay": "富途证券",
409
- "time": "2025-05-28T05:59:29.984021+00:00",
410
- "ex": null,
411
- "order": {
412
- "type": "order",
413
- "orderId": "6278888",
414
- "currency": "USD",
415
- "qty": 12, // 订单数量
416
- "filledQty": 0, // 已成交数量
417
- "avgPrice": 0, // 成交价
418
- "errorReason": "", // 如果订单异常, 这里记录错误信息
419
- "isCanceled": false, // 是否已撤销
420
- "isFilled": false, // 是否全部成交
421
- "isCompleted": false // 全部成交 或者 有异常 或者 已撤销, 亦等价于不可撤的标志
422
- }
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
+ "isCancelable": true // 是否可撤的标志
428
+ }
423
429
  }
424
430
  ```
425
431
 
@@ -498,8 +504,8 @@ from httptrading import *
498
504
  from httptrading.model import *
499
505
 
500
506
 
501
- @broker_register('myApi', 'XX证券')
502
- class MyTradingApi(BaseBroker):
507
+ @broker_register('myBroker', 'XX证券')
508
+ class MyBroker(BaseBroker):
503
509
  # 根据需要的功能实现接口
504
510
  # 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
505
511
  async def place_order(
@@ -533,6 +539,64 @@ class MyTradingApi(BaseBroker):
533
539
  async def quote(self, contract: Contract) -> Quote:
534
540
  raise NotImplementedError
535
541
 
536
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
542
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
537
543
  raise NotImplementedError
538
544
  ```
545
+
546
+
547
+ 编写新接口
548
+ --------
549
+
550
+ ```python
551
+ import aiohttp.web
552
+ import httptrading
553
+
554
+ class MyApi(httptrading.HttpTradingView):
555
+ async def get(self):
556
+ broker = self.current_broker()
557
+ return self.response_api(
558
+ broker=broker,
559
+ args={
560
+ 'method': 'GET',
561
+ 'hello': 'world',
562
+ },
563
+ )
564
+
565
+ async def post(self):
566
+ body_d: dict = await self.request.json()
567
+ broker = self.current_broker()
568
+ return self.response_api(
569
+ broker=broker,
570
+ args={
571
+ 'method': 'POST',
572
+ 'hello': 'world',
573
+ 'body': body_d,
574
+ },
575
+ )
576
+
577
+ httptrading.run(
578
+ host='127.0.0.1',
579
+ port=8080,
580
+ brokers=list(),
581
+ extend_apis=[aiohttp.web.view(r'/httptrading/api/{instance_id:\w{16,32}}/hello/world', MyApi), ],
582
+ )
583
+ ```
584
+
585
+ 改变默认的接口
586
+ -----------
587
+
588
+ ```python
589
+ import httptrading
590
+
591
+ def my_std_apis():
592
+ std_apis = httptrading.std_api_factory()
593
+ apis = [api for api in std_apis if 1 == 1]
594
+ return apis
595
+
596
+ httptrading.run(
597
+ host='127.0.0.1',
598
+ port=8080,
599
+ brokers=list(),
600
+ std_apis=my_std_apis,
601
+ )
602
+ ```
@@ -11,7 +11,7 @@ httptrading.egg-info/requires.txt
11
11
  httptrading.egg-info/top_level.txt
12
12
  httptrading/broker/__init__.py
13
13
  httptrading/broker/base.py
14
- httptrading/broker/futu.py
14
+ httptrading/broker/futu_sec.py
15
15
  httptrading/broker/interactive_brokers.py
16
16
  httptrading/broker/longbridge.py
17
17
  httptrading/broker/tiger.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "httptrading"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "统一交易通道的接口服务"
5
5
  authors = [
6
6
  {name = "songwei", email = "github@songwei.name"},
File without changes
File without changes