siglab-py 0.5.30__py3-none-any.whl → 0.6.12__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/constants.py +5 -0
- siglab_py/exchanges/binance.py +38 -0
- siglab_py/exchanges/deribit.py +83 -0
- siglab_py/exchanges/futubull.py +11 -2
- siglab_py/market_data_providers/candles_provider.py +2 -2
- siglab_py/market_data_providers/candles_ta_provider.py +3 -3
- siglab_py/market_data_providers/futu_candles_ta_to_csv.py +6 -4
- siglab_py/market_data_providers/google_monitor.py +320 -0
- siglab_py/market_data_providers/orderbooks_provider.py +15 -12
- siglab_py/market_data_providers/tg_monitor.py +6 -2
- siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
- siglab_py/ordergateway/encrypt_keys_util.py +1 -1
- siglab_py/ordergateway/gateway.py +97 -35
- siglab_py/tests/integration/market_data_util_tests.py +34 -0
- siglab_py/tests/unit/analytic_util_tests.py +37 -10
- siglab_py/tests/unit/simple_math_tests.py +235 -0
- siglab_py/tests/unit/trading_util_tests.py +0 -21
- siglab_py/util/analytic_util.py +195 -33
- siglab_py/util/market_data_util.py +177 -59
- siglab_py/util/notification_util.py +1 -1
- siglab_py/util/simple_math.py +240 -0
- siglab_py/util/trading_util.py +0 -12
- {siglab_py-0.5.30.dist-info → siglab_py-0.6.12.dist-info}/METADATA +1 -1
- siglab_py-0.6.12.dist-info/RECORD +44 -0
- {siglab_py-0.5.30.dist-info → siglab_py-0.6.12.dist-info}/WHEEL +1 -1
- siglab_py-0.5.30.dist-info/RECORD +0 -39
- {siglab_py-0.5.30.dist-info → siglab_py-0.6.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from typing import List, Dict, Union
|
|
3
|
+
|
|
4
|
+
from util.simple_math import generate_rand_nums, round_to_level, bucket_series, bucketize_val
|
|
5
|
+
|
|
6
|
+
class SimpleMathTests(unittest.TestCase):
|
|
7
|
+
|
|
8
|
+
def test_generate_rand_nums(self):
|
|
9
|
+
range_min : float = 0
|
|
10
|
+
range_max : float = 1
|
|
11
|
+
size : int = 100
|
|
12
|
+
percentage_in_range : float = 91
|
|
13
|
+
abs_min : float = -0.5
|
|
14
|
+
abs_max : float = 1.1
|
|
15
|
+
|
|
16
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
17
|
+
range_min = range_min,
|
|
18
|
+
range_max = range_max,
|
|
19
|
+
size = size,
|
|
20
|
+
percent_in_range = percentage_in_range,
|
|
21
|
+
abs_min = abs_min,
|
|
22
|
+
abs_max = abs_max
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert(len(rand_nums)==size)
|
|
26
|
+
assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
|
|
27
|
+
assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
range_min = -1
|
|
31
|
+
range_max = 1
|
|
32
|
+
percentage_in_range = 91
|
|
33
|
+
abs_min = -1.5
|
|
34
|
+
abs_max = 1.5
|
|
35
|
+
|
|
36
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
37
|
+
range_min = range_min,
|
|
38
|
+
range_max = range_max,
|
|
39
|
+
size = size,
|
|
40
|
+
percent_in_range = percentage_in_range,
|
|
41
|
+
abs_min = abs_min,
|
|
42
|
+
abs_max = abs_max
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert(len(rand_nums)==size)
|
|
46
|
+
assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
|
|
47
|
+
assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
range_min = 0
|
|
51
|
+
range_max = 100
|
|
52
|
+
percentage_in_range = 91
|
|
53
|
+
abs_min = -150
|
|
54
|
+
abs_max = 150
|
|
55
|
+
|
|
56
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
57
|
+
range_min = range_min,
|
|
58
|
+
range_max = range_max,
|
|
59
|
+
size = size,
|
|
60
|
+
percent_in_range = percentage_in_range,
|
|
61
|
+
abs_min = abs_min,
|
|
62
|
+
abs_max = abs_max
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert(len(rand_nums)==size)
|
|
66
|
+
assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
|
|
67
|
+
assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
range_min = -100
|
|
71
|
+
range_max = 100
|
|
72
|
+
percentage_in_range = 91
|
|
73
|
+
abs_min = -150
|
|
74
|
+
abs_max = 150
|
|
75
|
+
|
|
76
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
77
|
+
range_min = range_min,
|
|
78
|
+
range_max = range_max,
|
|
79
|
+
size = size,
|
|
80
|
+
percent_in_range = percentage_in_range,
|
|
81
|
+
abs_min = abs_min,
|
|
82
|
+
abs_max = abs_max
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert(len(rand_nums)==size)
|
|
86
|
+
assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
|
|
87
|
+
assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
|
|
88
|
+
|
|
89
|
+
def test_round_to_level(self):
|
|
90
|
+
prices = [
|
|
91
|
+
{ 'price' : 15080, 'rounded' : 15000},
|
|
92
|
+
{ 'price' : 15180, 'rounded' : 15200},
|
|
93
|
+
{ 'price' : 25080, 'rounded' : 25200},
|
|
94
|
+
{ 'price' : 25180, 'rounded' : 25200},
|
|
95
|
+
{ 'price' : 25380, 'rounded' : 25500},
|
|
96
|
+
{ 'price' : 95332, 'rounded' : 95000},
|
|
97
|
+
{ 'price' : 95878, 'rounded' : 96000},
|
|
98
|
+
{ 'price' : 103499, 'rounded' : 103000},
|
|
99
|
+
{ 'price' : 103500, 'rounded' : 104000},
|
|
100
|
+
{ 'price' : 150800, 'rounded' : 150000},
|
|
101
|
+
{ 'price' : 151800, 'rounded' : 152000}
|
|
102
|
+
]
|
|
103
|
+
for entry in prices:
|
|
104
|
+
price = entry['price']
|
|
105
|
+
expected = entry['rounded']
|
|
106
|
+
rounded_price = round_to_level(price, level_granularity=0.01)
|
|
107
|
+
print(f"{price} rounded to: {rounded_price}")
|
|
108
|
+
assert(rounded_price==expected)
|
|
109
|
+
|
|
110
|
+
def test_bucket_series(self):
|
|
111
|
+
|
|
112
|
+
level_granularity : float = 0.1
|
|
113
|
+
|
|
114
|
+
range_min : float = 0
|
|
115
|
+
range_max : float = 1
|
|
116
|
+
size : int = 100
|
|
117
|
+
percentage_in_range : float = 91
|
|
118
|
+
abs_min : float = -0.5
|
|
119
|
+
abs_max : float = 1.1
|
|
120
|
+
|
|
121
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
122
|
+
range_min = range_min,
|
|
123
|
+
range_max = range_max,
|
|
124
|
+
size = size,
|
|
125
|
+
percent_in_range = percentage_in_range,
|
|
126
|
+
abs_min = abs_min,
|
|
127
|
+
abs_max = abs_max
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
buckets : Dict[
|
|
131
|
+
str,
|
|
132
|
+
Dict[str,Union[float, List[float]]]
|
|
133
|
+
] = bucket_series(
|
|
134
|
+
values = rand_nums,
|
|
135
|
+
outlier_threshold_percent = 10,
|
|
136
|
+
level_granularity=level_granularity
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
bucketized = []
|
|
140
|
+
for num in rand_nums:
|
|
141
|
+
bucketized.append(
|
|
142
|
+
bucketize_val(num, buckets=buckets)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
range_min = -1
|
|
147
|
+
range_max = 1
|
|
148
|
+
size : int = 100
|
|
149
|
+
percentage_in_range = 91
|
|
150
|
+
abs_min = -1.5
|
|
151
|
+
abs_max = 1.5
|
|
152
|
+
|
|
153
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
154
|
+
range_min = range_min,
|
|
155
|
+
range_max = range_max,
|
|
156
|
+
size = size,
|
|
157
|
+
percent_in_range = percentage_in_range,
|
|
158
|
+
abs_min = abs_min,
|
|
159
|
+
abs_max = abs_max
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
buckets = bucket_series(
|
|
163
|
+
values = rand_nums,
|
|
164
|
+
outlier_threshold_percent = 10,
|
|
165
|
+
level_granularity=level_granularity
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
range_min = 0
|
|
170
|
+
range_max = 100
|
|
171
|
+
size : int = 100
|
|
172
|
+
percentage_in_range = 91
|
|
173
|
+
abs_min = -0.5
|
|
174
|
+
abs_max = 150
|
|
175
|
+
|
|
176
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
177
|
+
range_min = range_min,
|
|
178
|
+
range_max = range_max,
|
|
179
|
+
size = size,
|
|
180
|
+
percent_in_range = percentage_in_range,
|
|
181
|
+
abs_min = abs_min,
|
|
182
|
+
abs_max = abs_max
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
buckets = bucket_series(
|
|
186
|
+
values = rand_nums,
|
|
187
|
+
outlier_threshold_percent = 10,
|
|
188
|
+
level_granularity=level_granularity
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
range_min = -100
|
|
193
|
+
range_max = 100
|
|
194
|
+
size : int = 100
|
|
195
|
+
percentage_in_range = 91
|
|
196
|
+
abs_min = -150
|
|
197
|
+
abs_max = 150
|
|
198
|
+
|
|
199
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
200
|
+
range_min = range_min,
|
|
201
|
+
range_max = range_max,
|
|
202
|
+
size = size,
|
|
203
|
+
percent_in_range = percentage_in_range,
|
|
204
|
+
abs_min = abs_min,
|
|
205
|
+
abs_max = abs_max
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
buckets = bucket_series(
|
|
209
|
+
values = rand_nums,
|
|
210
|
+
outlier_threshold_percent = 10,
|
|
211
|
+
level_granularity=level_granularity
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
range_min = 20_000
|
|
216
|
+
range_max = 120_000
|
|
217
|
+
size : int = 100
|
|
218
|
+
percentage_in_range = 91
|
|
219
|
+
abs_min = 15_000
|
|
220
|
+
abs_max = 130_000
|
|
221
|
+
|
|
222
|
+
rand_nums : List[float] = generate_rand_nums(
|
|
223
|
+
range_min = range_min,
|
|
224
|
+
range_max = range_max,
|
|
225
|
+
size = size,
|
|
226
|
+
percent_in_range = percentage_in_range,
|
|
227
|
+
abs_min = abs_min,
|
|
228
|
+
abs_max = abs_max
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
buckets = bucket_series(
|
|
232
|
+
values = rand_nums,
|
|
233
|
+
outlier_threshold_percent = 10,
|
|
234
|
+
level_granularity=level_granularity
|
|
235
|
+
)
|
|
@@ -63,24 +63,3 @@ class TradingUtilTests(unittest.TestCase):
|
|
|
63
63
|
default_effective_tp_trailing_percent = default_effective_tp_trailing_percent
|
|
64
64
|
)
|
|
65
65
|
assert(effective_tp_trailing_percent==0) # Most tight trailing SL
|
|
66
|
-
|
|
67
|
-
def test_round_to_level(self):
|
|
68
|
-
prices = [
|
|
69
|
-
{ 'price' : 15080, 'rounded' : 15000},
|
|
70
|
-
{ 'price' : 15180, 'rounded' : 15200},
|
|
71
|
-
{ 'price' : 25080, 'rounded' : 25200},
|
|
72
|
-
{ 'price' : 25180, 'rounded' : 25200},
|
|
73
|
-
{ 'price' : 25380, 'rounded' : 25500},
|
|
74
|
-
{ 'price' : 95332, 'rounded' : 95000},
|
|
75
|
-
{ 'price' : 95878, 'rounded' : 96000},
|
|
76
|
-
{ 'price' : 103499, 'rounded' : 103000},
|
|
77
|
-
{ 'price' : 103500, 'rounded' : 104000},
|
|
78
|
-
{ 'price' : 150800, 'rounded' : 150000},
|
|
79
|
-
{ 'price' : 151800, 'rounded' : 152000}
|
|
80
|
-
]
|
|
81
|
-
for entry in prices:
|
|
82
|
-
price = entry['price']
|
|
83
|
-
expected = entry['rounded']
|
|
84
|
-
rounded_price = round_to_level(price, level_granularity=0.01)
|
|
85
|
-
print(f"{price} rounded to: {rounded_price}")
|
|
86
|
-
assert(rounded_price==expected)
|
siglab_py/util/analytic_util.py
CHANGED
|
@@ -11,9 +11,48 @@ from hurst import compute_Hc # compatible with pypy
|
|
|
11
11
|
from ccxt.base.exchange import Exchange as CcxtExchange
|
|
12
12
|
from ccxt import deribit
|
|
13
13
|
|
|
14
|
+
from siglab_py.util.simple_math import bucket_series, bucketize_val
|
|
14
15
|
from siglab_py.util.market_data_util import fix_column_types
|
|
15
16
|
from siglab_py.constants import TrendDirection
|
|
16
17
|
|
|
18
|
+
def classify_candle(
|
|
19
|
+
candle : pd.Series,
|
|
20
|
+
min_candle_height_ratio : float = 5,
|
|
21
|
+
distance_from_mid_doji_threshold_bps : float = 10
|
|
22
|
+
) -> Union[str, None]:
|
|
23
|
+
candle_class : Union[str, None] = None
|
|
24
|
+
open = candle['open']
|
|
25
|
+
high = candle['high']
|
|
26
|
+
low = candle['low']
|
|
27
|
+
close = candle['close']
|
|
28
|
+
candle_full_height = high - low # always positive
|
|
29
|
+
candle_body_height = close - open # can be negative
|
|
30
|
+
candle_full_mid = (high + low)/2
|
|
31
|
+
candle_body_mid = (open + close)/2
|
|
32
|
+
distance_from_mid_bps = (candle_full_mid/candle_body_mid -1)*10000 if candle_full_mid>candle_body_mid else (candle_body_mid/candle_full_mid -1)*10000
|
|
33
|
+
|
|
34
|
+
candle_height_ratio = candle_full_height / abs(candle_body_height) if candle_body_height!=0 else float('inf')
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
candle_height_ratio>=min_candle_height_ratio
|
|
38
|
+
and close>low
|
|
39
|
+
):
|
|
40
|
+
candle_class = 'hammer'
|
|
41
|
+
elif (
|
|
42
|
+
candle_height_ratio>=min_candle_height_ratio
|
|
43
|
+
and close<high
|
|
44
|
+
):
|
|
45
|
+
candle_class = 'shooting_star'
|
|
46
|
+
elif(
|
|
47
|
+
candle_height_ratio>=min_candle_height_ratio
|
|
48
|
+
and distance_from_mid_bps<=distance_from_mid_doji_threshold_bps
|
|
49
|
+
):
|
|
50
|
+
candle_class = 'doji'
|
|
51
|
+
|
|
52
|
+
# Keep add more ...
|
|
53
|
+
|
|
54
|
+
return candle_class
|
|
55
|
+
|
|
17
56
|
# Fibonacci
|
|
18
57
|
MAGIC_FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.00, 1.618, 2.618, 3.618, 4.236]
|
|
19
58
|
|
|
@@ -83,14 +122,15 @@ def trend_from_lows(series: np.ndarray) -> float:
|
|
|
83
122
|
'''
|
|
84
123
|
compute_candles_stats will calculate typical/basic technical indicators using in many trading strategies:
|
|
85
124
|
a. Basic SMA/EMAs (And slopes)
|
|
86
|
-
b.
|
|
87
|
-
c.
|
|
88
|
-
d.
|
|
89
|
-
e.
|
|
90
|
-
f.
|
|
91
|
-
g.
|
|
92
|
-
h.
|
|
93
|
-
i.
|
|
125
|
+
b. EMA crosses
|
|
126
|
+
c. ATR
|
|
127
|
+
d. Boillenger bands (Yes incorrect spelling sorry)
|
|
128
|
+
e. FVG
|
|
129
|
+
f. Hurst Exponent
|
|
130
|
+
g. RSI, MFI
|
|
131
|
+
h. MACD
|
|
132
|
+
i. Fibonacci
|
|
133
|
+
j. Inflections points: where 'close' crosses EMA from above or below.
|
|
94
134
|
|
|
95
135
|
Parameters:
|
|
96
136
|
a. boillenger_std_multiples: For boillenger upper and lower calc
|
|
@@ -119,7 +159,14 @@ def compute_candles_stats(
|
|
|
119
159
|
target_fib_level : float = 0.618,
|
|
120
160
|
pypy_compat : bool = True
|
|
121
161
|
):
|
|
162
|
+
BUCKETS_m0_100 = bucket_series(
|
|
163
|
+
values=list([i for i in range(0,100)]),
|
|
164
|
+
outlier_threshold_percent=10,
|
|
165
|
+
level_granularity=0.1
|
|
166
|
+
)
|
|
167
|
+
|
|
122
168
|
pd_candles['candle_height'] = pd_candles['high'] - pd_candles['low']
|
|
169
|
+
pd_candles['candle_body_height'] = pd_candles['close'] - pd_candles['open']
|
|
123
170
|
|
|
124
171
|
'''
|
|
125
172
|
market_data_gizmo inserted dummy lines --> Need exclude those or "TypeError: unorderable types for comparison": pd_btc_candles = pd_btc_candles[pd_btc_candles.close.notnull()]
|
|
@@ -136,12 +183,13 @@ def compute_candles_stats(
|
|
|
136
183
|
|
|
137
184
|
pd_candles['is_green'] = pd_candles['close'] >= pd_candles['open']
|
|
138
185
|
|
|
186
|
+
pd_candles['candle_class'] = pd_candles.apply(lambda row: classify_candle(row), axis=1) # type: ignore
|
|
187
|
+
|
|
139
188
|
close_short_periods_rolling = pd_candles['close'].rolling(window=int(sliding_window_how_many_candles/slow_fast_interval_ratio))
|
|
140
189
|
close_long_periods_rolling = pd_candles['close'].rolling(window=sliding_window_how_many_candles)
|
|
141
190
|
close_short_periods_ewm = pd_candles['close'].ewm(span=int(sliding_window_how_many_candles/slow_fast_interval_ratio), adjust=False)
|
|
142
191
|
close_long_periods_ewm = pd_candles['close'].ewm(span=sliding_window_how_many_candles, adjust=False)
|
|
143
192
|
|
|
144
|
-
|
|
145
193
|
pd_candles['pct_change_close'] = pd_candles['close'].pct_change() * 100
|
|
146
194
|
pd_candles['sma_short_periods'] = close_short_periods_rolling.mean()
|
|
147
195
|
pd_candles['sma_long_periods'] = close_long_periods_rolling.mean()
|
|
@@ -157,6 +205,9 @@ def compute_candles_stats(
|
|
|
157
205
|
pd_candles['candle_height_percent'] = pd_candles['candle_height'] / pd_candles['ema_close'] * 100
|
|
158
206
|
pd_candles['candle_height_percent_rounded'] = pd_candles['candle_height_percent'].round().astype('Int64')
|
|
159
207
|
|
|
208
|
+
pd_candles['candle_body_height_percent'] = pd_candles['candle_body_height'] / pd_candles['ema_close'] * 100
|
|
209
|
+
pd_candles['candle_body_height_percent_rounded'] = pd_candles['candle_body_height_percent'].round().astype('Int64')
|
|
210
|
+
|
|
160
211
|
'''
|
|
161
212
|
To annualize volatility:
|
|
162
213
|
if candle_interval == '1m':
|
|
@@ -168,6 +219,8 @@ def compute_candles_stats(
|
|
|
168
219
|
pd_candles['annualized_volatility'] = (
|
|
169
220
|
pd_candles['interval_historical_volatility'] * annualization_factor
|
|
170
221
|
)
|
|
222
|
+
|
|
223
|
+
Why log return? Trading Dude https://python.plainenglish.io/stop-using-percentage-returns-logarithmic-returns-explained-with-code-64a4634b883a
|
|
171
224
|
'''
|
|
172
225
|
pd_candles['log_return'] = np.log(pd_candles['close'] / pd_candles['close'].shift(1))
|
|
173
226
|
pd_candles['interval_hist_vol'] = pd_candles['log_return'].rolling(window=sliding_window_how_many_candles).std()
|
|
@@ -178,7 +231,6 @@ def compute_candles_stats(
|
|
|
178
231
|
annualization_factor = np.sqrt(candles_per_year)
|
|
179
232
|
pd_candles['annualized_hist_vol'] = pd_candles['interval_hist_vol'] * annualization_factor
|
|
180
233
|
|
|
181
|
-
|
|
182
234
|
pd_candles['chop_against_ema'] = (
|
|
183
235
|
(~pd_candles['is_green'] & (pd_candles['close'] > pd_candles['ema_close'])) | # Case 1: Green candle and close > EMA
|
|
184
236
|
(pd_candles['is_green'] & (pd_candles['close'] < pd_candles['ema_close'])) # Case 2: Red candle and close < EMA
|
|
@@ -199,22 +251,29 @@ def compute_candles_stats(
|
|
|
199
251
|
bearish_ema_crosses = (ema_short_periods_prev >= ema_long_periods_prev) & (ema_short_periods_curr < ema_long_periods_curr)
|
|
200
252
|
pd_candles.loc[bullish_ema_crosses, 'ema_cross'] = 1
|
|
201
253
|
pd_candles.loc[bearish_ema_crosses, 'ema_cross'] = -1
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
pd_candles['
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
pd_candles['ema_bullish_cross_last_id'].
|
|
216
|
-
|
|
217
|
-
|
|
254
|
+
bullish_indices = pd.Series(pd_candles.index.where(pd_candles['ema_cross'] == 1), index=pd_candles.index).astype('Int64')
|
|
255
|
+
bearish_indices = pd.Series(pd_candles.index.where(pd_candles['ema_cross'] == -1), index=pd_candles.index).astype('Int64')
|
|
256
|
+
pd_candles['ema_bullish_cross_last_id'] = bullish_indices.rolling(window=pd_candles.shape[0], min_periods=1).max().astype('Int64')
|
|
257
|
+
pd_candles['ema_bearish_cross_last_id'] = bearish_indices.rolling(window=pd_candles.shape[0], min_periods=1).max().astype('Int64')
|
|
258
|
+
conditions = [
|
|
259
|
+
(pd_candles['ema_bullish_cross_last_id'].notna() &
|
|
260
|
+
pd_candles['ema_bearish_cross_last_id'].notna() &
|
|
261
|
+
(pd_candles['ema_bullish_cross_last_id'] > pd_candles['ema_bearish_cross_last_id'])),
|
|
262
|
+
|
|
263
|
+
(pd_candles['ema_bullish_cross_last_id'].notna() &
|
|
264
|
+
pd_candles['ema_bearish_cross_last_id'].notna() &
|
|
265
|
+
(pd_candles['ema_bearish_cross_last_id'] > pd_candles['ema_bullish_cross_last_id'])),
|
|
266
|
+
|
|
267
|
+
(pd_candles['ema_bullish_cross_last_id'].notna() &
|
|
268
|
+
pd_candles['ema_bearish_cross_last_id'].isna()),
|
|
269
|
+
|
|
270
|
+
(pd_candles['ema_bearish_cross_last_id'].notna() &
|
|
271
|
+
pd_candles['ema_bullish_cross_last_id'].isna())
|
|
272
|
+
]
|
|
273
|
+
choices = ['bullish', 'bearish', 'bullish', 'bearish']
|
|
274
|
+
pd_candles['ema_cross_last'] = np.select(conditions, choices, default=None) # type: ignore
|
|
275
|
+
pd_candles.loc[bullish_ema_crosses, 'ema_cross'] = 'bullish'
|
|
276
|
+
pd_candles.loc[bearish_ema_crosses, 'ema_cross'] = 'bearish'
|
|
218
277
|
|
|
219
278
|
pd_candles['max_short_periods'] = close_short_periods_rolling.max()
|
|
220
279
|
pd_candles['max_long_periods'] = close_long_periods_rolling.max()
|
|
@@ -226,6 +285,11 @@ def compute_candles_stats(
|
|
|
226
285
|
pd_candles['idmin_short_periods'] = close_short_periods_rolling.apply(lambda x : x.idxmin())
|
|
227
286
|
pd_candles['idmin_long_periods'] = close_long_periods_rolling.apply(lambda x : x.idxmin())
|
|
228
287
|
|
|
288
|
+
pd_candles['max_candle_body_height_percent_long_periods'] = pd_candles['candle_body_height_percent'].rolling(window=sliding_window_how_many_candles).max()
|
|
289
|
+
pd_candles['idmax_candle_body_height_percent_long_periods'] = pd_candles['candle_body_height_percent'].rolling(window=sliding_window_how_many_candles).apply(lambda x : x.idxmax())
|
|
290
|
+
pd_candles['min_candle_body_height_percent_long_periods'] = pd_candles['candle_body_height_percent'].rolling(window=sliding_window_how_many_candles).min()
|
|
291
|
+
pd_candles['idmin_candle_body_height_percent_long_periods'] = pd_candles['candle_body_height_percent'].rolling(window=sliding_window_how_many_candles).apply(lambda x : x.idxmin())
|
|
292
|
+
|
|
229
293
|
pd_candles['price_swing_short_periods'] = np.where(
|
|
230
294
|
pd_candles['idmax_short_periods'] > pd_candles['idmin_short_periods'],
|
|
231
295
|
pd_candles['max_short_periods'] - pd_candles['min_short_periods'], # Up swing
|
|
@@ -311,14 +375,14 @@ def compute_candles_stats(
|
|
|
311
375
|
first_breach_index = aggressive_mask.idxmax()
|
|
312
376
|
candle_high = pd_candles.at[first_breach_index, 'high']
|
|
313
377
|
candle_low = pd_candles.at[first_breach_index, 'low']
|
|
314
|
-
candle_height = candle_high - candle_low
|
|
378
|
+
candle_height = candle_high - candle_low # type: ignore
|
|
315
379
|
else:
|
|
316
380
|
aggressive_mask = window['close'] <= window['boillenger_lower_agg']
|
|
317
381
|
if aggressive_mask.any():
|
|
318
382
|
first_breach_index = aggressive_mask.idxmax()
|
|
319
383
|
candle_high = pd_candles.at[first_breach_index, 'high']
|
|
320
384
|
candle_low = pd_candles.at[first_breach_index, 'low']
|
|
321
|
-
candle_height = candle_high - candle_low
|
|
385
|
+
candle_height = candle_high - candle_low # type: ignore
|
|
322
386
|
|
|
323
387
|
return {
|
|
324
388
|
'aggressive_move': aggressive_mask.any(),
|
|
@@ -423,10 +487,13 @@ def compute_candles_stats(
|
|
|
423
487
|
mitigated = pd_candles.iloc[idx + 1:row.name]['close'].lt(row['fvg_high']).any()
|
|
424
488
|
return mitigated
|
|
425
489
|
|
|
426
|
-
pd_candles['fvg_mitigated'] = pd_candles.apply(lambda row: compute_fvg_mitigated(row, pd_candles), axis=1)
|
|
490
|
+
pd_candles['fvg_mitigated'] = pd_candles.apply(lambda row: compute_fvg_mitigated(row, pd_candles), axis=1) # type: ignore
|
|
427
491
|
|
|
428
|
-
|
|
429
|
-
|
|
492
|
+
'''
|
|
493
|
+
RSI
|
|
494
|
+
Divergences from Bybit Learn https://www.youtube.com/watch?v=G9oUTi-PI18&t=809s
|
|
495
|
+
RSI Reversals from BK Traders https://www.youtube.com/watch?v=MvkbrHjiQlI
|
|
496
|
+
'''
|
|
430
497
|
pd_candles.loc[:,'close_delta'] = pd_candles['close'].diff()
|
|
431
498
|
pd_candles.loc[:,'close_delta_percent'] = pd_candles['close'].pct_change()
|
|
432
499
|
lo_up = pd_candles['close_delta'].clip(lower=0)
|
|
@@ -452,6 +519,7 @@ def compute_candles_stats(
|
|
|
452
519
|
|
|
453
520
|
lo_rs = lo_ma_up / lo_ma_down
|
|
454
521
|
pd_candles.loc[:,'rsi'] = 100 - (100/(1 + lo_rs))
|
|
522
|
+
pd_candles['rsi_bucket'] = pd_candles['rsi'].apply(lambda x: bucketize_val(x, buckets=BUCKETS_m0_100))
|
|
455
523
|
pd_candles['ema_rsi'] = pd_candles['rsi'].ewm(
|
|
456
524
|
span=rsi_sliding_window_how_many_candles,
|
|
457
525
|
adjust=False).mean()
|
|
@@ -513,13 +581,43 @@ def compute_candles_stats(
|
|
|
513
581
|
rsi_sliding_window_how_many_candles if rsi_sliding_window_how_many_candles else sliding_window_how_many_candles).sum()
|
|
514
582
|
pd_candles['money_flow_ratio'] = pd_candles['positive_flow_sum'] / pd_candles['negative_flow_sum']
|
|
515
583
|
pd_candles['mfi'] = 100 - (100 / (1 + pd_candles['money_flow_ratio']))
|
|
584
|
+
pd_candles['mfi_bucket'] = pd_candles['mfi'].apply(lambda x: bucketize_val(x, buckets=BUCKETS_m0_100))
|
|
516
585
|
|
|
517
586
|
|
|
518
587
|
# MACD https://www.investopedia.com/terms/m/macd.asp
|
|
519
588
|
# https://www.youtube.com/watch?v=jmPCL3l08ss
|
|
520
589
|
pd_candles['macd'] = pd_candles['ema_short_periods'] - pd_candles['ema_long_periods']
|
|
521
590
|
pd_candles['signal'] = pd_candles['macd'].ewm(span=int(sliding_window_how_many_candles/slow_fast_interval_ratio), adjust=False).mean()
|
|
522
|
-
pd_candles['macd_minus_signal'] = pd_candles['macd'] - pd_candles['signal']
|
|
591
|
+
pd_candles['macd_minus_signal'] = pd_candles['macd'] - pd_candles['signal'] # MACD histogram
|
|
592
|
+
macd_cur = pd_candles['macd_minus_signal']
|
|
593
|
+
macd_prev = pd_candles['macd_minus_signal'].shift(1)
|
|
594
|
+
bullish_macd_crosses = (macd_prev < 0) & (macd_cur > 0)
|
|
595
|
+
bearish_macd_crosses = (macd_prev > 0) & (macd_cur < 0)
|
|
596
|
+
pd_candles.loc[bullish_macd_crosses, 'macd_cross'] = 1
|
|
597
|
+
pd_candles.loc[bearish_macd_crosses, 'macd_cross'] = -1
|
|
598
|
+
bullish_indices = pd.Series(pd_candles.index.where(pd_candles['macd_cross'] == 1), index=pd_candles.index).astype('Int64')
|
|
599
|
+
bearish_indices = pd.Series(pd_candles.index.where(pd_candles['macd_cross'] == -1), index=pd_candles.index).astype('Int64')
|
|
600
|
+
pd_candles['macd_bullish_cross_last_id'] = bullish_indices.rolling(window=pd_candles.shape[0], min_periods=1).max().astype('Int64')
|
|
601
|
+
pd_candles['macd_bearish_cross_last_id'] = bearish_indices.rolling(window=pd_candles.shape[0], min_periods=1).max().astype('Int64')
|
|
602
|
+
conditions = [
|
|
603
|
+
(pd_candles['macd_bullish_cross_last_id'].notna() &
|
|
604
|
+
pd_candles['macd_bearish_cross_last_id'].notna() &
|
|
605
|
+
(pd_candles['macd_bullish_cross_last_id'] > pd_candles['macd_bearish_cross_last_id'])),
|
|
606
|
+
|
|
607
|
+
(pd_candles['macd_bullish_cross_last_id'].notna() &
|
|
608
|
+
pd_candles['macd_bearish_cross_last_id'].notna() &
|
|
609
|
+
(pd_candles['macd_bearish_cross_last_id'] > pd_candles['macd_bullish_cross_last_id'])),
|
|
610
|
+
|
|
611
|
+
(pd_candles['macd_bullish_cross_last_id'].notna() &
|
|
612
|
+
pd_candles['macd_bearish_cross_last_id'].isna()),
|
|
613
|
+
|
|
614
|
+
(pd_candles['macd_bearish_cross_last_id'].notna() &
|
|
615
|
+
pd_candles['macd_bullish_cross_last_id'].isna())
|
|
616
|
+
]
|
|
617
|
+
choices = ['bullish', 'bearish', 'bullish', 'bearish']
|
|
618
|
+
pd_candles['macd_cross_last'] = np.select(conditions, choices, default=None) # type: ignore
|
|
619
|
+
pd_candles.loc[bullish_macd_crosses, 'macd_cross'] = 'bullish'
|
|
620
|
+
pd_candles.loc[bearish_macd_crosses, 'macd_cross'] = 'bearish'
|
|
523
621
|
|
|
524
622
|
if not pypy_compat:
|
|
525
623
|
calculate_slope(
|
|
@@ -568,7 +666,7 @@ def compute_candles_stats(
|
|
|
568
666
|
pd_data=pd_candles,
|
|
569
667
|
src_col_name='ema_rsi',
|
|
570
668
|
slope_col_name='ema_rsi_slope',
|
|
571
|
-
sliding_window_how_many_candles=int(
|
|
669
|
+
sliding_window_how_many_candles=int(rsi_trend_sliding_window_how_many_candles)
|
|
572
670
|
)
|
|
573
671
|
|
|
574
672
|
pd_candles['regular_divergence'] = (
|
|
@@ -591,6 +689,8 @@ def compute_candles_stats(
|
|
|
591
689
|
|
|
592
690
|
# Inflection points
|
|
593
691
|
pd_candles['gap_close_vs_ema'] = pd_candles['close'] - pd_candles['ema_long_periods']
|
|
692
|
+
pd_candles['gap_close_vs_ema_percent'] = pd_candles['gap_close_vs_ema']/pd_candles['close'] *100
|
|
693
|
+
|
|
594
694
|
pd_candles['close_above_or_below_ema'] = None
|
|
595
695
|
pd_candles.loc[pd_candles['gap_close_vs_ema'] > 0, 'close_above_or_below_ema'] = 'above'
|
|
596
696
|
pd_candles.loc[pd_candles['gap_close_vs_ema'] < 0, 'close_above_or_below_ema'] = 'below'
|
|
@@ -600,6 +700,68 @@ def compute_candles_stats(
|
|
|
600
700
|
'close_vs_ema_inflection'
|
|
601
701
|
] = np.sign(pd_candles['close'] - pd_candles['ema_long_periods'])
|
|
602
702
|
|
|
703
|
+
def lookup_fib_target(
|
|
704
|
+
row,
|
|
705
|
+
pd_candles,
|
|
706
|
+
target_fib_level : float = 0.618
|
|
707
|
+
) -> Union[Dict, None]:
|
|
708
|
+
if row is None:
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
fib_target_short_periods = None
|
|
712
|
+
fib_target_long_periods = None
|
|
713
|
+
|
|
714
|
+
max_short_periods = row['max_short_periods']
|
|
715
|
+
idmax_short_periods = int(row['idmax_short_periods']) if not math.isnan(row['idmax_short_periods']) else None
|
|
716
|
+
max_long_periods = row['max_long_periods']
|
|
717
|
+
idmax_long_periods = int(row['idmax_long_periods']) if not math.isnan(row['idmax_long_periods']) else None
|
|
718
|
+
|
|
719
|
+
min_short_periods = row['min_short_periods']
|
|
720
|
+
idmin_short_periods = int(row['idmin_short_periods']) if not math.isnan(row['idmin_short_periods']) else None
|
|
721
|
+
min_long_periods = row['min_long_periods']
|
|
722
|
+
idmin_long_periods = int(row['idmin_long_periods']) if not math.isnan(row['idmin_long_periods']) else None
|
|
723
|
+
|
|
724
|
+
if idmax_short_periods and idmin_short_periods and idmax_short_periods>0 and idmin_short_periods>0:
|
|
725
|
+
if idmax_short_periods>idmin_short_periods and idmax_short_periods < len(pd_candles):
|
|
726
|
+
# Falling from prev peak
|
|
727
|
+
last_peak = pd_candles.iloc[idmax_short_periods]
|
|
728
|
+
fib_target_short_periods = last_peak[f'fib_{target_fib_level}_short_periods'] if not math.isnan(last_peak[f'fib_{target_fib_level}_short_periods']) else None
|
|
729
|
+
|
|
730
|
+
else:
|
|
731
|
+
# Bouncing from prev bottom
|
|
732
|
+
if idmin_short_periods < len(pd_candles):
|
|
733
|
+
last_bottom = pd_candles.iloc[idmin_short_periods]
|
|
734
|
+
fib_target_short_periods = last_bottom[f'fib_{target_fib_level}_short_periods'] if not math.isnan(last_bottom[f'fib_{target_fib_level}_short_periods']) else None
|
|
735
|
+
|
|
736
|
+
if idmax_long_periods and idmin_long_periods and idmax_long_periods>0 and idmin_long_periods>0:
|
|
737
|
+
if idmax_long_periods>idmin_long_periods and idmax_long_periods < len(pd_candles):
|
|
738
|
+
# Falling from prev peak
|
|
739
|
+
last_peak = pd_candles.iloc[idmax_long_periods]
|
|
740
|
+
fib_target_long_periods = last_peak[f'fib_{target_fib_level}_long_periods'] if not math.isnan(last_peak[f'fib_{target_fib_level}_long_periods']) else None
|
|
741
|
+
|
|
742
|
+
else:
|
|
743
|
+
# Bouncing from prev bottom
|
|
744
|
+
if idmin_long_periods < len(pd_candles):
|
|
745
|
+
last_bottom = pd_candles.iloc[idmin_long_periods]
|
|
746
|
+
fib_target_long_periods = last_bottom[f'fib_{target_fib_level}_long_periods'] if not math.isnan(last_bottom[f'fib_{target_fib_level}_long_periods']) else None
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
'short_periods' : {
|
|
750
|
+
'idmin' : idmin_short_periods,
|
|
751
|
+
'idmax' : idmax_short_periods,
|
|
752
|
+
'min' : min_short_periods,
|
|
753
|
+
'max' : max_short_periods,
|
|
754
|
+
'fib_target' : fib_target_short_periods,
|
|
755
|
+
},
|
|
756
|
+
'long_periods' : {
|
|
757
|
+
'idmin' : idmin_long_periods,
|
|
758
|
+
'idmax' : idmax_long_periods,
|
|
759
|
+
'min' : min_long_periods,
|
|
760
|
+
'max' : max_long_periods,
|
|
761
|
+
'fib_target' : fib_target_long_periods
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
603
765
|
'''
|
|
604
766
|
The implementation from Geeksforgeeks https://www.geeksforgeeks.org/find-indices-of-all-local-maxima-and-local-minima-in-an-array/ is wrong.
|
|
605
767
|
If you have consecutive-duplicates, things will gall apart!
|