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.
@@ -0,0 +1,318 @@
1
+ """
2
+ 接入盈透证券的API文档
3
+ https://ib-insync.readthedocs.io/readme.html
4
+ """
5
+ import re
6
+ import asyncio
7
+ from typing import Any
8
+ from httptrading.tool.leaky_bucket import *
9
+ from httptrading.tool.time import *
10
+ from httptrading.broker.base import *
11
+ from httptrading.model import *
12
+
13
+
14
+ @broker_register(name='interactiveBrokers', display='盈透证券', detect_pkg=DetectPkg('ib-insync', 'ib_insync'))
15
+ class InteractiveBrokers(SecuritiesBroker):
16
+ def __init__(self, broker_args: dict = None, instance_id: str = None, tokens: list[str] = None):
17
+ super().__init__(broker_args, instance_id, tokens)
18
+ self._lock = asyncio.Lock()
19
+ self._client = None
20
+ self._account_id = None
21
+ self._client_id = None
22
+ self._ib_contracts: dict[Contract, Any] = dict()
23
+ self._plugin_bucket = LeakyBucket(60)
24
+ self._account_bucket = LeakyBucket(60)
25
+ self._order_bucket = LeakyBucket(60)
26
+ self._quote_bucket = LeakyBucket(60)
27
+ self._on_init()
28
+
29
+ def _on_init(self):
30
+ self._account_id = self.broker_args.get('account_id')
31
+ self._client_id = self.broker_args.get('client_id')
32
+
33
+ async def start(self):
34
+ await self._try_create_client()
35
+
36
+ async def shutdown(self):
37
+ ib_socket = self._client
38
+ if ib_socket:
39
+ ib_socket.disconnect()
40
+ self._client = None
41
+
42
+ @property
43
+ def timeout(self):
44
+ timeout = max(4, self.broker_args.get('timeout', 8))
45
+ return timeout
46
+
47
+ @classmethod
48
+ def ib_contract_to_contract(cls, contract) -> Contract | None:
49
+ if contract.secType != 'STK':
50
+ return None
51
+ symbol = contract.symbol
52
+ trade_type = TradeType.Securities
53
+ region = ''
54
+ ticker = ''
55
+ if re.match(r'^[01356]\d{5}$', symbol):
56
+ region = 'CN'
57
+ ticker = symbol
58
+ if re.match(r'^\d{5}$', symbol):
59
+ region = 'HK'
60
+ ticker = symbol
61
+ if re.match(r'^\w{1,5}$', symbol):
62
+ region = 'US'
63
+ ticker = symbol
64
+ if not region or not ticker:
65
+ return None
66
+ return Contract(
67
+ trade_type=trade_type,
68
+ ticker=ticker,
69
+ region=region,
70
+ )
71
+
72
+ async def contract_to_ib_contract(self, contract) -> Any | None:
73
+ import ib_insync
74
+ async with self._lock:
75
+ if contract in self._ib_contracts:
76
+ return self._ib_contracts[contract]
77
+ print(f'{contract}未命中')
78
+ currency = self.contract_to_currency(contract)
79
+ ib_contract = ib_insync.Stock(contract.ticker, 'SMART', currency=currency)
80
+ client = self._client
81
+ client.qualifyContracts(*[ib_contract, ])
82
+ self._ib_contracts[contract] = ib_contract
83
+ return ib_contract
84
+
85
+ async def _try_create_client(self):
86
+ import ib_insync
87
+ async with self._lock:
88
+ ib_socket = self._client
89
+ if ib_socket:
90
+ try:
91
+ ib_dt = await ib_socket.reqCurrentTimeAsync()
92
+ now = TimeTools.utc_now()
93
+ if TimeTools.timedelta(ib_dt, seconds=self.timeout) <= now:
94
+ raise TimeoutError
95
+ assert ib_socket.isConnected()
96
+ return
97
+ except Exception as e:
98
+ pass
99
+ if ib_socket:
100
+ ib_socket.disconnect()
101
+ ib = ib_socket
102
+ else:
103
+ ib = ib_insync.IB()
104
+ host = self.broker_args.get('host', '127.0.0.1')
105
+ port = self.broker_args.get('port', 4000)
106
+ client_id = self.broker_args.get('client_id', self._client_id)
107
+ timeout = self.timeout
108
+ account_id = self._account_id
109
+ new_client = await ib.connectAsync(
110
+ host=host,
111
+ port=port,
112
+ clientId=client_id,
113
+ timeout=timeout,
114
+ account=account_id,
115
+ )
116
+ self._client = new_client
117
+
118
+ async def ping(self) -> bool:
119
+ async with self._plugin_bucket:
120
+ try:
121
+ await self._try_create_client()
122
+ return True
123
+ except Exception as e:
124
+ return False
125
+
126
+ async def _cash(self) -> Cash:
127
+ import ib_insync
128
+ async with self._account_bucket:
129
+ _client: ib_insync.IB = self._client
130
+ if not _client:
131
+ raise Exception('盈透连接对象未准备好')
132
+ l = await _client.accountSummaryAsync(account=self._account_id)
133
+ d: dict[str, ib_insync.AccountValue] = {i.tag: i for i in l}
134
+ item = d['TotalCashValue']
135
+ assert item.currency == 'USD'
136
+ amount = float(item.value)
137
+ cash = Cash(
138
+ currency='USD',
139
+ amount=amount,
140
+ )
141
+ return cash
142
+
143
+ async def cash(self) -> Cash:
144
+ return await self.call_async(self._cash())
145
+
146
+ async def _positions(self):
147
+ import ib_insync
148
+ result = list()
149
+ async with self._account_bucket:
150
+ _client: ib_insync.IB = self._client
151
+ if not _client:
152
+ raise Exception('盈透连接对象未准备好')
153
+ l = _client.positions(account=self._account_id)
154
+ for position in l:
155
+ ib_contract = position.contract
156
+ contract = self.ib_contract_to_contract(ib_contract)
157
+ if not contract:
158
+ continue
159
+ currency = ib_contract.currency
160
+ qty = int(position.position)
161
+ position = Position(
162
+ broker=self.broker_name,
163
+ broker_display=self.broker_display,
164
+ contract=contract,
165
+ unit=Unit.Share,
166
+ currency=currency,
167
+ qty=qty,
168
+ )
169
+ result.append(position)
170
+ return result
171
+
172
+ async def positions(self):
173
+ return await self.call_async(self._positions())
174
+
175
+ async def _place_order(
176
+ self,
177
+ contract: Contract,
178
+ order_type: OrderType,
179
+ time_in_force: TimeInForce,
180
+ lifecycle: Lifecycle,
181
+ direction: str,
182
+ qty: int,
183
+ price: float = None,
184
+ **kwargs
185
+ ) -> str:
186
+ import ib_insync
187
+ with self._order_bucket:
188
+ client = self._client
189
+ ib_contract = await self.contract_to_ib_contract(contract)
190
+
191
+ def _map_time_in_force():
192
+ match time_in_force:
193
+ case TimeInForce.DAY:
194
+ return 'DAY'
195
+ case TimeInForce.GTC:
196
+ return 'GTC'
197
+ case _:
198
+ raise Exception(f'不支持的订单有效期: {time_in_force}')
199
+
200
+ def _map_lifecycle():
201
+ match lifecycle:
202
+ case Lifecycle.RTH:
203
+ return False
204
+ case Lifecycle.ETH:
205
+ return True
206
+ case _:
207
+ raise Exception(f'不支持的交易时段: {lifecycle}')
208
+
209
+ def _map_order():
210
+ match order_type:
211
+ case OrderType.Limit:
212
+ return ib_insync.LimitOrder(
213
+ action=direction,
214
+ totalQuantity=qty,
215
+ lmtPrice=price,
216
+ )
217
+ case OrderType.Market:
218
+ return ib_insync.MarketOrder(
219
+ action=direction,
220
+ totalQuantity=qty,
221
+ )
222
+ case _:
223
+ raise Exception(f'不支持的订单类型: {order_type}')
224
+
225
+ ib_order = _map_order()
226
+ ib_order.tif = _map_time_in_force()
227
+ ib_order.outsideRth = _map_lifecycle()
228
+ trade: ib_insync.Trade = client.placeOrder(ib_contract, ib_order)
229
+ await asyncio.sleep(2.0)
230
+ order_id = str(trade.order.permId)
231
+ assert order_id
232
+ return order_id
233
+
234
+ async def place_order(
235
+ self,
236
+ contract: Contract,
237
+ order_type: OrderType,
238
+ time_in_force: TimeInForce,
239
+ lifecycle: Lifecycle,
240
+ direction: str,
241
+ qty: int,
242
+ price: float = None,
243
+ **kwargs
244
+ ) -> str:
245
+ return await self.call_async(self._place_order(
246
+ contract=contract,
247
+ order_type=order_type,
248
+ time_in_force=time_in_force,
249
+ lifecycle=lifecycle,
250
+ direction=direction,
251
+ qty=qty,
252
+ price=price,
253
+ **kwargs
254
+ ))
255
+
256
+ async def _cancel_order(self, order_id: str):
257
+ order_id_int = int(order_id)
258
+ with self._order_bucket:
259
+ client = self._client
260
+ trades = client.trades()
261
+ for ib_trade in trades:
262
+ ib_order = ib_trade.order
263
+ if ib_order.permId != order_id_int:
264
+ continue
265
+ client.cancelOrder(ib_order)
266
+ break
267
+
268
+ async def cancel_order(self, order_id: str):
269
+ await self.call_async(self._cancel_order(order_id=order_id))
270
+
271
+ async def _order(self, order_id: str) -> Order:
272
+ import ib_insync
273
+
274
+ def _total_fills(trade) -> int:
275
+ return int(trade.filled())
276
+
277
+ def _avg_price(trade) -> float:
278
+ total_fills = _total_fills(trade)
279
+ if not total_fills:
280
+ return 0
281
+ cap = sum([fill.execution.shares * fill.execution.avgPrice for fill in trade.fills], 0.0)
282
+ return round(cap / total_fills, 5)
283
+
284
+ with self._order_bucket:
285
+ client = self._client
286
+ trades = client.trades()
287
+ order_id_int = int(order_id)
288
+ for ib_trade in trades:
289
+ ib_order = ib_trade.order
290
+ if ib_order.permId != order_id_int:
291
+ continue
292
+ qty = int(_total_fills(ib_trade) + ib_trade.remaining())
293
+ filled_qty = _total_fills(ib_trade)
294
+ qty = qty or filled_qty
295
+ assert qty >= filled_qty
296
+ avg_fill_price = _avg_price(ib_trade)
297
+ reason = ''
298
+ if ib_trade.orderStatus.status == ib_insync.OrderStatus.Inactive:
299
+ reason = 'Inactive'
300
+
301
+ cancel_status = {ib_insync.OrderStatus.Cancelled, ib_insync.OrderStatus.ApiCancelled, }
302
+ is_cancelled = ib_trade.orderStatus.status in cancel_status
303
+ return Order(
304
+ order_id=order_id,
305
+ currency=ib_trade.contract.currency,
306
+ qty=qty,
307
+ filled_qty=filled_qty,
308
+ avg_price=avg_fill_price,
309
+ error_reason=reason,
310
+ is_canceled=is_cancelled,
311
+ )
312
+ raise Exception(f'查询不到订单{order_id}')
313
+
314
+ async def order(self, order_id: str) -> Order:
315
+ return await self.call_async(self._order(order_id))
316
+
317
+
318
+ __all__ = ['InteractiveBrokers', ]
@@ -0,0 +1,380 @@
1
+ """
2
+ 长桥证券文档见
3
+ https://open.longportapp.com/docs
4
+ """
5
+ import re
6
+ import tomllib
7
+ import threading
8
+ from decimal import Decimal
9
+ from datetime import datetime
10
+ import tomlkit
11
+ from httptrading.tool.leaky_bucket import *
12
+ from httptrading.broker.base import *
13
+ from httptrading.model import *
14
+ from httptrading.tool.time import *
15
+ from httptrading.tool.locate import *
16
+
17
+
18
+ class TokenKeeper:
19
+ """
20
+ 长桥证券的会话需要通过下发的 token 凭据验证身份,
21
+ 因为 token 有有效期, 该类负责判断 token 是否快要到过期时间(默认剩余3天以内), 使得交易通道对象可以通过 SDK 接口去下载新 token.
22
+
23
+ 因此, 初次使用的时候, 必须去他们的开发者网站上拿到初代 token, 做成一个 toml 格式文件, TokenKeeper 将读写这个文件.
24
+ 这个 toml 文件需要设置两个字段, 分别是 token 和 expiry,
25
+ token 字段填入给的令牌字符串,
26
+ expiry 是过期时间的字符串, 例如 "2025-01-01T00:00:00.000000+00:00"
27
+ 没有这个 toml 文件, 或者 token 文件的 expiry 已经过期了, 系统便没办法连接到他们的接口上做自动更新, 因此会有启动上的问题.
28
+ """
29
+
30
+ def __init__(self, token_file: str):
31
+ assert token_file
32
+ self.token_file = token_file
33
+ self.lock = threading.RLock()
34
+ with self.lock:
35
+ text = LocateTools.read_file(token_file)
36
+ d = tomllib.loads(text)
37
+ token, expiry = d.get('token'), d.get('expiry')
38
+ assert token
39
+ self.token = token
40
+ self.expiry = datetime.fromisoformat(expiry)
41
+ if self.is_expired:
42
+ raise ValueError(f'长桥证券的访问令牌已经过期{self.expiry}')
43
+
44
+ @property
45
+ def is_expired(self):
46
+ if not self.expiry:
47
+ return False
48
+ if TimeTools.utc_now() >= self.expiry:
49
+ return True
50
+ return False
51
+
52
+ @property
53
+ def should_refresh(self):
54
+ now = TimeTools.utc_now()
55
+ if now >= self.expiry:
56
+ return False
57
+ if now >= TimeTools.timedelta(self.expiry, days=-3):
58
+ return True
59
+ return False
60
+
61
+ def update_token(self, token: str, expiry: datetime):
62
+ assert self.token_file
63
+ assert token
64
+ assert expiry
65
+ with self.lock:
66
+ self.token = token
67
+ self.expiry = expiry
68
+ d = {
69
+ 'token': self.token,
70
+ 'expiry': self.expiry.isoformat(),
71
+ }
72
+ text = tomlkit.dumps(d)
73
+ LocateTools.write_file(self.token_file, text)
74
+
75
+
76
+ @broker_register(name='longBridge', display='长桥证券', detect_pkg=DetectPkg('longport', 'longport'))
77
+ class LongBridge(SecuritiesBroker):
78
+ def __init__(self, broker_args: dict = None, instance_id: str = None, tokens: list[str] = None):
79
+ super().__init__(broker_args, instance_id, tokens)
80
+ self._token_file = ''
81
+ self._token_keeper: TokenKeeper | None = None
82
+ self._auto_refresh_token = False
83
+ self._token_bucket = LeakyBucket(6)
84
+ self._quote_bucket = LeakyBucket(60)
85
+ self._assets_bucket = LeakyBucket(60)
86
+ self._on_init()
87
+
88
+ def _on_init(self):
89
+ config_dict = self.broker_args
90
+ token_file = config_dict.get('token_file', '')
91
+ assert token_file
92
+ self._token_file = token_file
93
+
94
+ keeper = TokenKeeper(token_file)
95
+ self._token_keeper = keeper
96
+
97
+ auto_refresh_token = config_dict.get('auto_refresh_token', False)
98
+ self._auto_refresh_token = auto_refresh_token
99
+ self._reset_client()
100
+
101
+ def _reset_client(self):
102
+ cfg = self.broker_args
103
+ from longport.openapi import Config, QuoteContext, TradeContext
104
+ app_key = cfg.get('app_key')
105
+ app_secret = cfg.get('app_secret')
106
+ assert app_key
107
+ assert app_secret
108
+ self._token_keeper = self._token_keeper or TokenKeeper(self._token_file)
109
+ config = Config(
110
+ app_key=app_key,
111
+ app_secret=app_secret,
112
+ access_token=self._token_keeper.token,
113
+ )
114
+ quote_ctx = QuoteContext(config)
115
+ trade_ctx = TradeContext(config)
116
+ self._lp_config = config
117
+ self._quote_client = quote_ctx
118
+ self._trade_client = trade_ctx
119
+
120
+ def _try_refresh(self):
121
+ if not self._auto_refresh_token:
122
+ return
123
+ cfg = self._lp_config
124
+ keeper = self._token_keeper
125
+ with keeper.lock:
126
+ if not keeper.should_refresh:
127
+ return
128
+ now = TimeTools.utc_now()
129
+ expiry = TimeTools.timedelta(now, days=90)
130
+ with self._token_bucket:
131
+ token = cfg.refresh_access_token()
132
+ assert token
133
+ keeper.update_token(token, expiry)
134
+ self._reset_client()
135
+
136
+ @classmethod
137
+ def symbol_to_contract(cls, symbol: str) -> Contract | None:
138
+ region = ''
139
+ ticker = ''
140
+ if m := re.match(r'^(\S+)\.US$', symbol):
141
+ region = 'US'
142
+ ticker = m.groups()[0]
143
+ if m := re.match(r'^(\d{5})\.HK$', symbol):
144
+ region = 'HK'
145
+ ticker = m.groups()[0]
146
+ if m := re.match(r'^(\d{6})\.SH$', symbol):
147
+ region = 'CN'
148
+ ticker = m.groups()[0]
149
+ if m := re.match(r'^(\d{6})\.SZ$', symbol):
150
+ region = 'CN'
151
+ ticker = m.groups()[0]
152
+ if not region or not ticker:
153
+ return None
154
+ return Contract(
155
+ trade_type=TradeType.Securities,
156
+ ticker=ticker,
157
+ region=region,
158
+ )
159
+
160
+ @classmethod
161
+ def contract_to_symbol(cls, contract: Contract) -> str:
162
+ if contract.trade_type != TradeType.Securities:
163
+ raise Exception(f'不能支持的交易品种{contract}映射为交易代码')
164
+ region, ticker = contract.region, contract.ticker
165
+ code = None
166
+ if region == 'CN' and re.match(r'^[56]\d{5}$', ticker):
167
+ code = f'{ticker}.SH'
168
+ elif region == 'CN' and re.match(r'^[013]\d{5}$', ticker):
169
+ code = f'{ticker}.SZ'
170
+ elif region == 'HK' and re.match(r'^\d{5}$', ticker):
171
+ code = f'{ticker}.HK'
172
+ elif region == 'US' and re.match(r'^\w+$', ticker):
173
+ code = f'{ticker}.US'
174
+ if not code:
175
+ raise Exception(f'不能映射{contract}为交易代码')
176
+ return code
177
+
178
+ def _positions(self):
179
+ result = list()
180
+ with self._assets_bucket:
181
+ self._try_refresh()
182
+ items = self._trade_client.stock_positions().channels
183
+ for channel in items:
184
+ if channel.account_channel != 'lb':
185
+ continue
186
+ for node in channel.positions:
187
+ symbol = node.symbol
188
+ contract = self.symbol_to_contract(symbol)
189
+ if not contract:
190
+ continue
191
+ position = Position(
192
+ broker=self.broker_name,
193
+ broker_display=self.broker_display,
194
+ contract=contract,
195
+ unit=Unit.Share,
196
+ currency=node.currency,
197
+ qty=int(node.quantity),
198
+ )
199
+ result.append(position)
200
+ return result
201
+
202
+ async def positions(self):
203
+ return await self.call_sync(lambda : self._positions())
204
+
205
+ def _cash(self) -> Cash:
206
+ with self._assets_bucket:
207
+ self._try_refresh()
208
+ resp = self._trade_client.account_balance('USD')
209
+ for node in resp:
210
+ if node.currency != 'USD':
211
+ continue
212
+ cash = Cash(
213
+ currency=node.currency,
214
+ amount=float(node.total_cash),
215
+ )
216
+ return cash
217
+ raise Exception('可用资金信息获取不到记录')
218
+
219
+ async def cash(self) -> Cash:
220
+ return await self.call_sync(lambda : self._cash())
221
+
222
+ def _quote(self, contract: Contract):
223
+ from longport.openapi import TradeStatus
224
+ symbol = self.contract_to_symbol(contract)
225
+ tz = self.contract_to_tz(contract)
226
+ currency = self.contract_to_currency(contract)
227
+
228
+ ctx = self._quote_client
229
+ with self._quote_bucket:
230
+ self._try_refresh()
231
+ resp = ctx.quote([symbol, ])
232
+ if not resp:
233
+ raise Exception(f'查询不到{contract}的快照报价')
234
+ item = resp[0]
235
+ update_dt = TimeTools.from_timestamp(item.timestamp.timestamp(), tz)
236
+ is_tradable = bool(item.trade_status == TradeStatus.Normal)
237
+ return Quote(
238
+ contract=contract,
239
+ currency=currency,
240
+ is_tradable=is_tradable,
241
+ latest=float(item.last_done),
242
+ pre_close=float(item.prev_close),
243
+ open_price=float(item.open),
244
+ high_price=float(item.high),
245
+ low_price=float(item.low),
246
+ time=update_dt,
247
+ )
248
+
249
+ async def quote(self, contract: Contract):
250
+ return await self.call_sync(lambda : self._quote(contract))
251
+
252
+ def _place_order(
253
+ self,
254
+ contract: Contract,
255
+ order_type: OrderType,
256
+ time_in_force: TimeInForce,
257
+ lifecycle: Lifecycle,
258
+ direction: str,
259
+ qty: int,
260
+ price: float = None,
261
+ **kwargs
262
+ ) -> str:
263
+ from longport.openapi import OrderType as LbOrderType, OrderSide, TimeInForceType, OutsideRTH
264
+ if contract.trade_type != TradeType.Securities:
265
+ raise Exception(f'不支持的下单品种: {contract.trade_type}')
266
+ if contract.region == 'US' and order_type == OrderType.Market and lifecycle != Lifecycle.RTH:
267
+ raise Exception(f'交易时段不支持市价单')
268
+ symbol = self.contract_to_symbol(contract)
269
+ assert qty > 0
270
+ assert price > 0
271
+
272
+ def _map_trade_side():
273
+ match direction:
274
+ case 'BUY':
275
+ return OrderSide.Buy
276
+ case 'SELL':
277
+ return OrderSide.Sell
278
+ case _:
279
+ raise Exception(f'不支持的买卖方向: {direction}')
280
+
281
+ def _map_order_type():
282
+ match order_type:
283
+ case OrderType.Market:
284
+ return LbOrderType.MO
285
+ case OrderType.Limit:
286
+ return LbOrderType.LO
287
+ case _:
288
+ raise Exception(f'不支持的订单类型: {order_type}')
289
+
290
+ def _map_time_in_force():
291
+ match time_in_force:
292
+ case TimeInForce.DAY:
293
+ return TimeInForceType.Day
294
+ case TimeInForce.GTC:
295
+ return TimeInForceType.GoodTilCanceled
296
+ case _:
297
+ raise Exception(f'不支持的订单有效期: {time_in_force}')
298
+
299
+ def _map_lifecycle():
300
+ match lifecycle:
301
+ case Lifecycle.RTH:
302
+ return OutsideRTH.RTHOnly
303
+ case Lifecycle.ETH:
304
+ return OutsideRTH.AnyTime # 没错, 允许盘前盘后
305
+ case Lifecycle.OVERNIGHT:
306
+ return OutsideRTH.Overnight
307
+ case _:
308
+ raise Exception(f'不支持的交易时段: {lifecycle}')
309
+
310
+ with self._assets_bucket:
311
+ self._try_refresh()
312
+ resp = self._trade_client.submit_order(
313
+ symbol=symbol,
314
+ order_type=_map_order_type(),
315
+ side=_map_trade_side(),
316
+ outside_rth=_map_lifecycle(),
317
+ submitted_quantity=Decimal(qty),
318
+ time_in_force=_map_time_in_force(),
319
+ submitted_price=Decimal(price) if price is not None else None,
320
+ )
321
+ assert resp.order_id
322
+ return resp.order_id
323
+
324
+ async def place_order(
325
+ self,
326
+ contract: Contract,
327
+ order_type: OrderType,
328
+ time_in_force: TimeInForce,
329
+ lifecycle: Lifecycle,
330
+ direction: str,
331
+ qty: int,
332
+ price: float = None,
333
+ **kwargs
334
+ ) -> str:
335
+ return await self.call_sync(lambda : self._place_order(
336
+ contract=contract,
337
+ order_type=order_type,
338
+ time_in_force=time_in_force,
339
+ lifecycle=lifecycle,
340
+ direction=direction,
341
+ qty=qty,
342
+ price=price,
343
+ **kwargs
344
+ ))
345
+
346
+ def _order(self, order_id: str) -> Order:
347
+ from longport.openapi import OrderStatus
348
+ with self._assets_bucket:
349
+ self._try_refresh()
350
+ resp = self._trade_client.order_detail(order_id=order_id)
351
+ reason = ''
352
+ if resp.status == OrderStatus.Rejected:
353
+ reason = '已拒绝'
354
+ if resp.status == OrderStatus.Expired:
355
+ reason = '已过期'
356
+ if resp.status == OrderStatus.PartialWithdrawal:
357
+ reason = '部分撤单'
358
+ return Order(
359
+ order_id=order_id,
360
+ currency=resp.currency,
361
+ qty=int(resp.quantity),
362
+ filled_qty=int(resp.executed_quantity),
363
+ avg_price=float(resp.executed_price) if resp.executed_price else 0.0,
364
+ error_reason=reason,
365
+ is_canceled=resp.status == OrderStatus.Canceled,
366
+ )
367
+
368
+ async def order(self, order_id: str) -> Order:
369
+ return await self.call_sync(lambda : self._order(order_id=order_id))
370
+
371
+ def _cancel_order(self, order_id: str):
372
+ with self._assets_bucket:
373
+ self._try_refresh()
374
+ self._trade_client.cancel_order(order_id=order_id)
375
+
376
+ async def cancel_order(self, order_id: str):
377
+ await self.call_sync(lambda: self._cancel_order(order_id=order_id))
378
+
379
+
380
+ __all__ = ['LongBridge', ]