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.
- {httptrading-1.0.1 → httptrading-1.0.2}/PKG-INFO +81 -22
- {httptrading-1.0.1 → httptrading-1.0.2}/README.md +81 -22
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/__init__.py +2 -2
- httptrading-1.0.1/httptrading/broker/futu.py → httptrading-1.0.2/httptrading/broker/futu_sec.py +38 -5
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/interactive_brokers.py +12 -8
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/longbridge.py +17 -7
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/tiger.py +19 -2
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/http_server.py +42 -13
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/model.py +8 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/PKG-INFO +81 -22
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/SOURCES.txt +1 -1
- {httptrading-1.0.1 → httptrading-1.0.2}/pyproject.toml +1 -1
- {httptrading-1.0.1 → httptrading-1.0.2}/LICENSE +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/__init__.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/broker/base.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/__init__.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/leaky_bucket.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/locate.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading/tool/time.py +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/dependency_links.txt +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/requires.txt +0 -0
- {httptrading-1.0.1 → httptrading-1.0.2}/httptrading.egg-info/top_level.txt +0 -0
- {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.
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
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('
|
507
|
-
class
|
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[
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
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('
|
493
|
-
class
|
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[
|
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.
|
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
|
6
|
+
from httptrading.http_server import *
|
httptrading-1.0.1/httptrading/broker/futu.py → httptrading-1.0.2/httptrading/broker/futu_sec.py
RENAMED
@@ -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
|
-
|
371
|
-
|
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
|
-
|
386
|
-
|
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=
|
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
|
319
|
-
reason =
|
320
|
-
|
321
|
-
|
322
|
-
|
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=
|
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
|
353
|
-
reason =
|
354
|
-
|
355
|
-
|
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=
|
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=
|
333
|
-
is_canceled=
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
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.
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
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('
|
507
|
-
class
|
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[
|
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/
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|