httptrading 1.0.0__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 +6 -0
- httptrading/broker/__init__.py +0 -0
- httptrading/broker/base.py +151 -0
- httptrading/broker/futu.py +418 -0
- httptrading/broker/interactive_brokers.py +318 -0
- httptrading/broker/longbridge.py +380 -0
- httptrading/broker/tiger.py +347 -0
- httptrading/http_server.py +295 -0
- httptrading/model.py +174 -0
- httptrading/tool/__init__.py +0 -0
- httptrading/tool/leaky_bucket.py +87 -0
- httptrading/tool/locate.py +85 -0
- httptrading/tool/time.py +77 -0
- httptrading-1.0.0.dist-info/METADATA +538 -0
- httptrading-1.0.0.dist-info/RECORD +18 -0
- httptrading-1.0.0.dist-info/WHEEL +5 -0
- httptrading-1.0.0.dist-info/licenses/LICENSE +21 -0
- httptrading-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,347 @@
|
|
1
|
+
"""
|
2
|
+
接入老虎证券的API文档
|
3
|
+
https://quant.itigerup.com/openapi/zh/python/overview/introduction.html
|
4
|
+
"""
|
5
|
+
import re
|
6
|
+
import threading
|
7
|
+
from httptrading.tool.leaky_bucket import *
|
8
|
+
from httptrading.broker.base import *
|
9
|
+
from httptrading.model import *
|
10
|
+
|
11
|
+
|
12
|
+
@broker_register(name='tiger', display='老虎证券', detect_pkg=DetectPkg('tigeropen', 'tigeropen'))
|
13
|
+
class Tiger(SecuritiesBroker):
|
14
|
+
def __init__(self, broker_args: dict = None, instance_id: str = None, tokens: list[str] = None):
|
15
|
+
super().__init__(broker_args, instance_id, tokens)
|
16
|
+
self._has_grab = False
|
17
|
+
self._config = None
|
18
|
+
self._quote_client = None
|
19
|
+
self._trade_client = None
|
20
|
+
self._push_client = None
|
21
|
+
self._grab_lock = threading.Lock()
|
22
|
+
self._market_status_bucket = LeakyBucket(9)
|
23
|
+
self._quote_bucket = LeakyBucket(119)
|
24
|
+
self._order_bucket = LeakyBucket(119)
|
25
|
+
self._assets_bucket = LeakyBucket(59)
|
26
|
+
self._on_init()
|
27
|
+
|
28
|
+
def _on_init(self):
|
29
|
+
from tigeropen.tiger_open_config import get_client_config
|
30
|
+
from tigeropen.quote.quote_client import QuoteClient
|
31
|
+
from tigeropen.trade.trade_client import TradeClient
|
32
|
+
from tigeropen.push.push_client import PushClient
|
33
|
+
config_dict = self.broker_args
|
34
|
+
pk_path = config_dict.get('pk_path')
|
35
|
+
tiger_id = config_dict.get('tiger_id')
|
36
|
+
account = config_dict.get('account')
|
37
|
+
timeout = config_dict.get('timeout', 8)
|
38
|
+
client_config = get_client_config(
|
39
|
+
private_key_path=pk_path,
|
40
|
+
tiger_id=tiger_id,
|
41
|
+
account=account,
|
42
|
+
timeout=timeout,
|
43
|
+
)
|
44
|
+
self._config = client_config
|
45
|
+
self._quote_client = self._quote_client or QuoteClient(client_config, is_grab_permission=False)
|
46
|
+
self._trade_client = self._trade_client or TradeClient(client_config)
|
47
|
+
protocol, host, port = client_config.socket_host_port
|
48
|
+
self._push_client = self._push_client or PushClient(host, port, use_ssl=(protocol == 'ssl'))
|
49
|
+
|
50
|
+
def _grab_quote(self):
|
51
|
+
with self._grab_lock:
|
52
|
+
if self._has_grab:
|
53
|
+
return
|
54
|
+
try:
|
55
|
+
config_dict = self.broker_args
|
56
|
+
if mac_address := config_dict.get('mac_address', None):
|
57
|
+
from tigeropen.tiger_open_config import TigerOpenClientConfig
|
58
|
+
TigerOpenClientConfig.__get_device_id = lambda: mac_address
|
59
|
+
except Exception as e:
|
60
|
+
pass
|
61
|
+
self._quote_client.grab_quote_permission()
|
62
|
+
self._has_grab = True
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def symbol_to_contract(cls, symbol) -> Contract | None:
|
66
|
+
region = ''
|
67
|
+
ticker = ''
|
68
|
+
if re.match(r'^[01356]\d{5}$', symbol):
|
69
|
+
region = 'CN'
|
70
|
+
ticker = symbol
|
71
|
+
if re.match(r'^\d{5}$', symbol):
|
72
|
+
region = 'HK'
|
73
|
+
ticker = symbol
|
74
|
+
if re.match(r'^\w{1,5}$', symbol):
|
75
|
+
region = 'US'
|
76
|
+
ticker = symbol
|
77
|
+
if not region or not ticker:
|
78
|
+
return None
|
79
|
+
return Contract(
|
80
|
+
trade_type=TradeType.Securities,
|
81
|
+
ticker=ticker,
|
82
|
+
region=region,
|
83
|
+
)
|
84
|
+
|
85
|
+
@classmethod
|
86
|
+
def contract_to_symbol(cls, contract: Contract) -> str | None:
|
87
|
+
if contract.trade_type != TradeType.Securities:
|
88
|
+
return None
|
89
|
+
region, ticker = contract.region, contract.ticker
|
90
|
+
symbol = None
|
91
|
+
if region == 'CN' and re.match(r'^[01356]\d{5}$', ticker):
|
92
|
+
symbol = ticker
|
93
|
+
elif region == 'HK' and re.match(r'^\d{5}$', ticker):
|
94
|
+
symbol = ticker
|
95
|
+
elif region == 'US' and re.match(r'^\w{1,5}$', ticker):
|
96
|
+
symbol = ticker
|
97
|
+
return symbol
|
98
|
+
|
99
|
+
def _positions(self):
|
100
|
+
from tigeropen.trade.domain.position import Position as TigerPosition
|
101
|
+
result = list()
|
102
|
+
with self._assets_bucket:
|
103
|
+
positions: list[TigerPosition] = self._trade_client.get_positions()
|
104
|
+
for p in positions or list():
|
105
|
+
symbol = p.contract.symbol
|
106
|
+
currency = p.contract.currency
|
107
|
+
|
108
|
+
if not symbol or not currency:
|
109
|
+
continue
|
110
|
+
contract = self.symbol_to_contract(symbol)
|
111
|
+
if not contract:
|
112
|
+
continue
|
113
|
+
qty = int(p.quantity)
|
114
|
+
position = Position(
|
115
|
+
broker=self.broker_name,
|
116
|
+
broker_display=self.broker_display,
|
117
|
+
contract=contract,
|
118
|
+
unit=Unit.Share,
|
119
|
+
currency=currency,
|
120
|
+
qty=qty,
|
121
|
+
)
|
122
|
+
result.append(position)
|
123
|
+
return result
|
124
|
+
|
125
|
+
async def positions(self):
|
126
|
+
return await self.call_sync(lambda: self._positions())
|
127
|
+
|
128
|
+
def _cash(self) -> Cash:
|
129
|
+
from tigeropen.trade.domain.prime_account import PortfolioAccount, Segment
|
130
|
+
with self._assets_bucket:
|
131
|
+
portfolio_account: PortfolioAccount = self._trade_client.get_prime_assets()
|
132
|
+
s: Segment = portfolio_account.segments.get('S')
|
133
|
+
currency = s.currency
|
134
|
+
assert currency == 'USD'
|
135
|
+
cash_balance = s.cash_balance
|
136
|
+
cash = Cash(
|
137
|
+
currency='USD',
|
138
|
+
amount=cash_balance,
|
139
|
+
)
|
140
|
+
return cash
|
141
|
+
|
142
|
+
async def cash(self) -> Cash:
|
143
|
+
return await self.call_sync(lambda: self._cash())
|
144
|
+
|
145
|
+
def _market_status(self) -> dict[str, dict[str, MarketStatus]]:
|
146
|
+
from tigeropen.common.consts import Market
|
147
|
+
from tigeropen.quote.domain.market_status import MarketStatus as TigerMarketStatus
|
148
|
+
client = self._quote_client
|
149
|
+
with self._market_status_bucket:
|
150
|
+
ms_list: list[TigerMarketStatus] = client.get_market_status(Market.ALL)
|
151
|
+
|
152
|
+
# 各个市场的状态定义见:
|
153
|
+
# https://quant.itigerup.com/openapi/zh/python/operation/quotation/stock.html#get-market-status-%E8%8E%B7%E5%8F%96%E5%B8%82%E5%9C%BA%E7%8A%B6%E6%80%81
|
154
|
+
sec_result = dict()
|
155
|
+
status_map = {
|
156
|
+
'CLOSING': UnifiedStatus.CLOSED,
|
157
|
+
'EARLY_CLOSED': UnifiedStatus.CLOSED,
|
158
|
+
'MARKET_CLOSED': UnifiedStatus.CLOSED,
|
159
|
+
'PRE_HOUR_TRADING': UnifiedStatus.PRE_HOURS,
|
160
|
+
'TRADING': UnifiedStatus.RTH,
|
161
|
+
'POST_HOUR_TRADING': UnifiedStatus.AFTER_HOURS,
|
162
|
+
# 这些映射是A股港股市场的映射
|
163
|
+
'MIDDLE_CLOSE': UnifiedStatus.REST,
|
164
|
+
}
|
165
|
+
for ms in ms_list:
|
166
|
+
region = ms.market
|
167
|
+
origin_status = ms.trading_status
|
168
|
+
unified_status = status_map.get(origin_status, UnifiedStatus.UNKNOWN)
|
169
|
+
sec_result[region] = MarketStatus(
|
170
|
+
region=region,
|
171
|
+
origin_status=origin_status,
|
172
|
+
unified_status=unified_status,
|
173
|
+
)
|
174
|
+
return {
|
175
|
+
TradeType.Securities.name.lower(): sec_result,
|
176
|
+
}
|
177
|
+
|
178
|
+
async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
|
179
|
+
return await self.call_sync(lambda: self._market_status())
|
180
|
+
|
181
|
+
def _quote(self, contract: Contract):
|
182
|
+
import pandas
|
183
|
+
self._grab_quote()
|
184
|
+
symbol = self.contract_to_symbol(contract)
|
185
|
+
currency = self.contract_to_currency(contract)
|
186
|
+
with self._quote_bucket:
|
187
|
+
pd = self._quote_client.get_stock_briefs(symbols=[symbol, ])
|
188
|
+
pd['us_date'] = pandas \
|
189
|
+
.to_datetime(pd['latest_time'], unit='ms') \
|
190
|
+
.dt.tz_localize('UTC') \
|
191
|
+
.dt.tz_convert('US/Eastern')
|
192
|
+
for index, row in pd.iterrows():
|
193
|
+
us_date, pre_close, open_price, latest_price, status, low_price, high_price = (
|
194
|
+
row['us_date'],
|
195
|
+
row['pre_close'],
|
196
|
+
row['open'],
|
197
|
+
row['latest_price'],
|
198
|
+
row['status'],
|
199
|
+
row['low'],
|
200
|
+
row['high'],
|
201
|
+
)
|
202
|
+
is_tradable = status == 'NORMAL'
|
203
|
+
return Quote(
|
204
|
+
contract=contract,
|
205
|
+
currency=currency,
|
206
|
+
is_tradable=is_tradable,
|
207
|
+
latest=latest_price,
|
208
|
+
pre_close=pre_close,
|
209
|
+
open_price=open_price,
|
210
|
+
high_price=high_price,
|
211
|
+
low_price=low_price,
|
212
|
+
time=us_date,
|
213
|
+
)
|
214
|
+
raise Exception(f'没有找到{contract}的快照行情')
|
215
|
+
|
216
|
+
async def quote(self, contract: Contract):
|
217
|
+
return await self.call_sync(lambda: self._quote(contract))
|
218
|
+
|
219
|
+
def _place_order(
|
220
|
+
self,
|
221
|
+
contract: Contract,
|
222
|
+
order_type: OrderType,
|
223
|
+
time_in_force: TimeInForce,
|
224
|
+
lifecycle: Lifecycle,
|
225
|
+
direction: str,
|
226
|
+
qty: int,
|
227
|
+
price: float = None,
|
228
|
+
**kwargs
|
229
|
+
) -> str:
|
230
|
+
if contract.trade_type != TradeType.Securities:
|
231
|
+
raise Exception(f'不支持的下单品种: {contract.trade_type}')
|
232
|
+
if contract.region == 'US' and order_type == OrderType.Market and lifecycle != Lifecycle.RTH:
|
233
|
+
raise Exception(f'交易时段不支持市价单')
|
234
|
+
symbol = self.contract_to_symbol(contract)
|
235
|
+
currency = self.contract_to_currency(contract)
|
236
|
+
from tigeropen.common.util.contract_utils import stock_contract
|
237
|
+
from tigeropen.tiger_open_config import TigerOpenClientConfig
|
238
|
+
from tigeropen.common.util.order_utils import market_order, limit_order
|
239
|
+
cfg: TigerOpenClientConfig = self._config
|
240
|
+
client = self._trade_client
|
241
|
+
tiger_contract = stock_contract(symbol=symbol, currency=currency)
|
242
|
+
|
243
|
+
def _map_trade_side():
|
244
|
+
match direction:
|
245
|
+
case 'BUY':
|
246
|
+
return direction
|
247
|
+
case 'SELL':
|
248
|
+
return direction
|
249
|
+
case _:
|
250
|
+
raise Exception(f'不支持的买卖方向: {direction}')
|
251
|
+
|
252
|
+
def _map_time_in_force():
|
253
|
+
match time_in_force:
|
254
|
+
case TimeInForce.DAY:
|
255
|
+
return 'DAY'
|
256
|
+
case TimeInForce.GTC:
|
257
|
+
return 'GTC'
|
258
|
+
case _:
|
259
|
+
raise Exception(f'不支持的订单有效期: {time_in_force}')
|
260
|
+
|
261
|
+
def _map_lifecycle():
|
262
|
+
match lifecycle:
|
263
|
+
case Lifecycle.RTH:
|
264
|
+
return False
|
265
|
+
case Lifecycle.ETH:
|
266
|
+
return True
|
267
|
+
case _:
|
268
|
+
raise Exception(f'不支持的交易时段: {lifecycle}')
|
269
|
+
|
270
|
+
def _map_order():
|
271
|
+
match order_type:
|
272
|
+
case OrderType.Limit:
|
273
|
+
return limit_order(
|
274
|
+
account=cfg.account,
|
275
|
+
contract=tiger_contract,
|
276
|
+
action=_map_trade_side(),
|
277
|
+
quantity=qty,
|
278
|
+
limit_price=price,
|
279
|
+
)
|
280
|
+
case OrderType.Market:
|
281
|
+
return market_order(
|
282
|
+
account=cfg.account,
|
283
|
+
contract=tiger_contract,
|
284
|
+
action=_map_trade_side(),
|
285
|
+
quantity=qty,
|
286
|
+
)
|
287
|
+
case _:
|
288
|
+
raise Exception(f'不支持的订单类型: {order_type}')
|
289
|
+
tiger_order = _map_order()
|
290
|
+
tiger_order.time_in_force = time_in_force=_map_time_in_force()
|
291
|
+
tiger_order.outside_rth = _map_lifecycle()
|
292
|
+
with self._order_bucket:
|
293
|
+
client.place_order(tiger_order)
|
294
|
+
order_id = str(tiger_order.id)
|
295
|
+
assert order_id
|
296
|
+
return order_id
|
297
|
+
|
298
|
+
async def place_order(
|
299
|
+
self,
|
300
|
+
contract: Contract,
|
301
|
+
order_type: OrderType,
|
302
|
+
time_in_force: TimeInForce,
|
303
|
+
lifecycle: Lifecycle,
|
304
|
+
direction: str,
|
305
|
+
qty: int,
|
306
|
+
price: float = None,
|
307
|
+
**kwargs
|
308
|
+
) -> str:
|
309
|
+
return await self.call_sync(lambda: self._place_order(
|
310
|
+
contract=contract,
|
311
|
+
order_type=order_type,
|
312
|
+
time_in_force=time_in_force,
|
313
|
+
lifecycle=lifecycle,
|
314
|
+
direction=direction,
|
315
|
+
qty=qty,
|
316
|
+
price=price,
|
317
|
+
**kwargs
|
318
|
+
))
|
319
|
+
|
320
|
+
def _order(self, order_id: str) -> Order:
|
321
|
+
from tigeropen.trade.domain.order import Order as TigerOrder, OrderStatus
|
322
|
+
with self._order_bucket:
|
323
|
+
tiger_order: TigerOrder = self._trade_client.get_order(id=int(order_id))
|
324
|
+
if tiger_order is None:
|
325
|
+
raise Exception(f'查询不到订单{order_id}')
|
326
|
+
return Order(
|
327
|
+
order_id=order_id,
|
328
|
+
currency=tiger_order.contract.currency,
|
329
|
+
qty=tiger_order.quantity or 0,
|
330
|
+
filled_qty=tiger_order.filled or 0,
|
331
|
+
avg_price=tiger_order.avg_fill_price or 0.0,
|
332
|
+
error_reason=tiger_order.reason,
|
333
|
+
is_canceled=tiger_order.status == OrderStatus.CANCELLED,
|
334
|
+
)
|
335
|
+
|
336
|
+
async def order(self, order_id: str) -> Order:
|
337
|
+
return await self.call_sync(lambda: self._order(order_id=order_id))
|
338
|
+
|
339
|
+
def _cancel_order(self, order_id: str):
|
340
|
+
with self._order_bucket:
|
341
|
+
self._trade_client.cancel_order(id=int(order_id))
|
342
|
+
|
343
|
+
async def cancel_order(self, order_id: str):
|
344
|
+
await self.call_sync(lambda: self._cancel_order(order_id=order_id))
|
345
|
+
|
346
|
+
|
347
|
+
__all__ = ['Tiger', ]
|
@@ -0,0 +1,295 @@
|
|
1
|
+
import json
|
2
|
+
from datetime import datetime, UTC
|
3
|
+
from aiohttp import web
|
4
|
+
from httptrading.broker.base import *
|
5
|
+
from httptrading.model import *
|
6
|
+
|
7
|
+
|
8
|
+
class HttpTradingView(web.View):
|
9
|
+
__BROKERS: list[BaseBroker] = list()
|
10
|
+
|
11
|
+
@classmethod
|
12
|
+
def set_brokers(cls, brokers: list[BaseBroker]):
|
13
|
+
HttpTradingView.__BROKERS = brokers
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
def brokers(cls):
|
17
|
+
return HttpTradingView.__BROKERS.copy()
|
18
|
+
|
19
|
+
def instance_id(self) -> str:
|
20
|
+
return self.request.match_info.get('instance_id', '')
|
21
|
+
|
22
|
+
def current_broker(self):
|
23
|
+
broker = getattr(self.request, '__current_broker__', None)
|
24
|
+
if broker is None:
|
25
|
+
raise web.HTTPNotFound()
|
26
|
+
return broker
|
27
|
+
|
28
|
+
async def get_contract(self, from_json=False):
|
29
|
+
params: dict = await self.request.json() if from_json else self.request.query
|
30
|
+
trade_type = TradeType[params.get('tradeType', '--')]
|
31
|
+
region = params.get('region', '--')
|
32
|
+
ticker = params.get('ticker', '--')
|
33
|
+
contract = Contract(
|
34
|
+
trade_type=trade_type,
|
35
|
+
region=region,
|
36
|
+
ticker=ticker,
|
37
|
+
)
|
38
|
+
return contract
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def json_default(cls, obj):
|
42
|
+
if isinstance(obj, Position):
|
43
|
+
return {
|
44
|
+
'type': 'position',
|
45
|
+
'broker': obj.broker,
|
46
|
+
'brokerDisplay': obj.broker_display,
|
47
|
+
'contract': cls.json_default(obj.contract),
|
48
|
+
'unit': obj.unit.name,
|
49
|
+
'currency': obj.currency,
|
50
|
+
'qty': obj.qty,
|
51
|
+
}
|
52
|
+
if isinstance(obj, Contract):
|
53
|
+
return {
|
54
|
+
'type': 'contract',
|
55
|
+
'tradeType': obj.trade_type.name,
|
56
|
+
'region': obj.region,
|
57
|
+
'ticker': obj.ticker,
|
58
|
+
}
|
59
|
+
if isinstance(obj, Cash):
|
60
|
+
return {
|
61
|
+
'type': 'cash',
|
62
|
+
'currency': obj.currency,
|
63
|
+
'amount': obj.amount,
|
64
|
+
}
|
65
|
+
if isinstance(obj, MarketStatus):
|
66
|
+
return {
|
67
|
+
'type': 'marketStatus',
|
68
|
+
'region': obj.region,
|
69
|
+
'originStatus': obj.origin_status,
|
70
|
+
'unifiedStatus': obj.unified_status.name,
|
71
|
+
}
|
72
|
+
if isinstance(obj, Quote):
|
73
|
+
return {
|
74
|
+
'type': 'quote',
|
75
|
+
'contract': cls.json_default(obj.contract),
|
76
|
+
'currency': obj.currency,
|
77
|
+
'isTradable': obj.is_tradable,
|
78
|
+
'latest': obj.latest,
|
79
|
+
'preClose': obj.pre_close,
|
80
|
+
'highPrice': obj.high_price,
|
81
|
+
'lowPrice': obj.low_price,
|
82
|
+
'openPrice': obj.open_price,
|
83
|
+
'timestamp': int(obj.time.timestamp() * 1000),
|
84
|
+
}
|
85
|
+
if isinstance(obj, Order):
|
86
|
+
return {
|
87
|
+
'type': 'order',
|
88
|
+
'orderId': obj.order_id,
|
89
|
+
'currency': obj.currency,
|
90
|
+
'qty': obj.qty,
|
91
|
+
'filledQty': obj.filled_qty,
|
92
|
+
'avgPrice': obj.avg_price,
|
93
|
+
'errorReason': obj.error_reason,
|
94
|
+
'isCanceled': obj.is_canceled,
|
95
|
+
'isFilled': obj.is_filled,
|
96
|
+
'isCompleted': obj.is_completed,
|
97
|
+
}
|
98
|
+
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
def dumps(cls, obj):
|
102
|
+
return json.dumps(obj, default=cls.json_default)
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def response_obj(cls, obj):
|
106
|
+
return web.Response(text=cls.dumps(obj), content_type='application/json')
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def response_api(cls, broker: BaseBroker, args: dict = None, ex: Exception = None):
|
110
|
+
resp = {
|
111
|
+
'type': 'apiResponse',
|
112
|
+
'instanceId': broker.instance_id if broker else None,
|
113
|
+
'broker': broker.broker_name if broker else None,
|
114
|
+
'brokerDisplay': broker.broker_display if broker else None,
|
115
|
+
'time': datetime.now(UTC).isoformat(),
|
116
|
+
'ex': ex.__str__() if ex else None,
|
117
|
+
}
|
118
|
+
if args:
|
119
|
+
resp.update(args)
|
120
|
+
return cls.response_obj(resp)
|
121
|
+
|
122
|
+
|
123
|
+
class PlaceOrderView(HttpTradingView):
|
124
|
+
async def post(self):
|
125
|
+
broker = self.current_broker()
|
126
|
+
contract = await self.get_contract(from_json=True)
|
127
|
+
body_d: dict = await self.request.json()
|
128
|
+
price = body_d.get('price', 0)
|
129
|
+
qty = body_d.get('qty', 0)
|
130
|
+
order_type = OrderType[body_d.get('orderType', '')]
|
131
|
+
time_in_force = TimeInForce[body_d.get('timeInForce', '')]
|
132
|
+
lifecycle = Lifecycle[body_d.get('lifecycle', '')]
|
133
|
+
direction = body_d.get('direction', '')
|
134
|
+
if price:
|
135
|
+
price = float(price)
|
136
|
+
order_id: str = await broker.place_order(
|
137
|
+
contract=contract,
|
138
|
+
order_type=order_type,
|
139
|
+
time_in_force=time_in_force,
|
140
|
+
lifecycle=lifecycle,
|
141
|
+
direction=direction,
|
142
|
+
qty=qty,
|
143
|
+
price=price,
|
144
|
+
json=body_d,
|
145
|
+
)
|
146
|
+
return self.response_api(broker, {
|
147
|
+
'orderId': order_id,
|
148
|
+
'args': body_d,
|
149
|
+
})
|
150
|
+
|
151
|
+
|
152
|
+
class OrderStateView(HttpTradingView):
|
153
|
+
async def get(self):
|
154
|
+
broker = self.current_broker()
|
155
|
+
order_id = self.request.query.get('orderId', '')
|
156
|
+
order: Order = await broker.order(order_id=order_id)
|
157
|
+
return self.response_api(broker, {
|
158
|
+
'order': order,
|
159
|
+
})
|
160
|
+
|
161
|
+
|
162
|
+
class CancelOrderView(HttpTradingView):
|
163
|
+
async def post(self):
|
164
|
+
broker = self.current_broker()
|
165
|
+
body_d: dict = await self.request.json()
|
166
|
+
order_id = body_d.get('orderId', '')
|
167
|
+
assert order_id
|
168
|
+
await broker.cancel_order(order_id=order_id)
|
169
|
+
return self.response_api(broker, {
|
170
|
+
'canceled': True,
|
171
|
+
})
|
172
|
+
|
173
|
+
|
174
|
+
class CashView(HttpTradingView):
|
175
|
+
async def get(self):
|
176
|
+
broker = self.current_broker()
|
177
|
+
cash: Cash = await broker.cash()
|
178
|
+
return self.response_api(broker, {
|
179
|
+
'cash': cash,
|
180
|
+
})
|
181
|
+
|
182
|
+
|
183
|
+
class PositionView(HttpTradingView):
|
184
|
+
async def get(self):
|
185
|
+
broker = self.current_broker()
|
186
|
+
positions: list[Position] = await broker.positions()
|
187
|
+
return self.response_api(broker, {
|
188
|
+
'positions': positions,
|
189
|
+
})
|
190
|
+
|
191
|
+
|
192
|
+
class PlugInView(HttpTradingView):
|
193
|
+
async def get(self):
|
194
|
+
broker = self.current_broker()
|
195
|
+
pong = await broker.ping()
|
196
|
+
return self.response_api(broker, {
|
197
|
+
'pong': pong,
|
198
|
+
})
|
199
|
+
|
200
|
+
|
201
|
+
class QuoteView(HttpTradingView):
|
202
|
+
async def get(self):
|
203
|
+
contract = await self.get_contract()
|
204
|
+
broker = self.current_broker()
|
205
|
+
quote: Quote = await broker.quote(contract)
|
206
|
+
return self.response_api(broker, {
|
207
|
+
'quote': quote,
|
208
|
+
})
|
209
|
+
|
210
|
+
|
211
|
+
class MarketStatusView(HttpTradingView):
|
212
|
+
async def get(self):
|
213
|
+
broker = self.current_broker()
|
214
|
+
ms_dict = await broker.market_status()
|
215
|
+
return self.response_api(broker, {
|
216
|
+
'marketStatus': ms_dict,
|
217
|
+
})
|
218
|
+
|
219
|
+
|
220
|
+
@web.middleware
|
221
|
+
async def auth_middleware(request: web.Request, handler):
|
222
|
+
instance_id = request.match_info.get('instance_id', '')
|
223
|
+
token = request.headers.get('HT-TOKEN', '')
|
224
|
+
if not instance_id:
|
225
|
+
raise web.HTTPNotFound
|
226
|
+
if not token:
|
227
|
+
raise web.HTTPNotFound
|
228
|
+
if len(token) < 16 or len(token) > 64:
|
229
|
+
raise web.HTTPNotFound
|
230
|
+
for broker in HttpTradingView.brokers():
|
231
|
+
if broker.instance_id != instance_id:
|
232
|
+
continue
|
233
|
+
if token not in broker.tokens:
|
234
|
+
raise web.HTTPNotFound
|
235
|
+
setattr(request, '__current_broker__', broker)
|
236
|
+
break
|
237
|
+
else:
|
238
|
+
raise web.HTTPNotFound
|
239
|
+
response: web.Response = await handler(request)
|
240
|
+
delattr(request, '__current_broker__')
|
241
|
+
return response
|
242
|
+
|
243
|
+
|
244
|
+
@web.middleware
|
245
|
+
async def exception_middleware(request: web.Request, handler):
|
246
|
+
try:
|
247
|
+
response: web.Response = await handler(request)
|
248
|
+
return response
|
249
|
+
except BrokerError as ex:
|
250
|
+
return HttpTradingView.response_api(broker=ex.broker, ex=ex)
|
251
|
+
except Exception as ex:
|
252
|
+
broker = getattr(request, '__current_broker__', None)
|
253
|
+
return HttpTradingView.response_api(broker=broker, ex=ex)
|
254
|
+
|
255
|
+
|
256
|
+
def run(
|
257
|
+
host: str,
|
258
|
+
port: int,
|
259
|
+
brokers: list[BaseBroker],
|
260
|
+
) -> None:
|
261
|
+
app = web.Application(
|
262
|
+
middlewares=[
|
263
|
+
auth_middleware,
|
264
|
+
exception_middleware,
|
265
|
+
],
|
266
|
+
)
|
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
|
+
)
|
279
|
+
|
280
|
+
async def _on_startup(app):
|
281
|
+
HttpTradingView.set_brokers(brokers)
|
282
|
+
for broker in brokers:
|
283
|
+
await broker.start()
|
284
|
+
|
285
|
+
async def _on_shutdown(app):
|
286
|
+
for broker in brokers:
|
287
|
+
await broker.shutdown()
|
288
|
+
|
289
|
+
app.on_startup.append(_on_startup)
|
290
|
+
app.on_shutdown.append(_on_shutdown)
|
291
|
+
web.run_app(
|
292
|
+
app,
|
293
|
+
host=host,
|
294
|
+
port=port,
|
295
|
+
)
|