httptrading 1.0.1__py3-none-any.whl → 1.0.3__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,297 +1,268 @@
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) -> BaseBroker:
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
- ms_dict = {t.name.lower(): d for t, d in ms_dict.items()}
216
- ms_dict['type'] = 'marketStatusMap'
217
- return self.response_api(broker, {
218
- 'marketStatus': ms_dict,
219
- })
220
-
221
-
222
- @web.middleware
223
- async def auth_middleware(request: web.Request, handler):
224
- instance_id = request.match_info.get('instance_id', '')
225
- token = request.headers.get('HT-TOKEN', '')
226
- if not instance_id:
227
- raise web.HTTPNotFound
228
- if not token:
229
- raise web.HTTPNotFound
230
- if len(token) < 16 or len(token) > 64:
231
- raise web.HTTPNotFound
232
- for broker in HttpTradingView.brokers():
233
- if broker.instance_id != instance_id:
234
- continue
235
- if token not in broker.tokens:
236
- raise web.HTTPNotFound
237
- setattr(request, '__current_broker__', broker)
238
- break
239
- else:
240
- raise web.HTTPNotFound
241
- response: web.Response = await handler(request)
242
- delattr(request, '__current_broker__')
243
- return response
244
-
245
-
246
- @web.middleware
247
- async def exception_middleware(request: web.Request, handler):
248
- try:
249
- response: web.Response = await handler(request)
250
- return response
251
- except BrokerError as ex:
252
- return HttpTradingView.response_api(broker=ex.broker, ex=ex)
253
- except Exception as ex:
254
- broker = getattr(request, '__current_broker__', None)
255
- return HttpTradingView.response_api(broker=broker, ex=ex)
256
-
257
-
258
- def run(
259
- host: str,
260
- port: int,
261
- brokers: list[BaseBroker],
262
- ) -> None:
263
- app = web.Application(
264
- middlewares=[
265
- auth_middleware,
266
- exception_middleware,
267
- ],
268
- )
269
- app.add_routes(
270
- [
271
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/place', PlaceOrderView),
272
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/state', OrderStateView),
273
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/cancel', CancelOrderView),
274
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/cash/state', CashView),
275
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/position/state', PositionView),
276
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/ping/state', PlugInView),
277
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/state', MarketStatusView),
278
- web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/quote', QuoteView),
279
- ]
280
- )
281
-
282
- async def _on_startup(app):
283
- HttpTradingView.set_brokers(brokers)
284
- for broker in brokers:
285
- await broker.start()
286
-
287
- async def _on_shutdown(app):
288
- for broker in brokers:
289
- await broker.shutdown()
290
-
291
- app.on_startup.append(_on_startup)
292
- app.on_shutdown.append(_on_shutdown)
293
- web.run_app(
294
- app,
295
- host=host,
296
- port=port,
297
- )
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
+ )
88
+ return self.response_api(broker, {
89
+ 'orderId': order_id,
90
+ 'args': body_d,
91
+ })
92
+
93
+
94
+ class OrderStateView(HttpTradingView):
95
+ async def get(self):
96
+ broker = self.current_broker()
97
+ order_id = self.request.query.get('orderId', '')
98
+ order: Order = await broker.order(order_id=order_id)
99
+ _ = asyncio.create_task(broker.call_sync(lambda : broker.dump_order(order)))
100
+ return self.response_api(broker, {
101
+ 'order': order,
102
+ })
103
+
104
+
105
+ class CancelOrderView(HttpTradingView):
106
+ async def post(self):
107
+ broker = self.current_broker()
108
+ body_d: dict = await self.request.json()
109
+ order_id = body_d.get('orderId', '')
110
+ assert order_id
111
+ await broker.cancel_order(order_id=order_id)
112
+ return self.response_api(broker, {
113
+ 'canceled': True,
114
+ })
115
+
116
+
117
+ class CashView(HttpTradingView):
118
+ async def get(self):
119
+ broker = self.current_broker()
120
+ cash: Cash = await broker.cash()
121
+ return self.response_api(broker, {
122
+ 'cash': cash,
123
+ })
124
+
125
+
126
+ class PositionView(HttpTradingView):
127
+ async def get(self):
128
+ broker = self.current_broker()
129
+ positions: list[Position] = await broker.positions()
130
+ return self.response_api(broker, {
131
+ 'positions': positions,
132
+ })
133
+
134
+
135
+ class PlugInView(HttpTradingView):
136
+ async def get(self):
137
+ broker = self.current_broker()
138
+ pong = await broker.ping()
139
+ return self.response_api(broker, {
140
+ 'pong': pong,
141
+ })
142
+
143
+
144
+ class QuoteView(HttpTradingView):
145
+ async def get(self):
146
+ contract = await self.get_contract()
147
+ broker = self.current_broker()
148
+ quote: Quote = await broker.quote(contract)
149
+ return self.response_api(broker, {
150
+ 'quote': quote,
151
+ })
152
+
153
+
154
+ class MarketStatusView(HttpTradingView):
155
+ async def get(self):
156
+ broker = self.current_broker()
157
+ ms_dict = await broker.market_status()
158
+ ms_dict = {t.name.lower(): d for t, d in ms_dict.items()}
159
+ ms_dict['type'] = 'marketStatusMap'
160
+ return self.response_api(broker, {
161
+ 'marketStatus': ms_dict,
162
+ })
163
+
164
+
165
+ @web.middleware
166
+ async def auth_middleware(request: web.Request, handler):
167
+ instance_id = request.match_info.get('instance_id', '')
168
+ token = request.headers.get('HT-TOKEN', '')
169
+ if not instance_id:
170
+ raise web.HTTPNotFound
171
+ if not token:
172
+ raise web.HTTPNotFound
173
+ if len(token) < 16 or len(token) > 64:
174
+ raise web.HTTPNotFound
175
+ for broker in HttpTradingView.brokers():
176
+ if broker.instance_id != instance_id:
177
+ continue
178
+ if token not in broker.tokens:
179
+ raise web.HTTPNotFound
180
+ setattr(request, '__current_broker__', broker)
181
+ break
182
+ else:
183
+ raise web.HTTPNotFound
184
+ response: web.Response = await handler(request)
185
+ delattr(request, '__current_broker__')
186
+ return response
187
+
188
+
189
+ @web.middleware
190
+ async def exception_middleware(request: web.Request, handler):
191
+ try:
192
+ response: web.Response = await handler(request)
193
+ return response
194
+ except BrokerError as ex:
195
+ return HttpTradingView.response_api(broker=ex.broker, ex=ex)
196
+ except Exception as ex:
197
+ broker = getattr(request, '__current_broker__', None)
198
+ return HttpTradingView.response_api(broker=broker, ex=ex)
199
+
200
+
201
+ def std_api_factory() -> list[web.RouteDef]:
202
+ apis = [
203
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/place', PlaceOrderView),
204
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/state', OrderStateView),
205
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/order/cancel', CancelOrderView),
206
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/cash/state', CashView),
207
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/position/state', PositionView),
208
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/ping/state', PlugInView),
209
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/state', MarketStatusView),
210
+ web.view(r'/httptrading/api/{instance_id:\w{16,32}}/market/quote', QuoteView),
211
+ ]
212
+ return apis
213
+
214
+
215
+ def run(
216
+ host: str,
217
+ port: int,
218
+ brokers: list[BaseBroker],
219
+ std_apis: Callable[[], list[web.RouteDef]] = None,
220
+ extend_apis: list[web.RouteDef] = None,
221
+ **kwargs
222
+ ) -> None:
223
+ """
224
+ @param host: 监听地址
225
+ @param port: 监听端口
226
+ @param brokers: 需要控制的交易通道对象列表
227
+ @param std_apis: 如果需要替换默认提供的接口, 这里提供工厂函数的回调
228
+ @param extend_apis: 如果需要增加自定义接口, 这里传入 RouteDef 列表
229
+ @param kwargs: 其他的参数将传给 aiohttp.web.run_app 函数
230
+ """
231
+ app = web.Application(
232
+ middlewares=[
233
+ auth_middleware,
234
+ exception_middleware,
235
+ ],
236
+ )
237
+
238
+ apis = (std_api_factory if std_apis is None else std_apis)()
239
+
240
+ if extend_apis:
241
+ apis.extend(extend_apis)
242
+
243
+ app.add_routes(apis)
244
+
245
+ async def _on_startup(app):
246
+ HttpTradingView.set_brokers(brokers)
247
+ for broker in brokers:
248
+ await broker.start()
249
+
250
+ async def _on_shutdown(app):
251
+ for broker in brokers:
252
+ await broker.shutdown()
253
+
254
+ app.on_startup.append(_on_startup)
255
+ app.on_shutdown.append(_on_shutdown)
256
+ web.run_app(
257
+ app,
258
+ host=host,
259
+ port=port,
260
+ **kwargs
261
+ )
262
+
263
+
264
+ __all__ = [
265
+ 'run',
266
+ 'std_api_factory',
267
+ 'HttpTradingView',
268
+ ]