siglab-py 0.1.30__py3-none-any.whl → 0.6.33__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.
Files changed (45) hide show
  1. siglab_py/algo/__init__.py +0 -0
  2. siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
  3. siglab_py/algo/strategy_base.py +122 -0
  4. siglab_py/algo/strategy_executor.py +1308 -0
  5. siglab_py/algo/tp_algo.py +529 -0
  6. siglab_py/backtests/__init__.py +0 -0
  7. siglab_py/backtests/backtest_core.py +2405 -0
  8. siglab_py/backtests/coinflip_15m_crypto.py +432 -0
  9. siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
  10. siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
  11. siglab_py/constants.py +26 -1
  12. siglab_py/exchanges/binance.py +38 -0
  13. siglab_py/exchanges/deribit.py +83 -0
  14. siglab_py/exchanges/futubull.py +12 -2
  15. siglab_py/market_data_providers/candles_provider.py +11 -10
  16. siglab_py/market_data_providers/candles_ta_provider.py +5 -5
  17. siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
  18. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
  19. siglab_py/market_data_providers/google_monitor.py +320 -0
  20. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  21. siglab_py/market_data_providers/tg_monitor.py +428 -0
  22. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  23. siglab_py/ordergateway/client.py +172 -41
  24. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  25. siglab_py/ordergateway/gateway.py +456 -347
  26. siglab_py/ordergateway/test_ordergateway.py +8 -7
  27. siglab_py/tests/integration/market_data_util_tests.py +75 -2
  28. siglab_py/tests/unit/analytic_util_tests.py +47 -12
  29. siglab_py/tests/unit/market_data_util_tests.py +45 -1
  30. siglab_py/tests/unit/simple_math_tests.py +252 -0
  31. siglab_py/tests/unit/trading_util_tests.py +65 -0
  32. siglab_py/util/analytic_util.py +476 -67
  33. siglab_py/util/datetime_util.py +39 -0
  34. siglab_py/util/market_data_util.py +528 -98
  35. siglab_py/util/module_util.py +40 -0
  36. siglab_py/util/notification_util.py +78 -0
  37. siglab_py/util/retry_util.py +16 -3
  38. siglab_py/util/simple_math.py +262 -0
  39. siglab_py/util/slack_notification_util.py +59 -0
  40. siglab_py/util/trading_util.py +118 -0
  41. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -9
  42. siglab_py-0.6.33.dist-info/RECORD +56 -0
  43. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
  44. siglab_py-0.1.30.dist-info/RECORD +0 -34
  45. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,473 @@
