trendvisdata 1.0.4__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.
- trendvisdata/__init__.py +5 -0
- trendvisdata/chart_data.py +400 -0
- trendvisdata/chart_prep.py +637 -0
- trendvisdata/market_data.py +541 -0
- trendvisdata/sector_mappings.py +666 -0
- trendvisdata/trend.py +316 -0
- trendvisdata/trend_data.py +785 -0
- trendvisdata/trend_params.py +197 -0
- trendvisdata-1.0.4.dist-info/LICENSE +21 -0
- trendvisdata-1.0.4.dist-info/METADATA +29 -0
- trendvisdata-1.0.4.dist-info/RECORD +13 -0
- trendvisdata-1.0.4.dist-info/WHEEL +5 -0
- trendvisdata-1.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate trend strength fields
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import copy
|
|
6
|
+
import warnings
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from technicalmethods.methods import Indicators
|
|
10
|
+
|
|
11
|
+
class Fields():
|
|
12
|
+
"""
|
|
13
|
+
Create trend strength fields across range of tickers
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def generate_fields(
|
|
19
|
+
cls,
|
|
20
|
+
params: dict,
|
|
21
|
+
ticker_dict: dict) -> dict:
|
|
22
|
+
"""
|
|
23
|
+
Create and add various trend indicators to each DataFrame in
|
|
24
|
+
the dictionary of tickers
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
params : Dict
|
|
29
|
+
ma_list : List, optional
|
|
30
|
+
List of Moving Average periods. The default is
|
|
31
|
+
[10, 20, 30, 50, 100, 200].
|
|
32
|
+
macd_params : List, optional
|
|
33
|
+
List of MACD parameters. The default is [12, 26, 9].
|
|
34
|
+
adx_list : List, optional
|
|
35
|
+
List of ADX periods. The default is [10, 20, 30, 50, 100, 200].
|
|
36
|
+
ma_cross_list : List, optional
|
|
37
|
+
List of Moving Average Crossover periods. The default is
|
|
38
|
+
[(10, 30), (20, 50), (50, 200)].
|
|
39
|
+
price_cross_list : List, optional
|
|
40
|
+
List of Moving Average Price Crossover periods. The default is
|
|
41
|
+
[10, 20, 30, 50, 100, 200].
|
|
42
|
+
rsi_list : List, optional
|
|
43
|
+
List of RSI periods. The default is [10, 20, 30, 50, 100, 200].
|
|
44
|
+
breakout_list : List, optional
|
|
45
|
+
List of breakout periods. The default is
|
|
46
|
+
[10, 20, 30, 50, 100, 200].
|
|
47
|
+
atr_list : List, optional
|
|
48
|
+
List of ATR periods. The default is [14].
|
|
49
|
+
ticker_dict : Dict
|
|
50
|
+
Dictionary of price history DataFrames, one for each ticker.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
ticker_dict : Dict
|
|
55
|
+
Dictionary of DataFrames of each ticker updated with additional
|
|
56
|
+
trend indicators.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
with warnings.catch_warnings():
|
|
61
|
+
warnings.filterwarnings("error")
|
|
62
|
+
|
|
63
|
+
# Loop through each ticker in ticker_dict
|
|
64
|
+
for ticker, frame in ticker_dict.items():
|
|
65
|
+
|
|
66
|
+
# Create moving averages of 10, 20, 30, 50 and 200 day
|
|
67
|
+
# timeframes
|
|
68
|
+
frame = cls._field_ma(
|
|
69
|
+
params=params, frame=frame, ticker=ticker)
|
|
70
|
+
|
|
71
|
+
# Create flag for price crossing moving average
|
|
72
|
+
frame = cls._field_px_ma(
|
|
73
|
+
params=params, frame=frame, ticker=ticker)
|
|
74
|
+
|
|
75
|
+
# Create MACD, Signal and Hist using default parameters
|
|
76
|
+
# of 12, 26, 9
|
|
77
|
+
frame = cls._field_macd(
|
|
78
|
+
params=params, frame=frame, ticker=ticker)
|
|
79
|
+
|
|
80
|
+
# Create ADX of 14, 20, 50, 100 and 200 day timeframes
|
|
81
|
+
# Create flags for ADX over 25
|
|
82
|
+
frame = cls._field_adx(
|
|
83
|
+
params=params, frame=frame, ticker=ticker)
|
|
84
|
+
|
|
85
|
+
# Create flag for fast moving average crossing slow moving
|
|
86
|
+
# average
|
|
87
|
+
frame = cls._field_ma_cross(
|
|
88
|
+
params=params, frame=frame, ticker=ticker)
|
|
89
|
+
|
|
90
|
+
# Create RSI with 14, 20, 50, 100 and 200 day timeframes
|
|
91
|
+
# Create flag for RSI over 70 or under 30
|
|
92
|
+
frame = cls._field_rsi(
|
|
93
|
+
params=params, frame=frame, ticker=ticker)
|
|
94
|
+
|
|
95
|
+
# Create breakout flags with 14, 20, 50, 100 and 200 day
|
|
96
|
+
# timeframes
|
|
97
|
+
frame = cls._field_breakout(
|
|
98
|
+
params=params, frame=frame, ticker=ticker)
|
|
99
|
+
|
|
100
|
+
# Create Average True Range with 14 day timeframe
|
|
101
|
+
frame = cls._field_atr(
|
|
102
|
+
params=params, frame=frame, ticker=ticker)
|
|
103
|
+
|
|
104
|
+
# Create column showing the percentage change in closing price since the start
|
|
105
|
+
frame = cls._start_change(
|
|
106
|
+
params=params, frame=frame, ticker=ticker)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
return ticker_dict
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _field_ma(
|
|
114
|
+
params:dict,
|
|
115
|
+
frame: pd.DataFrame,
|
|
116
|
+
ticker: str) -> pd.DataFrame:
|
|
117
|
+
|
|
118
|
+
for tenor in params['ma_list']:
|
|
119
|
+
try:
|
|
120
|
+
frame['MA_'+str(tenor)] = frame['Close'].rolling(
|
|
121
|
+
window=str(tenor)+'D').mean()
|
|
122
|
+
|
|
123
|
+
except RuntimeWarning:
|
|
124
|
+
print("Error with "
|
|
125
|
+
+ ticker
|
|
126
|
+
+ " MA_"
|
|
127
|
+
+ str(tenor)
|
|
128
|
+
+ " : More than "
|
|
129
|
+
+ str(tenor)
|
|
130
|
+
+ " consecutive days unchanged price")
|
|
131
|
+
|
|
132
|
+
return frame
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _field_px_ma(
|
|
137
|
+
params:dict,
|
|
138
|
+
frame: pd.DataFrame,
|
|
139
|
+
ticker: str) -> pd.DataFrame:
|
|
140
|
+
|
|
141
|
+
for tenor in params['price_cross_list']:
|
|
142
|
+
try:
|
|
143
|
+
frame['PX_MA_'+str(tenor)+'_flag'] = np.where(
|
|
144
|
+
frame['Close'] > frame['MA_'+str(tenor)], 1, -1)
|
|
145
|
+
|
|
146
|
+
except RuntimeWarning:
|
|
147
|
+
print("Error with "
|
|
148
|
+
+ ticker
|
|
149
|
+
+ " PX_MA_"
|
|
150
|
+
+ str(tenor)
|
|
151
|
+
+ " : More than "
|
|
152
|
+
+ str(tenor)
|
|
153
|
+
+ " consecutive days unchanged price")
|
|
154
|
+
|
|
155
|
+
return frame
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _field_macd(
|
|
160
|
+
params:dict,
|
|
161
|
+
frame: pd.DataFrame,
|
|
162
|
+
ticker: str) -> pd.DataFrame:
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
frame['MACD'], frame['MACD_SIGNAL'], \
|
|
166
|
+
frame['MACD_HIST'] = Indicators.MACD(
|
|
167
|
+
close=frame['Close'],
|
|
168
|
+
fast=params['macd_params'][0],
|
|
169
|
+
slow=params['macd_params'][1],
|
|
170
|
+
signal=params['macd_params'][2])
|
|
171
|
+
|
|
172
|
+
# Create flag for MACD histogram increasing
|
|
173
|
+
frame['MACD_flag'] = np.where(
|
|
174
|
+
frame['MACD_HIST'].diff() > 0, 1, -1)
|
|
175
|
+
|
|
176
|
+
except RuntimeWarning:
|
|
177
|
+
print("Error with " + ticker + " MACD")
|
|
178
|
+
|
|
179
|
+
return frame
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _field_adx(
|
|
184
|
+
params:dict,
|
|
185
|
+
frame: pd.DataFrame,
|
|
186
|
+
ticker: str) -> pd.DataFrame:
|
|
187
|
+
|
|
188
|
+
for tenor in params['adx_list']:
|
|
189
|
+
try:
|
|
190
|
+
frame['ADX_'+str(tenor)] = Indicators.ADX(
|
|
191
|
+
high=frame['High'],
|
|
192
|
+
low=frame['Low'],
|
|
193
|
+
close=frame['Close'],
|
|
194
|
+
time_period=tenor)
|
|
195
|
+
frame['ADX_'+str(tenor)+'_flag'] = np.where(
|
|
196
|
+
frame['ADX_'+str(tenor)] > 25, np.where(
|
|
197
|
+
frame['PX_MA_'+str(tenor)+'_flag'] == 1, 1, -1), 0)
|
|
198
|
+
|
|
199
|
+
except RuntimeWarning:
|
|
200
|
+
print("Error with "
|
|
201
|
+
+ ticker
|
|
202
|
+
+ " ADX_"+str(tenor)
|
|
203
|
+
+ " : More than "
|
|
204
|
+
+ str(tenor)
|
|
205
|
+
+ " consecutive days unchanged price")
|
|
206
|
+
|
|
207
|
+
except IndexError:
|
|
208
|
+
print("Error with "
|
|
209
|
+
+ ticker
|
|
210
|
+
+ " ADX_"+str(tenor)
|
|
211
|
+
+ " Problem with EMA calculation")
|
|
212
|
+
|
|
213
|
+
return frame
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _field_ma_cross(
|
|
218
|
+
params:dict,
|
|
219
|
+
frame: pd.DataFrame,
|
|
220
|
+
ticker: str) -> pd.DataFrame:
|
|
221
|
+
|
|
222
|
+
for tenor_pair in params['ma_cross_list']:
|
|
223
|
+
try:
|
|
224
|
+
frame['MA_'+str(tenor_pair[0])+'_'+str(
|
|
225
|
+
tenor_pair[1])+'_flag'] = np.where(
|
|
226
|
+
frame['MA_'+str(tenor_pair[0])] > frame[
|
|
227
|
+
'MA_'+str(tenor_pair[1])], 1, -1)
|
|
228
|
+
|
|
229
|
+
except RuntimeWarning:
|
|
230
|
+
print("Error with " + ticker + " MA_"+str(tenor_pair))
|
|
231
|
+
|
|
232
|
+
return frame
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _field_rsi(
|
|
237
|
+
params:dict,
|
|
238
|
+
frame: pd.DataFrame,
|
|
239
|
+
ticker: str) -> pd.DataFrame:
|
|
240
|
+
|
|
241
|
+
for tenor in params['rsi_list']:
|
|
242
|
+
try:
|
|
243
|
+
frame['RSI_'+str(tenor)] = Indicators.RSI(
|
|
244
|
+
close=frame['Close'], time_period=tenor)
|
|
245
|
+
frame['RSI_'+str(tenor)+'_flag'] = np.where(
|
|
246
|
+
frame['RSI_'+str(tenor)] > 70, 1, np.where(
|
|
247
|
+
frame['RSI_'+str(tenor)] < 30, -1, 0))
|
|
248
|
+
|
|
249
|
+
except RuntimeWarning:
|
|
250
|
+
print("Error with "
|
|
251
|
+
+ ticker
|
|
252
|
+
+ " RSI_"+str(tenor)
|
|
253
|
+
+ " : More than "
|
|
254
|
+
+ str(tenor)
|
|
255
|
+
+ " consecutive days unchanged price")
|
|
256
|
+
|
|
257
|
+
return frame
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def _field_breakout(
|
|
262
|
+
params:dict,
|
|
263
|
+
frame: pd.DataFrame,
|
|
264
|
+
ticker: str) -> pd.DataFrame:
|
|
265
|
+
|
|
266
|
+
for tenor in params['breakout_list']:
|
|
267
|
+
try:
|
|
268
|
+
frame['low_'+str(tenor)], frame['high_'+str(tenor)], \
|
|
269
|
+
frame['breakout_'+str(
|
|
270
|
+
tenor)+'_flag'] = Indicators.breakout(
|
|
271
|
+
high=frame['High'], low=frame['Low'],
|
|
272
|
+
time_period=tenor)
|
|
273
|
+
|
|
274
|
+
except RuntimeWarning:
|
|
275
|
+
print("Error with "
|
|
276
|
+
+ ticker
|
|
277
|
+
+ " breakout_"
|
|
278
|
+
+ str(tenor)
|
|
279
|
+
+ " : More than "
|
|
280
|
+
+ str(tenor)
|
|
281
|
+
+ " consecutive days unchanged price")
|
|
282
|
+
|
|
283
|
+
return frame
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _field_atr(
|
|
288
|
+
params:dict,
|
|
289
|
+
frame: pd.DataFrame,
|
|
290
|
+
ticker: str) -> pd.DataFrame:
|
|
291
|
+
|
|
292
|
+
for tenor in params['atr_list']:
|
|
293
|
+
try:
|
|
294
|
+
frame['ATR_'+str(tenor)] = Indicators.ATR(
|
|
295
|
+
high=frame['High'],
|
|
296
|
+
low=frame['Low'],
|
|
297
|
+
close=frame['Close'],
|
|
298
|
+
time_period=tenor)
|
|
299
|
+
|
|
300
|
+
except RuntimeWarning:
|
|
301
|
+
print("Error with "
|
|
302
|
+
+ ticker
|
|
303
|
+
+ " ATR_"
|
|
304
|
+
+ str(tenor)
|
|
305
|
+
+ " : More than "
|
|
306
|
+
+ str(tenor)
|
|
307
|
+
+ " consecutive days unchanged price")
|
|
308
|
+
|
|
309
|
+
return frame
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def _start_change(
|
|
313
|
+
params:dict,
|
|
314
|
+
frame: pd.DataFrame,
|
|
315
|
+
ticker: str) -> pd.DataFrame:
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
frame['first_close'] = frame['Close'].iloc[0]
|
|
319
|
+
frame['start_change'] = frame['Close'] - frame['first_close']
|
|
320
|
+
frame['start_change_percent'] = frame[
|
|
321
|
+
'start_change'] / frame['Close'].iloc[0] * 100
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
except RuntimeWarning:
|
|
325
|
+
print("Error with "
|
|
326
|
+
+ ticker
|
|
327
|
+
+ " percent change")
|
|
328
|
+
|
|
329
|
+
return frame
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def generate_trend_strength(
|
|
335
|
+
cls,
|
|
336
|
+
params: dict,
|
|
337
|
+
ticker_dict: dict,
|
|
338
|
+
sector_mappings_df: pd.DataFrame) -> pd.DataFrame:
|
|
339
|
+
"""
|
|
340
|
+
Create a DataFrame showing the strength of trend for selected
|
|
341
|
+
markets.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
|
|
346
|
+
params : Dict
|
|
347
|
+
trend_flags : List
|
|
348
|
+
The list of indicators used to calculate the strength of the
|
|
349
|
+
trend.
|
|
350
|
+
The default values are ['PX_MA_10',
|
|
351
|
+
'ADX_10_flag',
|
|
352
|
+
'RSI_10_flag',
|
|
353
|
+
'breakout_10_flag',
|
|
354
|
+
'MA_10_30',
|
|
355
|
+
'MACD_flag',
|
|
356
|
+
'PX_MA_20',
|
|
357
|
+
'MA_20_50',
|
|
358
|
+
'ADX_20_flag',
|
|
359
|
+
'RSI_20_flag',
|
|
360
|
+
'breakout_20_flag',
|
|
361
|
+
'PX_MA_30',
|
|
362
|
+
'ADX_30_flag',
|
|
363
|
+
'RSI_30_flag',
|
|
364
|
+
'breakout_30_flag',
|
|
365
|
+
'PX_MA_50',
|
|
366
|
+
'MA_50_200',
|
|
367
|
+
'ADX_50_flag',
|
|
368
|
+
'RSI_50_flag',
|
|
369
|
+
'breakout_50_flag',
|
|
370
|
+
'PX_MA_100',
|
|
371
|
+
'ADX_100_flag',
|
|
372
|
+
'RSI_100_flag',
|
|
373
|
+
'breakout_100_flag',
|
|
374
|
+
'PX_MA_200',
|
|
375
|
+
'ADX_200_flag',
|
|
376
|
+
'RSI_200_flag',
|
|
377
|
+
'breakout_200_flag'
|
|
378
|
+
].
|
|
379
|
+
ticker_dict : Dict
|
|
380
|
+
Dictionary of price history DataFrames, one for each ticker.
|
|
381
|
+
sector_mappings_df : DataFrame
|
|
382
|
+
Sector mappings DataFrame.
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
barometer : DataFrame
|
|
387
|
+
DataFrame showing trend strength for each ticker.
|
|
388
|
+
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
# Prepare dataframe from ticker_dict
|
|
392
|
+
frame = cls._prep_frame(params, ticker_dict)
|
|
393
|
+
|
|
394
|
+
# Loop through each ticker in ticker_dict to populate trend
|
|
395
|
+
# flags in frame
|
|
396
|
+
for ticker, dataframe in ticker_dict.items():
|
|
397
|
+
for flag in params['trend_flags']:
|
|
398
|
+
try:
|
|
399
|
+
frame.loc[ticker, flag] = dataframe[flag].iloc[-1]
|
|
400
|
+
|
|
401
|
+
except KeyError:
|
|
402
|
+
print("Missing ticker: "
|
|
403
|
+
+ ticker
|
|
404
|
+
+ " , trend flag: "
|
|
405
|
+
+ flag
|
|
406
|
+
+ " from ticker dict")
|
|
407
|
+
frame.loc[ticker, flag] = 0
|
|
408
|
+
|
|
409
|
+
# Add column of largest change to filter out trending but largely static instruments
|
|
410
|
+
try:
|
|
411
|
+
minval = min(dataframe['start_change_percent'])
|
|
412
|
+
maxval = max(dataframe['start_change_percent'])
|
|
413
|
+
largest_change = max(abs(minval), maxval)
|
|
414
|
+
frame.loc[ticker, 'largest_change'] = largest_change
|
|
415
|
+
|
|
416
|
+
except KeyError:
|
|
417
|
+
print("Missing ticker: "
|
|
418
|
+
+ ticker
|
|
419
|
+
+ " , largest change")
|
|
420
|
+
frame.loc[ticker, 'largest_change'] = 0
|
|
421
|
+
|
|
422
|
+
# Create trend strength column that sums the trend flags and
|
|
423
|
+
# sort by this column
|
|
424
|
+
frame['Trend Strength'] = frame.iloc[:,1:30].sum(axis=1)
|
|
425
|
+
frame = frame.sort_values(by=['Trend Strength'], ascending=False)
|
|
426
|
+
|
|
427
|
+
# Create short name column, stripping text from longname
|
|
428
|
+
short_name = np.array(frame['Long_name'], dtype=str)
|
|
429
|
+
long_name = np.array(frame['Long_name'], dtype=str)
|
|
430
|
+
|
|
431
|
+
for row in range(0, len(frame['Long_name'])):
|
|
432
|
+
short_name[row] = long_name[row].partition(" Continuous")[0]
|
|
433
|
+
|
|
434
|
+
frame['Short_name'] = short_name
|
|
435
|
+
|
|
436
|
+
# Add column showing trend strength as Red, Amber, Green
|
|
437
|
+
barometer = frame.apply(cls._col_color, axis=1)
|
|
438
|
+
|
|
439
|
+
# Apply sector mappings
|
|
440
|
+
barometer = cls._barometer_sectors(
|
|
441
|
+
params, barometer, sector_mappings_df)
|
|
442
|
+
|
|
443
|
+
# barometer = cls._barometer_changes(
|
|
444
|
+
# barometer, ticker_dict)
|
|
445
|
+
|
|
446
|
+
return barometer
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@staticmethod
|
|
450
|
+
def _prep_frame(
|
|
451
|
+
params: dict,
|
|
452
|
+
ticker_dict: dict) -> pd.DataFrame:
|
|
453
|
+
|
|
454
|
+
# Create list of tickers from ticker_dict
|
|
455
|
+
ticker_list = [ticker for ticker, df in ticker_dict.items()]
|
|
456
|
+
|
|
457
|
+
# Convert ticker_name_dict to DataFrame
|
|
458
|
+
ticker_name_df = pd.DataFrame.from_dict(
|
|
459
|
+
params['ticker_name_dict'], orient='index', columns=['Long_name'])
|
|
460
|
+
|
|
461
|
+
# Create empty DataFrame with Trend Flags as columns and
|
|
462
|
+
# tickers as rows
|
|
463
|
+
frame = pd.DataFrame(columns=params['trend_flags'], index=ticker_list)
|
|
464
|
+
|
|
465
|
+
# Merge the two DataFrames
|
|
466
|
+
frame = pd.merge(frame, ticker_name_df, left_index=True,
|
|
467
|
+
right_index=True)
|
|
468
|
+
|
|
469
|
+
# Reorder columns with list of column names moving the
|
|
470
|
+
# Long Name to the start
|
|
471
|
+
cols = [frame.columns[-1]] + [col for col in frame
|
|
472
|
+
if col != frame.columns[-1]]
|
|
473
|
+
frame = frame[cols]
|
|
474
|
+
|
|
475
|
+
return frame
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _col_color(row: pd.Series) -> pd.Series:
|
|
480
|
+
|
|
481
|
+
# Create trend strength color column and absolute strength column
|
|
482
|
+
row['Absolute Trend Strength'] = np.abs(row['Trend Strength'])
|
|
483
|
+
|
|
484
|
+
if np.abs(row['Trend Strength']) < 5:
|
|
485
|
+
row['Trend Color'] = 'red'
|
|
486
|
+
|
|
487
|
+
elif np.abs(row['Trend Strength']) < 10:
|
|
488
|
+
row['Trend Color'] = 'orange'
|
|
489
|
+
|
|
490
|
+
else:
|
|
491
|
+
row['Trend Color'] = 'green'
|
|
492
|
+
|
|
493
|
+
return row
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@staticmethod
|
|
497
|
+
def _barometer_sectors(
|
|
498
|
+
params: dict,
|
|
499
|
+
barometer: pd.DataFrame,
|
|
500
|
+
sector_mappings_df: pd.DataFrame) -> pd.DataFrame:
|
|
501
|
+
"""
|
|
502
|
+
Add sector mappings to the trend barometer table to use in summary
|
|
503
|
+
graph
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
params : Dict
|
|
508
|
+
Dictionary of key parameters.
|
|
509
|
+
sector_mappings_df : DataFrame
|
|
510
|
+
Sector mappings DataFrame.
|
|
511
|
+
barometer : DataFrame
|
|
512
|
+
DataFrame showing trend strength for each ticker.
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
barometer : DataFrame
|
|
517
|
+
barometer updated with additional sector columns.
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
# Calculate the number of trend indicators
|
|
522
|
+
num_flags = len(params['trend_flags'])
|
|
523
|
+
|
|
524
|
+
# Join the baometer table to the sector mappings table
|
|
525
|
+
barometer = barometer.join(sector_mappings_df)
|
|
526
|
+
|
|
527
|
+
# Calculate Trend Strength as a percentage by dividing by the number
|
|
528
|
+
# of trend indicators
|
|
529
|
+
barometer['Trend Strength %'] = barometer[
|
|
530
|
+
'Trend Strength'] / num_flags
|
|
531
|
+
|
|
532
|
+
# Calculate the absolute value of trend strength as a percentage
|
|
533
|
+
barometer['Absolute Trend Strength %'] = abs(
|
|
534
|
+
barometer['Trend Strength %'])
|
|
535
|
+
|
|
536
|
+
# Reset the index and rename column as 'Ticker
|
|
537
|
+
barometer = barometer.reset_index()
|
|
538
|
+
barometer = barometer.rename(columns={'index':'Ticker'})
|
|
539
|
+
|
|
540
|
+
# Add a column as the string 'Trend' to use in swarm plot
|
|
541
|
+
barometer['Trend'] = 'Trend'
|
|
542
|
+
|
|
543
|
+
return barometer
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@staticmethod
|
|
547
|
+
def _barometer_changes(
|
|
548
|
+
barometer: pd.DataFrame,
|
|
549
|
+
ticker_dict: dict ) -> pd.DataFrame:
|
|
550
|
+
|
|
551
|
+
for ticker, dataframe in ticker_dict.items():
|
|
552
|
+
# Add column of largest change to filter out trending but largely static instruments
|
|
553
|
+
try:
|
|
554
|
+
minval = min(dataframe['start_change_percent'])
|
|
555
|
+
maxval = max(dataframe['start_change_percent'])
|
|
556
|
+
largest_change = max(abs(minval), maxval)
|
|
557
|
+
barometer.loc[ticker, 'largest_change'] = largest_change
|
|
558
|
+
|
|
559
|
+
except KeyError:
|
|
560
|
+
print("Missing ticker: "
|
|
561
|
+
+ ticker
|
|
562
|
+
+ " , largest change")
|
|
563
|
+
barometer.loc[ticker, 'largest_change'] = 0
|
|
564
|
+
|
|
565
|
+
return barometer
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class TrendRank():
|
|
569
|
+
"""
|
|
570
|
+
Generate list of strongly trending securities
|
|
571
|
+
|
|
572
|
+
"""
|
|
573
|
+
@classmethod
|
|
574
|
+
def top_trend_calc(
|
|
575
|
+
cls,
|
|
576
|
+
tables: dict,
|
|
577
|
+
params: dict) -> tuple[dict, dict]:
|
|
578
|
+
"""
|
|
579
|
+
Prepare list of top trending securities.
|
|
580
|
+
|
|
581
|
+
Parameters
|
|
582
|
+
----------
|
|
583
|
+
barometer : DataFrame
|
|
584
|
+
DataFrame showing trend strength for each ticker.
|
|
585
|
+
norgate_source : Bool
|
|
586
|
+
Whether the market data source is norgate or yahoo.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
top_trends : Dict
|
|
591
|
+
Dictionary containing list and dictionary of most strongly
|
|
592
|
+
trending assets
|
|
593
|
+
ticker_dict : Dict
|
|
594
|
+
Dict of top trending securities.
|
|
595
|
+
ticker_list : List
|
|
596
|
+
List of top trending securities.
|
|
597
|
+
tables : Dict
|
|
598
|
+
Dictionary of key tables updated for the filtered barometer.
|
|
599
|
+
|
|
600
|
+
"""
|
|
601
|
+
top_trends = {}
|
|
602
|
+
|
|
603
|
+
# Configure sector name
|
|
604
|
+
params = cls._set_sector_level_name(params)
|
|
605
|
+
|
|
606
|
+
tables['filtered_barometer'] = cls._filter_barometer(
|
|
607
|
+
tables, params)
|
|
608
|
+
|
|
609
|
+
tables = cls._sector_split(tables, params)
|
|
610
|
+
tables = cls._return_filter(tables)
|
|
611
|
+
|
|
612
|
+
top_trends['top_ticker_dict'] = {}
|
|
613
|
+
top_trends['top_ticker_list'] = []
|
|
614
|
+
for ticker in tables['filtered_barometer']['Ticker']:
|
|
615
|
+
if params['source'] == 'norgate':
|
|
616
|
+
ticker_mod = '&'+ticker.upper()
|
|
617
|
+
ticker_mod = ticker_mod.replace('&C_','&')
|
|
618
|
+
else:
|
|
619
|
+
ticker_mod = ticker
|
|
620
|
+
top_trends['top_ticker_dict'][tables['filtered_barometer'].index[
|
|
621
|
+
tables['filtered_barometer'][
|
|
622
|
+
'Ticker'] == ticker][0] + 1] = (
|
|
623
|
+
ticker_mod, tables['raw_ticker_dict'][ticker][
|
|
624
|
+
['Open', 'High', 'Low', 'Close']])
|
|
625
|
+
|
|
626
|
+
for rank, ticker in top_trends['top_ticker_dict'].items():
|
|
627
|
+
if rank < params['top_trend_params']['final_size'] + 1:
|
|
628
|
+
top_trends['top_ticker_list'].append(ticker)
|
|
629
|
+
|
|
630
|
+
return top_trends, tables
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@staticmethod
|
|
634
|
+
def _set_sector_level_name(params):
|
|
635
|
+
if params['source'] == 'norgate':
|
|
636
|
+
params['sector_name'] = params[
|
|
637
|
+
'commodity_sector_levels'][params['sector_level']-1]
|
|
638
|
+
params['returns_sector_name'] = params[
|
|
639
|
+
'commodity_sector_levels'][params['returns_sector_level']-1]
|
|
640
|
+
# Otherwise for Yahoo SPX data
|
|
641
|
+
else:
|
|
642
|
+
params['sector_name'] = params[
|
|
643
|
+
'equity_sector_levels'][params['sector_level']-1]
|
|
644
|
+
params['returns_sector_name'] = params[
|
|
645
|
+
'equity_sector_levels'][params['returns_sector_level']-1]
|
|
646
|
+
|
|
647
|
+
return params
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@classmethod
|
|
651
|
+
def _filter_barometer(
|
|
652
|
+
cls,
|
|
653
|
+
tables: dict,
|
|
654
|
+
params: dict) -> pd.DataFrame:
|
|
655
|
+
"""
|
|
656
|
+
Sort and filter the top trending securities
|
|
657
|
+
|
|
658
|
+
Parameters
|
|
659
|
+
----------
|
|
660
|
+
barometer : DataFrame
|
|
661
|
+
DataFrame showing trend strength for each ticker.
|
|
662
|
+
norgate_source : Bool
|
|
663
|
+
Whether the market data source is norgate or yahoo.
|
|
664
|
+
|
|
665
|
+
Returns
|
|
666
|
+
-------
|
|
667
|
+
filtered_barometer : DataFrame
|
|
668
|
+
DataFrame showing the top trending securities.
|
|
669
|
+
|
|
670
|
+
"""
|
|
671
|
+
if params['source'] == 'norgate':
|
|
672
|
+
# Split the continuous futures data
|
|
673
|
+
tables = cls._futures_split(tables=tables, params=params)
|
|
674
|
+
barometer = copy.deepcopy(tables['futures_barometer'])
|
|
675
|
+
else:
|
|
676
|
+
barometer = copy.deepcopy(tables['barometer'])
|
|
677
|
+
|
|
678
|
+
data = barometer.sort_values(
|
|
679
|
+
by=['Absolute Trend Strength'],
|
|
680
|
+
ascending=False)[:params['top_trend_params']['initial_size']]
|
|
681
|
+
|
|
682
|
+
filtered_barometer = pd.DataFrame()
|
|
683
|
+
|
|
684
|
+
if params['source'] == 'norgate':
|
|
685
|
+
sectors = set(barometer['Mid Sector'])
|
|
686
|
+
data = data.sort_values(
|
|
687
|
+
by=['Mid Sector', 'Absolute Trend Strength'],
|
|
688
|
+
ascending=False)
|
|
689
|
+
else:
|
|
690
|
+
sectors = set(barometer['Sector'])
|
|
691
|
+
data = data.sort_values(
|
|
692
|
+
by=['Sector', 'Absolute Trend Strength'], ascending=False)
|
|
693
|
+
|
|
694
|
+
for sector in sectors:
|
|
695
|
+
if params['source'] == 'norgate':
|
|
696
|
+
frame = data[data['Mid Sector']==sector]
|
|
697
|
+
else:
|
|
698
|
+
frame = data[data['Sector']==sector]
|
|
699
|
+
|
|
700
|
+
if len(frame) > 0:
|
|
701
|
+
if params['source'] == 'norgate':
|
|
702
|
+
frame = frame.drop_duplicates(subset='Underlying')
|
|
703
|
+
else:
|
|
704
|
+
frame = frame.drop_duplicates(subset='Security')
|
|
705
|
+
|
|
706
|
+
if len(frame) > params['top_trend_params']['max_per_sector']:
|
|
707
|
+
frame = frame[:params['top_trend_params'][
|
|
708
|
+
'max_per_sector']]
|
|
709
|
+
filtered_barometer = pd.concat([filtered_barometer, frame])
|
|
710
|
+
|
|
711
|
+
filtered_barometer = filtered_barometer.sort_values(
|
|
712
|
+
by=['Absolute Trend Strength'],
|
|
713
|
+
ascending=False).reset_index().drop(['index'], axis=1)
|
|
714
|
+
|
|
715
|
+
return filtered_barometer
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
@staticmethod
|
|
719
|
+
def _futures_split(
|
|
720
|
+
tables: dict,
|
|
721
|
+
params: dict) -> dict:
|
|
722
|
+
"""
|
|
723
|
+
Split the futures from the rest of the norgate data (choosing
|
|
724
|
+
backadjusted data or not)
|
|
725
|
+
|
|
726
|
+
Parameters
|
|
727
|
+
----------
|
|
728
|
+
tables : Dict
|
|
729
|
+
Dictionary of key tables.
|
|
730
|
+
params : Dict
|
|
731
|
+
Dictionary of key parameters
|
|
732
|
+
|
|
733
|
+
Returns
|
|
734
|
+
-------
|
|
735
|
+
tables : Dict
|
|
736
|
+
Dictionary of key tables updated for futures_ticker_dict and
|
|
737
|
+
futures_barometer.
|
|
738
|
+
|
|
739
|
+
"""
|
|
740
|
+
barometer = copy.deepcopy(tables['barometer'])
|
|
741
|
+
|
|
742
|
+
# Select backadjusted futures
|
|
743
|
+
if params['tickers_adjusted']:
|
|
744
|
+
tables['futures_ticker_dict'] = {
|
|
745
|
+
k:v for k,v in tables['raw_ticker_dict'].items()
|
|
746
|
+
if '_ccb' in k}
|
|
747
|
+
tables['futures_barometer'] = barometer[
|
|
748
|
+
barometer['Ticker'].str.lower().str.contains('_ccb')]
|
|
749
|
+
|
|
750
|
+
# Select unadjusted futures
|
|
751
|
+
else:
|
|
752
|
+
tables['futures_ticker_dict'] = {
|
|
753
|
+
k:v for k,v in tables['raw_ticker_dict'].items()
|
|
754
|
+
if k.startswith('c_')}
|
|
755
|
+
tables['futures_barometer'] = barometer[
|
|
756
|
+
barometer['Ticker'].str.lower().str.startswith('c_')]
|
|
757
|
+
|
|
758
|
+
return tables
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@staticmethod
|
|
762
|
+
def _sector_split(tables: dict, params: dict) -> dict:
|
|
763
|
+
|
|
764
|
+
barometer = copy.deepcopy(tables['barometer'])
|
|
765
|
+
|
|
766
|
+
tables['sectors'] = {}
|
|
767
|
+
sector_level = params['returns_sector_name']
|
|
768
|
+
|
|
769
|
+
sectors = set(barometer[sector_level])
|
|
770
|
+
|
|
771
|
+
for sector in sectors:
|
|
772
|
+
tables['sectors'][sector] = tables[
|
|
773
|
+
'barometer'].loc[barometer[sector_level] == sector]
|
|
774
|
+
|
|
775
|
+
return tables
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
@staticmethod
|
|
779
|
+
def _return_filter(tables: dict) -> dict:
|
|
780
|
+
|
|
781
|
+
barometer = copy.deepcopy(tables['barometer'])
|
|
782
|
+
tables['return_barometer'] = barometer.loc[barometer['largest_change'] > 5]
|
|
783
|
+
|
|
784
|
+
return tables
|
|
785
|
+
|