httptrading 1.0.0__py3-none-any.whl → 1.0.2__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 +2 -2
- httptrading/broker/base.py +7 -1
- httptrading/broker/{futu.py → futu_sec.py} +41 -8
- httptrading/broker/interactive_brokers.py +34 -10
- httptrading/broker/longbridge.py +17 -7
- httptrading/broker/tiger.py +22 -5
- httptrading/http_server.py +45 -14
- httptrading/model.py +8 -0
- {httptrading-1.0.0.dist-info → httptrading-1.0.2.dist-info}/METADATA +114 -50
- httptrading-1.0.2.dist-info/RECORD +18 -0
- httptrading-1.0.0.dist-info/RECORD +0 -18
- {httptrading-1.0.0.dist-info → httptrading-1.0.2.dist-info}/WHEEL +0 -0
- {httptrading-1.0.0.dist-info → httptrading-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {httptrading-1.0.0.dist-info → httptrading-1.0.2.dist-info}/top_level.txt +0 -0
httptrading/__init__.py
CHANGED
@@ -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/broker/base.py
CHANGED
@@ -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[
|
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[
|
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
|
207
|
+
TradeType.Securities: sec_result,
|
208
208
|
}
|
209
209
|
|
210
|
-
async def market_status(self) -> dict[
|
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
|
-
|
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:
|
@@ -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
|
-
|
230
|
-
|
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
|
299
|
-
reason =
|
300
|
-
|
301
|
-
|
302
|
-
|
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=
|
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:
|
httptrading/broker/longbridge.py
CHANGED
@@ -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:
|
httptrading/broker/tiger.py
CHANGED
@@ -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[
|
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
|
175
|
+
TradeType.Securities: sec_result,
|
176
176
|
}
|
177
177
|
|
178
|
-
async def market_status(self) -> dict[
|
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=
|
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:
|
httptrading/http_server.py
CHANGED
@@ -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
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
+
]
|
httptrading/model.py
CHANGED
@@ -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
|
@@ -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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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('
|
502
|
-
class
|
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[
|
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
|
+
```
|
@@ -0,0 +1,18 @@
|
|
1
|
+
httptrading/__init__.py,sha256=Z9ITAueneLgf-L_TYMfmMwyUcfkBpcPxMFnvjFmRK_o,260
|
2
|
+
httptrading/http_server.py,sha256=MUf3B_rZmi9KziAeiVTsaQOaSIGBBEK_JQTDuQfAV1Y,10596
|
3
|
+
httptrading/model.py,sha256=C5Glx6jC1e9Pmyux4lLmJmpGMi7P6IluX5cxpl34noo,4281
|
4
|
+
httptrading/broker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
httptrading/broker/base.py,sha256=fqxfLTVavHxfFZDPt9GqhjT_C_9n_xcERahxbeuDywU,4923
|
6
|
+
httptrading/broker/futu_sec.py,sha256=PbI9lofWKE3rVu0rzxwGofTDFSRHLVnZkbOHqc0TGRA,17872
|
7
|
+
httptrading/broker/interactive_brokers.py,sha256=seNrJ7TO9fN31f4MSqdEaTy3a1xxuqw5Kg6PMi5CS9Q,12406
|
8
|
+
httptrading/broker/longbridge.py,sha256=OivkmVTo806XbIH0ZwUjLMOUQGaCtHTZRxuI9j2Pj68,14489
|
9
|
+
httptrading/broker/tiger.py,sha256=aKOR90T3pUy_QdS0NFpdjMnoNjSJWYb8jfSvJxHcoFM,14292
|
10
|
+
httptrading/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
httptrading/tool/leaky_bucket.py,sha256=Bvab3Fkn16hhZ1-WLCFcPkbFH_bHHhQ9boLv8HrmGSE,2817
|
12
|
+
httptrading/tool/locate.py,sha256=vSIzd09FWKmckkgY3mWtFXQm2Z0VIKv4FCXHT44e61s,2748
|
13
|
+
httptrading/tool/time.py,sha256=7eVmZ_td72JLjsBRLjMOHklxltNbOxeN97uSLi7wvIA,2188
|
14
|
+
httptrading-1.0.2.dist-info/licenses/LICENSE,sha256=KfMSrfnpo-TOqpCTJqnbcZNl0w7ErxadsMQf8uas_tY,1093
|
15
|
+
httptrading-1.0.2.dist-info/METADATA,sha256=zVOIp8n4ZK6oIxYEQ40o1kZSE4InX8c0SwscMYB-DHw,16947
|
16
|
+
httptrading-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
+
httptrading-1.0.2.dist-info/top_level.txt,sha256=rsLGrGN6QubO9ILQRktwrWtxfGsGAmWUnQq7XksKu-4,12
|
18
|
+
httptrading-1.0.2.dist-info/RECORD,,
|
@@ -1,18 +0,0 @@
|
|
1
|
-
httptrading/__init__.py,sha256=NIcpQPR6bj9_p0yN5RRaLvx5Q5zSfDekLp3fYLQY78A,258
|
2
|
-
httptrading/http_server.py,sha256=JlVwMSC_B6kYQWkoHYzD24jWgxUHr10NHa-IRpS7zOA,9633
|
3
|
-
httptrading/model.py,sha256=_j4NsBF5Ra5ach9ZMg_ZsBU6c57doYTOy7kKxHMaECo,3949
|
4
|
-
httptrading/broker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
httptrading/broker/base.py,sha256=UrCS2-PkwuNImow6bO4XacMqvFVS9cPmLXgi1PWy2BE,4622
|
6
|
-
httptrading/broker/futu.py,sha256=DfEYkIszxt386EMgQaAfwq28-uPKUGxyJjnSyGO8Qpw,15923
|
7
|
-
httptrading/broker/interactive_brokers.py,sha256=X-sCY-KSSw-hYMblbkomObrVY9AZe3AIUMFNZOPR7YA,11508
|
8
|
-
httptrading/broker/longbridge.py,sha256=alpPmPz_DbbWvg4SbL9-5WLpgzLHVk1SgXVcV6rOl4U,14139
|
9
|
-
httptrading/broker/tiger.py,sha256=ZoAA9jqvaN0ZgLcsUAD1eH00NS5QhjkfuMcdEnyK0Ss,13589
|
10
|
-
httptrading/tool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
httptrading/tool/leaky_bucket.py,sha256=Bvab3Fkn16hhZ1-WLCFcPkbFH_bHHhQ9boLv8HrmGSE,2817
|
12
|
-
httptrading/tool/locate.py,sha256=vSIzd09FWKmckkgY3mWtFXQm2Z0VIKv4FCXHT44e61s,2748
|
13
|
-
httptrading/tool/time.py,sha256=7eVmZ_td72JLjsBRLjMOHklxltNbOxeN97uSLi7wvIA,2188
|
14
|
-
httptrading-1.0.0.dist-info/licenses/LICENSE,sha256=KfMSrfnpo-TOqpCTJqnbcZNl0w7ErxadsMQf8uas_tY,1093
|
15
|
-
httptrading-1.0.0.dist-info/METADATA,sha256=vGRvdxUVSC4w41amErpVcAdfyZpFrEaZcBk6hKP-i_w,15352
|
16
|
-
httptrading-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
17
|
-
httptrading-1.0.0.dist-info/top_level.txt,sha256=rsLGrGN6QubO9ILQRktwrWtxfGsGAmWUnQq7XksKu-4,12
|
18
|
-
httptrading-1.0.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|