1
+ '''
2
+ Command line:
3
+ python macdrsi_h_tc_crypto.py --white_list_tickers BTC/USDT:USDT,ETH/USDT:USDT,BNB/USDT:USDT,SOL/USDT:USDT,XRP/USDT:USDT --reference_ticker BTC/USDT:USDT --force_reload Y --block_entries_on_impacting_ecoevents N
4
+
5
+ Debug from vscode, Launch.json:
6
+ {
7
+ "version": "0.2.0",
8
+ "configurations": [
9
+ {
10
+ "name": "Python: Current File",
11
+ "type": "python",
12
+ "request": "launch",
13
+ "program": "${file}",
14
+ "console": "integratedTerminal",
15
+ "justMyCode": true,
16
+ "args" : [
17
+ "--white_list_tickers", "BTC/USDT:USDT,ETH/USDT:USDT,BNB/USDT:USDT,SOL/USDT:USDT,XRP/USDT:USDT",
18
+ "--reference_ticker", "BTC/USDT:USDT",
19
+ "--force_reload", "Y",
20
+ "--block_entries_on_impacting_ecoevents", "N"
21
+ ]
22
+ }
23
+ ]
24
+ }
25
+ '''
26
+ import os
27
+ import sys
28
+ import argparse
29
+ import json
30
+ from datetime import datetime, timedelta, timezone
31
+ import time
32
+ from typing import Dict, List, Tuple, Any, Callable, Union
33
+ import pandas as pd
34
+
35
+ from ccxt.base.exchange import Exchange
36
+ from ccxt.bybit import bybit
37
+
38
+ from backtest_core import parseargs, get_logger, spawn_parameters, generic_pnl_eval, generic_tp_eval, generic_sort_filter_universe, run_all_scenario, dump_trades_to_disk
39
+
40
+ PYPY_COMPAT : bool = True
41
+
42
+ sys.path.append('../gizmo')
43
+ # from market_data_gizmo import fetch_historical_price, fetch_candles, fix_column_types, compute_candles_stats, partition_sliding_window, estimate_fib_retracement
44
+ base_dir : str = f"{os.path.dirname(sys.path[0])}\\single_leg_ta"
45
+
46
+ REPORT_NAME : str = "backtest_macdrsi_crosses_strategy_15m_tc_crypto"
47
+ CACHE_CANDLES : str = f"{os.path.dirname(sys.path[0])}\\cache\\candles"
48
+
49
+ '''
50
+ white_list_tickers : List[str] = [
51
+ "BTC/USDT:USDT",
52
+ "ETH/USDT:USDT",
53
+ "BNB/USDT:USDT",
54
+ "SOL/USDT:USDT",
55
+ "XRP/USDT:USDT",
56
+ "DOGE/USDT:USDT",
57
+ "ADA/USDT:USDT",
58
+ "TRX/USDT:USDT",
59
+ "AVAX/USDT:USDT",
60
+ "LINK/USDT:USDT",
61
+ "DOT/USDT:USDT",
62
+ "TON/USDT:USDT",
63
+ "MATIC/USDT:USDT",
64
+ "SHIB/USDT:USDT",
65
+ "LTC/USDT:USDT",
66
+ "BCH/USDT:USDT",
67
+ "UNI/USDT:USDT",
68
+ "NEAR/USDT:USDT",
69
+ "ICP/USDT:USDT",
70
+ "APT/USDT:USDT"
71
+ ]
72
+ '''
73
+ white_list_tickers : List[str] = [ "SOL/USDT:USDT" ]
74
+
75
+ force_reload : bool = False
76
+
77
+ num_candles_limit = 100 # Depends on exchange but generally 100 ok!
78
+ param = {
79
+ 'apiKey' : None,
80
+ 'secret' : None,
81
+ 'password' : None, # Other exchanges dont require this! This is saved in exchange.password!
82
+ 'subaccount' : None,
83
+ 'rateLimit' : 100, # In ms
84
+ 'options' : {
85
+ 'defaultType': 'linear',
86
+ 'leg_room_bps' : 5,
87
+ 'trade_fee_bps' : 10,
88
+
89
+ 'list_ts_field' : 'listTime' # list_ts_field: Response field in exchange.markets[symbol] to indiate timestamp of symbol's listing date in ms. For bybit, markets['launchTime'] is list date. For okx, it's markets['listTime'].
90
+ }
91
+ }
92
+
93
+ exchanges = [
94
+ bybit(param),
95
+ ]
96
+
97
+ exchanges[0].name='bybit_linear'
98
+
99
+ commission_bps : float = 5
100
+
101
+ '''
102
+ ******** STRATEGY_SPECIFIC parameters ********
103
+ '''
104
+ additional_trade_fields : List[str] = [
105
+ # Add fields you want to include in trade extract
106
+ ]
107
+
108
+
109
+ '''
110
+ ******** GENERIC parameters ********
111
+ '''
112
+ strategy_mode_values : List[str]= [ 'long_short'] # 'long_only', 'short_only', 'long_short'
113
+
114
+ '''
115
+ For example, Monday's are weird. Entries, SL adjustments ...etc may have STRATEGY_SPECIFIC logic around this.
116
+ '''
117
+ CAUTIOUS_DAYOFWEEK : List[int] = [ 0 ]
118
+ how_many_last_candles : int = 3
119
+ last_candles_timeframe : str = 'lo' # Either hi or lo (default)
120
+ enable_wait_entry : bool = True
121
+ enable_sliced_entry : bool = False
122
+ enable_athatl_logic : bool = False # If you have special logic in 'allow_entry_initial' or 'allow_entry_final'.
123
+
124
+ '''
125
+ Economic events comes from 'economic_calanedar.csv' in same folder.
126
+
127
+ Block entries if pending economic event in next x-intervals (applied on lo timeframe)
128
+ Set to -1 to disable this.
129
+ '''
130
+ adj_sl_on_ecoevents = False
131
+ block_entries_on_impacting_ecoevents = True
132
+ num_intervals_block_pending_ecoevents = 3
133
+ ECOEVENTS_MAPPED_REGIONS = [ 'united_states' ]
134
+
135
+ mapped_event_codes = [
136
+ 'core_inflation_rate_mom', 'core_inflation_rate_yoy',
137
+ 'inflation_rate_mom', 'inflation_rate_yoy',
138
+ 'fed_interest_rate_decision',
139
+ 'fed_chair_speech',
140
+ 'core_pce_price_index_mom',
141
+ 'core_pce_price_index_yoy',
142
+ 'unemployment_rate',
143
+ 'non_farm_payrolls',
144
+ 'gdp_growth_rate_qoq_adv',
145
+ 'gdp_growth_rate_qoq_final',
146
+ 'gdp_growth_rate_yoy'
147
+ ]
148
+
149
+ num_intervals_current_ecoevents = 8
150
+
151
+ sl_num_intervals_delay_values : List[float] = [ 15*4*8 ]
152
+ sl_hard_percent_values : List[float] = [ 2.5 ]
153
+ sl_percent_trailing_values : List[float] = [ 35 ]
154
+ use_gradual_tightened_trailing_stops : bool = True
155
+ trailing_stop_mode : str = "linear" # linear or parabolic
156
+
157
+ '''
158
+ This is for trailing stops slope calc.
159
+ Say if your trade's max profit potential is tp_max_percent=3%=300bps.
160
+ tp_min_percent = 0.3 means you will NOT TP until at least pnl > 0.3% or 30bps.
161
+ '''
162
+ tp_min_percent = 3
163
+ tp_max_percent = 5
164
+
165
+ POST_MOVE_NUM_INTERVALS : int = 24*3
166
+ POST_MOVE_PERCENT_THRESHOLD : int = 3
167
+
168
+ enable_hi_timeframe_confirm : bool = True
169
+
170
+ start_dates : List[datetime] = [
171
+ datetime(2024, 4, 1)
172
+ ]
173
+
174
+ hi_how_many_candles_values : List[Tuple[str, int, int]] = [
175
+ ('1h', 24*3, 24*572)
176
+ ]
177
+
178
+ lo_how_many_candles_values : List[Tuple[str, int, int]] = [
179
+ ('15m', 15 *10, 15*4*24 *572)
180
+ ]
181
+
182
+ hi_ma_short_vs_long_interval_values : List[Tuple[int, int]] = [ (12, 30) ]
183
+ lo_ma_short_vs_long_interval_values : List[Tuple[int, int]] = [ (5, 10) ]
184
+
185
+ rsi_sliding_window_how_many_candles : int = 14 # For RSI, 14 is standard. If you want see spikes >70 and <30, use this config.
186
+ rsi_trend_sliding_window_how_many_candles : int = 30 # This is for purpose of RSI trend identification (Locating local peaks/troughs in RSI). This should typically be multiples of 'rsi_sliding_window_how_many_candles'.
187
+ rsi_upper_threshold_values : List[float] = [ 60 ]
188
+ rsi_lower_threshold_values : List[float] = [ 40 ]
189
+ rsi_midrangeonly : bool = False
190
+
191
+ target_fib_level : float = 0.618
192
+ boillenger_std_multiples_values : List[float] = [ 2 ]
193
+ allow_entry_sit_bb : bool = True
194
+ hurst_exp_window_how_many_candles : int = 125 # For hurst, at least 125.
195
+
196
+
197
+ # 'strategy_mode' decides if strategy can long_only, short_only, long_short at get go of back test. If long_above_btc_ema_short_below==True, strategy can long at bottom only if BTC (General market) stands above say 90d EMA. Or short only if BTC below 90d EMA for the given point in time.
198
+ ref_ema_num_days_fast : int = 5
199
+ ref_ema_num_days_slow : int = 90
200
+ long_above_ref_ema_short_below : bool = True
201
+ ref_price_vs_ema_percent_threshold : float = 2
202
+ ath_atl_close_gap_threshold_percent : float = 3
203
+
204
+ ema_short_slope_threshold_values : List[float] = [ 999 ] # 999 essentially turn it off
205
+
206
+ initial_cash_values : List[float] = [ 100000 ]
207
+
208
+ entry_percent_initial_cash_values : List[float] = [ 70 ]
209
+ target_position_size_percent_total_equity_values : List[float] = [ 100 ]
210
+ min_volume_usdt_threshold_values : List[float] = [ 100000 ]
211
+ clip_order_notional_to_best_volumes : bool = False
212
+ constant_order_notional : bool = True if min(start_dates) <= datetime(2024,1,1) else False # This is avoid snowball effect in long dated back tests
213
+
214
+ dayofweek_adj_map_order_notional : Dict = {
215
+ 0 : 1,
216
+ 1 : 1,
217
+ 2 : 1,
218
+ 3 : 1,
219
+ 4 : 1,
220
+ 5 : 1,
221
+ 6 : 1
222
+ }
223
+
224
+ dayofweek_sl_adj_map : Dict = {
225
+ 0 : 1,
226
+ 1 : 1,
227
+ 2 : 1,
228
+ 3 : 1,
229
+ 4 : 1,
230
+ 5 : 1,
231
+ 6 : 0.5
232
+ }
233
+
234
+ # Segmentation related parameters https://norman-lm-fung.medium.com/time-series-slicer-and-price-pattern-extractions-81f9dd1108fd
235
+ sliding_window_ratio : float = 16
236
+ smoothing_window_size_ratio : int = 3
237
+ linregress_stderr_threshold : float = 10
238
+ max_recur_depth : int = 2
239
+ min_segment_size_how_many_candles : int = 15
240
+ segment_consolidate_slope_ratio_threshold : float = 2
241
+ sideway_price_condition_threshold : float = 0.05 # i.e. Price if stay within 5% between start and close it's considered 'Sideway' market.
242
+
243
+ ECONOMIC_CALENDARS_FILE : str = "economic_calanedar_archive.csv"
244
+
245
+ default_level_granularity : float = 0.001
246
+
247
+ args = parseargs()
248
+ force_reload = args['force_reload']
249
+ white_list_tickers : List[str] = args['white_list_tickers']
250
+ reference_ticker : str = args['reference_ticker']
251
+ block_entries_on_impacting_ecoevents = args['block_entries_on_impacting_ecoevents']
252
+ enable_sliced_entry = args['enable_sliced_entry']
253
+ asymmetric_tp_bps : int = args['asymmetric_tp_bps']
254
+
255
+ full_report_name = f"{REPORT_NAME}_{start_dates[0].strftime('%Y%m%d')}"
256
+ trade_extract_filename : str = f"{full_report_name}_{white_list_tickers[0].replace(':','').replace('/','')}_trades.csv"
257
+
258
+ logger = get_logger(full_report_name)
259
+
260
+ import inspect
261
+ import builtins
262
+ def is_external(obj):
263
+ if inspect.ismodule(obj):
264
+ return True
265
+ module = getattr(obj, '__module__', None)
266
+ return module and not module.startswith('__') # Exclude built-in/dunder modules
267
+
268
+ local_vars = {
269
+ k: v
270
+ for k, v in locals().items()
271
+ if not (k.startswith('__') and k.endswith('__')) # Exclude dunders
272
+ and not is_external(v) # Exclude anything from external modules
273
+ }
274
+
275
+ algo_params : List[Dict] = spawn_parameters(local_vars)
276
+
277
+ logger.info(f"#algo_params: {len(algo_params)}")
278
+
279
+
280
+ '''
281
+ ******** STRATEGY_SPECIFIC Logic here ********
282
+ a. order_notional_adj
283
+ Specific logic to adjust order sizes based on market condition(s) for example.
284
+ b. entry (initial + final)
285
+ 'allow_entry_initial' is first pass entry conditions determination.
286
+ If 'allow_entry_initial' allow entry, 'allow_entry_final' will perform the second pass entry condition determinations.
287
+ 'allow_entry_final' is generally for more expensive operations, keep 'allow_entry_initial' fast and nimble.
288
+ c. 'pnl_eval' (You may wish to use specific prices to mark your TPs)
289
+ d. 'tp_eval' (Logic to fire TP)
290
+ e. 'sl_adj'
291
+ Adjustment to sl_percent_hard
292
+ f. 'trailing_stop_threshold_eval'
293
+ g. 'sort_filter_universe' (optional, if 'white_list_tickers' only has one ticker for example, then you don't need bother)
294
+ h. 'additional_trade_fields' to be included in the trade extract file
295
+ '''
296
+ def order_notional_adj(
297
+ algo_param : Dict,
298
+ ) -> Dict[str, float]:
299
+ initial_cash : float = algo_param['initial_cash']
300
+ entry_percent_initial_cash : float = algo_param['entry_percent_initial_cash']
301
+ target_order_notional = initial_cash * entry_percent_initial_cash/100
302
+ return {
303
+ 'target_order_notional' : target_order_notional
304
+ }
305
+
306
+ def allow_entry_initial(
307
+ lo_row_tm1,
308
+ hi_row_tm1
309
+ ) -> Dict[str, bool]:
310
+ return {
311
+ 'long' : _allow_entry_initial('long', lo_row_tm1, hi_row_tm1),
312
+ 'short' : _allow_entry_initial('short', lo_row_tm1, hi_row_tm1)
313
+ }
314
+ def _allow_entry_initial(
315
+ long_or_short : str, # long or short
316
+ lo_row_tm1,
317
+ hi_row_tm1
318
+ ) -> Dict[str, bool]:
319
+ if long_or_short == "long":
320
+ if (
321
+ lo_row_tm1['macd_cross'] == 'bullish'
322
+ # use 'macd_cross_last' instead in combinations with 'macd_bullish_cross_last_id' if you want to make more entries
323
+ '''
324
+ and (
325
+ lo_row_tm1.name >= lo_row_tm1['macd_bullish_cross_last_id']
326
+ and
327
+ (lo_row_tm1.name - lo_row_tm1['macd_bullish_cross_last_id']) < 5
328
+ )
329
+ '''
330
+ and lo_row_tm1['rsi_trend']=="up"
331
+ and lo_row_tm1['close']>hi_row_tm1['ema_close']
332
+ ):
333
+ return True
334
+ else:
335
+ return False
336
+ elif long_or_short == "short":
337
+ if (
338
+ lo_row_tm1['macd_cross'] == 'bearish'
339
+ '''
340
+ and (
341
+ lo_row_tm1.name >= lo_row_tm1['macd_bearish_cross_last_id']
342
+ and
343
+ (lo_row_tm1.name - lo_row_tm1['macd_bearish_cross_last_id']) < 5
344
+ )
345
+ '''
346
+ and lo_row_tm1['rsi_trend']=="down"
347
+ and lo_row_tm1['close']<hi_row_tm1['ema_close']
348
+ ):
349
+ return True
350
+ else:
351
+ return False
352
+
353
+ def allow_entry_final(
354
+ lo_row,
355
+ algo_param : Dict
356
+
357
+ ) -> Dict[str, Union[bool, float, None]]:
358
+ reference_ticker = algo_param['reference_ticker']
359
+ timestamp_ms : int = lo_row['timestamp_ms']
360
+ open : float = lo_row['open']
361
+
362
+ entry_price_long, entry_price_short = open, open
363
+ allow_long, allow_short = True, True
364
+ reference_price = None
365
+
366
+ pnl_potential_bps = algo_param['tp_max_percent']*100
367
+
368
+ target_price_long = entry_price_long * (1 + pnl_potential_bps/10000)
369
+ target_price_short = entry_price_short * (1 - pnl_potential_bps/10000)
370
+
371
+ return {
372
+ 'long' : allow_long,
373
+ 'short' : allow_short,
374
+
375
+ # In additional to allow or not, allow_entry_final also calculate a few things which you may need to mark the entry trades.
376
+ 'entry_price_long' : entry_price_long,
377
+ 'entry_price_short' : entry_price_short,
378
+ 'target_price_long' : target_price_long,
379
+ 'target_price_short' : target_price_short,
380
+ 'reference_price' : reference_price
381
+ }
382
+
383
+ allow_slice_entry = allow_entry_initial
384
+
385
+ def sl_adj(
386
+ max_unrealized_pnl_live : float,
387
+ current_position_usdt : float,
388
+ algo_param : Dict
389
+ ):
390
+ tp_min_percent = algo_param['tp_min_percent']
391
+ max_pnl_percent_notional = max_unrealized_pnl_live / current_position_usdt * 100
392
+ running_sl_percent_hard = algo_param['sl_hard_percent']
393
+ return {
394
+ 'running_sl_percent_hard' : running_sl_percent_hard
395
+ }
396
+
397
+ def trailing_stop_threshold_eval(
398
+ algo_param : Dict
399
+ ) -> Dict[str, float]:
400
+ tp_min_percent = algo_param['tp_min_percent']
401
+ tp_max_percent = algo_param['tp_max_percent']
402
+ return {
403
+ 'tp_min_percent' : tp_min_percent,
404
+ 'tp_max_percent' : tp_max_percent
405
+ }
406
+
407
+ def pnl_eval (
408
+ this_candle,
409
+ lo_row_tm1,
410
+ running_sl_percent_hard : float,
411
+ this_ticker_open_trades : List[Dict],
412
+ algo_param : Dict
413
+ ) -> Dict[str, float]:
414
+ return generic_pnl_eval(
415
+ this_candle,
416
+ running_sl_percent_hard,
417
+ this_ticker_open_trades,
418
+ algo_param,
419
+ long_tp_indicator_name=None,
420
+ short_tp_indicator_name=None
421
+ )
422
+
423
+ def tp_eval (
424
+ this_ticker_open_positions_side : str,
425
+ lo_row,
426
+ this_ticker_open_trades : List[Dict],
427
+ algo_param : Dict
428
+ ) -> bool:
429
+ '''
430
+ Be very careful, backtest_core 'generic_pnl_eval' may use a) some indicator (tp_indicator_name), or b) target_price to evaluate 'unrealized_pnl_tp'.
431
+ 'tp_eval' only return True or False but it needs be congruent with backtest_core 'generic_pnl_eval', otherwise incorrect rosy pnl may be reported.
432
+ '''
433
+ return generic_tp_eval(lo_row, this_ticker_open_trades)
434
+
435
+ def sort_filter_universe(
436
+ tickers : List[str],
437
+ exchange : Exchange,
438
+
439
+ # Use "i" (row index) to find current/last interval's market data or TAs from "all_exchange_candles"
440
+ i,
441
+ all_exchange_candles : Dict[str, Dict[str, Dict[str, pd.DataFrame]]],
442
+
443
+ max_num_tickers : int = 10
444
+ ) -> List[str]:
445
+ return generic_sort_filter_universe(
446
+ tickers=tickers,
447
+ exchange=exchange,
448
+ i=i,
449
+ all_exchange_candles=all_exchange_candles,
450
+ max_num_tickers=max_num_tickers
451
+ )
452
+
453
+ algo_results : List[Dict] = run_all_scenario(
454
+ algo_params=algo_params,
455
+ exchanges=exchanges,
456
+ order_notional_adj_func=order_notional_adj,
457
+ allow_entry_initial_func=allow_entry_initial,
458
+ allow_entry_final_func=allow_entry_final,
459
+ allow_slice_entry_func=allow_slice_entry,
460
+ sl_adj_func=sl_adj,
461
+ trailing_stop_threshold_eval_func=trailing_stop_threshold_eval,
462
+ pnl_eval_func=pnl_eval,
463
+ tp_eval_func=tp_eval,
464
+ sort_filter_universe_func=sort_filter_universe,
465
+
466
+ logger=logger
467
+ )
468
+
469
+ dump_trades_to_disk(
470
+ algo_results,
471
+ trade_extract_filename,
472
+ logger
473
+ )
siglab_py/constants.py CHANGED
@@ -1,3 +1,28 @@
1
+ import enum
1
2
  from typing import Union, List, Dict, Any
