11import os
22from datetime import datetime , timezone , timedelta
3- from typing import Union , List
3+ from typing import Union , List , Tuple
44import json
55
66import numpy as np
1717"""
1818
1919
20+ def generate_smoothen_scores (df , config : dict ):
21+ """
22+ Smoothen several columns and rows. Used for smoothing scores.
23+
24+ The following operations are applied:
25+ - find average of the specified input columns (row-wise)
26+ - find moving average with the specified window
27+ - apply threshold to source buy/sell column(s) according to threshold parameter(s) by producing a boolean column
28+
29+ Notes:
30+ - Input point-wise scores in buy and sell columns are always positive
31+ """
32+
33+ columns = config .get ('columns' )
34+ if not columns :
35+ raise ValueError (f"The 'columns' parameter must be a non-empty string. { type (columns )} " )
36+ elif isinstance (columns , str ):
37+ columns = [columns ]
38+
39+ # TODO: check that all columns exist
40+ #if columns not in df.columns:
41+ # raise ValueError(f"{columns} do not exist in the input data. Existing columns: {df.columns.to_list()}")
42+
43+ # Average all buy and sell columns
44+ out_column = df [columns ].mean (skipna = True , axis = 1 )
45+
46+ # Apply thresholds (if specified) and binarize the score
47+ point_threshold = config .get ("point_threshold" )
48+ if point_threshold :
49+ out_column = out_column >= point_threshold
50+
51+ # Moving average
52+ window = config .get ("window" )
53+ if isinstance (window , int ):
54+ out_column = out_column .rolling (window , min_periods = window // 2 ).mean ()
55+ elif isinstance (window , float ):
56+ out_column = out_column .ewm (span = window , min_periods = window // 2 , adjust = False ).mean ()
57+
58+ names = config .get ('names' )
59+ if not isinstance (names , str ):
60+ raise ValueError (f"'names' parameter must be a non-empty string. { type (names )} " )
61+
62+ df [names ] = out_column
63+
64+ return df , [names ]
65+
66+
67+ # TODO: DEPRECATED, REMOVE, REPLACE BY generator_smoothen_signals
2068def aggregate_scores (df , model , score_column_out : str , score_columns : Union [List [str ], str ]):
2169 """
2270 Add two signal numeric (buy and sell) columns by processing a list of buy and sell point-wise predictions.
@@ -62,6 +110,43 @@ def aggregate_scores(df, model, score_column_out: str, score_columns: Union[List
62110 return score_column
63111
64112
113+ def generate_combine_scores (df , config : dict ):
114+ """
115+ ML algorithms predict score which is always positive and typically within [0,1].
116+ One score for price growth and one score for price fall. This function combines pairs
117+ of such scores and produce one score within [-1,+1]. Positive values mean growth
118+ and negative values mean fall of price.
119+ """
120+ columns = config .get ('columns' )
121+ if not columns :
122+ raise ValueError (f"The 'columns' parameter must be a non-empty string. { type (columns )} " )
123+ elif not isinstance (columns , list ) or len (columns ) != 2 :
124+ raise ValueError (f"'columns' parameter must be a list with buy column name and sell column name. { type (columns )} " )
125+
126+ up_column = columns [0 ]
127+ down_column = columns [1 ]
128+
129+ out_column = config .get ('names' )
130+
131+ if config .get ("combine" ) == "relative" :
132+ combine_scores_relative (df , up_column , down_column , out_column )
133+ elif config .get ("combine" ) == "difference" :
134+ combine_scores_difference (df , up_column , down_column , out_column )
135+ else :
136+ # If buy score is greater than sell score then positive buy, otherwise negative sell
137+ df [out_column ] = df [[up_column , down_column ]].apply (lambda x : x [0 ] if x [0 ] >= x [1 ] else - x [1 ], raw = True , axis = 1 )
138+
139+ # Scale the score distribution to make it symmetric or normalize
140+ # Always apply the transformation to buy score. It might be in [0,1] or [-1,+1] depending on combine parameter
141+ if config .get ("coefficient" ):
142+ df [out_column ] = df [out_column ] * config .get ("coefficient" )
143+ if config .get ("constant" ):
144+ df [out_column ] = df [out_column ] + config .get ("constant" )
145+
146+ return df , [out_column ]
147+
148+
149+ # TODO: DEPRECATED, REMOVE, REPLACE BY generator_smoothen_signals
65150def combine_scores (df , model , buy_score_column , sell_score_column , trade_score_column ):
66151 """
67152 Mutually adjust two independent scores with opposite semantics.
@@ -159,6 +244,32 @@ def linear_regr_fn(X):
159244# Signal rules
160245#
161246
247+ def generate_threshold_rule (df , config ):
248+ """
249+ Apply rules based on thresholds and generate trade signal buy, sell or do nothing.
250+
251+ Returns signals in two pre-defined columns: 'buy_signal_column' and 'sell_signal_column'
252+ """
253+ parameters = config .get ("parameters" , {})
254+
255+ columns = config .get ("columns" )
256+ if not columns :
257+ raise ValueError (f"The 'columns' parameter must be a non-empty string. { type (columns )} " )
258+ elif isinstance (columns , list ):
259+ columns = [columns ]
260+
261+ buy_signal_column = config .get ("names" )[0 ]
262+ sell_signal_column = config .get ("names" )[1 ]
263+
264+ df [buy_signal_column ] = \
265+ (df [columns ] >= parameters .get ("buy_signal_threshold" ))
266+ df [sell_signal_column ] = \
267+ (df [columns ] <= parameters .get ("sell_signal_threshold" ))
268+
269+ return df , [buy_signal_column , sell_signal_column ]
270+
271+
272+ # TODO: DEPRECATED TO BE REMOVED
162273def apply_rule_with_score_thresholds (df , score_column_names , model ):
163274 """
164275 Apply rules based on thresholds and generate trade signal buy, sell or do nothing.
@@ -178,6 +289,38 @@ def apply_rule_with_score_thresholds(df, score_column_names, model):
178289 (df [score_column ] <= parameters .get ("sell_signal_threshold" ))
179290
180291
292+ def generate_threshold_rule2 (df , config ):
293+ """
294+ Assume using difference combination with negative sell scores
295+ """
296+ parameters = config .get ("parameters" , {})
297+
298+ columns = config .get ("columns" )
299+ if not columns :
300+ raise ValueError (f"The 'columns' parameter must be a non-empty string. { type (columns )} " )
301+ elif not isinstance (columns , list ) or len (columns ) != 2 :
302+ raise ValueError (f"'columns' parameter must be a list with two column names. { type (columns )} " )
303+
304+ score_column = columns [0 ]
305+ score_column_2 = columns [1 ]
306+
307+ buy_signal_column = config .get ("names" )[0 ]
308+ sell_signal_column = config .get ("names" )[1 ]
309+
310+ # Both buy scores are greater than the corresponding thresholds
311+ df [buy_signal_column ] = \
312+ (df [score_column ] >= parameters .get ("buy_signal_threshold" )) & \
313+ (df [score_column_2 ] >= parameters .get ("buy_signal_threshold_2" ))
314+
315+ # Both sell scores are smaller than the corresponding thresholds
316+ df [sell_signal_column ] = \
317+ (df [score_column ] <= parameters .get ("sell_signal_threshold" )) & \
318+ (df [score_column_2 ] <= parameters .get ("sell_signal_threshold_2" ))
319+
320+ return df , [buy_signal_column , sell_signal_column ]
321+
322+
323+ # TODO: DEPRECATED TO BE REMOVED
181324def apply_rule_with_score_thresholds_2 (df , score_column_names , model ):
182325 """
183326 Assume using difference combination with negative sell scores
0 commit comments