httptrading 1.0.2__py3-none-any.whl → 1.0.5__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.
@@ -1,326 +1,276 @@
1
- import json
2
- from datetime import datetime, UTC
3
- from typing import Callable
4
- from aiohttp import web
5
- from httptrading.broker.base import *
6
- from httptrading.model import *
7
-
8
-
9
- class HttpTradingView(web.View):
10
- __BROKERS: list[BaseBroker] = list()
11
-
12
- @classmethod
13
- def set_brokers(cls, brokers: list[BaseBroker]):
14
- HttpTradingView.__BROKERS = brokers
15
-
16
- @classmethod
17
- def brokers(cls):
18
- return HttpTradingView.__BROKERS.copy()
19
-
20
- def instance_id(self) -> str:
21
- return self.request.match_info.get('instance_id', '')
22
-
23
- def current_broker(self) -> BaseBroker:
24
- broker = getattr(self.request, '__current_broker__', None)
25
- if broker is None:
26
- raise web.HTTPNotFound()
27
- return broker
28
-
29
- async def get_contract(self, from_json=False):
30
- params: dict = await self.request.json() if from_json else self.request.query
31
- trade_type = TradeType[params.get('tradeType', '--')]
32
- region = params.get('region', '--')
33
- ticker = params.get('ticker', '--')
34
- contract = Contract(
35
- trade_type=trade_type,
36
- region=region,
37
- ticker=ticker,
38
- )
39
- return contract
40
-
41
- @classmethod
42
- def json_default(cls, obj):
43
- if isinstance(obj, Position):
44
- return {
45
- 'type': 'position',
46
- 'broker': obj.broker,
47
- 'brokerDisplay': obj.broker_display,
48
- 'contract': cls.json_default(obj.contract),
49
- 'unit': obj.unit.name,
50
- 'currency': obj.currency,
51
- 'qty': obj.qty,
52
- }
53
- if isinstance(obj, Contract):
54
- return {
55
- 'type': 'contract',
56
- 'tradeType': obj.trade_type.name,
57
- 'region': obj.region,
58
- 'ticker': obj.ticker,
59
- }
60
- if isinstance(obj, Cash):
61
- return {
62
- 'type': 'cash',
63
- 'currency': obj.currency,
64
- 'amount': obj.amount,
65
- }
66
- if isinstance(obj, MarketStatus):
67
- return {
68
- 'type': 'marketStatus',
69
- 'region': obj.region,
70
- 'originStatus': obj.origin_status,
71
- 'unifiedStatus': obj.unified_status.name,
72
- }
73
- if isinstance(obj, Quote):
74
- return {
75
- 'type': 'quote',
76
- 'contract': cls.json_default(obj.contract),
77
- 'currency': obj.currency,
78
- 'isTradable': obj.is_tradable,
79
- 'latest': obj.latest,
80
- 'preClose': obj.pre_close,
81
- 'highPrice': obj.high_price,
82
- 'lowPrice': obj.low_price,
83
- 'openPrice': obj.open_price,
84
- 'timestamp': int(obj.time.timestamp() * 1000),
85
- }
86
- if isinstance(obj, Order):
87
- return {
88
- 'type': 'order',
89
- 'orderId': obj.order_id,
90
- 'currency': obj.currency,
91
- 'qty': obj.qty,
92
- 'filledQty': obj.filled_qty,
93
- 'avgPrice': obj.avg_price,
94
- 'errorReason': obj.error_reason,
95
- 'isCanceled': obj.is_canceled,
96
- 'isFilled': obj.is_filled,
97
- 'isCompleted': obj.is_completed,
98
- 'isCancelable': obj.is_cancelable,
99
- }
100
- raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
101
-
102
- @classmethod
103
- def dumps(cls, obj):
104
- return json.dumps(obj, default=cls.json_default)
105
-
106
- @classmethod
107
- def response_obj(cls, obj):
108
- return web.Response(text=cls.dumps(obj), content_type='application/json')
109
-
110
- @classmethod
111
- def response_api(cls, broker: BaseBroker = None, args: dict = None, ex: Exception = None):
112
- resp = {
113
- 'type': 'apiResponse',
114
- 'instanceId': broker.instance_id if broker else None,
115
- 'broker': broker.broker_name if broker else None,
116
- 'brokerDisplay': broker.broker_display if broker else None,
117
- 'time': datetime.now(UTC).isoformat(),
118
- 'ex': ex.__str__() if ex else None,
119
- }
120
- if args:
121
- resp.update(args)
122
- return cls.response_obj(resp)
123
-
124
-
125
- class PlaceOrderView(HttpTradingView):
126
- async def post(self):
127
- broker = self.current_broker()
128
- contract = await self.get_contract(from_json=True)
129
- body_d: dict = await self.request.json()
130
- price = body_d.get('price', 0)
131
- qty = body_d.get('qty', 0)
132
- order_type = OrderType[body_d.get('orderType', '')]
133
- time_in_force = TimeInForce[body_d.get('timeInForce', '')]
134
- lifecycle = Lifecycle[body_d.get('lifecycle', '')]
135
- direction = body_d.get('direction', '')
136
- if price:
137
- price = float(price)
138
- order_id: str = await broker.place_order(
139
- contract=contract,
140
- order_type=order_type,
141
- time_in_force=time_in_force,
142
- lifecycle=lifecycle,
143
- direction=direction,
144
- qty=qty,
145
- price=price,
146
- json=body_d,
147
- )
148
- return self.response_api(broker, {
149
- 'orderId': order_id,
150
- 'args': body_d,
151
- })
152
-
153
-
154
- class OrderStateView(HttpTradingView):
155
- async def get(self):
156
- broker = self.current_broker()
157
- order_id = self.request.query.get('orderId', '')
158
- order: Order = await broker.order(order_id=order_id)
159
- return self.response_api(broker, {
160
- 'order': order,
161
- })
162
-
163
-
164
- class CancelOrderView(HttpTradingView):
165
- async def post(self):
166
- broker = self.current_broker()
167
- body_d: dict = await self.request.json()
168
- order_id = body_d.get('orderId', '')
169
- assert order_id
170
- await broker.cancel_order(order_id=order_id)
171
- return self.response_api(broker, {
172
- 'canceled': True,
173
- })
174
-
175
-
176
- class CashView(HttpTradingView):
177
- async def get(self):
178
- broker = self.current_broker()
179
- cash: Cash = await broker.cash()
180
- return self.response_api(broker, {
181
- 'cash': cash,
182
- })
183
-
184
-
185
- class PositionView(HttpTradingView):
186
- async def get(self):
187
- broker = self.current_broker()
188
- positions: list[Position] = await broker.positions()
189
- return self.response_api(broker, {
190
- 'positions': positions,
191
- })
192
-
193
-
194
- class PlugInView(HttpTradingView):
195
- async def get(self):
196
- broker = self.current_broker()
197
- pong = await broker.ping()
198
- return self.response_api(broker, {
199
- 'pong': pong,
200
- })
201
-
202
-
203
- class QuoteView(HttpTradingView):
204
- async def get(self):
205
- contract = await self.get_contract()
206
- broker = self.current_broker()
207
- quote: Quote = await broker.quote(contract)
208
- return self.response_api(broker, {
209
- 'quote': quote,
210
- })
211
-
212
-
213
- class MarketStatusView(HttpTradingView):
214
- async def get(self):
215
- broker = self.current_broker()
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'
219
- return self.response_api(broker, {
220
- 'marketStatus': ms_dict,
221
- })
222
-
223
-
224
- @web.middleware
225
- async def auth_middleware(request: web.Request, handler):
226
- instance_id = request.match_info.get('instance_id', '')
227
- token = request.headers.get('HT-TOKEN', '')
228
- if not instance_id:
229
- raise web.HTTPNotFound
230
- if not token:
231
- raise web.HTTPNotFound
232
- if len(token) < 16 or len(token) > 64:
233
- raise web.HTTPNotFound
234
- for broker in HttpTradingView.brokers():
235
- if broker.instance_id != instance_id:
236
- continue
237
- if token not in broker.tokens:
238
- raise web.HTTPNotFound
239
- setattr(request, '__current_broker__', broker)
240
- break
241
- else:
242
- raise web.HTTPNotFound
243
- response: web.Response = await handler(request)
244
- delattr(request, '__current_broker__')
245
- return response
246
-
247
-
248
- @web.middleware
249
- async def exception_middleware(request: web.Request, handler):
250
- try:
251
- response: web.Response = await handler(request)
252
- return response
253
- except BrokerError as ex:
254
- return HttpTradingView.response_api(broker=ex.broker, ex=ex)
255
- except Exception as ex:
256
- broker = getattr(request, '__current_broker__', None)
257
- return HttpTradingView.response_api(broker=broker, ex=ex)
258
-
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
-
273
- def run(
274
- host: str,
275
- port: int,
276
- brokers: list[BaseBroker],
277
- std_apis: Callable[[], list[web.RouteDef]] = None,
278
- extend_apis: list[web.RouteDef] = None,
279
- **kwargs
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
- """
289
- app = web.Application(
290
- middlewares=[
291
- auth_middleware,
292
- exception_middleware,
293
- ],
294
- )
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)
302
-
303
- async def _on_startup(app):
304
- HttpTradingView.set_brokers(brokers)
305
- for broker in brokers:
306
- await broker.start()
307
-
308
- async def _on_shutdown(app):
309
- for broker in brokers:
310
- await broker.shutdown()
311
-
312
- app.on_startup.append(_on_startup)
313
- app.on_shutdown.append(_on_shutdown)
314
- web.run_app(
315
- app,
316
- host=host,
317
- port=port,
318
- **kwargs
319
- )
320
-
321
-
322
- __all__ = [
323
- 'run',
324
- 'std_api_factory',
325
- 'HttpTradingView',
1
+ import json
2
+ import asyncio
3
+ from datetime import datetime, UTC
4
+ from typing import Callable
5
+ from aiohttp import web
6
+ from httptrading.broker.base import *
7
+ from httptrading.model import *
8
+
9
+
10
+ class HttpTradingView(web.View):
11
+ __BROKERS: list[BaseBroker] = list()
12
+
13
+ @classmethod
14
+ def set_brokers(cls, brokers: list[BaseBroker]):
15
+ HttpTradingView.__BROKERS = brokers
16
+
17
+ @classmethod
18
+ def brokers(cls):
19
+ return HttpTradingView.__BROKERS.copy()
20
+
21
+ def instance_id(self) -> str:
22
+ return self.request.match_info.get('instance_id', '')
23
+
24
+ def current_broker(self) -> BaseBroker:
25
+ broker = getattr(self.request, '__current_broker__', None)
26
+ if broker is None:
27
+ raise web.HTTPNotFound()
28
+ return broker
29
+
30
+ async def get_contract(self, from_json=False):
31
+ params: dict = await self.request.json() if from_json else self.request.query
32
+ trade_type = TradeType[params.get('tradeType', '--')]
33
+ region = params.get('region', '--')
34
+ ticker = params.get('ticker', '--')
35
+ contract = Contract(
36
+ trade_type=trade_type,
37
+ region=region,
38
+ ticker=ticker,
39
+ )
40
+ return contract
41
+
42
+ @classmethod
43
+ def dumps(cls, obj):
44
+ return json.dumps(obj, default=HtGlobalConfig.JSON_DEFAULT.json_default)
45
+
46
+ @classmethod
47
+ def response_obj(cls, obj):
48
+ return web.Response(text=cls.dumps(obj), content_type='application/json')
49
+
50
+ @classmethod
51
+ def response_api(cls, broker: BaseBroker = None, args: dict = None, ex: Exception = None):
52
+ resp = {
53
+ 'type': 'apiResponse',
54
+ 'instanceId': broker.instance_id if broker else None,
55
+ 'broker': broker.broker_name if broker else None,
56
+ 'brokerDisplay': broker.broker_display if broker else None,
57
+ 'time': datetime.now(UTC).isoformat(),
58
+ 'ex': ex.__str__() if ex else None,
59
+ }
60
+ if args:
61
+ resp.update(args)
62
+ return cls.response_obj(resp)
63
+
64
+
65
+ class PlaceOrderView(HttpTradingView):
66
+ async def post(self):
67
+ broker = self.current_broker()
68
+ contract = await self.get_contract(from_json=True)
69
+ body_d: dict = await self.request.json()
70
+ price = body_d.get('price', 0)
71
+ qty = body_d.get('qty', 0)
72
+ order_type = OrderType[body_d.get('orderType', '')]
73
+ time_in_force = TimeInForce[body_d.get('timeInForce', '')]
74
+ lifecycle = Lifecycle[body_d.get('lifecycle', '')]
75
+ direction = body_d.get('direction', '')
76
+ if price:
77
+ price = float(price)
78
+ order_id: str = await broker.place_order(
79
+ contract=contract,
80
+ order_type=order_type,
81
+ time_in_force=time_in_force,
82
+ lifecycle=lifecycle,
83
+ direction=direction,
84
+ qty=qty,
85
+ price=price,
86
+ json=body_d,
87
+ full_args=body_d,
88
+ )
89
+ return self.response_api(broker, {
90
+ 'orderId': order_id,
91
+ 'args': body_d,
92
+ })
93
+
94
+
95
+ class OrderStateView(HttpTradingView):
96
+ async def get(self):
97
+ broker = self.current_broker()
98
+ order_id = self.request.query.get('orderId', '')
99
+ order: Order = await broker.order(order_id=order_id)
100
+ _ = asyncio.create_task(broker.call_sync(lambda : broker.dump_order(order)))
101
+ return self.response_api(broker, {
102
+ 'order': order,
103
+ })
104
+
105
+
106
+ class CancelOrderView(HttpTradingView):
107
+ async def post(self):
108
+ broker = self.current_broker()
109
+ body_d: dict = await self.request.json()
110
+ order_id = body_d.get('orderId', '')
111
+ assert order_id
112
+ await broker.cancel_order(order_id=order_id)
113
+ return self.response_api(broker, {
114
+ 'canceled': True,
115
+ })
116
+
117
+
118
+ class CashView(HttpTradingView):
119
+ async def get(self):
120
+ broker = self.current_broker()
121
+ cash: Cash = await broker.cash()
122
+ return self.response_api(broker, {
123
+ 'cash': cash,
124
+ })
125
+
126
+
127
+ class PositionView(HttpTradingView):
128
+ async def get(self):
129
+ broker = self.current_broker()
130
+ positions: list[Position] = await broker.positions()
131
+ return self.response_api(broker, {
132
+ 'positions': positions,
133
+ })
134
+
135
+
136
+ class PlugInView(HttpTradingView):
137
+ async def get(self):
138
+ broker = self.current_broker()
139
+ pong = await broker.ping()
140
+ return self.response_api(broker, {
141
+ 'pong': pong,
142
+ })
143
+
144
+
145
+ class QuoteView(HttpTradingView):
146
+ async def get(self):
147
+ contract = await self.get_contract()
148
+ broker = self.current_broker()
149
+ quote: Quote = await broker.quote(contract)
150
+ return self.response_api(broker, {
151
+ 'quote': quote,
152
+ })
153
+
154
+
155
+ class MarketStatusView(HttpTradingView):
156
+ async def get(self):
157
+ broker = self.current_broker()
158
+ ms_dict = await broker.market_status()
159
+ ms_dict = {t.name.lower(): d for t, d in ms_dict.items()}
160
+ ms_dict['type'] = 'marketStatusMap'
161
+ return self.response_api(broker, {
162
+ 'marketStatus': ms_dict,
163
+ })
164
+
165
+
166
+ def create_auth_middleware(token_header: str):
167
+ assert isinstance(token_header, str)
168
+ assert token_header
169
+
170
+ @web.middleware
171
+ async def _auth_middleware(request: web.Request, handler):
172
+ instance_id = request.match_info.get('instance_id', '')
173
+ token = request.headers.get(token_header, '')
174
+ if not instance_id:
175
+ raise web.HTTPNotFound
176
+ if not token:
177
+ raise web.HTTPNotFound
178
+ if len(token) < 16 or len(token) > 64:
179
+ raise web.HTTPNotFound
180
+ for broker in HttpTradingView.brokers():
181
+ if broker.instance_id != instance_id:
182
+ continue
183
+ if token not in broker.tokens:
184
+ raise web.HTTPNotFound
185
+ setattr(request, '__current_broker__', broker)
186
+ break
187
+ else:
188
+ raise web.HTTPNotFound
189
+ response: web.Response = await handler(request)
190
+ delattr(request, '__current_broker__')
191
+ return response
192
+ return _auth_middleware
193
+
194
+
195
+ @web.middleware
196
+ async def exception_middleware(request: web.Request, handler):
197
+ try:
198
+ response: web.Response = await handler(request)
199
+ return response
200
+ except BrokerError as ex:
201
+ return HttpTradingView.response_api(broker=ex.broker, ex=ex)
202
+ except Exception as ex:
203
+ broker = getattr(request, '__current_broker__', None)
204
+ return HttpTradingView.response_api(broker=broker, ex=ex)
205
+
206
+
207
+ def std_api_factory() -> list[web.RouteDef]:
208
+ apis = [
209
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/place', PlaceOrderView),
210
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/state', OrderStateView),
211
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/cancel', CancelOrderView),
212
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/cash/state', CashView),
213
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/position/state', PositionView),
214
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/ping/state', PlugInView),
215
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/state', MarketStatusView),
216
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/quote', QuoteView),
217
+ ]
218
+ return apis
219
+
220
+
221
+ def run(
222
+ host: str,
223
+ port: int,
224
+ brokers: list[BaseBroker],
225
+ std_apis: Callable[[], list[web.RouteDef]] = None,
226
+ extend_apis: list[web.RouteDef] = None,
227
+ token_header: str = 'HT-TOKEN',
228
+ **kwargs
229
+ ) -> None:
230
+ """
231
+ @param host: 监听地址
232
+ @param port: 监听端口
233
+ @param brokers: 需要控制的交易通道对象列表
234
+ @param std_apis: 如果需要替换默认提供的接口, 这里提供工厂函数的回调
235
+ @param extend_apis: 如果需要增加自定义接口, 这里传入 RouteDef 列表
236
+ @param token_header: 定制 token 凭据的 header 键名
237
+ @param kwargs: 其他的参数将传给 aiohttp.web.run_app 函数
238
+ """
239
+ app = web.Application(
240
+ middlewares=[
241
+ create_auth_middleware(token_header=token_header),
242
+ exception_middleware,
243
+ ],
244
+ )
245
+
246
+ apis = (std_api_factory if std_apis is None else std_apis)()
247
+
248
+ if extend_apis:
249
+ apis.extend(extend_apis)
250
+
251
+ app.add_routes(apis)
252
+
253
+ async def _on_startup(app):
254
+ HttpTradingView.set_brokers(brokers)
255
+ for broker in brokers:
256
+ await broker.start()
257
+
258
+ async def _on_shutdown(app):
259
+ for broker in brokers:
260
+ await broker.shutdown()
261
+
262
+ app.on_startup.append(_on_startup)
263
+ app.on_shutdown.append(_on_shutdown)
264
+ web.run_app(
265
+ app,
266
+ host=host,
267
+ port=port,
268
+ **kwargs
269
+ )
270
+
271
+
272
+ __all__ = [
273
+ 'run',
274
+ 'std_api_factory',
275
+ 'HttpTradingView',
326
276
  ]