2
3
 
3
- JSON_SERIALIZABLE_TYPES = Union[str, bool, int, float, None, List[Any], Dict[Any, Any]]
4
+ INVALID : int = -1
5
+
6
+ JSON_SERIALIZABLE_TYPES = Union[str, bool, int, float, None, List[Any], Dict[Any, Any]]
7
+
8
+ class LogLevel(enum.Enum):
9
+ CRITICAL = 50
10
+ ERROR = 40
11
+ WARNING = 30
12
+ INFO = 20
13
+ DEBUG = 10
14
+ NOTSET = 0
15
+
16
+ class TrendDirection(enum.Enum):
17
+ UNDEFINED = 0
18
+ HIGHER_HIGHS = 1
19
+ LOWER_HIGHS = 2
20
+ SIDEWAYS = 3
21
+ HIGHER_LOWS = 4
22
+ LOWER_LOWS = 5
23
+
24
+ def to_string(self) -> str:
25
+ return self.name.lower() if self != TrendDirection.UNDEFINED else ''
26
+
27
+ OrderSide = enum.Enum('OrderSide', 'UNDEFINED BUY SELL')
28
+ PositionStatus = enum.Enum("PositionStatus", 'UNDEFINED OPEN CLOSED SL')
@@ -0,0 +1,38 @@
1
+ from typing import Dict, Any
2
+
3
+ import ccxt
4
+ from ccxt.base.types import Balances
5
+ import ccxt.pro as ccxtpro
6
+
7
+ '''
8
+ Why override fetch_balance?
9
+ balances['total'] empty
10
+ But you can find that from exchange raw response under balances['info']['balances'] (verbose=True).
11
+ '''
12
+ def _populate_balance_total_if_missing(
13
+ balances : Dict[str, Any]
14
+ ):
15
+ for ccy_balance in balances['info']['balances']:
16
+ ccy = ccy_balance['asset']
17
+ free = float(ccy_balance.get('free', 0))
18
+ locked = float(ccy_balance.get('locked', 0))
19
+ total = free + locked
20
+ if total!=0 and ccy not in balances['total']:
21
+ balances['total'][ccy] = total
22
+ class Binance(ccxt.binance):
23
+ def __init__(self, *args: Dict[str, Any]) -> None:
24
+ super().__init__(*args) # type: ignore
25
+
26
+ def fetch_balance(self, params={}) -> Balances: # type: ignore
27
+ balances = super().fetch_balance(params=params)
28
+ _populate_balance_total_if_missing(balances)
29
+ return balances
30
+
31
+ class BinanceAsync(ccxtpro.binance):
32
+ def __init__(self, *args: Dict[str, Any]) -> None:
33
+ super().__init__(*args) # type: ignore
34
+
35
+ async def fetch_balance(self, params={}) -> Balances: # type: ignore
36
+ balances = await super().fetch_balance(params=params)
37
+ _populate_balance_total_if_missing(balances)
38
+ return balances
@@ -0,0 +1,83 @@
1
+ from typing import Dict, Any
2
+
3
+ import ccxt
4
+ import ccxt.pro as ccxtpro
5
+
6
+ '''
7
+ Why override load_markets?
8
+ Deribit is one of the OG CEX. If you look at 'contractSize' for BTC/USDC:USDC, a linear perp, for example:
9
+ exchange.markets['BTC/USDC:USDC']['contractSize'] 0.0001
10
+ However, for Deribit, create_order actually expects 'amount' in base ccy, not in "# of contracts" as with most other exchanges supported by CCXT.
11
+ Also note that 'filled' in response from create_order also in base ccy, not in # contracts.
12
+ The general prevailing convention in CCXT is: 'amount' should be quoted in '# contracts'.
13
+ Why CCXT not fix Deribit, so that it follows the prevailing convention? This is because this would be a breaking changes.
14
+ Thus, we override 'contractSize' to 1 for all markets.
15
+
16
+ Additionally, we need to override 'fetch_position' as it swapped 'notional' with 'contracts'!!! Real ugly. Example below.
17
+ 'id' = None
18
+ 'symbol' = 'BTC/USDC:USDC'
19
+ 'timestamp' = None
20
+ 'datetime' = None
21
+ 'lastUpdateTimestamp' = None
22
+ 'initialMargin' = ???
23
+ 'initialMarginPercentage' = ???
24
+ 'maintenanceMargin' = ???
25
+ 'maintenanceMarginPercentage' = ???
26
+ 'entryPrice' = 85657.0
27
+ 'notional' = 0.0009 <-- This is NOT USD! And this is NOT # Contracts! This is # BTC!
28
+ 'leverage' = 50
29
+ 'unrealizedPnl' = ???
30
+ 'realizedPnl' = ???
31
+ 'contracts' = 77.081445 <-- This is NOT "# contracts"! 0.0009 BTC x markPrice 85646.05
32
+ 'contractSize' = 1.0
33
+ 'marginRatio' = None
34
+ 'liquidationPrice' = None
35
+ 'markPrice' = 85646.05 <-- They use 'markPrice' to calc 'contracts'
36
+ 'lastPrice' = None
37
+ 'collateral' = None
38
+ 'marginMode' = None
39
+ 'side' = 'long'
40
+ 'percentage' = None
41
+ 'hedged' = None
42
+ 'stopLossPrice' = None
43
+ 'takeProfitPrice' = None
44
+ '''
45
+ class Deribit(ccxt.deribit):
46
+ def __init__(self, *args: Dict[str, Any]) -> None:
47
+ super().__init__(*args) # type: ignore
48
+
49
+ def load_markets(self, reload=False, params={}):
50
+ self.markets = super().load_markets(reload=reload, params=params)
51
+
52
+ for market in self.markets:
53
+ self.markets[market]['contractSize'] = 1
54
+
55
+ return self.markets
56
+
57
+ def fetch_position(self, symbol: str, params={}): # type: ignore
58
+ position = super().fetch_position(symbol=symbol, params=params)
59
+ pos_usdt = position['contracts']
60
+ pos_baseccy = position['notional']
61
+ position['contracts'] = pos_baseccy
62
+ position['notional'] = pos_usdt
63
+ return position
64
+
65
+ class DeribitAsync(ccxtpro.deribit):
66
+ def __init__(self, *args: Dict[str, Any]) -> None:
67
+ super().__init__(*args) # type: ignore
68
+
69
+ async def load_markets(self, reload=False, params={}):
70
+ self.markets = await super().load_markets(reload=reload, params=params)
71
+
72
+ for market in self.markets:
73
+ self.markets[market]['contractSize'] = 1
74
+
75
+ return self.markets
76
+
77
+ async def fetch_position(self, symbol: str, params={}): # type: ignore
78
+ position = await super().fetch_position(symbol=symbol, params=params)
79
+ pos_usdt = position['contracts']
80
+ pos_baseccy = position['notional']
81
+ position['contracts'] = pos_baseccy
82
+ position['notional'] = pos_usdt
83
+ return position
@@ -4,7 +4,7 @@ https://www.futuhk.com/en/support/categories/909?global_content=%7B%22promote_id
4
4
 
