siglab-py 0.1.17__tar.gz → 0.1.19__tar.gz

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.

Files changed (35) hide show
  1. {siglab_py-0.1.17 → siglab_py-0.1.19}/PKG-INFO +1 -1
  2. {siglab_py-0.1.17 → siglab_py-0.1.19}/pyproject.toml +1 -1
  3. {siglab_py-0.1.17 → siglab_py-0.1.19}/setup.cfg +1 -1
  4. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/exchanges/any_exchange.py +7 -1
  5. siglab_py-0.1.19/siglab_py/exchanges/futubull.py +468 -0
  6. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/ordergateway/gateway.py +5 -4
  7. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/tests/integration/market_data_util_tests.py +38 -1
  8. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/util/market_data_util.py +23 -9
  9. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py.egg-info/PKG-INFO +1 -1
  10. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py.egg-info/SOURCES.txt +1 -0
  11. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/__init__.py +0 -0
  12. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/constants.py +0 -0
  13. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/exchanges/__init__.py +0 -0
  14. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/__init__.py +0 -0
  15. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/aggregated_orderbook_provider.py +0 -0
  16. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/candles_provider.py +0 -0
  17. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/candles_ta_provider.py +0 -0
  18. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/deribit_options_expiry_provider.py +0 -0
  19. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/orderbooks_provider.py +0 -0
  20. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/market_data_providers/test_provider.py +0 -0
  21. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/ordergateway/__init__.py +0 -0
  22. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/ordergateway/client.py +0 -0
  23. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/ordergateway/encrypt_keys_util.py +0 -0
  24. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/ordergateway/test_ordergateway.py +0 -0
  25. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/tests/__init__.py +0 -0
  26. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/tests/integration/__init__.py +0 -0
  27. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/tests/unit/__init__.py +0 -0
  28. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/tests/unit/analytic_util_tests.py +0 -0
  29. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/util/__init__.py +0 -0
  30. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/util/analytic_util.py +0 -0
  31. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/util/aws_util.py +0 -0
  32. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py/util/retry_util.py +0 -0
  33. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py.egg-info/dependency_links.txt +0 -0
  34. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py.egg-info/requires.txt +0 -0
  35. {siglab_py-0.1.17 → siglab_py-0.1.19}/siglab_py.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: siglab_py
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Summary: Market data fetches, TA calculations and generic order gateway.
5
5
  Author: r0bbarh00d
6
6
  Author-email: r0bbarh00d <r0bbarh00d@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "siglab_py"
7
- version = "0.1.17"
7
+ version = "0.1.19"
8
8
  description = "Market data fetches, TA calculations and generic order gateway."
9
9
  authors = [{name = "r0bbarh00d", email = "r0bbarh00d@gmail.com"}]
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = siglab_py
3
- version = 0.1.17
3
+ version = 0.1.19
4
4
  description = Market data fetches, TA calculations and generic order gateway.
5
5
  author = r0bbarh00d
6
6
  author_email = r0bbarh00d@gmail.com
@@ -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(Exception):
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 siglab_py.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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: siglab-py
3
- Version: 0.1.17
3
+ Version: 0.1.19
4
4
  Summary: Market data fetches, TA calculations and generic order gateway.
5
5
  Author: r0bbarh00d
6
6
  Author-email: r0bbarh00d <r0bbarh00d@gmail.com>
@@ -9,6 +9,7 @@ siglab_py.egg-info/requires.txt
9
9
  siglab_py.egg-info/top_level.txt
10
10
  siglab_py/exchanges/__init__.py
11
11
  siglab_py/exchanges/any_exchange.py
12
+ siglab_py/exchanges/futubull.py
12
13
  siglab_py/market_data_providers/__init__.py
13
14
  siglab_py/market_data_providers/aggregated_orderbook_provider.py
14
15
  siglab_py/market_data_providers/candles_provider.py