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
httptrading/__init__.py
ADDED
File without changes
|
@@ -0,0 +1,151 @@
|
|
1
|
+
import asyncio
|
2
|
+
import importlib
|
3
|
+
from abc import ABC
|
4
|
+
from typing import Type, Callable, Any
|
5
|
+
from httptrading.model import *
|
6
|
+
|
7
|
+
|
8
|
+
class BaseBroker(ABC):
|
9
|
+
def __init__(self, broker_args: dict = None, instance_id: str = None, tokens: list[str] = None):
|
10
|
+
self.detect_package()
|
11
|
+
self.broker_args = broker_args or dict()
|
12
|
+
self.instance_id = instance_id or ''
|
13
|
+
self.tokens: set[str] = set(tokens or list())
|
14
|
+
assert '' not in self.tokens
|
15
|
+
|
16
|
+
@property
|
17
|
+
def broker_name(self):
|
18
|
+
return BrokerRegister.get_meta(type(self)).name
|
19
|
+
|
20
|
+
@property
|
21
|
+
def broker_display(self):
|
22
|
+
return BrokerRegister.get_meta(type(self)).display
|
23
|
+
|
24
|
+
def detect_package(self):
|
25
|
+
pkg_info = BrokerRegister.get_meta(type(self)).detect_package
|
26
|
+
if pkg_info is None:
|
27
|
+
return
|
28
|
+
try:
|
29
|
+
importlib.import_module(pkg_info.import_name)
|
30
|
+
except ImportError:
|
31
|
+
raise ImportError(f'需要安装依赖包: {pkg_info.pkg_name}')
|
32
|
+
|
33
|
+
async def start(self):
|
34
|
+
pass
|
35
|
+
|
36
|
+
async def shutdown(self):
|
37
|
+
pass
|
38
|
+
|
39
|
+
async def call_sync(self, f: Callable[[], Any]):
|
40
|
+
try:
|
41
|
+
r = await asyncio.get_running_loop().run_in_executor(None, f)
|
42
|
+
return r
|
43
|
+
except Exception as ex:
|
44
|
+
raise BrokerError(self, ex)
|
45
|
+
|
46
|
+
async def call_async(self, coro):
|
47
|
+
try:
|
48
|
+
r = await coro
|
49
|
+
return r
|
50
|
+
except Exception as ex:
|
51
|
+
raise BrokerError(self, ex)
|
52
|
+
|
53
|
+
async def place_order(
|
54
|
+
self,
|
55
|
+
contract: Contract,
|
56
|
+
order_type: OrderType,
|
57
|
+
time_in_force: TimeInForce,
|
58
|
+
lifecycle: Lifecycle,
|
59
|
+
direction: str,
|
60
|
+
qty: int,
|
61
|
+
price: float = None,
|
62
|
+
**kwargs
|
63
|
+
) -> str:
|
64
|
+
raise NotImplementedError
|
65
|
+
|
66
|
+
async def order(self, order_id: str) -> Order:
|
67
|
+
raise NotImplementedError
|
68
|
+
|
69
|
+
async def cancel_order(self, order_id: str):
|
70
|
+
raise NotImplementedError
|
71
|
+
|
72
|
+
async def positions(self) -> list[Position]:
|
73
|
+
raise NotImplementedError
|
74
|
+
|
75
|
+
async def cash(self) -> Cash:
|
76
|
+
raise NotImplementedError
|
77
|
+
|
78
|
+
async def ping(self) -> bool:
|
79
|
+
return True
|
80
|
+
|
81
|
+
async def quote(self, contract: Contract) -> Quote:
|
82
|
+
raise NotImplementedError
|
83
|
+
|
84
|
+
async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
|
85
|
+
raise NotImplementedError
|
86
|
+
|
87
|
+
|
88
|
+
class SecuritiesBroker(BaseBroker):
|
89
|
+
@classmethod
|
90
|
+
def contract_to_tz(cls, contract: Contract) -> str:
|
91
|
+
region = contract.region
|
92
|
+
match region:
|
93
|
+
case 'CN':
|
94
|
+
return 'Asia/Shanghai'
|
95
|
+
case 'HK':
|
96
|
+
return 'Asia/Hong_Kong'
|
97
|
+
case 'US':
|
98
|
+
return 'US/Eastern'
|
99
|
+
case _:
|
100
|
+
raise Exception(f'不能映射{contract}为已知时区')
|
101
|
+
|
102
|
+
@classmethod
|
103
|
+
def contract_to_currency(cls, contract: Contract) -> str | None:
|
104
|
+
region = contract.region
|
105
|
+
match region:
|
106
|
+
case 'CN':
|
107
|
+
return 'CNY'
|
108
|
+
case 'HK':
|
109
|
+
return 'HKD'
|
110
|
+
case 'US':
|
111
|
+
return 'USD'
|
112
|
+
case _:
|
113
|
+
raise Exception(f'不能映射{contract}为已知币种')
|
114
|
+
|
115
|
+
|
116
|
+
class BrokerRegister:
|
117
|
+
_D: dict[str, Type[BaseBroker]] = dict()
|
118
|
+
_META: dict[str, BrokerMeta] = dict()
|
119
|
+
|
120
|
+
@classmethod
|
121
|
+
def register(cls, broker_class: Type[BaseBroker], name: str, display: str, detect_pkg: DetectPkg = None):
|
122
|
+
type_name = broker_class.__name__
|
123
|
+
if type_name in cls._D:
|
124
|
+
raise ValueError(f"Duplicate broker class name '{type_name}'")
|
125
|
+
cls._D[type_name] = broker_class
|
126
|
+
cls._META[type_name] = BrokerMeta(name=name, display=display, detect_package=detect_pkg)
|
127
|
+
|
128
|
+
@classmethod
|
129
|
+
def get_meta(cls, broker_class: Type[BaseBroker]) -> BrokerMeta:
|
130
|
+
return BrokerRegister._META.get(broker_class.__name__)
|
131
|
+
|
132
|
+
|
133
|
+
def broker_register(name: str, display: str, detect_pkg: DetectPkg = None):
|
134
|
+
def decorator(cls: Type[BaseBroker]):
|
135
|
+
BrokerRegister.register(cls, name, display, detect_pkg)
|
136
|
+
return cls
|
137
|
+
return decorator
|
138
|
+
|
139
|
+
|
140
|
+
class BrokerError(ValueError):
|
141
|
+
def __init__(self, broker: BaseBroker, ex, *args, **kwargs):
|
142
|
+
self.broker = broker
|
143
|
+
super().__init__(f'[{broker.instance_id}]<{broker.broker_name} {broker.broker_display}>异常: {ex}')
|
144
|
+
|
145
|
+
|
146
|
+
__all__ = [
|
147
|
+
'BaseBroker',
|
148
|
+
'SecuritiesBroker',
|
149
|
+
'broker_register',
|
150
|
+
'BrokerError',
|
151
|
+
]
|
@@ -0,0 +1,418 @@
|
|
1
|
+
"""
|
2
|
+
接入富途证券的API文档
|
3
|
+
https://openapi.futunn.com/futu-api-doc/
|
4
|
+
"""
|
5
|
+
import re
|
6
|
+
from datetime import datetime
|
7
|
+
from httptrading.tool.leaky_bucket import *
|
8
|
+
from httptrading.broker.base import *
|
9
|
+
from httptrading.model import *
|
10
|
+
from httptrading.tool.time import *
|
11
|
+
|
12
|
+
|
13
|
+
@broker_register(name='futu', display='富途证券', detect_pkg=DetectPkg('futu-api', 'futu'))
|
14
|
+
class Futu(SecuritiesBroker):
|
15
|
+
def __init__(self, broker_args: dict = None, instance_id: str = None, tokens: list[str] = None):
|
16
|
+
super().__init__(broker_args, instance_id, tokens)
|
17
|
+
self._unlock_pin = ''
|
18
|
+
self._trd_env = 'REAL'
|
19
|
+
self._quote_client = None
|
20
|
+
self._trade_client = None
|
21
|
+
self._market_status_bucket = LeakyBucket(10)
|
22
|
+
self._snapshot_bucket = LeakyBucket(120)
|
23
|
+
self._assets_bucket = LeakyBucket(20)
|
24
|
+
self._position_bucket = LeakyBucket(20)
|
25
|
+
self._unlock_bucket = LeakyBucket(20)
|
26
|
+
self._place_order_bucket = LeakyBucket(30)
|
27
|
+
self._cancel_order_bucket = LeakyBucket(30)
|
28
|
+
self._refresh_order_bucket = LeakyBucket(20)
|
29
|
+
self._on_init()
|
30
|
+
|
31
|
+
def _on_init(self):
|
32
|
+
from futu import SysConfig, OpenQuoteContext, OpenSecTradeContext, SecurityFirm, TrdMarket, TrdEnv
|
33
|
+
config_dict = self.broker_args
|
34
|
+
self._trd_env: str = config_dict.get('trade_env', TrdEnv.REAL) or TrdEnv.REAL
|
35
|
+
pk_path = config_dict.get('pk_path', '')
|
36
|
+
self._unlock_pin = config_dict.get('unlock_pin', '')
|
37
|
+
if pk_path:
|
38
|
+
SysConfig.enable_proto_encrypt(is_encrypt=True)
|
39
|
+
SysConfig.set_init_rsa_file(pk_path)
|
40
|
+
if self._trade_client is None:
|
41
|
+
SysConfig.set_all_thread_daemon(True)
|
42
|
+
host = config_dict.get('host', '127.0.0.1')
|
43
|
+
port = config_dict.get('port', 11111)
|
44
|
+
trade_ctx = OpenSecTradeContext(
|
45
|
+
filter_trdmarket=TrdMarket.US,
|
46
|
+
host=host,
|
47
|
+
port=port,
|
48
|
+
security_firm=SecurityFirm.FUTUSECURITIES,
|
49
|
+
)
|
50
|
+
trade_ctx.set_sync_query_connect_timeout(6.0)
|
51
|
+
self._trade_client = trade_ctx
|
52
|
+
if self._quote_client is None:
|
53
|
+
SysConfig.set_all_thread_daemon(True)
|
54
|
+
host = config_dict.get('host', '127.0.0.1')
|
55
|
+
port = config_dict.get('port', 11111)
|
56
|
+
quote_ctx = OpenQuoteContext(host=host, port=port)
|
57
|
+
quote_ctx.set_sync_query_connect_timeout(6.0)
|
58
|
+
self._quote_client = quote_ctx
|
59
|
+
|
60
|
+
@classmethod
|
61
|
+
def code_to_contract(cls, code) -> Contract | None:
|
62
|
+
region = ''
|
63
|
+
ticker = ''
|
64
|
+
if m := re.match(r'^US\.(\S+)$', code):
|
65
|
+
region = 'US'
|
66
|
+
ticker = m.groups()[0]
|
67
|
+
if m := re.match(r'^HK\.(\d{5})$', code):
|
68
|
+
region = 'HK'
|
69
|
+
ticker = m.groups()[0]
|
70
|
+
if m := re.match(r'^SH\.(\d{6})$', code):
|
71
|
+
region = 'CN'
|
72
|
+
ticker = m.groups()[0]
|
73
|
+
if m := re.match(r'^SZ\.(\d{6})$', code):
|
74
|
+
region = 'CN'
|
75
|
+
ticker = m.groups()[0]
|
76
|
+
if not region or not ticker:
|
77
|
+
return None
|
78
|
+
return Contract(
|
79
|
+
trade_type=TradeType.Securities,
|
80
|
+
ticker=ticker,
|
81
|
+
region=region,
|
82
|
+
)
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def contract_to_code(cls, contract: Contract) -> str | None:
|
86
|
+
if contract.trade_type != TradeType.Securities:
|
87
|
+
return None
|
88
|
+
region, ticker = contract.region, contract.ticker
|
89
|
+
code = None
|
90
|
+
if region == 'CN' and re.match(r'^[56]\d{5}$', ticker):
|
91
|
+
code = f'SH.{ticker}'
|
92
|
+
elif region == 'CN' and re.match(r'^[013]\d{5}$', ticker):
|
93
|
+
code = f'SZ.{ticker}'
|
94
|
+
elif region == 'HK' and re.match(r'^\d{5}$', ticker):
|
95
|
+
code = f'HK.{ticker}'
|
96
|
+
elif region == 'US' and re.match(r'^\w+$', ticker):
|
97
|
+
code = f'US.{ticker}'
|
98
|
+
return code
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
def df_to_dict(cls, df) -> dict:
|
102
|
+
return df.to_dict(orient='records')
|
103
|
+
|
104
|
+
def _positions(self):
|
105
|
+
result = list()
|
106
|
+
from futu import RET_OK
|
107
|
+
with self._position_bucket:
|
108
|
+
resp, data = self._trade_client.position_list_query(
|
109
|
+
trd_env=self._trd_env,
|
110
|
+
refresh_cache=True,
|
111
|
+
)
|
112
|
+
if resp != RET_OK:
|
113
|
+
raise Exception(f'返回失败: {resp}')
|
114
|
+
positions = self.df_to_dict(data)
|
115
|
+
for d in positions:
|
116
|
+
code = d.get('code')
|
117
|
+
currency = d.get('currency')
|
118
|
+
if not code or not currency:
|
119
|
+
continue
|
120
|
+
contract = self.code_to_contract(code)
|
121
|
+
if not contract:
|
122
|
+
continue
|
123
|
+
qty = int(d['qty'])
|
124
|
+
position = Position(
|
125
|
+
broker=self.broker_name,
|
126
|
+
broker_display=self.broker_display,
|
127
|
+
contract=contract,
|
128
|
+
unit=Unit.Share,
|
129
|
+
currency=currency,
|
130
|
+
qty=qty,
|
131
|
+
)
|
132
|
+
result.append(position)
|
133
|
+
return result
|
134
|
+
|
135
|
+
async def positions(self):
|
136
|
+
return await self.call_sync(lambda : self._positions())
|
137
|
+
|
138
|
+
async def ping(self) -> bool:
|
139
|
+
try:
|
140
|
+
client = self._trade_client
|
141
|
+
conn_id = client.get_sync_conn_id()
|
142
|
+
return bool(conn_id)
|
143
|
+
except Exception as e:
|
144
|
+
return False
|
145
|
+
|
146
|
+
def _cash(self) -> Cash:
|
147
|
+
from futu import RET_OK, Currency
|
148
|
+
with self._assets_bucket:
|
149
|
+
resp, data = self._trade_client.accinfo_query(
|
150
|
+
trd_env=self._trd_env,
|
151
|
+
refresh_cache=True,
|
152
|
+
currency=Currency.USD,
|
153
|
+
)
|
154
|
+
if resp != RET_OK:
|
155
|
+
raise Exception(f'可用资金信息获取失败: {data}')
|
156
|
+
assets = self.df_to_dict(data)
|
157
|
+
if len(assets) == 1:
|
158
|
+
cash = Cash(
|
159
|
+
currency='USD',
|
160
|
+
amount=assets[0]['cash'],
|
161
|
+
)
|
162
|
+
return cash
|
163
|
+
else:
|
164
|
+
raise Exception(f'可用资金信息获取不到记录')
|
165
|
+
|
166
|
+
async def cash(self) -> Cash:
|
167
|
+
return await self.call_sync(lambda : self._cash())
|
168
|
+
|
169
|
+
def _market_status(self) -> dict[str, dict[str, MarketStatus]]:
|
170
|
+
# 各个市场的状态定义见:
|
171
|
+
# https://openapi.futunn.com/futu-api-doc/qa/quote.html#2090
|
172
|
+
from futu import RET_OK
|
173
|
+
sec_result = dict()
|
174
|
+
region_map = {
|
175
|
+
'market_sh': 'CN',
|
176
|
+
'market_hk': 'HK',
|
177
|
+
'market_us': 'US',
|
178
|
+
}
|
179
|
+
status_map = {
|
180
|
+
'CLOSED': UnifiedStatus.CLOSED,
|
181
|
+
'PRE_MARKET_BEGIN': UnifiedStatus.PRE_HOURS,
|
182
|
+
'MORNING': UnifiedStatus.RTH,
|
183
|
+
'AFTERNOON': UnifiedStatus.RTH,
|
184
|
+
'AFTER_HOURS_BEGIN': UnifiedStatus.AFTER_HOURS,
|
185
|
+
'AFTER_HOURS_END': UnifiedStatus.CLOSED, # 根据文档, 盘后收盘时段跟夜盘时段重合
|
186
|
+
'OVERNIGHT': UnifiedStatus.OVERNIGHT,
|
187
|
+
# 这些映射是A股港股市场的映射
|
188
|
+
'REST': UnifiedStatus.REST,
|
189
|
+
'HK_CAS': UnifiedStatus.CLOSED,
|
190
|
+
}
|
191
|
+
client = self._quote_client
|
192
|
+
with self._market_status_bucket:
|
193
|
+
ret, data = client.get_global_state()
|
194
|
+
if ret != RET_OK:
|
195
|
+
raise Exception(f'市场状态接口调用失败: {data}')
|
196
|
+
for k, origin_status in data.items():
|
197
|
+
if k not in region_map:
|
198
|
+
continue
|
199
|
+
region = region_map[k]
|
200
|
+
unified_status = status_map.get(origin_status, UnifiedStatus.UNKNOWN)
|
201
|
+
sec_result[region] = MarketStatus(
|
202
|
+
region=region,
|
203
|
+
origin_status=origin_status,
|
204
|
+
unified_status=unified_status,
|
205
|
+
)
|
206
|
+
return {
|
207
|
+
TradeType.Securities.name.lower(): sec_result,
|
208
|
+
}
|
209
|
+
|
210
|
+
async def market_status(self) -> dict[str, dict[str, MarketStatus]]:
|
211
|
+
return await self.call_sync(lambda : self._market_status())
|
212
|
+
|
213
|
+
def _quote(self, contract: Contract):
|
214
|
+
from futu import RET_OK
|
215
|
+
tz = self.contract_to_tz(contract)
|
216
|
+
code = self.contract_to_code(contract)
|
217
|
+
currency = self.contract_to_currency(contract)
|
218
|
+
with self._snapshot_bucket:
|
219
|
+
ret, data = self._quote_client.get_market_snapshot([code, ])
|
220
|
+
if ret != RET_OK:
|
221
|
+
raise ValueError(f'快照接口调用失败: {data}')
|
222
|
+
table = self.df_to_dict(data)
|
223
|
+
if len(table) != 1:
|
224
|
+
raise ValueError(f'快照接口调用无数据: {data}')
|
225
|
+
d = table[0]
|
226
|
+
"""
|
227
|
+
格式:yyyy-MM-dd HH:mm:ss
|
228
|
+
港股和 A 股市场默认是北京时间,美股市场默认是美东时间
|
229
|
+
"""
|
230
|
+
update_time: str = d['update_time']
|
231
|
+
update_dt = datetime.strptime(update_time, '%Y-%m-%d %H:%M:%S')
|
232
|
+
update_dt = TimeTools.from_params(
|
233
|
+
year=update_dt.year,
|
234
|
+
month=update_dt.month,
|
235
|
+
day=update_dt.day,
|
236
|
+
hour=update_dt.hour,
|
237
|
+
minute=update_dt.minute,
|
238
|
+
second=update_dt.second,
|
239
|
+
tz=tz,
|
240
|
+
)
|
241
|
+
is_tradable = d['sec_status'] == 'NORMAL'
|
242
|
+
return Quote(
|
243
|
+
contract=contract,
|
244
|
+
currency=currency,
|
245
|
+
is_tradable=is_tradable,
|
246
|
+
latest=d['last_price'],
|
247
|
+
pre_close=d['prev_close_price'],
|
248
|
+
open_price=d['open_price'],
|
249
|
+
high_price=d['high_price'],
|
250
|
+
low_price=d['low_price'],
|
251
|
+
time=update_dt,
|
252
|
+
)
|
253
|
+
|
254
|
+
async def quote(self, contract: Contract):
|
255
|
+
return await self.call_sync(lambda : self._quote(contract))
|
256
|
+
|
257
|
+
def _try_unlock(self):
|
258
|
+
from futu import RET_OK, TrdEnv
|
259
|
+
if self._trd_env != TrdEnv.REAL:
|
260
|
+
return
|
261
|
+
if not self._unlock_pin:
|
262
|
+
return
|
263
|
+
with self._unlock_bucket:
|
264
|
+
ret, data = self._trade_client.unlock_trade(password_md5=self._unlock_pin)
|
265
|
+
if ret != RET_OK:
|
266
|
+
raise Exception(f'解锁交易失败: {data}')
|
267
|
+
|
268
|
+
def _place_order(
|
269
|
+
self,
|
270
|
+
contract: Contract,
|
271
|
+
order_type: OrderType,
|
272
|
+
time_in_force: TimeInForce,
|
273
|
+
lifecycle: Lifecycle,
|
274
|
+
direction: str,
|
275
|
+
qty: int,
|
276
|
+
price: float = None,
|
277
|
+
**kwargs
|
278
|
+
) -> str:
|
279
|
+
from futu import RET_OK, TrdSide, OrderType as FutuOrderType, TimeInForce as FutuTimeInForce, Session
|
280
|
+
if contract.trade_type != TradeType.Securities:
|
281
|
+
raise Exception(f'不支持的下单品种: {contract.trade_type}')
|
282
|
+
if contract.region == 'US' and order_type == OrderType.Market and lifecycle != Lifecycle.RTH:
|
283
|
+
raise Exception(f'交易时段不支持市价单')
|
284
|
+
code = self.contract_to_code(contract)
|
285
|
+
assert qty > 0
|
286
|
+
assert price > 0
|
287
|
+
|
288
|
+
def _map_trade_side():
|
289
|
+
match direction:
|
290
|
+
case 'BUY':
|
291
|
+
return TrdSide.BUY
|
292
|
+
case 'SELL':
|
293
|
+
return TrdSide.SELL
|
294
|
+
case _:
|
295
|
+
raise Exception(f'不支持的买卖方向: {direction}')
|
296
|
+
|
297
|
+
def _map_order_type():
|
298
|
+
match order_type:
|
299
|
+
case OrderType.Market:
|
300
|
+
return FutuOrderType.MARKET
|
301
|
+
case OrderType.Limit:
|
302
|
+
return FutuOrderType.NORMAL
|
303
|
+
case _:
|
304
|
+
raise Exception(f'不支持的订单类型: {order_type}')
|
305
|
+
|
306
|
+
def _map_time_in_force():
|
307
|
+
match time_in_force:
|
308
|
+
case TimeInForce.DAY:
|
309
|
+
return FutuTimeInForce.DAY
|
310
|
+
case TimeInForce.GTC:
|
311
|
+
return FutuTimeInForce.GTC
|
312
|
+
case _:
|
313
|
+
raise Exception(f'不支持的订单有效期: {time_in_force}')
|
314
|
+
|
315
|
+
def _map_lifecycle():
|
316
|
+
match lifecycle:
|
317
|
+
case Lifecycle.RTH:
|
318
|
+
return Session.RTH
|
319
|
+
case Lifecycle.ETH:
|
320
|
+
return Session.ETH
|
321
|
+
case Lifecycle.OVERNIGHT:
|
322
|
+
return Session.OVERNIGHT
|
323
|
+
case _:
|
324
|
+
raise Exception(f'不支持的交易时段: {lifecycle}')
|
325
|
+
|
326
|
+
self._try_unlock()
|
327
|
+
with self._place_order_bucket:
|
328
|
+
ret, data = self._trade_client.place_order(
|
329
|
+
code=code,
|
330
|
+
price=price or 10.0, # 富途必须要填充这个字段
|
331
|
+
qty=qty,
|
332
|
+
trd_side=_map_trade_side(),
|
333
|
+
order_type=_map_order_type(),
|
334
|
+
time_in_force=_map_time_in_force(),
|
335
|
+
trd_env=self._trd_env,
|
336
|
+
session=_map_lifecycle(),
|
337
|
+
)
|
338
|
+
if ret != RET_OK:
|
339
|
+
raise Exception(f'下单失败: {data}')
|
340
|
+
orders = self.df_to_dict(data)
|
341
|
+
assert len(orders) == 1
|
342
|
+
order_id = orders[0]['order_id']
|
343
|
+
assert order_id
|
344
|
+
return order_id
|
345
|
+
|
346
|
+
async def place_order(
|
347
|
+
self,
|
348
|
+
contract: Contract,
|
349
|
+
order_type: OrderType,
|
350
|
+
time_in_force: TimeInForce,
|
351
|
+
lifecycle: Lifecycle,
|
352
|
+
direction: str,
|
353
|
+
qty: int,
|
354
|
+
price: float = None,
|
355
|
+
**kwargs
|
356
|
+
) -> str:
|
357
|
+
return await self.call_sync(lambda : self._place_order(
|
358
|
+
contract=contract,
|
359
|
+
order_type=order_type,
|
360
|
+
time_in_force=time_in_force,
|
361
|
+
lifecycle=lifecycle,
|
362
|
+
direction=direction,
|
363
|
+
qty=qty,
|
364
|
+
price=price,
|
365
|
+
**kwargs
|
366
|
+
))
|
367
|
+
|
368
|
+
def _order(self, order_id: str) -> Order:
|
369
|
+
from futu import RET_OK, OrderStatus
|
370
|
+
error_set = {OrderStatus.FAILED, OrderStatus.DISABLED, OrderStatus.DELETED, }
|
371
|
+
cancel_set = {OrderStatus.CANCELLED_PART, OrderStatus.CANCELLED_ALL, }
|
372
|
+
with self._refresh_order_bucket:
|
373
|
+
ret, data = self._trade_client.order_list_query(
|
374
|
+
order_id=order_id,
|
375
|
+
refresh_cache=True,
|
376
|
+
trd_env=self._trd_env,
|
377
|
+
)
|
378
|
+
if ret != RET_OK:
|
379
|
+
raise Exception(f'调用获取订单失败, 订单: {order_id}')
|
380
|
+
orders = self.df_to_dict(data)
|
381
|
+
if len(orders) != 1:
|
382
|
+
raise Exception(f'找不到订单(未完成), 订单: {order_id}')
|
383
|
+
futu_order = orders[0]
|
384
|
+
reason = ''
|
385
|
+
if futu_order['order_status'] in error_set:
|
386
|
+
reason = futu_order['order_status']
|
387
|
+
return Order(
|
388
|
+
order_id=order_id,
|
389
|
+
currency=futu_order['currency'],
|
390
|
+
qty=int(futu_order['qty']),
|
391
|
+
filled_qty=int(futu_order['dealt_qty']),
|
392
|
+
avg_price=futu_order['dealt_avg_price'] or 0.0,
|
393
|
+
error_reason=reason,
|
394
|
+
is_canceled=futu_order['order_status'] in cancel_set,
|
395
|
+
)
|
396
|
+
|
397
|
+
async def order(self, order_id: str) -> Order:
|
398
|
+
return await self.call_sync(lambda : self._order(order_id=order_id))
|
399
|
+
|
400
|
+
def _cancel_order(self, order_id: str):
|
401
|
+
from futu import RET_OK, ModifyOrderOp
|
402
|
+
self._try_unlock()
|
403
|
+
with self._cancel_order_bucket:
|
404
|
+
ret, data = self._trade_client.modify_order(
|
405
|
+
modify_order_op=ModifyOrderOp.CANCEL,
|
406
|
+
order_id=order_id,
|
407
|
+
qty=100,
|
408
|
+
price=10.0,
|
409
|
+
trd_env=self._trd_env,
|
410
|
+
)
|
411
|
+
if ret != RET_OK:
|
412
|
+
raise Exception(f'撤单失败, 订单: {order_id}, 原因: {data}')
|
413
|
+
|
414
|
+
async def cancel_order(self, order_id: str):
|
415
|
+
await self.call_sync(lambda: self._cancel_order(order_id=order_id))
|
416
|
+
|
417
|
+
|
418
|
+
__all__ = ['Futu', ]
|