5
5
  Fees: https://www.futuhk.com/en/commissionnew#crypto
6
6
 
7
- Subscribe L2 data: https://openapi.futunn.com/futu-api-doc/en/intro/authority.html#5331
7
+ Subscribe L2 data: https://openapi.futunn.com/futu-api-doc/en/intro/authority.html
8
8
 
9
9
  Investor Protection: https://www.futuhk.com/en
10
10
 
@@ -15,8 +15,17 @@ Margin Trading:
15
15
  Download Futu OpenD
16
16
  https://www.futuhk.com/en/support/topic1_464?global_content=%7B%22promote_id%22%3A13765%2C%22sub_promote_id%22%3A10%7D
17
17
 
18
- If you run the installer version "Futu_OpenD-GUI_9.0.5008_Windows.exe", it'd be installed under:
18
+ If you run the installer version "Futu_OpenD-GUI_9.0.5008_Windows.exe", it'd be installed under C-Drive:
19
19
  C:\\Users\\xxx\\AppData\\Roaming\\Futu_OpenD\\Futu_OpenD.exe
20
+ Unfortunately, log folder also under C-drive as a result, and they are big.
21
+
22
+ For command line version: https://openapi.futunn.com/futu-api-doc/opend/opend-cmd.html
23
+ Binary under downloaded package (You can put it under for example D-drive):
24
+ ...\Futu_OpenD_9.4.5408_Windows\Futu_OpenD_9.4.5408_Windows
25
+
26
+ Put a batch file "start_futu_opend.bat", if login_pwd include special characters, enclose pwd with double quotes:
27
+ FutuOpenD -login_account=1234567 -login_pwd="... Your Secret here ..."
28
+ Config file is "FutuOpenD.xml", you can adjust logging verbosity here.
20
29
 
21
30
  Architecture: https://openapi.futunn.com/futu-api-doc/en/intro/intro.html
22
31
 
@@ -30,6 +39,7 @@ API
30
39
  stock basic info https://openapi.futunn.com/futu-api-doc/en/quote/get-static-info.html
31
40
  historical candles https://openapi.futunn.com/futu-api-doc/en/quote/request-history-kline.html
32
41
  realtime candles https://openapi.futunn.com/futu-api-doc/en/quote/get-kl.html
42
+ orderbook https://openapi.futunn.com/futu-api-doc/en/quote/get-order-book.html
33
43
  real time quote https://openapi.futunn.com/futu-api-doc/en/quote/get-stock-quote.html
34
44
  open orders https://openapi.futunn.com/futu-api-doc/en/trade/get-order-list.html
35
45
  historical orders https://openapi.futunn.com/futu-api-doc/en/trade/get-history-order-list.html