From a7c50fdf88a9dbbd82e5be0e6ffd840001667b3e Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Mon, 16 Dec 2024 14:16:30 +0100 Subject: [PATCH] Fix flake8 issues --- investing_algorithm_framework/__init__.py | 1 - .../app/algorithm.py | 64 +++++--- investing_algorithm_framework/app/app.py | 40 ++--- investing_algorithm_framework/app/strategy.py | 153 ++++++++++-------- .../models/backtesting/backtest_date_range.py | 3 +- .../domain/services/market_data_sources.py | 3 +- .../domain/utils/polars.py | 3 +- .../indicators/__init__.py | 2 +- .../indicators/advanced.py | 137 ++++++++++------ .../indicators/momentum.py | 16 +- .../indicators/trend.py | 30 ++-- .../indicators/utils.py | 152 ++++++++++------- .../models/market_data_sources/ccxt.py | 13 +- .../backtest_report_writer_service.py | 10 +- .../services/backtesting/backtest_service.py | 116 +++++++------ .../market_data_source_service.py | 14 +- 16 files changed, 450 insertions(+), 307 deletions(-) diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py index 9462307..0f2ab53 100644 --- a/investing_algorithm_framework/__init__.py +++ b/investing_algorithm_framework/__init__.py @@ -17,7 +17,6 @@ CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \ CSVTickerMarketDataSource from .create_app import create_app -from investing_algorithm_framework.indicators import * __all__ = [ "Algorithm", diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py index fd18371..c19396b 100644 --- a/investing_algorithm_framework/app/algorithm.py +++ b/investing_algorithm_framework/app/algorithm.py @@ -89,11 +89,16 @@ def _validate_name(self, name): None """ if not isinstance(name, str): - raise OperationalException("The name of the algorithm must be a string") - + raise OperationalException( + "The name of the algorithm must be a string" + ) + pattern = re.compile(r"^[a-zA-Z0-9]*$") if not pattern.match(name): - raise OperationalException("The name of the algorithm can only contain letters and numbers") + raise OperationalException( + "The name of the algorithm can only contain" + + " letters and numbers" + ) def initialize_services( self, @@ -256,19 +261,19 @@ def create_order( return self.order_service.create( order_data, execute=execute, validate=validate, sync=sync ) - + def has_balance(self, symbol, amount, market=None): """ Function to check if the portfolio has enough balance to create an order. This function will return True if the - portfolio has enough balance to create an order, False + portfolio has enough balance to create an order, False otherwise. Parameters: symbol: The symbol of the asset amount: The amount of the asset market: The market of the asset - + Returns: Boolean: True if the portfolio has enough balance """ @@ -309,20 +314,26 @@ def create_limit_order( price: The price of the asset order_side: The side of the order amount (optional): The amount of the asset to trade - amount_trading_symbol (optional): The amount of the trading symbol to trade - percentage (optional): The percentage of the portfolio to allocate to the + amount_trading_symbol (optional): The amount of the + trading symbol to trade + percentage (optional): The percentage of the portfolio + to allocate to the order - percentage_of_portfolio (optional): The percentage of the portfolio to - allocate to the order - percentage_of_position (optional): The percentage of the position to - allocate to the order. (Only supported for SELL orders) + percentage_of_portfolio (optional): The percentage + of the portfolio to allocate to the order + percentage_of_position (optional): The percentage + of the position to allocate to + the order. (Only supported for SELL orders) precision (optional): The precision of the amount market (optional): The market to trade the asset - execute (optional): Default True. If set to True, the order will be executed - validate (optional): Default True. If set to True, the order will be validated - sync (optional): Default True. If set to True, the created order will be synced with the + execute (optional): Default True. If set to True, + the order will be executed + validate (optional): Default True. If set to + True, the order will be validated + sync (optional): Default True. If set to True, + the created order will be synced with the portfolio of the algorithm - + Returns: Order: Instance of the order created """ @@ -369,10 +380,10 @@ def create_limit_order( raise OperationalException( "The amount parameter is required to create a limit order." + "Either the amount, amount_trading_symbol, percentage, " + - "percentage_of_portfolio or percentage_of_position parameter " + - "must be specified." + "percentage_of_portfolio or percentage_of_position " + "parameter must be specified." ) - + order_data = { "target_symbol": target_symbol, "price": price, @@ -417,7 +428,7 @@ def create_market_order( validate: If set to True, the order will be validated sync: If set to True, the created order will be synced with the portfolio of the algorithm - + Returns: Order: Instance of the order created """ @@ -454,7 +465,7 @@ def get_portfolio(self, market=None) -> Portfolio: Parameters: market: The market of the portfolio - + Returns: Portfolio: The portfolio of the algorithm """ @@ -492,7 +503,7 @@ def get_total_size(self): """ Returns the total size of the portfolio. - The total size of the portfolio is the unallocated balance and the + The total size of the portfolio is the unallocated balance and the allocated balance of the portfolio. Returns: @@ -597,7 +608,7 @@ def get_positions( amount_lt: The amount of the asset must be less than this amount_lte: The amount of the asset must be less than or equal to this - + Returns: List[Position]: A list of positions that match the query parameters """ @@ -1167,7 +1178,7 @@ def get_closed_trades(self) -> List[Trade]: """ Function to get all closed trades. This function will return all closed trades of the algorithm. - + Returns: List[Trade]: A list of closed trades """ @@ -1234,8 +1245,9 @@ def has_trading_symbol_position_available( the position must be greater than the net_size of the portfolio. - :param amount_gt: The amount of the position must be greater than - this amount. + Parameters: + amount_gt: The amount of the position must be greater than this + amount. :param amount_gte: The amount of the position must be greater than or equal to this amount. :param percentage_of_portfolio: The amount of the position must be diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 15f5163..4f65d07 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -283,7 +283,7 @@ def _create_backtest_database_if_not_exists(self): should be called before running a backtest for an algorithm. It creates the database if it does not exist. - Args: + Parameters: None Returns @@ -728,9 +728,10 @@ def run_backtest( Algorithm) backtest_date_range: The date range to run the backtest for (instance of BacktestDateRange) - pending_order_check_interval: str - pending_order_check_interval: The interval at which to check - pending orders (e.g. 1h, 1d, 1w) - output_directory: str - The directory to write the backtest report to + pending_order_check_interval: str - pending_order_check_interval: + The interval at which to check pending orders (e.g. 1h, 1d, 1w) + output_directory: str - The directory to + write the backtest report to Returns: Instance of BacktestReport @@ -776,7 +777,7 @@ def run_backtests( date_ranges: List[BacktestDateRange] = None, pending_order_check_interval=None, output_directory=None, - checkpoint = False + checkpoint=False ) -> List[BacktestReport]: """ Run a backtest for a set algorithm. This method should be called when @@ -788,15 +789,17 @@ def run_backtests( backtests for pending_order_check_interval: str - The interval at which to check pending orders - output_directory: str - The directory to write the backtest report to. - checkpoint: bool - Whether to checkpoint the backtest, If True, then it - will be checked if for a given algorithm name and date range, - a backtest report already exists. If it does, then the backtest will - not be run again. This is useful when running backtests - for a large number of algorithms and date ranges where some of the - backtests may fail and you want to re-run only the failed backtests. - - Returns + output_directory: str - The directory to write the backtest + report to. + checkpoint: bool - Whether to checkpoint the backtest, + If True, then it will be checked if for a given algorithm name + and date range, a backtest report already exists. If it does, + then the backtest will not be run again. This is useful + when running backtests for a large number of algorithms + and date ranges where some of the backtests may fail + and you want to re-run only the failed backtests. + + Returns List of BacktestReport intances """ logger.info("Initializing backtests") @@ -822,17 +825,18 @@ def run_backtests( if checkpoint: backtest_service = self.container.backtest_service() report = backtest_service.get_report( - algorithm_name=algorithm.name, - backtest_date_range=date_range, + algorithm_name=algorithm.name, + backtest_date_range=date_range, directory=output_directory ) if report is not None: - + print( f"{COLOR_YELLOW}Backtest already exists " f"for algorithm {algorithm.name} date " - f"range:{COLOR_RESET} {COLOR_GREEN}{date_range.name} " + f"range:{COLOR_RESET} {COLOR_GREEN} " + f"{date_range.name} " f"{date_range.start_date} - " f"{date_range.end_date}" ) diff --git a/investing_algorithm_framework/app/strategy.py b/investing_algorithm_framework/app/strategy.py index 5e6367f..37362d8 100644 --- a/investing_algorithm_framework/app/strategy.py +++ b/investing_algorithm_framework/app/strategy.py @@ -70,7 +70,7 @@ def __init__( def run_strategy(self, algorithm, market_data): self.algorithm = algorithm - + # Check pending orders before running the strategy algorithm.check_pending_orders() @@ -215,79 +215,88 @@ def get_traces(self) -> dict: dict: The traces object """ return self.traces - - def has_open_orders(self, target_symbol=None, identifier=None, market=None) -> bool: + + def has_open_orders( + self, target_symbol=None, identifier=None, market=None + ) -> bool: """ Check if there are open orders for a given symbol Parameters: - target_symbol (str): The symbol of the asset e.g BTC if the asset is BTC/USDT + target_symbol (str): The symbol of the asset e.g BTC if the + asset is BTC/USDT identifier (str): The identifier of the portfolio market (str): The market of the asset Returns: bool: True if there are open orders, False otherwise """ - return self.algorithm.has_open_orders(target_symbol=target_symbol, identifier=identifier, market=market) + return self.algorithm.has_open_orders( + target_symbol=target_symbol, identifier=identifier, market=market + ) def create_limit_order( - self, - target_symbol, - price, - order_side, - amount=None, - amount_trading_symbol=None, - percentage=None, - percentage_of_portfolio=None, - percentage_of_position=None, - precision=None, - market=None, - execute=True, - validate=True, - sync=True - ): - """ - Function to create a limit order. This function will create a limit - order and execute it if the execute parameter is set to True. If the - validate parameter is set to True, the order will be validated - - Parameters: - target_symbol: The symbol of the asset to trade - price: The price of the asset - order_side: The side of the order - amount (optional): The amount of the asset to trade - amount_trading_symbol (optional): The amount of the trading symbol to trade - percentage (optional): The percentage of the portfolio to allocate to the - order - percentage_of_portfolio (optional): The percentage of the portfolio to - allocate to the order - percentage_of_position (optional): The percentage of the position to - allocate to the order. (Only supported for SELL orders) - precision (optional): The precision of the amount - market (optional): The market to trade the asset - execute (optional): Default True. If set to True, the order will be executed - validate (optional): Default True. If set to True, the order will be validated - sync (optional): Default True. If set to True, the created order will be synced with the - portfolio of the algorithm - - Returns: - Order: Instance of the order created - """ - self.algorithm.create_limit_order( - target_symbol=target_symbol, - price=price, - order_side=order_side, - amount=amount, - amount_trading_symbol=amount_trading_symbol, - percentage=percentage, - percentage_of_portfolio=percentage_of_portfolio, - percentage_of_position=percentage_of_position, - precision=precision, - market=market, - execute=execute, - validate=validate, - sync=sync - ) + self, + target_symbol, + price, + order_side, + amount=None, + amount_trading_symbol=None, + percentage=None, + percentage_of_portfolio=None, + percentage_of_position=None, + precision=None, + market=None, + execute=True, + validate=True, + sync=True + ): + """ + Function to create a limit order. This function will create + a limit order and execute it if the execute parameter is set to True. + If the validate parameter is set to True, the order will be validated + + Parameters: + target_symbol: The symbol of the asset to trade + price: The price of the asset + order_side: The side of the order + amount (optional): The amount of the asset to trade + amount_trading_symbol (optional): The amount of the trading + symbol to trade + percentage (optional): The percentage of the portfolio to + allocate to the order + percentage_of_portfolio (optional): The percentage of + the portfolio to allocate to the order + percentage_of_position (optional): The percentage of + the position to allocate to the order. + (Only supported for SELL orders) + precision (optional): The precision of the amount + market (optional): The market to trade the asset + execute (optional): Default True. If set to True, the order + will be executed + validate (optional): Default True. If set to True, the order + will be validated + sync (optional): Default True. If set to True, the created + order will be synced with the portfolio of the algorithm + + Returns: + Order: Instance of the order created + """ + self.algorithm.create_limit_order( + target_symbol=target_symbol, + price=price, + order_side=order_side, + amount=amount, + amount_trading_symbol=amount_trading_symbol, + percentage=percentage, + percentage_of_portfolio=percentage_of_portfolio, + percentage_of_position=percentage_of_position, + precision=precision, + market=market, + execute=execute, + validate=validate, + sync=sync + ) def create_market_order( self, @@ -313,7 +322,7 @@ def create_market_order( validate: If set to True, the order will be validated sync: If set to True, the created order will be synced with the portfolio of the algorithm - + Returns: Order: Instance of the order created """ @@ -325,7 +334,7 @@ def create_market_order( execute=execute, validate=validate, sync=sync - ) + ) def close_position( self, symbol, market=None, identifier=None, precision=None @@ -385,7 +394,7 @@ def get_positions( amount_lt: The amount of the asset must be less than this amount_lte: The amount of the asset must be less than or equal to this - + Returns: List[Position]: A list of positions that match the query parameters """ @@ -416,7 +425,7 @@ def get_closed_trades(self) -> List[Trade]: """ Function to get all closed trades. This function will return all closed trades of the algorithm. - + Returns: List[Trade]: A list of closed trades """ @@ -439,7 +448,7 @@ def get_open_trades(self, target_symbol=None, market=None) -> List[Trade]: List[Trade]: A list of open trades that match the query parameters """ return self.algorithm.get_open_trades(target_symbol, market) - + def close_trade(self, trade, market=None, precision=None) -> None: """ Function to close a trade. This function will close a trade by @@ -455,7 +464,9 @@ def close_trade(self, trade, market=None, precision=None) -> None: Returns: None """ - self.algorithm.close_trade(trade=trade, market=market, precision=precision) + self.algorithm.close_trade( + trade=trade, market=market, precision=precision + ) def get_number_of_positions(self): """ @@ -490,7 +501,7 @@ def get_position( market=market, identifier=identifier ) - + def has_position( self, symbol, @@ -534,14 +545,14 @@ def has_balance(self, symbol, amount, market=None): """ Function to check if the portfolio has enough balance to create an order. This function will return True if the - portfolio has enough balance to create an order, False + portfolio has enough balance to create an order, False otherwise. Parameters: symbol: The symbol of the asset amount: The amount of the asset market: The market of the asset - + Returns: Boolean: True if the portfolio has enough balance """ diff --git a/investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py b/investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py index 60d3981..e61d5fd 100644 --- a/investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +++ b/investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py @@ -24,7 +24,8 @@ def __init__(self, start_date, end_date=None, name=None): if end_date < start_date: raise ValueError( "End date cannot be before start date for a backtest " - f"date range. (start_date: {start_date}, end_date: {end_date})" + "date range. " + + f"(start_date: {start_date}, end_date: {end_date})" ) @property diff --git a/investing_algorithm_framework/domain/services/market_data_sources.py b/investing_algorithm_framework/domain/services/market_data_sources.py index 7a3e222..49ac40f 100644 --- a/investing_algorithm_framework/domain/services/market_data_sources.py +++ b/investing_algorithm_framework/domain/services/market_data_sources.py @@ -169,7 +169,6 @@ def __init__( self._config = None self._storage_path = storage_path - if self._identifier is None: self._identifier = f"{self.market}_{self.symbol}" @@ -187,7 +186,7 @@ def initialize(self, config): @property def identifier(self): return self._identifier - + @identifier.setter def identifier(self, value): self._identifier = value diff --git a/investing_algorithm_framework/domain/utils/polars.py b/investing_algorithm_framework/domain/utils/polars.py index 284d3eb..20a13cb 100644 --- a/investing_algorithm_framework/domain/utils/polars.py +++ b/investing_algorithm_framework/domain/utils/polars.py @@ -22,7 +22,8 @@ def convert_polars_to_pandas( This is only used if add_index is set to True Returns: - DataFrame - Pandas DataFrame that has been converted from a Polars DataFrame + DataFrame - Pandas DataFrame that has been converted + from a Polars DataFrame """ data = data.to_pandas().copy() diff --git a/investing_algorithm_framework/indicators/__init__.py b/investing_algorithm_framework/indicators/__init__.py index a610449..9d7d3b7 100644 --- a/investing_algorithm_framework/indicators/__init__.py +++ b/investing_algorithm_framework/indicators/__init__.py @@ -1,5 +1,5 @@ from .advanced import get_peaks, is_divergence, is_lower_low_detected -from .trend import is_downtrend, get_rsi, is_uptrend, \ +from .trend import is_downtrend, is_uptrend, \ get_sma, get_up_and_downtrends, get_rsi, get_ema, get_adx from .utils import is_crossunder, is_crossover, is_above, \ is_below, has_crossed_upward, has_crossed_downward, \ diff --git a/investing_algorithm_framework/indicators/advanced.py b/investing_algorithm_framework/indicators/advanced.py index 1f1f89a..4c9b402 100644 --- a/investing_algorithm_framework/indicators/advanced.py +++ b/investing_algorithm_framework/indicators/advanced.py @@ -56,7 +56,7 @@ def get_lower_highs(data: np.array, order=5, K=2): condition is met, then the high at index i is considered a lower high. If the condition is not met, then the high at index i is not considered a lower high. - + Returns: extrema: list - A list of lists containing the indices of the consecutive lower highs in the data array. @@ -214,12 +214,14 @@ def get_peaks(data: pd.DataFrame, key, order=5, k=None): Parameters: data: DataFrame - The data to calculate the peaks for. column: str - The column to calculate the peaks for. - order: int - The number of periods (data points) to consider when calculating the peaks. - K: int - The number of consecutive peaks that need to be higher or lower in order to be classified as - a peak. - + order: int - The number of periods (data points) to consider + when calculating the peaks. + K: int - The number of consecutive peaks that need to be + higher or lower in order to be classified as a peak. + Returns: - DataFrame - The data DataFrame with the peaks calculated for the given key. + DataFrame - The data DataFrame with the peaks calculated + for the given key. """ vals = data[key].values hh_idx = get_higher_high_index(vals, order, K=k) @@ -253,108 +255,141 @@ def get_peaks(data: pd.DataFrame, key, order=5, k=None): return data -def is_divergence(data: pd.DataFrame, column_one: str, column_two: str, window_size=1, number_of_data_points=1): + +def is_divergence( + data: pd.DataFrame, + column_one: str, + column_two: str, + window_size=1, + number_of_data_points=1 +) -> bool: """ - Given two columns in a DataFrame with peaks and lows, check if there is a divergence. - Peaks and lows are calculated using the get_peaks function and look as follows: - [-1, 0] or [1, 0] or [0, -1, 0] or [0, 1, 0]. + Given two columns in a DataFrame with peaks and lows, check if + there is a divergence. + Peaks and lows are calculated using the get_peaks function + and look as follows: [-1, 0] or [1, 0] or [0, -1, 0] or [0, 1, 0]. For a bullish divergence: - * Indicator (First Column): Look for higher lows (-1) in a technical indicator, - such as RSI, MACD, or another momentum oscillator. - * Price Action (Second Column): Identify lower lows (1) in the price of the asset. - This indicates that the price is trending downwards. + * Indicator (First Column): Look for higher + lows (-1) in a technical indicator, such as RSI, MACD, or + another momentum oscillator. + * Price Action (Second Column): Identify lower lows (1) + in the price of the asset. This indicates that the price + is trending downwards. For a bearish divergence: - * Indicator (First Column): Look for lower highs (-1) in a technical indicator, - such as RSI, MACD, or another momentum oscillator. - * Price Action (Second Column): Identify higher highs (1) in the price of the asset. - This indicates that the price is trending upwards. - - A divergence occurs when the value of column_one makes a higher high or lower low and the + * Indicator (First Column): Look for lower highs (-1) in + a technical indicator, such as RSI, MACD, or + another momentum oscillator. + * Price Action (Second Column): Identify higher highs (1) + in the price of the asset. This indicates that the + price is trending upwards. + + A divergence occurs when the value of column_one makes + a higher high or lower low and the value of column_two makes a lower high or higher low. - This is represented by the following sequences: [-1, 0] or [1, 0] or [0, -1, 0] or [0, 1, 0]. - This indicates that column_one may be gaining momentum and could be due for a reversal. + This is represented by the following sequences: + [-1, 0] or [1, 0] or [0, -1, 0] or [0, 1, 0]. + This indicates that column_one may be gaining momentum + and could be due for a reversal. Parameters: data: DataFrame - The data to check for bullish divergence. column_one: str - The column to check for higher low. column_two: str - The column to check for lower low. - window_size: int - The windows size represent the total search space when checking for divergence. For example, - if the window_size is 1, the function will consider only the current two data - points, e.g. this will be true [1] and [-1] and false [0] and [-1]. - If the window_size is 2, the function will consider the current and previous data point, e.g. this will be true - [1, 0] and [0, -1] and false [0, 0] and [0, -1]. - number_of_data_points: int - The number of data points to consider when using a sliding windows size when checking for divergence. - For example, if the number_of_data_points is 1, the function will consider only the current two data points. - If the number_of_data_points is 4 and the window size is 2, the function will consider the current and previous 3 data points - when checking for divergence. Then the function will slide the window by 1 and check the next 2 data points until the end of the data. + window_size: int - The windows size represent the + total search space when checking for divergence. For example, + if the window_size is 1, the function will consider only the + current two data points, e.g. this will be true [1] and [-1] + and false [0] and [-1]. If the window_size is 2, + the function will consider the current and previous data point, + e.g. this will be true [1, 0] and [0, -1] + and false [0, 0] and [0, -1]. + number_of_data_points: int - The number of data points + to consider when using a sliding windows size when checking for + divergence. For example, if the number_of_data_points + is 1, the function will consider only the current two data points. + If the number_of_data_points is 4 and the window size is 2, + the function will consider the current and previous 3 data + points when checking for divergence. Then the function will + slide the window by 1 and check the next 2 data points until + the end of the data. Returns: Boolean - True if there is a bullish divergence, False otherwise. """ - + # Check if the two columns are in the data if column_one not in data.columns or column_two not in data.columns: - raise OperationalException(f"{column_one} and {column_two} columns are required in the data") + raise OperationalException( + f"{column_one} and {column_two} columns are required in the data" + ) if window_size < 1: raise OperationalException("Window size must be greater than 0") - + if len(data) < window_size: raise OperationalException( f"Data must have at least {window_size} data points." + f"It currently has {len(data)} data points" - ) + ) # Limit the DataFrame to the last `number_of_data_points` rows last_x_rows = data.tail(number_of_data_points) - + # Extract the column values as lists column_one_highs = last_x_rows[column_one].tolist() column_two_highs = last_x_rows[column_two].tolist() - + # Iterate through the rows up to the specified number_of_data_points - # Reverse iterate through the rows up to the specified + # Reverse iterate through the rows up to the specified # number_of_data_points for i, value in reversed(list(enumerate(column_one_highs))): - + if value == 0 or value == 1: continue - + if value == -1: # Select up to the window_size number of rows of the second column selected_window_column_two = column_two_highs[i:i + window_size] - for _, valueSecond in reversed(list(enumerate(selected_window_column_two))): + for _, valueSecond in reversed( + list(enumerate(selected_window_column_two)) + ): if valueSecond == 0: continue - + # Check if the sequence (-1, 1) occurs within the window if valueSecond == 1: return True - + if valueSecond == -1: valueSecond return False -def is_lower_low_detected(data: pd.DataFrame, column: str, number_of_data_points = 1) -> bool: + +def is_lower_low_detected( + data: pd.DataFrame, column: str, number_of_data_points=1 +) -> bool: """ - Function to check if a lower low is detected in the data. A lower low is detected - if the value of the column is -1 thar represents a peak. + Function to check if a lower low is detected in the data. A lower + low is detected if the value of the column is -1 thar represents a peak. - IMPORTANT: The data must have the column with the peaks calculated using the get_peaks function. The - get_peaks function calculates the peaks in the data and assigns the value of -1 to the column. You - can find the get_peaks function in the indicators module. + IMPORTANT: The data must have the column with the peaks + calculated using the get_peaks function. The get_peaks + function calculates the peaks in the data and assigns the value + of -1 to the column. You can find the get_peaks function in the + indicators module. Parameters: data: DataFrame - The data to check for lower low. column: str - The column to check for lower low. - number_of_data_points: int - The number of data points to consider when checking for lower low. + number_of_data_points: int - The number of data points + to consider when checking for lower low. Returns: Boolean - True if a lower low is detected, False otherwise. @@ -365,5 +400,5 @@ def is_lower_low_detected(data: pd.DataFrame, column: str, number_of_data_points for item in selected_column: if item == -1: return True - + return False diff --git a/investing_algorithm_framework/indicators/momentum.py b/investing_algorithm_framework/indicators/momentum.py index 360ff3d..536ff87 100644 --- a/investing_algorithm_framework/indicators/momentum.py +++ b/investing_algorithm_framework/indicators/momentum.py @@ -19,11 +19,19 @@ def get_willr(data: DataFrame, period=14, result_column="WILLR") -> DataFrame: """ # Check if high, low, and close columns are present in the data - if "High" not in data.columns or "Low" not in data.columns or "Close" not in data.columns: - raise OperationalException("High, Low, and Close columns are required in the data") - + if "High" not in data.columns or "Low" not in data.columns \ + or "Close" not in data.columns: + raise OperationalException( + "High, Low, and Close columns are required in the data" + ) + # Calculate williams%R - willr_values = tp.willr(data["High"].to_numpy(), data["Low"].to_numpy(), data["Close"].to_numpy(), period=period) + willr_values = tp.willr( + data["High"].to_numpy(), + data["Low"].to_numpy(), + data["Close"].to_numpy(), + period=period + ) # Pad NaN values for initial rows with a default value, e.g., 0 willr_values = np.concatenate((np.full(period - 1, 0), willr_values)) diff --git a/investing_algorithm_framework/indicators/trend.py b/investing_algorithm_framework/indicators/trend.py index 7fce2dc..e2c6f50 100644 --- a/investing_algorithm_framework/indicators/trend.py +++ b/investing_algorithm_framework/indicators/trend.py @@ -16,7 +16,9 @@ def is_uptrend( - data: Union[pd.DataFrame, pd.Series], fast_column="SMA_50", slow_column="SMA_200" + data: Union[pd.DataFrame, pd.Series], + fast_column="SMA_50", + slow_column="SMA_200" ) -> bool: """ Check if the price data is in a upturn. By default if will check if the @@ -46,10 +48,14 @@ def is_uptrend( # Check if the data keys are present in the data if fast_column not in data.columns: - raise OperationalException(f"Data column {fast_column} not present in the data.") + raise OperationalException( + f"Data column {fast_column} not present in the data." + ) if slow_column not in data.columns: - raise OperationalException(f"Data columns {slow_column} not present in the data.") + raise OperationalException( + f"Data columns {slow_column} not present in the data." + ) # Check if the index of the data is a datetime index if not isinstance(data.index, pd.DatetimeIndex): @@ -66,7 +72,9 @@ def is_uptrend( def is_downtrend( - data: Union[pd.DataFrame, pd.Series], fast_column="SMA_50", slow_column="SMA_200" + data: Union[pd.DataFrame, pd.Series], + fast_column="SMA_50", + slow_column="SMA_200" ) -> bool: """ Check if the price data is in a downturn. @@ -125,7 +133,6 @@ def is_crossover(data, key1, key2, strict=True) -> bool: and data[key1].iloc[-2] <= data[key2].iloc[-2] - def get_up_and_downtrends(data: pd.DataFrame) -> List[DateRange]: """ Function to get the up and down trends of a pandas dataframe. @@ -166,7 +173,9 @@ def get_up_and_downtrends(data: pd.DataFrame) -> List[DateRange]: continue if is_uptrend( - selected_rows, fast_column="SMA_Close_50", slow_column="SMA_Close_200" + selected_rows, + fast_column="SMA_Close_50", + slow_column="SMA_Close_200" ): if current_trend != 'Up': @@ -270,14 +279,15 @@ def get_sma( sma = tp.sma(data[source_column_name].to_numpy(), period=period) - # Pad NaN values for initial rows with a default value, e.g., 0 up to period - 1 + # Pad NaN values for initial rows with a default value, + # e.g., 0 up to period - 1 sma = np.concatenate((np.full(period - 1, 0), sma)) if result_column_name: data[result_column_name] = sma else: data[f"SMA_{source_column_name}_{period}"] = sma - + return data @@ -301,7 +311,7 @@ def get_rsi( dataframe where the result will be written to. If not set the result column is named 'RSI_{key}_{period}'. - + Returns: Pandas dataframe with RSI column added, named 'RSI_{period}' or named according to the @@ -317,7 +327,7 @@ def get_rsi( data[result_column_name] = rsi_values else: data[f"RSI_{period}"] = rsi_values - + return data diff --git a/investing_algorithm_framework/indicators/utils.py b/investing_algorithm_framework/indicators/utils.py index 9319c31..535844a 100644 --- a/investing_algorithm_framework/indicators/utils.py +++ b/investing_algorithm_framework/indicators/utils.py @@ -3,7 +3,11 @@ def is_crossover( - data: DataFrame, first_column: str, second_column: str, strict=True, number_of_data_points=1 + data: DataFrame, + first_column: str, + second_column: str, + strict=True, + number_of_data_points=1 ) -> bool: """ Check if the given keys have crossed over. @@ -15,10 +19,11 @@ def is_crossover( strict: bool - Whether to check for a strict crossover. Means that the first key has to be strictly greater than the second key. number_of_data_points: int - The number of data points to consider - for the crossover. Default is 1. If 2 then 2 data points will be considered. - which means that any first key has to be greater than the second key - for the last 2 data points. - + for the crossover. Default is 1. If 2 then 2 data points + will be considered. + which means that any first key has to be greater than the + second key for the last 2 data points. + Returns: bool - True if the first key has crossed over the second key. """ @@ -26,22 +31,29 @@ def is_crossover( if len(data) < 2: return False - # Loop through the data points and check if the first key is greater than the second key + # Loop through the data points and check if the first key + # is greater than the second key for i in range(number_of_data_points, 0, -1): if strict: - if data[first_column].iloc[-(i + 1)] < data[second_column].iloc[-(i + 1)] \ - and data[first_column].iloc[-i] > data[second_column].iloc[-i]: + if data[first_column].iloc[-(i + 1)] \ + < data[second_column].iloc[-(i + 1)] \ + and data[first_column].iloc[-i] \ + > data[second_column].iloc[-i]: return True else: - if data[first_column].iloc[-(i + 1)] <= data[second_column].iloc[-(i + 1)] \ - and data[first_column].iloc[-i] >= data[second_column].iloc[-i]: + if data[first_column].iloc[-(i + 1)] \ + <= data[second_column].iloc[-(i + 1)] \ + and data[first_column].iloc[-i] >= \ + data[second_column].iloc[-i]: return True return False -def is_crossunder(data: DataFrame, first_column: str, second_column: str, strict=True) -> bool: +def is_crossunder( + data: DataFrame, first_column: str, second_column: str, strict=True +) -> bool: """ Check if the given keys have crossed under. @@ -106,7 +118,7 @@ def has_crossed_upward(data: DataFrame, key, threshold, strict=True) -> bool: strict: bool - Whether to check for a strict crossover. Returns: - Boolean indicating if the key has crossed upward + Boolean indicating if the key has crossed upward through the threshold within the given data frame. """ @@ -141,7 +153,7 @@ def has_crossed_downward(data: DataFrame, key, threshold, strict=True) -> bool: strict: bool - Whether to check for a strict crossover. Returns: - Boolean indicating if the key has crossed downward + Boolean indicating if the key has crossed downward through the threshold within the given data frame. """ @@ -169,18 +181,19 @@ def has_any_lower_then_threshold( data: DataFrame, column, threshold, strict=True, number_of_data_points=1 ) -> bool: """ - Check if the given column has reached the threshold with a given number of data points. + Check if the given column has reached the threshold with a given + number of data points. Parameters: data: DataFrame - The data to check. column: str - The column to check. threshold: float - The threshold to check. strict: bool - Whether to check for a strict crossover downward. - number_of_data_points: int - The number of data points to consider + number_of_data_points: int - The number of data points to consider for the threshold. Default is 1. Returns: - bool - True if the column has reached the threshold by having a + bool - True if the column has reached the threshold by having a value lower then the threshold. """ if len(data) < number_of_data_points: @@ -188,38 +201,43 @@ def has_any_lower_then_threshold( selected_data = data[-number_of_data_points:] - # Check if any of the values in the column are lower or equal than the threshold + # Check if any of the values in the column are lower or + # equal than the threshold if strict: return (selected_data[column] < threshold).any() - + return (selected_data[column] <= threshold).any() -def has_any_higher_then_threshold(data: DataFrame, column, threshold, strict=True, number_of_data_points=1): +def has_any_higher_then_threshold( + data: DataFrame, column, threshold, strict=True, number_of_data_points=1 +) -> bool: """ - Check if the given column has reached the threshold with a given number of data points. + Check if the given column has reached the threshold with a given + number of data points. Parameters: data: DataFrame - The data to check. column: str - The column to check. threshold: float - The threshold to check. strict: bool - Whether to check for a strict crossover upward. - number_of_data_points: int - The number of data points to consider + number_of_data_points: int - The number of data points to consider for the threshold. Default is 1. Returns: - bool - True if the column has reached the threshold by having a - value higher then the threshold. + bool - True if the column has reached the threshold by + having a value higher then the threshold. """ if len(data) < number_of_data_points: return False selected_data = data[-number_of_data_points:] - # Check if any of the values in the column are higher or equal than the threshold + # Check if any of the values in the column are + # higher or equal than the threshold if strict: return (selected_data[column] > threshold).any() - + return (selected_data[column] >= threshold).any() @@ -227,17 +245,17 @@ def get_slope(data: DataFrame, column, number_of_data_points=10) -> float: """ Function to get the slope of the given column for the last n data points using linear regression. - + Parameters: data: DataFrame - The data to check. column: str - The column to check. number_of_data_points: int - The number of data points - to consider for the slope. Default is 10. - + to consider for the slope. Default is 10. + Returns: float - The slope of the given column for the last n data points. """ - + if len(data) < number_of_data_points or number_of_data_points < 2: return 0.0 @@ -249,17 +267,23 @@ def get_slope(data: DataFrame, column, number_of_data_points=10) -> float: # Create an array of x-values (0, 1, 2, ..., number_of_data_points-1) x_values = np.arange(number_of_data_points) - # Use numpy's polyfit to get the slope of the best-fit line (degree 1 for linear fit) + # Use numpy's polyfit to get the slope of the best-fit + # line (degree 1 for linear fit) slope, _ = np.polyfit(x_values, selected_data, 1) return slope + def has_slope_above_threshold( - data: DataFrame, column: str, threshold, number_of_data_points=10, window_size=10 + data: DataFrame, + column: str, + threshold, + number_of_data_points=10, + window_size=10 ) -> bool: """ Check if the slope of the given column is greater than the - threshold for the last n data points. If the + threshold for the last n data points. If the slope is not greater than the threshold for the last n data points, then the function will check the slope for the last n-1 data points and so on until @@ -281,24 +305,25 @@ def has_slope_above_threshold( if len(data) < number_of_data_points: return False - - if number_of_data_points < window_size: + + if number_of_data_points < window_size: raise ValueError( - "The number of data points should be larger or equal to the window size." + "The number of data points should be larger or equal" + + " to the window size." ) - + if window_size < number_of_data_points: difference = number_of_data_points - window_size else: slope = get_slope(data, column, number_of_data_points) return slope > threshold - + index = -(window_size) count = 0 # Loop over sliding windows that shrink from the beginning while count <= difference: - + if count == 0: selected_window = data.iloc[index:] else: @@ -306,7 +331,7 @@ def has_slope_above_threshold( count += 1 index -= 1 - + # Calculate the slope of the window with the given number of points slope = get_slope(selected_window, column, window_size) @@ -317,11 +342,15 @@ def has_slope_above_threshold( def has_slope_below_threshold( - data: DataFrame, column: str, threshold, number_of_data_points=10, window_size=10 + data: DataFrame, + column: str, + threshold, + number_of_data_points=10, + window_size=10 ) -> bool: """ Check if the slope of the given column is lower than the - threshold for the last n data points. If the + threshold for the last n data points. If the slope is not lower than the threshold for the last n data points, then the function will check the slope for the last n-1 data points and @@ -343,22 +372,24 @@ def has_slope_below_threshold( if len(data) < number_of_data_points: return False - - if number_of_data_points > window_size: - raise ValueError("The number of data points should be less than the window size.") - + + if number_of_data_points > window_size: + raise ValueError( + "The number of data points should be less than the window size." + ) + if window_size > number_of_data_points: difference = window_size - number_of_data_points else: slope = get_slope(data, column, number_of_data_points) return slope < threshold - + index = -(number_of_data_points) count = 0 # Loop over sliding windows that shrink from the beginning while count <= difference: - + if count == 0: selected_window = data.iloc[index:] else: @@ -366,10 +397,10 @@ def has_slope_below_threshold( count += 1 index -= 1 - + # Calculate the slope of the window with the given number of points slope = get_slope(selected_window, column, number_of_data_points) - + if slope < threshold: return True @@ -381,53 +412,56 @@ def has_values_below_threshold( ) -> bool: """ Detect if the last N data points in a column are below a certain threshold. - + Parameters: - df: pandas DataFrame - column: str, the column containing the values to analyze - threshold: float, the threshold for "low" values - number_of_data_points: int, the number of recent data points to analyze - proportion: float, the required proportion of values below the threshold - + Returns: - - bool: True if the last N data points are below the threshold, False otherwise + - bool: True if the last N data points are below the threshold, + False otherwise """ # Get the last `window_size` data points recent_data = df[column].iloc[-number_of_data_points:] proportion = proportion / 100 - + # Calculate the proportion of values below the threshold below_threshold = recent_data < threshold proportion_below = below_threshold.mean() - + # Determine if this window qualifies as a low period return proportion_below >= proportion + def has_values_above_threshold( df, column, threshold, number_of_data_points, proportion=100 ) -> bool: """ Detect if the last N data points in a column are above a certain threshold. - + Parameters: - df: pandas DataFrame - column: str, the column containing the values to analyze - threshold: float, the threshold for values - number_of_data_points: int, the number of recent data points to analyze - proportion: float, the required proportion of values below the threshold - + Returns: - - bool: True if the last N data points are above the threshold, False otherwise + - bool: True if the last N data points are above the threshold, + False otherwise """ # Get the last `window_size` data points recent_data = df[column].iloc[-number_of_data_points:] proportion = proportion / 100 - + # Calculate the proportion of values below the threshold above_threshold = recent_data < threshold proportion_below = above_threshold.mean() - + # Determine if this window qualifies as a low period return proportion_below >= proportion diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py index 419d7b6..f41ace9 100644 --- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py @@ -83,7 +83,7 @@ def prepare_data( time_frame: string - the time frame of the data window_size: int - the total amount of candle sticks that need to be returned - + Returns: None """ @@ -215,9 +215,10 @@ def get_data(self, **kwargs): f"End date {end_date} is after the end date " f"of the data source {self._end_date_data_source}" ) - + time_frame = TimeFrame.from_string(self.time_frame) - start_date = start_date - timedelta(minutes=time_frame.amount_of_minutes) + start_date = start_date - \ + timedelta(minutes=time_frame.amount_of_minutes) selection = self.data.filter( (self.data['Datetime'] >= start_date.strftime(DATETIME_FORMAT)) & (self.data['Datetime'] <= end_date.strftime(DATETIME_FORMAT)) @@ -553,12 +554,12 @@ def to_backtest_market_data_source(self) -> BacktestMarketDataSource: if self.window_size is None: raise OperationalException( - "Window_size should be defined before the " + + "Window_size should be defined before the " + "CCXTOHLCVMarketDataSource can be converted to " + "a backtest market data source. Make sure to set " + "the window_size attribute on your CCXTOHLCVMarketDataSource" ) - + return CCXTOHLCVBacktestMarketDataSource( identifier=self.identifier, market=self.market, @@ -746,7 +747,7 @@ def to_backtest_market_data_source(self) -> BacktestMarketDataSource: "the backtest_time_frame attribute on your " + "CCXTTickerMarketDataSource" ) - + return CCXTTickerBacktestMarketDataSource( identifier=self.identifier, market=self.market, diff --git a/investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py b/investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py index 040b5b6..2e4b27b 100644 --- a/investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py +++ b/investing_algorithm_framework/services/backtesting/backtest_report_writer_service.py @@ -31,11 +31,11 @@ def write_report_to_json( if not os.path.exists(output_directory): os.makedirs(output_directory) - + json_file_path = self.create_report_file_path( report, output_directory, extension=".json" ) - + report_dict = report.to_dict() # Convert dictionary to JSON json_data = json.dumps(report_dict, indent=4) @@ -58,9 +58,11 @@ def create_report_name(report, output_directory, extension=".json"): f"{backtest_end_date}_created-at_{created_at}{extension}" ) return file_path - + @staticmethod - def create_report_file_path(report, output_directory, extension=".json") -> str: + def create_report_file_path( + report, output_directory, extension=".json" + ) -> str: """ Function to create a file path for a backtest report. diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py index eff1076..62dc528 100644 --- a/investing_algorithm_framework/services/backtesting/backtest_service.py +++ b/investing_algorithm_framework/services/backtesting/backtest_service.py @@ -15,6 +15,13 @@ MarketDataSourceService +BACKTEST_REPORT_FILE_NAME_PATTERN = ( + r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_" + r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_" + r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$" +) + + class BacktestService: """ Service that facilitates backtests for algorithm objects. @@ -122,7 +129,7 @@ def run_backtests( Parameters - algorithms: The algorithms to run the backtests for - backtest_date_range: The backtest date range of the backtests - + Returns: List - A list of backtest reports """ @@ -450,16 +457,20 @@ def _check_if_required_market_data_sources_are_registered(self): ) def get_report( - self, algorithm_name: str, backtest_date_range: BacktestDateRange, directory: str + self, + algorithm_name: str, + backtest_date_range: BacktestDateRange, + directory: str ) -> BacktestReport: """ - Function to get a report based on the algorithm name and backtest date range if it exists. + Function to get a report based on the algorithm name and + backtest date range if it exists. Parameters: algorithm_name: str - The name of the algorithm backtest_date_range: BacktestDateRange - The backtest date range directory: str - The output directory - + Returns: BacktestReport - The backtest report if it exists, otherwise None """ @@ -467,36 +478,40 @@ def get_report( # Loop through all files in the output directory for root, _, files in os.walk(directory): for file in files: - # Check if the file contains the algorithm name and backtest date range - if self._is_backtest_report(os.path.join(root, file)): - # Read the file - with open(os.path.join(root, file), "r") as json_file: - - name = \ - self._get_backtest_report_algorithm_name_from_backtest_report_file( + # Check if the file contains the algorithm name + # and backtest date range + if self._is_backtest_report(os.path.join(root, file)): + # Read the file + with open(os.path.join(root, file), "r") as json_file: + + name = \ + self._get_algorithm_name_from_backtest_report_file( + os.path.join(root, file) + ) + + if name == algorithm_name: + backtest_start_date = \ + self._get_start_date_from_backtest_report_file( + os.path.join(root, file) + ) + backtest_end_date = \ + self._get_end_date_from_backtest_report_file( os.path.join(root, file) - ) - - if name == algorithm_name: - backtest_start_date = \ - self._get_backtest_start_date_from_backtest_report_file( - os.path.join(root, file) - ) - backtest_end_date = \ - self._get_backtest_end_date_from_backtest_report_file( - os.path.join(root, file) - ) - - if backtest_start_date == backtest_date_range.start_date \ - and backtest_end_date == backtest_date_range.end_date: - # Parse the JSON file - report = json.load(json_file) - # Convert the JSON file to a BacktestReport object - return BacktestReport.from_dict(report) - - return None - - def _get_backtest_start_date_from_backtest_report_file(self, path: str) -> datetime: + ) + + if backtest_start_date == \ + backtest_date_range.start_date \ + and backtest_end_date == \ + backtest_date_range.end_date: + # Parse the JSON file + report = json.load(json_file) + # Convert the JSON file to a + # BacktestReport object + return BacktestReport.from_dict(report) + + return None + + def _get_start_date_from_backtest_report_file(self, path: str) -> datetime: """ Function to get the backtest start date from a backtest report file. @@ -510,9 +525,11 @@ def _get_backtest_start_date_from_backtest_report_file(self, path: str) -> datet # Get the backtest start date from the file name backtest_start_date = os.path.basename(path).split("_")[3] # Parse the backtest start date - return datetime.strptime(backtest_start_date, DATETIME_FORMAT_BACKTESTING) - - def _get_backtest_end_date_from_backtest_report_file(self, path: str) -> datetime: + return datetime.strptime( + backtest_start_date, DATETIME_FORMAT_BACKTESTING + ) + + def _get_end_date_from_backtest_report_file(self, path: str) -> datetime: """ Function to get the backtest end date from a backtest report file. @@ -522,13 +539,15 @@ def _get_backtest_end_date_from_backtest_report_file(self, path: str) -> datetim Returns: datetime - The backtest end date """ - + # Get the backtest end date from the file name backtest_end_date = os.path.basename(path).split("_")[5] # Parse the backtest end date - return datetime.strptime(backtest_end_date, DATETIME_FORMAT_BACKTESTING) - - def _get_backtest_report_algorithm_name_from_backtest_report_file(self, path: str) -> str: + return datetime.strptime( + backtest_end_date, DATETIME_FORMAT_BACKTESTING + ) + + def _get_algorithm_name_from_backtest_report_file(self, path: str) -> str: """ Function to get the algorithm name from a backtest report file. @@ -538,7 +557,8 @@ def _get_backtest_report_algorithm_name_from_backtest_report_file(self, path: st Returns: str - The algorithm name """ - # Get the word between "report_" and "_backtest_start_date" it can contain _ + # Get the word between "report_" and "_backtest_start_date" + # it can contain _ # Get the algorithm name from the file name algorithm_name = os.path.basename(path).split("_")[1] return algorithm_name @@ -549,17 +569,19 @@ def _is_backtest_report(self, path: str) -> bool: Parameters: path: str - The path to the file - + Returns: bool - True if the file is a backtest report file, otherwise False """ # Check if the file is a JSON file if path.endswith(".json"): - - BACKTEST_REPORT_FILE_NAME_PATTERN = r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$" - # Check if the file name matches the backtest report file name pattern - if re.match(BACKTEST_REPORT_FILE_NAME_PATTERN, os.path.basename(path)): + + # Check if the file name matches the backtest + # report file name pattern + if re.match( + BACKTEST_REPORT_FILE_NAME_PATTERN, os.path.basename(path) + ): return True - + return False diff --git a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py index a4b04ff..b6adb77 100644 --- a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +++ b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py @@ -113,11 +113,11 @@ def get_ohlcv_market_data_source( self, symbol, time_frame=None, market=None ): """ - Function to get the OHLCV market data source for a symbol, time frame - and market. + Function to get the OHLCV market data source for a symbol, + time frame and market. Parameters: - symbol: str - The symbol of the asset + symbol: str - The symbol of the asset time_frame: TimeFrame - The time frame of the data market: str - The market of the asset @@ -137,7 +137,9 @@ def get_ohlcv_market_data_source( if market_data_source.market.lower() == market.lower()\ and market_data_source.symbol.lower() \ == symbol.lower() and \ - time_frame.equals(market_data_source.time_frame): + time_frame.equals( + market_data_source.time_frame + ): return market_data_source elif market is not None: if market_data_source.market.lower() == market.lower()\ @@ -147,7 +149,9 @@ def get_ohlcv_market_data_source( elif time_frame is not None: if market_data_source.symbol.lower() \ == symbol.lower() and \ - time_frame.equals(market_data_source.time_frame): + time_frame.equals( + market_data_source.time_frame + ): return market_data_source else: if market_data_source.symbol.lower() \