siglab-py 0.1.17__py3-none-any.whl → 0.1.18__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.
Potentially problematic release.
This version of siglab-py might be problematic. Click here for more details.
- siglab_py/exchanges/any_exchange.py +7 -1
- siglab_py/exchanges/futubull.py +468 -0
- siglab_py/ordergateway/gateway.py +5 -4
- siglab_py/tests/integration/market_data_util_tests.py +38 -1
- siglab_py/util/market_data_util.py +23 -9
- {siglab_py-0.1.17.dist-info → siglab_py-0.1.18.dist-info}/METADATA +1 -1
- {siglab_py-0.1.17.dist-info → siglab_py-0.1.18.dist-info}/RECORD +9 -8
- {siglab_py-0.1.17.dist-info → siglab_py-0.1.18.dist-info}/WHEEL +0 -0
- {siglab_py-0.1.17.dist-info → siglab_py-0.1.18.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,13 @@ b. Override ccxt basic functions
|
|
|
15
15
|
- order amount rounding: amount_to_precision
|
|
16
16
|
- order price rounding: price_to_precision
|
|
17
17
|
... etc
|
|
18
|
+
|
|
19
|
+
Besides above, it's very common below are required from algo's
|
|
20
|
+
- fetch_time
|
|
21
|
+
- fetch_order_book
|
|
22
|
+
- fetch_positions
|
|
23
|
+
|
|
18
24
|
'''
|
|
19
|
-
class AnyExchange(
|
|
25
|
+
class AnyExchange(Exchange):
|
|
20
26
|
def __init__(self, *args: object) -> None:
|
|
21
27
|
super().__init__(*args)
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
'''
|
|
2
|
+
https://github.com/FutunnOpen/py-futu-api/blob/master/README.md
|
|
3
|
+
https://www.futuhk.com/en/support/categories/909?global_content=%7B%22promote_id%22%3A13765%2C%22sub_promote_id%22%3A10%7D
|
|
4
|
+
|
|
5
|
+
Fees: https://www.futuhk.com/en/commissionnew#crypto
|
|
6
|
+
|
|
7
|
+
Subscribe L2 data: https://openapi.futunn.com/futu-api-doc/en/intro/authority.html#5331
|
|
8
|
+
|
|
9
|
+
Investor Protection: https://www.futuhk.com/en
|
|
10
|
+
|
|
11
|
+
Margin Trading: https://www.futunn.com/en/learn/detail-what-is-margin-trading-62010-220679271
|
|
12
|
+
|
|
13
|
+
Download Futu OpenD
|
|
14
|
+
https://www.futuhk.com/en/support/topic1_464?global_content=%7B%22promote_id%22%3A13765%2C%22sub_promote_id%22%3A10%7D
|
|
15
|
+
|
|
16
|
+
If you run the installer version "Futu_OpenD-GUI_9.0.5008_Windows.exe", it'd be installed under:
|
|
17
|
+
C:\\Users\\xxx\\AppData\\Roaming\\Futu_OpenD\\Futu_OpenD.exe
|
|
18
|
+
|
|
19
|
+
Architecture: https://openapi.futunn.com/futu-api-doc/en/intro/intro.html
|
|
20
|
+
|
|
21
|
+
python -mpip install futu-api
|
|
22
|
+
|
|
23
|
+
Whatsapp support: Moomoo OpenAPI 1群
|
|
24
|
+
|
|
25
|
+
API
|
|
26
|
+
Fetches:
|
|
27
|
+
market status https://openapi.futunn.com/futu-api-doc/en/quote/get-global-state.html
|
|
28
|
+
stock basic info https://openapi.futunn.com/futu-api-doc/en/quote/get-static-info.html
|
|
29
|
+
historical candles https://openapi.futunn.com/futu-api-doc/en/quote/request-history-kline.html
|
|
30
|
+
realtime candles https://openapi.futunn.com/futu-api-doc/en/quote/get-kl.html
|
|
31
|
+
real time quote https://openapi.futunn.com/futu-api-doc/en/quote/get-stock-quote.html
|
|
32
|
+
open orders https://openapi.futunn.com/futu-api-doc/en/trade/get-order-list.html
|
|
33
|
+
historical orders https://openapi.futunn.com/futu-api-doc/en/trade/get-history-order-list.html
|
|
34
|
+
positions https://openapi.futunn.com/futu-api-doc/en/trade/get-position-list.html
|
|
35
|
+
balances https://openapi.futunn.com/futu-api-doc/en/trade/get-funds.html
|
|
36
|
+
|
|
37
|
+
Trading:
|
|
38
|
+
create order https://openapi.futunn.com/futu-api-doc/en/trade/place-order.html
|
|
39
|
+
amend order https://openapi.futunn.com/futu-api-doc/en/trade/modify-order.html
|
|
40
|
+
|
|
41
|
+
'''
|
|
42
|
+
from datetime import datetime, timedelta
|
|
43
|
+
from typing import List, Dict, Union, Any, NoReturn
|
|
44
|
+
import pandas as pd
|
|
45
|
+
|
|
46
|
+
import futu as ft
|
|
47
|
+
from futu import *
|
|
48
|
+
|
|
49
|
+
from exchanges.any_exchange import AnyExchange
|
|
50
|
+
|
|
51
|
+
class Futubull(AnyExchange):
|
|
52
|
+
def __init__(self, *args: Dict[str, Any]) -> None:
|
|
53
|
+
super().__init__(*args)
|
|
54
|
+
|
|
55
|
+
self.name = 'futubull'
|
|
56
|
+
|
|
57
|
+
params : Dict[str, Any] = args[0] # Type "object" is not assignable to declared type "Dict[Unknown, Unknown]"
|
|
58
|
+
self.host_addr = params['daemon']['host']
|
|
59
|
+
self.port = params['daemon']['port']
|
|
60
|
+
self.market = params['market']
|
|
61
|
+
self.security_type = params['security_type']
|
|
62
|
+
self.quote_ctx = OpenQuoteContext(host=self.host_addr, port=self.port)
|
|
63
|
+
self.trd_ctx = OpenSecTradeContext(
|
|
64
|
+
filter_trdmarket=params['trdmarket'],
|
|
65
|
+
host=self.host_addr, port=self.port,
|
|
66
|
+
security_firm=params['security_firm']
|
|
67
|
+
)
|
|
68
|
+
self.markets : Dict = {}
|
|
69
|
+
|
|
70
|
+
def load_markets(self, reload=False, params={}): # type: ignore
|
|
71
|
+
'''
|
|
72
|
+
Mimic CCXT load_markets https://github.com/ccxt/ccxt/blob/master/python/ccxt/base/exchange.py
|
|
73
|
+
|
|
74
|
+
Examplem,
|
|
75
|
+
{
|
|
76
|
+
... more pairs ...
|
|
77
|
+
'ETH/USDC:USDC': {
|
|
78
|
+
'id': 'ETH-USDC-SWAP',
|
|
79
|
+
'lowercaseId': None,
|
|
80
|
+
'symbol': 'ETH/USDC:USDC',
|
|
81
|
+
'base': 'ETH',
|
|
82
|
+
'quote': 'USDC',
|
|
83
|
+
'settle': 'USDC',
|
|
84
|
+
'baseId': 'ETH',
|
|
85
|
+
'quoteId': 'USDC',
|
|
86
|
+
'settleId': 'USDC',
|
|
87
|
+
'type': 'swap',
|
|
88
|
+
'spot': False,
|
|
89
|
+
'margin': False,
|
|
90
|
+
'swap': True,
|
|
91
|
+
'future': False,
|
|
92
|
+
'option': False,
|
|
93
|
+
'index': None,
|
|
94
|
+
'active': True,
|
|
95
|
+
'contract': True,
|
|
96
|
+
'linear': True,
|
|
97
|
+
'inverse': False,
|
|
98
|
+
'subType': 'linear',
|
|
99
|
+
'taker': 0.0005,
|
|
100
|
+
'maker': 0.0002,
|
|
101
|
+
'contractSize': 0.001,
|
|
102
|
+
'expiry': None,
|
|
103
|
+
'expiryDatetime': None,
|
|
104
|
+
'strike': None,
|
|
105
|
+
'optionType': None,
|
|
106
|
+
'precision': {'amount': 1.0, 'price': 0.01, 'cost': None, 'base': None, 'quote': None},
|
|
107
|
+
'limits': {'leverage': {'min': 1.0, 'max': 50.0}, 'amount': {'min': 1.0, 'max': None}, 'price': {'min': None, 'max': None}, 'cost': {'min': None, 'max': None}},
|
|
108
|
+
'marginModes': {'cross': None, 'isolated': None},
|
|
109
|
+
'created': 1666076197702,
|
|
110
|
+
'info' : {
|
|
111
|
+
... raw exchange response here ...
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
... more pairs ...
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
gateway.py will call:
|
|
118
|
+
1. multiplier = market['contractSize'] (Note this is contract size for futures, not same as 'Lot Size')
|
|
119
|
+
2. price_to_precision -> market['precision']['price']
|
|
120
|
+
3. amount_to_precision -> market['precision']['amount'] <-- This is 'Lot Size'
|
|
121
|
+
'''
|
|
122
|
+
|
|
123
|
+
ret, data = self.quote_ctx.get_stock_basicinfo(self.market, self.security_type)
|
|
124
|
+
if ret == RET_OK:
|
|
125
|
+
for index, row in data.iterrows(): # type: ignore
|
|
126
|
+
symbol = row['code']
|
|
127
|
+
|
|
128
|
+
name = row['name']
|
|
129
|
+
stock_type = row['stock_type']
|
|
130
|
+
stock_child_type = row['stock_child_type']
|
|
131
|
+
stock_owner = row['stock_owner']
|
|
132
|
+
option_type = row['option_type']
|
|
133
|
+
strike_time = row['strike_time']
|
|
134
|
+
strike_price = row['strike_price']
|
|
135
|
+
suspension = row['suspension']
|
|
136
|
+
stock_id = row['stock_id']
|
|
137
|
+
listing_date = row['listing_date']
|
|
138
|
+
delisting = bool(row['delisting'])
|
|
139
|
+
main_contract = bool(row['main_contract'])
|
|
140
|
+
last_trade_time = row['last_trade_time']
|
|
141
|
+
exchange_type = row['exchange_type']
|
|
142
|
+
lot_size = row['lot_size']
|
|
143
|
+
|
|
144
|
+
# No additional information from 'get_stock_basicinfo'
|
|
145
|
+
# ret, detail = self.quote_ctx.get_stock_basicinfo(self.market, self.security_type, [ symbol ])
|
|
146
|
+
|
|
147
|
+
info : Dict = {
|
|
148
|
+
'symbol' : symbol,
|
|
149
|
+
'name' : name,
|
|
150
|
+
'stock_type' : stock_type,
|
|
151
|
+
'stock_child_type' : stock_child_type,
|
|
152
|
+
'stock_owner' : stock_owner,
|
|
153
|
+
'option_type' : option_type,
|
|
154
|
+
'strike_time' : strike_time,
|
|
155
|
+
'strike_price' : strike_price,
|
|
156
|
+
'suspension' : suspension,
|
|
157
|
+
'stock_id' : stock_id,
|
|
158
|
+
'listing_date' : listing_date,
|
|
159
|
+
'delisting' : delisting,
|
|
160
|
+
'main_contract' : main_contract,
|
|
161
|
+
'last_trade_time' : last_trade_time,
|
|
162
|
+
'exchange_type' : exchange_type,
|
|
163
|
+
'lot_size' : lot_size
|
|
164
|
+
}
|
|
165
|
+
self.markets[symbol] = {
|
|
166
|
+
'symbol' : symbol,
|
|
167
|
+
'id' : symbol,
|
|
168
|
+
|
|
169
|
+
'base': None,
|
|
170
|
+
'quote': None,
|
|
171
|
+
'settle': None,
|
|
172
|
+
'baseId': None,
|
|
173
|
+
'quoteId': None,
|
|
174
|
+
'settleId': None,
|
|
175
|
+
|
|
176
|
+
'type': self.security_type,
|
|
177
|
+
'spot': False,
|
|
178
|
+
'margin': False,
|
|
179
|
+
'swap': False,
|
|
180
|
+
'future': False,
|
|
181
|
+
'option': False,
|
|
182
|
+
'index': None,
|
|
183
|
+
'active': not delisting,
|
|
184
|
+
'contract': False,
|
|
185
|
+
'linear': False,
|
|
186
|
+
'inverse': False,
|
|
187
|
+
'subType': False,
|
|
188
|
+
'taker': 0,
|
|
189
|
+
'maker': 0,
|
|
190
|
+
'contractSize': 0,
|
|
191
|
+
'expiry': strike_time,
|
|
192
|
+
'expiryDatetime': strike_time,
|
|
193
|
+
'strike': strike_price,
|
|
194
|
+
'optionType': option_type,
|
|
195
|
+
|
|
196
|
+
'precision': {
|
|
197
|
+
'amount': lot_size,
|
|
198
|
+
'price': 0.01,
|
|
199
|
+
'cost': None,
|
|
200
|
+
'base': None,
|
|
201
|
+
'quote': None
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
'limits': {
|
|
205
|
+
'leverage': {'min': 1, 'max': 5},
|
|
206
|
+
'amount': {'min': 1.0, 'max': None},
|
|
207
|
+
'price': {'min': None, 'max': None},
|
|
208
|
+
'cost': {'min': None, 'max': None}
|
|
209
|
+
},
|
|
210
|
+
'marginModes': {'cross': None, 'isolated': None},
|
|
211
|
+
|
|
212
|
+
'info' : info
|
|
213
|
+
}
|
|
214
|
+
return self.markets
|
|
215
|
+
|
|
216
|
+
'''
|
|
217
|
+
With CCXT, 'params' is used to parse exchange specific parameters. Not actually used with Futu.
|
|
218
|
+
|
|
219
|
+
Get Historical Candlesticks max_count: In API doc it advises<1000, but when I tested, it's 246 (1 year).
|
|
220
|
+
To fetch more than this, use market_data_util.fetch_candles (Sliding window implemented).
|
|
221
|
+
Under the hood, it'd use implementation below.
|
|
222
|
+
|
|
223
|
+
CCXT fetch_ohlcv response format is:
|
|
224
|
+
[
|
|
225
|
+
[1704038400000, 42469.9, 42723.1, 42442.3, 42609.8, 3624.59]
|
|
226
|
+
[1704042000000, 42609.8, 42672.4, 42473.9, 42630.6, 2829.69]
|
|
227
|
+
[1704045600000, 42630.6, 42742.9, 42542.9, 42673.8, 2097.12]
|
|
228
|
+
[1704049200000, 42673.8, 42705.0, 42593.7, 42627.7, 2055.54]
|
|
229
|
+
[1704052800000, 42627.6, 42693.0, 42511.1, 42565.5, 2061.97]
|
|
230
|
+
... more candles ...
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
Fields are: timestamp (in ms), open, high, low, close, volume.
|
|
234
|
+
'''
|
|
235
|
+
def fetch_ohlcv(self, symbol: str, timeframe : str ='1h', since: Union[int, None] = None, limit: int = 100, params={}) -> Union[List[List[Union[int, float]]], None]: # type: ignore
|
|
236
|
+
if not symbol or not since:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
ktype=KLType.K_DAY
|
|
240
|
+
if timeframe=="1m":
|
|
241
|
+
ktype=KLType.K_1M
|
|
242
|
+
elif timeframe=="1h":
|
|
243
|
+
ktype=KLType.K_60M
|
|
244
|
+
elif timeframe=="1h":
|
|
245
|
+
ktype=KLType.K_DAY
|
|
246
|
+
|
|
247
|
+
candles = []
|
|
248
|
+
dt_start = datetime.fromtimestamp(int(since/1000))
|
|
249
|
+
s_start : str = dt_start.strftime("%Y-%m-%d")
|
|
250
|
+
ret, data, page_req_key = self.quote_ctx.request_history_kline(code=symbol, start=s_start, ktype=ktype, max_count=limit)
|
|
251
|
+
if ret == RET_OK:
|
|
252
|
+
for index, row in data.iterrows(): # type: ignore
|
|
253
|
+
|
|
254
|
+
'''
|
|
255
|
+
From doc:
|
|
256
|
+
Format: yyyy-MM-dd HH:mm:ss
|
|
257
|
+
The default of HK stock market and A-share market is Beijing time,
|
|
258
|
+
while that of US stock market is US Eastern time.
|
|
259
|
+
'''
|
|
260
|
+
time_key = row['time_key']
|
|
261
|
+
dt = datetime.strptime(time_key, "%Y-%m-%d %H:%M:%S")
|
|
262
|
+
timestamp_ms = int(dt.timestamp() * 1000)
|
|
263
|
+
open = row['open']
|
|
264
|
+
high = row['high']
|
|
265
|
+
low = row['low']
|
|
266
|
+
close = row['close']
|
|
267
|
+
volume = row['volume']
|
|
268
|
+
candles.append(
|
|
269
|
+
[ timestamp_ms, open, high, low, close, volume ]
|
|
270
|
+
)
|
|
271
|
+
return candles
|
|
272
|
+
|
|
273
|
+
def fetch_candles(
|
|
274
|
+
self,
|
|
275
|
+
start_ts,
|
|
276
|
+
end_ts,
|
|
277
|
+
symbols,
|
|
278
|
+
candle_size
|
|
279
|
+
) -> Dict[str, Union[pd.DataFrame, None]]:
|
|
280
|
+
exchange_candles : Dict[str, Union[pd.DataFrame, None]] = {}
|
|
281
|
+
|
|
282
|
+
for symbol in symbols:
|
|
283
|
+
pd_candles = self._fetch_candles(symbol=symbol, start_ts=start_ts, end_ts=end_ts, candle_size=candle_size)
|
|
284
|
+
pd_candles['exchange'] = self.name
|
|
285
|
+
exchange_candles[symbol] = pd_candles
|
|
286
|
+
|
|
287
|
+
return exchange_candles
|
|
288
|
+
|
|
289
|
+
def _fetch_candles(
|
|
290
|
+
self,
|
|
291
|
+
symbol : str,
|
|
292
|
+
start_ts : int,
|
|
293
|
+
end_ts : int,
|
|
294
|
+
candle_size : str = '1d',
|
|
295
|
+
num_candles_limit : int = 100
|
|
296
|
+
):
|
|
297
|
+
def _fetch_ohlcv(symbol, timeframe, since, limit, params) -> Union[List[List[Union[int, float]]], None]:
|
|
298
|
+
one_timeframe = f"1{timeframe[-1]}"
|
|
299
|
+
candles = self.fetch_ohlcv(symbol=symbol, timeframe=one_timeframe, since=since, limit=limit, params=params)
|
|
300
|
+
if candles and len(candles)>0:
|
|
301
|
+
candles.sort(key=lambda x : x[0], reverse=False)
|
|
302
|
+
|
|
303
|
+
return candles
|
|
304
|
+
|
|
305
|
+
all_candles = []
|
|
306
|
+
params = {}
|
|
307
|
+
this_cutoff = start_ts
|
|
308
|
+
while this_cutoff<=end_ts:
|
|
309
|
+
candles = _fetch_ohlcv(symbol=symbol, timeframe=candle_size, since=int(this_cutoff * 1000), limit=num_candles_limit, params=params)
|
|
310
|
+
if candles and len(candles)>0:
|
|
311
|
+
all_candles = all_candles + [[ int(x[0]), float(x[1]), float(x[2]), float(x[3]), float(x[4]), float(x[5]) ] for x in candles if x[1] and x[2] and x[3] and x[4] and x[5] ]
|
|
312
|
+
|
|
313
|
+
record_ts = max([int(record[0]) for record in candles])
|
|
314
|
+
record_ts_str : str = str(record_ts)
|
|
315
|
+
if len(record_ts_str)==13:
|
|
316
|
+
record_ts = int(int(record_ts_str)/1000) # Convert from milli-seconds to seconds
|
|
317
|
+
|
|
318
|
+
if this_cutoff==record_ts+1:
|
|
319
|
+
break
|
|
320
|
+
else:
|
|
321
|
+
this_cutoff = record_ts + 1
|
|
322
|
+
columns = ['exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume']
|
|
323
|
+
pd_all_candles = pd.DataFrame([ [ "futubull", symbol, x[0], x[1], x[2], x[3], x[4], x[5] ] for x in all_candles], columns=columns)
|
|
324
|
+
# fix_column_types(pd_all_candles)
|
|
325
|
+
pd_all_candles['pct_chg_on_close'] = pd_all_candles['close'].pct_change()
|
|
326
|
+
return pd_all_candles
|
|
327
|
+
|
|
328
|
+
if __name__ == '__main__':
|
|
329
|
+
params : Dict = {
|
|
330
|
+
'trdmarket' : TrdMarket.HK,
|
|
331
|
+
'security_firm' : SecurityFirm.FUTUSECURITIES,
|
|
332
|
+
'market' : Market.HK,
|
|
333
|
+
'security_type' : SecurityType.STOCK,
|
|
334
|
+
'daemon' : {
|
|
335
|
+
'host' : '127.0.0.1',
|
|
336
|
+
'port' : 11111
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
exchange = Futubull(params)
|
|
340
|
+
markets = exchange.load_markets()
|
|
341
|
+
|
|
342
|
+
dt_end = datetime.today()
|
|
343
|
+
dt_start : datetime = dt_end - timedelta(days=365*3)
|
|
344
|
+
timestamp_ms : int = int(dt_start.timestamp() * 1000)
|
|
345
|
+
candles = exchange.fetch_ohlcv(symbol="HK.00700", timeframe="1h", since=timestamp_ms, limit=1000)
|
|
346
|
+
|
|
347
|
+
'''
|
|
348
|
+
Examples from Futu https://openapi.futunn.com/futu-api-doc/en
|
|
349
|
+
|
|
350
|
+
'''
|
|
351
|
+
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
|
352
|
+
|
|
353
|
+
print(quote_ctx.get_global_state())
|
|
354
|
+
|
|
355
|
+
ret, data = quote_ctx.get_stock_basicinfo(Market.HK, SecurityType.STOCK)
|
|
356
|
+
if ret == RET_OK:
|
|
357
|
+
print(data)
|
|
358
|
+
else:
|
|
359
|
+
print('error:', data)
|
|
360
|
+
print('******************************************')
|
|
361
|
+
ret, data = quote_ctx.get_stock_basicinfo(Market.HK, SecurityType.STOCK, ['HK.06998', 'HK.00700'])
|
|
362
|
+
if ret == RET_OK:
|
|
363
|
+
assert isinstance(data, pd.DataFrame)
|
|
364
|
+
print(data)
|
|
365
|
+
print(data['name'][0])
|
|
366
|
+
print(data['name'].values.tolist())
|
|
367
|
+
else:
|
|
368
|
+
print('error:', data)
|
|
369
|
+
|
|
370
|
+
'''
|
|
371
|
+
KLType: https://openapi.futunn.com/futu-api-doc/en/quote/quote.html#66
|
|
372
|
+
'''
|
|
373
|
+
ret, data, page_req_key = quote_ctx.request_history_kline('HK.00700', start='2024-01-01', end='2025-03-07', ktype=KLType.K_60M, max_count=100)
|
|
374
|
+
if ret == RET_OK:
|
|
375
|
+
assert isinstance(data, pd.DataFrame)
|
|
376
|
+
print(data)
|
|
377
|
+
print(data['code'][0])
|
|
378
|
+
print(data['close'].values.tolist())
|
|
379
|
+
else:
|
|
380
|
+
print('error:', data)
|
|
381
|
+
while page_req_key != None:
|
|
382
|
+
print('*************************************')
|
|
383
|
+
ret, data, page_req_key = quote_ctx.request_history_kline('HK.00700', start='2019-09-11', end='2019-09-18', max_count=5,page_req_key=page_req_key) # Request the page after turning data
|
|
384
|
+
if ret == RET_OK:
|
|
385
|
+
print(data)
|
|
386
|
+
else:
|
|
387
|
+
print('error:', data)
|
|
388
|
+
|
|
389
|
+
# For any real time data, you need subscribe https://openapi.futunn.com/futu-api-doc/en/intro/authority.html#5331
|
|
390
|
+
ret_sub, err_message = quote_ctx.subscribe(['HK.00700'], [SubType.K_DAY], subscribe_push=False)
|
|
391
|
+
if ret_sub == RET_OK:
|
|
392
|
+
ret, data = quote_ctx.get_cur_kline('HK.00700', 2, SubType.K_DAY, AuType.QFQ)
|
|
393
|
+
if ret == RET_OK:
|
|
394
|
+
assert isinstance(data, pd.DataFrame)
|
|
395
|
+
print(data)
|
|
396
|
+
print(data['turnover_rate'][0])
|
|
397
|
+
print(data['turnover_rate'].values.tolist())
|
|
398
|
+
else:
|
|
399
|
+
print('error:', data)
|
|
400
|
+
else:
|
|
401
|
+
# (-1, '无权限订阅HK.00005的行情,请检查香港市场股票行情权限')
|
|
402
|
+
print('subscription failed', err_message)
|
|
403
|
+
|
|
404
|
+
ret_sub, err_message = quote_ctx.subscribe(['HK.00700'], [SubType.QUOTE], subscribe_push=False)
|
|
405
|
+
if ret_sub == RET_OK:
|
|
406
|
+
ret, data = quote_ctx.get_stock_quote(['HK.00700'])
|
|
407
|
+
if ret == RET_OK:
|
|
408
|
+
assert isinstance(data, pd.DataFrame)
|
|
409
|
+
print(data)
|
|
410
|
+
print(data['code'][0])
|
|
411
|
+
print(data['code'].values.tolist())
|
|
412
|
+
else:
|
|
413
|
+
print('error:', data)
|
|
414
|
+
else:
|
|
415
|
+
# '无权限订阅HK.00700的行情,请检查香港市场股票行情权限'
|
|
416
|
+
print('subscription failed', err_message)
|
|
417
|
+
|
|
418
|
+
ret_sub = quote_ctx.subscribe(['HK.00700'], [SubType.ORDER_BOOK], subscribe_push=False)[0]
|
|
419
|
+
if ret_sub == RET_OK:
|
|
420
|
+
ret, data = quote_ctx.get_order_book('HK.00700', num=3)
|
|
421
|
+
if ret == RET_OK:
|
|
422
|
+
print(data)
|
|
423
|
+
else:
|
|
424
|
+
print('error:', data)
|
|
425
|
+
else:
|
|
426
|
+
print('subscription failed')
|
|
427
|
+
|
|
428
|
+
trd_ctx = OpenSecTradeContext(filter_trdmarket=TrdMarket.HK, host='127.0.0.1', port=11111, security_firm=SecurityFirm.FUTUSECURITIES)
|
|
429
|
+
ret, data = trd_ctx.accinfo_query()
|
|
430
|
+
if ret == RET_OK:
|
|
431
|
+
assert isinstance(data, pd.DataFrame)
|
|
432
|
+
print(data)
|
|
433
|
+
print(data['power'][0])
|
|
434
|
+
print(data['power'].values.tolist())
|
|
435
|
+
else:
|
|
436
|
+
print('accinfo_query error: ', data)
|
|
437
|
+
|
|
438
|
+
ret, data = trd_ctx.order_list_query()
|
|
439
|
+
if ret == RET_OK:
|
|
440
|
+
assert isinstance(data, pd.DataFrame)
|
|
441
|
+
print(data)
|
|
442
|
+
if data.shape[0] > 0:
|
|
443
|
+
print(data['order_id'][0])
|
|
444
|
+
print(data['order_id'].values.tolist())
|
|
445
|
+
else:
|
|
446
|
+
print('order_list_query error: ', data)
|
|
447
|
+
|
|
448
|
+
ret, data = trd_ctx.history_order_list_query()
|
|
449
|
+
if ret == RET_OK:
|
|
450
|
+
assert isinstance(data, pd.DataFrame)
|
|
451
|
+
print(data)
|
|
452
|
+
if data.shape[0] > 0:
|
|
453
|
+
print(data['order_id'][0])
|
|
454
|
+
print(data['order_id'].values.tolist())
|
|
455
|
+
else:
|
|
456
|
+
print('history_order_list_query error: ', data)
|
|
457
|
+
|
|
458
|
+
ret, data = trd_ctx.position_list_query()
|
|
459
|
+
if ret == RET_OK:
|
|
460
|
+
assert isinstance(data, pd.DataFrame)
|
|
461
|
+
print(data)
|
|
462
|
+
if data.shape[0] > 0:
|
|
463
|
+
print(data['stock_name'][0])
|
|
464
|
+
print(data['stock_name'].values.tolist())
|
|
465
|
+
else:
|
|
466
|
+
print('position_list_query error: ', data)
|
|
467
|
+
|
|
468
|
+
trd_ctx.close()
|
|
@@ -210,10 +210,6 @@ sh = logging.StreamHandler()
|
|
|
210
210
|
sh.setLevel(log_level)
|
|
211
211
|
sh.setFormatter(formatter)
|
|
212
212
|
logger.addHandler(sh)
|
|
213
|
-
fh = logging.FileHandler(f"ordergateway.log")
|
|
214
|
-
fh.setLevel(log_level)
|
|
215
|
-
fh.setFormatter(formatter)
|
|
216
|
-
logger.addHandler(fh)
|
|
217
213
|
|
|
218
214
|
DUMMY_EXECUTION : Dict[str, Any] = {
|
|
219
215
|
"clientOrderId": None,
|
|
@@ -776,6 +772,11 @@ async def work(
|
|
|
776
772
|
async def main():
|
|
777
773
|
parse_args()
|
|
778
774
|
|
|
775
|
+
fh = logging.FileHandler(f"ordergateway_{param['gateway_id']}.log")
|
|
776
|
+
fh.setLevel(log_level)
|
|
777
|
+
fh.setFormatter(formatter)
|
|
778
|
+
logger.addHandler(fh)
|
|
779
|
+
|
|
779
780
|
if not param['apikey']:
|
|
780
781
|
log("Loading credentials from .env")
|
|
781
782
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import unittest
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
3
|
from typing import Union
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from util.market_data_util import *
|
|
7
|
+
from exchanges.futubull import Futubull
|
|
7
8
|
|
|
8
9
|
from ccxt.binance import binance
|
|
9
10
|
from ccxt.bybit import bybit
|
|
@@ -11,6 +12,9 @@ from ccxt.okx import okx
|
|
|
11
12
|
from ccxt.deribit import deribit
|
|
12
13
|
from ccxt.base.exchange import Exchange
|
|
13
14
|
|
|
15
|
+
from futu import *
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
# @unittest.skip("Skip all integration tests.")
|
|
15
19
|
class MarketDataUtilTests(unittest.TestCase):
|
|
16
20
|
|
|
@@ -115,4 +119,37 @@ class MarketDataUtilTests(unittest.TestCase):
|
|
|
115
119
|
assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
|
|
116
120
|
assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
|
|
117
121
|
|
|
122
|
+
def test_fetch_candles_futubull(self):
|
|
123
|
+
end_date : datetime = datetime.today()
|
|
124
|
+
start_date : datetime = end_date - timedelta(days=365*3)
|
|
125
|
+
|
|
126
|
+
params : Dict = {
|
|
127
|
+
'trdmarket' : TrdMarket.HK,
|
|
128
|
+
'security_firm' : SecurityFirm.FUTUSECURITIES,
|
|
129
|
+
'market' : Market.HK,
|
|
130
|
+
'security_type' : SecurityType.STOCK,
|
|
131
|
+
'daemon' : {
|
|
132
|
+
'host' : '127.0.0.1',
|
|
133
|
+
'port' : 11111
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exchange = Futubull(params)
|
|
137
|
+
|
|
138
|
+
symbol = "HK.00700"
|
|
139
|
+
pd_candles: Union[pd.DataFrame, None] = fetch_candles(
|
|
140
|
+
start_ts=int(start_date.timestamp()),
|
|
141
|
+
end_ts=int(end_date.timestamp()),
|
|
142
|
+
exchange=exchange,
|
|
143
|
+
normalized_symbols=[ symbol ],
|
|
144
|
+
candle_size='1d'
|
|
145
|
+
)[symbol]
|
|
146
|
+
|
|
147
|
+
assert pd_candles is not None
|
|
118
148
|
|
|
149
|
+
if pd_candles is not None:
|
|
150
|
+
assert len(pd_candles) > 0, "No candles returned."
|
|
151
|
+
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute'}
|
|
152
|
+
assert set(pd_candles.columns) >= expected_columns, "Missing expected columns."
|
|
153
|
+
assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
|
|
154
|
+
assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
|
|
155
|
+
|
|
@@ -15,6 +15,8 @@ from yahoofinancials import YahooFinancials
|
|
|
15
15
|
# yfinance allows intervals '1m', '5m', '15m', '1h', '1d', '1wk', '1mo'. yahoofinancials not as flexible
|
|
16
16
|
import yfinance as yf
|
|
17
17
|
|
|
18
|
+
from exchanges.futubull import Futubull
|
|
19
|
+
|
|
18
20
|
def timestamp_to_datetime_cols(pd_candles : pd.DataFrame):
|
|
19
21
|
pd_candles['datetime'] = pd_candles['timestamp_ms'].apply(
|
|
20
22
|
lambda x: datetime.fromtimestamp(int(x.timestamp()) if isinstance(x, pd.Timestamp) else int(x / 1000))
|
|
@@ -172,15 +174,15 @@ class YahooExchange:
|
|
|
172
174
|
# From yf, "DateTime" in UTC
|
|
173
175
|
# The requested range must be within the last 730 days. Otherwise API will return empty DataFrame.
|
|
174
176
|
pd_candles = yf.download(tickers=symbol, start=start_date_str, end=end_date_str, interval=candle_size)
|
|
175
|
-
pd_candles.reset_index(inplace=True)
|
|
176
|
-
pd_candles.rename(columns={ 'Date' : 'datetime', 'Datetime' : 'datetime', 'Open': 'open', 'High': 'high', 'Low': 'low', 'Close' : 'close', 'Adj Close' : 'adj_close', 'Volume' : 'volume' }, inplace=True)
|
|
177
|
-
pd_candles['datetime'] = pd.to_datetime(pd_candles['datetime'])
|
|
178
|
-
if pd_candles['datetime'].dt.tz is None:
|
|
179
|
-
pd_candles['datetime'] = pd.to_datetime(pd_candles['datetime']).dt.tz_localize('UTC')
|
|
180
|
-
pd_candles['datetime'] = pd_candles['datetime'].dt.tz_convert(local_tz)
|
|
181
|
-
pd_candles['datetime'] = pd_candles['datetime'].dt.tz_localize(None)
|
|
182
|
-
pd_candles['timestamp_ms'] = pd_candles.datetime.values.astype(np.int64) // 10**6
|
|
183
|
-
pd_candles = pd_candles.sort_values(by=['timestamp_ms'], ascending=[True])
|
|
177
|
+
pd_candles.reset_index(inplace=True) # type: ignore
|
|
178
|
+
pd_candles.rename(columns={ 'Date' : 'datetime', 'Datetime' : 'datetime', 'Open': 'open', 'High': 'high', 'Low': 'low', 'Close' : 'close', 'Adj Close' : 'adj_close', 'Volume' : 'volume' }, inplace=True) # type: ignore
|
|
179
|
+
pd_candles['datetime'] = pd.to_datetime(pd_candles['datetime']) # type: ignore
|
|
180
|
+
if pd_candles['datetime'].dt.tz is None: # type: ignore
|
|
181
|
+
pd_candles['datetime'] = pd.to_datetime(pd_candles['datetime']).dt.tz_localize('UTC') # type: ignore
|
|
182
|
+
pd_candles['datetime'] = pd_candles['datetime'].dt.tz_convert(local_tz) # type: ignore
|
|
183
|
+
pd_candles['datetime'] = pd_candles['datetime'].dt.tz_localize(None) # type: ignore
|
|
184
|
+
pd_candles['timestamp_ms'] = pd_candles.datetime.values.astype(np.int64) // 10**6 # type: ignore
|
|
185
|
+
pd_candles = pd_candles.sort_values(by=['timestamp_ms'], ascending=[True]) # type: ignore
|
|
184
186
|
|
|
185
187
|
fix_column_types(pd_candles)
|
|
186
188
|
pd_candles['symbol'] = symbol
|
|
@@ -249,6 +251,18 @@ def fetch_candles(
|
|
|
249
251
|
symbols=normalized_symbols,
|
|
250
252
|
candle_size=candle_size
|
|
251
253
|
)
|
|
254
|
+
elif type(exchange) is Futubull:
|
|
255
|
+
exchange_candles = exchange.fetch_candles(
|
|
256
|
+
start_ts=start_ts,
|
|
257
|
+
end_ts=end_ts,
|
|
258
|
+
symbols=normalized_symbols,
|
|
259
|
+
candle_size=candle_size
|
|
260
|
+
)
|
|
261
|
+
for symbol in exchange_candles:
|
|
262
|
+
pd_candles = exchange_candles[symbol]
|
|
263
|
+
if not pd_candles is None:
|
|
264
|
+
fix_column_types(pd_candles) # You don't want to do this from Futubull as you'd need import Futubull from there: Circular references
|
|
265
|
+
return exchange_candles
|
|
252
266
|
elif issubclass(exchange.__class__, CcxtExchange):
|
|
253
267
|
return _fetch_candles_ccxt(
|
|
254
268
|
start_ts=start_ts,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
siglab_py/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
siglab_py/constants.py,sha256=YGNdEsWtQ99V2oltaynZTjM8VIboSfyIjDXJKSlhv4U,132
|
|
3
3
|
siglab_py/exchanges/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
siglab_py/exchanges/any_exchange.py,sha256=
|
|
4
|
+
siglab_py/exchanges/any_exchange.py,sha256=Y-zue75ZSmu9Ga1fONbjBGLNH5pDHQI01hCSjuLBkAk,889
|
|
5
|
+
siglab_py/exchanges/futubull.py,sha256=yjXVZEyHUv_ZDe8NOsAkgymaS_rT5r6cOEW8vbv7Kao,19540
|
|
5
6
|
siglab_py/market_data_providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
7
|
siglab_py/market_data_providers/aggregated_orderbook_provider.py,sha256=FZRobEBNRzcNGlOG3u38OVhmOZYlkNm8dVvR-S7Ii2g,23342
|
|
7
8
|
siglab_py/market_data_providers/candles_provider.py,sha256=fqHJjlECsBiBlpgyywrc4gTgxiROPNzZM8KxQBB5cOg,14139
|
|
@@ -12,19 +13,19 @@ siglab_py/market_data_providers/test_provider.py,sha256=wBLCgcWjs7FGZJXWsNyn30lk
|
|
|
12
13
|
siglab_py/ordergateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
14
|
siglab_py/ordergateway/client.py,sha256=qh4vrCJm8iITKAl07lisZxJ3V4JkbsYlmIBMYihfUaU,9575
|
|
14
15
|
siglab_py/ordergateway/encrypt_keys_util.py,sha256=-qi87db8To8Yf1WS1Q_Cp2Ya7ZqgWlRqSHfNXCM7wE4,1339
|
|
15
|
-
siglab_py/ordergateway/gateway.py,sha256=
|
|
16
|
+
siglab_py/ordergateway/gateway.py,sha256=jP_jH1rXbY-VrPRAI4tYJJuFDTEM2kxRNNnWF1kh2VU,36977
|
|
16
17
|
siglab_py/ordergateway/test_ordergateway.py,sha256=_Gz2U_VqljogGWqGyNDYYls1INqUiig9veyPttfGRpg,3901
|
|
17
18
|
siglab_py/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
19
|
siglab_py/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
siglab_py/tests/integration/market_data_util_tests.py,sha256=
|
|
20
|
+
siglab_py/tests/integration/market_data_util_tests.py,sha256=5RbWkeEWPqCOXiW3e04aBhE2cINqkavKjCleFwI1DSE,6813
|
|
20
21
|
siglab_py/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
siglab_py/tests/unit/analytic_util_tests.py,sha256=-FGj-cWdIZ_WZQqQCIpDLRSv5TlJIG81fHyBlWuAm1U,2961
|
|
22
23
|
siglab_py/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
24
|
siglab_py/util/analytic_util.py,sha256=QLabbEMqM4rKKH2PE_LqxIyo-BUdCRhkLybLATBImcc,43438
|
|
24
25
|
siglab_py/util/aws_util.py,sha256=KGmjHrr1rpnnxr33nXHNzTul4tvyyxl9p6gpwNv0Ygc,2557
|
|
25
|
-
siglab_py/util/market_data_util.py,sha256=
|
|
26
|
+
siglab_py/util/market_data_util.py,sha256=PmGPBXJDTuK-so4qoGT27ag-_Qig7GNFV6znxkmcRW4,17252
|
|
26
27
|
siglab_py/util/retry_util.py,sha256=mxYuRFZRZoaQQjENcwPmxhxixtd1TFvbxIdPx4RwfRc,743
|
|
27
|
-
siglab_py-0.1.
|
|
28
|
-
siglab_py-0.1.
|
|
29
|
-
siglab_py-0.1.
|
|
30
|
-
siglab_py-0.1.
|
|
28
|
+
siglab_py-0.1.18.dist-info/METADATA,sha256=-8lmN_HYchcMmWOl9xlrJOAbCz-AVSlUEWHw_Q7Ubks,1097
|
|
29
|
+
siglab_py-0.1.18.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
30
|
+
siglab_py-0.1.18.dist-info/top_level.txt,sha256=AbD4VR9OqmMOGlMJLkAVPGQMtUPIQv0t1BF5xmcLJSk,10
|
|
31
|
+
siglab_py-0.1.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|