httptrading 1.0.1__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.1 → httptrading-1.0.2}/PKG-INFO +81 -22
  2. {httptrading-1.0.1 → httptrading-1.0.2}/README.md +81 -22
  3. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/__init__.py +2 -2
  4. httptrading-1.0.1/httptrading/broker/futu.py → httptrading-1.0.2/httptrading/broker/futu_sec.py +38 -5
  5. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/interactive_brokers.py +12 -8
  6. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/longbridge.py +17 -7
  7. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/tiger.py +19 -2
  8. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/http_server.py +42 -13
  9. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/model.py +8 -0
  10. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/PKG-INFO +81 -22
  11. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/SOURCES.txt +1 -1
  12. {httptrading-1.0.1 → httptrading-1.0.2}/pyproject.toml +1 -1
  13. {httptrading-1.0.1 → httptrading-1.0.2}/LICENSE +0 -0
  14. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/__init__.py +0 -0
  15. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/base.py +0 -0
  16. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/__init__.py +0 -0
  17. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/leaky_bucket.py +0 -0
  18. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/locate.py +0 -0
  19. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/time.py +0 -0
  20. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/dependency_links.txt +0 -0
  21. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/requires.txt +0 -0
  22. {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/top_level.txt +0 -0
  23. {httptrading-1.0.1 → 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.1
3
+ Version: 1.0.2
4
4
  Summary: 统一交易通道的接口服务
5
5
  Author-email: songwei <github@songwei.name>
6
6
  License: MIT
@@ -407,24 +407,25 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
407
407
 
408
408
  ```json lines
409
409
  {
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
- }
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
+ }
428
429
  }
429
430
  ```
430
431
 
@@ -503,8 +504,8 @@ from httptrading import *
503
504
  from httptrading.model import *
504
505
 
505
506
 
506
- @broker_register('myApi', 'XX证券')
507
- class MyTradingApi(BaseBroker):
507
+ @broker_register('myBroker', 'XX证券')
508
+ class MyBroker(BaseBroker):
508
509
  # 根据需要的功能实现接口
509
510
  # 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
510
511
  async def place_order(
@@ -538,6 +539,64 @@ class MyTradingApi(BaseBroker):
538
539
  async def quote(self, contract: Contract) -> Quote:
539
540
  raise NotImplementedError
540
541
 
541
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
542
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
542
543
  raise NotImplementedError
543
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
+ ```
@@ -393,24 +393,25 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
393
393
 
394
394
  ```json lines
395
395
  {
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
- }
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
+ }
414
415
  }
415
416
  ```
416
417
 
@@ -489,8 +490,8 @@ from httptrading import *
489
490
  from httptrading.model import *
490
491
 
491
492
 
492
- @broker_register('myApi', 'XX证券')
493
- class MyTradingApi(BaseBroker):
493
+ @broker_register('myBroker', 'XX证券')
494
+ class MyBroker(BaseBroker):
494
495
  # 根据需要的功能实现接口
495
496
  # 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
496
497
  async def place_order(
@@ -524,6 +525,64 @@ class MyTradingApi(BaseBroker):
524
525
  async def quote(self, contract: Contract) -> Quote:
525
526
  raise NotImplementedError
526
527
 
527
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
528
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
528
529
  raise NotImplementedError
529
- ```
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 *
@@ -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:
@@ -5,7 +5,6 @@ https://ib-insync.readthedocs.io/readme.html
5
5
  import re
6
6
  import asyncio
7
7
  from typing import Any
8
- from collections import defaultdict
9
8
  from httptrading.tool.leaky_bucket import *
10
9
  from httptrading.tool.time import *
11
10
  from httptrading.broker.base import *
@@ -291,6 +290,10 @@ class InteractiveBrokers(SecuritiesBroker):
291
290
  async def _order(self, order_id: str) -> Order:
292
291
  import ib_insync
293
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
+
294
297
  def _total_fills(trade) -> int:
295
298
  return int(trade.filled())
296
299
 
@@ -315,20 +318,21 @@ class InteractiveBrokers(SecuritiesBroker):
315
318
  assert qty >= filled_qty
316
319
  avg_fill_price = _avg_price(ib_trade)
317
320
  reason = ''
318
- if ib_trade.orderStatus.status == ib_insync.OrderStatus.Inactive:
319
- reason = 'Inactive'
320
-
321
- cancel_status = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
322
- is_cancelled = ib_trade.orderStatus.status in cancel_status
323
- 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(
324
326
  order_id=order_id,
325
327
  currency=ib_trade.contract.currency,
326
328
  qty=qty,
327
329
  filled_qty=filled_qty,
328
330
  avg_price=avg_fill_price,
329
331
  error_reason=reason,
330
- is_canceled=is_cancelled,
332
+ is_canceled=is_canceled,
333
+ is_pending_cancel=is_pending_cancel,
331
334
  )
335
+ return order
332
336
  raise Exception(f'查询不到订单{order_id}')
333
337
 
334
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:
@@ -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 *
@@ -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,
@@ -255,29 +257,48 @@ async def exception_middleware(request: web.Request, handler):
255
257
  return HttpTradingView.response_api(broker=broker, ex=ex)
256
258
 
257
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
+
258
273
  def run(
259
274
  host: str,
260
275
  port: int,
261
276
  brokers: list[BaseBroker],
277
+ std_apis: Callable[[], list[web.RouteDef]] = None,
278
+ extend_apis: list[web.RouteDef] = None,
279
+ **kwargs
262
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
+ """
263
289
  app = web.Application(
264
290
  middlewares=[
265
291
  auth_middleware,
266
292
  exception_middleware,
267
293
  ],
268
294
  )
269
- app.add_routes(
270
- [
271
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/place', PlaceOrderView),
272
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/state', OrderStateView),
273
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/cancel', CancelOrderView),
274
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/cash/state', CashView),
275
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/position/state', PositionView),
276
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/ping/state', PlugInView),
277
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/state', MarketStatusView),
278
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/quote', QuoteView),
279
- ]
280
- )
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)
281
302
 
282
303
  async def _on_startup(app):
283
304
  HttpTradingView.set_brokers(brokers)
@@ -294,4 +315,12 @@ def run(
294
315
  app,
295
316
  host=host,
296
317
  port=port,
318
+ **kwargs
297
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.1
3
+ Version: 1.0.2
4
4
  Summary: 统一交易通道的接口服务
5
5
  Author-email: songwei <github@songwei.name>
6
6
  License: MIT
@@ -407,24 +407,25 @@ GET /httptrading/api/{instanceId}/order/state?orderId={订单号}
407
407
 
408
408
  ```json lines
409
409
  {
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
- }
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
+ }
428
429
  }
429
430
  ```
430
431
 
@@ -503,8 +504,8 @@ from httptrading import *
503
504
  from httptrading.model import *
504
505
 
505
506
 
506
- @broker_register('myApi', 'XX证券')
507
- class MyTradingApi(BaseBroker):
507
+ @broker_register('myBroker', 'XX证券')
508
+ class MyBroker(BaseBroker):
508
509
  # 根据需要的功能实现接口
509
510
  # 如果 sdk 提供的方式会阻塞 eventloop, 需要使用 self.call_sync 方法传入阻塞方法
510
511
  async def place_order(
@@ -538,6 +539,64 @@ class MyTradingApi(BaseBroker):
538
539
  async def quote(self, contract: Contract) -> Quote:
539
540
  raise NotImplementedError
540
541
 
541
- async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
542
+ async def market_status(self) -> dict[TradeType, dict[str, MarketStatus] | str]:
542
543
  raise NotImplementedError
543
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.1"
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