diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 2afb913bd..e5334fb46 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -13,6 +13,7 @@ from freqtrade.arguments import Arguments from freqtrade.arguments import TimeRange from freqtrade.strategy.interface import SellType from freqtrade.strategy.resolver import IStrategy, StrategyResolver +from collections import namedtuple import sys logger = logging.getLogger(__name__) @@ -21,16 +22,11 @@ logger = logging.getLogger(__name__) class Edge(): config: Dict = {} - _last_updated: int # Timestamp of pairs last updated time - _cached_pairs: list = [] # Keeps an array of - # [pair, stoploss, winrate, risk reward ratio, required risk reward, expectancy] - - _total_capital: float - _allowed_risk: float - _since_number_of_days: int - _timerange: TimeRange + _cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs def __init__(self, config: Dict[str, Any], exchange=None) -> None: + + # Increasing recursive limit as with need it for large datasets sys.setrecursionlimit(10000) self.config = config self.exchange = exchange @@ -42,13 +38,18 @@ class Edge(): self.advise_buy = self.strategy.advise_buy self.edge_config = self.config.get('edge', {}) - self._cached_pairs: list = [] - self._total_capital = self.edge_config.get('total_capital_in_stake_currency') - self._allowed_risk = self.edge_config.get('allowed_risk') - self._since_number_of_days = self.edge_config.get('calculate_since_number_of_days', 14) - self._last_updated = 0 - self._timerange = Arguments.parse_timerange("%s-" % arrow.now().shift( + # pair info data type + self._pair_info = namedtuple( + 'pair_info', 'stoploss, winrate, risk_reward_ratio, required_risk_reward, expectancy') + self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs + + self._total_capital: float = self.edge_config.get('total_capital_in_stake_currency') + self._allowed_risk: float = self.edge_config.get('allowed_risk') + self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14) + self._last_updated: int = 0 # Timestamp of pairs last updated time + + self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift( days=-1 * self._since_number_of_days).format('YYYYMMDD')) self.fee = self.exchange.get_fee() @@ -132,34 +133,24 @@ class Edge(): return True def stake_amount(self, pair: str) -> float: - info = [x for x in self._cached_pairs if x[0] == pair][0] - stoploss = info[1] + stoploss = self._cached_pairs[pair].stoploss allowed_capital_at_risk = round(self._total_capital * self._allowed_risk, 5) position_size = abs(round((allowed_capital_at_risk / stoploss), 5)) return position_size def stoploss(self, pair: str) -> float: - info = [x for x in self._cached_pairs if x[0] == pair][0] - return info[1] + return self._cached_pairs[pair].stoploss def filter(self, pairs) -> list: - # Filtering pairs acccording to the expectancy - filtered_expectancy: list = [] - # [pair, stoploss, winrate, risk reward ratio, required risk reward, expectancy] - filtered_expectancy = [ - x[0] for x in self._cached_pairs if ( - (x[5] > float( - self.edge_config.get( - 'minimum_expectancy', - 0.2))) & ( - x[2] > float( - self.edge_config.get( - 'minimum_winrate', - 0.60))))] + final = [] + + for pair, info in self._cached_pairs.items(): + if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ + info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \ + pair in pairs: + final.append(pair) - # Only return pairs which are included in "pairs" argument list - final = [x for x in filtered_expectancy if x in pairs] if final: logger.info( 'Edge validated only %s', @@ -220,7 +211,7 @@ class Edge(): return result - def _process_expectancy(self, results: DataFrame) -> list: + def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]: """ This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs The calulation will be done per pair and per strategy. @@ -246,7 +237,7 @@ class Edge(): ####################################################################### if results.empty: - return [] + return {} groupby_aggregator = { 'profit_abs': [ @@ -286,12 +277,17 @@ class Edge(): df = df.sort_values(by=['expectancy', 'stoploss'], ascending=False).groupby( 'pair').first().sort_values(by=['expectancy'], ascending=False).reset_index() - # dropping unecessary columns - df.drop(columns=['nb_loss_trades', 'nb_win_trades', 'average_win', 'average_loss', - 'profit_sum', 'loss_sum', 'avg_trade_duration', 'nb_trades'], inplace=True) + final = {} + for x in df.itertuples(): + final[x.pair] = self._pair_info( + x.stoploss, + x.winrate, + x.risk_reward_ratio, + x.required_risk_reward, + x.expectancy) - # Returning an array of pairs in order of "expectancy" - return df.values + # Returning a list of pairs in order of "expectancy" + return final def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range): buy_column = ticker_data['buy'].values diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index 510a79af1..710920e91 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -2,6 +2,7 @@ from freqtrade.tests.conftest import get_patched_exchange from freqtrade.edge import Edge from pandas import DataFrame, to_datetime from freqtrade.strategy.interface import SellType +from collections import namedtuple import arrow import numpy as np import math @@ -20,17 +21,19 @@ from unittest.mock import MagicMock ticker_start_time = arrow.get(2018, 10, 3) ticker_interval_in_minute = 60 _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} +_pair_info = namedtuple( + 'pair_info', 'stoploss, winrate, risk_reward_ratio, required_risk_reward, expectancy') def test_filter(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf) edge = Edge(default_conf, exchange) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( - return_value=[ - ['E/F', -0.01, 0.66, 3.71, 0.50, 1.71], - ['C/D', -0.01, 0.66, 3.71, 0.50, 1.71], - ['N/O', -0.01, 0.66, 3.71, 0.50, 1.71] - ] + return_value={ + 'E/F': _pair_info(-0.01, 0.66, 3.71, 0.50, 1.71), + 'C/D': _pair_info(-0.01, 0.66, 3.71, 0.50, 1.71), + 'N/O': _pair_info(-0.01, 0.66, 3.71, 0.50, 1.71) + } )) pairs = ['A/B', 'C/D', 'E/F', 'G/H'] @@ -41,11 +44,11 @@ def test_stoploss(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf) edge = Edge(default_conf, exchange) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( - return_value=[ - ['E/F', -0.01, 0.66, 3.71, 0.50, 1.71], - ['C/D', -0.01, 0.66, 3.71, 0.50, 1.71], - ['N/O', -0.01, 0.66, 3.71, 0.50, 1.71] - ] + return_value={ + 'E/F': _pair_info(-0.01, 0.66, 3.71, 0.50, 1.71), + 'C/D': _pair_info(-0.01, 0.66, 3.71, 0.50, 1.71), + 'N/O': _pair_info(-0.01, 0.66, 3.71, 0.50, 1.71) + } )) assert edge.stoploss('E/F') == -0.01 @@ -61,7 +64,7 @@ def _validate_ohlc(buy_ohlc_sell_matrice): def _build_dataframe(buy_ohlc_sell_matrice): _validate_ohlc(buy_ohlc_sell_matrice) - tickers = [] + tickers= [] for ohlc in buy_ohlc_sell_matrice: ticker = { 'date': ticker_start_time.shift( @@ -79,9 +82,9 @@ def _build_dataframe(buy_ohlc_sell_matrice): frame = DataFrame(tickers) frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + unit = 'ms', + utc = True, + infer_datetime_format = True) return frame @@ -92,17 +95,17 @@ def _time_on_candle(number): def test_edge_heartbeat_calculate(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf) - edge = Edge(default_conf, exchange) - heartbeat = default_conf['edge']['process_throttle_secs'] + exchange=get_patched_exchange(mocker, default_conf) + edge=Edge(default_conf, exchange) + heartbeat=default_conf['edge']['process_throttle_secs'] # should not recalculate if heartbeat not reached - edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1 + edge._last_updated=arrow.utcnow().timestamp - heartbeat + 1 assert edge.calculate() is False -def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, +def mocked_load_data(datadir, pairs = [], ticker_interval = '0m', refresh_pairs = False, timerange=None, exchange=None): hz = 0.1 base = 0.001 @@ -202,13 +205,12 @@ def test_process_expectancy(mocker, default_conf): final = edge._process_expectancy(trades_df) assert len(final) == 1 - assert final[0][0] == 'TEST/BTC' - assert final[0][1] == -0.9 - assert round(final[0][2], 10) == 0.3333333333 - assert round(final[0][3], 10) == 306.5384615384 - assert round(final[0][4], 10) == 2.0 - assert round(final[0][5], 10) == 101.5128205128 - + assert 'TEST/BTC' in final + assert final['TEST/BTC'].stoploss == -0.9 + assert round(final['TEST/BTC'].winrate, 10) == 0.3333333333 + assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384 + assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0 + assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128 # 1) Open trade should be removed from the end def test_case_1(mocker, default_conf):