From f0a154692de57912c1d5d9596133c17808f152f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 06:37:42 +0100 Subject: [PATCH 001/348] Wallets should use trade_proxy --- freqtrade/wallets.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index d7dcfd487..078bcd07e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import Trade +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -26,13 +27,14 @@ class Wallet(NamedTuple): class Wallets: - def __init__(self, config: dict, exchange: Exchange) -> None: + def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: self._config = config self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 - self.update() + if not skip_update: + self.update() def get_free(self, currency: str) -> float: balance = self._wallets.get(currency) @@ -64,8 +66,8 @@ class Wallets: """ # Recreate _wallets to reset closed trade balances _wallets = {} - closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() + closed_trades = Trade.get_trades_proxy(is_open=False) + open_trades = Trade.get_trades_proxy(is_open=True) tot_profit = sum([trade.calc_profit() for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) @@ -102,7 +104,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - def update(self, require_update: bool = True) -> None: + def update(self, require_update: bool = True, log: bool = True) -> None: """ Updates wallets from the configured version. By default, updates from the exchange. @@ -111,11 +113,12 @@ class Wallets: :param require_update: Allow skipping an update if balances were recently refreshed """ if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): - if self._config['dry_run']: - self._update_dry() - else: + if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE): self._update_live() - logger.info('Wallets synced.') + else: + self._update_dry() + if log: + logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp def get_all_balances(self) -> Dict[str, Any]: From 9361aa1c95d5a408a4015e4ae5b04d29fd129b58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jan 2021 07:06:58 +0100 Subject: [PATCH 002/348] Add wallets to backtesting --- freqtrade/optimize/backtesting.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3186313e1..b68732d5c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -28,6 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) @@ -114,6 +115,8 @@ class Backtesting: if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) + self.wallets = Wallets(self.config, self.exchange) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy @@ -176,6 +179,10 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() + def update_wallets(self): + if self.wallets: + self.wallets.update(log=False) + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -276,8 +283,10 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = SellType.FORCE_SELL trade.close(sell_row[OPEN_IDX], show_msg=False) - trade.is_open = True - trades.append(trade) + # Deepcopy object to have wallets update correctly + trade1 = deepcopy(trade) + trade1.is_open = True + trades.append(trade1) return trades def backtest(self, processed: Dict, stake_amount: float, @@ -346,6 +355,7 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + self.update_wallets() # Enter trade trade = Trade( pair=pair, @@ -372,6 +382,7 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: + self.update_wallets() # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) @@ -384,6 +395,7 @@ class Backtesting: tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) + self.update_wallets() return trade_list_to_dataframe(trades) @@ -425,6 +437,7 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) + print(self.wallets.get_all_balances()) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, From 4ce4eadc2366f6f58ab0811bc2fc01e2018627e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jan 2021 07:15:04 +0100 Subject: [PATCH 003/348] remove only ccxt objects when hyperopting --- freqtrade/optimize/hyperopt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index eee0f13b3..9cc5f2059 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -661,7 +661,9 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt - self.backtesting.exchange = None # type: ignore + self.backtesting.exchange._api = None # type: ignore + self.backtesting.exchange._api_async = None # type: ignore + # self.backtesting.exchange = None # type: ignore self.backtesting.pairlists = None # type: ignore self.backtesting.strategy.dp = None # type: ignore IStrategy.dp = None # type: ignore From b5177eadabe2a349382d7ee537aaae423942435f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 10:22:59 +0100 Subject: [PATCH 004/348] Extract close method for exchange --- freqtrade/exchange/exchange.py | 3 +++ freqtrade/optimize/hyperopt.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 617cd6c26..0e9a90548 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -147,6 +147,9 @@ class Exchange: """ Destructor - clean up async stuff """ + self.close() + + def close(self): logger.debug("Exchange object destroyed, closing async loop") if self._api_async and inspect.iscoroutinefunction(self._api_async.close): asyncio.get_event_loop().run_until_complete(self._api_async.close()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9cc5f2059..155f1e69b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -661,6 +661,7 @@ class Hyperopt: dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt + self.backtesting.exchange.close() self.backtesting.exchange._api = None # type: ignore self.backtesting.exchange._api_async = None # type: ignore # self.backtesting.exchange = None # type: ignore From 712d503e6ca51acde5a676f833f073018d465cab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Feb 2021 10:30:50 +0100 Subject: [PATCH 005/348] Use sell-reason value in backtesting, not the enum object --- freqtrade/optimize/backtesting.py | 9 +++++---- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/persistence/models.py | 12 ++++++++++-- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 2 +- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b68732d5c..718fd2c42 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -259,11 +259,11 @@ class Backtesting: sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type + trade.sell_reason = sell.sell_type.value + trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) trade.close(closerate, show_msg=False) return trade @@ -281,7 +281,7 @@ class Backtesting: sell_row = data[pair][-1] trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = SellType.FORCE_SELL + trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) # Deepcopy object to have wallets update correctly trade1 = deepcopy(trade) @@ -366,6 +366,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, + exchange='backtesting', ) # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 88b2028ba..6338b1d71 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -132,7 +132,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List tabular_data.append( { - 'sell_reason': reason.value, + 'sell_reason': reason, 'trades': count, 'wins': len(result[result['profit_abs'] > 0]), 'draws': len(result[result['profit_abs'] == 0]), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dff59819c..a05aa2c96 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -268,6 +268,14 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') + @property + def open_date_utc(self): + return self.open_date.replace(tzinfo=timezone.utc) + + @property + def close_date_utc(self): + return self.close_date.replace(tzinfo=timezone.utc) + def to_json(self) -> Dict[str, Any]: return { 'trade_id': self.id, @@ -306,9 +314,9 @@ class Trade(_DECL_BASE): 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'close_profit_abs': self.close_profit_abs, # Deprecated - 'trade_duration_s': (int((self.close_date - self.open_date).total_seconds()) + 'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds()) if self.close_date else None), - 'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60) + 'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60) if self.close_date else None), 'profit_ratio': self.close_profit, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index daf7c2053..c9499cc42 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -514,6 +514,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] - assert res.sell_reason == trade.sell_reason + assert res.sell_reason == trade.sell_reason.value assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c8d4338af..db14749c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -486,7 +486,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'trade_duration': [235, 40], 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'sell_reason': [SellType.ROI, SellType.ROI], + 'sell_reason': [SellType.ROI.value, SellType.ROI.value], 'initial_stop_loss_abs': [0.0940005, 0.09272236], 'initial_stop_loss_ratio': [-0.1, -0.1], 'stop_loss_abs': [0.0940005, 0.09272236], diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 51a78c7cc..8b64c2764 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -265,7 +265,7 @@ def test_generate_sell_reason_stats(): 'wins': [2, 0, 0], 'draws': [0, 0, 0], 'losses': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + 'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value] } ) From e32b2097f0010127f0bbb095a3968ccc5c71f6c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Feb 2021 10:20:43 +0100 Subject: [PATCH 006/348] Use timestamp in UTC timezone for ROI comparisons --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2f64f3dac..fd2f8bdd0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -932,7 +932,7 @@ class FreqtradeBot(LoggingMixin): Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8a0b27e96..6d40e56cc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -649,7 +649,7 @@ class IStrategy(ABC): :return: True if bot should sell at current rate """ # Check if time matches and current rate is above threshold - trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60) _, roi = self.min_roi_reached_entry(trade_dur) if roi is None: return False From 081b9be45c072d4d39f5672122d3c5dbbbf5aa07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Feb 2021 10:49:47 +0100 Subject: [PATCH 007/348] use get_all_locks to get locks for backtest result --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 718fd2c42..f37107767 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -442,7 +442,7 @@ class Backtesting: self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, - 'locks': PairLocks.locks, + 'locks': PairLocks.get_all_locks(), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 8644146d8..f0048bb52 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -123,3 +123,11 @@ class PairLocks(): now = datetime.now(timezone.utc) return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now) + + @staticmethod + def get_all_locks() -> List[PairLock]: + + if PairLocks.use_db: + return PairLock.query.all() + else: + return PairLocks.locks From 20455de2a9533ebbf3990b9efe241a1bec1543e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Feb 2021 20:22:33 +0100 Subject: [PATCH 008/348] Small enhancements to docs --- docs/strategy-customization.md | 2 +- freqtrade/persistence/migrations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fdc95a3c1..fd733c88e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -709,7 +709,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning - Locking pairs is not available during backtesting. + Manually locking pairs is not available during backtesting, only locks via Protections are allowed. #### Pair locking example diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index ed976c2a9..961363b0e 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -141,7 +141,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') - if 'orders' not in previous_tables: + if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: From 0faa6f84dcd6ee8d5990fd8f2bbe0c7fed80dac9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:23:11 +0100 Subject: [PATCH 009/348] Improve Wallet logging disabling for backtesting --- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/wallets.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f37107767..7f2ba60f2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -116,6 +116,7 @@ class Backtesting: self.protections = ProtectionManager(self.config) self.wallets = Wallets(self.config, self.exchange) + self.wallets._log = False # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -181,7 +182,7 @@ class Backtesting: def update_wallets(self): if self.wallets: - self.wallets.update(log=False) + self.wallets.update() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 078bcd07e..9562f34e6 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -29,6 +29,7 @@ class Wallets: def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: self._config = config + self._log = True self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] @@ -104,7 +105,7 @@ class Wallets: if currency not in balances: del self._wallets[currency] - def update(self, require_update: bool = True, log: bool = True) -> None: + def update(self, require_update: bool = True) -> None: """ Updates wallets from the configured version. By default, updates from the exchange. @@ -117,7 +118,7 @@ class Wallets: self._update_live() else: self._update_dry() - if log: + if self._log: logger.info('Wallets synced.') self._last_wallet_refresh = arrow.utcnow().int_timestamp From 0754a7a78f36d379d9332de0bfef3124780debe0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:33:39 +0100 Subject: [PATCH 010/348] total_open_trades_stake should support no-db mode --- freqtrade/persistence/models.py | 10 +++++++--- tests/conftest.py | 20 +++++++++++++------- tests/conftest_trades.py | 4 ++++ tests/test_persistence.py | 8 ++++++-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a05aa2c96..f72705c34 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -671,9 +671,13 @@ class Trade(_DECL_BASE): Calculates total invested amount in open trades in stake currency """ - total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ - .filter(Trade.is_open.is_(True))\ - .scalar() + if Trade.use_db: + total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ + .filter(Trade.is_open.is_(True))\ + .scalar() + else: + total_open_stake_amount = sum( + t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) return total_open_stake_amount or 0 @staticmethod diff --git a/tests/conftest.py b/tests/conftest.py index 61899dd53..946ae1fb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -183,28 +183,34 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee): +def create_mock_trades(fee, use_db: bool = True): """ Create some fake trades ... """ + def add_trade(trade): + if use_db: + Trade.session.add(trade) + else: + Trade.trades.append(trade) + # Simulate dry_run entries trade = mock_trade_1(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_2(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_3(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_4(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_5(fee) - Trade.session.add(trade) + add_trade(trade) trade = mock_trade_6(fee) - Trade.session.add(trade) + add_trade(trade) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index fa9910b8d..6a42d04e3 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -28,6 +28,7 @@ def mock_trade_1(fee): amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', open_order_id='dry_run_buy_12345', @@ -180,6 +181,7 @@ def mock_trade_4(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', open_order_id='prod_buy_12345', @@ -230,6 +232,7 @@ def mock_trade_5(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.123, exchange='bittrex', strategy='SampleStrategy', @@ -281,6 +284,7 @@ def mock_trade_6(fee): amount_requested=2.0, fee_open=fee.return_value, fee_close=fee.return_value, + is_open=True, open_rate=0.15, exchange='bittrex', strategy='SampleStrategy', diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d0d29f142..1fced3e16 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1039,14 +1039,18 @@ def test_fee_updated(fee): @pytest.mark.usefixtures("init_persistence") -def test_total_open_trades_stakes(fee): +@pytest.mark.parametrize('use_db', [True, False]) +def test_total_open_trades_stakes(fee, use_db): + Trade.use_db = use_db res = Trade.total_open_trades_stakes() assert res == 0 - create_mock_trades(fee) + create_mock_trades(fee, use_db) res = Trade.total_open_trades_stakes() assert res == 0.004 + Trade.use_db = True + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): From 959ff990460acd0e137c0c2aaccbb6cfc1efd932 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 19:45:59 +0100 Subject: [PATCH 011/348] Add Dry-run wallet CLI option --- docs/backtesting.md | 4 ++++ docs/bot-usage.md | 4 ++++ docs/configuration.md | 2 +- docs/hyperopt.md | 6 +++++- freqtrade/commands/arguments.py | 6 +++--- freqtrade/commands/cli_options.py | 5 +++++ freqtrade/configuration/configuration.py | 4 +++- freqtrade/wallets.py | 1 + 8 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index a14c8f2e4..38d1af45a 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -16,6 +16,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--eps] [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -48,6 +49,9 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to backtest. Please note that ticker-interval needs to be diff --git a/docs/bot-usage.md b/docs/bot-usage.md index c7fe8634d..4ff6168a0 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -56,6 +56,7 @@ optional arguments: usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [--db-url PATH] [--sd-notify] [--dry-run] + [--dry-run-wallet DRY_RUN_WALLET] optional arguments: -h, --help show this help message and exit @@ -66,6 +67,9 @@ optional arguments: --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/configuration.md b/docs/configuration.md index 0163e1671..663d9c5b2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,7 +49,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean -| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float | `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions.
*Defaults to `false`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ec155062f..ee3d75d0b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -43,7 +43,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] [-e INT] + [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] @@ -82,6 +83,9 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections + --dry-run-wallet DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. -e INT, --epochs INT Specify number of epochs (default: 100). --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] Specify which parameters to hyperopt. Space-separated diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index c64c11a18..88cec7b3e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -14,18 +14,18 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", ] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", - "enable_protections", + "enable_protections", "dry_run_wallet", "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "position_stacking", "use_max_market_positions", - "enable_protections", + "enable_protections", "dry_run_wallet", "epochs", "spaces", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 7dc85377d..90ebb5e6a 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -110,6 +110,11 @@ AVAILABLE_CLI_OPTIONS = { help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', action='store_true', ), + "dry_run_wallet": Arg( + '--dry-run-wallet', + help='Starting balance, used for backtesting / hyperopt and dry-runs.', + type=float, + ), # Optimize common "timeframe": Arg( '-i', '--timeframe', '--ticker-interval', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7bf3e6bf2..6295d01d4 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -232,7 +232,9 @@ class Configuration: self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') - + self._args_to_config(config, argname='dry_run_wallet', + logstring='Parameter --dry-run-wallet detected, ' + 'overriding dry_run_wallet to: {} ...') self._args_to_config(config, argname='fee', logstring='Parameter --fee detected, ' 'setting fee to: {} ...') diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9562f34e6..f5ce4c102 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -158,6 +158,7 @@ class Wallets: Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount + :raise: DependencyException if balance is lower than stake-amount """ available_amount = self._get_available_stake_amount() From e4abe902fc924b30c41ddc67edb744eb2095951e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Feb 2021 20:37:55 +0100 Subject: [PATCH 012/348] Enable compounding for backtesting --- freqtrade/optimize/backtesting.py | 66 ++++++++++++++------------ freqtrade/optimize/hyperopt.py | 1 - tests/optimize/test_backtest_detail.py | 1 - tests/optimize/test_backtesting.py | 6 --- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7f2ba60f2..7ed5064e7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, @@ -270,6 +270,30 @@ class Backtesting: return None + def _enter_trade(self, pair: str, row, max_open_trades: int, + open_trade_count: int) -> Optional[Trade]: + self.update_wallets() + try: + stake_amount = self.wallets.get_trade_stake_amount( + pair, max_open_trades - open_trade_count, None) + except DependencyException: + stake_amount = 0 + if stake_amount: + # Enter trade + trade = Trade( + pair=pair, + open_rate=row[OPEN_IDX], + open_date=row[DATE_IDX], + stake_amount=stake_amount, + amount=round(stake_amount / row[OPEN_IDX], 8), + fee_open=self.fee, + fee_close=self.fee, + is_open=True, + exchange='backtesting', + ) + return trade + return None + def handle_left_open(self, open_trades: Dict[str, List[Trade]], data: Dict[str, List[Tuple]]) -> List[Trade]: """ @@ -290,7 +314,7 @@ class Backtesting: trades.append(trade1) return trades - def backtest(self, processed: Dict, stake_amount: float, + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, enable_protections: bool = False) -> DataFrame: @@ -302,7 +326,6 @@ class Backtesting: Avoid extensive logging in this method and functions it calls. :param processed: a processed dictionary with format {pair, data} - :param stake_amount: amount to use for each trade :param start_date: backtesting timerange start datetime :param end_date: backtesting timerange end datetime :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited @@ -310,10 +333,6 @@ class Backtesting: :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ - logger.debug(f"Run backtest, stake_amount: {stake_amount}, " - f"start_date: {start_date}, end_date: {end_date}, " - f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" - ) trades: List[Trade] = [] self.prepare_backtest(enable_protections) @@ -356,30 +375,18 @@ class Backtesting: and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): - self.update_wallets() - # Enter trade - trade = Trade( - pair=pair, - open_rate=row[OPEN_IDX], - open_date=row[DATE_IDX], - stake_amount=stake_amount, - amount=round(stake_amount / row[OPEN_IDX], 8), - fee_open=self.fee, - fee_close=self.fee, - is_open=True, - exchange='backtesting', - ) - # TODO: hacky workaround to avoid opening > max_open_trades - # This emulates previous behaviour - not sure if this is correct - # Prevents buying if the trade-slot was freed in this candle - open_trade_count_start += 1 - open_trade_count += 1 - # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") - open_trades[pair].append(trade) - Trade.trades.append(trade) + trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) + if trade: + # TODO: hacky workaround to avoid opening > max_open_trades + # This emulates previous behaviour - not sure if this is correct + # Prevents buying if the trade-slot was freed in this candle + open_trade_count_start += 1 + open_trade_count += 1 + # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + open_trades[pair].append(trade) + Trade.trades.append(trade) for trade in open_trades[pair]: - # since indexes has been incremented before, we need to go one step back to # also check the buying candle for sell conditions. trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured @@ -431,7 +438,6 @@ class Backtesting: # Execute backtest and store results results = self.backtest( processed=preprocessed, - stake_amount=self.config['stake_amount'], start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=max_open_trades, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 155f1e69b..79ecb6052 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -537,7 +537,6 @@ class Hyperopt: backtesting_results = self.backtesting.backtest( processed=processed, - stake_amount=self.config['stake_amount'], start_date=min_date.datetime, end_date=max_date.datetime, max_open_trades=self.max_open_trades, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index c9499cc42..4d6605b9f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -503,7 +503,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( processed=data_processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=10, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index db14749c3..620bd1df5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -90,7 +90,6 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None: assert isinstance(processed, dict) results = backtesting.backtest( processed=processed, - stake_amount=config['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=1, @@ -111,7 +110,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): min_date, max_date = get_timerange(processed) return { 'processed': processed, - 'stake_amount': conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 10, @@ -461,7 +459,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=10, @@ -523,7 +520,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, - stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=1, @@ -678,7 +674,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) min_date, max_date = get_timerange(processed) backtest_conf = { 'processed': processed, - 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 3, @@ -694,7 +689,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtest_conf = { 'processed': processed, - 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, 'max_open_trades': 1, From 8d61a263823943cdbdb911d2c40bc283ba415903 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Feb 2021 20:20:32 +0100 Subject: [PATCH 013/348] Allow dynamic stake for backtesting and hyperopt --- freqtrade/commands/optimize_commands.py | 14 +++++++++----- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 7 ++++--- tests/optimize/test_hyperopt.py | 8 ++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 7411ca9c6..bf36972c4 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -3,7 +3,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -23,10 +23,14 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.HYPEROPT: 'hyperoptimization', } if (method in no_unlimited_runmodes.keys() and - config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT): - raise DependencyException( - f'The value of `stake_amount` cannot be set as "{constants.UNLIMITED_STAKE_AMOUNT}" ' - f'for {no_unlimited_runmodes[method]}') + config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT and + config['max_open_trades'] != float('inf')): + pass + # config['dry_run_wallet'] = config['stake_amount'] * \ + # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) + + # logger.warning(f"Changing dry-run-wallet to {config['dry_run_wallet']} " + # "(max_open_trades * stake_amount).") return config diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7ed5064e7..29559126b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -445,11 +445,11 @@ class Backtesting: enable_protections=self.config.get('enable_protections', False), ) backtest_end_time = datetime.now(timezone.utc) - print(self.wallets.get_all_balances()) self.all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), + 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), } diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 620bd1df5..061bcbaa0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -17,7 +17,7 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode @@ -242,8 +242,9 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con '--strategy', 'DefaultStrategy', ] - with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): - setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + # TODO: does this test still make sense? + conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + assert isinstance(conf, dict) def test_start(mocker, fee, default_conf, caplog) -> None: diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 68eb3d6f7..88a4cea2d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,7 +15,7 @@ from filelock import Timeout from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -140,9 +140,9 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', ] - - with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): - setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + # TODO: does this test still make sense? + conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + assert isinstance(conf, dict) def test_hyperoptresolver(mocker, default_conf, caplog) -> None: From 35e6a9ab3aa70cad59d8cc75b50bedecdce56e0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Feb 2021 09:01:05 +0100 Subject: [PATCH 014/348] Backtest-reports should calculate total gains based on starting capital --- docs/backtesting.md | 18 +++++++++++--- freqtrade/optimize/optimize_reports.py | 32 +++++++++++++++++-------- tests/conftest.py | 1 + tests/optimize/test_optimize_reports.py | 10 ++++---- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 38d1af45a..eab64a7a9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,7 +252,10 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Total Profit % | 152.41% | +| Starting capital | 0.01000000 BTC | +| End capital | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total Profit % | 76.2% | | Trades per day | 3.575 | | | | | Best Pair | LSK/BTC 26.26% | @@ -261,6 +264,7 @@ A backtesting result will look like that: | Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | +| Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | @@ -328,7 +332,10 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Total Profit % | 152.41% | +| Starting capital | 0.01000000 BTC | +| End capital | 0.01762792 BTC | +| Absolute profit | 0.00762792 BTC | +| Total Profit % | 76.2% | | Trades per day | 3.575 | | | | | Best Pair | LSK/BTC 26.26% | @@ -337,6 +344,7 @@ It contains some useful key metrics about performance of your strategy on backte | Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | +| Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | @@ -351,11 +359,15 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. -- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. +- `Starting capital`: Start capital - as given by dry-run-wallet (config or command line). +- `End capital`: Final capital - starting capital + absolute profit. +- `Absolute profit`: Profit made in stake currency. +- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. +- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6338b1d71..d6adfdf50 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -56,12 +56,13 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: 'Wins', 'Draws', 'Losses'] -def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict: +def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. """ profit_sum = result['profit_ratio'].sum() - profit_total = profit_sum / max_open_trades + # (end-capital - starting capital) / starting capital + profit_total = result['profit_abs'].sum() / starting_balance return { 'key': first_column, @@ -88,13 +89,13 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: } -def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, +def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_balance: int, results: DataFrame, skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list for the given backtest data and the results dataframe :param data: Dict of containing data that was used during backtesting. :param stake_currency: stake-currency - used to correctly name headers - :param max_open_trades: Maximum allowed open trades + :param starting_balance: Starting balance :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades :return: List of Dicts containing the metrics per pair @@ -107,10 +108,10 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t if skip_nan and result['profit_abs'].isnull().all(): continue - tabular_data.append(_generate_result_line(result, max_open_trades, pair)) + tabular_data.append(_generate_result_line(result, starting_balance, pair)) # Append Total - tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) + tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data @@ -159,7 +160,7 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]: tabular_data = [] for strategy, results in all_results.items(): tabular_data.append(_generate_result_line( - results['results'], results['config']['max_open_trades'], strategy) + results['results'], results['config']['dry_run_wallet'], strategy) ) return tabular_data @@ -246,15 +247,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], continue config = content['config'] max_open_trades = min(config['max_open_trades'], len(btdata.keys())) + starting_balance = config['dry_run_wallet'] stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - max_open_trades=max_open_trades, + starting_balance=starting_balance, results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, - max_open_trades=max_open_trades, + starting_balance=starting_balance, results=results.loc[results['is_open']], skip_nan=True) daily_stats = generate_daily_stats(results) @@ -276,7 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, - 'profit_total': results['profit_ratio'].sum() / max_open_trades, + 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.int_timestamp * 1000, @@ -292,6 +294,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], + 'starting_balance': starting_balance, + 'dry_run_wallet': starting_balance, + 'final_balance': content['final_balance'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -431,6 +436,13 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), + ('Starting capital', round_coin_value(strat_results['starting_balance'], + strat_results['stake_currency'])), + ('End capital', round_coin_value(strat_results['final_balance'], + strat_results['stake_currency'])), + ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], + strat_results['stake_currency'])), + ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), ('', ''), # Empty line to improve readability diff --git a/tests/conftest.py b/tests/conftest.py index 946ae1fb5..6e70603b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -261,6 +261,7 @@ def get_default_conf(testdatadir): "20": 0.02, "0": 0.04 }, + "dry_run_wallet": 1000, "stoploss": -0.10, "unfilledtimeout": { "buy": 10, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8b64c2764..405cc599b 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -48,7 +48,7 @@ def test_text_table_bt_results(): ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, results=results) + starting_balance=4, results=results) assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str @@ -78,6 +78,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): }), 'config': default_conf, 'locks': [], + 'final_balance': 1000.02, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -189,7 +190,7 @@ def test_generate_pair_metrics(): ) pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', - max_open_trades=2, results=results) + starting_balance=2, results=results) assert isinstance(pair_results, list) assert len(pair_results) == 2 assert pair_results[-1]['key'] == 'TOTAL' @@ -291,6 +292,7 @@ def test_generate_sell_reason_stats(): def test_text_table_strategy(default_conf): default_conf['max_open_trades'] = 2 + default_conf['dry_run_wallet'] = 3 results = {} results['TestStrategy1'] = {'results': pd.DataFrame( { @@ -323,9 +325,9 @@ def test_text_table_strategy(default_conf): '|---------------+--------+----------------+----------------+------------------+' '----------------+----------------+--------+---------+----------|\n' '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' - ' 30.00 | 0:17:00 | 3 | 0 | 0 |\n' + ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' - ' 45.00 | 0:20:00 | 3 | 0 | 0 |' + ' 43.33 | 0:20:00 | 3 | 0 | 0 |' ) strategy_results = generate_strategy_metrics(all_results=results) From 72f21fc5ec90ae15f622c362d2a24bd31f068613 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 13:08:49 +0100 Subject: [PATCH 015/348] Add trade-volume metric --- docs/backtesting.md | 3 +++ freqtrade/optimize/optimize_reports.py | 5 ++++- tests/optimize/test_backtesting.py | 2 ++ tests/optimize/test_optimize_reports.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index eab64a7a9..ada788da9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -257,6 +257,7 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total Profit % | 76.2% | | Trades per day | 3.575 | +| Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | | Worst Pair | ZEC/BTC -10.18% | @@ -337,6 +338,7 @@ It contains some useful key metrics about performance of your strategy on backte | Absolute profit | 0.00762792 BTC | | Total Profit % | 76.2% | | Trades per day | 3.575 | +| Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | | Worst Pair | ZEC/BTC -10.18% | @@ -364,6 +366,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Absolute profit`: Profit made in stake currency. - `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d6adfdf50..dde0f8dd2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,6 +277,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), + 'total_volume': results['stake_amount'].sum(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), @@ -442,9 +443,11 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('Total trade volume', round_coin_value(strat_results['total_volume'], + strat_results['stake_currency'])), + ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 061bcbaa0..8fba8724b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -817,6 +817,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 05:35:00', ], utc=True), 'trade_duration': [235, 40], 'is_open': [False, False], + 'stake_amount': [0.01, 0.01], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], 'sell_reason': [SellType.ROI, SellType.ROI] @@ -833,6 +834,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '2018-01-30 08:30:00'], utc=True), 'trade_duration': [47, 40, 20], 'is_open': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], 'open_rate': [0.104445, 0.10302485, 0.122541], 'close_rate': [0.104969, 0.103541, 0.123541], 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 405cc599b..ca6a4ab01 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -73,6 +73,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], "sell_reason": [SellType.ROI, SellType.STOP_LOSS, SellType.ROI, SellType.FORCE_SELL] }), From 74fc4bdab5e108c72015f80112df8ff7aa12bdcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 07:56:35 +0100 Subject: [PATCH 016/348] Shorten debug log --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 29559126b..c60cfa9b7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -382,7 +382,7 @@ class Backtesting: # Prevents buying if the trade-slot was freed in this candle open_trade_count_start += 1 open_trade_count += 1 - # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) Trade.trades.append(trade) From 0d2f877e77b17dc87c4efede8097dc90fd6202a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Feb 2021 19:30:17 +0100 Subject: [PATCH 017/348] Use absolute drawdown calc --- freqtrade/data/btanalysis.py | 10 ++++++--- freqtrade/optimize/optimize_reports.py | 17 ++++++++++++++- freqtrade/plot/plotting.py | 2 +- .../protections/max_drawdown_protection.py | 2 +- tests/data/test_btanalysis.py | 21 ++++++++++++------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8e851a8e8..117278585 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -360,13 +360,14 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', value_col: str = 'profit_ratio' - ) -> Tuple[float, pd.Timestamp, pd.Timestamp]: + ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]: """ Calculate max drawdown and the corresponding close dates :param trades: DataFrame containing trades (requires columns close_date and profit_ratio) :param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio') - :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time + :return: Tuple (float, highdate, lowdate, highvalue, lowvalue) with absolute max drawdown, + high and low time and high and low value. :raise: ValueError if trade-dataframe was found empty. """ if len(trades) == 0: @@ -382,7 +383,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] low_date = profit_results.loc[idxmin, date_col] - return abs(min(max_drawdown_df['drawdown'])), high_date, low_date + high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin] + ['high_value'].idxmax(), 'cumulative'] + low_val = max_drawdown_df.loc[idxmin, 'cumulative'] + return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index dde0f8dd2..5b3f813f2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -322,14 +322,20 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], result['strategy'][strategy] = strat_stats try: - max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( + max_drawdown, _, _, _, _ = calculate_max_drawdown( results, value_col='profit_ratio') + drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown( + results, value_col='profit_abs') strat_stats.update({ 'max_drawdown': max_drawdown, + 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start, 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, + + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results) @@ -341,6 +347,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], except ValueError: strat_stats.update({ 'max_drawdown': 0.0, + 'max_drawdown_abs': 0.0, + 'max_drawdown_low': 0.0, + 'max_drawdown_high': 0.0, 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start_ts': 0, 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), @@ -471,6 +480,12 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'], + strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 4325e537e..682c2b018 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, Add scatter points indicating max drawdown """ try: - max_drawdown, highdate, lowdate = calculate_max_drawdown(trades) + max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades) drawdown = go.Scatter( x=[highdate, lowdate], diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d54e6699b..d1c6b192d 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -55,7 +55,7 @@ class MaxDrawdown(IProtection): # Drawdown is always positive try: - drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') except ValueError: return False, None, None diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 3c4687745..555808679 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir): def test_calculate_max_drawdown(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) - drawdown, h, low = calculate_max_drawdown(bt_data) + drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data) assert isinstance(drawdown, float) assert pytest.approx(drawdown) == 0.21142322 - assert isinstance(h, Timestamp) - assert isinstance(low, Timestamp) - assert h == Timestamp('2018-01-24 14:25:00', tz='UTC') - assert low == Timestamp('2018-01-30 04:45:00', tz='UTC') + assert isinstance(hdate, Timestamp) + assert isinstance(lowdate, Timestamp) + assert isinstance(hval, float) + assert isinstance(lval, float) + assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC') + assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC') with pytest.raises(ValueError, match='Trade dataframe empty.'): - drawdown, h, low = calculate_max_drawdown(DataFrame()) + drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame()) def test_calculate_csum(testdatadir): @@ -310,13 +312,16 @@ def test_calculate_max_drawdown2(): # sort by profit and reset index df = df.sort_values('profit').reset_index(drop=True) df1 = df.copy() - drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit') + drawdown, hdate, ldate, hval, lval = calculate_max_drawdown( + df, date_col='open_date', value_col='profit') # Ensure df has not been altered. assert df.equals(df1) assert isinstance(drawdown, float) # High must be before low - assert h < low + assert hdate < ldate + # High value must be higher than low value + assert hval > lval assert drawdown == 0.091755 df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) From aed23d55c280806af19fe9c7927fe1088ff1e0b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 20:12:59 +0100 Subject: [PATCH 018/348] Add starting balance to profit cumsum calculation --- freqtrade/data/btanalysis.py | 7 ++++--- tests/data/test_btanalysis.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 117278585..3adee8775 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -389,10 +389,11 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val -def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: +def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]: """ Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :param starting_balance: Add starting balance to results, to show the wallets high / low points :return: Tuple (float, float) with cumsum of profit_abs :raise: ValueError if trade-dataframe was found empty. """ @@ -401,7 +402,7 @@ def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: csum_df = pd.DataFrame() csum_df['sum'] = trades['profit_abs'].cumsum() - csum_min = csum_df['sum'].min() - csum_max = csum_df['sum'].max() + csum_min = csum_df['sum'].min() + starting_balance + csum_max = csum_df['sum'].max() + starting_balance return csum_min, csum_max diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 555808679..538c89a90 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -296,6 +296,11 @@ def test_calculate_csum(testdatadir): assert isinstance(csum_max, float) assert csum_min < 0.01 assert csum_max > 0.02 + csum_min1, csum_max1 = calculate_csum(bt_data, 5) + + assert csum_min1 == csum_min + 5 + assert csum_max1 == csum_max + 5 + with pytest.raises(ValueError, match='Trade dataframe empty.'): csum_min, csum_max = calculate_csum(DataFrame()) From f367375e5b6240625ddd0ba51120ad48faa41409 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Feb 2021 20:39:50 +0100 Subject: [PATCH 019/348] ABS drawdown should show wallet high and low values --- freqtrade/optimize/optimize_reports.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5b3f813f2..1ac0ae1d6 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -334,11 +334,11 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - 'max_drawdown_low': low_val, - 'max_drawdown_high': high_val, + 'max_drawdown_low': low_val + starting_balance, + 'max_drawdown_high': high_val + starting_balance, }) - csum_min, csum_max = calculate_csum(results) + csum_min, csum_max = calculate_csum(results, starting_balance) strat_stats.update({ 'csum_min': csum_min, 'csum_max': csum_max @@ -493,7 +493,15 @@ def text_table_add_metrics(strat_results: Dict) -> str: return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") else: - return '' + start_balance = round_coin_value(strat_results['starting_balance'], + strat_results['stake_currency']) + stake_amount = round_coin_value(strat_results['stake_amount'], + strat_results['stake_currency']) + message = ("No trades made. " + f"Your starting balance was {start_balance}, " + f"and your stake was {stake_amount}." + ) + return message def show_backtest_results(config: Dict, backtest_stats: Dict): From 37d7d2afd5c953c413024964d0078172ba1e3e1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 19:50:10 +0100 Subject: [PATCH 020/348] Wallets should not recalculate close_profit for closed trades --- freqtrade/wallets.py | 2 +- tests/conftest_trades.py | 2 ++ tests/test_freqtradebot.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f5ce4c102..c2085641e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -69,7 +69,7 @@ class Wallets: _wallets = {} closed_trades = Trade.get_trades_proxy(is_open=False) open_trades = Trade.get_trades_proxy(is_open=True) - tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 6a42d04e3..025aac1b6 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -82,6 +82,7 @@ def mock_trade_2(fee): open_rate=0.123, close_rate=0.128, close_profit=0.005, + close_profit_abs=0.000584127, exchange='bittrex', is_open=False, open_order_id='dry_run_sell_12345', @@ -141,6 +142,7 @@ def mock_trade_3(fee): open_rate=0.05, close_rate=0.06, close_profit=0.01, + close_profit_abs=0.000155, exchange='bittrex', is_open=False, strategy='DefaultStrategy', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3bd2f5607..d7d2e19f6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2243,6 +2243,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade.close_profit_abs = 0.001 open_trade.is_open = False Trade.session.add(open_trade) @@ -2290,6 +2291,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade.close_profit_abs = 0.001 open_trade.is_open = False Trade.session.add(open_trade) From 7913166453518733fcd793879d0150d1339796d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:07:27 +0100 Subject: [PATCH 021/348] Improve performance by updating wallets only when necessary --- freqtrade/optimize/backtesting.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c60cfa9b7..f921f64c3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -180,10 +180,6 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() - def update_wallets(self): - if self.wallets: - self.wallets.update() - def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -272,7 +268,6 @@ class Backtesting: def _enter_trade(self, pair: str, row, max_open_trades: int, open_trade_count: int) -> Optional[Trade]: - self.update_wallets() try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) @@ -391,7 +386,6 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: - self.update_wallets() # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) @@ -404,7 +398,7 @@ class Backtesting: tmp += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) - self.update_wallets() + self.wallets.update() return trade_list_to_dataframe(trades) From f04f07299c7689841eaf7eab15c574c09c5774b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Feb 2021 20:19:03 +0100 Subject: [PATCH 022/348] Improve backtesting metrics --- docs/backtesting.md | 39 ++++++++++++++++++-------- freqtrade/optimize/optimize_reports.py | 36 +++++++++++++----------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ada788da9..bac12dae0 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,11 +252,12 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Starting capital | 0.01000000 BTC | -| End capital | 0.01762792 BTC | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | -| Total Profit % | 76.2% | +| Total profit % | 76.2% | | Trades per day | 3.575 | +| Avg. stake amount | 0.001 | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -269,7 +270,12 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | -| Max Drawdown | 50.63% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Drawdown | 50.63% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | | Drawdown Start | 2019-02-15 14:10:00 | | Drawdown End | 2019-04-11 18:15:00 | | Market change | -5.88% | @@ -333,11 +339,12 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Starting capital | 0.01000000 BTC | -| End capital | 0.01762792 BTC | +| Starting balance | 0.01000000 BTC | +| Final balance | 0.01762792 BTC | | Absolute profit | 0.00762792 BTC | -| Total Profit % | 76.2% | +| Total profit % | 76.2% | | Trades per day | 3.575 | +| Avg. stake amount | 0.001 | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -350,7 +357,12 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | | | -| Max Drawdown | 50.63% | +| Min balance | 0.00945123 BTC | +| Max balance | 0.01846651 BTC | +| Drawdown | 50.63% | +| Drawdown | 0.0015 BTC | +| Drawdown high | 0.0013 BTC | +| Drawdown low | -0.0002 BTC | | Drawdown Start | 2019-02-15 14:10:00 | | Drawdown End | 2019-04-11 18:15:00 | | Market change | -5.88% | @@ -361,18 +373,21 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. -- `Starting capital`: Start capital - as given by dry-run-wallet (config or command line). -- `End capital`: Final capital - starting capital + absolute profit. +- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). +- `End balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. -- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. +- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. -- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. +- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1ac0ae1d6..cee0bb1ce 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -278,6 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': results['stake_amount'].sum(), + 'avg_stake_amount': results['stake_amount'].mean(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), @@ -295,6 +296,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], @@ -334,8 +336,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'drawdown_end': drawdown_end, 'drawdown_end_ts': drawdown_end.timestamp() * 1000, - 'max_drawdown_low': low_val + starting_balance, - 'max_drawdown_high': high_val + starting_balance, + 'max_drawdown_low': low_val, + 'max_drawdown_high': high_val, }) csum_min, csum_max = calculate_csum(results, starting_balance) @@ -446,14 +448,16 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), - ('Starting capital', round_coin_value(strat_results['starting_balance'], + ('Starting balance', round_coin_value(strat_results['starting_balance'], strat_results['stake_currency'])), - ('End capital', round_coin_value(strat_results['final_balance'], - strat_results['stake_currency'])), + ('Final balance', round_coin_value(strat_results['final_balance'], + strat_results['stake_currency'])), ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'], strat_results['stake_currency'])), - ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), + ('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'], + strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), @@ -474,18 +478,18 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('', ''), # Empty line to improve readability - ('Abs Profit Min', round_coin_value(strat_results['csum_min'], - strat_results['stake_currency'])), - ('Abs Profit Max', round_coin_value(strat_results['csum_max'], - strat_results['stake_currency'])), + ('Min balance', round_coin_value(strat_results['csum_min'], + strat_results['stake_currency'])), + ('Max balance', round_coin_value(strat_results['csum_max'], + strat_results['stake_currency'])), - ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), - ('Max Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + strat_results['stake_currency'])), + ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], + strat_results['stake_currency'])), + ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'], strat_results['stake_currency'])), - ('Max Drawdown high', round_coin_value(strat_results['max_drawdown_high'], - strat_results['stake_currency'])), - ('Max Drawdown low', round_coin_value(strat_results['max_drawdown_low'], - strat_results['stake_currency'])), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), From 52acacbed5b43f5cec2f07af74336998c3e51523 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 07:20:51 +0100 Subject: [PATCH 023/348] Check min-trade-stake in backtesting --- freqtrade/optimize/backtesting.py | 4 +++- tests/optimize/test_backtest_detail.py | 3 ++- tests/optimize/test_backtesting.py | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f921f64c3..bd185234f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,7 +273,9 @@ class Backtesting: pair, max_open_trades - open_trade_count, None) except DependencyException: stake_amount = 0 - if stake_amount: + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) + if stake_amount and stake_amount > min_stake_amount: + # print(f"{pair}, {stake_amount}") # Enter trade trade = Trade( pair=pair, diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 4d6605b9f..a56e024f7 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -489,7 +489,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal} - mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) + mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8fba8724b..eda8aac9d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -450,6 +450,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' @@ -510,6 +511,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -555,6 +557,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad default_conf['enable_protections'] = True mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) tests = [ ['sine', 9], ['raise', 10], @@ -586,6 +589,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, default_conf['protections'] = protections default_conf['enable_protections'] = True + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results @@ -623,6 +627,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) @@ -655,6 +660,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) return dataframe + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) From 394a6bbf2a86c8c4990c1f38e566ebaaa2ea2561 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:21:30 +0100 Subject: [PATCH 024/348] Fix some type errors --- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/optimize_reports.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bd185234f..7028a38cd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,7 +274,7 @@ class Backtesting: except DependencyException: stake_amount = 0 min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) - if stake_amount and stake_amount > min_stake_amount: + if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") # Enter trade trade = Trade( @@ -341,7 +341,7 @@ class Backtesting: indexes: Dict = {} tmp = start_date + timedelta(minutes=self.timeframe_min) - open_trades: Dict[str, List] = defaultdict(list) + open_trades: Dict[str, List[Trade]] = defaultdict(list) open_trade_count = 0 # Loop timerange and get candle for each pair at that point in time diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index cee0bb1ce..e7111f20c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -479,9 +479,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Max balance', round_coin_value(strat_results['csum_max'], - strat_results['stake_currency'])), + strat_results['stake_currency'])), ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], From 03eb23a4ce7cdf54ffbc3595814ca2029f979262 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 19:29:04 +0100 Subject: [PATCH 025/348] 2 levels of Trade models, one with and one without sqlalchemy Fixes a performance issue when backtesting with sqlalchemy, as that uses descriptors for all properties. --- freqtrade/optimize/backtesting.py | 11 +- freqtrade/persistence/__init__.py | 3 +- freqtrade/persistence/models.py | 244 ++++++++++++++++++++---------- 3 files changed, 170 insertions(+), 88 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7028a38cd..322a3f00b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -23,6 +23,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import LocalTrade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -267,7 +268,7 @@ class Backtesting: return None def _enter_trade(self, pair: str, row, max_open_trades: int, - open_trade_count: int) -> Optional[Trade]: + open_trade_count: int) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) @@ -277,7 +278,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") # Enter trade - trade = Trade( + trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], open_date=row[DATE_IDX], @@ -291,8 +292,8 @@ class Backtesting: return trade return None - def handle_left_open(self, open_trades: Dict[str, List[Trade]], - data: Dict[str, List[Tuple]]) -> List[Trade]: + def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]], + data: Dict[str, List[Tuple]]) -> List[LocalTrade]: """ Handling of left open trades at the end of backtesting """ @@ -381,7 +382,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) - Trade.trades.append(trade) + LocalTrade.trades.append(trade) for trade in open_trades[pair]: # also check the buying candle for sell conditions. diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 35f2bc406..d1fcac0ba 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.models import (LocalTrade, Order, Trade, clean_dry_run_db, cleanup_db, + init_db) from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f72705c34..48ae8bb40 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -199,67 +199,67 @@ class Order(_DECL_BASE): return Order.query.filter(Order.ft_is_open.is_(True)).all() -class Trade(_DECL_BASE): +class LocalTrade(): """ Trade database model. - Also handles updating and querying trades + Used in backtesting - must be aligned to Trade model! + """ - __tablename__ = 'trades' - - use_db: bool = True + use_db: bool = False # Trades container for backtesting - trades: List['Trade'] = [] + trades: List['LocalTrade'] = [] - id = Column(Integer, primary_key=True) + id: int = 0 - orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") + orders: List[Order] = [] - exchange = Column(String, nullable=False) - pair = Column(String, nullable=False, index=True) - is_open = Column(Boolean, nullable=False, default=True, index=True) - fee_open = Column(Float, nullable=False, default=0.0) - fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String, nullable=True) - fee_close = Column(Float, nullable=False, default=0.0) - fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String, nullable=True) - open_rate = Column(Float) - open_rate_requested = Column(Float) + exchange: str = '' + pair: str = '' + is_open: bool = True + fee_open: float = 0.0 + fee_open_cost: Optional[float] = None + fee_open_currency: str = '' + fee_close: float = 0.0 + fee_close_cost: Optional[float] = None + fee_close_currency: str = '' + open_rate: float + open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value = Column(Float) - close_rate = Column(Float) - close_rate_requested = Column(Float) - close_profit = Column(Float) - close_profit_abs = Column(Float) - stake_amount = Column(Float, nullable=False) - amount = Column(Float) - amount_requested = Column(Float) - open_date = Column(DateTime, nullable=False, default=datetime.utcnow) - close_date = Column(DateTime) - open_order_id = Column(String) + open_trade_value: float + close_rate: Optional[float] = None + close_rate_requested: Optional[float] = None + close_profit: Optional[float] = None + close_profit_abs: Optional[float] = None + stake_amount: float + amount: float + amount_requested: Optional[float] = None + open_date: datetime + close_date: Optional[datetime] = None + open_order_id: Optional[str] = None # absolute value of the stop loss - stop_loss = Column(Float, nullable=True, default=0.0) + stop_loss: float = 0.0 # percentage value of the stop loss - stop_loss_pct = Column(Float, nullable=True) + stop_loss_pct: float = 0.0 # absolute value of the initial stop loss - initial_stop_loss = Column(Float, nullable=True, default=0.0) + initial_stop_loss: float = 0.0 # percentage value of the initial stop loss - initial_stop_loss_pct = Column(Float, nullable=True) + initial_stop_loss_pct: float = 0.0 # stoploss order id which is on exchange - stoploss_order_id = Column(String, nullable=True, index=True) + stoploss_order_id: Optional[str] = None # last update time of the stoploss order on exchange - stoploss_last_update = Column(DateTime, nullable=True) + stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price - max_rate = Column(Float, nullable=True, default=0.0) + max_rate: float = 0.0 # Lowest price reached - min_rate = Column(Float, nullable=True) - sell_reason = Column(String, nullable=True) - sell_order_status = Column(String, nullable=True) - strategy = Column(String, nullable=True) - timeframe = Column(Integer, nullable=True) + min_rate: float = 0.0 + sell_reason: str = '' + sell_order_status: str = '' + strategy: str = '' + timeframe: Optional[int] = None def __init__(self, **kwargs): - super().__init__(**kwargs) + for key in kwargs: + setattr(self, key, kwargs[key]) self.recalc_open_trade_value() def __repr__(self): @@ -349,8 +349,7 @@ class Trade(_DECL_BASE): """ Resets all trades. Only active for backtesting mode. """ - if not Trade.use_db: - Trade.trades = [] + LocalTrade.trades = [] def adjust_min_max_rates(self, current_price: float) -> None: """ @@ -418,8 +417,8 @@ class Trade(_DECL_BASE): if order_type in ('market', 'limit') and order['side'] == 'buy': # Update open rate and actual amount - self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) - self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) + self.open_rate = float(safe_value_fallback(order, 'average', 'price')) + self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_value() if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') @@ -443,7 +442,7 @@ class Trade(_DECL_BASE): Sets close_rate to the given rate, calculates total profit and marks trade as closed """ - self.close_rate = Decimal(rate) + self.close_rate = rate self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() self.close_date = self.close_date or datetime.utcnow() @@ -488,14 +487,6 @@ class Trade(_DECL_BASE): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) - def delete(self) -> None: - - for order in self.orders: - Order.session.delete(order) - - Trade.session.delete(self) - Trade.session.flush() - def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. @@ -525,7 +516,7 @@ class Trade(_DECL_BASE): if rate is None and not self.close_rate: return 0.0 - sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) + sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore fees = sell_trade * Decimal(fee or self.fee_close) return float(sell_trade - fees) @@ -597,7 +588,7 @@ class Trade(_DECL_BASE): @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, - ) -> List['Trade']: + ) -> List['LocalTrade']: """ Helper function to query Trades. Returns a List of trades, filtered on the parameters given. @@ -606,30 +597,19 @@ class Trade(_DECL_BASE): :return: unsorted List[Trade] """ - if Trade.use_db: - trade_filter = [] - if pair: - trade_filter.append(Trade.pair == pair) - if open_date: - trade_filter.append(Trade.open_date > open_date) - if close_date: - trade_filter.append(Trade.close_date > close_date) - if is_open is not None: - trade_filter.append(Trade.is_open.is_(is_open)) - return Trade.get_trades(trade_filter).all() - else: - # Offline mode - without database - sel_trades = [trade for trade in Trade.trades] - if pair: - sel_trades = [trade for trade in sel_trades if trade.pair == pair] - if open_date: - sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] - if close_date: - sel_trades = [trade for trade in sel_trades if trade.close_date - and trade.close_date > close_date] - if is_open is not None: - sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] - return sel_trades + + # Offline mode - without database + sel_trades = [trade for trade in LocalTrade.trades] + if pair: + sel_trades = [trade for trade in sel_trades if trade.pair == pair] + if open_date: + sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] + if close_date: + sel_trades = [trade for trade in sel_trades if trade.close_date + and trade.close_date > close_date] + if is_open is not None: + sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades @staticmethod def get_open_trades() -> List[Any]: @@ -735,6 +715,106 @@ class Trade(_DECL_BASE): logger.info(f"New stoploss: {trade.stop_loss}.") +class Trade(_DECL_BASE, LocalTrade): + """ + Trade database model. + Also handles updating and querying trades + """ + __tablename__ = 'trades' + + use_db: bool = True + + id = Column(Integer, primary_key=True) + + orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") + + exchange = Column(String, nullable=False) + pair = Column(String, nullable=False, index=True) + is_open = Column(Boolean, nullable=False, default=True, index=True) + fee_open = Column(Float, nullable=False, default=0.0) + fee_open_cost = Column(Float, nullable=True) + fee_open_currency = Column(String, nullable=True) + fee_close = Column(Float, nullable=False, default=0.0) + fee_close_cost = Column(Float, nullable=True) + fee_close_currency = Column(String, nullable=True) + open_rate = Column(Float) + open_rate_requested = Column(Float) + # open_trade_value - calculated via _calc_open_trade_value + open_trade_value = Column(Float) + close_rate = Column(Float) + close_rate_requested = Column(Float) + close_profit = Column(Float) + close_profit_abs = Column(Float) + stake_amount = Column(Float, nullable=False) + amount = Column(Float) + amount_requested = Column(Float) + open_date = Column(DateTime, nullable=False, default=datetime.utcnow) + close_date = Column(DateTime) + open_order_id = Column(String) + # absolute value of the stop loss + stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the stop loss + stop_loss_pct = Column(Float, nullable=True) + # absolute value of the initial stop loss + initial_stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the initial stop loss + initial_stop_loss_pct = Column(Float, nullable=True) + # stoploss order id which is on exchange + stoploss_order_id = Column(String, nullable=True, index=True) + # last update time of the stoploss order on exchange + stoploss_last_update = Column(DateTime, nullable=True) + # absolute value of the highest reached price + max_rate = Column(Float, nullable=True, default=0.0) + # Lowest price reached + min_rate = Column(Float, nullable=True) + sell_reason = Column(String, nullable=True) + sell_order_status = Column(String, nullable=True) + strategy = Column(String, nullable=True) + timeframe = Column(Integer, nullable=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.recalc_open_trade_value() + + def delete(self) -> None: + + for order in self.orders: + Order.session.delete(order) + + Trade.session.delete(self) + Trade.session.flush() + + @staticmethod + def get_trades_proxy(*, pair: str = None, is_open: bool = None, + open_date: datetime = None, close_date: datetime = None, + ) -> List['LocalTrade']: + """ + Helper function to query Trades. + Returns a List of trades, filtered on the parameters given. + In live mode, converts the filter to a database query and returns all rows + In Backtest mode, uses filters on Trade.trades to get the result. + + :return: unsorted List[Trade] + """ + if Trade.use_db: + trade_filter = [] + if pair: + trade_filter.append(Trade.pair == pair) + if open_date: + trade_filter.append(Trade.open_date > open_date) + if close_date: + trade_filter.append(Trade.close_date > close_date) + if is_open is not None: + trade_filter.append(Trade.is_open.is_(is_open)) + return Trade.get_trades(trade_filter).all() + else: + return LocalTrade.get_trades_proxy( + pair=pair, is_open=is_open, + open_date=open_date, + close_date=close_date + ) + + class PairLock(_DECL_BASE): """ Pair Locks database model. From 53a57f2c81f05c6bf7f2ce3cf5bd5cc95d591464 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:22:00 +0100 Subject: [PATCH 026/348] Change some types Fix types of new model object --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/plugins/protections/cooldown_period.py | 3 ++- freqtrade/plugins/protections/iprotection.py | 6 +++--- freqtrade/plugins/protections/low_profit_pairs.py | 2 +- freqtrade/plugins/protections/stoploss_guard.py | 2 +- tests/conftest.py | 4 ++-- tests/optimize/test_backtest_detail.py | 1 - 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3adee8775..c98477f4e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -10,7 +10,7 @@ import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load -from freqtrade.persistence import Trade, init_db +from freqtrade.persistence import LocalTrade, Trade, init_db logger = logging.getLogger(__name__) @@ -224,7 +224,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] -def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame: +def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: """ Convert list of Trade objects to pandas Dataframe :param trades: List of trade objects diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 322a3f00b..aeafaffd3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -211,7 +211,7 @@ class Backtesting: data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] return data - def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple, + def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, trade_dur: int) -> float: """ Get close rate for backtesting result @@ -251,10 +251,10 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]: + def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX], - sell_row[BUY_IDX], sell_row[SELL_IDX], + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore + sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: @@ -331,7 +331,7 @@ class Backtesting: :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ - trades: List[Trade] = [] + trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) # Use dict of lists with data for performance @@ -342,7 +342,7 @@ class Backtesting: indexes: Dict = {} tmp = start_date + timedelta(minutes=self.timeframe_min) - open_trades: Dict[str, List[Trade]] = defaultdict(list) + open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 # Loop timerange and get candle for each pair at that point in time diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 2d7d7b4c7..f74f83885 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -44,7 +44,8 @@ class CooldownPeriod(IProtection): trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) if trades: # Get latest trade - trade = sorted(trades, key=lambda t: t.close_date)[-1] + # Ignore type error as we know we only get closed trades. + trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 684bf6cd3..d034beefc 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import plural from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Trade +from freqtrade.persistence import LocalTrade logger = logging.getLogger(__name__) @@ -93,11 +93,11 @@ class IProtection(LoggingMixin, ABC): """ @staticmethod - def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime: + def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime: """ Get lock end time """ - max_date: datetime = max([trade.close_date for trade in trades]) + max_date: datetime = max([trade.close_date for trade in trades if trade.close_date]) # comming from Database, tzinfo is not set. if max_date.tzinfo is None: max_date = max_date.replace(tzinfo=timezone.utc) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 9d5ed35b4..7822ce73c 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -53,7 +53,7 @@ class LowProfitPairs(IProtection): # Not enough trades in the relevant period return False, None, None - profit = sum(trade.close_profit for trade in trades) + profit = sum(trade.close_profit for trade in trades if trade.close_profit) if profit < self._required_profit: self.log_once( f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 5a9b9ddd0..635c0be04 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -56,7 +56,7 @@ class StoplossGuard(IProtection): trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.STOPLOSS_ON_EXCHANGE.value) - and trade.close_profit < 0)] + and trade.close_profit and trade.close_profit < 0)] if len(trades) < self._trade_limit: return False, None, None diff --git a/tests/conftest.py b/tests/conftest.py index 6e70603b1..793ba83b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade, init_db +from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, @@ -191,7 +191,7 @@ def create_mock_trades(fee, use_db: bool = True): if use_db: Trade.session.add(trade) else: - Trade.trades.append(trade) + LocalTrade.trades.append(trade) # Simulate dry_run entries trade = mock_trade_1(fee) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a56e024f7..0ba6f4a7f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument import logging -from unittest.mock import MagicMock import pytest From 60db6ccf454715aa9d8b2ba56e4676006e8fb1fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Feb 2021 20:07:00 +0100 Subject: [PATCH 027/348] Add test for subclassing --- freqtrade/persistence/models.py | 8 ++++---- tests/test_persistence.py | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 48ae8bb40..51a48c246 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -222,16 +222,16 @@ class LocalTrade(): fee_close: float = 0.0 fee_close_cost: Optional[float] = None fee_close_currency: str = '' - open_rate: float + open_rate: float = 0.0 open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value: float + open_trade_value: float = 0.0 close_rate: Optional[float] = None close_rate_requested: Optional[float] = None close_profit: Optional[float] = None close_profit_abs: Optional[float] = None - stake_amount: float - amount: float + stake_amount: float = 0.0 + amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime close_date: Optional[datetime] = None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1fced3e16..18a377ca3 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,14 +1,16 @@ # pragma pylint: disable=missing-docstring, C0103 +from types import FunctionType import logging from unittest.mock import MagicMock import arrow import pytest from sqlalchemy import create_engine +from sqlalchemy.sql.schema import Column from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1176,3 +1178,25 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None + + +def test_Trade_object_idem(): + + assert issubclass(Trade, LocalTrade) + + trade = vars(Trade) + localtrade = vars(LocalTrade) + + # Parent (LocalTrade) should have the same attributes + for item in trade: + # Exclude private attributes and open_date (as it's not assigned a default) + if (not item.startswith('_') + and item not in ('delete', 'session', 'query', 'open_date')): + assert item in localtrade + + # Fails if only a column is added without corresponding parent field + for item in localtrade: + if (not item.startswith('__') + and item not in ('trades', ) + and type(getattr(LocalTrade, item)) not in (property, FunctionType)): + assert item in trade From fc256749af4a29ee30353a2ae4edc17f9b3a4021 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Feb 2021 06:54:33 +0100 Subject: [PATCH 028/348] Add test for backtesting _enter_trade --- freqtrade/optimize/backtesting.py | 7 +++-- tests/data/test_btanalysis.py | 1 - tests/optimize/test_backtesting.py | 41 +++++++++++++++++++++++++++++- tests/test_persistence.py | 3 +-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aeafaffd3..9a4a3787a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -22,8 +22,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.persistence import PairLocks, Trade -from freqtrade.persistence.models import LocalTrade +from freqtrade.persistence import LocalTrade, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -267,13 +266,13 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row, max_open_trades: int, + def _enter_trade(self, pair: str, row: List, max_open_trades: int, open_trade_count: int) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount( pair, max_open_trades - open_trade_count, None) except DependencyException: - stake_amount = 0 + return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # print(f"{pair}, {stake_amount}") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 538c89a90..e42c13e18 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -301,7 +301,6 @@ def test_calculate_csum(testdatadir): assert csum_min1 == csum_min + 5 assert csum_max1 == csum_max + 5 - with pytest.raises(ValueError, match='Trade dataframe empty.'): csum_min, csum_max = calculate_csum(DataFrame()) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index eda8aac9d..354b3f6b0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -17,8 +17,9 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting +from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType @@ -447,6 +448,44 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) +def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: + default_conf['ask_strategy']['use_sell_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) + patch_exchange(mocker) + default_conf['stake_amount'] = 'unlimited' + backtesting = Backtesting(default_conf) + pair = 'UNITTEST/BTC' + row = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 1, # Sell + 0.001, # Open + 0.0011, # Close + 0, # Sell + 0.00099, # Low + 0.0012, # High + ] + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert isinstance(trade, LocalTrade) + assert trade.stake_amount == 495 + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + assert trade is None + + # Stake-amount too high! + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert trade is None + + # Stake-amount too high! + mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", + side_effect=DependencyException) + + trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) + assert trade is None + + def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 18a377ca3..1a8124b00 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,12 +1,11 @@ # pragma pylint: disable=missing-docstring, C0103 -from types import FunctionType import logging +from types import FunctionType from unittest.mock import MagicMock import arrow import pytest from sqlalchemy import create_engine -from sqlalchemy.sql.schema import Column from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException From d3fb473e578e0f1ea5b7275d7023e6f8088d2583 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Feb 2021 20:21:50 +0100 Subject: [PATCH 029/348] Improve backtesting documentation --- docs/backtesting.md | 86 ++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index bac12dae0..9fa9025d8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -95,8 +95,7 @@ Strategy arguments: ## Test your strategy with Backtesting Now you have good Buy and Sell strategies and some historic data, you want to test it against -real data. This is what we call -[backtesting](https://en.wikipedia.org/wiki/Backtesting). +real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/` by default. If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`. @@ -104,6 +103,8 @@ For details on downloading, please refer to the [Data Downloading](data-download The result of backtesting will confirm if your bot has better odds of making a profit than a loss. +All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation. + !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. @@ -111,38 +112,46 @@ The result of backtesting will confirm if your bot has better odds of making a p To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. -### Run a backtesting against the currencies listed in your config file +### Example backtesting commands -#### With 5 min candle (OHLCV) data (per default) +With 5 min candle (OHLCV) data (per default) ```bash -freqtrade backtesting +freqtrade backtesting --strategy AwesomeStrategy ``` -#### With 1 min candle (OHLCV) data +Where `--strategy AwesomeStrategy` / `-s AwesomeStrategy` refers to the class name of the strategy, which is within a python file in the `user_data/strategies` directory. + +--- + +With 1 min candle (OHLCV) data ```bash -freqtrade backtesting --timeframe 1m +freqtrade backtesting --strategy AwesomeStrategy --timeframe 1m ``` -#### Using a different on-disk historical candle (OHLCV) data source +--- + +Providing a custom starting balance of 1000 (in stake currency) + +```bash +freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000 +``` + +--- + +Using a different on-disk historical candle (OHLCV) data source Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. You can then use this data for backtesting as follows: ```bash -freqtrade --datadir user_data/data/bittrex-20180101 backtesting +freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101 ``` -#### With a (custom) strategy file +--- -```bash -freqtrade backtesting -s SampleStrategy -``` - -Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory. - -#### Comparing multiple Strategies +Comparing multiple Strategies ```bash freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m @@ -150,23 +159,29 @@ freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timefram Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies. -#### Exporting trades to file +--- + +Exporting trades to file ```bash -freqtrade backtesting --export trades --config config.json --strategy SampleStrategy +freqtrade backtesting --strategy backtesting --export trades --config config.json ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. -#### Exporting trades to file specifying a custom filename +--- + +Exporting trades to file specifying a custom filename ```bash -freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json +freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json ``` Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). -#### Supplying custom fee value +--- + +Supplying custom fee value Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. @@ -181,26 +196,26 @@ freqtrade backtesting --fee 0.001 !!! Note Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info. -#### Running backtest with smaller testset by using timerange +--- -Use the `--timerange` argument to change how much of the testset you want to use. +Running backtest with smaller test-set by using timerange +Use the `--timerange` argument to change how much of the test-set you want to use. -For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata. +For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your input data. ```bash freqtrade backtesting --timerange=20190501- ``` -You can also specify particular dates or a range span indexed by start and stop. +You can also specify particular date ranges. The full timerange specification: -- Use tickframes till 2018/01/31: `--timerange=-20180131` -- Use tickframes since 2018/01/31: `--timerange=20180131-` -- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` -- Use tickframes between POSIX timestamps 1527595200 1527618600: - `--timerange=1527595200-1527618600` +- Use data until 2018/01/31: `--timerange=-20180131` +- Use data since 2018/01/31: `--timerange=20180131-` +- Use data since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` +- Use data between POSIX / epoch timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` ## Understand the backtesting result @@ -296,9 +311,9 @@ here: The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. -The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses. -The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). -In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. +The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. +The column `Tot Profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). +In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `Tot Profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. @@ -452,6 +467,5 @@ Detailed output for all strategies one after the other will be available, so mak ## Next step -Great, your strategy is profitable. What if the bot can give your the -optimal parameters to use for your strategy? +Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy? Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md) From 86f9409fd293604e03408e89beb460078768d103 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Feb 2021 20:14:33 +0100 Subject: [PATCH 030/348] fix --stake-amount parameter --- freqtrade/commands/cli_options.py | 1 - freqtrade/configuration/configuration.py | 7 +++++++ tests/test_configuration.py | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 90ebb5e6a..3b27237da 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -133,7 +133,6 @@ AVAILABLE_CLI_OPTIONS = { "stake_amount": Arg( '--stake-amount', help='Override the value of the `stake_amount` configuration setting.', - type=float, ), # Backtesting "position_stacking": Arg( diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 6295d01d4..88447e490 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -229,6 +229,13 @@ class Configuration: elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + if self.args.get('stake_amount', None): + # Convert explicitly to float to support CLI argument for both unlimited and value + try: + self.args['stake_amount'] = float(self.args['stake_amount']) + except ValueError: + pass + self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 94c3e24f6..6b3df392b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -430,7 +430,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non '--enable-position-stacking', '--disable-max-market-positions', '--timerange', ':100', - '--export', '/bar/foo' + '--export', '/bar/foo', + '--stake-amount', 'unlimited' ] args = Arguments(arglist).get_parsed_arg() @@ -463,6 +464,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'export' in config assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) + assert 'stake_amount' in config + assert config['stake_amount'] == 'unlimited' def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: From 98f3142b30e2067b4ead4e3dec51848d56a9c0cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:48:06 +0100 Subject: [PATCH 031/348] Improve handling of backtesting params --- freqtrade/commands/cli_options.py | 2 +- freqtrade/commands/optimize_commands.py | 11 ++++++++--- freqtrade/configuration/configuration.py | 6 +++--- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 17 +++++++++++++---- tests/optimize/test_hyperopt.py | 17 +++++++++++++---- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3b27237da..15c13cec9 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -111,7 +111,7 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), "dry_run_wallet": Arg( - '--dry-run-wallet', + '--dry-run-wallet', '--starting-balance', help='Starting balance, used for backtesting / hyperopt and dry-runs.', type=float, ), diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index bf36972c4..130743f68 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,6 +4,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_coin_value from freqtrade.state import RunMode @@ -22,9 +23,13 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.BACKTEST: 'backtesting', RunMode.HYPEROPT: 'hyperoptimization', } - if (method in no_unlimited_runmodes.keys() and - config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT and - config['max_open_trades'] != float('inf')): + if method in no_unlimited_runmodes.keys(): + if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT + and config['stake_amount'] > config['dry_run_wallet']): + wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency']) + stake = round_coin_value(config['stake_amount'], config['stake_currency']) + raise OperationalException(f"Starting balance ({wallet}) " + f"is smaller than stake_amount {stake}.") pass # config['dry_run_wallet'] = config['stake_amount'] * \ # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 88447e490..a40a4fd83 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -214,9 +214,6 @@ class Configuration: self._args_to_config( config, argname='enable_protections', logstring='Parameter --enable-protections detected, enabling Protections. ...') - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: config.update({'use_max_market_positions': False}) @@ -228,6 +225,9 @@ class Configuration: 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) elif config['runmode'] in NON_UTIL_MODES: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + # Setting max_open_trades to infinite if -1 + if config.get('max_open_trades') == -1: + config['max_open_trades'] = float('inf') if self.args.get('stake_amount', None): # Convert explicitly to float to support CLI argument for both unlimited and value diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9a4a3787a..13ffc1d25 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -128,7 +128,7 @@ class Backtesting: PairLocks.use_db = True Trade.use_db = True - def _set_strategy(self, strategy): + def _set_strategy(self, strategy: IStrategy): """ Load strategy into backtesting """ diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 354b3f6b0..4bbfe8a78 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -9,7 +9,6 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.configuration import TimeRange from freqtrade.data import history @@ -232,8 +231,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog) -def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT +def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -241,12 +239,23 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con 'backtesting', '--config', 'config.json', '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '2' ] - # TODO: does this test still make sense? conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) assert isinstance(conf, dict) + args = [ + 'backtesting', + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '0.5' + ] + with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"): + setup_optimize_configuration(get_args(args), RunMode.BACKTEST) + def test_start(mocker, fee, default_conf, caplog) -> None: start_mock = MagicMock() diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 88a4cea2d..9ebdad2b5 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -12,7 +12,6 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException @@ -130,8 +129,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert log_has('Parameter --print-all detected ...', caplog) -def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None: - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT +def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -139,11 +137,22 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--stake-amount', '1', + '--starting-balance', '2' ] - # TODO: does this test still make sense? conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) assert isinstance(conf, dict) + args = [ + 'hyperopt', + '--config', 'config.json', + '--strategy', 'DefaultStrategy', + '--stake-amount', '1', + '--starting-balance', '0.5' + ] + with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"): + setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + def test_hyperoptresolver(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) From f5bb5f56f1aeefe13075f418d3ea15f24969fdbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Feb 2021 19:53:29 +0100 Subject: [PATCH 032/348] Update documentation with backtesting compounding possibilities --- docs/backtesting.md | 15 ++++++++++++--- docs/bot-usage.md | 2 +- docs/configuration.md | 7 ++++--- docs/hyperopt.md | 2 +- freqtrade/commands/optimize_commands.py | 6 ------ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 9fa9025d8..96911763e 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -49,7 +49,7 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] @@ -108,10 +108,19 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](plugins.md#pairlists) for more information. - + Please read the [pairlists documentation](plugins.md#pairlists) for more information. To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. +### Starting balance + +Backtesting will require a starting balance, which can be given as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. +This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade. + +### Dynamic stake amount + +Backtesting supports [dynamic stake amount](configuration.md#dynamic-stake-amount) by configuring `stake_amount` as `"unlimited"`, which will split the starting balance into `max_open_trades` pieces. +Profits from early trades will result in subsequent higher stake amounts, resulting in compounding of profits over the backtesting period. + ### Example backtesting commands With 5 min candle (OHLCV) data (per default) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4ff6168a0..b65220722 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -67,7 +67,7 @@ optional arguments: --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. diff --git a/docs/configuration.md b/docs/configuration.md index 663d9c5b2..2cc22d6ec 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -218,11 +218,12 @@ To allow the bot to trade all the available `stake_currency` in your account (mi "tradable_balance_ratio": 0.99, ``` -!!! Note - This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). +!!! Tip "Compounding profits" + This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding. !!! Note "When using Dry-Run Mode" - When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. + It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. --8<-- "includes/pricing.md" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ee3d75d0b..d6959b457 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -83,7 +83,7 @@ optional arguments: Enable protections for backtesting.Will slow backtesting down by a considerable amount, but will include configured protections - --dry-run-wallet DRY_RUN_WALLET + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. -e INT, --epochs INT Specify number of epochs (default: 100). diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 130743f68..6323bc2b1 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -30,12 +30,6 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ stake = round_coin_value(config['stake_amount'], config['stake_currency']) raise OperationalException(f"Starting balance ({wallet}) " f"is smaller than stake_amount {stake}.") - pass - # config['dry_run_wallet'] = config['stake_amount'] * \ - # config['max_open_trades'] * (2 - config['tradable_balance_ratio']) - - # logger.warning(f"Changing dry-run-wallet to {config['dry_run_wallet']} " - # "(max_open_trades * stake_amount).") return config From fb489c11c921b77bf6f029c59ecb71f4d8712486 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:07:02 +0100 Subject: [PATCH 033/348] Improve test-coverage of pairlocks --- tests/plugins/test_pairlocks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index dfcbff0ed..fce3a8cd1 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -73,9 +73,13 @@ def test_PairLocks(use_db): assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) if use_db: - assert len(PairLock.query.all()) > 0 + locks = PairLocks.get_all_locks() + locks_db = PairLock.query.all() + assert len(locks) == len(locks_db) + assert len(locks_db) > 0 else: # Nothing was pushed to the database + assert len(PairLocks.get_all_locks()) > 0 assert len(PairLock.query.all()) == 0 # Reset use-db variable PairLocks.reset_locks() From f65092459a39ccd7238550a9518f989bab41feb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:14:25 +0100 Subject: [PATCH 034/348] Fix optimize_reports test --- tests/optimize/test_optimize_reports.py | 32 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index ca6a4ab01..8119c732b 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -102,6 +102,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 + # Retry with losing trade results = {'DefStrat': { 'results': pd.DataFrame( {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], @@ -118,18 +119,31 @@ def test_generate_backtest_stats(default_conf, testdatadir): "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] + "is_open": [False, False, False, True], + "stake_amount": [0.01, 0.01, 0.01, 0.01], + "sell_reason": [SellType.ROI, SellType.ROI, + SellType.STOP_LOSS, SellType.FORCE_SELL] }), - 'config': default_conf} + 'config': default_conf, + 'locks': [], + 'final_balance': 1000.02, + 'backtest_start_time': Arrow.utcnow().int_timestamp, + 'backtest_end_time': Arrow.utcnow().int_timestamp, + } } - assert strat_stats['max_drawdown'] == 0.0 - assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc) - assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc) - assert strat_stats['drawdown_end_ts'] == 0 - assert strat_stats['drawdown_start_ts'] == 0 + stats = generate_backtest_stats(btdata, results, min_date, max_date) + assert isinstance(stats, dict) + assert 'strategy' in stats + assert 'DefStrat' in stats['strategy'] + assert 'strategy_comparison' in stats + strat_stats = stats['strategy']['DefStrat'] + + assert strat_stats['max_drawdown'] == 0.013803 + assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc) + assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc) + assert strat_stats['drawdown_end_ts'] == 1510699380000 + assert strat_stats['drawdown_start_ts'] == 1510697400000 assert strat_stats['pairlist'] == ['UNITTEST/BTC'] # Test storing stats From 324b9dbdff126f53470919398081b2374d30c8b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:31:21 +0100 Subject: [PATCH 035/348] Simplify wallet code --- freqtrade/optimize/backtesting.py | 3 +-- freqtrade/wallets.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 13ffc1d25..b9ae096e2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -115,8 +115,7 @@ class Backtesting: if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) - self.wallets = Wallets(self.config, self.exchange) - self.wallets._log = False + self.wallets = Wallets(self.config, self.exchange, log=False) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c2085641e..553f7c61d 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -27,15 +27,14 @@ class Wallet(NamedTuple): class Wallets: - def __init__(self, config: dict, exchange: Exchange, skip_update: bool = False) -> None: + def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None: self._config = config - self._log = True + self._log = log self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] self._last_wallet_refresh = 0 - if not skip_update: - self.update() + self.update() def get_free(self, currency: str) -> float: balance = self._wallets.get(currency) From 6018a0534367bff3895778a50cec945d03d1a0a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 10:45:22 +0100 Subject: [PATCH 036/348] Improve backtest documentation --- docs/backtesting.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 96911763e..29ddb494b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -108,12 +108,13 @@ All profit calculations include fees, and freqtrade will use the exchange's defa !!! Warning "Using dynamic pairlists for backtesting" Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. - Please read the [pairlists documentation](plugins.md#pairlists) for more information. + Please read the [pairlists documentation](plugins.md#pairlists) for more information. + To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. ### Starting balance -Backtesting will require a starting balance, which can be given as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. +Backtesting will require a starting balance, which can be provided as `--dry-run-wallet ` or `--starting-balance ` command line argument, or via `dry_run_wallet` configuration setting. This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade. ### Dynamic stake amount @@ -281,7 +282,7 @@ A backtesting result will look like that: | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | Trades per day | 3.575 | -| Avg. stake amount | 0.001 | +| Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -368,7 +369,7 @@ It contains some useful key metrics about performance of your strategy on backte | Absolute profit | 0.00762792 BTC | | Total profit % | 76.2% | | Trades per day | 3.575 | -| Avg. stake amount | 0.001 | +| Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | | Best Pair | LSK/BTC 26.26% | @@ -398,7 +399,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower). - `Total trades`: Identical to the total trades of the backtest output table. - `Starting balance`: Start balance - as given by dry-run-wallet (config or command line). -- `End balance`: Final balance - starting balance + absolute profit. +- `Final balance`: Final balance - starting balance + absolute profit. - `Absolute profit`: Profit made in stake currency. - `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). From b2e9295d7f86688e40278ebe253c81b7b6a6450e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 19:57:42 +0100 Subject: [PATCH 037/348] Small stylistic fixes --- freqtrade/optimize/backtesting.py | 1 - freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b9ae096e2..1b6d2e89c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,7 +274,6 @@ class Backtesting: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): - # print(f"{pair}, {stake_amount}") # Enter trade trade = LocalTrade( pair=pair, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 51a48c246..3a6474696 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -652,9 +652,8 @@ class LocalTrade(): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ - .filter(Trade.is_open.is_(True))\ - .scalar() + total_open_stake_amount = Trade.session.query( + func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() else: total_open_stake_amount = sum( t.stake_amount for t in Trade.get_trades_proxy(is_open=True)) @@ -719,6 +718,8 @@ class Trade(_DECL_BASE, LocalTrade): """ Trade database model. Also handles updating and querying trades + + Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' From d9d5617432cc991a2de976f8e84cb107913ea0d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Feb 2021 20:26:13 +0100 Subject: [PATCH 038/348] UPdate backtesting doc for total profit calc --- docs/backtesting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 29ddb494b..2e91b6e74 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -322,8 +322,8 @@ The bot has made `429` trades for an average duration of `4:12:00`, with a perfo earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. -The column `Tot Profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). -In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `Tot Profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. +The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. +In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. From 9cb37409fda6b0d9235ec7069489fbd062f0b873 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Feb 2021 09:56:29 +0100 Subject: [PATCH 039/348] Explicitly convert starting-balance to float --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index e7111f20c..0de0c16a0 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,7 +277,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), - 'total_volume': results['stake_amount'].sum(), + 'total_volume': float(results['stake_amount'].sum()), 'avg_stake_amount': results['stake_amount'].mean(), 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, From 3d65ba2dcbb72f8e96e6ea4e3df65cc34eed5035 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 07:51:33 +0100 Subject: [PATCH 040/348] Add rpc method to delete locks --- freqtrade/persistence/models.py | 1 + freqtrade/rpc/rpc.py | 24 ++++++++++++++++++++++-- tests/rpc/test_rpc.py | 21 ++++++++++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dff59819c..3c9a10fb7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -765,6 +765,7 @@ class PairLock(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { + 'id': self.id, 'pair': self.pair, 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7549c38be..37a2dc1e5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,7 +3,7 @@ This module contains class to define a RPC communications """ import logging from abc import abstractmethod -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from enum import Enum from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union @@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import shorten_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State @@ -663,7 +664,7 @@ class RPC: } def _rpc_locks(self) -> Dict[str, Any]: - """ Returns the current locks""" + """ Returns the current locks """ locks = PairLocks.get_pair_locks(None) return { @@ -671,6 +672,25 @@ class RPC: 'locks': [lock.to_json() for lock in locks] } + def _rpc_delete_lock(self, lockid: Optional[int] = None, + pair: Optional[str] = None) -> Dict[str, Any]: + """ Delete specific lock(s) """ + locks = [] + + if pair: + locks = PairLocks.get_pair_locks(pair) + if lockid: + locks = PairLock.query.filter(PairLock.id == lockid).all() + + for lock in locks: + lock.active = False + lock.lock_end_time = datetime.now(timezone.utc) + + # session is always the same + PairLock.session.flush() + + return self._rpc_locks() + def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 60d9950aa..f745be506 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments -from datetime import datetime +from datetime import datetime, timedelta, timezone +from freqtrade.persistence.pairlock_middleware import PairLocks from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -911,6 +912,24 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: rpc._rpc_forcebuy(pair, None) +@pytest.mark.usefixtures("init_persistence") +def test_rpc_delete_lock(mocker, default_conf): + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtradebot) + pair = 'ETH/BTC' + + PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4)) + PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5)) + PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10)) + locks = rpc._rpc_locks() + assert locks['lock_count'] == 3 + locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id']) + assert locks1['lock_count'] == 2 + + locks2 = rpc._rpc_delete_lock(pair=pair) + assert locks2['lock_count'] == 0 + + def test_rpc_whitelist(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) From 4e5136405788d5f0a593083bfbb9a8b31187bc62 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 19:12:02 +0100 Subject: [PATCH 041/348] Add warning about sandboxes closes #4468 --- docs/sandbox-testing.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md index 9c14412de..5f572eba8 100644 --- a/docs/sandbox-testing.md +++ b/docs/sandbox-testing.md @@ -6,6 +6,10 @@ With some configuration, freqtrade (in combination with ccxt) provides access to This document is an overview to configure Freqtrade to be used with sandboxes. This can be useful to developers and trader alike. +!!! Warning + Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all. + Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading. + ## Exchanges known to have a sandbox / testnet * [binance](https://testnet.binance.vision/) From 6640156ac70f36ff328c0fa2a05ed0ed1d5d8970 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 19:50:39 +0100 Subject: [PATCH 042/348] Support deleting locks via API --- docs/rest-api.md | 9 +++++++++ freqtrade/rpc/api_server/api_schemas.py | 6 ++++++ freqtrade/rpc/api_server/api_v1.py | 14 ++++++++++++-- scripts/rest_client.py | 8 ++++++++ tests/rpc/test_rpc_apiserver.py | 10 ++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index e2b94f080..c41c3f24c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -131,6 +131,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `status` | Lists all open trades. | `count` | Displays number of trades used and available. | `locks` | Displays currently locked pairs. +| `delete_lock ` | Deletes (disables) the lock by id. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). @@ -182,6 +183,11 @@ count daily Return the amount of open trades. +delete_lock + Delete (disable) lock from the database. + + :param lock_id: ID for the lock to delete + delete_trade Delete trade from the database. Tries to close open orders. Requires manual handling of this asset on the exchange. @@ -202,6 +208,9 @@ forcesell :param tradeid: Id of the trade (can be received via status command) +locks + Return current locks + logs Show latest logs. diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 2738e5368..244c5540a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -210,6 +210,7 @@ class ForceBuyResponse(BaseModel): class LockModel(BaseModel): + id: int active: bool lock_end_time: str lock_end_timestamp: int @@ -224,6 +225,11 @@ class Locks(BaseModel): locks: List[LockModel] +class DeleteLockRequest(BaseModel): + pair: Optional[str] + lockid: Optional[int] + + class Logs(BaseModel): log_count: int logs: List[List] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 90e3a612f..7f1179a0b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -11,7 +11,7 @@ from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, DeleteTrade, + BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, ForceBuyPayload, ForceBuyResponse, ForceSellPayload, Locks, Logs, OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, @@ -136,11 +136,21 @@ def whitelist(rpc: RPC = Depends(get_rpc)): return rpc._rpc_whitelist() -@router.get('/locks', response_model=Locks, tags=['info']) +@router.get('/locks', response_model=Locks, tags=['info', 'locks']) def locks(rpc: RPC = Depends(get_rpc)): return rpc._rpc_locks() +@router.delete('/locks/{lockid}', response_model=Locks, tags=['info', 'locks']) +def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_delete_lock(lockid=lockid) + + +@router.post('/locks/delete', response_model=Locks, tags=['info', 'locks']) +def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair) + + @router.get('/logs', response_model=Logs, tags=['info']) def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): return rpc._rpc_get_logs(limit) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b6e66cfa4..90d2e24d4 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -118,6 +118,14 @@ class FtRestClient(): """ return self._get("locks") + def delete_lock(self, lock_id): + """Delete (disable) lock from the database. + + :param lock_id: ID for the lock to delete + :return: json object + """ + return self._delete("locks/{}".format(lock_id)) + def daily(self, days=None): """Return the amount of open trades. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d7d69d0ae..56a496de2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -418,6 +418,16 @@ def test_api_locks(botclient): assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) + # Test deletions + rc = client_delete(client, f"{BASE_URI}/locks/1") + assert_response(rc) + assert rc.json()['lock_count'] == 1 + + rc = client_post(client, f"{BASE_URI}/locks/delete", + data='{"pair": "XRP/BTC"}') + assert_response(rc) + assert rc.json()['lock_count'] == 0 + def test_api_show_config(botclient, mocker): ftbot, client = botclient From 007ac7abb53a377af8aadd90c8be01ed71e9ce35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Mar 2021 20:08:49 +0100 Subject: [PATCH 043/348] Add telegram pair unlocking --- docs/telegram-usage.md | 1 + freqtrade/rpc/api_server/api_v1.py | 15 ++++++++------- freqtrade/rpc/telegram.py | 29 +++++++++++++++++++++++++++-- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_telegram.py | 13 ++++++++++++- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index d4a6fb118..833fae1fe 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,6 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 7f1179a0b..b983402e9 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -11,13 +11,14 @@ from freqtrade.data.history import get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, - BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, - ForceBuyPayload, ForceBuyResponse, - ForceSellPayload, Locks, Logs, OpenTradeSchema, - PairHistory, PerformanceEntry, Ping, PlotConfig, - Profit, ResultMsg, ShowConfig, Stats, StatusMsg, - StrategyListResponse, StrategyResponse, - TradeResponse, Version, WhitelistResponse) + BlacklistResponse, Count, Daily, + DeleteLockRequest, DeleteTrade, ForceBuyPayload, + ForceBuyResponse, ForceSellPayload, Locks, Logs, + OpenTradeSchema, PairHistory, PerformanceEntry, + Ping, PlotConfig, Profit, ResultMsg, ShowConfig, + Stats, StatusMsg, StrategyListResponse, + StrategyResponse, TradeResponse, Version, + WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9d05ae142..fc9676a49 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,6 +6,7 @@ This module manage Telegram communication import json import logging from datetime import timedelta +from html import escape from itertools import chain from typing import Any, Callable, Dict, List, Union @@ -144,6 +145,7 @@ class Telegram(RPCHandler): CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), + CommandHandler(['unlock', 'delete_locks'], self._delete_locks), CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -722,17 +724,39 @@ class Telegram(RPCHandler): try: locks = self._rpc._rpc_locks() message = tabulate([[ + lock['id'], lock['pair'], lock['lock_end_time'], lock['reason']] for lock in locks['locks']], - headers=['Pair', 'Until', 'Reason'], + headers=['ID', 'Pair', 'Until', 'Reason'], tablefmt='simple') - message = "
{}
".format(message) + message = f"
{escape(message)}
" logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _delete_locks(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /delete_locks. + Returns the currently active locks + """ + try: + arg = context.args[0] if context.args and len(context.args) > 0 else None + lockid = None + pair = None + if arg: + try: + lockid = int(arg) + except ValueError: + pair = arg + + self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) + self._locks(update, context) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: """ @@ -850,6 +874,7 @@ class Telegram(RPCHandler): "Avg. holding durationsfor buys and sells.`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/locks:* `Show currently locked pairs`\n" + "*/unlock :* `Unlock this Pair (or this lock id if it's numeric)`\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index f745be506..a22accab5 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -2,7 +2,6 @@ # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments from datetime import datetime, timedelta, timezone -from freqtrade.persistence.pairlock_middleware import PairLocks from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -11,6 +10,7 @@ from numpy import isnan from freqtrade.edge import PairInfo from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade +from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 922aa2de8..0d86c578a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -92,7 +92,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " - "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " + "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], " + "['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" "]") @@ -981,6 +982,16 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None assert 'deadbeef' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0] + context = MagicMock() + context.args = ['XRP/BTC'] + msg_mock.reset_mock() + telegram._delete_locks(update=update, context=context) + + assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] + assert 'randreason' in msg_mock.call_args_list[0][0][0] + assert 'XRP/BTC' not in msg_mock.call_args_list[0][0][0] + assert 'deadbeef' not in msg_mock.call_args_list[0][0][0] + def test_whitelist_static(default_conf, update, mocker) -> None: From 4bb6a27745df744442f10db7f0e4fd30a76d89b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Mar 2021 06:59:58 +0100 Subject: [PATCH 044/348] Don't catch errors that can't happen --- freqtrade/rpc/telegram.py | 48 +++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fc9676a49..168ae0e6a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -721,20 +721,17 @@ class Telegram(RPCHandler): Handler for /locks. Returns the currently active locks """ - try: - locks = self._rpc._rpc_locks() - message = tabulate([[ - lock['id'], - lock['pair'], - lock['lock_end_time'], - lock['reason']] for lock in locks['locks']], - headers=['ID', 'Pair', 'Until', 'Reason'], - tablefmt='simple') - message = f"
{escape(message)}
" - logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) - except RPCException as e: - self._send_msg(str(e)) + locks = self._rpc._rpc_locks() + message = tabulate([[ + lock['id'], + lock['pair'], + lock['lock_end_time'], + lock['reason']] for lock in locks['locks']], + headers=['ID', 'Pair', 'Until', 'Reason'], + tablefmt='simple') + message = f"
{escape(message)}
" + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only def _delete_locks(self, update: Update, context: CallbackContext) -> None: @@ -742,20 +739,17 @@ class Telegram(RPCHandler): Handler for /delete_locks. Returns the currently active locks """ - try: - arg = context.args[0] if context.args and len(context.args) > 0 else None - lockid = None - pair = None - if arg: - try: - lockid = int(arg) - except ValueError: - pair = arg + arg = context.args[0] if context.args and len(context.args) > 0 else None + lockid = None + pair = None + if arg: + try: + lockid = int(arg) + except ValueError: + pair = arg - self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) - self._locks(update, context) - except RPCException as e: - self._send_msg(str(e)) + self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) + self._locks(update, context) @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: From 7c35d107abfbba0152baa04f8c3ad2c4f3607502 Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:24:00 +0530 Subject: [PATCH 045/348] rest_client.py first --- scripts/rest_client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b6e66cfa4..eb084f400 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -75,7 +75,7 @@ class FtRestClient(): :return: json object """ return self._post("start") - + def stop(self): """Stop the bot. Use `start` to restart. @@ -174,6 +174,14 @@ class FtRestClient(): """ return self._get("show_config") + def ping(self): + """simple ping""" + + if self.show_config()['state']=="running": + return {"status": "pong"} + else: + return{"status": "not_running"} + def logs(self, limit=None): """Show latest logs. From 4fe2e542b4d9d63c17ca7f3490f25c7b823b7c7a Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:25:37 +0530 Subject: [PATCH 046/348] rest_client.py removing tab --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index eb084f400..183a906df 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -75,7 +75,7 @@ class FtRestClient(): :return: json object """ return self._post("start") - + def stop(self): """Stop the bot. Use `start` to restart. From 82bf65f696af779bfbe537e813de6f2492b76c10 Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:49:33 +0530 Subject: [PATCH 047/348] rest_client.py flake8 issues --- scripts/rest_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 183a906df..6aba92e7a 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,12 +176,11 @@ class FtRestClient(): def ping(self): """simple ping""" - if self.show_config()['state']=="running": return {"status": "pong"} else: - return{"status": "not_running"} - + return {"status": "not_running"} + def logs(self, limit=None): """Show latest logs. From 95c635091ef1e7f16ec5fce669d67b26af4a7abc Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:57:05 +0530 Subject: [PATCH 048/348] rest_client.py fixed operator --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 6aba92e7a..39da0a406 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,7 +176,7 @@ class FtRestClient(): def ping(self): """simple ping""" - if self.show_config()['state']=="running": + if self.show_config()['state'] == "running": return {"status": "pong"} else: return {"status": "not_running"} From 218d22ed528da1bcc22b6971c4ff4f4ec847eae6 Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 15:45:16 +0530 Subject: [PATCH 049/348] rest_client.py updated for connection error case --- scripts/rest_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 39da0a406..a7d7705fe 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,7 +176,9 @@ class FtRestClient(): def ping(self): """simple ping""" - if self.show_config()['state'] == "running": + if not self.show_config(): + return {"status": "not_running"} + elif self.show_config()['state'] == "running": return {"status": "pong"} else: return {"status": "not_running"} From a85e656e8d5320db721216365205c16160756d5f Mon Sep 17 00:00:00 2001 From: av1nxsh <79896600+av1nxsh@users.noreply.github.com> Date: Tue, 2 Mar 2021 16:16:20 +0530 Subject: [PATCH 050/348] rest_client.py optimised with var 'configstatus' --- scripts/rest_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index a7d7705fe..ecf961ddd 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -176,9 +176,10 @@ class FtRestClient(): def ping(self): """simple ping""" - if not self.show_config(): + configstatus = self.show_config() + if not configstatus: return {"status": "not_running"} - elif self.show_config()['state'] == "running": + elif configstatus['state'] == "running": return {"status": "pong"} else: return {"status": "not_running"} From 55a315be14c00072983bd15e5e6c1ec21ce430c3 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 2 Mar 2021 13:34:14 +0100 Subject: [PATCH 051/348] fix: avg_stake_amount should not be `NaN` if df is empty --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0de0c16a0..47ddfc9fc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -278,7 +278,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'total_volume': float(results['stake_amount'].sum()), - 'avg_stake_amount': results['stake_amount'].mean(), + 'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_abs'].sum() / starting_balance, 'profit_total_abs': results['profit_abs'].sum(), From 078b77d41be278208b9bc4fb5ddfe224aa562e68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Mar 2021 16:12:22 +0100 Subject: [PATCH 052/348] Fix crash when using unlimited stake and no trades are made --- freqtrade/optimize/optimize_reports.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 47ddfc9fc..52ae09ad1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -8,7 +8,7 @@ from numpy import int64 from pandas import DataFrame from tabulate import tabulate -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, calculate_max_drawdown) from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value @@ -499,8 +499,10 @@ def text_table_add_metrics(strat_results: Dict) -> str: else: start_balance = round_coin_value(strat_results['starting_balance'], strat_results['stake_currency']) - stake_amount = round_coin_value(strat_results['stake_amount'], - strat_results['stake_currency']) + stake_amount = round_coin_value( + strat_results['stake_amount'], strat_results['stake_currency'] + ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited' + message = ("No trades made. " f"Your starting balance was {start_balance}, " f"and your stake was {stake_amount}." From 0968ecc1af8c965e407c45ef038a36a5c888d1e4 Mon Sep 17 00:00:00 2001 From: raoulus Date: Thu, 4 Mar 2021 17:27:04 +0100 Subject: [PATCH 053/348] added "Median profit" column to hyperopt -> export-csv --- freqtrade/optimize/hyperopt.py | 4 ++-- tests/commands/test_commands.py | 2 +- tests/conftest.py | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index eee0f13b3..c46d0da48 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -419,13 +419,13 @@ class Hyperopt: trials['Stake currency'] = config['stake_currency'] base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.avg_profit', 'results_metrics.median_profit', 'results_metrics.total_profit', 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency', + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', 'Stake currency', 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] param_columns = list(results[0]['params_dict'].keys()) trials.columns = base_columns + param_columns diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index c81909025..d5e76eeb6 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1145,7 +1145,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): captured = capsys.readouterr() log_has("CSV file created: test_file.csv", caplog) f = Path("test_file.csv") - assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() + assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() assert f.is_file() f.unlink() diff --git a/tests/conftest.py b/tests/conftest.py index 61899dd53..9834aa36c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1766,7 +1766,7 @@ def hyperopt_results(): 'params_dict': { 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 - 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 + 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'median_profit': -1.2222, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 'total_profit': -0.00125625, 'current_epoch': 1, @@ -1781,7 +1781,7 @@ def hyperopt_results(): 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 'stoploss': {'stoploss': -0.338070047333259}}, - 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'median_profit': -1.2222, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 'total_profit': 6.185e-05, 'current_epoch': 2, @@ -1791,7 +1791,7 @@ def hyperopt_results(): 'loss': 14.241196856510731, 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 - 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 + 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'median_profit': -1.2222, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 'total_profit': -0.13639474, 'current_epoch': 3, @@ -1801,14 +1801,14 @@ def hyperopt_results(): 'loss': 100000, 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 - 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False }, { 'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 - 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 + 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'median_profit': -1.2222, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 'total_profit': -0.002480140000000001, 'current_epoch': 5, @@ -1818,7 +1818,7 @@ def hyperopt_results(): 'loss': 0.545315889154162, 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 - 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 + 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'median_profit': -1.2222, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 'total_profit': -0.0041773, 'current_epoch': 6, @@ -1830,7 +1830,7 @@ def hyperopt_results(): 'params_details': { 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 - 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 + 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'median_profit': -1.2222, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 'total_profit': -0.06339929, 'current_epoch': 7, @@ -1840,7 +1840,7 @@ def hyperopt_results(): 'loss': 20.0, # noqa: E501 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 - 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'median_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 'total_profit': 0.0, 'current_epoch': 8, @@ -1850,7 +1850,7 @@ def hyperopt_results(): 'loss': 2.4731817780991223, 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 - 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 + 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'median_profit': -1.2222, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 'total_profit': -0.044050070000000004, # noqa: E501 'current_epoch': 9, @@ -1860,7 +1860,7 @@ def hyperopt_results(): 'loss': -0.2604606005845212, # noqa: E501 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 - 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 + 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'median_profit': -1.2222, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 'total_profit': 0.00021629, 'current_epoch': 10, @@ -1870,7 +1870,7 @@ def hyperopt_results(): 'loss': 4.876465945994304, # noqa: E501 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 - 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 + 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'median_profit': -1.2222, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 'total_profit': -0.07436117, 'current_epoch': 11, @@ -1880,7 +1880,7 @@ def hyperopt_results(): 'loss': 100000, 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 - 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 'total_profit': 0, 'current_epoch': 12, From d5993db064bcbd5fd1c83163e68d3e26a6af31c2 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 14:59:55 +0100 Subject: [PATCH 054/348] refactor(docs/strategy-customization): change variable name for better readability `cust_info` -> `custom_info` --- docs/strategy-customization.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index fdc95a3c1..6eaafa15c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -311,18 +311,18 @@ The name of the variable can be chosen at will, but should be prefixed with `cus ```python class AwesomeStrategy(IStrategy): # Create custom dictionary - cust_info = {} + custom_info = {} def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Check if the entry already exists - if not metadata["pair"] in self.cust_info: + if not metadata["pair"] in self.custom_info: # Create empty entry for this pair - self.cust_info[metadata["pair"]] = {} + self.custom_info[metadata["pair"]] = {} - if "crosstime" in self.cust_info[metadata["pair"]]: - self.cust_info[metadata["pair"]]["crosstime"] += 1 + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 else: - self.cust_info[metadata["pair"]]["crosstime"] = 1 + self.custom_info[metadata["pair"]]["crosstime"] = 1 ``` !!! Warning From 5cf3194fab8b026d204f8992aa0ad76c18c8bcb5 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 15:03:44 +0100 Subject: [PATCH 055/348] chore(docs/strategy-customization): clean up left over trailing whitespaces --- docs/strategy-advanced.md | 2 +- docs/strategy-customization.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index dcd340fd1..2fe29d431 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -142,7 +142,7 @@ class AwesomeStrategy(IStrategy): return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - desired_stoploss = current_profit / 2 + desired_stoploss = current_profit / 2 # Use a minimum of 2.5% and a maximum of 5% return max(min(desired_stoploss, 0.05), 0.025) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6eaafa15c..983a5f60a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -399,7 +399,7 @@ if self.dp: ### *current_whitelist()* -Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. +Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. The strategy might look something like this: @@ -418,7 +418,7 @@ This is where calling `self.dp.current_whitelist()` comes in handy. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [(pair, '1d') for pair in pairs] - return informative_pairs + return informative_pairs ``` ### *get_pair_dataframe(pair, timeframe)* @@ -583,7 +583,7 @@ All columns of the informative dataframe will be available on the returning data ``` python 'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe - 'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe + 'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe ``` ??? Example "Custom implementation" From cc4e84bb7009330031dfd0aadc8b2db9a1d13b49 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 15:04:18 +0100 Subject: [PATCH 056/348] feature(docs/strategy-customization): add example how to store indicator with DatetimeIndex into custom_info --- docs/strategy-customization.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 983a5f60a..0b09e073f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -333,6 +333,22 @@ class AwesomeStrategy(IStrategy): *** +#### Storing custom information using DatetimeIndex from `dataframe` + +Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. + +class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # add indicator mapped to correct DatetimeIndex to custom_info + # using "ATR" here as example + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + return dataframe + +See: (custom_stoploss example)[WIP] for how to access it + ## Additional data (informative_pairs) ### Get data for non-tradeable pairs From c5900bbd384e3d3c22a5bc717dfddb47dc13c6a4 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 15:16:27 +0100 Subject: [PATCH 057/348] feature(docs/strategy-customization): add example "Custom stoploss using an indicator from dataframe" --- docs/strategy-advanced.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2fe29d431..0cd4d1be0 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -179,6 +179,35 @@ class AwesomeStrategy(IStrategy): return (-0.07 + current_profit) return 1 ``` +#### Custom stoploss using an indicator from dataframe example + +Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" + +See: (Storing custom information using DatetimeIndex from `dataframe` +)[WIP] on how to store the indicator into `custom_info` + +``` python +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + result = 1 + if self.custom_info[pair] is not None and trade is not None: + atr = self.custom_info[pair].loc[current_time]['atr'] + if (atr is not None): + # new stoploss relative to current_rate + new_stoploss = (current_rate-atr)/current_rate + # turn into relative negative offset required by `custom_stoploss` return implementation + result = new_stoploss - 1 + return result +``` --- From 32f35fcd904f44b8a96e23d726da1e5f1e5484b3 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 21:26:21 +0100 Subject: [PATCH 058/348] fix(docs/strategy-customization): "custom_stoploss indicator" example need to check for RUN_MODE --- docs/strategy-advanced.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 0cd4d1be0..8c730b3df 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -188,6 +188,7 @@ See: (Storing custom information using DatetimeIndex from `dataframe` ``` python from freqtrade.persistence import Trade +from freqtrade.state import RunMode class AwesomeStrategy(IStrategy): @@ -200,12 +201,23 @@ class AwesomeStrategy(IStrategy): result = 1 if self.custom_info[pair] is not None and trade is not None: - atr = self.custom_info[pair].loc[current_time]['atr'] - if (atr is not None): + # using current_time directly (like below) will only work in backtesting. + # so check "runmode" to make sure that it's only used in backtesting + if(self.dp.runmode == RunMode.BACKTEST): + relative_sl = self.custom_info[pair].loc[current_time]['atr] + # in live / dry-run, it'll be really the current time + else: + # but we can just use the last entry to get the current value + relative_sl = self.custom_info[pair]['atr].iloc[ -1 ] + + if (relative_sl is not None): + print("Custom SL: {}".format(relative_sl)) # new stoploss relative to current_rate - new_stoploss = (current_rate-atr)/current_rate + new_stoploss = (current_rate-relative_sl)/current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 + + print("Result: {}".format(result)) return result ``` From d05acc30fa2819fe9ca03c224b62d3b46cae86fe Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 3 Mar 2021 22:10:08 +0100 Subject: [PATCH 059/348] fix(docs/strategy-customization): remove superflous `prints` from example code --- docs/strategy-advanced.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 8c730b3df..504b7270e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -211,13 +211,11 @@ class AwesomeStrategy(IStrategy): relative_sl = self.custom_info[pair]['atr].iloc[ -1 ] if (relative_sl is not None): - print("Custom SL: {}".format(relative_sl)) # new stoploss relative to current_rate new_stoploss = (current_rate-relative_sl)/current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 - print("Result: {}".format(result)) return result ``` From b52698197b469d7739076b2c538f7c216f27cc8e Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 14:44:51 +0100 Subject: [PATCH 060/348] refactor(docs/strategy-advanced): extract "Storing information" section from `strategy-customization.md` --- docs/strategy-advanced.md | 47 ++++++++++++++++++++++++++++++++++ docs/strategy-customization.md | 47 ---------------------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 504b7270e..2cd411078 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -11,6 +11,53 @@ If you're just getting started, please be familiar with the methods described in !!! Tip You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` +## Storing information + +Storing information can be accomplished by creating a new dictionary within the strategy class. + +The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. + +```python +class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Check if the entry already exists + if not metadata["pair"] in self.custom_info: + # Create empty entry for this pair + self.custom_info[metadata["pair"]] = {} + + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 + else: + self.custom_info[metadata["pair"]]["crosstime"] = 1 +``` + +!!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + +!!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + +*** + +### Storing custom information using DatetimeIndex from `dataframe` + +Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. + +class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # add indicator mapped to correct DatetimeIndex to custom_info + # using "ATR" here as example + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + return dataframe + +See `custom_stoploss` examples below on how to access the saved dataframe columns + ## Custom stoploss A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0b09e073f..a66be013e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -302,53 +302,6 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w The Metadata-dict should not be modified and does not persist information across multiple calls. Instead, have a look at the section [Storing information](#Storing-information) -### Storing information - -Storing information can be accomplished by creating a new dictionary within the strategy class. - -The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. - -```python -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # Check if the entry already exists - if not metadata["pair"] in self.custom_info: - # Create empty entry for this pair - self.custom_info[metadata["pair"]] = {} - - if "crosstime" in self.custom_info[metadata["pair"]]: - self.custom_info[metadata["pair"]]["crosstime"] += 1 - else: - self.custom_info[metadata["pair"]]["crosstime"] = 1 -``` - -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. - -!!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - -*** - -#### Storing custom information using DatetimeIndex from `dataframe` - -Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. - -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # add indicator mapped to correct DatetimeIndex to custom_info - # using "ATR" here as example - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') - return dataframe - -See: (custom_stoploss example)[WIP] for how to access it - ## Additional data (informative_pairs) ### Get data for non-tradeable pairs From 4064f856d1b47d68dbd1d40aa09cbb47c342720c Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 14:50:04 +0100 Subject: [PATCH 061/348] fix(docs/strategy-customization): add "hyperopt" to runmode check for custom_info in custom_stoploss example --- docs/strategy-advanced.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2cd411078..f230a2371 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -249,8 +249,8 @@ class AwesomeStrategy(IStrategy): result = 1 if self.custom_info[pair] is not None and trade is not None: # using current_time directly (like below) will only work in backtesting. - # so check "runmode" to make sure that it's only used in backtesting - if(self.dp.runmode == RunMode.BACKTEST): + # so check "runmode" to make sure that it's only used in backtesting/hyperopt + if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): relative_sl = self.custom_info[pair].loc[current_time]['atr] # in live / dry-run, it'll be really the current time else: From 1a02a146a1e1023fba4f040e88eec3d9b8ace32a Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 14:59:08 +0100 Subject: [PATCH 062/348] feature(docs/strategy-advanced/custom_info-storage/example): add ATR column calculation --- docs/strategy-advanced.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f230a2371..81ba24a67 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,15 +46,19 @@ class AwesomeStrategy(IStrategy): Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. +```python +import talib.abstract as ta class AwesomeStrategy(IStrategy): # Create custom dictionary custom_info = {} def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # add indicator mapped to correct DatetimeIndex to custom_info # using "ATR" here as example + dataframe['atr'] = ta.ATR(dataframe) + # add indicator mapped to correct DatetimeIndex to custom_info self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') return dataframe +``` See `custom_stoploss` examples below on how to access the saved dataframe columns From 22a558e33120a9e16a29c53b4f00ee268e30d7fe Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 15:01:21 +0100 Subject: [PATCH 063/348] fix(docs/strategy-advanced): fix link to custom_info storage --- docs/strategy-advanced.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 81ba24a67..d685662eb 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -234,8 +234,7 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -See: (Storing custom information using DatetimeIndex from `dataframe` -)[WIP] on how to store the indicator into `custom_info` +See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` ``` python from freqtrade.persistence import Trade From a6ef354a5fc5c289aee14ace1881055471370a10 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 18:52:23 +0100 Subject: [PATCH 064/348] fix(docs/strategy-advanced): use `get_analyzed_dataframe()` instead of `custom_info.iloc` --- docs/strategy-advanced.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index d685662eb..42ffaa423 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -257,8 +257,11 @@ class AwesomeStrategy(IStrategy): relative_sl = self.custom_info[pair].loc[current_time]['atr] # in live / dry-run, it'll be really the current time else: + # but we can just use the last entry from an already analyzed dataframe instead + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, + timeframe=self.timeframe) # but we can just use the last entry to get the current value - relative_sl = self.custom_info[pair]['atr].iloc[ -1 ] + relative_sl = dataframe['atr'].iat[-1] if (relative_sl is not None): # new stoploss relative to current_rate From c56b9cd75172ef43d3de33814fd79c3c1f1a3ec1 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 18:50:48 +0100 Subject: [PATCH 065/348] fix(docs/strategy-advanced): add warnings --- docs/strategy-advanced.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 42ffaa423..4cc1adb49 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -60,6 +60,12 @@ class AwesomeStrategy(IStrategy): return dataframe ``` +!!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + +!!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + See `custom_stoploss` examples below on how to access the saved dataframe columns ## Custom stoploss @@ -236,6 +242,11 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` +!!! Warning + only use .iat[-1] in live mode, not in backtesting/hyperopt + otherwise you will look into the future + see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies + ``` python from freqtrade.persistence import Trade from freqtrade.state import RunMode @@ -260,7 +271,10 @@ class AwesomeStrategy(IStrategy): # but we can just use the last entry from an already analyzed dataframe instead dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - # but we can just use the last entry to get the current value + # WARNING + # only use .iat[-1] in live mode, not in backtesting/hyperopt + # otherwise you will look into the future + # see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies relative_sl = dataframe['atr'].iat[-1] if (relative_sl is not None): From 900deb663a282b5e234993e6b9b36ccc5a4df487 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 19:58:43 +0100 Subject: [PATCH 066/348] fix(docs/strategy-advanced/custom_stoploss/example): check if "pair" exists in "custom_info" before requesting --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4cc1adb49..c166f87fb 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -261,7 +261,7 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: result = 1 - if self.custom_info[pair] is not None and trade is not None: + if self.custom_info and pair in self.custom_info and trade: # using current_time directly (like below) will only work in backtesting. # so check "runmode" to make sure that it's only used in backtesting/hyperopt if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): From 1304918a29de43e34b207b21a6f9b3b92bd79023 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Thu, 4 Mar 2021 19:59:57 +0100 Subject: [PATCH 067/348] fix(docs/strategy-advanced/custom_info-storage/example): only add to "custom_info" in backtesting and hyperopt --- docs/strategy-advanced.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index c166f87fb..5c7ae83bc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -55,8 +55,9 @@ class AwesomeStrategy(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # using "ATR" here as example dataframe['atr'] = ta.ATR(dataframe) - # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + if self.dp.runmode.value in ('backtest', 'hyperopt'): + # add indicator mapped to correct DatetimeIndex to custom_info + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') return dataframe ``` From 161a4656d5769ac3942fc2f1ec96d971bf224494 Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Mar 2021 20:05:21 +0100 Subject: [PATCH 068/348] Update docs/strategy-advanced.md Co-authored-by: Matthias --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 5c7ae83bc..56061365e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -246,7 +246,7 @@ See: "Storing custom information using DatetimeIndex from `dataframe`" example a !!! Warning only use .iat[-1] in live mode, not in backtesting/hyperopt otherwise you will look into the future - see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies + see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. ``` python from freqtrade.persistence import Trade From dfeafc22044b169c99c39436ac9679ea31211afc Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 4 Mar 2021 20:05:27 +0100 Subject: [PATCH 069/348] Update docs/strategy-customization.md Co-authored-by: Matthias --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a66be013e..aebc51509 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -300,7 +300,7 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. The Metadata-dict should not be modified and does not persist information across multiple calls. -Instead, have a look at the section [Storing information](#Storing-information) +Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information) ## Additional data (informative_pairs) From bc05d03126aa7a9622b2fa75552c1e026c17d0f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 19:21:09 +0100 Subject: [PATCH 070/348] Make best / worst day absolute --- docs/backtesting.md | 10 +++++----- freqtrade/optimize/optimize_reports.py | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2e91b6e74..d02c59f05 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -289,8 +289,8 @@ A backtesting result will look like that: | Worst Pair | ZEC/BTC -10.18% | | Best Trade | LSK/BTC 4.25% | | Worst Trade | ZEC/BTC -10.25% | -| Best day | 25.27% | -| Worst day | -30.67% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | @@ -376,8 +376,8 @@ It contains some useful key metrics about performance of your strategy on backte | Worst Pair | ZEC/BTC -10.18% | | Best Trade | LSK/BTC 4.25% | | Worst Trade | ZEC/BTC -10.25% | -| Best day | 25.27% | -| Worst day | -30.67% | +| Best day | 0.00076 BTC | +| Worst day | -0.00036 BTC | | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | @@ -406,7 +406,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. - `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. -- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade +- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 52ae09ad1..099976aa9 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -196,13 +196,18 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: return { 'backtest_best_day': 0, 'backtest_worst_day': 0, + 'backtest_best_day_abs': 0, + 'backtest_worst_day_abs': 0, 'winning_days': 0, 'draw_days': 0, 'losing_days': 0, 'winner_holding_avg': timedelta(), 'loser_holding_avg': timedelta(), } - daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum() + daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum() + daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10) + worst_rel = min(daily_profit_rel) + best_rel = max(daily_profit_rel) worst = min(daily_profit) best = max(daily_profit) winning_days = sum(daily_profit > 0) @@ -213,8 +218,10 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: losing_trades = results.loc[results['profit_ratio'] < 0] return { - 'backtest_best_day': best, - 'backtest_worst_day': worst, + 'backtest_best_day': best_rel, + 'backtest_worst_day': worst_rel, + 'backtest_best_day_abs': best, + 'backtest_worst_day_abs': worst, 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, @@ -470,8 +477,10 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Worst trade', f"{worst_trade['pair']} " f"{round(worst_trade['profit_ratio'] * 100, 2)}%"), - ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), - ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), + ('Best day', round_coin_value(strat_results['backtest_best_day_abs'], + strat_results['stake_currency'])), + ('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'], + strat_results['stake_currency'])), ('Days win/draw/lose', f"{strat_results['winning_days']} / " f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), From 731ab5d2a775ff5f10d345e24066ac0398cf597b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 19:22:57 +0100 Subject: [PATCH 071/348] Fix too long line errors --- freqtrade/optimize/hyperopt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c46d0da48..955f97f33 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -419,14 +419,16 @@ class Hyperopt: trials['Stake currency'] = config['stake_currency'] base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', 'results_metrics.total_profit', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', 'loss', 'is_initial_point', 'is_best'] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] trials = trials[base_metrics + param_metrics] - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', 'Stake currency', - 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', + 'Stake currency', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] param_columns = list(results[0]['params_dict'].keys()) trials.columns = base_columns + param_columns From 345f7404e990451e4e2024b20df9d9f0a975cdf0 Mon Sep 17 00:00:00 2001 From: Patrick Weber Date: Fri, 5 Mar 2021 12:56:11 -0600 Subject: [PATCH 072/348] Add strategy name to HyperOpt results filename This just extends the HyperOpt result filename by adding the strategy name. This allows analysis of HyperOpt results folder with no additional necessary context. An alternative idea would be to expand the result dict, but the additional static copies are non value added. --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 955f97f33..66e11cf68 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -77,8 +77,9 @@ class Hyperopt: self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + strategy = str(self.config['strategy']) self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / f'hyperopt_results_{time_now}.pickle') + 'hyperopt_results' / f'strategy_{strategy}_' f'hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) From 5196306407a7d0c764d961d43e19c07ccefc4161 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 20:03:49 +0100 Subject: [PATCH 073/348] Remove deprecated profit return value --- freqtrade/rpc/api_server/api_schemas.py | 2 -- freqtrade/rpc/rpc.py | 2 -- tests/rpc/test_rpc.py | 8 ++++---- tests/rpc/test_rpc_apiserver.py | 2 -- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 244c5540a..32a1c8597 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -62,14 +62,12 @@ class PerformanceEntry(BaseModel): class Profit(BaseModel): profit_closed_coin: float - profit_closed_percent: float profit_closed_percent_mean: float profit_closed_ratio_mean: float profit_closed_percent_sum: float profit_closed_ratio_sum: float profit_closed_fiat: float profit_all_coin: float - profit_all_percent: float profit_all_percent_mean: float profit_all_ratio_mean: float profit_all_percent_sum: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 37a2dc1e5..fa830486e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -402,14 +402,12 @@ class RPC: num = float(len(durations) or 1) return { 'profit_closed_coin': profit_closed_coin_sum, - 'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), 'profit_closed_ratio_mean': profit_closed_ratio_mean, 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), 'profit_closed_ratio_sum': profit_closed_ratio_sum, 'profit_closed_fiat': profit_closed_fiat, 'profit_all_coin': profit_all_coin_sum, - 'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), 'profit_all_ratio_mean': profit_all_ratio_mean, 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a22accab5..b11470711 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -413,10 +413,10 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) - assert prec_satoshi(stats['profit_closed_percent'], 6.2) + assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_all_coin'], 5.802e-05) - assert prec_satoshi(stats['profit_all_percent'], 2.89) + assert prec_satoshi(stats['profit_all_percent_mean'], 2.89) assert prec_satoshi(stats['profit_all_fiat'], 0.8703) assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' @@ -482,10 +482,10 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert prec_satoshi(stats['profit_closed_coin'], 0) - assert prec_satoshi(stats['profit_closed_percent'], 0) + assert prec_satoshi(stats['profit_closed_percent_mean'], 0) assert prec_satoshi(stats['profit_closed_fiat'], 0) assert prec_satoshi(stats['profit_all_coin'], 0) - assert prec_satoshi(stats['profit_all_percent'], 0) + assert prec_satoshi(stats['profit_all_percent_mean'], 0) assert prec_satoshi(stats['profit_all_fiat'], 0) assert stats['trade_count'] == 1 assert stats['first_trade_date'] == 'just now' diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 56a496de2..8590e0d21 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -624,14 +624,12 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'latest_trade_timestamp': ANY, 'profit_all_coin': 6.217e-05, 'profit_all_fiat': 0.76748865, - 'profit_all_percent': 6.2, 'profit_all_percent_mean': 6.2, 'profit_all_ratio_mean': 0.06201058, 'profit_all_percent_sum': 6.2, 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, 'profit_closed_fiat': 0.76748865, - 'profit_closed_percent': 6.2, 'profit_closed_ratio_mean': 0.06201058, 'profit_closed_percent_mean': 6.2, 'profit_closed_ratio_sum': 0.06201058, From 45322220107ea447b8a0d7626cc6bb1bdd388bdb Mon Sep 17 00:00:00 2001 From: Patrick Weber Date: Fri, 5 Mar 2021 13:16:49 -0600 Subject: [PATCH 074/348] Fixed line length in HyperOpt for new name Fixed line length errors and multiple f strings to facilitate strategy being added in the name --- freqtrade/optimize/hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 66e11cf68..9001a3657 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -79,7 +79,8 @@ class Hyperopt: time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") strategy = str(self.config['strategy']) self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / f'strategy_{strategy}_' f'hyperopt_results_{time_now}.pickle') + 'hyperopt_results' / + f'strategy_{strategy}_hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) From a405d578da0a6eaa6e0e1e5f794672771b094ad3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Mar 2021 20:22:04 +0100 Subject: [PATCH 075/348] Introduce forcebuy ordertype to allow specifiying a different ordertype for forcebuy / forcesells --- config_full.json.example | 1 + docs/configuration.md | 6 ++++-- docs/stoploss.md | 4 ++++ freqtrade/constants.py | 2 ++ freqtrade/freqtradebot.py | 7 ++++++- freqtrade/rpc/rpc.py | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 9a613c0a1..8366774c4 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -50,6 +50,7 @@ "sell": "limit", "emergencysell": "market", "forcesell": "market", + "forcebuy": "market", "stoploss": "market", "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 diff --git a/docs/configuration.md b/docs/configuration.md index 99a5fea04..83ffbbff9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -278,7 +278,7 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy ### Understand order_types -The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. +The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoplosses using market orders. It also allows to set the @@ -290,7 +290,7 @@ the buy order is fulfilled. If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise the bot will fail to start. -For information on (`emergencysell`,`forcesell`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) +For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) Syntax for Strategy: @@ -299,6 +299,7 @@ order_types = { "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcebuy": "market", "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": False, @@ -314,6 +315,7 @@ Configuration: "buy": "limit", "sell": "limit", "emergencysell": "market", + "forcebuy": "market", "forcesell": "market", "stoploss": "market", "stoploss_on_exchange": false, diff --git a/docs/stoploss.md b/docs/stoploss.md index 4a4391655..ae191f639 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -55,6 +55,10 @@ This same logic will reapply a stoploss order on the exchange should you cancel `forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API. +### forcebuy + +`forcebuy` is an optional value, which defaults to the same value as `buy` and is used when sending a `/forcebuy` command from Telegram or from the Rest API. + ### emergencysell `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c03bff0ad..06eaad4f9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -179,6 +179,8 @@ CONF_SCHEMA = { 'properties': { 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, + 'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2f64f3dac..f605d61c4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -520,7 +520,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool: + def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, + forcebuy: bool = False) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -548,6 +549,10 @@ class FreqtradeBot(LoggingMixin): amount = stake_amount / buy_limit_requested order_type = self.strategy.order_types['buy'] + if forcebuy: + # Forcebuy can define a different ordertype + order_type = self.strategy.order_types.get('forcebuy', order_type) + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fa830486e..61e22234d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -593,7 +593,7 @@ class RPC: pair, self._freqtrade.get_free_open_trades()) # execute buy - if self._freqtrade.execute_buy(pair, stakeamount, price): + if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: From 03b89e7f7829ebeb24f79df45f021360af580ce7 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Sat, 6 Mar 2021 00:04:12 +0200 Subject: [PATCH 076/348] Add trade_id in Telegram messages --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 168ae0e6a..fb93caee2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -193,7 +193,7 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n" + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']} (#{msg['trade_id']})\n" f"*Amount:* `{msg['amount']:.8f}`\n" f"*Open Rate:* `{msg['limit']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n" @@ -216,7 +216,7 @@ class Telegram(RPCHandler): msg['emoji'] = self._get_sell_emoji(msg) - message = ("{emoji} *{exchange}:* Selling {pair}\n" + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" From 2472f52874661c79566adbd08469ddd131df9388 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Sat, 6 Mar 2021 01:07:37 +0200 Subject: [PATCH 077/348] Add trade_id to tests --- tests/rpc/test_rpc_telegram.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0d86c578a..86b978f3b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1196,6 +1196,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, @@ -1212,7 +1213,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ + == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1256,6 +1257,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', @@ -1273,7 +1275,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1285,6 +1287,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', @@ -1301,7 +1304,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1384,6 +1387,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'limit': 1.099e-05, @@ -1396,7 +1400,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' @@ -1409,6 +1413,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', @@ -1425,7 +1430,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' From ad0e60b5b662b20be9b2aa4cf799e4e387809aa9 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Sat, 6 Mar 2021 15:07:47 +0200 Subject: [PATCH 078/348] Add trade_id to Cancel messages and reduced lines length --- freqtrade/rpc/telegram.py | 8 +++++--- tests/rpc/test_rpc_telegram.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fb93caee2..037e40983 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -193,7 +193,8 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']} (#{msg['trade_id']})\n" + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" f"*Amount:* `{msg['amount']:.8f}`\n" f"*Open Rate:* `{msg['limit']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n" @@ -205,7 +206,8 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg)) + "Cancelling open buy Order for {pair} (#{trade_id}). " + "Reason: {reason}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) @@ -236,7 +238,7 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair}. Reason: {reason}").format(**msg) + "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 86b978f3b..25b7e35cf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1241,12 +1241,14 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' - 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') + 'Cancelling open buy Order for ETH/BTC (#1). ' + 'Reason: cancelled due to timeout.') def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1324,23 +1326,26 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. ' - 'Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange') msg_mock.reset_mock() telegram.send_msg({ 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' + ' Reason: timeout') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount From 02d7dc47802fe1ea80442d64f0410756e3415075 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Mar 2021 19:55:02 +0100 Subject: [PATCH 079/348] Increase cache size to be large enough to hold all pairs closes #4483 --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index db51a9c77..a1430a223 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -28,7 +28,7 @@ class RangeStabilityFilter(IPairList): self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._refresh_period = pairlistconfig.get('refresh_period', 1440) - self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) if self._days < 1: raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") From 0b81b58d287cad3ffbd628ec221abd44f53b41ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Mar 2021 11:28:54 +0100 Subject: [PATCH 080/348] Use pandas.values.tolist instead of itertuples speeds up backtesting closes #4494 --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1b6d2e89c..bb90fedce 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -206,7 +206,7 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] + data[pair] = df_analyzed.values.tolist() return data def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, From 46965b1a2c9807c6f4521c1c3f9c9755f965725d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:11 +0000 Subject: [PATCH 081/348] Bump coveralls from 3.0.0 to 3.0.1 Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/TheKevJames/coveralls-python/releases) - [Changelog](https://github.com/TheKevJames/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/TheKevJames/coveralls-python/compare/3.0.0...3.0.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6ca1a4d9c..68b1dd53f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==3.0.0 +coveralls==3.0.1 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 From 1f314f7d45e732db9a49d01ba827e3490f8d207e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:15 +0000 Subject: [PATCH 082/348] Bump ccxt from 1.42.47 to 1.42.66 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.42.47 to 1.42.66. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.42.47...1.42.66) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed5d24be1..45a378d6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.2 -ccxt==1.42.47 +ccxt==1.42.66 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4 From a2b9236082379d17157d05a9585360a52718ccdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:23 +0000 Subject: [PATCH 083/348] Bump arrow from 1.0.2 to 1.0.3 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.0.2 to 1.0.3. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.0.2...1.0.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed5d24be1..24c2d2560 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ cryptography==3.4.6 aiohttp==3.7.4 SQLAlchemy==1.3.23 python-telegram-bot==13.3 -arrow==1.0.2 +arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 urllib3==1.26.3 From a9c114d30196f9d9c372bc5f77ca5fbd07970a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 05:28:27 +0000 Subject: [PATCH 084/348] Bump mkdocs-material from 7.0.3 to 7.0.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.3 to 7.0.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.3...7.0.5) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 73ae3ad29..22c09ff69 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.3 +mkdocs-material==7.0.5 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 7950acf6d4883092c8e68fab6e78a5cb104a2f9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 08:53:20 +0000 Subject: [PATCH 085/348] Bump pandas from 1.2.2 to 1.2.3 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.2...v1.2.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e1a8d26f9..3228cc89a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.1 -pandas==1.2.2 +pandas==1.2.3 ccxt==1.42.66 # Pin cryptography for now due to rust build errors with piwheels From 25c9e89956931c660d3656148f728ae66099cb40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 09:15:30 +0000 Subject: [PATCH 086/348] Bump aiohttp from 3.7.4 to 3.7.4.post0 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.4 to 3.7.4.post0. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.4...v3.7.4.post0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3228cc89a..f62c8ff52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.2.3 ccxt==1.42.66 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 -aiohttp==3.7.4 +aiohttp==3.7.4.post0 SQLAlchemy==1.3.23 python-telegram-bot==13.3 arrow==1.0.3 From 4b550dab17d8121dd79545f6d809f38e67f0a5f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Mar 2021 19:40:29 +0100 Subject: [PATCH 087/348] Always reset fake-databases Otherwise results may stick around for the next strategy --- freqtrade/optimize/backtesting.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bb90fedce..aa289dc2b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -174,10 +174,8 @@ class Backtesting: PairLocks.use_db = False PairLocks.timeframe = self.config['timeframe'] Trade.use_db = False - if enable_protections: - # Reset persisted data - used for protections only - PairLocks.reset_locks() - Trade.reset_trades() + PairLocks.reset_locks() + Trade.reset_trades() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ From 37e60061692e43e9094638de77328a65b2ffc541 Mon Sep 17 00:00:00 2001 From: Th0masL Date: Mon, 8 Mar 2021 23:21:56 +0200 Subject: [PATCH 088/348] Fix order_by in trades command --- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/rpc/telegram.py | 8 ++++---- tests/rpc/test_rpc_telegram.py | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 61e22234d..62f1c2592 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -289,9 +289,10 @@ class RPC: """ Returns the X last trades """ if limit > 0: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.id.desc()).limit(limit) + Trade.close_date.desc()).limit(limit) else: - trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all() + trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( + Trade.close_date.desc()).all() output = [trade.to_json() for trade in trades] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 037e40983..759d40197 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -637,13 +637,13 @@ class Telegram(RPCHandler): nrecent ) trades_tab = tabulate( - [[arrow.get(trade['open_date']).humanize(), - trade['pair'], + [[arrow.get(trade['close_date']).humanize(), + trade['pair'] + " (#" + str(trade['trade_id']) + ")", f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades']], headers=[ - 'Open Date', - 'Pair', + 'Close Date', + 'Pair (ID)', f'Profit ({stake_cur})', ], tablefmt='simple') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 25b7e35cf..924490821 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1128,8 +1128,10 @@ def test_telegram_trades(mocker, update, default_conf, fee): msg_mock.call_count == 1 assert "2 recent trades:" in msg_mock.call_args_list[0][0][0] assert "Profit (" in msg_mock.call_args_list[0][0][0] - assert "Open Date" in msg_mock.call_args_list[0][0][0] + assert "Close Date" in msg_mock.call_args_list[0][0][0] assert "
" in msg_mock.call_args_list[0][0][0]
+    assert bool(re.search("just now[ ]*XRP\\/BTC \\(#3\\)  1.00% \\(None\\)",
+                msg_mock.call_args_list[0][0][0]))
 
 
 def test_telegram_delete_trade(mocker, update, default_conf, fee):

From a1902f226d682ae795ef31cf154eebec62042039 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 9 Mar 2021 19:29:00 +0100
Subject: [PATCH 089/348] Make trade-close sequence clear for mock trades

---
 tests/conftest_trades.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py
index fa9910b8d..df9929db7 100644
--- a/tests/conftest_trades.py
+++ b/tests/conftest_trades.py
@@ -88,7 +88,7 @@ def mock_trade_2(fee):
         timeframe=5,
         sell_reason='sell_signal',
         open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
-        close_date=datetime.now(tz=timezone.utc),
+        close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
     )
     o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
     trade.orders.append(o)

From 99583bbd0ca9525aca9968740b4eda5f9e3da9a8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 9 Mar 2021 20:21:08 +0100
Subject: [PATCH 090/348] Fix problem with FTX

 where cancelled orders are "cancelled", not "canceled"
---
 freqtrade/exchange/exchange.py  | 3 ++-
 freqtrade/freqtradebot.py       | 4 ++--
 freqtrade/persistence/models.py | 2 +-
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 617cd6c26..e457b78de 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -1053,7 +1053,8 @@ class Exchange:
         :param order: Order dict as returned from fetch_order()
         :return: True if order has been cancelled without being filled, False otherwise.
         """
-        return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0
+        return (order.get('status') in ('closed', 'canceled', 'cancelled')
+                and order.get('filled') == 0.0)
 
     @retrier
     def cancel_order(self, order_id: str, pair: str) -> Dict:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index f605d61c4..27c8bd48a 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1023,13 +1023,13 @@ class FreqtradeBot(LoggingMixin):
         was_trade_fully_canceled = False
 
         # Cancelled orders may have the status of 'canceled' or 'closed'
-        if order['status'] not in ('canceled', 'closed'):
+        if order['status'] not in ('cancelled', 'canceled', 'closed'):
             corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
                                                             trade.amount)
             # Avoid race condition where the order could not be cancelled coz its already filled.
             # Simply bailing here is the only safe way - as this order will then be
             # handled in the next iteration.
-            if corder.get('status') not in ('canceled', 'closed'):
+            if corder.get('status') not in ('cancelled', 'canceled', 'closed'):
                 logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
                 return False
         else:
diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index 3c9a10fb7..a4702fdf5 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -425,7 +425,7 @@ class Trade(_DECL_BASE):
             self.close_rate_requested = self.stop_loss
             if self.is_open:
                 logger.info(f'{order_type.upper()} is hit for {self}.')
-            self.close(order['average'])
+            self.close(safe_value_fallback(order, 'average', 'price'))
         else:
             raise ValueError(f'Unknown order type: {order_type}')
         cleanup_db()

From 60f6b998d3cc27599b3e95fa2da63f573d3727b8 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 10 Mar 2021 09:27:03 +0100
Subject: [PATCH 091/348] Update logo with smiling one

---
 docs/images/logo.png | Bin 12030 -> 11025 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/docs/images/logo.png b/docs/images/logo.png
index c7138e84b7e6f06a523e1d1a27b21691c6793288..8a7ffdd7091e40e0713c71967fa2b4bc32cad64c 100644
GIT binary patch
literal 11025
zcmZWvbySpn6Mg9J25A(eQ9wkx8>AcQ4rM8+r8}gN?(PO9MY_9}?gnZ2p7-x>&)GP8
zcF%8jo|!v$?wv3dC21^la&!Oyuw-Quixw)}hJJ>p#8atV@JASiFI}soU02)A6LR8&7{W!zJ
zi%|0ZMOZL?V3;9r8Ks|WIG0ug7Y`1BX|MFKmi1)a52d#92*)6C?K`x5gYNLamPnff
zXZiYz0WP6r^tMoU!8$5!ph$OOY#;+eyy=_x)PZ4$_sgC_dMd=_X86kNUH)2?)^GP8
zl_jMo-&`u%V#6&1Xl;h!rCd-vfW4EuU
zOTi|PQt8kvHfF*U7&J~yM&_YfHJ1f^AsX;>OKvjEkv>Oe>Q0W0RdHJH4zie3($LUw
z*4NjkWfO)fi+@8w7YsIlk^Iulo3NraRM*nd1L{?LoEaG0aHWdHR-l}WL8mc2!C0RN
z2ng~npFMSTf3gb-K4BKH{w`;0L;f7d!`wBubO3vL;_p#Y@CAvneX?`n~y;MSQOQTgJe|rmkXT$&EPunK`l4Yq|NW$HKybG-)sw>NXOmAmXvsp7D_gEDgRW$7`Fg
z6^e(eb&K(u5&G6()h38)=q2E#`(6rlv248OjsPHTM>|-{E-oh&`=bt!(6Z+zst~KA
zuK;XNHxZKQWH^bCau;-T!$)|Cu_C5%TQ|9+K`t&YQ+MhAKV*t-(UXhVd6Lt`I%4t}
z*kJ&s4;|?EDz3_2j6|JgKo8t9Dhazq(((5#Xvk6VNFDJhj1|3A@u}fDYtpf-VvzuCnqBzz2If^1Xx0BYJRu|0Vkq#L9?F0}wE=REjG!AC|$%MZ=Xh$iq$
zsIZ+%?*#O&Exb^S(yu4WEiLx;_R%V6B_EKGkdpIqbCtsC(*RCrgnc&djso15fHTI9
z{Nmzb=IQQ%kQE2FRo|B@%G$2aI!
zXeLb&kBp4WvgtKWJI+_bhA)_X#zKYHCdRdmpARvNUG
zqTo<@;V0|`;^EW<6kD+o4`rEMpzwnd@18l^Kf
zKVw~gQmnen8H60Uwr%YPF4(!Qem|`6z#n4d&p5fr5f6#1zbGv&jRX#m2HoGx_?maCyn%#GGpNxZHW8
z+rshdtlP(LEm`!VGl}XIEImgM4n_nF0p*C**qvx9{3pF6MgkItXTN^^LcX76zW{OZh!<8xn7pggc)+GGT6Oqey`27S-3g$n-}
zct$@_$*7_=ir|ER>Fo;=W
z1scxet^r~ZGH-KpCg|l^U0#uLmJ+3gR+S-}OEhx|coCJ)631#;C-xx)-==H6ZvM4t
z=Ldsp4~uqx&~s&n){A_S^;>#Z;rBwh&ieLkiyXnMaZm8)-5;1e2~XEmzAJ7!!>RI~
zHz#c5+*acbIjy5v!fX3#8Oh0v&6|+Rkq^|%jrL31-QC@T!Y_AY51o_~J0qXIU+tOJ
zKag^n{8L=wH2Id*z`A
z>YYGhg2ib19YRIG(&A!qn+T53Elj9=t-i65Ty#P3`_>QX@ZXopsqeP=3ZgY5db@4s
zz1O^NCZx`Ii{g#W$7g3PkOZa5Ira@(_?5A{2j*sm=;qLRI9-?%!@m<-g8}fio>O=e
zAK-eNq~{Q}{m^Dub&3RIUTY{UEL3vvV{mX?ao76*B4IzTGIWMHn*bAF7NxRbs`x60sJXJr`>LA-hFx>080f0Oc!ci
zM#et5HcfT)1R2yYM_XH4gI35s_V|;%V(_=(Y0G=Ri;|xo99Oxj#&*csy)b*ya~0wA
zi`7f{=$(P%Y~xlRv1ssBX1(3$;_L+a1@ldavsr)ryo>HZorJCi1qk$8>B1
zPt3v2&S>qjy}eDgd^!4)CxTayc27!bdHr3j>cH^u6)B#co}P!Wy>7)UlcuJtt1Hj8
zs=ky|@KZsu;h-Q0Zb8?Fb6E&;y!MOreyhG|Lf-r{jx!({oG8l6FEu%=
zw8nw`T5~kx(+9WV6t2(N<%5h0DNeF`hLclMCG@#7HrFW^3h8{GnMOs^RRqy9a-z$#
zeC|s^l$?*|Yl`kJdV71f!=uXenj8uw%m2LvQHwHX=0`#T;a(vEoXEe^HDAd+!$!M>
z?uS=Eto>4>$DS2fH{P<@l@!|te?-t8erOb2{6bpnFP#dbpj`33Ss9o!a9bGw(j;cf
zb*8MsPrp(~`<$yR`HL+G7qXnlvG^KUAAaIuG{gVIf9%1
zAI93cx)>j@J_-I@9M9L7PJ;(xc(G*}rvxLQU>BockxNW5L$YUVO1f=ow6$+uA|lII
z{)z}61rep=K(Xz3DNY4T)R_mEQ$C$E)3XT+ua(RlB9U-8XlHrTugN{UYBG7TR3Si^LFer_ozCwH~k7ZZ~lXH+($DlPA5eErPtF=4N+Wz13T+P1RU
z)zr{%E~IZ1NCZelkrT}=FAG?mZ}jfw=H{}WZ5xvo?wqup3hChJ64V
z!|?s}p`*~(jllE5G>3b*$5y4d*jOiv-f$w4!^14mG`!<
zd~zhEG-am}Drt_2;IKn_9^(IcDnCb-uVeObPJ0yhtXm@+bCMRj;;95`2vr*XKH!?8
z4@CUc*-DK26WKkcJTWox@m>g9QhR&jxM?jK!k2O_<{y6x*HuZ0!R^I
zuYt7y=ME~!8wFgR;<%{u%|4VXPwGOr3g90!qEnD`5bubc7B9Vt*pQL*!3!vooL{?xvVsi~=mz2MgkiBu8Fh|b?fzx}64L}~Smn}?Nc
z*i%KyIQ68TsXULk{`4ZbUdft`+5ko3*LrRE=#{UHT_%V@Le8oKU!ghuZ^6;T#N_Ot
ztO{8_x`}780ZLac^bWG8^6PFVO_b&3>1IuOtzF&dRUBSwU2p--4XJUWvl^^rH?v1g
zS@5;iS4vjxYT&))XhH!io;}ncNzPv!9=ZmQ5+4LZkba$4ZfSpM*@W;9ydM*wAMy`3
zCcRj+tr}Pmh)YXj*-Ae?uiQfwzD4r6O5c+pX>b;R&UJZcKVv}kT5~5|U2`!4XL@~~
zFR9}a61JMM{~a$Sd)=(K)i&9VWeSRQ-+$3^36}VdaErs=R7aPRSQvoO(|-ekr2jZG*Ns$FNHm@4w5)|O43
z{|>WL=d9`S8z)Oef%<`*E#Bi)T-ogb2&ReHA>!P8k~H=8^|4j$_u<2tf@#Z;DMhO8
zwAfgYzma6(V2>8(by${RCAN9Q(s93@(HcU$_nV7+hxmvNnM>CM-ur*Heg%}A^!XA;
zA~$noY2I9H4+ZeHPNHh?&8MWKhz&Yd1E`rNiu|S`F`>?QqL`a4k*aTv7Rk${>fc9e
zHpvz6+w!Ibe9Cu0Na8T^5rN$~;#^!Nqq9jZNC9zP^tV0NFkkU@L{7Nnn$SwGIxz)M*mi8Fk&(RZZ3hQrXw%Ndru60Ua_kb!&wNCbb4CUWYNsT7
zukDYW8L+b%>RH^(TFtO0rg2)g_xv0wLI|8g$1g>lL4}adA(iKaUMNt5Sl1YP!0KfD
z)0f4svU1s2S0ssY1tu*|)2IL!zHcLuzR}&J16#AzVvpp-6HX^Wtb=Je3dvNbH@`OA
zs00_2bVpy_<&rTH=QvwT>yzV&2R33J7op*T?(iE@4uvzVoAMhJwu#F;$h|LG%x{nQO^(sL~ZD
zTP6LGqZmcj0(Z|0Zl(-p=zBP~x=MYx^Cq*OHcaRNN7Vg_sycTvtaeGWXg6_vz@
zlV_+-C>Osl3VPKuhU0HmM969AI|6+D+((`sZiLT9Kb>xAX=w1sUCutEUVZoOI;`sO`D4Z1ILMl<4mQz6Znu+Vappw1Ql2!=d(4XkBeTl{uz}GO
zWY-~gB{urCJDGI8BWf#@sJr@cXIlkl|rb
zhjOxZa&m*u$4gCFhs0p>cp!B`YZeSiMEI?p*qTqZ8j=Oy(j;xNqFQbZDoaA+pF
zO5U>+xKlh^9EPbq&ibpZuAw-TB{;fs5EPT{Jr>r7ZQ;Dpflaf=@wd92u6lrx?;w?<
z-S&I>x8$fSEh&kHg!E5|=xq7mUvkz(f&s>Tks#KUpE1VXw@x>oQPYEYiMQ&kXc?Pf
z5|#wjYVdafve<5+<21NJbEEi+=AQ>)cO4@cWla%)haO
za<$2v);SRRA>RF>vHKXK%hN)Z!5*)r<%dAWT};2D{)byPCyW+iC-jXNn6)DeLm0uW
zF83xOGt(!#u`$U)W9Ar?Ksd2p@tqYXc-s0Vx!v7i8k|}C%h7nHcW~U2Y$K-2(qwgB
zEMT&_7$^W|IH*gatwrK6Esd5-rm@rKj($fAsIQ?yr{Z48`sCYXIHldUuEGs0n&o_i
zHtUzkO&~w<^xNGL6jS_nPS#2Q=Y#ajA?f2rxhI{;3tb|0+O>dadNjiCZ8``QA$^ec
z#_bf`D<5v~uQTu>SY|W~b?fvfaJEF}>oE{IfxEHBRkisLpqH8yHjU+!^_vp+Gpn?O
zL^|`^w_%FP$~f+QMHLkzK0Cu?YCb+b^0KmF4Xv#|c}OwwqN1Wek`@{hV+rzt1tIVI
z;$R%=N=hLwcLhyN%}A*{w0S3bP@y^j2gaI@8A^^%MIS1YpHs3l1fxd(%6;k_u@2fp
zagdG}(|*tcI)aKeqoM$xlf;th&WCR1^F`IUT@ZH@LT;kyB4Y}w+n<|myMWRZR{DFnWt
z-=C;2w%YXajr8?V?q9i53Sc=7{;HglDeqytzvK>dZQs!n@X&F}tblV++Voy|uy^Z%
zuAOz$8~8q5FFk(bOi4@A`}Cy~nztvAg)U_?G&(Azsjq)@$({<*_Nl2U-&mDC9hvt#
z#$DdV25R*o9v1>i`vgMwP@!xujL}aRo%>JTWW|pgy8=9XHL&VrPW
z!u0n?*;rU?Tb}O^3o2W;mHvVJk-DO!q~>nY0vz^!owOdp*LuQaJ>izo
z9YznlN3<|r)W_XgnY%`GejDXS2+h)l1qoom^tjkJ2DDHd@!@enn|1DJolg`$Pepa`3l;3~nwt
zBbQxve}7+^f}ETorWQG%t6a5&U7l$PS*h!E5_=?&+l3U)l4_45EStiqjS<6t>!RPY
zdo^&BQQ=i$U2=@2w)ZzF(G>5q%
z3L!?@6Gv_Iz!}3M#w@RI5;lDf^VRG4d=)lu9^d
zy4(^c&Y*f}4I?2ab#zcHtB=Dwh+b3rTRn1PW;h;TG#%=mosvwwdiAR_-tE25Rrn*`
z*mZP5YHEw3O-Q9~!vWo1ba$(1?bA)iOQIRx-&)qRpFicC)z{_3zUrdDr>taJa`C-x
zx7*DZL&{R)z6l8Q+PzhTT{(K0`BgIGW9AQWDnx-gjqLugW5<(bW!@WXr!vh?Yc7d$
z8falsJoxedX{&Kk^A{HvQmEAHT>ccYo|t*#ceKs^GuQ|({S7Ml3hhW~R-kySf#Iy16B%VU5I|dT6vesy}
z4Z$xN*I)&7gkAOWNIF7*Uj{f>N3=gG&O1#ieZSMW_;-YSvZbcIP
z^haaTzbhz2WWqj=*f)oPvDhSY{~cb=uM#Dt=Dz`q7U76QoH)fkdEKm%Z7#chpfvGFMCgyI!F(=v)ReYdSoLI(T^
zA5z0|e}8|Eg#Yu}cUyVYMr4v%jqq`Ma5t`)-ur4oI@fuow)Wk7GtJK)7>D3|wYJ?o
z$f=-aZ(mXDH~M&Y*~=@?%AQ+`(|>|kma*HZGxrPqL?AlC8CHmxhQRayw}=#MJo<9t
z)C@`MRbL7#Kgft!c#)Q53v#rYxD~9e4xt;vFyV?0+V~3ib0@2dWsc#Dkv!_ZG#QTIy$&0n*!ag85aYN
zO!fHT!QdDY2!$
zxwtg+UcZ2<)jj@GKEdH^1;OPh^mCpRn!Pa&R=!UQ8l!Q+$+~i;K*r{xV)YEF)be|oyoveqjj)9epl4T%kDz^?T3+hzeO!VfKKcnBS<*NXPSam9n~~CU!)4_<=hCs4zqRn!Q6R=WqRf
zUHXL@jI&ht)Z3q*Y;K7+-W3N9VJF3kq|k2oPk|pgF8Bo=_-kr^D}w|qND3R5-NN^n
zo8M0&4Xa|n;-+Hc!$Jp$OHF5@F--yOuM)Qv)VDDz0cI@f>
z2mt7D|Lp=Ou}DM)q0yG>W1VLrW0G^rFrkNyX;)~*rKWlWo9pO6(%-~eW!rEWVFdLZNJupQ0$HY|0%gBs3$4TW)aAI!C+obWk9KeB`1Hz)#=KbKBOdSAK32Zpz
z3aAs`rVq!ZYhL*o7dM$vv6=@CDH-ueNv<_HIXR{pLM{i>k)Z4GD1xD^MC>Ye4BXl0OoL#O|gECbf{h
zladK1X<(*-AYa|EgJ?4<3!tB4z~}j|92WQC{rmS{w!dqssHkubT5-F2iJpK6AmSD~
z4<_!U=3OO!P^N)B^m<5+5AD!nF$ff^rE;i2ujYU(xFo5BvP(d%ik*Ywv^s$t#6S*2
zPDhOrb@Tt+(9)vQju%5KL8q>@)&NmHG?wcw`-NIbh#{Ey8m}Fl{lyg&6#6XNT3Qx@
z0mJAT)4p(r6%dKaWMSV*I$Tf!obWXd0zP@&R_|Fp=Y&ul@Gb>&)6vn{pkR`YbKw({
zkf78%Up=h77*4xC^#7n2JMiBSIXE3S3z|RT<1nkyMqH^jYWddd->DRd4{U+^9Q->j
z+?+mQLQY|7_y@Y_qPq|*EUXG#q2ohi?K~PvP@rHr4KV2N@vc$n
z!xfVOwVTe62N0y5ym13(gdA4|Vv@5P8fN}-My^p|gVY$~Pi9}KMp>Z00e4=Sw?~gT
z3Ih5oBYuGt$5yqL@B5f
zKp^!`!4wWE!cSLI-j2V{rEoCH`al8HQ>>rCcd5l}0RQt`;>+W%aL}SRup(nm6#a;n
z@B?a`5&?GWASrw>&&){wuEky>0;%9ErBs8*qSAi55h14X%Titz)c&n(wn!
zMjXVa%uG36-^tZGO6jGe39;Ni8yPuTl1kY`GPdJ+q0s&*3iVax^Lcb%h@};Gy<1|}
zQY}$WB#w|g!w^TM1v6F51qpwN*(z`rR`c;Zx`aSo`C$rap{NMnCg#8b`*d%Qa-0%PRfmDVPFWBZ*{zwy!wfaq1qOB$G04X;1!NseIXb>1sb1OG;~pixj9go0RW&s=9KR?c
zAZX9Hi=#EX1$VC#jo^(P(XpVIjJUXXno?1`DfT{iMfMHFPzT1wszA0Y9`X^i8&_C|
zbgK-1>*meaY&*rv0Q~T&)1Pao4V?CIYMW2Li^!ElXkZt>yHLTLWzB(lStwQHFf5hyPrRQzBy-q
z{sDS*_sz|GI_i3U`QSbR0SUW4=iTJLjg1XCfQQbU+7Ew=jzIw$s$&2y<9rqu4=8?~
zp)8t`O=36D&DSg&3L3y&KT*uGs;;h13L;6W%-tKNBn{eaq)=3-VU_Ug
z{Jhma4Zw~!?UKXyPX4o*+1AGzet1Q2!($j6iy}b3-wV4<$I#G_1P|}4rkJ2IVg(5y
zLlI-gnRAU;ieW}k;O`lR>em-G$Nc9A_Pu+c0QzCGx|-W!1c~2(-psw{@ZB!|F5!SKVUR6+de0^v?eFtt>btkf)}nv!->u@LsQ~WbZy84
z+|Sm{hmwfE$noHd!B56Cntt;9ceMRlK56Uvc`t=P7-L3agK7c4
zB7w+{q`@UroL@w)|AY~6fi>p+{rh))aBuAtgn-aLeC(p68u5${bSepSgKQI60XR>w9EMMi)UOK)XH=$D1*0E;
ze%IJ4j7eg^)c=Bkup6ETN{0K9nP8)y2x_;~gFc7+5okwz%D2%f{0`l~j_b6#L@;KZuHj5C8xG

literal 12030
zcmY*<1ymeCvn~X8cMb0DED$8PyE|-fw_w5DgIfsB;_mM5?h=B#+ui*4zWd(Wb7rTf
zr>CS(bywF{6``ysg@Q+eVU{C&!
zu8FCGo2w87#Xm;>`}=P>U9BwtFDHAK|4|F9AnU(3tn4gotp7_J{3+mHuYi)Xl?9md
zKl;M#fd7pA|N8!i4}kR_^Z!Sf|JL+Byb#+BF_CRMw|y{SrYTB?)v92M(rz(qGJe8i&K+Ai1lUMP&5D
z2I7?0P|>7lv;8u~PAIy*|9IPI?^xmBY-{RxId2opo$~hZ7)?*-1Og|s_z$BG5Aw?J
zJ#S~En$~QpI?f?W9G+|l_#C%8Mzt0qU5xo*WqZ0yi*p3Z)QT02lm{hC1|ADjbggHs
zinH%Qs@sQNF39T9kXsLeB)+#Zsh>)uqjhrO&`1HDyRiEZ&dBBi-535JFMA_;9C?i?
zR!?UR{(@XK240=SP!?v}TOaQa*4cs{@5B%Xm>|}a?QD-to#InTE}0${h1sbLx4ObY
zi^n7-;TAn=90)V|QU9lTH6sOu?w!cfKGsLe
zVb0CNrniHcaPq}!!wCTR)yp(CK$eZoY%rRc&Vj1i_hBQ|SBuN%{ps-CzU|C%oR!c2
zWo>J*sz8XO!}c(Cvq$&-;-B1uJdfcum@;y#9rYgj2~sq>LE--IH@R;?hp)U50a@Bf
zd?UnARaOU2ONQQdlv&AIhAa40!!Ss=u5~nj5DXQ|anO5?K~?Vo6^~G80Au;@XAMiy
zSCRTI6Wl-@teaqBsAS{q{5WA<$eKAZW2(n#?nUiB|CR!Z%c2O@5
zRy`T->so%;EPTGdzk=+kgAyQ8_3Zimm#aVTv{)|Jn?M5>CJuY>E
z#U@%Uc;391pG2#)S!$hp@WUBdJrXij^AL(sw0wxfWBnkx*M9N(2|=?i)(7fdkaiK4
zxZcvvS+RUV;2LH5B1@adaXO=6gujO8WSG~u%i<*|43X`JZ3B|*a>q=yuE%D(7vn^(
zi2oC7*Q;}Bo_H9cn?Yfn49#d{g3QUI7A!}GmODvne@d00F);MnBL
zV)@=+jNne@*6W{*O#9X&BpwVLZkm40%H}G+JCIA`=J%7Mt+#Uz+D*TgvzHqT*9t?B
zi?l)zWVvF~`vI#zKk?Z|(_dE^32o8$6y~P&>xd|p*1y62Zzo1J+$gm-u_9&GPSdeH|*!wD&e`EVMjvh{cf6+oSswgGsp{!VA$?fY_n_k4S_
zmp66nd%$RP#YQ2&A1B46-#ncu=uye*@<$%+5O?$CqU&O(Zb6)tCuiXaRYdUPdW==%
zPf31kBC|HB00S)fyoEb>s^N}|iVlc->G0o8SPv-G3O(6)>*1B9%Cx>r+GnUu=jGg~xhNE1yR05HyJv-B}+^D)d}mM6w+_f7OR7
zcq)M91nabppuA0R^7W_H9fjE)@0QPvdSJ)XpG>sB(tC@pzB_-fwFNdX(Jf!`4lq9!
z^&AxPQ(KGs9A4HRcd4|0n*aF|28SZMTh;ac9W7Px?jY4@Gnd0cb_0Rk1}cR~XZNCM
z%TF;dWXc!9^T@d#mShff&6Y=fZkN;QGM2jRV_F=|qA{M*M~GHq*R66oqNlD0_~dU|~!OTQ2kK$9gT#jP*l={Lxc!p)@K
zO$nSZeLNlJdi=aU*xaCxI&fv|vH@X)FStqJJeR%$bA2tTgV97@r=J{
zL?6lgUu$qu=y!t9gfFkgRbL^X$8KqRlgH#^feOZmPWh5C94R<}$E&!T3EZ@AQvoWc
z(KnlDR{2ikWPXn(C);~LvO%^?z@V^*$hehA9811{lW-qwplsk)hqs4Ov2M{1`WO-8
zZU{zzV$)#5VU2ZJam?FZ8}%xJ0*zBVXh
zunGO4SmbR4(W5^cW6*h>>KijwYTRzqLdJtf*Nef(Wep2kN(=a8QgLTKK*IUbksPhY
zJj#PDK2hE}C-Qz3H^C^{FZ^<9#vHpomtn~mrrx6qi_<4fh=(e**~eN9x3jd%35Pat
z_y`3XdsWigHg3n)!ck-q<8#>q_h4bk%gewR&v0l%M-68i6T14`HNmsyDMjX6AC~JE
zM^qvtFCMLf!A!7+6S0`gt-zyN)Ghjhz7;(z)SqesCKtp;=@o>howCh-1!^-^5Di|f
zG7>?%Qm4>)-0@u5`_;2ovItcSLsTsxIdyW|kEL`f_L>x04`4ung6{yv)7JD?G;MzO
z^OR}7>_x)z3UpSjs)I48Bi)bs_zh>nMAJx-!|Un47sE?op?`@c{J1T>x&2jk8?ybe
zibt*Svckc4Oj}V_aL0Lf&JJUYhq*bO8i5)aw#W4dEgmYix}xl&
zz;VNYhM<9HD{Nz}wKt#RQ~*2cgNqPsv^y4IB(T8*<>(95$l%yfk+u6wLi-LLbBy?Z)E{ZZ7PFwnC+2+NBizc
zVA<)ZzMoyV__9#thZBKhs4VT|$2NMntCq~N1^w7Lo6kKQ7QnpxXr}jmkGb_ZBAI$J
z+(^%dkMp%EEQ^C!|9B(g2THLiKV%_53mm1i66%D<%<8kJDMQQ
zI{X1uraak{ltlHS8)~fW;o)WLqw^st_qBfodb~|Yu`J8WHsbPdGtTdpQqP%P*v^g4
z()L$amNrTdb;2C@40=$bB6pb`dKyA^)oW`FaK#1GKvt3);`Ver?2!Q#39!+
z`J8?uxrQl@MJ%wl?+wL0ykDnwd7h?_v;Z8y`@}6@^=1EsB?`s2ayubxB>=Wf)&O*F
zz5&_<9k4St=i3&c$88Sqlx!$A=Y1c5;;VCURGfQXpDBs_=kw~7veIoc7CB|TInRDx
z&O0=jh!&|-eU9QRRm-#}T+SyX#jQl@Oja7&#b~qGx8&lk3m>V*umv>8*9+eZ?*+I`
zQSv{wU*HIaE&ob}k!!ZUaQA61l*c`ivGs;l*=uoBhEiz81Lu#MKi*%2Vnt)%rzkh@
z(Kq{PGe1eSe(($|F;?bzYCg8&S&paG_!8@^UjDk=$+O+fyVbv>A(=|*`apxymi5$L
z`i@WfJ&8u)+LqQ?@S^=j&Hpu@1vDZ)g6|35M+(-h)LVx!Swmth5m>%nGc{98Vg4Lw
z+HTqi4$aU}!SIn02BjHzRKUTy6|q?0MpQRTS2)ss;a>Y3g1MnWPK})%EaR$C^<(I4
zE#YI7ffDV&6hg3MLb)z!&}D?vwuO^XLYED5{FtR9t1*4__F`*mi3I8(Sk_V#+fO+3
zt9Jf6`{-B*+a-7bnTYV{Q9mJ|v;Pz?AOD53_5?b2{z$X)rB6yF9HrJ|LJ-aXJ8@GZ
zbTR*TZ3bAjmSxP1*;KjDfvOQ2oh1bl2C^g#qc)si-|EEQKMH%lvR>ApF3a>;1h9{>
zj_AqxoLQUW@{CEBtX*d)pu+eGrEdXQ(CgZq$c|>06Z0ey>koAp
z2Ow}iZMa&*p9My#hT~L#+}ZO(5j0Wxc8fR=80=ykF-{bCH(cW{B{bVY%nO55g04}h
zQKV1&<4A>Y6M>&CCvbAx^_>QACXWlc1DC$oJSX}rMN~8~{smqG{npUy5D6%5>#LfN^CV@BY9vg`Y6uO9?#IxvL}q{u6dT0L
zilVg=?a6Tk=IasYW10BTDW+C;y4vtd2_@wm=iBQ~|ZA|jHK5@nzHPai3
zd{bxkTR9^X`gH{8(s4DqYBl}JocLp9v7bkFy4L@+X2>vFD^n0Z$zpeb;-kWsh+SMb
zH8Pt+7G5(}(Pl94vL?E=0kTiH%smVo`^~PtgQs%JpqX0(6j6%eEc%n0*kzo3@R~Tx
zlKy-Rm8&{28xErB$u7&e>{anCTSdfYI+$#rW+bZ*hKfk5WtEPPMT*GW<_jQaj6OCd
zrE-&qW%l50l5g2IUKOrwCgKhptzNn9TJ%B^HMKI9?DFoYtF2)V!Jb%>XkY5LLBP0*
zZNK4iOT_mh*FiJWKix$1@2MSk>NZGn!nwaj?YSKraFV{(Cm9o^Yr74^>yKnUSMs$_
zL~l3QL-w-p$UbNI7U^N7F#u|WV1q-
zqOwt@RnGd#gm>&LVuchVTYeqSXFiN>maF%&rFvdIaNaIBD!z6c*Vf6y;Bm^9mmua*
z5OZ0~sfTki29w!A46lt^xYNHBNR-3*iLN4&W+Evtf!GFcTeh7|2uGchs2iTI+
z9}!ZOPDe_Zpl#&$WZ}Q%`@ntA{g;>1e&k&<;iS^HA45t$T!|;sq4P;Ct)E!^xLfFc
z*$gW19=a>7X)7K^QKLv7!9|Jx;Ad1Xj`P{n%kUIWurv~;JN^qRE~}ybx_aJld$57=
zm5_d;?$l**>W=-PCtmyAlF=3spQG^(d=khF@jOr)1#weV{$Rfo&Ptp@2KP57aFU4+
zWQ?VGIh{Bx0WIlC3UxLDnYC^?%F(^AYH-fF(}6|)lh-kX{_3auo4`UKJRr4?J7pzA
zUvjBQDOdP{>W41Hw3;@QQ1GEjvi?xwS3m5^Vj=M~A1?L+*mkEq%cgB$Q(Ef8Vt#$s
z{UVvJ=A|}mVfe6?(D&QFA4s}E%g3NASJ>k!(eD+0>FjNy*c{$5^%V$PdX5vq1?qsh
zsz(=G{m(^NzZbZXe3m0LmX)O8^@*`)Dnrr`??zNRe*@S~`}%NB(pPp#;0tXa@cmW1
z`){_@A0o*+Qo43Sl!aw%FL%$5hA$qSJF3-*qj;HNXv0U(f#X=$s%jnjG|p1QJlBgL
z1uqb8aaHhL
zvX%uuG#Bp$y9ck+BG+Q2OR+fdimSuS_>rwquk>(+824?^3-^l!=Q~Buu_LB5p9BE#C(mZ+yAax;2wb?x;{U@voD|J$HkjT3;dtPOtJwd
zC`NYm%X-~B!Bv2
zxLgmph=*ptCu*rmo57ofXM-{9)&i+s+~HC`Xcdyb9U%rERQ8cNRhT}8-;WB12Aq2L
zT}2_1)-b*6K4;@h<$D}=!RUJ+Txa+>?X=|Qonn@Iy<}c%VGp&OQ(uiXCp_s863p-#
z)3yMNy!tVdNsh*FGz?8l8dP|sq>(C(si$GfsW($4zo&j7saY^MC7YCV^0`BtQKPe`
zps~~J8w1RS7)tX}8A$s))*nvtG54>96fGF-TC`w2oO^agZc=+kjcurx+Ly&bN$&Y+
z6^4AUU5-&{MfH{43l5(`i@eG8TPJK-HCO254ET%QcAn?VQupv7+}F@tR<)fA7&Y1E
z>!?2-@U@~>xpGY-rXso=KhM<^wc8QUennV)m2J1v+2r@V*=g}%slDX)xlF+0q&a}wpAxoY61HFS{psVQ
z{z&mSI!kkqkXPX8k^{ozdk3I=NoZti>y%WYBnxqj2#itL?HzDP8aDcgJV0dmo0`Wu
zY42(DJDyU2K9kdVB}op#whV1HSLma3m;z^YscpsXEhKxUAFhMes$gZ*t4hA))B1?6
zu!SWZg2;u-^P!6;dXJv&%TJcyCNv^H4kMvvbL_XRgnv++bvwIH@i`KA*oE{qCxm^K
z_tF9QU)tXor5t~Q`nZQ07}|l{r^<}SGvi_2Nf_L#^kFbFHZOTv&5b^+8u}wUoEIb4
zH0nWl7T3i3irThkMy&0)XC{y_@x|K$sN$P-`WQy~{o9}WvNK)TmYIVlfEKYf%oL<_
zf#0Ba212z&FflrTfuPez>iVdJfb+ChBE(4a^E_$1~gC_+s=~pVp;Z0kQEM1!7VW@Zf
zV$7M#Wje}r%w!oFcT@AM^N<%RU5q`9pkaY+u_ki7D=JcwxugyS#{+t}KyF#RX831k
zHGpCh
z{``i3L{BDIZv&b-XOk-3YU8r(nBzZb!`&iO*2%5`2Y%05nGqd
zNMK_}6swpcfDe)mo85M~`eyIos`d3uIi;VHC~a=fS*K@umU6990#YitoaSh*OI`R(
zNvl4rD!>6ca6vK-hrPk!STM7Jz~yjKp)8935>GL4ohw${{;ZQDi}oGayBQ8tdz*q~
zC*jIVkwcc08+_X7_PVPhq?F0fk|bg1sRx2F|Bb1^B-ZhK44QbAfM*(8z&=3x*n#)V
zR8ASDObqIG#$o{-{zLJw>CL?Ymh_Ves5Q8#*%JV_N4odc&{xeR__89ux92o==~fJm
zv;=)dj4a?%7E%~16fiSo#4v`EjmR+qL#CChWTjSE}b6k=oi
zfBd%0f!Mb9hoLD89)AI2euC4w0hoW)wRjAqP{ANG&Cf5EHTzS0&TCrfy+e88D;qk^
z+!zKkS}+WcNu?1LF6epKdXZjsQ_rey^Kz}+)AxmX9_5OjX{2h#_
zYLUy1NQu_h2@?{_X!$8R`Pe4<@T(>j`L{rBeWWjc%I;E=N-%b_9p%oX9AF^7e^!3r
zA&2Bc!#j(Rff;g4^p|{w=y@9#K(9bp{V}&ROvC)6&)Q3~xm_j-4fpQYk@S!=`=s9}
zc*qf#T!oXUpI~PAc&Td3rN~*orITWk67v<&i!vNxbIwxBsu9l1u8a8T*~isl2G*x_
zioazF)512lVzS;7z6b*EzOdZA##&}FW3>pJ5plz|yE5O7NPG<|a<{Tr5DpDxs%S^7
zy*i}*M0S5PC}uh;;>8kdyyxQpiA(c5u%Mc0VreTA9eMXHDeY5dt@$znLQxdXF@eah
zrLU{W@3D9yWGTf|f$<<2=8JDFQmdF@
z1~DF2hiw;~-a2){oEAHOc`HcTz!3Y-dVuUwEU{HDA;6}=fTof(JSg%oIfc4-A1svGEpLoWshqO|2I_}F+#Ie%$j50zW
z(Y_81po_Uu{B@mZT#s*o$Olg4P>(4dEae_>&W1u5R`{Fr`AqB=LPuY7)IjU3vD~A<
zJR4Be#WS&~y?6P*8ZX4^2SgDhdYw%@7*ge3$djL|p`)UW!)7kWVe&O5Gvf
z87YE!&xos9kITrW1K|U|44Wb!xWns)28Q9kNs}syVa7P%@JL;Uj9G0{DNhl`Ge5U7
zQzPeZPMM03FKB27VWe78W@O2E*-!4oa!HpXU+XeDyk<)W5
zJe+UnB?g#6?2P?>yoAKSFgK0jtVxRq(=>WW08mQB0w}ZKigEPB_irMi_K~_c@pXol
z!?y0NGI2EV?e#<`)32V=I3K?5*4Fcg&3h*l$r0NSiYj#iBb!z%?!*{o|g#^l?cuvj?J&^_Y>nwaI3w|
z@T#O6j#Vm$R*eV`;VKJvAWtII$GBt)xf_UQ4g94U;GfqVdm;ZC7puK@>4uCKM(y>h
z0EN8X2GpusQ0eFwk;(=$#Z~#&0atOf_h9X$jtaI~CH;BeszI%wP@I(c{xuIANK
zbK4euj)A}=2dW;aes&_QFOvKw$|7yc@=)Js&_
z+;^xDd%U5GW~ap%3-YW9WH@_935#RQaCvjYpjEMf9o*a@MdD{1-vo+HF#5?gzmveM
z%gJZ(!IzM^x)}!oYE>i9=GJLW%$1(<@4Z`ms?@7!iW}|>6Qi6Qc6a{Cn7=<
zDI#6}d;#6dl~OZHzaN)DVf|c_cBmxRIz=C&y9g7j9
zUKFREg1+M(mYa~%j&Lrks>=XG9-~)NCZ;Pk)+OjPtdLoAS>;0Qc~>dv!tr>{qU{Rz
zeWR<-?+;5MR(=-#G=OQd#-8Bb`_o3+(l2gL9Vo0v;mf%GDcqJV5yp3Bx2=+VH$TB%
z{4?rjZ*~PV(F~TbAd5@3Blp(wXza%JzdVG#k4hGrdX|ZU0{dlmlp}hZTs`JIJ&ukuo{O6kPjl#|
zar1U8!T56i+A(?x`bh9*Vw`8i4PUi1O?^-rI1}$Ft=O(4l6-LO4<(EnIF#tGvU#=b
zpM9{sQE^neT}`t{pO+V|;Rm6;8%@FZ$Tbz>vy++*Qv5cl$^4dN7H?|Fl^r>C0O;Nl
zjLOrsvf!>RdDI0Zhhjj2kDen2R}L&0*YlyZ)IzjdL-)VK-jKk!|yBiim&vg7Yk8YyQo*h
zAN+!}W3zGF2BmV>pdz4rEnoi)Y#5L+gCxJ*wNfRy%xaO9feZYFMZfgbR#bSciV8Wr
z+IA$w^VM5tZYMvrfoAbDSS1`I^;$wUSQ3ZUPpcA@`|b%eVh663%eZqWK?D8KXqP#Y
zS)Lz_ML`Zt#M8R9JpcHNb6~ubv&r(gA#~B?VARv@uBt&JQjrR0w>ygP#ekY4^uu?8
zcTAx!hb(g4vz2Xg*fR4LId0KKU3YsXTz~y@eF@aE}qJY#24nnesT|gd%fZnyBT%
zr61l(XmF$<0J<+@zQtkeg*Ebv!(6xn1NO$;FHVc!S1EFR%dDeT<;wFHOc%8oVrbn)
zuwo$A_*A8O)VXqk{6y_sIU2H@Ymja{T$)BGe?S@Qi3jIZyx4X
z+;c_vAX&f(-J%onMj+gEF;>Vi{)}=zBHA4p52@w~S?5P~2{D`T56+Iw2!U1i^*kpTAj9&72h@E2EnM={*T$Ssj}WhaRo<|uTeL4mY)>Y{0$EoCl<96=*vqFkUs!2Fg&jqK-xQh%-04nlmm(9&
zvI@@U6p;%gKMI%kwcQ!OGrDb_T1YwV3AFvOeB#f;vzX--hy#_zz&b`hC1uW*fl-`p
zHa4%LrL{T=j6lmY4O1uvT`@H!KR#BK_?DBFDkKbv>KUn+dmvTNr-eo18?#kWgD_TF
zPd`r>8s_}AJE-eVb-s~fSP14Y<~*P@1eMr@xBB#ZelGbVwV`Ko!boeiCu8H0x3dgk
zqO4`UJJ3JsxI;+mR=QM~dBQVBi0)VLOaT;z{%Y>
z-!w;2tj1XfHO_{~At)1<%pGxQF{f&Q=P5J9AN6kbH9R}DVTXT1u+k3t$Y4f!1xi+E%!~mS>X=0qR6hml}y@
z_z7|H3qw<1R+pbz^Kqh7DUpe6^#GiDHNq`f=AjI|F>>_1Ry>|MdkcQZ6j5$#q`_!f
z9SYufxZ#yv-#P>toi})saS^DZ;H99(YX1`Vv&k=UI?ItwynL!~#M7=JVF)r5XFm%i
zvJWq4F)k`R%8&LY~^uak;@5G
zG`*C^NJ}tpy8hA>4ME3?gjR5Ww|)w1YTA{wld|;X@ACSFg#3$eIX7K0C2dg@11+8V
zFN;o9s#7pZO$c2`i|5a$6}PI)7nYF3&6#GlG5a-
z#dIl_Xmdr14|V;qy%F);-CM>aBK1eaP2@tfvAg>m+bV1$nRvJ3zccwAbyLEE3z)bk
zn?o%D7UYQu`k*q*f~DU~EFqevElU=kCJ8i;=*)n#Fx(T?cah>&YfH@AI?qEjx5x&w
z`bdC7Hc&9+tyAOpi*jB*AwX~
z!wy4~aE>#)DP{}1&eg!-hBYAn6jx?h?jx!vW~zkBOQtS`V3+|vEO~)_7KBhaQ$xFG!yIQX{(YH!sZ~kDOAqe31m!fJH;xGk~VK1qGQm=wBGez-h|@T}i(o
z8ygkeJCwsP$AE5MtXNe2)9!=x?=kmdj5g~ZCZhZm5lX0Bf_Z@HawUVwOU`YQG!A7W-v(O#b7HT+Q^^-V#$mbQE!n+X1dGRaWF2%~?2B(IkAooYUGjEsd2;kM-Muw<8~G)3qq}Tjr@%QFb=(`_roJ{O!!YX!3zduN3mq4c{%z?g=*4
zXslC!lxax!qONLkP@_+c
Date: Wed, 10 Mar 2021 10:39:38 +0100
Subject: [PATCH 092/348] Fix random test failure

---
 tests/rpc/test_rpc_telegram.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 924490821..27babb1b7 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -1130,7 +1130,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
     assert "Close Date" in msg_mock.call_args_list[0][0][0]
     assert "
" in msg_mock.call_args_list[0][0][0]
-    assert bool(re.search("just now[ ]*XRP\\/BTC \\(#3\\)  1.00% \\(None\\)",
+    assert bool(re.search(r"just now[ ]*XRP\/BTC \(#3\)  1.00% \(",
                 msg_mock.call_args_list[0][0][0]))
 
 

From ef9977fc1e8d0cafb5de9fd57c93103e2bb9eb37 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 10 Mar 2021 10:43:44 +0100
Subject: [PATCH 093/348] Make stake_amount + stake_currency mandatory for
 backtesting

---
 docs/configuration.md                        |  6 ++----
 freqtrade/configuration/config_validation.py |  2 ++
 freqtrade/constants.py                       | 10 ++++++++++
 3 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/docs/configuration.md b/docs/configuration.md
index 823b4bc20..20b26ec13 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -40,8 +40,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
 |  Parameter | Description |
 |------------|-------------|
 | `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. -| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Positive float or `"unlimited"`. +| `stake_currency` | **Required.** Crypto-currency used for trading.
**Datatype:** String +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) @@ -142,8 +142,6 @@ Values set in the configuration file always overwrite values set in the strategy * `process_only_new_candles` * `order_types` * `order_time_in_force` -* `stake_currency` -* `stake_amount` * `unfilledtimeout` * `disable_dataframe_checks` * `protections` diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 187b2e3c7..df9f16f3e 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -47,6 +47,8 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: conf_schema = deepcopy(constants.CONF_SCHEMA) if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE): conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED + elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT): + conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED else: conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED try: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 06eaad4f9..f25f6653d 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -378,6 +378,16 @@ SCHEMA_TRADE_REQUIRED = [ 'dataformat_trades', ] +SCHEMA_BACKTEST_REQUIRED = [ + 'exchange', + 'max_open_trades', + 'stake_currency', + 'stake_amount', + 'dry_run_wallet', + 'dataformat_ohlcv', + 'dataformat_trades', +] + SCHEMA_MINIMAL_REQUIRED = [ 'exchange', 'dry_run', From 425cd7adba609b810ad0a0486925c4585af42b76 Mon Sep 17 00:00:00 2001 From: Jackson Law <178053+jlaw@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:16:03 -0800 Subject: [PATCH 094/348] Create event loop manually if uvloop is available asyncio.get_event_loop() does not call new_event_loop() if current_thread() != main_thread() --- freqtrade/rpc/api_server/uvicorn_threaded.py | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index 1554a8e52..2f72cb74c 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -8,12 +8,33 @@ import uvicorn class UvicornServer(uvicorn.Server): """ Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 + + Removed install_signal_handlers() override based on changes from this commit: + https://github.com/encode/uvicorn/commit/ce2ef45a9109df8eae038c0ec323eb63d644cbc6 + + Cannot rely on asyncio.get_event_loop() to create new event loop because of this check: + https://github.com/python/cpython/blob/4d7f11e05731f67fd2c07ec2972c6cb9861d52be/Lib/asyncio/events.py#L638 + + Fix by overriding run() and forcing creation of new event loop if uvloop is available """ - def install_signal_handlers(self): + + def run(self, sockets=None): + import asyncio + """ - In the parent implementation, this starts the thread, therefore we must patch it away here. + Parent implementation calls self.config.setup_event_loop(), + but we need to create uvloop event loop manually """ - pass + try: + import uvloop # noqa + except ImportError: # pragma: no cover + from uvicorn.loops.asyncio import asyncio_setup + asyncio_setup() + else: + asyncio.set_event_loop(uvloop.new_event_loop()) + + loop = asyncio.get_event_loop() + loop.run_until_complete(self.serve(sockets=sockets)) @contextlib.contextmanager def run_in_thread(self): From d1acc8092cef3bd2a2035642640a7cab92429dcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 10:16:32 +0100 Subject: [PATCH 095/348] Improve backtest performance --- freqtrade/optimize/backtesting.py | 4 +++- freqtrade/persistence/models.py | 32 ++++++++++++++++++++++++++++--- freqtrade/wallets.py | 11 ++++++++--- tests/conftest.py | 2 +- tests/test_persistence.py | 2 +- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 575ad486a..f2cf0d0dc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -377,7 +377,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) - LocalTrade.trades.append(trade) + LocalTrade.add_bt_trade(trade) for trade in open_trades[pair]: # also check the buying candle for sell conditions. @@ -387,6 +387,8 @@ class Backtesting: # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) + + LocalTrade.close_bt_trade(trade) trades.append(trade_entry) if enable_protections: self.protections.stop_per_pair(pair, row[DATE_IDX]) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ab714ae8b..41a5a99ff 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -208,6 +208,8 @@ class LocalTrade(): use_db: bool = False # Trades container for backtesting trades: List['LocalTrade'] = [] + trades_open: List['LocalTrade'] = [] + total_profit: float = 0 id: int = 0 @@ -350,6 +352,8 @@ class LocalTrade(): Resets all trades. Only active for backtesting mode. """ LocalTrade.trades = [] + LocalTrade.trades_open = [] + LocalTrade.total_profit = 0 def adjust_min_max_rates(self, current_price: float) -> None: """ @@ -599,7 +603,17 @@ class LocalTrade(): """ # Offline mode - without database - sel_trades = [trade for trade in LocalTrade.trades] + if is_open is not None: + if is_open: + sel_trades = LocalTrade.trades_open + else: + sel_trades = LocalTrade.trades + + else: + # Not used during backtesting, but might be used by a strategy + sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open + if trade.is_open == is_open] + if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] if open_date: @@ -607,10 +621,22 @@ class LocalTrade(): if close_date: sel_trades = [trade for trade in sel_trades if trade.close_date and trade.close_date > close_date] - if is_open is not None: - sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades + @staticmethod + def close_bt_trade(trade): + LocalTrade.trades_open.remove(trade) + LocalTrade.trades.append(trade) + LocalTrade.total_profit += trade.close_profit_abs + + @staticmethod + def add_bt_trade(trade): + if trade.is_open: + LocalTrade.trades_open.append(trade) + else: + LocalTrade.trades.append(trade) + @staticmethod def get_open_trades() -> List[Any]: """ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 553f7c61d..575fe1b67 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -10,7 +10,7 @@ import arrow from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange -from freqtrade.persistence import Trade +from freqtrade.persistence import LocalTrade, Trade from freqtrade.state import RunMode @@ -66,9 +66,14 @@ class Wallets: """ # Recreate _wallets to reset closed trade balances _wallets = {} - closed_trades = Trade.get_trades_proxy(is_open=False) open_trades = Trade.get_trades_proxy(is_open=True) - tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + # If not backtesting... + # TODO: potentially remove the ._log workaround to determine backtest mode. + if self._log: + closed_trades = Trade.get_trades_proxy(is_open=False) + tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + else: + tot_profit = LocalTrade.total_profit tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades diff --git a/tests/conftest.py b/tests/conftest.py index 498d65b0a..801ffad2f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -191,7 +191,7 @@ def create_mock_trades(fee, use_db: bool = True): if use_db: Trade.session.add(trade) else: - LocalTrade.trades.append(trade) + LocalTrade.add_bt_trade(trade) # Simulate dry_run entries trade = mock_trade_1(fee) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1a8124b00..8c89c98ed 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1196,6 +1196,6 @@ def test_Trade_object_idem(): # Fails if only a column is added without corresponding parent field for item in localtrade: if (not item.startswith('__') - and item not in ('trades', ) + and item not in ('trades', 'trades_open', 'total_profit') and type(getattr(LocalTrade, item)) not in (property, FunctionType)): assert item in trade From 5e872273d1217e43e15ce91317f9ef1f68f36277 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys <19151258+rokups@users.noreply.github.com> Date: Sat, 13 Mar 2021 09:45:16 +0200 Subject: [PATCH 096/348] Provide access to strategy instance from hyperopt class. --- freqtrade/optimize/hyperopt.py | 1 + freqtrade/optimize/hyperopt_interface.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 16a39d7d6..6b5bc171b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -73,6 +73,7 @@ class Hyperopt: self.backtesting = Backtesting(self.config) self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.custom_hyperopt.__class__.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index b8c44ed59..561fb8e11 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -34,6 +35,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str + strategy: IStrategy def __init__(self, config: dict) -> None: self.config = config From 0320c8dc92d4e4acb4f61310f202ba6db2be840a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 15:46:20 +0100 Subject: [PATCH 097/348] Improve tests for trades_proxy --- freqtrade/persistence/models.py | 3 +-- tests/conftest_trades.py | 4 ++++ tests/test_persistence.py | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 41a5a99ff..ed8a2259b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -611,8 +611,7 @@ class LocalTrade(): else: # Not used during backtesting, but might be used by a strategy - sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open - if trade.is_open == is_open] + sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open] if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 1d775830d..8e4be9165 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -29,6 +29,7 @@ def mock_trade_1(fee): fee_open=fee.return_value, fee_close=fee.return_value, is_open=True, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, exchange='bittrex', open_order_id='dry_run_buy_12345', @@ -183,6 +184,7 @@ def mock_trade_4(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), is_open=True, open_rate=0.123, exchange='bittrex', @@ -234,6 +236,7 @@ def mock_trade_5(fee): amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), is_open=True, open_rate=0.123, exchange='bittrex', @@ -284,6 +287,7 @@ def mock_trade_6(fee): stake_amount=0.001, amount=2.0, amount_requested=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), fee_open=fee.return_value, fee_close=fee.return_value, is_open=True, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8c89c98ed..ab900cbb8 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from datetime import datetime, timedelta, timezone from types import FunctionType from unittest.mock import MagicMock @@ -1044,6 +1045,7 @@ def test_fee_updated(fee): def test_total_open_trades_stakes(fee, use_db): Trade.use_db = use_db + Trade.reset_trades() res = Trade.total_open_trades_stakes() assert res == 0 create_mock_trades(fee, use_db) @@ -1053,6 +1055,26 @@ def test_total_open_trades_stakes(fee, use_db): Trade.use_db = True +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('use_db', [True, False]) +def test_get_trades_proxy(fee, use_db): + Trade.use_db = use_db + Trade.reset_trades() + create_mock_trades(fee, use_db) + trades = Trade.get_trades_proxy() + assert len(trades) == 6 + + assert isinstance(trades[0], Trade) + + assert len(Trade.get_trades_proxy(is_open=True)) == 4 + assert len(Trade.get_trades_proxy(is_open=False)) == 2 + opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15) + + assert len(Trade.get_trades_proxy(open_date=opendate)) == 3 + + Trade.use_db = True + + @pytest.mark.usefixtures("init_persistence") def test_get_overall_performance(fee): From 6389e86ed68c42425c456fe3c8373436be78f71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 15:36:34 +0100 Subject: [PATCH 098/348] Add test for uvloop fix --- tests/conftest.py | 12 +++++++++-- tests/exchange/test_exchange.py | 10 +-------- tests/rpc/test_rpc_apiserver.py | 38 ++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 498d65b0a..75632e4c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from copy import deepcopy from datetime import datetime from functools import reduce from pathlib import Path -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock import arrow import numpy as np @@ -64,6 +64,14 @@ def get_args(args): return Arguments(args).get_parsed_arg() +# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines +def get_mock_coro(return_value): + async def mock_coro(*args, **kwargs): + return return_value + + return Mock(wraps=mock_coro) + + def patched_configuration_load_config_file(mocker, config) -> None: mocker.patch( 'freqtrade.configuration.configuration.load_config_file', @@ -1736,7 +1744,7 @@ def import_fails() -> None: realimport = builtins.__import__ def mockedimport(name, *args, **kwargs): - if name in ["filelock", 'systemd.journal']: + if name in ["filelock", 'systemd.journal', 'uvloop']: raise ImportError(f"No module named '{name}'") return realimport(name, *args, **kwargs) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 75db2de26..3fd566fa3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -18,21 +18,13 @@ from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_patched_exchange, log_has, log_has_re +from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] -# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines -def get_mock_coro(return_value): - async def mock_coro(*args, **kwargs): - return return_value - - return Mock(wraps=mock_coro) - - def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8590e0d21..01492b4f2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,8 +23,8 @@ from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.state import RunMode, State -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, - patch_get_signal) +from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, + log_has_re, patch_get_signal) BASE_URI = "/api/v1" @@ -230,7 +230,7 @@ def test_api__init__(default_conf, mocker): assert apiserver._config == default_conf -def test_api_UvicornServer(default_conf, mocker): +def test_api_UvicornServer(mocker): thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread') s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert thread_mock.call_count == 0 @@ -248,6 +248,38 @@ def test_api_UvicornServer(default_conf, mocker): assert s.should_exit is True +def test_api_UvicornServer_run(mocker): + serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve', + get_mock_coro(None)) + s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) + assert serve_mock.call_count == 0 + + s.install_signal_handlers() + # Original implementation starts a thread - make sure that's not the case + assert serve_mock.call_count == 0 + + # Fake started to avoid sleeping forever + s.started = True + s.run() + assert serve_mock.call_count == 1 + + +def test_api_UvicornServer_run_no_uvloop(mocker, import_fails): + serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve', + get_mock_coro(None)) + s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) + assert serve_mock.call_count == 0 + + s.install_signal_handlers() + # Original implementation starts a thread - make sure that's not the case + assert serve_mock.call_count == 0 + + # Fake started to avoid sleeping forever + s.started = True + s.run() + assert serve_mock.call_count == 1 + + def test_api_run(default_conf, mocker, caplog): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", From eb4f05eb23efa08be363a374054439b0a3ccd07e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Mar 2021 16:30:47 +0100 Subject: [PATCH 099/348] Add documentation for hyperopt.strategy availability --- docs/advanced-hyperopt.md | 47 ++++++++++++++++++++++++++++++++++++++- docs/hyperopt.md | 2 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index d2237b3e8..bdaafb936 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -6,7 +6,7 @@ class. ## Derived hyperopt classes -Custom hyperop classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). +Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace: @@ -32,6 +32,51 @@ or $ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... ``` +## Sharing methods with your strategy + +Hyperopt classes provide access to the Strategy via the `strategy` class attribute. +This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. + +``` python +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + + buy_params = { + 'rsi-value': 30, + 'adx-value': 35, + } + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return self.buy_strategy_generator(self.buy_params, dataframe, metadata) + + @staticmethod + def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & + dataframe['adx'] > params['adx-value']) & + dataframe['volume'] > 0 + ) + , 'buy'] = 1 + return dataframe + +class MyAwesomeHyperOpt(IHyperOpt): + ... + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + # Call strategy's buy strategy generator + return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) + + return populate_buy_trend +``` + ## Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index d6959b457..69bc57d1a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -283,7 +283,7 @@ So let's write the buy strategy using these values: """ Define the buy strategy parameters to be used by Hyperopt. """ - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # GUARDS AND TRENDS if 'adx-enabled' in params and params['adx-enabled']: From 618bae23a64930c3b4cb84fb1298e8a0e7b4c40d Mon Sep 17 00:00:00 2001 From: Jackson Law <178053+jlaw@users.noreply.github.com> Date: Sat, 13 Mar 2021 11:14:36 -0800 Subject: [PATCH 100/348] fix: Use now() to match timezone of download data --- tests/commands/test_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d5e76eeb6..27875ac94 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -706,7 +706,7 @@ def test_download_data_timerange(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 # 20days ago - days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp + days_ago = arrow.get(arrow.now().shift(days=-20).date()).int_timestamp assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago dl_mock.reset_mock() From b57c150654235086164f1199584ce990aa38f854 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 09:48:40 +0100 Subject: [PATCH 101/348] Final balance should include forcesold pairs --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f2cf0d0dc..0b884dae5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -301,6 +301,7 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) + LocalTrade.close_bt_trade(trade) # Deepcopy object to have wallets update correctly trade1 = deepcopy(trade) trade1.is_open = True From 7a63f8cc319d9543b9041caa5be9497e6fb01d4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 13:25:08 +0100 Subject: [PATCH 102/348] Fix hdf5 support on raspberry --- Dockerfile.armhf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index f938ec457..eecd9fdc0 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir \ +RUN apt-get install -y libhdf5-serial-dev \ + && apt-get clean \ + && pip install -e . --no-cache-dir \ && freqtrade install-ui ENTRYPOINT ["freqtrade"] From e92441643157652467eed377a43d057d32c18708 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Sun, 14 Mar 2021 22:02:53 +0900 Subject: [PATCH 103/348] correct math used in examples and clarify some terminology regarding custom stoploss functions --- docs/strategy-advanced.md | 64 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 56061365e..cda988acd 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -71,12 +71,13 @@ See `custom_stoploss` examples below on how to access the saved dataframe column ## Custom stoploss -A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. -Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. +The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. -The method must return a stoploss value (float / number) with a relative ratio below the current price. -E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). +The method must return a stoploss value (float / number) as a percentage of the current price. +E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. + +The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: @@ -177,16 +178,33 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` + +#### Calculating stoploss relative to open price + +Stoploss values returned from `custom_stoploss` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +This can be calculated as: + +``` python +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + +``` + +For example, say our open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, 0.21)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + #### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. -Please note that the stoploss can only increase, values lower than the current stoploss are ignored. ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -197,28 +215,32 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: - return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss + return 1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - desired_stoploss = current_profit / 2 - # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025) + desired_stop_from_open = max(min(current_profit / 2, 0.05), 0.025) + + return stoploss_from_open(desired_stop_from_open, current_profit) ``` -#### Absolute stoploss +#### Stepped stoploss -The below example sets absolute profit levels based on the current profit. +Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. -* Once profit is > 25% - stoploss will be 15%. -* Once profit is > 20% - stoploss will be set to 7%. +* Once profit is > 20% - set stoploss to 7% above open price. +* Once profit is > 25% - set stoploss to 15% above open price. +* Once profit is > 40% - set stoploss to 25% above open price. + ``` python from datetime import datetime from freqtrade.persistence import Trade +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + return 1-((1+open_relative_stop)/(1+current_profit)) + class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -228,13 +250,15 @@ class AwesomeStrategy(IStrategy): def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return (-0.25 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) - if current_profit > 0.20: - return (-0.07 + current_profit) + return stoploss_from_open(0.25, current_profit) + elif current_profit > 0.25: + return stoploss_from_open(0.15, current_profit) + elif current_profit > 0.20: + return stoploss_from_open(0.07, current_profit) + + # return maximum stoploss value, keeping current stoploss price unchanged return 1 ``` #### Custom stoploss using an indicator from dataframe example From 0b35c0571fd1d0ac5beadd645c2c87958331e61a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 19:37:30 +0100 Subject: [PATCH 104/348] Allow custom fee to be used during dry-run closes #3696 --- docs/configuration.md | 1 + freqtrade/commands/arguments.py | 2 +- freqtrade/exchange/exchange.py | 2 ++ tests/exchange/test_exchange.py | 8 ++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 20b26ec13..2e8edca2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -58,6 +58,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean +| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 88cec7b3e..9468a7f7d 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -14,7 +14,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", ] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 138be3537..fdb34eb41 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1232,6 +1232,8 @@ class Exchange: def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, price: float = 1, taker_or_maker: str = 'maker') -> float: try: + if self._config['dry_run'] and self._config.get('fee', None) is not None: + return self._config['fee'] # validate that markets are loaded before trying to get fee if self._api.markets is None or len(self._api.markets) == 0: self._api.load_markets() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3fd566fa3..8a8c95a62 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2268,12 +2268,20 @@ def test_get_fee(default_conf, mocker, exchange_name): 'cost': 0.05 }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._config.pop('fee', None) assert exchange.get_fee('ETH/BTC') == 0.025 + assert api_mock.calculate_fee.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_fee', 'calculate_fee', symbol="ETH/BTC") + api_mock.calculate_fee.reset_mock() + exchange._config['fee'] = 0.001 + + assert exchange.get_fee('ETH/BTC') == 0.001 + assert api_mock.calculate_fee.call_count == 0 + def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') From b191663a7ec512c68607a40408ae9e07f0d5275d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Mar 2021 19:49:46 +0100 Subject: [PATCH 105/348] Adapt hyperopt templates to be better aligned closes #3027 --- freqtrade/templates/base_hyperopt.py.j2 | 23 ++--- freqtrade/templates/sample_hyperopt.py | 94 ++++++------------ .../templates/sample_hyperopt_advanced.py | 97 ++++++------------- 3 files changed, 72 insertions(+), 142 deletions(-) diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 index ec787cbb6..f6ca1477a 100644 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -39,6 +39,15 @@ class {{ hyperopt }}(IHyperOpt): https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. """ + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + {{ buy_space | indent(12) }} + ] + @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -79,12 +88,12 @@ class {{ hyperopt }}(IHyperOpt): return populate_buy_trend @staticmethod - def indicator_space() -> List[Dimension]: + def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching buy strategy parameters. + Define your Hyperopt space for searching sell strategy parameters. """ return [ - {{ buy_space | indent(12) }} + {{ sell_space | indent(12) }} ] @staticmethod @@ -126,11 +135,3 @@ class {{ hyperopt }}(IHyperOpt): return populate_sell_trend - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - {{ sell_space | indent(12) }} - ] diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 10743e911..ed1af7718 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -45,6 +45,23 @@ class SampleHyperOpt(IHyperOpt): https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. """ + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + Integer(10, 25, name='mfi-value'), + Integer(15, 45, name='fastd-value'), + Integer(20, 50, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -92,20 +109,22 @@ class SampleHyperOpt(IHyperOpt): return populate_buy_trend @staticmethod - def indicator_space() -> List[Dimension]: + def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching buy strategy parameters. + Define your Hyperopt space for searching sell strategy parameters. """ return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + Integer(75, 100, name='sell-mfi-value'), + Integer(50, 100, name='sell-fastd-value'), + Integer(50, 100, name='sell-adx-value'), + Integer(60, 100, name='sell-rsi-value'), + Categorical([True, False], name='sell-mfi-enabled'), + Categorical([True, False], name='sell-fastd-enabled'), + Categorical([True, False], name='sell-adx-enabled'), + Categorical([True, False], name='sell-rsi-enabled'), + Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') ] @staticmethod @@ -153,56 +172,3 @@ class SampleHyperOpt(IHyperOpt): return dataframe return populate_sell_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') - ] - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include buy space. - """ - dataframe.loc[ - ( - (dataframe['close'] < dataframe['bb_lowerband']) & - (dataframe['mfi'] < 16) & - (dataframe['adx'] > 25) & - (dataframe['rsi'] < 21) - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include sell space. - """ - dataframe.loc[ - ( - (qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] > 54) - ), - 'sell'] = 1 - - return dataframe diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 52e397466..7736570f7 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -60,6 +60,23 @@ class AdvancedSampleHyperOpt(IHyperOpt): dataframe['sar'] = ta.SAR(dataframe) return dataframe + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + Integer(10, 25, name='mfi-value'), + Integer(15, 45, name='fastd-value'), + Integer(20, 50, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -106,20 +123,22 @@ class AdvancedSampleHyperOpt(IHyperOpt): return populate_buy_trend @staticmethod - def indicator_space() -> List[Dimension]: + def sell_indicator_space() -> List[Dimension]: """ - Define your Hyperopt space for searching strategy parameters + Define your Hyperopt space for searching sell strategy parameters. """ return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + Integer(75, 100, name='sell-mfi-value'), + Integer(50, 100, name='sell-fastd-value'), + Integer(50, 100, name='sell-adx-value'), + Integer(60, 100, name='sell-rsi-value'), + Categorical([True, False], name='sell-mfi-enabled'), + Categorical([True, False], name='sell-fastd-enabled'), + Categorical([True, False], name='sell-adx-enabled'), + Categorical([True, False], name='sell-rsi-enabled'), + Categorical(['sell-bb_upper', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], name='sell-trigger') ] @staticmethod @@ -168,25 +187,6 @@ class AdvancedSampleHyperOpt(IHyperOpt): return populate_sell_trend - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') - ] - @staticmethod def generate_roi_table(params: Dict) -> Dict[int, float]: """ @@ -267,40 +267,3 @@ class AdvancedSampleHyperOpt(IHyperOpt): Categorical([True, False], name='trailing_only_offset_is_reached'), ] - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. - Can be a copy of the corresponding method from the strategy, - or will be loaded from the strategy. - Must align to populate_indicators used (either from this File, or from the strategy) - Only used when --spaces does not include buy - """ - dataframe.loc[ - ( - (dataframe['close'] < dataframe['bb_lowerband']) & - (dataframe['mfi'] < 16) & - (dataframe['adx'] > 25) & - (dataframe['rsi'] < 21) - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. - Can be a copy of the corresponding method from the strategy, - or will be loaded from the strategy. - Must align to populate_indicators used (either from this File, or from the strategy) - Only used when --spaces does not include sell - """ - dataframe.loc[ - ( - (qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] > 54) - ), - 'sell'] = 1 - return dataframe From 09872d8e42cd6a2ccccb228bad037c0aa90def42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:27:18 +0000 Subject: [PATCH 106/348] Bump flake8 from 3.8.4 to 3.9.0 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.4 to 3.9.0. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.4...3.9.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 68b1dd53f..4f0ea7706 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.8.4 +flake8==3.9.0 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 From 22c34faca388ee38887813710825e2283c7651ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:28:02 +0000 Subject: [PATCH 107/348] Bump mkdocs-material from 7.0.5 to 7.0.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.5 to 7.0.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.5...7.0.6) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 22c09ff69..0068dd5d2 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.5 +mkdocs-material==7.0.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 1173d8971a57f4005a48b4067110fd985d90e726 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:28:06 +0000 Subject: [PATCH 108/348] Bump prompt-toolkit from 3.0.16 to 3.0.17 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.16 to 3.0.17. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f62c8ff52..1cbfadb17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,4 +39,4 @@ aiofiles==0.6.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.16 +prompt-toolkit==3.0.17 From a209b0a392415c3b2b8382f097f4982c10497fe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 05:28:18 +0000 Subject: [PATCH 109/348] Bump python-telegram-bot from 13.3 to 13.4.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.3 to 13.4.1. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.3...v13.4.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f62c8ff52..77d22dbf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.42.66 cryptography==3.4.6 aiohttp==3.7.4.post0 SQLAlchemy==1.3.23 -python-telegram-bot==13.3 +python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 From b6c29bebb07bbab854f1086c75df7b8ad16d6ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Mar 2021 06:56:48 +0100 Subject: [PATCH 110/348] Update slack action --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f294347a..61ecaa522 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: mypy freqtrade scripts - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -194,7 +194,7 @@ jobs: mypy freqtrade scripts - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -257,7 +257,7 @@ jobs: mypy freqtrade scripts - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -288,7 +288,7 @@ jobs: mkdocs build - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -311,7 +311,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} @@ -398,7 +398,7 @@ jobs: - name: Slack Notification - uses: homoluctus/slatify@v1.8.0 + uses: lazy-actions/slatify@v3.0.0 if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} From 8f26935259f20fadf33d3b928fa5f6a1dde10a43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Mar 2021 09:26:02 +0000 Subject: [PATCH 111/348] Bump ccxt from 1.42.66 to 1.43.27 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.42.66 to 1.43.27. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.42.66...1.43.27) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 77d22dbf3..090cdc588 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.3 -ccxt==1.42.66 +ccxt==1.43.27 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 79d4585dadf14e7e3749cabb498ef8cfe47f99eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Mar 2021 19:24:03 +0100 Subject: [PATCH 112/348] Add check to ensure close_profit_abs is filled on closed trades Technically, this should not be possible, but #4554 shows it is. closes #4554 --- freqtrade/wallets.py | 3 ++- tests/test_persistence.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 575fe1b67..f4432e932 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -71,7 +71,8 @@ class Wallets: # TODO: potentially remove the ._log workaround to determine backtest mode. if self._log: closed_trades = Trade.get_trades_proxy(is_open=False) - tot_profit = sum([trade.close_profit_abs for trade in closed_trades]) + tot_profit = sum( + [trade.close_profit_abs for trade in closed_trades if trade.close_profit_abs]) else: tot_profit = LocalTrade.total_profit tot_in_trades = sum([trade.stake_amount for trade in open_trades]) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index ab900cbb8..1820250a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1066,8 +1066,14 @@ def test_get_trades_proxy(fee, use_db): assert isinstance(trades[0], Trade) - assert len(Trade.get_trades_proxy(is_open=True)) == 4 - assert len(Trade.get_trades_proxy(is_open=False)) == 2 + trades = Trade.get_trades_proxy(is_open=True) + assert len(trades) == 4 + assert trades[0].is_open + trades = Trade.get_trades_proxy(is_open=False) + + assert len(trades) == 2 + assert not trades[0].is_open + opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15) assert len(Trade.get_trades_proxy(open_date=opendate)) == 3 From aee2591490b442a731c8efb039658e8230958cd5 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Wed, 17 Mar 2021 17:58:23 +0900 Subject: [PATCH 113/348] add stoploss_from_open() as a strategy_helper --- docs/strategy-customization.md | 36 +++++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/strategy_helper.py | 18 ++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a1708a481..bf086bc0a 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -587,6 +587,42 @@ All columns of the informative dataframe will be available on the returning data *** +### *stoploss_from_open()* + +Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price. + +??? Example "Returning a stoploss relative to the open price from the custom stoploss function" + + Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). + + If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + + + ``` python + + from freqtrade.strategy import IStrategy, stoploss_from_open + from datetime import datetime + from freqtrade.persistence import Trade + + class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # once the profit has risin above 10%, keep the stoploss at 7% above the open price + if current_profit > 0.10: + return stoploss_from_open(0.07, current_profit) + + return 1 + + ``` + + + ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 662156ae9..3de90666e 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,3 +3,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair +from freqtrade.strategy.strategy_helper import stoploss_from_open diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index d7b1327d9..f40fa285d 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -56,3 +56,21 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, dataframe = dataframe.ffill() return dataframe + + +def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: + """ + + Given the current profit, and a desired stop loss value relative to the open price, + return a stop loss value that is relative to the current price, and which can be + returned from `custom_stoploss`. + + :param open_relative_stop: Desired stop loss value relative to open price + :param current_profit: The current profit percentage + :return: Stop loss value relative to current price + """ + + if current_profit == -1: + return 1 + + return 1-((1+open_relative_stop)/(1+current_profit)) From ce1ed76269370862f6f717c4d9ffe98b049d7caa Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Wed, 17 Mar 2021 22:44:10 +0900 Subject: [PATCH 114/348] complete stoploss_from_open and associated test --- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/strategy_helper.py | 15 ++++++++-- tests/strategy/test_strategy_helpers.py | 39 ++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 3de90666e..85148b6ea 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,5 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair -from freqtrade.strategy.strategy_helper import stoploss_from_open +from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index f40fa285d..22b6f0be5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -65,12 +65,21 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa return a stop loss value that is relative to the current price, and which can be returned from `custom_stoploss`. - :param open_relative_stop: Desired stop loss value relative to open price + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + + :param open_relative_stop: Desired stop loss percentage relative to open price :param current_profit: The current profit percentage - :return: Stop loss value relative to current price + :return: Positive stop loss value relative to current price """ + # formula is undefined for current_profit -1, return maximum value if current_profit == -1: return 1 - return 1-((1+open_relative_stop)/(1+current_profit)) + stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 252288e2e..3b84fc254 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,8 +1,10 @@ +from math import isclose + import numpy as np import pandas as pd import pytest -from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes +from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes def generate_test_data(timeframe: str, size: int): @@ -95,3 +97,38 @@ def test_merge_informative_pair_lower(): with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"): merge_informative_pair(data, informative, '1h', '15m', ffill=True) + + +def test_stoploss_from_open(): + open_price_ranges = [ + [0.01, 1.00, 30], + [1, 100, 30], + [100, 10000, 30], + ] + current_profit_range = [-0.99, 2, 30] + desired_stop_range = [-0.50, 0.50, 30] + + for open_range in open_price_ranges: + for open_price in np.linspace(*open_range): + for desired_stop in np.linspace(*desired_stop_range): + + # -1 is not a valid current_profit, should return 1 + assert stoploss_from_open(desired_stop, -1) == 1 + + for current_profit in np.linspace(*current_profit_range): + current_price = open_price * (1 + current_profit) + expected_stop_price = open_price * (1 + desired_stop) + + stoploss = stoploss_from_open(desired_stop, current_profit) + + assert stoploss >= 0 + assert stoploss <= 1 + + stop_price = current_price * (1 - stoploss) + + # there is no correct answer if the expected stop price is above + # the current price + if expected_stop_price > current_price: + assert stoploss == 0 + else: + assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) From 6597055a24f5592057f62ec330e72dda310a606c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 19:36:11 +0100 Subject: [PATCH 115/348] Ensure ccxt tests run without dry-run closes #4566 --- tests/exchange/test_ccxt_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 03cb30d62..870e6cabd 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -44,6 +44,7 @@ EXCHANGES = { def exchange_conf(): config = get_default_conf((Path(__file__).parent / "testdata").resolve()) config['exchange']['pair_whitelist'] = [] + config['dry_run'] = False return config From b05de6d4687299f5012aec4eeebc0ed8dbebb173 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 19:36:35 +0100 Subject: [PATCH 116/348] Move advanced exchange config to exchange page --- docs/configuration.md | 20 -------------------- docs/exchanges.md | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 2e8edca2e..ca1e03b0a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -417,26 +417,6 @@ This configuration enables binance, as well as rate limiting to avoid bans from Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. -#### Advanced Freqtrade Exchange configuration - -Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours. - -Available options are listed in the exchange-class as `_ft_has_default`. - -For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): - -```json -"exchange": { - "name": "kraken", - "_ft_has_params": { - "order_time_in_force": ["gtc", "fok"], - "ohlcv_candle_limit": 200 - } -``` - -!!! Warning - Please make sure to fully understand the impacts of these settings before modifying them. - ### What values can be used for fiat_display_currency? The `fiat_display_currency` configuration parameter sets the base currency to use for the diff --git a/docs/exchanges.md b/docs/exchanges.md index 2e5bdfadd..4c7e44b06 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -118,3 +118,23 @@ Whether your exchange returns incomplete candles or not can be checked using [th Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. + +### Advanced Freqtrade Exchange configuration + +Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behavior. + +Available options are listed in the exchange-class as `_ft_has_default`. + +For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): + +```json +"exchange": { + "name": "kraken", + "_ft_has_params": { + "order_time_in_force": ["gtc", "fok"], + "ohlcv_candle_limit": 200 + } +``` + +!!! Warning + Please make sure to fully understand the impacts of these settings before modifying them. From 76ca3c219f9d7b7ccf8a2723267529acbf54f658 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Mar 2021 20:43:51 +0100 Subject: [PATCH 117/348] extract result-printing from hyperopt class --- freqtrade/commands/hyperopt_commands.py | 21 +- freqtrade/optimize/hyperopt.py | 290 +---------------------- freqtrade/optimize/hyperopt_tools.py | 294 ++++++++++++++++++++++++ tests/commands/test_commands.py | 4 +- tests/optimize/test_hyperopt.py | 19 +- 5 files changed, 324 insertions(+), 304 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_tools.py diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index fd8f737f0..268e3eeef 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -17,7 +17,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: """ List hyperopt epochs previously evaluated """ - from freqtrade.optimize.hyperopt import Hyperopt + from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -47,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: config.get('hyperoptexportfilename')) # Previous evaluations - epochs = Hyperopt.load_previous_results(results_file) + epochs = HyperoptTools.load_previous_results(results_file) total_epochs = len(epochs) epochs = hyperopt_filter_epochs(epochs, filteroptions) @@ -57,18 +57,19 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if not export_csv: try: - print(Hyperopt.get_result_table(config, epochs, total_epochs, - not filteroptions['only_best'], print_colorized, 0)) + print(HyperoptTools.get_result_table(config, epochs, total_epochs, + not filteroptions['only_best'], + print_colorized, 0)) except KeyboardInterrupt: print('User interrupted..') if epochs and not no_details: sorted_epochs = sorted(epochs, key=itemgetter('loss')) results = sorted_epochs[0] - Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) + HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header) if epochs and export_csv: - Hyperopt.export_csv_file( + HyperoptTools.export_csv_file( config, epochs, total_epochs, not filteroptions['only_best'], export_csv ) @@ -77,7 +78,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: """ Show details of a hyperopt epoch previously evaluated """ - from freqtrade.optimize.hyperopt import Hyperopt + from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -105,7 +106,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: } # Previous evaluations - epochs = Hyperopt.load_previous_results(results_file) + epochs = HyperoptTools.load_previous_results(results_file) total_epochs = len(epochs) epochs = hyperopt_filter_epochs(epochs, filteroptions) @@ -124,8 +125,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: if epochs: val = epochs[n] - Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, - header_str="Epoch details") + HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header, + header_str="Epoch details") def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6b5bc171b..03f34a511 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,36 +4,31 @@ This module contains the hyperopt logic """ -import io import locale import logging import random import warnings -from collections import OrderedDict from datetime import datetime from math import ceil from operator import itemgetter from pathlib import Path -from pprint import pformat from typing import Any, Dict, List, Optional import progressbar -import rapidjson -import tabulate from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects -from pandas import DataFrame, isna, json_normalize +from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException -from freqtrade.misc import file_dump_json, plural, round_dict +from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 +from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy @@ -169,15 +164,6 @@ class Hyperopt: file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, log=False) - @staticmethod - def _read_results(results_file: Path) -> List: - """ - Read hyperopt results from file - """ - logger.info("Reading epochs from '%s'", results_file) - data = load(results_file) - return data - def _get_params_details(self, params: Dict) -> Dict: """ Return the params for each space @@ -200,102 +186,16 @@ class Hyperopt: return result - @staticmethod - def print_epoch_details(results, total_epochs: int, print_json: bool, - no_header: bool = False, header_str: str = None) -> None: - """ - Display details of the hyperopt result - """ - params = results.get('params_details', {}) - - # Default header string - if header_str is None: - header_str = "Best result" - - if not no_header: - explanation_str = Hyperopt._format_explanation_string(results, total_epochs) - print(f"\n{header_str}:\n\n{explanation_str}\n") - - if print_json: - result_dict: Dict = {} - for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: - Hyperopt._params_update_for_json(result_dict, params, s) - print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) - - else: - Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:") - Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:") - Hyperopt._params_pretty_print(params, 'roi', "ROI table:") - Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:") - Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:") - - @staticmethod - def _params_update_for_json(result_dict, params, space: str) -> None: - if space in params: - space_params = Hyperopt._space_params(params, space) - if space in ['buy', 'sell']: - result_dict.setdefault('params', {}).update(space_params) - elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - - # Convert keys in min_roi dict to strings because - # rapidjson cannot dump dicts with integer keys... - # OrderedDict is used to keep the numeric order of the items - # in the dict. - result_dict['minimal_roi'] = OrderedDict( - (str(k), v) for k, v in space_params.items() - ) - else: # 'stoploss', 'trailing' - result_dict.update(space_params) - - @staticmethod - def _params_pretty_print(params, space: str, header: str) -> None: - if space in params: - space_params = Hyperopt._space_params(params, space, 5) - params_result = f"\n# {header}\n" - if space == 'stoploss': - params_result += f"stoploss = {space_params.get('stoploss')}" - elif space == 'roi': - # TODO: get rid of OrderedDict when support for python 3.6 will be - # dropped (dicts keep the order as the language feature) - minimal_roi_result = rapidjson.dumps( - OrderedDict( - (str(k), v) for k, v in space_params.items() - ), - default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - params_result += f"minimal_roi = {minimal_roi_result}" - elif space == 'trailing': - - for k, v in space_params.items(): - params_result += f'{k} = {v}\n' - - else: - params_result += f"{space}_params = {pformat(space_params, indent=4)}" - params_result = params_result.replace("}", "\n}").replace("{", "{\n ") - - params_result = params_result.replace("\n", "\n ") - print(params_result) - - @staticmethod - def _space_params(params, space: str, r: int = None) -> Dict: - d = params[space] - # Round floats to `r` digits after the decimal point if requested - return round_dict(d, r) if r else d - - @staticmethod - def is_best_loss(results, current_best_loss: float) -> bool: - return results['loss'] < current_best_loss - def print_results(self, results) -> None: """ Log results if it is better than any previous evaluation + TODO: this should be moved to HyperoptTools too """ is_best = results['is_best'] if self.print_all or is_best: print( - self.get_result_table( + HyperoptTools.get_result_table( self.config, results, self.total_epochs, self.print_all, self.print_colorized, self.hyperopt_table_header @@ -303,166 +203,6 @@ class Hyperopt: ) self.hyperopt_table_header = 2 - @staticmethod - def _format_explanation_string(results, total_epochs) -> str: - return (("*" if results['is_initial_point'] else " ") + - f"{results['current_epoch']:5d}/{total_epochs}: " + - f"{results['results_explanation']} " + - f"Objective: {results['loss']:.5f}") - - @staticmethod - def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, - print_colorized: bool, remove_header: int) -> str: - """ - Log result table - """ - if not results: - return '' - - tabulate.PRESERVE_WHITESPACE = True - - trials = json_normalize(results, max_level=1) - trials['Best'] = '' - if 'results_metrics.winsdrawslosses' not in trials.columns: - # Ensure compatibility with older versions of hyperopt results - trials['results_metrics.winsdrawslosses'] = 'N/A' - - trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.winsdrawslosses', - 'results_metrics.avg_profit', 'results_metrics.total_profit', - 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', - 'Total profit', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] - trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '* ' - trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' - trials.loc[trials['Total profit'] > 0, 'is_profit'] = True - trials['Trades'] = trials['Trades'].astype(str) - - trials['Epoch'] = trials['Epoch'].apply( - lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) - ) - trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') - ) - trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') - ) - - trials['Profit'] = trials.apply( - lambda x: '{:,.8f} {} {}'.format( - x['Total profit'], config['stake_currency'], - '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') - ).rjust(25+len(config['stake_currency'])) - if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), - axis=1 - ) - trials = trials.drop(columns=['Total profit']) - - if print_colorized: - for i in range(len(trials)): - if trials.loc[i]['is_profit']: - for j in range(len(trials.loc[i])-3): - trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, - str(trials.loc[i][j]), Fore.RESET) - if trials.loc[i]['is_best'] and highlight_best: - for j in range(len(trials.loc[i])-3): - trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, - str(trials.loc[i][j]), Style.RESET_ALL) - - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) - if remove_header > 0: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='orgtbl', - headers='keys', stralign="right" - ) - - table = table.split("\n", remove_header)[remove_header] - elif remove_header < 0: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='psql', - headers='keys', stralign="right" - ) - table = "\n".join(table.split("\n")[0:remove_header]) - else: - table = tabulate.tabulate( - trials.to_dict(orient='list'), tablefmt='psql', - headers='keys', stralign="right" - ) - return table - - @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: - """ - Log result to csv-file - """ - if not results: - return - - # Verification for overwrite - if Path(csv_file).is_file(): - logger.error(f"CSV file already exists: {csv_file}") - return - - try: - io.open(csv_file, 'w+').close() - except IOError: - logger.error(f"Failed to create CSV file: {csv_file}") - return - - trials = json_normalize(results, max_level=1) - trials['Best'] = '' - trials['Stake currency'] = config['stake_currency'] - - base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', - 'results_metrics.avg_profit', 'results_metrics.median_profit', - 'results_metrics.total_profit', - 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', - 'loss', 'is_initial_point', 'is_best'] - param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] - trials = trials[base_metrics + param_metrics] - - base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', - 'Stake currency', 'Profit', 'Avg duration', 'Objective', - 'is_initial_point', 'is_best'] - param_columns = list(results[0]['params_dict'].keys()) - trials.columns = base_columns + param_columns - - trials['is_profit'] = False - trials.loc[trials['is_initial_point'], 'Best'] = '*' - trials.loc[trials['is_best'], 'Best'] = 'Best' - trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' - trials.loc[trials['Total profit'] > 0, 'is_profit'] = True - trials['Epoch'] = trials['Epoch'].astype(str) - trials['Trades'] = trials['Trades'].astype(str) - - trials['Total profit'] = trials['Total profit'].apply( - lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" - ) - trials['Profit'] = trials['Profit'].apply( - lambda x: '{:,.2f}'.format(x) if not isna(x) else "" - ) - trials['Avg profit'] = trials['Avg profit'].apply( - lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" - ) - trials['Avg duration'] = trials['Avg duration'].apply( - lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" - ) - trials['Objective'] = trials['Objective'].apply( - lambda x: '{:,.5f}'.format(x) if x != 100000 else "" - ) - - trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) - trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') - logger.info(f"CSV file created: {csv_file}") - def has_space(self, space: str) -> bool: """ Tell if the space value is contained in the configuration @@ -626,22 +366,6 @@ class Hyperopt: return parallel(delayed( wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) - @staticmethod - def load_previous_results(results_file: Path) -> List: - """ - Load data for epochs from the file if we have one - """ - epochs: List = [] - if results_file.is_file() and results_file.stat().st_size > 0: - epochs = Hyperopt._read_results(results_file) - # Detection of some old format, without 'is_best' field saved - if epochs[0].get('is_best') is None: - raise OperationalException( - "The file with Hyperopt results is incompatible with this version " - "of Freqtrade and cannot be loaded.") - logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") - return epochs - def _set_random_state(self, random_state: Optional[int]) -> int: return random_state or random.randint(1, 2**16 - 1) @@ -734,7 +458,7 @@ class Hyperopt: logger.debug(f"Optimizer epoch evaluated: {val}") - is_best = self.is_best_loss(val, self.current_best_loss) + is_best = HyperoptTools.is_best_loss(val, self.current_best_loss) # This value is assigned here and not in the optimization method # to keep proper order in the list of results. That's because # evaluations can take different time. Here they are aligned in the @@ -762,7 +486,7 @@ class Hyperopt: if self.epochs: sorted_epochs = sorted(self.epochs, key=itemgetter('loss')) best_epoch = sorted_epochs[0] - self.print_epoch_details(best_epoch, self.total_epochs, self.print_json) + HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json) else: # This is printed when Ctrl+C is pressed quickly, before first epochs have # a chance to be evaluated. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py new file mode 100644 index 000000000..d4c347f80 --- /dev/null +++ b/freqtrade/optimize/hyperopt_tools.py @@ -0,0 +1,294 @@ + +import io +import logging +from collections import OrderedDict +from pathlib import Path +from pprint import pformat +from typing import Dict, List + +import rapidjson +import tabulate +from colorama import Fore, Style +from joblib import load +from pandas import isna, json_normalize + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_dict + + +logger = logging.getLogger(__name__) + + +class HyperoptTools(): + + @staticmethod + def _read_results(results_file: Path) -> List: + """ + Read hyperopt results from file + """ + logger.info("Reading epochs from '%s'", results_file) + data = load(results_file) + return data + + @staticmethod + def load_previous_results(results_file: Path) -> List: + """ + Load data for epochs from the file if we have one + """ + epochs: List = [] + if results_file.is_file() and results_file.stat().st_size > 0: + epochs = HyperoptTools._read_results(results_file) + # Detection of some old format, without 'is_best' field saved + if epochs[0].get('is_best') is None: + raise OperationalException( + "The file with HyperoptTools results is incompatible with this version " + "of Freqtrade and cannot be loaded.") + logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") + return epochs + + @staticmethod + def print_epoch_details(results, total_epochs: int, print_json: bool, + no_header: bool = False, header_str: str = None) -> None: + """ + Display details of the hyperopt result + """ + params = results.get('params_details', {}) + + # Default header string + if header_str is None: + header_str = "Best result" + + if not no_header: + explanation_str = HyperoptTools._format_explanation_string(results, total_epochs) + print(f"\n{header_str}:\n\n{explanation_str}\n") + + if print_json: + result_dict: Dict = {} + for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: + HyperoptTools._params_update_for_json(result_dict, params, s) + print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + + else: + HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:") + HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:") + HyperoptTools._params_pretty_print(params, 'roi', "ROI table:") + HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:") + HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:") + + @staticmethod + def _params_update_for_json(result_dict, params, space: str) -> None: + if space in params: + space_params = HyperoptTools._space_params(params, space) + if space in ['buy', 'sell']: + result_dict.setdefault('params', {}).update(space_params) + elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + + # Convert keys in min_roi dict to strings because + # rapidjson cannot dump dicts with integer keys... + # OrderedDict is used to keep the numeric order of the items + # in the dict. + result_dict['minimal_roi'] = OrderedDict( + (str(k), v) for k, v in space_params.items() + ) + else: # 'stoploss', 'trailing' + result_dict.update(space_params) + + @staticmethod + def _params_pretty_print(params, space: str, header: str) -> None: + if space in params: + space_params = HyperoptTools._space_params(params, space, 5) + params_result = f"\n# {header}\n" + if space == 'stoploss': + params_result += f"stoploss = {space_params.get('stoploss')}" + elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + minimal_roi_result = rapidjson.dumps( + OrderedDict( + (str(k), v) for k, v in space_params.items() + ), + default=str, indent=4, number_mode=rapidjson.NM_NATIVE) + params_result += f"minimal_roi = {minimal_roi_result}" + elif space == 'trailing': + + for k, v in space_params.items(): + params_result += f'{k} = {v}\n' + + else: + params_result += f"{space}_params = {pformat(space_params, indent=4)}" + params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + + params_result = params_result.replace("\n", "\n ") + print(params_result) + + @staticmethod + def _space_params(params, space: str, r: int = None) -> Dict: + d = params[space] + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + + @staticmethod + def is_best_loss(results, current_best_loss: float) -> bool: + return results['loss'] < current_best_loss + + @staticmethod + def _format_explanation_string(results, total_epochs) -> str: + return (("*" if results['is_initial_point'] else " ") + + f"{results['current_epoch']:5d}/{total_epochs}: " + + f"{results['results_explanation']} " + + f"Objective: {results['loss']:.5f}") + + @staticmethod + def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, + print_colorized: bool, remove_header: int) -> str: + """ + Log result table + """ + if not results: + return '' + + tabulate.PRESERVE_WHITESPACE = True + + trials = json_normalize(results, max_level=1) + trials['Best'] = '' + if 'results_metrics.winsdrawslosses' not in trials.columns: + # Ensure compatibility with older versions of hyperopt results + trials['results_metrics.winsdrawslosses'] = 'N/A' + + trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.winsdrawslosses', + 'results_metrics.avg_profit', 'results_metrics.total_profit', + 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best']] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] + trials['is_profit'] = False + trials.loc[trials['is_initial_point'], 'Best'] = '* ' + trials.loc[trials['is_best'], 'Best'] = 'Best' + trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[trials['Total profit'] > 0, 'is_profit'] = True + trials['Trades'] = trials['Trades'].astype(str) + + trials['Epoch'] = trials['Epoch'].apply( + lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs) + ) + trials['Avg profit'] = trials['Avg profit'].apply( + lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + ) + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ') + ) + trials['Objective'] = trials['Objective'].apply( + lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ') + ) + + trials['Profit'] = trials.apply( + lambda x: '{:,.8f} {} {}'.format( + x['Total profit'], config['stake_currency'], + '({:,.2f}%)'.format(x['Profit']).rjust(10, ' ') + ).rjust(25+len(config['stake_currency'])) + if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])), + axis=1 + ) + trials = trials.drop(columns=['Total profit']) + + if print_colorized: + for i in range(len(trials)): + if trials.loc[i]['is_profit']: + for j in range(len(trials.loc[i])-3): + trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, + str(trials.loc[i][j]), Fore.RESET) + if trials.loc[i]['is_best'] and highlight_best: + for j in range(len(trials.loc[i])-3): + trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, + str(trials.loc[i][j]), Style.RESET_ALL) + + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + if remove_header > 0: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='orgtbl', + headers='keys', stralign="right" + ) + + table = table.split("\n", remove_header)[remove_header] + elif remove_header < 0: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='psql', + headers='keys', stralign="right" + ) + table = "\n".join(table.split("\n")[0:remove_header]) + else: + table = tabulate.tabulate( + trials.to_dict(orient='list'), tablefmt='psql', + headers='keys', stralign="right" + ) + return table + + @staticmethod + def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, + csv_file: str) -> None: + """ + Log result to csv-file + """ + if not results: + return + + # Verification for overwrite + if Path(csv_file).is_file(): + logger.error(f"CSV file already exists: {csv_file}") + return + + try: + io.open(csv_file, 'w+').close() + except IOError: + logger.error(f"Failed to create CSV file: {csv_file}") + return + + trials = json_normalize(results, max_level=1) + trials['Best'] = '' + trials['Stake currency'] = config['stake_currency'] + + base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count', + 'results_metrics.avg_profit', 'results_metrics.median_profit', + 'results_metrics.total_profit', + 'Stake currency', 'results_metrics.profit', 'results_metrics.duration', + 'loss', 'is_initial_point', 'is_best'] + param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] + trials = trials[base_metrics + param_metrics] + + base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', + 'Stake currency', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] + param_columns = list(results[0]['params_dict'].keys()) + trials.columns = base_columns + param_columns + + trials['is_profit'] = False + trials.loc[trials['is_initial_point'], 'Best'] = '*' + trials.loc[trials['is_best'], 'Best'] = 'Best' + trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best' + trials.loc[trials['Total profit'] > 0, 'is_profit'] = True + trials['Epoch'] = trials['Epoch'].astype(str) + trials['Trades'] = trials['Trades'].astype(str) + + trials['Total profit'] = trials['Total profit'].apply( + lambda x: '{:,.8f}'.format(x) if x != 0.0 else "" + ) + trials['Profit'] = trials['Profit'].apply( + lambda x: '{:,.2f}'.format(x) if not isna(x) else "" + ) + trials['Avg profit'] = trials['Avg profit'].apply( + lambda x: '{:,.2f}%'.format(x) if not isna(x) else "" + ) + trials['Avg duration'] = trials['Avg duration'].apply( + lambda x: '{:,.1f} m'.format(x) if not isna(x) else "" + ) + trials['Objective'] = trials['Objective'].apply( + lambda x: '{:,.5f}'.format(x) if x != 100000 else "" + ) + + trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) + trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') + logger.info(f"CSV file created: {csv_file}") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 27875ac94..e21ef4dd1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -920,7 +920,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', MagicMock(return_value=hyperopt_results) ) @@ -1152,7 +1152,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): def test_hyperopt_show(mocker, capsys, hyperopt_results): mocker.patch( - 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', + 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', MagicMock(return_value=hyperopt_results) ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 9ebdad2b5..193d997db 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,6 +16,7 @@ from freqtrade.commands.optimize_commands import setup_optimize_configuration, s from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, @@ -336,9 +337,9 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = hyperopt._read_results(results_file) + hyperopt_epochs = HyperoptTools._read_results(results_file) assert log_has(f"Reading epochs from '{results_file}'", caplog) assert hyperopt_epochs == epochs mock_load.assert_called_once() @@ -346,7 +347,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) statmock = MagicMock() statmock.st_size = 5 @@ -354,16 +355,16 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: results_file = testdatadir / 'optimize' / 'ut_results.pickle' - hyperopt_epochs = hyperopt.load_previous_results(results_file) + hyperopt_epochs = HyperoptTools.load_previous_results(results_file) assert hyperopt_epochs == epochs mock_load.assert_called_once() del epochs[0]['is_best'] - mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs) with pytest.raises(OperationalException): - hyperopt.load_previous_results(results_file) + HyperoptTools.load_previous_results(results_file) def test_roi_table_generation(hyperopt) -> None: @@ -453,7 +454,7 @@ def test_format_results(hyperopt): 'is_initial_point': True, } - result = hyperopt._format_explanation_string(results, 1) + result = HyperoptTools._format_explanation_string(results, 1) assert result.find(' 66.67%') assert result.find('Total profit 1.00000000 BTC') assert result.find('2.0000Σ %') @@ -467,7 +468,7 @@ def test_format_results(hyperopt): df = pd.DataFrame.from_records(trades, columns=labels) results_metrics = hyperopt._calculate_results_metrics(df) results['total_profit'] = results_metrics['total_profit'] - result = hyperopt._format_explanation_string(results, 1) + result = HyperoptTools._format_explanation_string(results, 1) assert result.find('Total profit 1.00000000 EUR') @@ -1076,7 +1077,7 @@ def test_print_epoch_details(capsys): 'is_best': True } - Hyperopt.print_epoch_details(test_result, 5, False, no_header=True) + HyperoptTools.print_epoch_details(test_result, 5, False, no_header=True) captured = capsys.readouterr() assert '# Trailing stop:' in captured.out # re.match(r"Pairs for .*", captured.out) From 983c0ef118e5ee6a63d91478e051477300616fcf Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Thu, 18 Mar 2021 09:47:03 +0900 Subject: [PATCH 118/348] update stoploss_from_open examples to use helper function --- docs/strategy-advanced.md | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cda988acd..ddf845fca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -181,17 +181,9 @@ class AwesomeStrategy(IStrategy): #### Calculating stoploss relative to open price -Stoploss values returned from `custom_stoploss` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. -This can be calculated as: - -``` python -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) - -``` - -For example, say our open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, 0.21)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. +The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. #### Trailing stoploss with positive offset @@ -201,9 +193,7 @@ Use the initial stoploss until the profit is above 4%, then use a trailing stopl ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade - -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) +from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -237,9 +227,7 @@ Instead of continuously trailing behind the current price, this example sets fix ``` python from datetime import datetime from freqtrade.persistence import Trade - -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: - return 1-((1+open_relative_stop)/(1+current_profit)) +from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -290,7 +278,7 @@ class AwesomeStrategy(IStrategy): # using current_time directly (like below) will only work in backtesting. # so check "runmode" to make sure that it's only used in backtesting/hyperopt if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): - relative_sl = self.custom_info[pair].loc[current_time]['atr] + relative_sl = self.custom_info[pair].loc[current_time]['atr'] # in live / dry-run, it'll be really the current time else: # but we can just use the last entry from an already analyzed dataframe instead From b6e9e74a8b3eb9c2efcf50350555783da6fe104a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Mar 2021 06:46:08 +0100 Subject: [PATCH 119/348] Add link between stoploss_from_open and custom_stop documentation --- docs/strategy-advanced.md | 4 +--- docs/strategy-customization.md | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index ddf845fca..4e8ecb67e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -178,10 +178,9 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` - #### Calculating stoploss relative to open price -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. @@ -189,7 +188,6 @@ The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_ Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. - ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index bf086bc0a..a00928a67 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -600,9 +600,9 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati ``` python - from freqtrade.strategy import IStrategy, stoploss_from_open from datetime import datetime from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open class AwesomeStrategy(IStrategy): @@ -621,6 +621,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati ``` + Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. ## Additional data (Wallets) From bf14796d4ceb372545ab9f71844c82c6f6a97ed2 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Thu, 18 Mar 2021 21:50:54 +0900 Subject: [PATCH 120/348] revert "Trailing stoploss with positive offset" example as stoploss_from_open() wasn't adding value --- docs/strategy-advanced.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4e8ecb67e..962b750b5 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -178,20 +178,15 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` -#### Calculating stoploss relative to open price - -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. - -The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. - #### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. +Please note that the stoploss can only increase, values lower than the current stoploss are ignored. + ``` python from datetime import datetime, timedelta from freqtrade.persistence import Trade -from freqtrade.strategy import stoploss_from_open class AwesomeStrategy(IStrategy): @@ -203,15 +198,21 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: if current_profit < 0.04: - return 1 # return a value bigger than the inital stoploss to keep using the inital stoploss + return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit - # Use a minimum of 2.5% and a maximum of 5% - desired_stop_from_open = max(min(current_profit / 2, 0.05), 0.025) + desired_stoploss = current_profit / 2 - return stoploss_from_open(desired_stop_from_open, current_profit) + # Use a minimum of 2.5% and a maximum of 5% + return max(min(desired_stoploss, 0.05), 0.025 ``` +#### Calculating stoploss relative to open price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. From dd4d1d82d46341f7ee99b18b5f3eb7237051834e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Mar 2021 14:19:33 +0100 Subject: [PATCH 121/348] Update docs/strategy-advanced.md --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 962b750b5..801bc4731 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -204,7 +204,7 @@ class AwesomeStrategy(IStrategy): desired_stoploss = current_profit / 2 # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025 + return max(min(desired_stoploss, 0.05), 0.025) ``` #### Calculating stoploss relative to open price From 4d52732d30b8f55ec683de156c3ee87203398a08 Mon Sep 17 00:00:00 2001 From: Patrick Brunier Date: Thu, 18 Mar 2021 22:38:54 +0100 Subject: [PATCH 122/348] Added a small snippet to give users a descent error message, when their start date is afer the stop date. Also updated the tests. --- freqtrade/configuration/timerange.py | 2 ++ tests/test_timerange.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 32bbd02a0..2075b38c6 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -103,5 +103,7 @@ class TimeRange: stop = int(stops) // 1000 else: stop = int(stops) + if start > stop > 0: + raise Exception('Start date is after stop date for timerange "%s"' % text) return TimeRange(stype[0], stype[1], start, stop) raise Exception('Incorrect syntax for timerange "%s"' % text) diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 5c35535f0..cd10e219f 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -30,6 +30,9 @@ def test_parse_timerange_incorrect(): with pytest.raises(Exception, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') + with pytest.raises(Exception, match=r'Start date is after stop date for timerange.*'): + TimeRange.parse_timerange('20100523-20100522') + def test_subtract_start(): x = TimeRange('date', 'date', 1274486400, 1438214400) From 0d5833ed9133ff629423143db9c810051f3abf45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Mar 2021 06:40:04 +0100 Subject: [PATCH 123/348] Use OperationalException for TimeRange errors --- freqtrade/configuration/timerange.py | 7 +++++-- tests/test_timerange.py | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 2075b38c6..6072e296c 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -7,6 +7,8 @@ from typing import Optional import arrow +from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) @@ -104,6 +106,7 @@ class TimeRange: else: stop = int(stops) if start > stop > 0: - raise Exception('Start date is after stop date for timerange "%s"' % text) + raise OperationalException( + f'Start date is after stop date for timerange "{text}"') return TimeRange(stype[0], stype[1], start, stop) - raise Exception('Incorrect syntax for timerange "%s"' % text) + raise OperationalException(f'Incorrect syntax for timerange "{text}"') diff --git a/tests/test_timerange.py b/tests/test_timerange.py index cd10e219f..dcdaad09d 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -3,6 +3,7 @@ import arrow import pytest from freqtrade.configuration import TimeRange +from freqtrade.exceptions import OperationalException def test_parse_timerange_incorrect(): @@ -27,10 +28,11 @@ def test_parse_timerange_incorrect(): timerange = TimeRange.parse_timerange('-1231006505000') assert TimeRange(None, 'date', 0, 1231006505) == timerange - with pytest.raises(Exception, match=r'Incorrect syntax.*'): + with pytest.raises(OperationalException, match=r'Incorrect syntax.*'): TimeRange.parse_timerange('-') - with pytest.raises(Exception, match=r'Start date is after stop date for timerange.*'): + with pytest.raises(OperationalException, + match=r'Start date is after stop date for timerange.*'): TimeRange.parse_timerange('20100523-20100522') From c1f79922700cd51020cab850905ad6d46599a4f4 Mon Sep 17 00:00:00 2001 From: Maycon Maia Vitali Date: Fri, 19 Mar 2021 10:39:45 -0300 Subject: [PATCH 124/348] Added slash to fix a broken formatting On the command table the pipe(|) broke the formatting. --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 833fae1fe..5ecdf8065 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,7 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. -| `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From fb90901bb3408555331d24e1f46a0144e3afbb55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Mar 2021 20:12:12 +0100 Subject: [PATCH 125/348] Fix telegram table for both rendered and github markdown --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5ecdf8065..377977892 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -146,7 +146,7 @@ official commands. You can ask at any moment for help with `/help`. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. -| `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/unlock ` | Remove the lock for this pair (or for this lock id). | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From 7ffe1fd36a230712a1f0eb19cc9005bf9564e231 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 07:21:22 +0100 Subject: [PATCH 126/348] Fix calculation error for min-trade-stake --- docs/configuration.md | 17 +++++++++++++++++ freqtrade/exchange/exchange.py | 8 ++++---- tests/exchange/test_exchange.py | 18 +++++++++++++----- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index ca1e03b0a..573cbfba2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -156,6 +156,23 @@ Values set in the configuration file always overwrite values set in the strategy There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below. +#### Minimum trade stake + +The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages. +Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$. + +The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`. +This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case. + +To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). + +With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). + +To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. + +!!! Warning + Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. + #### Available balance By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fdb34eb41..6b8261afc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -531,16 +531,16 @@ class Exchange: return None # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent', + amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += stoploss + amount_reserve_percent += abs(stoploss) # it should not be more than 50% - amount_reserve_percent = max(amount_reserve_percent, 0.5) + amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return max(min_stake_amounts) / amount_reserve_percent + return max(min_stake_amounts) * amount_reserve_percent def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8a8c95a62..942ffd4ab 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,6 +1,7 @@ import copy import logging from datetime import datetime, timedelta, timezone +from math import isclose from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch @@ -370,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert result == 2 / 0.9 + assert isclose(result, 2 * 1.1) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -382,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == 2 * 2 / 0.9 + assert isclose(result, 2 * 2 * 1.1) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -394,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == max(2, 2 * 2) / 0.9 + assert isclose(result, max(2, 2 * 2) * 1.1) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -406,7 +407,14 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert result == max(8, 2 * 2) / 0.9 + assert isclose(result, max(8, 2 * 2) * 1.1) + + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) + assert isclose(result, max(8, 2 * 2) * 1.45) + + # Really big stoploss + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) + assert isclose(result, max(8, 2 * 2) * 1.5) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -424,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) def test_set_sandbox(default_conf, mocker): From 69799532a67fc9732e851c71a500e223d4ffb589 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 08:13:10 +0100 Subject: [PATCH 127/348] Document usage of open_date_utc closes #4580 --- docs/strategy-advanced.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 801bc4731..7fa824a5b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -146,9 +146,9 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. - if current_time - timedelta(minutes=120) > trade.open_date: + if current_time - timedelta(minutes=120) > trade.open_date_utc: return -0.05 - elif current_time - timedelta(minutes=60) > trade.open_date: + elif current_time - timedelta(minutes=60) > trade.open_date_utc: return -0.10 return 1 ``` @@ -317,7 +317,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to The function must return either `True` (cancel order) or `False` (keep order alive). ``` python -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from freqtrade.persistence import Trade class AwesomeStrategy(IStrategy): @@ -331,21 +331,21 @@ class AwesomeStrategy(IStrategy): } def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): return True return False def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): return True return False ``` From 066dd72210889776c06726ee54bbd1ae798d1f20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 08:34:15 +0100 Subject: [PATCH 128/348] add orderbook structure documentation --- docs/strategy-customization.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a00928a67..256b28990 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -436,6 +436,26 @@ if self.dp: dataframe['best_ask'] = ob['asks'][0][0] ``` +The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will look as follows: + +``` js +{ + 'bids': [ + [ price, amount ], // [ float, float ] + [ price, amount ], + ... + ], + 'asks': [ + [ price, amount ], + [ price, amount ], + //... + ], + //... +} +``` + +Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position. + !!! Warning "Warning about backtesting" The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values. From fe7f3d9c37f70983fdb55459528378969c1f3d71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 11:48:39 +0100 Subject: [PATCH 129/348] Add price side validation for market orders --- freqtrade/configuration/config_validation.py | 14 +++++++++ tests/test_configuration.py | 32 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index df9f16f3e..b6029b6a5 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -74,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: # validating trailing stoploss _validate_trailing_stoploss(conf) + _validate_price_config(conf) _validate_edge(conf) _validate_whitelist(conf) _validate_protections(conf) @@ -95,6 +96,19 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") +def _validate_price_config(conf: Dict[str, Any]) -> None: + """ + When using market orders, price sides must be using the "other" side of the price + """ + if (conf['order_types'].get('buy') == 'market' + and conf['bid_strategy'].get('price_side') != 'ask'): + raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".') + + if (conf['order_types'].get('sell') == 'market' + and conf['ask_strategy'].get('price_side') != 'bid'): + raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".') + + def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6b3df392b..a0824e65c 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -790,6 +790,38 @@ def test_validate_max_open_trades(default_conf): validate_config_consistency(default_conf) +def test_validate_price_side(default_conf): + default_conf['order_types'] = { + "buy": "limit", + "sell": "limit", + "stoploss": "limit", + "stoploss_on_exchange": False, + } + # Default should pass + validate_config_consistency(default_conf) + + conf = deepcopy(default_conf) + conf['order_types']['buy'] = 'market' + with pytest.raises(OperationalException, + match='Market buy orders require bid_strategy.price_side = "ask".'): + validate_config_consistency(conf) + + conf = deepcopy(default_conf) + conf['order_types']['sell'] = 'market' + with pytest.raises(OperationalException, + match='Market sell orders require ask_strategy.price_side = "bid".'): + validate_config_consistency(conf) + + # Validate inversed case + conf = deepcopy(default_conf) + conf['order_types']['sell'] = 'market' + conf['order_types']['buy'] = 'market' + conf['ask_strategy']['price_side'] = 'bid' + conf['bid_strategy']['price_side'] = 'ask' + + validate_config_consistency(conf) + + def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' From 16a54b3616efa47bd394ddb660a00881d1fda989 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 13:08:02 +0100 Subject: [PATCH 130/348] Don't require non-mandatory arguments --- freqtrade/configuration/config_validation.py | 8 ++++---- tests/test_freqtradebot.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index b6029b6a5..c7e49f33d 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -100,12 +100,12 @@ def _validate_price_config(conf: Dict[str, Any]) -> None: """ When using market orders, price sides must be using the "other" side of the price """ - if (conf['order_types'].get('buy') == 'market' - and conf['bid_strategy'].get('price_side') != 'ask'): + if (conf.get('order_types', {}).get('buy') == 'market' + and conf.get('bid_strategy', {}).get('price_side') != 'ask'): raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".') - if (conf['order_types'].get('sell') == 'market' - and conf['ask_strategy'].get('price_side') != 'bid'): + if (conf.get('order_types', {}).get('sell') == 'market' + and conf.get('ask_strategy', {}).get('price_side') != 'bid'): raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7d2e19f6..5ef9960ab 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -94,6 +94,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: 'stoploss': 'limit', 'stoploss_on_exchange': True, } + conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) assert freqtrade.strategy.order_types['stoploss_on_exchange'] @@ -128,6 +129,7 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None: 'stoploss': 'limit', 'stoploss_on_exchange': True, } + conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) From 73876b61b491b62f1337b9500eb5e926e53247e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 13:33:49 +0100 Subject: [PATCH 131/348] Show potential errors when loading markets --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6b8261afc..5b6e2b20d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -311,8 +311,8 @@ class Exchange: self._markets = self._api.load_markets() self._load_async_markets() self._last_markets_refresh = arrow.utcnow().int_timestamp - except ccxt.BaseError as e: - logger.warning('Unable to initialize markets. Reason: %s', e) + except ccxt.BaseError: + logger.exception('Unable to initialize markets.') def reload_markets(self) -> None: """Reload markets both sync and async if refresh interval has passed """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 942ffd4ab..3439c7a09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -498,7 +498,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) - assert log_has('Unable to initialize markets. Reason: SomeError', caplog) + assert log_has('Unable to initialize markets.', caplog) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() From f4e71c1f145a47c3bd104da5f068b22df902642f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:02:13 +0100 Subject: [PATCH 132/348] get_buy_rate tests should be sensible --- tests/test_freqtradebot.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5ef9960ab..8f55a8fe6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -839,17 +839,17 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask ('ask', 4, 5, None, 1, 4), # last not available - uses ask ('ask', 4, 5, None, 0, 4), # last not available - uses ask - ('bid', 10, 20, 10, 0.0, 20), # Full bid side - ('bid', 10, 20, 10, 1.0, 10), # Full last side - ('bid', 10, 20, 10, 0.5, 15), # Between bid and last - ('bid', 10, 20, 10, 0.7, 13), # Between bid and last - ('bid', 10, 20, 10, 0.3, 17), # Between bid and last - ('bid', 4, 5, 10, 1.0, 5), # last bigger than bid - ('bid', 4, 5, 10, 0.5, 5), # last bigger than bid - ('bid', 10, 20, None, 0.5, 20), # last not available - uses bid - ('bid', 4, 5, None, 0.5, 5), # last not available - uses bid - ('bid', 4, 5, None, 1, 5), # last not available - uses bid - ('bid', 4, 5, None, 0, 5), # last not available - uses bid + ('bid', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 1.0, 10), # Full last side + ('bid', 21, 20, 10, 0.5, 15), # Between bid and last + ('bid', 21, 20, 10, 0.7, 13), # Between bid and last + ('bid', 21, 20, 10, 0.3, 17), # Between bid and last + ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid + ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid + ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid + ('bid', 6, 5, None, 1, 5), # last not available - uses bid + ('bid', 6, 5, None, 0, 5), # last not available - uses bid ]) def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: @@ -858,7 +858,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, default_conf['bid_strategy']['price_side'] = side freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': ask, 'last': last, 'bid': bid})) + return_value={'ask': ask, 'last': last, 'bid': bid}) assert freqtrade.get_buy_rate('ETH/BTC', True) == expected assert not log_has("Using cached buy rate for ETH/BTC.", caplog) From 43d7f9ac67a16b10edb63b5e48a0bc23bc7e5bb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:38:26 +0100 Subject: [PATCH 133/348] Add bid_last_balance parameter to interpolate sell prices closes #3270 --- docs/configuration.md | 3 ++- docs/includes/pricing.md | 4 ++++ freqtrade/constants.py | 6 ++++++ freqtrade/freqtradebot.py | 10 ++++++++-- tests/test_freqtradebot.py | 39 ++++++++++++++++++++++++-------------- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 573cbfba2..eb3351b8f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,12 +62,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). -| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). +| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) | `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).
*Defaults to `ask`.*
**Datatype:** String (either `ask` or `bid`). +| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled). | `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled).
**Datatype:** Boolean | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
**Datatype:** Positive Integer diff --git a/docs/includes/pricing.md b/docs/includes/pricing.md index d8a72cc58..bdf27eb20 100644 --- a/docs/includes/pricing.md +++ b/docs/includes/pricing.md @@ -103,6 +103,10 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. + +The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. + ### Market order pricing When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f25f6653d..3a2ed98e9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -165,6 +165,12 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'}, + 'bid_last_balance': { + 'type': 'number', + 'minimum': 0, + 'maximum': 1, + 'exclusiveMaximum': False, + }, 'use_order_book': {'type': 'boolean'}, 'order_book_min': {'type': 'integer', 'minimum': 1}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c60d65f72..73f4c91be 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -432,7 +432,7 @@ class FreqtradeBot(LoggingMixin): ticker = self.exchange.fetch_ticker(pair) ticker_rate = ticker[bid_strategy['price_side']] if ticker['last'] and ticker_rate > ticker['last']: - balance = self.config['bid_strategy']['ask_last_balance'] + balance = bid_strategy['ask_last_balance'] ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) used_rate = ticker_rate @@ -745,7 +745,13 @@ class FreqtradeBot(LoggingMixin): logger.warning("Sell Price at location from orderbook could not be determined.") raise PricingError from e else: - rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']] + ticker = self.exchange.fetch_ticker(pair) + ticker_rate = ticker[ask_strategy['price_side']] + if ticker['last'] and ticker_rate < ticker['last']: + balance = ask_strategy.get('bid_last_balance', 0.0) + ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last']) + rate = ticker_rate + if rate is None: raise PricingError(f"Sell-Rate for {pair} was empty.") self._sell_rate_cache[pair] = rate diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8f55a8fe6..c1a17164f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4112,22 +4112,33 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog) -@pytest.mark.parametrize('side,ask,bid,expected', [ - ('bid', 10.0, 11.0, 11.0), - ('bid', 10.0, 11.2, 11.2), - ('bid', 10.0, 11.0, 11.0), - ('bid', 9.8, 11.0, 11.0), - ('bid', 0.0001, 0.002, 0.002), - ('ask', 10.0, 11.0, 10.0), - ('ask', 10.11, 11.2, 10.11), - ('ask', 0.001, 0.002, 0.001), - ('ask', 0.006, 1.0, 0.006), +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ + ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side + ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side + ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat + ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid + ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid + ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid + ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side + ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side + ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat + ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask + ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask + ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask + ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), + ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), + ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), + ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), ]) -def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None: +def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, + last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) default_conf['ask_strategy']['price_side'] = side - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid}) + default_conf['ask_strategy']['bid_last_balance'] = last_ab + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': ask, 'bid': bid, 'last': last}) pair = "ETH/BTC" # Test regular mode @@ -4186,7 +4197,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): default_conf['ask_strategy']['price_side'] = 'ask' pair = "ETH/BTC" mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': None, 'bid': 0.12}) + return_value={'ask': None, 'bid': 0.12, 'last': None}) ft = get_patched_freqtradebot(mocker, default_conf) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): ft.get_sell_rate(pair, True) @@ -4195,7 +4206,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog): assert ft.get_sell_rate(pair, True) == 0.12 # Reverse sides mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.13, 'bid': None}) + return_value={'ask': 0.13, 'bid': None, 'last': None}) with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): ft.get_sell_rate(pair, True) From e315a6a0da4e9e0232f3463f12cff7561a64c32d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Mar 2021 14:58:51 +0100 Subject: [PATCH 134/348] assume "last" can miss from a ticker response closes #4573 --- freqtrade/plugins/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 6558f196f..a0579b196 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -64,7 +64,7 @@ class PriceFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if ticker['last'] is None or ticker['last'] == 0: + if ticker.get('last', None) is None or ticker.get('last') == 0: self.log_once(f"Removed {pair} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).", logger.info) From 4f5a1e94a7b94c75999404fc06be0d48c7132666 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:30 +0530 Subject: [PATCH 135/348] Add .deepsource.toml Signed-off-by: shubhendra --- .deepsource.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..7a00ca8d6 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,16 @@ +version = 1 + +test_patterns = ["tests/**/test_*.py"] + +exclude_patterns = [ + "docs/**", + "user_data/**", + "build/helpers/**" +] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" From 62d99a0b7406136f24d51e6d704fcc18969a3f5b Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:30 +0530 Subject: [PATCH 136/348] Remove unnecessary comprehension Signed-off-by: shubhendra --- freqtrade/resolvers/strategy_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index b1b66e3ae..19bd014f9 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -196,9 +196,9 @@ class StrategyResolver(IResolver): strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) - if any([x == 2 for x in [strategy._populate_fun_len, + if any(x == 2 for x in [strategy._populate_fun_len, strategy._buy_fun_len, - strategy._sell_fun_len]]): + strategy._sell_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy From 537ad059bc3b643eb2d022ddb798ed4d84aa8c78 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:31 +0530 Subject: [PATCH 137/348] Remove unnecessary use of comprehension Signed-off-by: shubhendra --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 78f45de0b..465f3d443 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -611,7 +611,7 @@ class LocalTrade(): else: # Not used during backtesting, but might be used by a strategy - sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open] + sel_trades = list(LocalTrade.trades + LocalTrade.trades_open) if pair: sel_trades = [trade for trade in sel_trades if trade.pair == pair] From 6d6ad035d6cf7fcedac7697fd69c8948f2e3fd76 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:32 +0530 Subject: [PATCH 138/348] Remove length check in favour of truthiness of the object Signed-off-by: shubhendra --- freqtrade/commands/list_commands.py | 2 +- freqtrade/exchange/exchange.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 9e6076dfb..5f53fc824 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -177,7 +177,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: # human-readable formats. print() - if len(pairs): + if pairs: if args.get('print_list', False): # print data as a list, with human-readable summary print(f"{summary_str}: {', '.join(pairs.keys())}.") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5b6e2b20d..9c868df2b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -958,7 +958,7 @@ class Exchange: while True: t = await self._async_fetch_trades(pair, params={self._trades_pagination_arg: from_id}) - if len(t): + if t: # Skip last id since its the key for the next call trades.extend(t[:-1]) if from_id == t[-1][1] or t[-1][0] > until: @@ -990,7 +990,7 @@ class Exchange: # DEFAULT_TRADES_COLUMNS: 1 -> id while True: t = await self._async_fetch_trades(pair, since=since) - if len(t): + if t: since = t[-1][0] trades.extend(t) # Reached the end of the defined-download period From 910e15b17431a8c7be26fde1346d9ef2fb9e45d1 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:33 +0530 Subject: [PATCH 139/348] Remove methods with unnecessary super delegation. Signed-off-by: shubhendra --- freqtrade/plugins/pairlist/PerformanceFilter.py | 5 ----- freqtrade/plugins/protections/cooldown_period.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 7d91bb77c..c1355f655 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -15,11 +15,6 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): - def __init__(self, exchange, pairlistmanager, - config: Dict[str, Any], pairlistconfig: Dict[str, Any], - pairlist_pos: int) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - @property def needstickers(self) -> bool: """ diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index f74f83885..197d74c2e 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -15,9 +15,6 @@ class CooldownPeriod(IProtection): has_global_stop: bool = False has_local_stop: bool = True - def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: - super().__init__(config, protection_config) - def _reason(self) -> str: """ LockReason to use From 45da3a70227cc95ee7610e0bb12dabd348b0086e Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:34 +0530 Subject: [PATCH 140/348] Refactor unnecessary `else` / `elif` when `if` block has a `continue` statement Signed-off-by: shubhendra --- freqtrade/configuration/directory_operations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 51310f013..1ce8d1461 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -72,6 +72,5 @@ def copy_sample_files(directory: Path, overwrite: bool = False) -> None: if not overwrite: logger.warning(f"File `{targetfile}` exists already, not deploying sample file.") continue - else: - logger.warning(f"File `{targetfile}` exists already, overwriting.") + logger.warning(f"File `{targetfile}` exists already, overwriting.") shutil.copy(str(sourcedir / source), str(targetfile)) From 4d8183491218783c92bc7230d98d00dffdf9b115 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:34 +0530 Subject: [PATCH 141/348] Merge `isinstance` calls Signed-off-by: shubhendra --- freqtrade/exchange/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index c66db860f..be0a1e483 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -140,7 +140,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT): logger.warning('retrying %s() still for %s times', f.__name__, count) count -= 1 kwargs.update({'count': count}) - if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + if isinstance(ex, (DDosProtection, RetryableOrderError)): # increasing backoff backoff_delay = calculate_backoff(count + 1, retries) logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") From ac7a1305cbb78d588bbb1d0849370828b17219fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:11 +0000 Subject: [PATCH 142/348] Bump ccxt from 1.43.27 to 1.43.89 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.43.27 to 1.43.89. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.43.27...1.43.89) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..7c8841e67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.1 pandas==1.2.3 -ccxt==1.43.27 +ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 9612ba34ed0c97e97d681c4726c13bc2f5d69048 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:17 +0000 Subject: [PATCH 143/348] Bump urllib3 from 1.26.3 to 1.26.4 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.3 to 1.26.4. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.3...1.26.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..0554f326b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 requests==2.25.1 -urllib3==1.26.3 +urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From 09c7ee9e923b936cd8e236b7253c08ec4d58a309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:28 +0000 Subject: [PATCH 144/348] Bump isort from 5.7.0 to 5.8.0 Bumps [isort](https://github.com/pycqa/isort) from 5.7.0 to 5.8.0. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.7.0...5.8.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4f0ea7706..02f7fbca8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 -isort==5.7.0 +isort==5.8.0 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 From ea3012e94d78c5119ba3b457a4799f2b714aafee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Mar 2021 05:25:35 +0000 Subject: [PATCH 145/348] Bump sqlalchemy from 1.3.23 to 1.4.2 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.23 to 1.4.2. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 08f5b9078..c714a8f1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.43.27 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 -SQLAlchemy==1.3.23 +SQLAlchemy==1.4.2 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From e39cff522d33e9a529045ec58610e87362a37328 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Mar 2021 17:30:16 +0100 Subject: [PATCH 146/348] Remove duplicate dict keys in test --- tests/rpc/test_rpc_apiserver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 01492b4f2..5a0a04943 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -810,14 +810,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'stoploss_entry_dist_ratio': -0.10448878, 'trade_id': 1, 'close_rate_requested': None, - 'current_rate': 1.099e-05, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, - 'open_date': ANY, 'is_open': True, 'max_rate': 1.099e-05, 'min_rate': 1.098e-05, From b7702a1e9f121764605144d63c2480a2b82b08cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Mar 2021 19:39:06 +0100 Subject: [PATCH 147/348] Improve tests to work with new sqlalchemy version --- tests/test_freqtradebot.py | 2 +- tests/test_persistence.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c1a17164f..486c31090 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2798,7 +2798,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) - sellmock = MagicMock() + sellmock = MagicMock(return_value={'id': '12345555'}) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1820250a5..6a388327c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging from datetime import datetime, timedelta, timezone +from pathlib import Path from types import FunctionType from unittest.mock import MagicMock @@ -21,14 +22,15 @@ def test_init_create_session(default_conf): assert 'scoped_session' in type(Trade.session).__name__ -def test_init_custom_db_url(default_conf, mocker): +def test_init_custom_db_url(default_conf, tmpdir): # Update path to a value other than default, but still in-memory - default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) - create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) + filename = f"{tmpdir}/freqtrade2_test.sqlite" + assert not Path(filename).is_file() + + default_conf.update({'db_url': f'sqlite:///{filename}'}) init_db(default_conf['db_url'], default_conf['dry_run']) - assert create_engine_mock.call_count == 1 - assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' + assert Path(filename).is_file() def test_init_invalid_db_url(default_conf): @@ -49,15 +51,16 @@ def test_init_prod_db(default_conf, mocker): assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' -def test_init_dryrun_db(default_conf, mocker): - default_conf.update({'dry_run': True}) - default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) - - create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) +def test_init_dryrun_db(default_conf, tmpdir): + filename = f"{tmpdir}/freqtrade2_prod.sqlite" + assert not Path(filename).is_file() + default_conf.update({ + 'dry_run': True, + 'db_url': f'sqlite:///{filename}' + }) init_db(default_conf['db_url'], default_conf['dry_run']) - assert create_engine_mock.call_count == 1 - assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' + assert Path(filename).is_file() @pytest.mark.usefixtures("init_persistence") From 4e8999ade3e8e9d39a5e78d175b736ed6d6b7dc1 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Mon, 22 Mar 2021 20:40:11 +0100 Subject: [PATCH 148/348] Changed the code for status table a bit so that it splits up the trades per 50 trades, to make sure it can be sent regardless of number of trades Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7ec67e5d0..2d753db70 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -330,8 +330,12 @@ class Telegram(RPCHandler): statlist, head = self._rpc._rpc_status_table( self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - message = tabulate(statlist, headers=head, tablefmt='simple') - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) + max_trades_per_msg = 50 + for i in range(0, max(int(len(statlist) / max_trades_per_msg), 1)): + message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], + headers=head, + tablefmt='simple') + self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 6856963aef37303b40fd29a0745e22ff312dc11c Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:09:41 +0200 Subject: [PATCH 149/348] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0b884dae5..1dcf6428b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,7 +30,6 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) # Indexes for backtest tuples @@ -252,8 +251,17 @@ class Backtesting: sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) - if sell.sell_flag: + time_in_force = self.strategy.order_time_in_force['sell'] + + # confirm_trade_exit + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=False)( + pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], + time_in_force=time_in_force, + sell_reason=sell.sell_type.value): + return None + + if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) @@ -271,6 +279,15 @@ class Backtesting: except DependencyException: return None min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) + + order_type = self.strategy.order_types['buy'] + time_in_force = self.strategy.order_time_in_force['sell'] + # confirm_trade_entry + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], + time_in_force=time_in_force): + return None + if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade trade = LocalTrade( From eb5d69dcd486305d3448caf3eab1152dbd9cf59f Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:12:08 +0200 Subject: [PATCH 150/348] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1dcf6428b..080e6b1a2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -253,7 +253,6 @@ class Backtesting: low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) time_in_force = self.strategy.order_time_in_force['sell'] - # confirm_trade_exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=False)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], From dc4ea604dd6d1fc8bd6af7a68e5fba36f3e7c517 Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:19:16 +0200 Subject: [PATCH 151/348] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 080e6b1a2..321b60ecd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -254,7 +254,7 @@ class Backtesting: time_in_force = self.strategy.order_time_in_force['sell'] # confirm_trade_exit - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=False)( + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], time_in_force=time_in_force, sell_reason=sell.sell_type.value): From f51f4b1817efd2451d4877478312f22d9acabefe Mon Sep 17 00:00:00 2001 From: rextea Date: Tue, 23 Mar 2021 10:35:46 +0200 Subject: [PATCH 152/348] Add confirm_trade_exit and confirm_trade_entry to backtesting --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 321b60ecd..d858acc1a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,8 @@ class Backtesting: time_in_force = self.strategy.order_time_in_force['sell'] # confirm_trade_exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=sell_row[LOW_IDX], + pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, + rate=sell_row[LOW_IDX], time_in_force=time_in_force, sell_reason=sell.sell_type.value): return None From d5301b4d6316f822d387ae6d2947cec5a49e316f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Mar 2021 10:53:09 +0100 Subject: [PATCH 153/348] RateLimit should be enabled by default --- config_full.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 8366774c4..717797933 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -113,7 +113,7 @@ "password": "", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { - "enableRateLimit": false, + "enableRateLimit": true, "rateLimit": 500, "aiohttp_trust_env": false }, From c928cd38dc2e5a67fcf70d7a76d350f5b7549560 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 16:45:42 +0100 Subject: [PATCH 154/348] Small bugfix to make sure it shows all the trades Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2d753db70..b83cbf1a2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,7 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 - for i in range(0, max(int(len(statlist) / max_trades_per_msg), 1)): + for i in range(0, max(int(len(statlist) / max_trades_per_msg) + 1, 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, tablefmt='simple') From 65a9763fa587aa42823956cd4e207f08905e7906 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 16:54:38 +0100 Subject: [PATCH 155/348] Fixed an issue when there were exactly 50 trades, it was sending an extra empty table Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b83cbf1a2..61a0188cb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,7 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 - for i in range(0, max(int(len(statlist) / max_trades_per_msg) + 1, 1)): + for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, tablefmt='simple') From 2fd510e6e4e8a2108f2a64c6ccad72f83fb047d7 Mon Sep 17 00:00:00 2001 From: Erwin Hoeckx Date: Tue, 23 Mar 2021 21:52:46 +0100 Subject: [PATCH 156/348] Added comment with an example calculation Signed-off-by: Erwin Hoeckx --- freqtrade/rpc/telegram.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 61a0188cb..2063a4f58 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,6 +331,11 @@ class Telegram(RPCHandler): self._config['stake_currency'], self._config.get('fiat_display_currency', '')) max_trades_per_msg = 50 + """ + Calculate the number of messages of 50 trades per message + 0.99 is used to make sure that there are no extra (empty) messages + As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message + """ for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)): message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg], headers=head, From d795febf9243e667de70d06a8c2ff30e951f847a Mon Sep 17 00:00:00 2001 From: rextea Date: Wed, 24 Mar 2021 18:26:03 +0200 Subject: [PATCH 157/348] Add info to documantation --- docs/bot-basics.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 13694c316..943af0362 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -53,6 +53,7 @@ This loop will be repeated again and again until the bot is stopped. * Calls `bot_loop_start()` once. * Calculate indicators (calls `populate_indicators()` once per pair). * Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair) +* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy) * Loops per candle simulating entry and exit points. * Generate backtest report output From ec15610bff2314e8f47620e410097106878ffd18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Mar 2021 19:21:07 +0100 Subject: [PATCH 158/348] Fix isort issue --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d858acc1a..7de5b171c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) # Indexes for backtest tuples From 0ca95aa0c2035b41c9835f2291020222f9ed690d Mon Sep 17 00:00:00 2001 From: rextea Date: Thu, 25 Mar 2021 10:25:25 +0200 Subject: [PATCH 159/348] Change rate to acctual close rate --- freqtrade/optimize/backtesting.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7de5b171c..00b2f278d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,7 +30,6 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) # Indexes for backtest tuples @@ -253,20 +252,21 @@ class Backtesting: sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) - time_in_force = self.strategy.order_time_in_force['sell'] - # confirm_trade_exit - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, - rate=sell_row[LOW_IDX], - time_in_force=time_in_force, - sell_reason=sell.sell_type.value): - return None - if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + + # Confirm trade exit: + time_in_force = self.strategy.order_time_in_force['sell'] + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, + rate=closerate, + time_in_force=time_in_force, + sell_reason=sell.sell_type.value): + return None + trade.close(closerate, show_msg=False) return trade @@ -283,7 +283,7 @@ class Backtesting: order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] - # confirm_trade_entry + # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], time_in_force=time_in_force): From 292ea8c1d03b0ecf100d9a1935f0718f4c316cae Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Mar 2021 09:34:33 +0100 Subject: [PATCH 160/348] Update backtesting.py --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 00b2f278d..765e2844a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) # Indexes for backtest tuples From 0a205f52b06ad9b12aa13d03794e9e3dfd780440 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 23 Mar 2021 10:02:32 +0200 Subject: [PATCH 161/348] Optional support for defining hyperopt parameters in a strategy file and reusing common hyperopt/strategy parts. --- freqtrade/optimize/hyperopt.py | 8 ++- freqtrade/optimize/hyperopt_auto.py | 83 +++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/hyper.py | 93 +++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_auto.py create mode 100644 freqtrade/strategy/hyper.py diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..8dd8f01ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -26,6 +26,7 @@ from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -67,8 +68,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.custom_hyperopt.__class__.strategy = self.backtesting.strategy + if self.config['hyperopt'] == 'HyperOptAuto': + self.custom_hyperopt = HyperOptAuto(self.config) + else: + self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py new file mode 100644 index 000000000..788bd5a79 --- /dev/null +++ b/freqtrade/optimize/hyperopt_auto.py @@ -0,0 +1,83 @@ +""" +HyperOptAuto class. +This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement +IHyperStrategy interface. +""" +from typing import Any, Callable, Dict, List +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +# noinspection PyUnresolvedReferences +class HyperOptAuto(IHyperOpt): + """ + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time + Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt + methods can be overridden as well. + """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('buy'): + attr.value = params[attr_name] + return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend + + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('sell'): + attr.value = params[attr_name] + return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend + + def _get_func(self, name) -> Callable: + """ + Return a function defined in Strategy.HyperOpt class, or one defined in super() class. + :param name: function name. + :return: a requested function. + """ + hyperopt_cls = getattr(self.strategy, 'HyperOpt') + default_func = getattr(super(), name) + if hyperopt_cls: + return getattr(hyperopt_cls, name, default_func) + else: + return default_func + + def _generate_indicator_space(self, category): + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + for attr_name, attr in self.strategy.enumerate_parameters(category): + yield attr.get_space(attr_name) + + def _get_indicator_space(self, category, fallback_method_name): + indicator_space = list(self._generate_indicator_space(category)) + if len(indicator_space) > 0: + return indicator_space + else: + return self._get_func(fallback_method_name)() + + def indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('buy', 'indicator_space') + + def sell_indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('sell', 'sell_indicator_space') + + def generate_roi_table(self, params: Dict) -> Dict[int, float]: + return self._get_func('generate_roi_table')(params) + + def roi_space(self) -> List[Dimension]: + return self._get_func('roi_space')() + + def stoploss_space(self) -> List[Dimension]: + return self._get_func('stoploss_space')() + + def generate_trailing_params(self, params: Dict) -> Dict: + return self._get_func('generate_trailing_params')(params) + + def trailing_space(self) -> List[Dimension]: + return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 85148b6ea..80a7c00de 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,4 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.hyper import IHyperStrategy, Parameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py new file mode 100644 index 000000000..41bb836d4 --- /dev/null +++ b/freqtrade/strategy/hyper.py @@ -0,0 +1,93 @@ +""" +IHyperStrategy interface, hyperoptable Parameter class. +This module defines a base class for auto-hyperoptable strategies. +""" +from abc import ABC +from typing import Union, List, Iterator, Tuple + +from skopt.space import Integer, Real, Categorical + +from freqtrade.strategy.interface import IStrategy + + +class Parameter(object): + """ + Defines a parameter that can be optimized by hyperopt. + """ + default: Union[int, float, str, bool] + space: List[Union[int, float, str, bool]] + category: str + + def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, + category: str = None, **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. + :param default: A default value. Required for ints and floats, optional for categorial parameters (first item + from the space will be used). Type of default value determines skopt space used for optimization. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). + """ + assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' + self.value = default + self.space = space + self.category = category + self._space_params = kwargs + if default is None: + assert len(space) > 0 + self.value = space[0] + + def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + """ + Create skopt optimization space. + :param name: A name of parameter field. + :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) + """ + if not self.space: + return None + if isinstance(self.value, int): + assert len(self.space) == 2 + return Integer(*self.space, name=name, **self._space_params) + if isinstance(self.value, float): + assert len(self.space) == 2 + return Real(*self.space, name=name, **self._space_params) + + assert len(self.space) > 0 + return Categorical(self.space, name=name, **self._space_params) + + +class IHyperStrategy(IStrategy, ABC): + """ + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + """ + + def __init__(self, config): + super().__init__(config) + self._load_params(getattr(self, 'buy_params', None)) + self._load_params(getattr(self, 'sell_params', None)) + + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + """ + Find all optimizeable parameters and return (name, attr) iterator. + :param category: + :return: + """ + assert category in ('buy', 'sell', None) + for attr_name in dir(self): + if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. + attr = getattr(self, attr_name) + if isinstance(attr, Parameter): + if category is None or category == attr.category or attr_name.startswith(category + '_'): + yield attr_name, attr + + def _load_params(self, params: dict) -> None: + """ + Set optimizeable parameter values. + :param params: Dictionary with new parameter values. + """ + if not params: + return + for attr_name, attr in self.enumerate_parameters(): + if attr_name in params: + attr.value = params[attr_name] From bb89e44e19825dc9b9c1bca393ced8a646800cf5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 10:32:34 +0200 Subject: [PATCH 162/348] [SQUASH] Address PR comments. * Split Parameter into IntParameter/FloatParameter/CategoricalParameter. * Rename IHyperStrategy to HyperStrategyMixin and use it as mixin. * --hyperopt parameter is now optional if strategy uses HyperStrategyMixin. * Use OperationalException() instead of asserts. --- freqtrade/commands/cli_options.py | 1 + freqtrade/optimize/hyperopt.py | 7 +- freqtrade/optimize/hyperopt_auto.py | 22 ++-- freqtrade/optimize/hyperopt_interface.py | 7 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 149 +++++++++++++++++------ 6 files changed, 138 insertions(+), 51 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..8e9f0c994 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -195,6 +195,7 @@ AVAILABLE_CLI_OPTIONS = { '--hyperopt', help='Specify hyperopt class name which will be used by the bot.', metavar='NAME', + required=False, ), "hyperopt_path": Arg( '--hyperopt-path', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8dd8f01ac..b5ee1da1a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,6 +23,7 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange +from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -68,7 +69,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - if self.config['hyperopt'] == 'HyperOptAuto': + if not self.config.get('hyperopt'): + if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' + 'parameter or add HyperStrategyMixin mixin to your ' + 'strategy class.') self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 788bd5a79..fdc4da14c 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -1,7 +1,7 @@ """ HyperOptAuto class. -This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement -IHyperStrategy interface. +This module implements a convenience auto-hyperopt class, which can be used together with strategies + that implement IHyperStrategy interface. """ from typing import Any, Callable, Dict, List from pandas import DataFrame @@ -13,26 +13,31 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noinspection PyUnresolvedReferences class HyperOptAuto(IHyperOpt): """ - This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time - Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt - methods can be overridden as well. + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. + Most of the time Strategy.HyperOpt class would only implement indicator_space and + sell_indicator_space methods, but other hyperopt methods can be overridden as well. """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend def _get_func(self, name) -> Callable: @@ -49,7 +54,8 @@ class HyperOptAuto(IHyperOpt): return default_func def _generate_indicator_space(self, category): - assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + if not getattr(self.strategy, 'HYPER_STRATEGY', False): + raise OperationalException('Strategy must inherit from IHyperStrategy.') for attr_name, attr in self.strategy.enumerate_parameters(category): yield attr.get_space(attr_name) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..2dd8500a6 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,15 +5,14 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.strategy import IStrategy - +from freqtrade.strategy import IStrategy, HyperStrategyMixin logger = logging.getLogger(__name__) @@ -35,7 +34,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str - strategy: IStrategy + strategy: Union[IStrategy, HyperStrategyMixin] def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 80a7c00de..e395be106 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,5 +2,6 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import IHyperStrategy, Parameter +from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ + CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 41bb836d4..8de986950 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,83 +2,158 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ -from abc import ABC -from typing import Union, List, Iterator, Tuple +from typing import Iterator, Tuple, Any, Optional, Sequence from skopt.space import Integer, Real, Categorical -from freqtrade.strategy.interface import IStrategy +from freqtrade.exceptions import OperationalException -class Parameter(object): +class BaseParameter(object): """ Defines a parameter that can be optimized by hyperopt. """ - default: Union[int, float, str, bool] - space: List[Union[int, float, str, bool]] - category: str + category: Optional[str] + default: Any + value: Any + space: Sequence[Any] - def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, - category: str = None, **kwargs): + def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, + **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. - :param default: A default value. Required for ints and floats, optional for categorial parameters (first item - from the space will be used). Type of default value determines skopt space used for optimization. :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ - assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' - self.value = default - self.space = space + if 'name' in kwargs: + raise OperationalException( + 'Name is determined by parameter field name and can not be specified manually.') self.category = category self._space_params = kwargs - if default is None: - assert len(space) > 0 - self.value = space[0] + self.value = default + self.space = space - def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + def __repr__(self): + return f'{self.__class__.__name__}({self.value})' + + +class IntParameter(BaseParameter): + default: int + value: int + space: Sequence[int] + + def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [min, max]. + :param default: A default value. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Integer. + """ + if len(space) != 2: + raise OperationalException('IntParameter space must be [min, max]') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Integer: """ Create skopt optimization space. :param name: A name of parameter field. - :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) """ - if not self.space: - return None - if isinstance(self.value, int): - assert len(self.space) == 2 - return Integer(*self.space, name=name, **self._space_params) - if isinstance(self.value, float): - assert len(self.space) == 2 - return Real(*self.space, name=name, **self._space_params) + return Integer(*self.space, name=name, **self._space_params) - assert len(self.space) > 0 + +class FloatParameter(BaseParameter): + default: float + value: float + space: Sequence[float] + + def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [min, max]. + :param default: A default value. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Real. + """ + if len(space) != 2: + raise OperationalException('IntParameter space must be [min, max]') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Real: + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ + return Real(*self.space, name=name, **self._space_params) + + +class CategoricalParameter(BaseParameter): + default: Any + value: Any + space: Sequence[Any] + + def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, + category: Optional[str] = None, + **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space, [a, b, ...]. + :param default: A default value. If not specified, first item from specified space will be + used. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.Categorical. + """ + if len(space) < 2: + raise OperationalException( + 'IntParameter space must be [a, b, ...] (at least two parameters)') + super().__init__(space=space, default=default, category=category, **kwargs) + + def get_space(self, name: str) -> Categorical: + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ return Categorical(self.space, name=name, **self._space_params) -class IHyperStrategy(IStrategy, ABC): +class HyperStrategyMixin(object): """ - A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell + strategy logic. """ - def __init__(self, config): - super().__init__(config) + # Hint that class can be used with HyperOptAuto. + HYPER_STRATEGY = 1 + + def __init__(self): + """ + Initialize hyperoptable strategy mixin. + :param config: + """ self._load_params(getattr(self, 'buy_params', None)) self._load_params(getattr(self, 'sell_params', None)) - def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ Find all optimizeable parameters and return (name, attr) iterator. :param category: :return: """ - assert category in ('buy', 'sell', None) + if category not in ('buy', 'sell', None): + raise OperationalException('Category must be one of: "buy", "sell", None.') for attr_name in dir(self): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) - if isinstance(attr, Parameter): - if category is None or category == attr.category or attr_name.startswith(category + '_'): + if issubclass(attr.__class__, BaseParameter): + if category is None or category == attr.category or \ + attr_name.startswith(category + '_'): yield attr_name, attr def _load_params(self, params: dict) -> None: From 2d13e5fd5052511d1077632c281099977a548573 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 11:17:17 +0200 Subject: [PATCH 163/348] [SQUASH] Oopsies. --- freqtrade/optimize/hyperopt_auto.py | 2 +- freqtrade/strategy/hyper.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index fdc4da14c..41f086ad2 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,10 +7,10 @@ from typing import Any, Callable, Dict, List from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt -# noinspection PyUnresolvedReferences class HyperOptAuto(IHyperOpt): """ This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 8de986950..154dcead6 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,7 +2,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ -from typing import Iterator, Tuple, Any, Optional, Sequence +from typing import Iterator, Tuple, Any, Optional, Sequence, Union from skopt.space import Integer, Real, Categorical @@ -22,7 +22,8 @@ class BaseParameter(object): **kwargs): """ Initialize hyperopt-optimizable parameter. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ @@ -37,6 +38,9 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' + def get_space(self, name: str) -> Union[Integer, Real, Categorical]: + raise NotImplementedError() + class IntParameter(BaseParameter): default: int @@ -49,7 +53,8 @@ class IntParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param space: Optimization space, [min, max]. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ @@ -76,7 +81,8 @@ class FloatParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param space: Optimization space, [min, max]. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ From e9f0babe8a7276cdb1fa3f22cb2b21368e209c31 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 16:03:38 +0200 Subject: [PATCH 164/348] [SQUASH] Use HyperStrategyMixin as part of IStrategy interface. --- freqtrade/optimize/hyperopt.py | 4 ---- freqtrade/optimize/hyperopt_auto.py | 10 ---------- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/strategy/__init__.py | 3 +-- freqtrade/strategy/hyper.py | 4 ---- freqtrade/strategy/interface.py | 3 ++- 6 files changed, 6 insertions(+), 24 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b5ee1da1a..4926bf1b3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -70,10 +70,6 @@ class Hyperopt: self.backtesting = Backtesting(self.config) if not self.config.get('hyperopt'): - if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' - 'parameter or add HyperStrategyMixin mixin to your ' - 'strategy class.') self.custom_hyperopt = HyperOptAuto(self.config) else: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 41f086ad2..753e8175e 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,7 +7,6 @@ from typing import Any, Callable, Dict, List from pandas import DataFrame from skopt.space import Categorical, Dimension, Integer, Real # noqa -from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -19,9 +18,6 @@ class HyperOptAuto(IHyperOpt): """ def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): attr.value = params[attr_name] @@ -30,9 +26,6 @@ class HyperOptAuto(IHyperOpt): return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): attr.value = params[attr_name] @@ -54,9 +47,6 @@ class HyperOptAuto(IHyperOpt): return default_func def _generate_indicator_space(self, category): - if not getattr(self.strategy, 'HYPER_STRATEGY', False): - raise OperationalException('Strategy must inherit from IHyperStrategy.') - for attr_name, attr in self.strategy.enumerate_parameters(category): yield attr.get_space(attr_name) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 2dd8500a6..46adf55b8 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,14 +5,14 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.strategy import IStrategy, HyperStrategyMixin +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ class IHyperOpt(ABC): """ ticker_interval: str # DEPRECATED timeframe: str - strategy: Union[IStrategy, HyperStrategyMixin] + strategy: IStrategy def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e395be106..a300c601b 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,6 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ - CategoricalParameter +from freqtrade.strategy.hyper import IntParameter, FloatParameter, CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 154dcead6..53a0c6462 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -135,13 +135,9 @@ class HyperStrategyMixin(object): strategy logic. """ - # Hint that class can be used with HyperOptAuto. - HYPER_STRATEGY = 1 - def __init__(self): """ Initialize hyperoptable strategy mixin. - :param config: """ self._load_params(getattr(self, 'buy_params', None)) self._load_params(getattr(self, 'sell_params', None)) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6d40e56cc..b00e0ccb8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -59,7 +60,7 @@ class SellCheckTuple(NamedTuple): sell_type: SellType -class IStrategy(ABC): +class IStrategy(ABC, HyperStrategyMixin): """ Interface for freqtrade strategies Defines the mandatory structure must follow any custom strategies From 11689100e7c2b6a8d8fdcb23060d2f97e3aa206f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 16:24:24 +0200 Subject: [PATCH 165/348] [SQUASH] Fix exception when HyperOpt nested class is not defined. --- freqtrade/optimize/hyperopt_auto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 753e8175e..08269b092 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -39,7 +39,7 @@ class HyperOptAuto(IHyperOpt): :param name: function name. :return: a requested function. """ - hyperopt_cls = getattr(self.strategy, 'HyperOpt') + hyperopt_cls = getattr(self.strategy, 'HyperOpt', None) default_func = getattr(super(), name) if hyperopt_cls: return getattr(hyperopt_cls, name, default_func) From fd45dfd89413cf938f14e9e65335ce8a21f316f1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 25 Mar 2021 10:00:52 +0200 Subject: [PATCH 166/348] [SQUASH] Make skopt imports optional. --- freqtrade/optimize/hyperopt_auto.py | 15 +++++++++------ freqtrade/strategy/hyper.py | 12 +++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 08269b092..fb8adfe6b 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -3,9 +3,12 @@ HyperOptAuto class. This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement IHyperStrategy interface. """ +from contextlib import suppress from typing import Any, Callable, Dict, List + from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +with suppress(ImportError): + from skopt.space import Dimension from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -57,23 +60,23 @@ class HyperOptAuto(IHyperOpt): else: return self._get_func(fallback_method_name)() - def indicator_space(self) -> List[Dimension]: + def indicator_space(self) -> List['Dimension']: return self._get_indicator_space('buy', 'indicator_space') - def sell_indicator_space(self) -> List[Dimension]: + def sell_indicator_space(self) -> List['Dimension']: return self._get_indicator_space('sell', 'sell_indicator_space') def generate_roi_table(self, params: Dict) -> Dict[int, float]: return self._get_func('generate_roi_table')(params) - def roi_space(self) -> List[Dimension]: + def roi_space(self) -> List['Dimension']: return self._get_func('roi_space')() - def stoploss_space(self) -> List[Dimension]: + def stoploss_space(self) -> List['Dimension']: return self._get_func('stoploss_space')() def generate_trailing_params(self, params: Dict) -> Dict: return self._get_func('generate_trailing_params')(params) - def trailing_space(self) -> List[Dimension]: + def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 53a0c6462..0378be1d5 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,9 +2,11 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ +from contextlib import suppress from typing import Iterator, Tuple, Any, Optional, Sequence, Union -from skopt.space import Integer, Real, Categorical +with suppress(ImportError): + from skopt.space import Integer, Real, Categorical from freqtrade.exceptions import OperationalException @@ -38,7 +40,7 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' - def get_space(self, name: str) -> Union[Integer, Real, Categorical]: + def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: raise NotImplementedError() @@ -62,7 +64,7 @@ class IntParameter(BaseParameter): raise OperationalException('IntParameter space must be [min, max]') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Integer: + def get_space(self, name: str) -> 'Integer': """ Create skopt optimization space. :param name: A name of parameter field. @@ -90,7 +92,7 @@ class FloatParameter(BaseParameter): raise OperationalException('IntParameter space must be [min, max]') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Real: + def get_space(self, name: str) -> 'Real': """ Create skopt optimization space. :param name: A name of parameter field. @@ -121,7 +123,7 @@ class CategoricalParameter(BaseParameter): 'IntParameter space must be [a, b, ...] (at least two parameters)') super().__init__(space=space, default=default, category=category, **kwargs) - def get_space(self, name: str) -> Categorical: + def get_space(self, name: str) -> 'Categorical': """ Create skopt optimization space. :param name: A name of parameter field. From 424cd2a91422f0f85d1b4362a38ff7fbb3950798 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 25 Mar 2021 17:58:11 +0200 Subject: [PATCH 167/348] [SQUASH] Use "space" instead of category. --- freqtrade/strategy/hyper.py | 73 +++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0378be1d5..9f7fc3fb6 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -18,13 +18,13 @@ class BaseParameter(object): category: Optional[str] default: Any value: Any - space: Sequence[Any] + opt_range: Sequence[Any] - def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, + def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). @@ -32,10 +32,10 @@ class BaseParameter(object): if 'name' in kwargs: raise OperationalException( 'Name is determined by parameter field name and can not be specified manually.') - self.category = category + self.category = space self._space_params = kwargs self.value = default - self.space = space + self.opt_range = opt_range def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -47,88 +47,97 @@ class BaseParameter(object): class IntParameter(BaseParameter): default: int value: int - space: Sequence[int] + opt_range: Sequence[int] - def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, - **kwargs): + def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, + space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [min, max]. + :param low: lower end of optimization space or [low, high]. + :param high: high end of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if len(space) != 2: - raise OperationalException('IntParameter space must be [min, max]') - super().__init__(space=space, default=default, category=category, **kwargs) + if high is None: + if len(low) != 2: + raise OperationalException('IntParameter space must be [low, high]') + opt_range = low + else: + opt_range = [low, high] + super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Integer': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.space, name=name, **self._space_params) + return Integer(*self.opt_range, name=name, **self._space_params) class FloatParameter(BaseParameter): default: float value: float - space: Sequence[float] + opt_range: Sequence[float] - def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, - **kwargs): + def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, + default: float, space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [min, max]. + :param low: lower end of optimization space or [low, high]. + :param high: high end of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if len(space) != 2: - raise OperationalException('IntParameter space must be [min, max]') - super().__init__(space=space, default=default, category=category, **kwargs) + if high is None: + if len(low) != 2: + raise OperationalException('IntParameter space must be [low, high]') + opt_range = low + else: + opt_range = [low, high] + super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Real': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.space, name=name, **self._space_params) + return Real(*self.opt_range, name=name, **self._space_params) class CategoricalParameter(BaseParameter): default: Any value: Any - space: Sequence[Any] + opt_range: Sequence[Any] - def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, - category: Optional[str] = None, - **kwargs): + def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, + space: Optional[str] = None, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param space: Optimization space, [a, b, ...]. + :param categories: Optimization space, [a, b, ...]. :param default: A default value. If not specified, first item from specified space will be used. - :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Categorical. """ - if len(space) < 2: + if len(categories) < 2: raise OperationalException( 'IntParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(space=space, default=default, category=category, **kwargs) + super().__init__(opt_range=categories, default=default, space=space, **kwargs) def get_space(self, name: str) -> 'Categorical': """ Create skopt optimization space. :param name: A name of parameter field. """ - return Categorical(self.space, name=name, **self._space_params) + return Categorical(self.opt_range, name=name, **self._space_params) class HyperStrategyMixin(object): From bbe6ece38ddd89ca3c4174e7c9a5d824216a9a98 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 14:25:17 +0200 Subject: [PATCH 168/348] [SQUASH] Fix parameter configs not loading. --- freqtrade/strategy/hyper.py | 2 +- freqtrade/strategy/interface.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 9f7fc3fb6..0b3021af2 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -146,7 +146,7 @@ class HyperStrategyMixin(object): strategy logic. """ - def __init__(self): + def __init__(self, *args, **kwargs): """ Initialize hyperoptable strategy mixin. """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b00e0ccb8..54c7f2353 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -141,6 +141,7 @@ class IStrategy(ABC, HyperStrategyMixin): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} + super().__init__(config) @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From 40f5c7853ed06422c53e3e320f670266f0f6cb64 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 14:25:49 +0200 Subject: [PATCH 169/348] [SQUASH] Add a way to temporarily disable a parameter (excludes from parameter loading/hyperopt) and print parameter values when executed. --- freqtrade/optimize/hyperopt_auto.py | 3 ++- freqtrade/strategy/hyper.py | 32 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index fb8adfe6b..31a11e303 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -51,7 +51,8 @@ class HyperOptAuto(IHyperOpt): def _generate_indicator_space(self, category): for attr_name, attr in self.strategy.enumerate_parameters(category): - yield attr.get_space(attr_name) + if attr.enabled: + yield attr.get_space(attr_name) def _get_indicator_space(self, category, fallback_method_name): indicator_space = list(self._generate_indicator_space(category)) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0b3021af2..e6f63f5a5 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -2,6 +2,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ +import logging from contextlib import suppress from typing import Iterator, Tuple, Any, Optional, Sequence, Union @@ -11,6 +12,9 @@ with suppress(ImportError): from freqtrade.exceptions import OperationalException +logger = logging.getLogger(__name__) + + class BaseParameter(object): """ Defines a parameter that can be optimized by hyperopt. @@ -21,7 +25,7 @@ class BaseParameter(object): opt_range: Sequence[Any] def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, - **kwargs): + enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if @@ -36,6 +40,7 @@ class BaseParameter(object): self._space_params = kwargs self.value = default self.opt_range = opt_range + self.enabled = enabled def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -50,7 +55,7 @@ class IntParameter(BaseParameter): opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, - space: Optional[str] = None, **kwargs): + space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: lower end of optimization space or [low, high]. @@ -67,7 +72,8 @@ class IntParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Integer': """ @@ -83,7 +89,7 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, - default: float, space: Optional[str] = None, **kwargs): + default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: lower end of optimization space or [low, high]. @@ -100,7 +106,8 @@ class FloatParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Real': """ @@ -116,7 +123,7 @@ class CategoricalParameter(BaseParameter): opt_range: Sequence[Any] def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, - space: Optional[str] = None, **kwargs): + space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. @@ -130,7 +137,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'IntParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, **kwargs) + super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, + **kwargs) def get_space(self, name: str) -> 'Categorical': """ @@ -167,7 +175,8 @@ class HyperStrategyMixin(object): if issubclass(attr.__class__, BaseParameter): if category is None or category == attr.category or \ attr_name.startswith(category + '_'): - yield attr_name, attr + if attr.enabled: + yield attr_name, attr def _load_params(self, params: dict) -> None: """ @@ -178,4 +187,9 @@ class HyperStrategyMixin(object): return for attr_name, attr in self.enumerate_parameters(): if attr_name in params: - attr.value = params[attr_name] + if attr.enabled: + attr.value = params[attr_name] + logger.info(f'attr_name = {attr.value}') + else: + logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' + f'Default value "{attr.value}" used.') From e934d3ddfbd32ab5cc897883d96c242eec66c5db Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 26 Mar 2021 16:55:48 +0200 Subject: [PATCH 170/348] [SQUASH] Oopsie. --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e6f63f5a5..51f937fca 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -88,7 +88,7 @@ class FloatParameter(BaseParameter): value: float opt_range: Sequence[float] - def __init__(self, low: Union[float, Sequence[float]], high: Optional[int] = None, *, + def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. From 39bfe5e1a79263b18c10e5b8ca51c58705885e66 Mon Sep 17 00:00:00 2001 From: Masoud Azizi Date: Fri, 26 Mar 2021 22:50:27 +0430 Subject: [PATCH 171/348] Thee to the --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 69bc57d1a..96c7354b9 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -165,7 +165,7 @@ Depending on the space you want to optimize, only some of the below are required * fill `sell_indicator_space` - for sell signal optimization !!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. Optional in hyperopt - can also be loaded from a strategy (recommended): From 786ddc6a9114dd8b26978be7bbeb1c67db44f48a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 10:40:48 +0100 Subject: [PATCH 172/348] remove unused imports --- freqtrade/optimize/hyperopt.py | 1 - freqtrade/optimize/hyperopt_auto.py | 2 ++ freqtrade/optimize/hyperopt_interface.py | 1 + freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/hyper.py | 19 ++++++++++--------- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4926bf1b3..a680d85c8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,6 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange -from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 31a11e303..847d8697a 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -7,6 +7,8 @@ from contextlib import suppress from typing import Any, Callable, Dict, List from pandas import DataFrame + + with suppress(ImportError): from skopt.space import Dimension diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 46adf55b8..561fb8e11 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -14,6 +14,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict from freqtrade.strategy import IStrategy + logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index a300c601b..bc0c45f7c 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) +from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.hyper import IntParameter, FloatParameter, CategoricalParameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 51f937fca..32b03d57e 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -4,7 +4,8 @@ This module defines a base class for auto-hyperoptable strategies. """ import logging from contextlib import suppress -from typing import Iterator, Tuple, Any, Optional, Sequence, Union +from typing import Any, Iterator, Optional, Sequence, Tuple, Union + with suppress(ImportError): from skopt.space import Integer, Real, Categorical @@ -58,12 +59,12 @@ class IntParameter(BaseParameter): space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param low: lower end of optimization space or [low, high]. - :param high: high end of optimization space. Must be none of entire range is passed first parameter. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ if high is None: @@ -92,12 +93,12 @@ class FloatParameter(BaseParameter): default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. - :param low: lower end of optimization space or [low, high]. - :param high: high end of optimization space. Must be none of entire range is passed first parameter. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ if high is None: From 71e2134694c88815d0c4b39e704abdbfbcadfa30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:26:26 +0100 Subject: [PATCH 173/348] Add some simple tests for hyperoptParameters --- freqtrade/strategy/hyper.py | 14 +- tests/rpc/test_rpc_apiserver.py | 6 +- .../strategy/strats/hyperoptable_strategy.py | 170 ++++++++++++++++++ tests/strategy/test_interface.py | 38 +++- tests/strategy/test_strategy_loading.py | 6 +- 5 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/hyperoptable_strategy.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32b03d57e..b8bfef767 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -67,8 +67,10 @@ class IntParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is None: - if len(low) != 2: + if high is not None and isinstance(low, Sequence): + raise OperationalException('IntParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') opt_range = low else: @@ -101,9 +103,11 @@ class FloatParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is None: - if len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') + if high is not None and isinstance(low, Sequence): + raise OperationalException('FloatParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException('FloatParameter space must be [low, high]') opt_range = low else: opt_range = [low, high] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..bef70a5dd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1149,7 +1149,11 @@ def test_api_strategies(botclient): rc = client_get(client, f"{BASE_URI}/strategies") assert_response(rc) - assert rc.json() == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + assert rc.json() == {'strategies': [ + 'DefaultStrategy', + 'HyperoptableStrategy', + 'TestStrategyLegacy' + ]} def test_api_strategy(botclient): diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py new file mode 100644 index 000000000..8cde28321 --- /dev/null +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -0,0 +1,170 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy import FloatParameter, IntParameter, IStrategy + + +class HyperoptableStrategy(IStrategy): + """ + Default Strategy provided by freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal ticker interval for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74 + } + + buy_rsi = IntParameter([0, 50], default=30, space='buy') + buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['rsi'] < self.buy_rsi.value) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), + 'sell'] = 1 + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f158a1518..9c831d194 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -10,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -552,3 +553,38 @@ def test_strategy_safe_wrapper(value): assert type(ret) == type(value) assert ret == value + + +def test_hyperopt_parameters(): + with pytest.raises(OperationalException, match=r"Name is determined.*"): + IntParameter(low=0, high=5, default=1, name='hello') + + with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): + IntParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): + FloatParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): + IntParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): + FloatParameter([0, 10], high=7, default=5, space='buy') + + x = BaseParameter(opt_range=[0, 1], default=1, space='buy') + with pytest.raises(NotImplementedError): + x.get_space('space') + + fltpar = IntParameter(low=0, high=5, default=1, space='buy') + assert fltpar.value == 1 + + +def test_auto_hyperopt_interface(default_conf): + default_conf.update({'strategy': 'HyperoptableStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi'] + # PlusDI is NOT in the buy-params, so default should be used + assert strategy.buy_plusdi.value == 0.5 + assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 1c692d2da..965c3d37b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 2 + assert len(strategies) == 3 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 2 + assert len([x for x in strategies if x['class'] is not None]) == 3 assert len([x for x in strategies if x['class'] is None]) == 1 From 4fd7bedcb28f98a8920e51b41a5b7300e62f85de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:32:51 +0100 Subject: [PATCH 174/348] Sort imports ... --- tests/strategy/test_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 9c831d194..5dd459238 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -14,6 +13,7 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re From 8022386404e4aca3fdad77e8c43464990eb11c0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 18:00:07 +0100 Subject: [PATCH 175/348] Type custom_hyperopt --- freqtrade/optimize/hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a680d85c8..f7c2da4ec 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -62,6 +62,7 @@ class Hyperopt: hyperopt = Hyperopt(config) hyperopt.start() """ + custom_hyperopt: IHyperOpt def __init__(self, config: Dict[str, Any]) -> None: self.config = config From 20f7e9b4b79dcc9db09592826289d7a030ec5558 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 19:31:54 +0200 Subject: [PATCH 176/348] Make BaseParameter get_space abstract --- freqtrade/strategy/hyper.py | 8 ++++++-- tests/strategy/test_interface.py | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index b8bfef767..461e6314f 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -3,6 +3,7 @@ IHyperStrategy interface, hyperoptable Parameter class. This module defines a base class for auto-hyperoptable strategies. """ import logging +from abc import ABC, abstractmethod from contextlib import suppress from typing import Any, Iterator, Optional, Sequence, Tuple, Union @@ -16,7 +17,7 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -class BaseParameter(object): +class BaseParameter(ABC): """ Defines a parameter that can be optimized by hyperopt. """ @@ -46,8 +47,11 @@ class BaseParameter(object): def __repr__(self): return f'{self.__class__.__name__}({self.value})' + @abstractmethod def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: - raise NotImplementedError() + """ + Get-space - will be used by Hyperopt to get the hyperopt Space + """ class IntParameter(BaseParameter): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5dd459238..e9d57dd17 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -571,9 +571,8 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): FloatParameter([0, 10], high=7, default=5, space='buy') - x = BaseParameter(opt_range=[0, 1], default=1, space='buy') - with pytest.raises(NotImplementedError): - x.get_space('space') + with pytest.raises(TypeError): + BaseParameter(opt_range=[0, 1], default=1, space='buy') fltpar = IntParameter(low=0, high=5, default=1, space='buy') assert fltpar.value == 1 From 929f3296079b4536bfb163b544dcf4ffda46f6ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 19:49:20 +0200 Subject: [PATCH 177/348] more tests --- freqtrade/strategy/hyper.py | 2 +- .../strategy/strats/hyperoptable_strategy.py | 6 ++++-- tests/strategy/test_interface.py | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 461e6314f..64e457d75 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -145,7 +145,7 @@ class CategoricalParameter(BaseParameter): """ if len(categories) < 2: raise OperationalException( - 'IntParameter space must be [a, b, ...] (at least two parameters)') + 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, **kwargs) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 8cde28321..97d2092d4 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -55,12 +55,14 @@ class HyperoptableStrategy(IStrategy): } sell_params = { - 'sell_rsi': 74 + 'sell_rsi': 74, + 'sell_minusdi': 0.4 } buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', enabled=False) def informative_pairs(self): """ @@ -164,7 +166,7 @@ class HyperoptableStrategy(IStrategy): ) | ( (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) + (dataframe['minus_di'] > self.sell_minusdi.value) ), 'sell'] = 1 return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e9d57dd17..72b78338d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,7 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, + IntParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -556,6 +557,7 @@ def test_strategy_safe_wrapper(value): def test_hyperopt_parameters(): + from skopt.space import Categorical, Integer, Real with pytest.raises(OperationalException, match=r"Name is determined.*"): IntParameter(low=0, high=5, default=1, name='hello') @@ -571,12 +573,24 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): + CategoricalParameter(['aa'], default='aa', space='buy') + with pytest.raises(TypeError): BaseParameter(opt_range=[0, 1], default=1, space='buy') - fltpar = IntParameter(low=0, high=5, default=1, space='buy') + intpar = IntParameter(low=0, high=5, default=1, space='buy') + assert intpar.value == 1 + assert isinstance(intpar.get_space(''), Integer) + + fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') + assert isinstance(catpar.get_space(''), Categorical) + assert catpar.value == 'buy_macd' + def test_auto_hyperopt_interface(default_conf): default_conf.update({'strategy': 'HyperoptableStrategy'}) @@ -587,3 +601,6 @@ def test_auto_hyperopt_interface(default_conf): # PlusDI is NOT in the buy-params, so default should be used assert strategy.buy_plusdi.value == 0.5 assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] + + # Parameter is disabled - so value from sell_param dict will NOT be used. + assert strategy.sell_minusdi.value == 0.5 From fc8478111e3d4b9810cee75857c68e3768390f82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 20:06:30 +0200 Subject: [PATCH 178/348] Improve strategy template --- freqtrade/templates/base_strategy.py.j2 | 5 +++-- freqtrade/templates/sample_strategy.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index dd6b773e1..db73d4da3 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -26,8 +26,9 @@ class {{ strategy }}(IStrategy): You must keep: - the lib in the section "Do not remove these libs" - - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, - populate_sell_trend, hyperopt_space, buy_strategy_generator + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend + You should keep: + - timeframe, minimal_roi, stoploss, trailing_* """ # Strategy interface version - allow new iterations of the strategy interface. # Check the documentation or the Sample strategy to get the latest version. diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index db1ba48b8..5dfa42bcc 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -5,7 +5,7 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- # Add your lib to import here @@ -27,8 +27,9 @@ class SampleStrategy(IStrategy): You must keep: - the lib in the section "Do not remove these libs" - - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, - populate_sell_trend, hyperopt_space, buy_strategy_generator + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend + You should keep: + - timeframe, minimal_roi, stoploss, trailing_* """ # Strategy interface version - allow new iterations of the strategy interface. # Check the documentation or the Sample strategy to get the latest version. From f6211bc00ebe43b4f8ae981658dc9f11a6c711aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Mar 2021 20:19:39 +0200 Subject: [PATCH 179/348] new-config should include API config --- freqtrade/commands/build_config_commands.py | 30 +++++++++++++++++++++ freqtrade/templates/base_config.json.j2 | 10 +++---- tests/commands/test_build_config.py | 4 +++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 3c34ff162..0dee480b3 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -1,4 +1,5 @@ import logging +import secrets from pathlib import Path from typing import Any, Dict, List @@ -138,6 +139,32 @@ def ask_user_config() -> Dict[str, Any]: "message": "Insert Telegram chat id", "when": lambda x: x['telegram'] }, + { + "type": "confirm", + "name": "api_server", + "message": "Do you want to enable the Rest API (includes FreqUI)?", + "default": False, + }, + { + "type": "text", + "name": "api_server_listen_addr", + "message": "Insert Api server Listen Address (best left untouched default!)", + "default": "127.0.0.1", + "when": lambda x: x['api_server'] + }, + { + "type": "text", + "name": "api_server_username", + "message": "Insert api-server username", + "default": "freqtrader", + "when": lambda x: x['api_server'] + }, + { + "type": "text", + "name": "api_server_password", + "message": "Insert api-server password", + "when": lambda x: x['api_server'] + }, ] answers = prompt(questions) @@ -145,6 +172,9 @@ def ask_user_config() -> Dict[str, Any]: # Interrupted questionary sessions return an empty dict. raise OperationalException("User interrupted interactive questions.") + # Force JWT token to be a random string + answers['api_server_jwt_key'] = secrets.token_hex() + return answers diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 226bf1a81..42f088f9f 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -54,15 +54,15 @@ "chat_id": "{{ telegram_chat_id }}" }, "api_server": { - "enabled": false, - "listen_ip_address": "127.0.0.1", + "enabled": {{ api_server | lower }}, + "listen_ip_address": "{{ api_server_listen_addr | default("127.0.0.1", true) }}", "listen_port": 8080, "verbosity": "error", "enable_openapi": false, - "jwt_secret_key": "somethingrandom", + "jwt_secret_key": "{{ api_server_jwt_key }}", "CORS_origins": [], - "username": "", - "password": "" + "username": "{{ api_server_username }}", + "password": "{{ api_server_password }}" }, "bot_name": "freqtrade", "initial_state": "running", diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py index 291720f4b..66c750e79 100644 --- a/tests/commands/test_build_config.py +++ b/tests/commands/test_build_config.py @@ -50,6 +50,10 @@ def test_start_new_config(mocker, caplog, exchange): 'telegram': False, 'telegram_token': 'asdf1244', 'telegram_chat_id': '1144444', + 'api_server': False, + 'api_server_listen_addr': '127.0.0.1', + 'api_server_username': 'freqtrader', + 'api_server_password': 'MoneyMachine', } mocker.patch('freqtrade.commands.build_config_commands.ask_user_config', return_value=sample_selections) From 932284574077efca1c6cb1b578e2df48dc93540d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:27:22 +0000 Subject: [PATCH 180/348] Bump mkdocs-material from 7.0.6 to 7.0.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.6 to 7.0.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.6...7.0.7) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 0068dd5d2..711b6ca46 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.6 +mkdocs-material==7.0.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From e5789b36cf8d6488abf1a8e0e2b71585c1893e9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:27:57 +0000 Subject: [PATCH 181/348] Bump numpy from 1.20.1 to 1.20.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.20.1 to 1.20.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.20.1...v1.20.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..201ce22fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.20.1 +numpy==1.20.2 pandas==1.2.3 ccxt==1.43.89 From 607c05b3cec2523eec63b87639c64adec8bf928f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:28:16 +0000 Subject: [PATCH 182/348] Bump sqlalchemy from 1.4.2 to 1.4.3 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..ff5412fb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.2 +SQLAlchemy==1.4.3 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 8e49271e6f948a37695438e8e57e43053e0c623a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 05:28:30 +0000 Subject: [PATCH 183/348] Bump prompt-toolkit from 3.0.17 to 3.0.18 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.17 to 3.0.18. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.17...3.0.18) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 56ada691f..127b9a776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,4 +39,4 @@ aiofiles==0.6.0 colorama==0.4.4 # Building config files interactively questionary==1.9.0 -prompt-toolkit==3.0.17 +prompt-toolkit==3.0.18 From 95a9c92769c389fd44693ee8c6fdfa92755e43d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 09:13:48 +0200 Subject: [PATCH 184/348] Add permission-check before slack notify --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61ecaa522..dd6af0a06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,9 +310,17 @@ jobs: needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: + - name: Check user permission + id: check + uses: scherermichael-oss/action-has-permission@1.0.6 + with: + required-permission: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Slack Notification uses: lazy-actions/slatify@v3.0.0 - if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) with: type: ${{ job.status }} job_name: '*Freqtrade CI*' From 8d01767a421113c80870b392175effbfd3300def Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 09:20:34 +0200 Subject: [PATCH 185/348] Fix CI syntax --- .github/workflows/ci.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd6af0a06..8e15a5a89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,13 +310,14 @@ jobs: needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: + - name: Check user permission - id: check - uses: scherermichael-oss/action-has-permission@1.0.6 - with: - required-permission: write - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + id: check + uses: scherermichael-oss/action-has-permission@1.0.6 + with: + required-permission: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Slack Notification uses: lazy-actions/slatify@v3.0.0 From dacaa4a732a396315eeb21f58301d7cccbef7056 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:34:57 +0000 Subject: [PATCH 186/348] Bump ccxt from 1.43.89 to 1.45.44 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.43.89 to 1.45.44. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.43.89...1.45.44) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10225f910..d3b8acb25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.43.89 +ccxt==1.45.44 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.6 aiohttp==3.7.4.post0 From 3e864a87ad66b10058df601a4f7759dfe236ff70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 09:53:50 +0000 Subject: [PATCH 187/348] Bump cryptography from 3.4.6 to 3.4.7 Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.6 to 3.4.7. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.6...3.4.7) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10225f910..a57233823 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.2.3 ccxt==1.43.89 # Pin cryptography for now due to rust build errors with piwheels -cryptography==3.4.6 +cryptography==3.4.7 aiohttp==3.7.4.post0 SQLAlchemy==1.4.3 python-telegram-bot==13.4.1 From 5d5debab667facfb0bed113ed878392d4b52caeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Mar 2021 10:01:20 +0000 Subject: [PATCH 188/348] Bump scipy from 1.6.1 to 1.6.2 Bumps [scipy](https://github.com/scipy/scipy) from 1.6.1 to 1.6.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.6.1...v1.6.2) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 8cdb6fd28..9eb490f83 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.6.1 +scipy==1.6.2 scikit-learn==0.24.1 scikit-optimize==0.8.1 filelock==3.0.12 From 6954a1e02951e587cee1d6b8e46365913ace34cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 19:27:19 +0200 Subject: [PATCH 189/348] MOre tests for ParameterHyperopt --- tests/optimize/conftest.py | 2 ++ tests/optimize/test_hyperopt.py | 17 +++++++++++++++++ tests/strategy/test_interface.py | 3 ++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index df6f22e01..5c789ec1e 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -14,6 +14,7 @@ from tests.conftest import patch_exchange def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ + 'datadir': Path(default_conf['datadir']), 'hyperopt': 'DefaultHyperOpt', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), @@ -21,6 +22,7 @@ def hyperopt_conf(default_conf): 'timerange': None, 'spaces': ['default'], 'hyperopt_jobs': 1, + 'hyperopt_min_trades': 1, }) return hyperconf diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 193d997db..36b6f1229 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,6 +16,7 @@ from freqtrade.commands.optimize_commands import setup_optimize_configuration, s from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode @@ -1089,3 +1090,19 @@ def test_print_epoch_details(capsys): assert '# ROI table:' in captured.out assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE) assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) + + +def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: + # mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + # mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # No hyperopt needed + del hyperopt_conf['hyperopt'] + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + }) + hyperopt = Hyperopt(hyperopt_conf) + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + hyperopt.start() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 72b78338d..4d93f7049 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -587,7 +587,8 @@ def test_hyperopt_parameters(): assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 - catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], + default='buy_macd', space='buy') assert isinstance(catpar.get_space(''), Categorical) assert catpar.value == 'buy_macd' From 89bbfd2324ebf0974c60f1b8689941a079b14eea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Mar 2021 20:22:52 +0200 Subject: [PATCH 190/348] Remove candle_count from dataframe before backtesting closes #3754 --- freqtrade/data/converter.py | 16 +++++++++++----- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/optimize/hyperopt.py | 3 ++- tests/data/test_converter.py | 10 ++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index d4053abaa..defc96ef8 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -115,17 +115,23 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) return df -def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date') -> DataFrame: +def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', + startup_candles: int = 0) -> DataFrame: """ Trim dataframe based on given timerange :param df: Dataframe to trim :param timerange: timerange (use start and end date if available) - :param: df_date_col: Column in the dataframe to use as Date column + :param df_date_col: Column in the dataframe to use as Date column + :param startup_candles: When not 0, is used instead the timerange start date :return: trimmed dataframe """ - if timerange.starttype == 'date': - start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) - df = df.loc[df[df_date_col] >= start, :] + if startup_candles: + # Trim candles instead of timeframe in case of given startup_candle count + df = df.iloc[startup_candles:, :] + else: + if timerange.starttype == 'date': + start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) + df = df.loc[df[df_date_col] >= start, :] if timerange.stoptype == 'date': stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) df = df.loc[df[df_date_col] <= stop, :] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 765e2844a..ff1dd934c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -443,7 +443,8 @@ class Backtesting: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) + preprocessed[pair] = trim_dataframe(df, timerange, + startup_candles=self.required_startup) min_date, max_date = history.get_timerange(preprocessed) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..ee453489d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,8 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): - preprocessed[pair] = trim_dataframe(df, timerange) + preprocessed[pair] = trim_dataframe(df, timerange, + startup_candles=self.backtesting.required_startup) min_date, max_date = get_timerange(preprocessed) logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 4fdcce4d2..2420dca8f 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -197,6 +197,16 @@ def test_trim_dataframe(testdatadir) -> None: assert all(data_modify.iloc[-1] == data.iloc[-1]) assert all(data_modify.iloc[0] == data.iloc[30]) + data_modify = data.copy() + tr = TimeRange('date', None, min_date + 1800, 0) + # Remove first 20 candles - ignores min date + data_modify = trim_dataframe(data_modify, tr, startup_candles=20) + assert not data_modify.equals(data) + assert len(data_modify) < len(data) + assert len(data_modify) == len(data) - 20 + assert all(data_modify.iloc[-1] == data.iloc[-1]) + assert all(data_modify.iloc[0] == data.iloc[20]) + data_modify = data.copy() # Remove last 30 minutes (1800 s) tr = TimeRange(None, 'date', 0, max_date - 1800) From 50fcb3f330a16a477e3fdf48243f1d3accad6402 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Mar 2021 07:26:39 +0200 Subject: [PATCH 191/348] Reduce verbosity of missing data if less than 1% of data is missing --- freqtrade/data/converter.py | 9 ++++++++- tests/data/test_converter.py | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index defc96ef8..c9d4ef19f 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -110,8 +110,15 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) df.reset_index(inplace=True) len_before = len(dataframe) len_after = len(df) + pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 if len_before != len_after: - logger.info(f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}") + message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" + f" - {round(pct_missing * 100, 2)} %") + if pct_missing > 0.01: + logger.info(message) + else: + # Don't be verbose if only a small amount is missing + logger.debug(message) return df diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 2420dca8f..68960af1c 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -10,7 +10,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma trades_to_ohlcv, trim_dataframe) from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) -from tests.conftest import log_has +from tests.conftest import log_has, log_has_re from tests.data.test_history import _backup_file, _clean_test_file @@ -62,8 +62,8 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", caplog) + assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}.*", caplog) # Test fillup actually fixes invalid backtest data min_date, max_date = get_timerange({'UNITTEST/BTC': data}) @@ -125,8 +125,8 @@ def test_ohlcv_fill_up_missing_data2(caplog): # Column names should not change assert (data.columns == data2.columns).all() - assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " - f"{len(data)} - after: {len(data2)}", caplog) + assert log_has_re(f"Missing data fillup for UNITTEST/BTC: before: " + f"{len(data)} - after: {len(data2)}.*", caplog) def test_ohlcv_drop_incomplete(caplog): From 2869d5368de5868b7dfb0c72d402449cf72c2997 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Mar 2021 20:20:24 +0200 Subject: [PATCH 192/348] Allow edge to use dynamic pairlists closes #4298 --- docs/edge.md | 6 +++--- freqtrade/configuration/config_validation.py | 5 ----- freqtrade/edge/edge_positioning.py | 5 ++--- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/edge_cli.py | 2 +- tests/edge/test_edge.py | 8 ++++---- tests/test_configuration.py | 16 ---------------- 7 files changed, 11 insertions(+), 33 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 5565ca2f9..0aa76cd12 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,9 +1,9 @@ # Edge positioning -The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. +The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. !!! Warning - `Edge positioning` is not compatible with dynamic (volume-based) whitelist. + WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data. !!! Note `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. @@ -14,7 +14,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. +To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. !!! tip "It doesn't matter how often, but how much!" A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index c7e49f33d..31e38d572 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -149,11 +149,6 @@ def _validate_edge(conf: Dict[str, Any]) -> None: if not conf.get('edge', {}).get('enabled'): return - if conf.get('pairlist', {}).get('method') == 'VolumePairList': - raise OperationalException( - "Edge and VolumePairList are incompatible, " - "Edge will override whatever pairs VolumePairlist selects." - ) if not conf.get('ask_strategy', {}).get('use_sell_signal', True): raise OperationalException( "Edge requires `use_sell_signal` to be True, otherwise no sells will happen." diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index ff86e522e..d1f76c21f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -84,9 +84,8 @@ class Edge: self.fee = self.exchange.get_fee(symbol=expand_pairlist( self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) - def calculate(self) -> bool: - pairs = expand_pairlist(self.config['exchange']['pair_whitelist'], - list(self.exchange.markets)) + def calculate(self, pairs: List[str]) -> bool: + heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73f4c91be..dd6966848 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -225,7 +225,7 @@ class FreqtradeBot(LoggingMixin): # Calculating Edge positioning if self.edge: - self.edge.calculate() + self.edge.calculate(_whitelist) _whitelist = self.edge.adjust(_whitelist) if trades: diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a5f505bee..aab7def05 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -44,7 +44,7 @@ class EdgeCli: 'timerange') is None else str(self.config.get('timerange'))) def start(self) -> None: - result = self.edge.calculate() + result = self.edge.calculate(self.config['exchange']['pair_whitelist']) if result: print('') # blank line for readability print(generate_edge_table(self.edge._cached_pairs)) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index c30bce6a4..5142dd985 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -266,7 +266,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): # should not recalculate if heartbeat not reached edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1 - assert edge.calculate() is False + assert edge.calculate(edge_conf['exchange']['pair_whitelist']) is False def mocked_load_data(datadir, pairs=[], timeframe='0m', @@ -310,7 +310,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert edge.calculate() + assert edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 2 assert edge._last_updated <= arrow.utcnow().int_timestamp + 2 @@ -322,7 +322,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert not edge.calculate() + assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 0 assert log_has("No data found. Edge is stopped ...", caplog) assert edge._last_updated == 0 @@ -337,7 +337,7 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) - assert not edge.calculate() + assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 0 assert log_has("No trades found.", caplog) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a0824e65c..15fbab7f8 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -860,22 +860,6 @@ def test_validate_tsl(default_conf): validate_config_consistency(default_conf) -def test_validate_edge(edge_conf): - edge_conf.update({"pairlist": { - "method": "VolumePairList", - }}) - - with pytest.raises(OperationalException, - match="Edge and VolumePairList are incompatible, " - "Edge will override whatever pairs VolumePairlist selects."): - validate_config_consistency(edge_conf) - - edge_conf.update({"pairlist": { - "method": "StaticPairList", - }}) - validate_config_consistency(edge_conf) - - def test_validate_edge2(edge_conf): edge_conf.update({"ask_strategy": { "use_sell_signal": True, From 5e5b11d4d60c6f7f8544e8f1cd8064df01f18a6d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 31 Mar 2021 12:31:28 +0300 Subject: [PATCH 193/348] Split "enabled" to "load" and "optimize" parameters. --- freqtrade/optimize/hyperopt_auto.py | 8 ++++--- freqtrade/strategy/hyper.py | 36 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 847d8697a..0e3cf7eac 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -25,7 +25,8 @@ class HyperOptAuto(IHyperOpt): def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): - attr.value = params[attr_name] + if attr.optimize: + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -33,7 +34,8 @@ class HyperOptAuto(IHyperOpt): def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): - attr.value = params[attr_name] + if attr.optimize: + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend @@ -53,7 +55,7 @@ class HyperOptAuto(IHyperOpt): def _generate_indicator_space(self, category): for attr_name, attr in self.strategy.enumerate_parameters(category): - if attr.enabled: + if attr.optimize: yield attr.get_space(attr_name) def _get_indicator_space(self, category, fallback_method_name): diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 64e457d75..c06ae36da 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -27,12 +27,14 @@ class BaseParameter(ABC): opt_range: Sequence[Any] def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, - enabled: bool = True, **kwargs): + optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). """ if 'name' in kwargs: @@ -42,7 +44,8 @@ class BaseParameter(ABC): self._space_params = kwargs self.value = default self.opt_range = opt_range - self.enabled = enabled + self.optimize = optimize + self.load = load def __repr__(self): return f'{self.__class__.__name__}({self.value})' @@ -60,7 +63,7 @@ class IntParameter(BaseParameter): opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, - space: Optional[str] = None, enabled: bool = True, **kwargs): + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. @@ -69,6 +72,8 @@ class IntParameter(BaseParameter): :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ if high is not None and isinstance(low, Sequence): @@ -79,8 +84,8 @@ class IntParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Integer': """ @@ -96,7 +101,7 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, enabled: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. @@ -105,6 +110,8 @@ class FloatParameter(BaseParameter): :param default: A default value. :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): @@ -115,8 +122,8 @@ class FloatParameter(BaseParameter): opt_range = low else: opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Real': """ @@ -132,7 +139,7 @@ class CategoricalParameter(BaseParameter): opt_range: Sequence[Any] def __init__(self, categories: Sequence[Any], *, default: Optional[Any] = None, - space: Optional[str] = None, enabled: bool = True, **kwargs): + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. @@ -141,13 +148,15 @@ class CategoricalParameter(BaseParameter): :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field name is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Categorical. """ if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, enabled=enabled, - **kwargs) + super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': """ @@ -184,8 +193,7 @@ class HyperStrategyMixin(object): if issubclass(attr.__class__, BaseParameter): if category is None or category == attr.category or \ attr_name.startswith(category + '_'): - if attr.enabled: - yield attr_name, attr + yield attr_name, attr def _load_params(self, params: dict) -> None: """ @@ -196,7 +204,7 @@ class HyperStrategyMixin(object): return for attr_name, attr in self.enumerate_parameters(): if attr_name in params: - if attr.enabled: + if attr.load: attr.value = params[attr_name] logger.info(f'attr_name = {attr.value}') else: From 5acdc9bf424e6c46329a5f886182b5317321846a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 06:47:23 +0200 Subject: [PATCH 194/348] Fix type errors by converting all hyperopt methods to instance methods --- freqtrade/optimize/hyperopt_interface.py | 33 +++++++++--------------- freqtrade/strategy/hyper.py | 3 ++- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..e951efddd 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -44,36 +44,31 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: """ Create a buy strategy generator. """ raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy')) - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: """ Create a sell strategy generator. """ raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell')) - @staticmethod - def indicator_space() -> List[Dimension]: + def indicator_space(self) -> List[Dimension]: """ Create an indicator space. """ raise OperationalException(_format_exception_message('indicator_space', 'buy')) - @staticmethod - def sell_indicator_space() -> List[Dimension]: + def sell_indicator_space(self) -> List[Dimension]: """ Create a sell indicator space. """ raise OperationalException(_format_exception_message('sell_indicator_space', 'sell')) - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: + def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. @@ -88,8 +83,7 @@ class IHyperOpt(ABC): return roi_table - @staticmethod - def roi_space() -> List[Dimension]: + def roi_space(self) -> List[Dimension]: """ Create a ROI space. @@ -109,7 +103,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_min = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_min = timeframe_to_minutes(self.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: @@ -145,7 +139,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -154,7 +148,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), @@ -165,8 +159,7 @@ class IHyperOpt(ABC): Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), ] - @staticmethod - def stoploss_space() -> List[Dimension]: + def stoploss_space(self) -> List[Dimension]: """ Create a stoploss space. @@ -177,8 +170,7 @@ class IHyperOpt(ABC): Real(-0.35, -0.02, name='stoploss'), ] - @staticmethod - def generate_trailing_params(params: Dict) -> Dict: + def generate_trailing_params(self, params: Dict) -> Dict: """ Create dict with trailing stop parameters. """ @@ -190,8 +182,7 @@ class IHyperOpt(ABC): 'trailing_only_offset_is_reached': params['trailing_only_offset_is_reached'], } - @staticmethod - def trailing_space() -> List[Dimension]: + def trailing_space(self) -> List[Dimension]: """ Create a trailing stoploss space. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c06ae36da..a6603ecbf 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -101,7 +101,8 @@ class FloatParameter(BaseParameter): opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, + optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. From d64295ba24a13958d4d79c7dd8dfb6134b83b85b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 Apr 2021 06:55:25 +0200 Subject: [PATCH 195/348] Adapt test strategy to new parameters --- tests/strategy/strats/hyperoptable_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 97d2092d4..a08293058 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -62,7 +62,7 @@ class HyperoptableStrategy(IStrategy): buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') - sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', enabled=False) + sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) def informative_pairs(self): """ From 51f0fcb2cb071ae2753c94a21e23c87b9a57e129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 12:20:38 +0200 Subject: [PATCH 196/348] Add profit_fiat to REST API --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 32a1c8597..eaca477d7 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -168,6 +168,7 @@ class TradeSchema(BaseModel): profit_ratio: Optional[float] profit_pct: Optional[float] profit_abs: Optional[float] + profit_fiat: Optional[float] sell_reason: Optional[str] sell_order_status: Optional[str] stop_loss_abs: Optional[float] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 62f1c2592..1b2dc5d66 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -173,6 +173,14 @@ class RPC: current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) + + # Calculate fiat profit + current_profit_fiat = self._fiat_converter.convert_amount( + current_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) + # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss) @@ -191,6 +199,7 @@ class RPC: profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, + profit_fiat=current_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), From 2c0079b00b8902405f487f1c8ffbc1f614de8651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 13:16:52 +0200 Subject: [PATCH 197/348] Add profit_fiat to tests, use ANY, as price changes... --- tests/rpc/test_rpc_apiserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..3b1ef6a74 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -786,6 +786,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, + 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', From f47dc317867ebe5186519d99e4ee5d3b0ffab2d3 Mon Sep 17 00:00:00 2001 From: shubhendra Date: Sun, 21 Mar 2021 17:14:35 +0530 Subject: [PATCH 198/348] Refactor the comparison involving `not` Signed-off-by: shubhendra --- .deepsource.toml | 16 ---------------- freqtrade/exchange/exchange.py | 2 +- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- freqtrade/plugins/protections/cooldown_period.py | 1 - freqtrade/resolvers/strategy_resolver.py | 4 ++-- 5 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 7a00ca8d6..000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,16 +0,0 @@ -version = 1 - -test_patterns = ["tests/**/test_*.py"] - -exclude_patterns = [ - "docs/**", - "user_data/**", - "build/helpers/**" -] - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9c868df2b..85c5b4c6d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -806,7 +806,7 @@ class Exchange: # Gather coroutines to run for pair, timeframe in set(pair_list): - if (not ((pair, timeframe) in self._klines) + if (((pair, timeframe) not in self._klines) or self._now_is_time_to_refresh(pair, timeframe)): input_coroutines.append(self._async_get_candle_history(pair, timeframe, since_ms=since_ms)) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index c1355f655..73a9436fa 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Any, Dict, List +from typing import Dict, List import pandas as pd diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 197d74c2e..a2d8eca34 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,7 +1,6 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 19bd014f9..05fbac10d 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -197,8 +197,8 @@ class StrategyResolver(IResolver): strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) if any(x == 2 for x in [strategy._populate_fun_len, - strategy._buy_fun_len, - strategy._sell_fun_len]): + strategy._buy_fun_len, + strategy._sell_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy From ede26091b980a22d84bc44ce5e5e3b74119ba76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 14:35:19 +0200 Subject: [PATCH 199/348] Add validation in the right places... --- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index b11470711..64918ed47 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -92,6 +92,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, + 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, @@ -159,6 +160,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, + 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3b1ef6a74..20d32024f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -966,6 +966,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'profit_ratio': None, 'profit_pct': None, 'profit_abs': None, + 'profit_fiat': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, From 3691ae8686d9987966956018aa8992b36278ce22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Fri, 2 Apr 2021 14:50:47 +0200 Subject: [PATCH 200/348] Make sure the fiat converter exists before calling it --- freqtrade/rpc/rpc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1b2dc5d66..1359729b9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -175,11 +175,12 @@ class RPC: current_profit_abs = trade.calc_profit(current_rate) # Calculate fiat profit - current_profit_fiat = self._fiat_converter.convert_amount( - current_profit_abs, - self._freqtrade.config['stake_currency'], - self._freqtrade.config['fiat_display_currency'] - ) + if self._fiat_converter: + current_profit_fiat = self._fiat_converter.convert_amount( + current_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) From ea43d5ba85caf490604d02a6abae8e620324f034 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 1 Apr 2021 10:17:39 +0300 Subject: [PATCH 201/348] Implement DecimalParameter and rename FloatParameter to RealParameter. --- freqtrade/optimize/hyperopt_auto.py | 6 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 66 +++++++++++++++++-- .../strategy/strats/hyperoptable_strategy.py | 7 +- tests/strategy/test_interface.py | 26 ++++++-- 5 files changed, 88 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 0e3cf7eac..ed6f2d6f7 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -26,7 +26,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -35,7 +36,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index bc0c45f7c..bd49165df 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter, + RealParameter) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index a6603ecbf..e58aac273 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -56,6 +56,14 @@ class BaseParameter(ABC): Get-space - will be used by Hyperopt to get the hyperopt Space """ + def _set_value(self, value: Any): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: A numerical value. + """ + self.value = value + class IntParameter(BaseParameter): default: int @@ -65,7 +73,7 @@ class IntParameter(BaseParameter): def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable integer parameter. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none of entire range is passed first parameter. @@ -95,16 +103,16 @@ class IntParameter(BaseParameter): return Integer(*self.opt_range, name=name, **self._space_params) -class FloatParameter(BaseParameter): +class RealParameter(BaseParameter): default: float value: float opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, - default: float, space: Optional[str] = None, - optimize: bool = True, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, + load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable floating point parameter with unlimited precision. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none if entire range is passed first parameter. @@ -116,10 +124,10 @@ class FloatParameter(BaseParameter): :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): - raise OperationalException('FloatParameter space invalid.') + raise OperationalException(f'{self.__class__.__name__} space invalid.') if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('FloatParameter space must be [low, high]') + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') opt_range = low else: opt_range = [low, high] @@ -134,6 +142,50 @@ class FloatParameter(BaseParameter): return Real(*self.opt_range, name=name, **self._space_params) +class DecimalParameter(RealParameter): + default: float + value: float + opt_range: Sequence[float] + + def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, + default: float, decimals: int = 3, space: Optional[str] = None, + optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable decimal parameter with a limited precision. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. + :param default: A default value. + :param decimals: A number of decimals after floating point to be included in testing. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.Real. + """ + self._decimals = decimals + default = round(default, self._decimals) + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, + load=load, **kwargs) + + def get_space(self, name: str) -> 'Integer': + """ + Create skopt optimization space. + :param name: A name of parameter field. + """ + low = int(self.opt_range[0] * pow(10, self._decimals)) + high = int(self.opt_range[1] * pow(10, self._decimals)) + return Integer(low, high, name=name, **self._space_params) + + def _set_value(self, value: int): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: An integer value. + """ + self.value = round(value * pow(0.1, self._decimals), self._decimals) + + class CategoricalParameter(BaseParameter): default: Any value: Any diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index a08293058..cc4734e13 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import FloatParameter, IntParameter, IStrategy +from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, RealParameter class HyperoptableStrategy(IStrategy): @@ -60,9 +60,10 @@ class HyperoptableStrategy(IStrategy): } buy_rsi = IntParameter([0, 50], default=30, space='buy') - buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') - sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) def informative_pairs(self): """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4d93f7049..71f877cc3 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,8 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, - IntParameter) +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, + IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -564,14 +564,20 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): IntParameter(low=0, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): - FloatParameter(low=0, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space must be.*"): + RealParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space must be.*"): + DecimalParameter(low=0, default=5, space='buy') with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): IntParameter([0, 10], high=7, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): - FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space invalid\."): + RealParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space invalid\."): + DecimalParameter([0, 10], high=7, default=5, space='buy') with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): CategoricalParameter(['aa'], default='aa', space='buy') @@ -583,10 +589,16 @@ def test_hyperopt_parameters(): assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) - fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') + assert isinstance(fltpar.get_space(''), Integer) + assert fltpar.value == 1 + fltpar._set_value(2222) + assert fltpar.value == 2.222 + catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') assert isinstance(catpar.get_space(''), Categorical) From 7728e269fdcae6b078913628a067edb5cbcdfd7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 07:17:02 +0200 Subject: [PATCH 202/348] Include Technical in default image --- docker-compose.yml | 2 +- docker/{Dockerfile.technical => Dockerfile.custom} | 3 ++- docs/docker_quickstart.md | 4 ++-- requirements.txt | 1 + setup.py | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) rename docker/{Dockerfile.technical => Dockerfile.custom} (50%) diff --git a/docker-compose.yml b/docker-compose.yml index 1f63059f0..80e194ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: # Build step - only needed when additional dependencies are needed # build: # context: . - # dockerfile: "./docker/Dockerfile.technical" + # dockerfile: "./docker/Dockerfile.custom" restart: unless-stopped container_name: freqtrade volumes: diff --git a/docker/Dockerfile.technical b/docker/Dockerfile.custom similarity index 50% rename from docker/Dockerfile.technical rename to docker/Dockerfile.custom index 9431e72d0..10620e6b8 100644 --- a/docker/Dockerfile.technical +++ b/docker/Dockerfile.custom @@ -3,4 +3,5 @@ FROM freqtradeorg/freqtrade:develop RUN apt-get update \ && apt-get -y install git \ && apt-get clean \ - && pip install git+https://github.com/freqtrade/technical + # The below dependency - pyti - serves as an example. Please use whatever you need! + && pip install pyti diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 017264569..9e74841b4 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -156,8 +156,8 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) for an example). +If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.cusotm) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. diff --git a/requirements.txt b/requirements.txt index 93ed7570e..e4984cf47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ urllib3==1.26.4 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 +technical==1.2.2 tabulate==0.8.9 pycoingecko==1.4.0 jinja2==2.11.3 diff --git a/setup.py b/setup.py index 118bc8485..bf23fb999 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ setup(name='freqtrade', 'wrapt', 'jsonschema', 'TA-Lib', + 'technical', 'tabulate', 'pycoingecko', 'py_find_1st', From e7a1924aa0e1c9c795516db10690af8fbccffb37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 08:36:06 +0200 Subject: [PATCH 203/348] Fix typo --- docs/docker_quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 9e74841b4..ca0515281 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -157,7 +157,7 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.cusotm) for an example). +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.custom) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. From 23c19b6852f62026f80366bde35ad7f8254f61b1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 3 Apr 2021 11:17:18 +0300 Subject: [PATCH 204/348] New hyperopt documentation. --- docs/hyperopt.md | 283 +++++++++--------- docs/hyperopt_legacy.md | 629 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 759 insertions(+), 153 deletions(-) create mode 100644 docs/hyperopt_legacy.md diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 69bc57d1a..e9d440a5a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -14,6 +14,9 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) +!!! Note + Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + ## Install hyperopt dependencies Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. @@ -137,47 +140,19 @@ Strategy arguments: ``` -## Prepare Hyperopting - -Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). - -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - ### Hyperopt checklist Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization +* define parameters with `space='buy'` - for buy signal optimization +* define parameters with `space='sell'` - for sell signal optimization !!! Note `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `populate_indicators` - fallback to create indicators -* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy -* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy - -!!! Note - You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: +Rarely you may also need to create a nested class named `HyperOpt` and implement: * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) @@ -185,31 +160,19 @@ Rarely you may also need to override: * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" - You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. + You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. ```python # Have a working strategy at hand. - freqtrade new-hyperopt --hyperopt EmptyHyperopt - - freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` -### Create a Custom Hyperopt File - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -This command will create a new hyperopt file from a template, allowing you to get started quickly. - ### Configure your Guards and Triggers -There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: +There are two places you need to change in your strategy file to add a new buy hyperopt for testing: -* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. +* Define the parameters at the class level hyperopt shall be optimizing. +* Within `populate_buy_trend()` - use defined parameter values instead of raw constants. There you have two different types of indicators: 1. `guards` and 2. `triggers`. @@ -225,24 +188,46 @@ Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if ADX > 10*". -If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. +```python +from freqtrade.strategy import IntParameter, IStrategy + +class MyAwesomeStrategy(IStrategy): + # If parameter is prefixed with `buy_` or `sell_` then specifying `space` parameter is optional + # and space is inferred from parameter name. + buy_adx_min = IntParameter(0, 100, default=10) + + def populate_buy_trend(self, dataframe: 'DataFrame', metadata: dict) -> 'DataFrame': + dataframe.loc[ + ( + (dataframe['adx'] > self.buy_adx_min.value) + ), 'buy'] = 1 + return dataframe +``` #### Sell optimization Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. +* Define the parameters at the class level hyperopt shall be optimizing. +* Within `populate_sell_trend()` - use defined parameter values instead of raw constants. The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. -#### Using timeframe as a part of the Strategy +```python +class MyAwesomeStrategy(IStrategy): + # There is no strict parameter naming scheme. If you do not use `buy_` or `sell_` prefixes - + # please specify to which space parameter belongs using `space` parameter. Possible values: + # 'buy' or 'sell'. + adx_max = IntParameter(0, 100, default=50, space='sell') -The Strategy class exposes the timeframe value as the `self.timeframe` attribute. -The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['adx'] < self.adx_max.value) + ), 'buy'] = 1 + return dataframe +``` ## Solving a Mystery @@ -252,25 +237,20 @@ help with those buy decisions. If you decide to use RSI or ADX, which values should I use for them? So let's use hyperparameter optimization to solve this mystery. -We will start by defining a search space: +We will start by defining hyperoptable parameters: ```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] +class MyAwesomeStrategy(IStrategy): + buy_adx = IntParameter(20, 40, default=30) + buy_rsi = IntParameter(20, 40, default=30) + buy_adx_enabled = CategoricalParameter([True, False]), + buy_rsi_enabled = CategoricalParameter([True, False]), + buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), ``` Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +to find the best combination. Two of them are integer values (`buy_adx` +and `buy_rsi`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. @@ -278,39 +258,31 @@ one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: ```python - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if self.buy_adx_enabled.value: + conditions.append(dataframe['adx'] > self.buy_adx.value) + if self.buy_rsi_enabled.value: + conditions.append(dataframe['rsi'] < self.buy_rsi.value) - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) + # TRIGGERS + if self.buy_trigger.value == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if self.buy_trigger.value == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 - return dataframe - - return populate_buy_trend + return dataframe ``` Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. @@ -322,6 +294,20 @@ Based on the results, hyperopt will tell you which parameter combination produce When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in your strategy or hyperopt file. +## Parameter types + +There are four parameter types each suited for different purposes. +* `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. +* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. +* `RealParameter` - defines a floating point parameter with upper and lower boundarie and no precision limit. Rarely used. +* `CategoricalParameter` - defines a parameter with a predetermined number of choices. + +!!! Tip "Disabling parameter optimization" + Each parameter takes two boolean parameters: + * `load` - when set to `False` it will not load values configured in `buy_params` and `sell_params`. + * `optimize` - when set to `False` parameter will not be included in optimization process. + Use these parameters to quickly prototype various ideas. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. @@ -348,11 +334,9 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +freqtrade hyperopt --config config.json --hyperopt-loss --strategy -e 500 --spaces all ``` -Use `` as the name of the custom hyperopt used. - The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. @@ -378,14 +362,6 @@ For example, to use one month of data, pass the following parameter to the hyper freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 ``` -### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. @@ -439,7 +415,7 @@ If you have not changed anything in the command line options, configuration, tim ## Understand the Hyperopt Result -Once Hyperopt is completed you can use the result to create a new strategy. +Once Hyperopt is completed you can use the result to update your strategy. Given the following result from hyperopt: ``` @@ -447,40 +423,36 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } ``` You should understand this result like: - The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) -- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) +- You should not use ADX because `'buy_adx_enabled': False`) +- You should **consider** using the RSI indicator (`'buy_rsi_enabled': True` and the best value is `29.0` (`'buy_rsi': 29.0`) -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. +Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste it at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: +Transferring your whole hyperopt result to your strategy would then look like: ```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe +class MyAwsomeStrategy(IStrategy): + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } ``` By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. @@ -499,11 +471,13 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -ROI table: -{ 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0} + # ROI table: + minimal_roi = { + 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0 + } ``` In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: @@ -549,13 +523,16 @@ Best result: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -Stoploss: -0.27996 + # Buy hyperspace params: + buy_params = { + 'buy_adx': 44, + 'buy_rsi': 29, + 'buy_adx_enabled': False, + 'buy_rsi_enabled': True, + 'buy_trigger': 'bb_lower' + } + + stoploss: -0.27996 ``` In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: @@ -585,11 +562,11 @@ Best result: 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 -Trailing stop: -{ 'trailing_only_offset_is_reached': True, - 'trailing_stop': True, - 'trailing_stop_positive': 0.02001, - 'trailing_stop_positive_offset': 0.06038} + # Trailing stop: + trailing_stop = True + trailing_stop_positive = 0.02001 + trailing_stop_positive_offset = 0.06038 + trailing_only_offset_is_reached = True ``` In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md new file mode 100644 index 000000000..8c6972b5f --- /dev/null +++ b/docs/hyperopt_legacy.md @@ -0,0 +1,629 @@ +# Legacy Hyperopt + +This page explains how to tune your strategy by finding the optimal +parameters, a process called hyperparameter optimization. The bot uses several +algorithms included in the `scikit-optimize` package to accomplish this. The +search will burn all your CPU cores, make your laptop sound like a fighter jet +and still take a long time. + +In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). + +Hyperopt requires historic data to be available, just as backtesting does. +To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. + +!!! Note + Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Please update your strategy class following new documentation at [Hyperopt](hyperopt.md). + +!!! Bug +Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) + +## Install hyperopt dependencies + +Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. + +!!! Note +Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported. + +### Docker + +The docker-image includes hyperopt dependencies, no further action needed. + +### Easy installation script (setup.sh) / Manual installation + +```bash +source .env/bin/activate +pip install -r requirements-hyperopt.txt +``` + +## Hyperopt command reference + + +``` +usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [-s NAME] [--strategy-path PATH] + [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] + [--hyperopt NAME] [--hyperopt-path PATH] [--eps] + [--dmmp] [--enable-protections] + [--dry-run-wallet DRY_RUN_WALLET] [-e INT] + [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] + [--print-all] [--no-color] [--print-json] [-j JOBS] + [--random-state INT] [--min-trades INT] + [--hyperopt-loss NAME] + +optional arguments: + -h, --help show this help message and exit + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + --timerange TIMERANGE + Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. + --fee FLOAT Specify fee ratio. Will be applied twice (on trade + entry and exit). + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --hyperopt-path PATH Specify additional lookup path for Hyperopt and + Hyperopt Loss functions. + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking). + --dmmp, --disable-max-market-positions + Disable applying `max_open_trades` during backtest + (same as setting `max_open_trades` to a very high + number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections + --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET + Starting balance, used for backtesting / hyperopt and + dry-runs. + -e INT, --epochs INT Specify number of epochs (default: 100). + --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] + Specify which parameters to hyperopt. Space-separated + list. + --print-all Print all results, not only the best ones. + --no-color Disable colorization of hyperopt results. May be + useful if you are redirecting output to a file. + --print-json Print output in JSON format. + -j JOBS, --job-workers JOBS + The number of concurrently running jobs for + hyperoptimization (hyperopt worker processes). If -1 + (default), all CPUs are used, for -2, all CPUs but one + are used, etc. If 1 is given, no parallel computing + code is used at all. + --random-state INT Set random state to some positive integer for + reproducible hyperopt results. + --min-trades INT Set minimal desired number of trades for evaluations + in the hyperopt optimization path (default: 1). + --hyperopt-loss NAME Specify the class name of the hyperopt loss function + class (IHyperOptLoss). Different functions can + generate completely different results, since the + target for optimization is different. Built-in + Hyperopt-loss-functions are: + ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, + SharpeHyperOptLoss, SharpeHyperOptLossDaily, + SortinoHyperOptLoss, SortinoHyperOptLossDaily + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. + +``` + +## Prepare Hyperopting + +Before we start digging into Hyperopt, we recommend you to take a look at +the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). + +Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. + +!!! Tip "About this page" +For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + +The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +### Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt + +Depending on the space you want to optimize, only some of the below are required: + +* fill `buy_strategy_generator` - for buy signal optimization +* fill `indicator_space` - for buy signal optimization +* fill `sell_strategy_generator` - for sell signal optimization +* fill `sell_indicator_space` - for sell signal optimization + +!!! Note +`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + +Optional in hyperopt - can also be loaded from a strategy (recommended): + +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy + +!!! Note +You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. +Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) +* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) + +!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" +You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. + + ```python + # Have a working strategy at hand. + freqtrade new-hyperopt --hyperopt EmptyHyperopt + + freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + ``` + +### Create a Custom Hyperopt File + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +This command will create a new hyperopt file from a template, allowing you to get started quickly. + +### Configure your Guards and Triggers + +There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: + +* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. + +There you have two different types of indicators: 1. `guards` and 2. `triggers`. + +1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. +2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". + +!!! Hint "Guards and Triggers" +Technically, there is no difference between Guards and Triggers. +However, this guide will make this distinction to make it clear that signals should not be "sticking". +Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). + +Hyper-optimization will, for each epoch round, pick one trigger and possibly +multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if +ADX > 10*". + +If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. + +#### Sell optimization + +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods + +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. + +The configuration and rules are the same than for buy signals. +To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. + +#### Using timeframe as a part of the Strategy + +The Strategy class exposes the timeframe value as the `self.timeframe` attribute. +The same value is available as class-attribute `HyperoptName.timeframe`. +In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. + +## Solving a Mystery + +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this +mystery. + +We will start by defining a search space: + +```python + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(20, 40, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') + ] +``` + +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` +and `rsi-value`) and I want you test in the range of values 20 to 40. +Then we have three category variables. First two are either `True` or `False`. +We use these to either enable or disable the ADX and RSI guards. The last +one we call `trigger` and use it to decide which buy trigger we want to use. + +So let's write the buy strategy using these values: + +```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend +``` + +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). + +!!! Note +The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. +When you want to test an indicator that isn't used by the bot currently, remember to +add it to the `populate_indicators()` method in your strategy or hyperopt file. + +## Loss-functions + +Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. + +A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). +This class should be in its own file within the `user_data/hyperopts/` directory. + +Currently, the following loss functions are builtin: + +* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) +* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) +* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) +* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) +* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) + +Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. + +## Execute Hyperopt + +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + +```bash +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +``` + +Use `` as the name of the custom hyperopt used. + +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. + +The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. + +!!! Note +Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. +Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. +You can find a list of filenames with `ls -l user_data/hyperopt_results/`. + +### Execute Hyperopt with different historical data source + +If you would like to hyperopt parameters using an alternate historical data set that +you have on-disk, use the `--datadir PATH` option. By default, hyperopt +uses data from directory `user_data/data`. + +### Running Hyperopt with a smaller test-set + +Use the `--timerange` argument to change how much of the test-set you want to use. +For example, to use one month of data, pass the following parameter to the hyperopt call: + +```bash +freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 +``` + +### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy +``` + +### Running Hyperopt with Smaller Search Space + +Use the `--spaces` option to limit the search space used by hyperopt. +Letting Hyperopt optimize everything is a huuuuge search space. +Often it might make more sense to start by just searching for initial buy algorithm. +Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. + +Legal values are: + +* `all`: optimize everything +* `buy`: just search for a new buy strategy +* `sell`: just search for a new sell strategy +* `roi`: just optimize the minimal profit table for your strategy +* `stoploss`: search for the best stoploss value +* `trailing`: search for the best trailing stop values +* `default`: `all` except `trailing` +* space-separated list of any of the above values for example `--spaces roi stoploss` + +The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. + +### Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previously open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note +Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. + +## Understand the Hyperopt Result + +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +``` + +You should understand this result like: + +- The buy trigger that worked best was `bb_lower`. +- You should not use ADX because `adx-enabled: False`) +- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) + +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. + +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + +```python +(dataframe['rsi'] < 29.0) +``` + +Translating your whole hyperopt result as the new buy-signal would then look like: + +```python +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 29.0) & # rsi-value + dataframe['close'] < dataframe['bb_lowerband'] # trigger + ), + 'buy'] = 1 + return dataframe +``` + +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. + +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. + +!!! Note "Windows and color output" +Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + +### Understand Hyperopt ROI results + +If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +ROI table: +{ 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0} +``` + +In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: + +``` + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + 0: 0.10674, + 21: 0.09158, + 78: 0.03634, + 118: 0 + } +``` + +As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file. + +#### Default ROI Search Space + +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): + +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | +| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | +| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | +| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | + +These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. + +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. + +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). + +A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +### Understand Hyperopt Stoploss results + +If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +Stoploss: -0.27996 +``` + +In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: + +``` python + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.27996 +``` + +As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. + +#### Default Stoploss Search Space + +If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases. + +If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +### Understand Hyperopt Trailing Stop results + +If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: + +``` +Best result: + + 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 + +Trailing stop: +{ 'trailing_only_offset_is_reached': True, + 'trailing_stop': True, + 'trailing_stop_positive': 0.02001, + 'trailing_stop_positive_offset': 0.06038} +``` + +In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: + +``` python + # Trailing stop + # These attributes will be overridden if the config file contains corresponding values. + trailing_stop = True + trailing_stop_positive = 0.02001 + trailing_stop_positive_offset = 0.06038 + trailing_only_offset_is_reached = True +``` + +As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file. + +#### Default Trailing Stop Search Space + +If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. + +Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). + +## Show details of Hyperopt results + +After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. + +## Validate backtesting results + +Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. + +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From faf40482ef9bebf9cbb5469f17b4d136fded3ced Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 3 Apr 2021 13:49:24 +0300 Subject: [PATCH 205/348] Fix parameter printing. --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e58aac273..6282d91c0 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -259,7 +259,7 @@ class HyperStrategyMixin(object): if attr_name in params: if attr.load: attr.value = params[attr_name] - logger.info(f'attr_name = {attr.value}') + logger.info(f'{attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') From 4eb7ce52cd61fcd14f546b72be3b45ee03bcc8a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 15:15:38 +0200 Subject: [PATCH 206/348] Remove duplicate entries from hyperopt_legacy --- docs/hyperopt.md | 47 ++--- docs/hyperopt_legacy.md | 446 ++++------------------------------------ 2 files changed, 62 insertions(+), 431 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e9d440a5a..725afebf3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,21 +1,21 @@ # Hyperopt This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several -algorithms included in the `scikit-optimize` package to accomplish this. The -search will burn all your CPU cores, make your laptop sound like a fighter jet -and still take a long time. +parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this. +The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). -Hyperopt requires historic data to be available, just as backtesting does. +Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters). To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) !!! Note - Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. + The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. + The legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). ## Install hyperopt dependencies @@ -37,7 +37,6 @@ pip install -r requirements-hyperopt.txt ## Hyperopt command reference - ``` usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] @@ -150,7 +149,7 @@ Depending on the space you want to optimize, only some of the below are required * define parameters with `space='sell'` - for sell signal optimization !!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. Rarely you may also need to create a nested class named `HyperOpt` and implement: @@ -299,7 +298,7 @@ Based on the results, hyperopt will tell you which parameter combination produce There are four parameter types each suited for different purposes. * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. -* `RealParameter` - defines a floating point parameter with upper and lower boundarie and no precision limit. Rarely used. +* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. * `CategoricalParameter` - defines a parameter with a predetermined number of choices. !!! Tip "Disabling parameter optimization" @@ -329,7 +328,7 @@ Creation of a custom loss function is covered in the [Advanced Hyperopt](advance ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. We strongly recommend to use `screen` or `tmux` to prevent any connection loss. @@ -365,7 +364,7 @@ freqtrade hyperopt --hyperopt --strategy --timeran ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. +Letting Hyperopt optimize everything is a huuuuge search space. Often it might make more sense to start by just searching for initial buy algorithm. Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. @@ -435,16 +434,16 @@ Best result: You should understand this result like: -- The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `'buy_adx_enabled': False`) -- You should **consider** using the RSI indicator (`'buy_rsi_enabled': True` and the best value is `29.0` (`'buy_rsi': 29.0`) +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `'buy_adx_enabled': False`. +* You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`) -Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste it at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. +Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed. Transferring your whole hyperopt result to your strategy would then look like: ```python -class MyAwsomeStrategy(IStrategy): +class MyAwesomeStrategy(IStrategy): # Buy hyperspace params: buy_params = { 'buy_adx': 44, @@ -455,13 +454,6 @@ class MyAwsomeStrategy(IStrategy): } ``` -By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. - -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. - -!!! Note "Windows and color output" - Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - ### Understand Hyperopt ROI results If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: @@ -588,6 +580,15 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +### Output formatting + +By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. + +You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. + +!!! Note "Windows and color output" + Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md index 8c6972b5f..03c1eb358 100644 --- a/docs/hyperopt_legacy.md +++ b/docs/hyperopt_legacy.md @@ -1,162 +1,29 @@ # Legacy Hyperopt -This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several -algorithms included in the `scikit-optimize` package to accomplish this. The -search will burn all your CPU cores, make your laptop sound like a fighter jet -and still take a long time. +This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). -In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). +!!! Warning "Deprecated / legacy mode" + Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. + Please read the [main hyperopt page](hyperopt.md) for more details. -Hyperopt requires historic data to be available, just as backtesting does. -To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. +## Prepare hyperopt file -!!! Note - Since 2021.4 release you no longer have to write a separate hyperopt class. Legacy method is still supported, but it is no longer a preferred way of hyperopting. Please update your strategy class following new documentation at [Hyperopt](hyperopt.md). - -!!! Bug -Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) - -## Install hyperopt dependencies - -Since Hyperopt dependencies are not needed to run the bot itself, are heavy, can not be easily built on some platforms (like Raspberry PI), they are not installed by default. Before you run Hyperopt, you need to install the corresponding dependencies, as described in this section below. - -!!! Note -Since Hyperopt is a resource intensive process, running it on a Raspberry Pi is not recommended nor supported. - -### Docker - -The docker-image includes hyperopt dependencies, no further action needed. - -### Easy installation script (setup.sh) / Manual installation - -```bash -source .env/bin/activate -pip install -r requirements-hyperopt.txt -``` - -## Hyperopt command reference - - -``` -usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] - [--userdir PATH] [-s NAME] [--strategy-path PATH] - [-i TIMEFRAME] [--timerange TIMERANGE] - [--data-format-ohlcv {json,jsongz,hdf5}] - [--max-open-trades INT] - [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] - [--dry-run-wallet DRY_RUN_WALLET] [-e INT] - [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] - [--print-all] [--no-color] [--print-json] [-j JOBS] - [--random-state INT] [--min-trades INT] - [--hyperopt-loss NAME] - -optional arguments: - -h, --help show this help message and exit - -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). - --timerange TIMERANGE - Specify what timerange of data to use. - --data-format-ohlcv {json,jsongz,hdf5} - Storage format for downloaded candle (OHLCV) data. - (default: `None`). - --max-open-trades INT - Override the value of the `max_open_trades` - configuration setting. - --stake-amount STAKE_AMOUNT - Override the value of the `stake_amount` configuration - setting. - --fee FLOAT Specify fee ratio. Will be applied twice (on trade - entry and exit). - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. - --eps, --enable-position-stacking - Allow buying the same pair multiple times (position - stacking). - --dmmp, --disable-max-market-positions - Disable applying `max_open_trades` during backtest - (same as setting `max_open_trades` to a very high - number). - --enable-protections, --enableprotections - Enable protections for backtesting.Will slow - backtesting down by a considerable amount, but will - include configured protections - --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET - Starting balance, used for backtesting / hyperopt and - dry-runs. - -e INT, --epochs INT Specify number of epochs (default: 100). - --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] - Specify which parameters to hyperopt. Space-separated - list. - --print-all Print all results, not only the best ones. - --no-color Disable colorization of hyperopt results. May be - useful if you are redirecting output to a file. - --print-json Print output in JSON format. - -j JOBS, --job-workers JOBS - The number of concurrently running jobs for - hyperoptimization (hyperopt worker processes). If -1 - (default), all CPUs are used, for -2, all CPUs but one - are used, etc. If 1 is given, no parallel computing - code is used at all. - --random-state INT Set random state to some positive integer for - reproducible hyperopt results. - --min-trades INT Set minimal desired number of trades for evaluations - in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function - class (IHyperOptLoss). Different functions can - generate completely different results, since the - target for optimization is different. Built-in - Hyperopt-loss-functions are: - ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, - SharpeHyperOptLoss, SharpeHyperOptLossDaily, - SortinoHyperOptLoss, SortinoHyperOptLossDaily - -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: - `userdir/config.json` or `config.json` whichever - exists). Multiple --config options may be used. Can be - set to `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - -``` - -## Prepare Hyperopting - -Before we start digging into Hyperopt, we recommend you to take a look at -the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). - -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. +Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. !!! Tip "About this page" -For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. -The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. +### Create a Custom Hyperopt File + +The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: ``` bash freqtrade new-hyperopt --hyperopt AwesomeHyperopt ``` -### Hyperopt checklist +### Legacy Hyperopt checklist Checklist on all tasks / possibilities in hyperopt @@ -168,7 +35,7 @@ Depending on the space you want to optimize, only some of the below are required * fill `sell_indicator_space` - for sell signal optimization !!! Note -`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. Optional in hyperopt - can also be loaded from a strategy (recommended): @@ -177,8 +44,8 @@ Optional in hyperopt - can also be loaded from a strategy (recommended): * `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy !!! Note -You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. -Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. Rarely you may also need to override: @@ -187,67 +54,7 @@ Rarely you may also need to override: * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) -!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" -You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations. - - ```python - # Have a working strategy at hand. - freqtrade new-hyperopt --hyperopt EmptyHyperopt - - freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 - ``` - -### Create a Custom Hyperopt File - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -This command will create a new hyperopt file from a template, allowing you to get started quickly. - -### Configure your Guards and Triggers - -There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: - -* Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. - -There you have two different types of indicators: 1. `guards` and 2. `triggers`. - -1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. -2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". - -!!! Hint "Guards and Triggers" -Technically, there is no difference between Guards and Triggers. -However, this guide will make this distinction to make it clear that signals should not be "sticking". -Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). - -Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if -ADX > 10*". - -If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. - -#### Sell optimization - -Similar to the buy-signal above, sell-signals can also be optimized. -Place the corresponding settings into the following methods - -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. - -The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. - -#### Using timeframe as a part of the Strategy - -The Strategy class exposes the timeframe value as the `self.timeframe` attribute. -The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. - -## Solving a Mystery +### Defining a buy signal optimization Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys. And you also wonder should you use RSI or ADX to @@ -272,13 +79,12 @@ We will start by defining a search space: ``` Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. The last -one we call `trigger` and use it to decide which buy trigger we want to use. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. -So let's write the buy strategy using these values: +So let's write the buy strategy generator using these values: ```python @staticmethod @@ -321,27 +127,20 @@ It will use the given historical data and make buys based on the buy signals gen Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). !!! Note -The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. -When you want to test an indicator that isn't used by the bot currently, remember to -add it to the `populate_indicators()` method in your strategy or hyperopt file. + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + When you want to test an indicator that isn't used by the bot currently, remember to + add it to the `populate_indicators()` method in your strategy or hyperopt file. -## Loss-functions +### Sell optimization -Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods -A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). -This class should be in its own file within the `user_data/hyperopts/` directory. +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. -Currently, the following loss functions are builtin: - -* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. -* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) -* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) -* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) -* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) -* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) - -Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. +The configuration and rules are the same than for buy signals. +To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. ## Execute Hyperopt @@ -362,24 +161,9 @@ Doing multiple runs (executions) with a few 1000 epochs and different random sta The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. !!! Note -Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. -Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. -You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -### Execute Hyperopt with different historical data source - -If you would like to hyperopt parameters using an alternate historical data set that -you have on-disk, use the `--datadir PATH` option. By default, hyperopt -uses data from directory `user_data/data`. - -### Running Hyperopt with a smaller test-set - -Use the `--timerange` argument to change how much of the test-set you want to use. -For example, to use one month of data, pass the following parameter to the hyperopt call: - -```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 -``` + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. ### Running Hyperopt using methods from a strategy @@ -389,57 +173,6 @@ Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_t freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy ``` -### Running Hyperopt with Smaller Search Space - -Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. -Often it might make more sense to start by just searching for initial buy algorithm. -Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. - -Legal values are: - -* `all`: optimize everything -* `buy`: just search for a new buy strategy -* `sell`: just search for a new sell strategy -* `roi`: just optimize the minimal profit table for your strategy -* `stoploss`: search for the best stoploss value -* `trailing`: search for the best trailing stop values -* `default`: `all` except `trailing` -* space-separated list of any of the above values for example `--spaces roi stoploss` - -The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. - -### Position stacking and disabling max market positions - -In some situations, you may need to run Hyperopt (and Backtesting) with the -`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. - -By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one -open trade is allowed for every traded pair. The total number of trades open for all pairs -is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to -some potential trades to be hidden (or masked) by previously open trades. - -The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, -while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high -number). - -!!! Note -Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. - -You can also enable position stacking in the configuration file by explicitly setting -`"position_stacking"=true`. - -### Reproducible results - -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. - -The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. - -If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. - -If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. - ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. @@ -460,9 +193,9 @@ Buy hyperspace params: You should understand this result like: -- The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) -- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `adx-enabled: False`) +* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) You have to look inside your strategy file into `buy_strategy_generator()` method, what those values match to. @@ -486,63 +219,6 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: return dataframe ``` -By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. - -You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. - -!!! Note "Windows and color output" -Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - -### Understand Hyperopt ROI results - -If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -ROI table: -{ 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0} -``` - -In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy: - -``` - # Minimal ROI designed for the strategy. - # This attribute will be overridden if the config file contains "minimal_roi" - minimal_roi = { - 0: 0.10674, - 21: 0.09158, - 78: 0.03634, - 118: 0 - } -``` - -As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file. - -#### Default ROI Search Space - -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): - -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | - -These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. - -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. - -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). - -A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -571,56 +247,10 @@ In order to use this best stoploss value found by Hyperopt in backtesting and fo As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. -#### Default Stoploss Search Space - -If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace vary in the range -0.35...-0.02, which is sufficient in most cases. - -If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. - -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - -### Understand Hyperopt Trailing Stop results - -If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: - -``` -Best result: - - 45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161 - -Trailing stop: -{ 'trailing_only_offset_is_reached': True, - 'trailing_stop': True, - 'trailing_stop_positive': 0.02001, - 'trailing_stop_positive_offset': 0.06038} -``` - -In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: - -``` python - # Trailing stop - # These attributes will be overridden if the config file contains corresponding values. - trailing_stop = True - trailing_stop_positive = 0.02001 - trailing_stop_positive_offset = 0.06038 - trailing_only_offset_is_reached = True -``` - -As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file. - -#### Default Trailing Stop Search Space - -If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. - -Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). - -## Show details of Hyperopt results - -After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. ## Validate backtesting results -Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. +Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. From 32a503491d76fa84f4c4823095b279eff0515bfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 15:41:43 +0200 Subject: [PATCH 207/348] Reorder hyperopt methods --- docs/hyperopt.md | 70 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 725afebf3..806ce3c94 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -355,10 +355,12 @@ uses data from directory `user_data/data`. ### Running Hyperopt with a smaller test-set Use the `--timerange` argument to change how much of the test-set you want to use. -For example, to use one month of data, pass the following parameter to the hyperopt call: +For example, to use one month of data, pass `--timerange 20210101-20210201` (from january 2021 - february 2021) to the hyperopt call. + +Full command: ```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 +freqtrade hyperopt --hyperopt --strategy --timerange 20210101-20210201 ``` ### Running Hyperopt with Smaller Search Space @@ -381,37 +383,6 @@ Legal values are: The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. -### Position stacking and disabling max market positions - -In some situations, you may need to run Hyperopt (and Backtesting) with the -`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. - -By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one -open trade is allowed for every traded pair. The total number of trades open for all pairs -is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to -some potential trades to be hidden (or masked) by previously open trades. - -The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, -while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` -during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high -number). - -!!! Note - Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. - -You can also enable position stacking in the configuration file by explicitly setting -`"position_stacking"=true`. - -### Reproducible results - -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. - -The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. - -If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. - -If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. - ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to update your strategy. @@ -580,7 +551,17 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). -### Output formatting +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used. + +## Output formatting By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line. @@ -589,6 +570,27 @@ You can use the `--print-all` command line option if you would like to see all r !!! Note "Windows and color output" Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. +## Position stacking and disabling max market positions + +In some situations, you may need to run Hyperopt (and Backtesting) with the +`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments. + +By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one +open trade is allowed for every traded pair. The total number of trades open for all pairs +is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to +some potential trades to be hidden (or masked) by previously open trades. + +The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, +while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` +during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high +number). + +!!! Note + Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. + +You can also enable position stacking in the configuration file by explicitly setting +`"position_stacking"=true`. + ## Show details of Hyperopt results After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. From c2d43a526cf5f0e96f016851b2b74eb814c9ac74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:08:08 +0200 Subject: [PATCH 208/348] Combine Legacy and advanced hyperopt sections --- docs/advanced-hyperopt.md | 349 ++++++++++++++++++++++++++++++-------- docs/hyperopt.md | 14 +- docs/hyperopt_legacy.md | 259 ---------------------------- 3 files changed, 282 insertions(+), 340 deletions(-) delete mode 100644 docs/hyperopt_legacy.md diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index bdaafb936..6a559ec96 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -4,79 +4,6 @@ This page explains some advanced Hyperopt topics that may require higher coding skills and Python knowledge than creation of an ordinal hyperoptimization class. -## Derived hyperopt classes - -Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). - -Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace: - -```python -class MyAwesomeHyperOpt(IHyperOpt): - ... - # Uses default stoploss dimension - -class MyAwesomeHyperOpt2(MyAwesomeHyperOpt): - @staticmethod - def stoploss_space() -> List[Dimension]: - # Override boundaries for stoploss - return [ - Real(-0.33, -0.01, name='stoploss'), - ] -``` - -and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case: - -``` -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... -or -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... -``` - -## Sharing methods with your strategy - -Hyperopt classes provide access to the Strategy via the `strategy` class attribute. -This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. - -``` python -from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy -import freqtrade.vendor.qtpylib.indicators as qtpylib - -class MyAwesomeStrategy(IStrategy): - - buy_params = { - 'rsi-value': 30, - 'adx-value': 35, - } - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - return self.buy_strategy_generator(self.buy_params, dataframe, metadata) - - @staticmethod - def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & - dataframe['adx'] > params['adx-value']) & - dataframe['volume'] > 0 - ) - , 'buy'] = 1 - return dataframe - -class MyAwesomeHyperOpt(IHyperOpt): - ... - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - # Call strategy's buy strategy generator - return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) - - return populate_buy_trend -``` - ## Creating and using a custom loss function To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. @@ -142,3 +69,279 @@ This function needs to return a floating point number (`float`). Smaller numbers !!! Note Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. + +## Legacy Hyperopt + +This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). + +!!! Warning "Deprecated / legacy mode" + Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. + Please read the [main hyperopt page](hyperopt.md) for more details. + +### Prepare hyperopt file + +Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. + +!!! Tip "About this page" + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. + +#### Create a Custom Hyperopt File + +The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +Let assume you want a hyperopt file `AwesomeHyperopt.py`: + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +#### Legacy Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt + +Depending on the space you want to optimize, only some of the below are required: + +* fill `buy_strategy_generator` - for buy signal optimization +* fill `indicator_space` - for buy signal optimization +* fill `sell_strategy_generator` - for sell signal optimization +* fill `sell_indicator_space` - for sell signal optimization + +!!! Note + `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. + +Optional in hyperopt - can also be loaded from a strategy (recommended): + +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy + +!!! Note + You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. + +Rarely you may also need to override: + +* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) +* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) +* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) +* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) + +#### Defining a buy signal optimization + +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this +mystery. + +We will start by defining a search space: + +```python + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + Integer(20, 40, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') + ] +``` + +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. +Then we have three category variables. First two are either `True` or `False`. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. + +So let's write the buy strategy generator using these values: + +```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] > params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] < params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend +``` + +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). + +!!! Note + The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. + When you want to test an indicator that isn't used by the bot currently, remember to + add it to the `populate_indicators()` method in your strategy or hyperopt file. + +#### Sell optimization + +Similar to the buy-signal above, sell-signals can also be optimized. +Place the corresponding settings into the following methods + +* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. + +The configuration and rules are the same than for buy signals. +To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. + +### Execute Hyperopt + +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + +```bash +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all +``` + +Use `` as the name of the custom hyperopt used. + +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. + +The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. + +!!! Note + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. + +#### Running Hyperopt using methods from a strategy + +Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. + +```bash +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy +``` + +### Understand the Hyperopt Result + +Once Hyperopt is completed you can use the result to create a new strategy. +Given the following result from hyperopt: + +``` +Best result: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: +{ 'adx-value': 44, + 'rsi-value': 29, + 'adx-enabled': False, + 'rsi-enabled': True, + 'trigger': 'bb_lower'} +``` + +You should understand this result like: + +* The buy trigger that worked best was `bb_lower`. +* You should not use ADX because `adx-enabled: False`) +* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) + +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. + +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + +```python +(dataframe['rsi'] < 29.0) +``` + +Translating your whole hyperopt result as the new buy-signal would then look like: + +```python +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 29.0) & # rsi-value + dataframe['close'] < dataframe['bb_lowerband'] # trigger + ), + 'buy'] = 1 + return dataframe +``` + +### Validate backtesting results + +Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. + +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). + +### Sharing methods with your strategy + +Hyperopt classes provide access to the Strategy via the `strategy` class attribute. +This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. + +``` python +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + + buy_params = { + 'rsi-value': 30, + 'adx-value': 35, + } + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + return self.buy_strategy_generator(self.buy_params, dataframe, metadata) + + @staticmethod + def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & + dataframe['adx'] > params['adx-value']) & + dataframe['volume'] > 0 + ) + , 'buy'] = 1 + return dataframe + +class MyAwesomeHyperOpt(IHyperOpt): + ... + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + # Call strategy's buy strategy generator + return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) + + return populate_buy_trend +``` diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 806ce3c94..cb8d4ad9d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -15,7 +15,7 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Note Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. - The legacy documentation is available at [Legacy Hyperopt](hyperopt_legacy.md). + The legacy documentation is available at [Legacy Hyperopt](advanced-hyperopt.md#legacy-hyperopt). ## Install hyperopt dependencies @@ -247,12 +247,11 @@ class MyAwesomeStrategy(IStrategy): buy_trigger = CategoricalParameter(['bb_lower', 'macd_cross_signal']), ``` -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`buy_adx` -and `buy_rsi`) and I want you test in the range of values 20 to 40. +Above definition says: I have five parameters I want to randomly combine to find the best combination. +Two of them are integer values (`buy_adx` and `buy_rsi`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. The last -one we call `trigger` and use it to decide which buy trigger we want to use. +We use these to either enable or disable the ADX and RSI guards. +The last one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: @@ -349,8 +348,7 @@ The `--spaces all` option determines that all possible parameters should be opti ### Execute Hyperopt with different historical data source If you would like to hyperopt parameters using an alternate historical data set that -you have on-disk, use the `--datadir PATH` option. By default, hyperopt -uses data from directory `user_data/data`. +you have on-disk, use the `--datadir PATH` option. By default, hyperopt uses data from directory `user_data/data`. ### Running Hyperopt with a smaller test-set diff --git a/docs/hyperopt_legacy.md b/docs/hyperopt_legacy.md deleted file mode 100644 index 03c1eb358..000000000 --- a/docs/hyperopt_legacy.md +++ /dev/null @@ -1,259 +0,0 @@ -# Legacy Hyperopt - -This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). - -!!! Warning "Deprecated / legacy mode" - Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. - Please read the [main hyperopt page](hyperopt.md) for more details. - -## Prepare hyperopt file - -Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -### Create a Custom Hyperopt File - -The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -### Legacy Hyperopt checklist - -Checklist on all tasks / possibilities in hyperopt - -Depending on the space you want to optimize, only some of the below are required: - -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization - -!!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. - -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `populate_indicators` - fallback to create indicators -* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy -* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy - -!!! Note - You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: - -* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) -* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) -* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) -* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) - -### Defining a buy signal optimization - -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining a search space: - -```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] -``` - -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. -Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. -The last one we call `trigger` and use it to decide which buy trigger we want to use. - -So let's write the buy strategy generator using these values: - -```python - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend -``` - -Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. -Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). - -!!! Note - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. - When you want to test an indicator that isn't used by the bot currently, remember to - add it to the `populate_indicators()` method in your strategy or hyperopt file. - -### Sell optimization - -Similar to the buy-signal above, sell-signals can also be optimized. -Place the corresponding settings into the following methods - -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. - -The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. - -## Execute Hyperopt - -Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. - -We strongly recommend to use `screen` or `tmux` to prevent any connection loss. - -```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all -``` - -Use `` as the name of the custom hyperopt used. - -The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. - -The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. - -!!! Note - Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. - Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. - You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - -## Understand the Hyperopt Result - -Once Hyperopt is completed you can use the result to create a new strategy. -Given the following result from hyperopt: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -``` - -You should understand this result like: - -* The buy trigger that worked best was `bb_lower`. -* You should not use ADX because `adx-enabled: False`) -* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) - -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. - -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: - -```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe -``` - -### Understand Hyperopt Stoploss results - -If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -Stoploss: -0.27996 -``` - -In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: - -``` python - # Optimal stoploss designed for the strategy - # This attribute will be overridden if the config file contains "stoploss" - stoploss = -0.27996 -``` - -As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file. - - -## Validate backtesting results - -Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. - -To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. - -Should results don't match, please double-check to make sure you transferred all conditions correctly. -Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. -You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From 093d6ce8af24bdf37a14b0505f23dd414d40682f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:13:49 +0200 Subject: [PATCH 209/348] Add sample for Nested space --- docs/advanced-hyperopt.md | 12 ++++++++++++ docs/hyperopt.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 6a559ec96..723163b2c 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -70,6 +70,18 @@ This function needs to return a floating point number (`float`). Smaller numbers !!! Note Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. +## Overriding pre-defined spaces + +To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows: + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + # Define a custom stoploss space. + def stoploss_space(self): + return [Real(-0.05, -0.01, name='stoploss')] +``` + ## Legacy Hyperopt This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index cb8d4ad9d..1ce1f9a86 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -151,7 +151,7 @@ Depending on the space you want to optimize, only some of the below are required !!! Note `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. -Rarely you may also need to create a nested class named `HyperOpt` and implement: +Rarely you may also need to create a [nested class](advanced-hyperopt.md#overriding-pre-defined-spaces) named `HyperOpt` and implement * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) From 771fc057494a55902624e633d9ff8066eb1da323 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:32:16 +0200 Subject: [PATCH 210/348] Update sample strategy with hyperoptable Parameters --- freqtrade/templates/base_strategy.py.j2 | 4 +++- freqtrade/templates/sample_strategy.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index dd6b773e1..9d69ee520 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 # --- Do not remove these libs --- import numpy as np # noqa @@ -6,6 +7,7 @@ import pandas as pd # noqa from pandas import DataFrame from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter # -------------------------------- # Add your lib to import here @@ -16,7 +18,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class {{ strategy }}(IStrategy): """ This is a strategy template to get you started. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + More information in https://www.freqtrade.io/en/latest/strategy-customization/ You can: :return: a Dataframe with all mandatory indicators for the strategies diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index db1ba48b8..84f3fbc9e 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,11 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 # isort: skip_file # --- Do not remove these libs --- import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy +from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter # -------------------------------- # Add your lib to import here @@ -52,7 +54,11 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. + # Hyperoptable parameters + buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, defualt=70, space='buy', optimize=True, load=True) + + # Optimal timeframe for the strategy. timeframe = '5m' # Run "populate_indicators()" only for new candle. @@ -339,7 +345,8 @@ class SampleStrategy(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 @@ -357,7 +364,8 @@ class SampleStrategy(IStrategy): """ dataframe.loc[ ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + # Signal: RSI crosses above 70 + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 From 6555454bd287a230de64e9686a09f6bf70fd36fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 16:54:47 +0200 Subject: [PATCH 211/348] Remove more ticker_interval occurances --- docs/backtesting.md | 3 +-- docs/edge.md | 3 +-- docs/hyperopt.md | 3 +-- docs/plotting.md | 6 ++---- docs/utils.md | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/commands/list_commands.py | 2 +- freqtrade/optimize/hyperopt_interface.py | 6 +++--- freqtrade/templates/sample_strategy.py | 2 +- tests/optimize/test_backtest_detail.py | 2 +- tests/strategy/strats/default_strategy.py | 2 +- tests/strategy/strats/legacy_strategy.py | 2 +- 12 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index d02c59f05..c8acfdbe1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -23,8 +23,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} diff --git a/docs/edge.md b/docs/edge.md index 0aa76cd12..7f0a9cb2d 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -221,8 +221,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --max-open-trades INT diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 96c7354b9..7ae06660b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -53,8 +53,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} diff --git a/docs/plotting.md b/docs/plotting.md index d7ed5ab1f..63afa16b6 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -66,8 +66,7 @@ optional arguments: --timerange TIMERANGE Specify what timerange of data to use. -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --no-trades Skip using trades from backtesting file and DB. Common arguments: @@ -264,8 +263,7 @@ optional arguments: Specify the source for trades (Can be DB or file (backtest file)) Default: file -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/utils.md b/docs/utils.md index cf7d5f1d1..a84f068e9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -264,7 +264,7 @@ All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpr ## List Timeframes -Use the `list-timeframes` subcommand to see the list of timeframes (ticker intervals) available for the exchange. +Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. ``` usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..12c03d824 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -118,7 +118,7 @@ AVAILABLE_CLI_OPTIONS = { # Optimize common "timeframe": Arg( '-i', '--timeframe', '--ticker-interval', - help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', + help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).', ), "timerange": Arg( '--timerange', diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 5f53fc824..d509bfaa5 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -99,7 +99,7 @@ def start_list_hyperopts(args: Dict[str, Any]) -> None: def start_list_timeframes(args: Dict[str, Any]) -> None: """ - Print ticker intervals (timeframes) available on Exchange + Print timeframes available on Exchange """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Do not use timeframe set in the config diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..a9bbc021c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -31,7 +31,7 @@ class IHyperOpt(ABC): Defines the mandatory structure must follow any custom hyperopt Class attributes you can use: - ticker_interval -> int: value of the ticker interval to use for the strategy + timeframe -> int: value of the timeframe to use for the strategy """ ticker_interval: str # DEPRECATED timeframe: str @@ -97,7 +97,7 @@ class IHyperOpt(ABC): This method implements adaptive roi hyperspace with varied ranges for parameters which automatically adapts to the - ticker interval used. + timeframe used. It's used by Freqtrade by default, if no custom roi_space method is defined. """ @@ -119,7 +119,7 @@ class IHyperOpt(ABC): # * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically. # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() - # method for the 5m ticker interval. + # method for the 5m timeframe. roi_t_scale = timeframe_min / 5 roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_limits = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 5dfa42bcc..904597d21 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -53,7 +53,7 @@ class SampleStrategy(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. + # Optimal timeframe for the strategy. timeframe = '5m' # Run "populate_indicators()" only for new candle. diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 0ba6f4a7f..3655b941d 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -268,7 +268,7 @@ tc16 = BTContainer(data=[ # Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) -# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval. +# Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe. tc17 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index 98842ff7c..7171b93ae 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -28,7 +28,7 @@ class DefaultStrategy(IStrategy): # Optimal stoploss designed for the strategy stoploss = -0.10 - # Optimal ticker interval for the strategy + # Optimal timeframe for the strategy timeframe = '5m' # Optional order type mapping diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 1e7bb5e1e..9ef00b110 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -31,7 +31,7 @@ class TestStrategyLegacy(IStrategy): # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.10 - # Optimal ticker interval for the strategy + # Optimal timeframe for the strategy # Keep the legacy value here to test compatibility ticker_interval = '5m' From 5f6eae52a296a83b6426bb6c8f0104bc59577c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 19:12:36 +0200 Subject: [PATCH 212/348] fix too long performance message closes #4655 --- freqtrade/rpc/telegram.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 92899d67f..b418c3dab 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -695,14 +695,18 @@ class Telegram(RPCHandler): """ try: trades = self._rpc._rpc_performance() - stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( - index=i + 1, - pair=trade['pair'], - profit=trade['profit'], - count=trade['count'] - ) for i, trade in enumerate(trades)) - message = 'Performance:\n{}'.format(stats) - self._send_msg(message, parse_mode=ParseMode.HTML) + output = "Performance:\n" + for i, trade in enumerate(trades): + stat_line = (f"{i+1}.\t {trade['pair']}\t{trade['profit']:.2f}% " + f"({trade['count']})\n") + + if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output) + output = stat_line + else: + output += stat_line + + self._send_msg(output, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) From 9d4b5cc6bb961125df6a5afa94ed62848537f010 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 17:10:39 +0200 Subject: [PATCH 213/348] Fix typo --- freqtrade/optimize/hyperopt_interface.py | 2 +- freqtrade/templates/sample_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 8eefff99c..633c8bdd5 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -103,7 +103,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_min = timeframe_to_minutes(self.ticker_interval) + timeframe_min = timeframe_to_minutes(self.timeframe) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 29b550ea4..a51b30f3f 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -57,7 +57,7 @@ class SampleStrategy(IStrategy): # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) - sell_rsi = IntParameter(low=50, high=100, defualt=70, space='buy', optimize=True, load=True) + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) # Optimal timeframe for the strategy. timeframe = '5m' From 30e5e9296817f54ca773772a124548fd290c9d9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Apr 2021 20:17:48 +0200 Subject: [PATCH 214/348] Don't allow one parmeter to be in 2 spaces use the explicit user wish (given explicitly with "space") --- freqtrade/strategy/hyper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 6282d91c0..e7f31e20d 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -244,8 +244,8 @@ class HyperStrategyMixin(object): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): - if category is None or category == attr.category or \ - attr_name.startswith(category + '_'): + if (category is None or category == attr.category or + (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr def _load_params(self, params: dict) -> None: From 9e56f6d4ebba51b07b6facb81da588306741d488 Mon Sep 17 00:00:00 2001 From: rextea Date: Sun, 4 Apr 2021 01:19:38 +0300 Subject: [PATCH 215/348] Sort pair lists by total profit --- freqtrade/optimize/optimize_reports.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 099976aa9..a80dc5d31 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -110,6 +110,9 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b tabular_data.append(_generate_result_line(result, starting_balance, pair)) + # Sort by total profit %: + tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) + # Append Total tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data From c2be9b971c89795adcbdc98bd2aee2ae8a039c5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Apr 2021 07:02:59 +0200 Subject: [PATCH 216/348] Improve backtest assumptions with fill rules --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index c8acfdbe1..e16225f94 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -420,6 +420,7 @@ It contains some useful key metrics about performance of your strategy on backte Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - Buys happen at open-price +- All orders are filled at the requested price (no slippage, no unfilled orders) - Sell-signal sells happen at open-price of the consecutive candle - Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - ROI From 342f14472c1e6fb4082218424d865cb1c97675d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:26:45 +0000 Subject: [PATCH 217/348] Bump mkdocs-material from 7.0.7 to 7.1.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.0.7 to 7.1.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.0.7...7.1.0) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 711b6ca46..3dbaea111 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.0.7 +mkdocs-material==7.1.0 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From fc2f9fd0c77cf0791952e42f39cd25e54f042cbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:16 +0000 Subject: [PATCH 218/348] Bump ccxt from 1.45.44 to 1.46.38 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.45.44 to 1.46.38. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.45.44...1.46.38) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..5177ae5d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.45.44 +ccxt==1.46.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From 320172a224172582a0156cda6ccd7e4f0fad69c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:26 +0000 Subject: [PATCH 219/348] Bump sqlalchemy from 1.4.3 to 1.4.5 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.3 to 1.4.5. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..9b3638789 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.45.44 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.3 +SQLAlchemy==1.4.5 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 36b39f91365400f011410f0ac2830b456040b1bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:31 +0000 Subject: [PATCH 220/348] Bump pycoingecko from 1.4.0 to 1.4.1 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.4.0...1.4.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4984cf47..964701830 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonschema==3.2.0 TA-Lib==0.4.19 technical==1.2.2 tabulate==0.8.9 -pycoingecko==1.4.0 +pycoingecko==1.4.1 jinja2==2.11.3 tables==3.6.1 blosc==1.10.2 From abbc56c1ccae81cd32e013f698f1eb60b601deaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:27:44 +0000 Subject: [PATCH 221/348] Bump pytest from 6.2.2 to 6.2.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.2...6.2.3) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 02f7fbca8..cd93f2433 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.9.0 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 -pytest==6.2.2 +pytest==6.2.3 pytest-asyncio==0.14.0 pytest-cov==2.11.1 pytest-mock==3.5.1 From 0407bf755f157cf812b718faea5ac8338ae9787d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 07:28:51 +0200 Subject: [PATCH 222/348] Use .query.session to make sure the scoped session is used properly --- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/persistence/models.py | 21 +++++++++----------- freqtrade/persistence/pairlock_middleware.py | 6 +++--- freqtrade/rpc/rpc.py | 6 +++--- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dd6966848..a701e8db9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -187,7 +187,7 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() - Trade.session.flush() + Trade.query.session.flush() def process_stopped(self) -> None: """ @@ -621,8 +621,8 @@ class FreqtradeBot(LoggingMixin): if order_status == 'closed': self.update_trade_state(trade, order_id, order) - Trade.session.add(trade) - Trade.session.flush() + Trade.query.session.add(trade) + Trade.query.session.flush() # Updating wallets self.wallets.update() @@ -1205,7 +1205,7 @@ class FreqtradeBot(LoggingMixin): # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) - Trade.session.flush() + Trade.query.session.flush() # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 465f3d443..a82c047c3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -61,11 +61,8 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # We should use the scoped_session object - not a seperately initialized version Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.query = Trade.session.query_property() - # Copy session attributes to order object too - Order.session = Trade.session - Order.query = Order.session.query_property() - PairLock.session = Trade.session - PairLock.query = PairLock.session.query_property() + Order.query = Trade.session.query_property() + PairLock.query = Trade.session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) @@ -81,7 +78,7 @@ def cleanup_db() -> None: Flushes all pending operations to disk. :return: None """ - Trade.session.flush() + Trade.query.session.flush() def clean_dry_run_db() -> None: @@ -677,7 +674,7 @@ class LocalTrade(): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.session.query( + total_open_stake_amount = Trade.query.with_entities( func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() else: total_open_stake_amount = sum( @@ -689,7 +686,7 @@ class LocalTrade(): """ Returns List of dicts containing all Trades, including profit and trade count """ - pair_rates = Trade.session.query( + pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.count(Trade.pair).label('count') @@ -712,7 +709,7 @@ class LocalTrade(): Get best pair with closed trade. :returns: Tuple containing (pair, profit_sum) """ - best_pair = Trade.session.query( + best_pair = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum') ).filter(Trade.is_open.is_(False)) \ .group_by(Trade.pair) \ @@ -805,10 +802,10 @@ class Trade(_DECL_BASE, LocalTrade): def delete(self) -> None: for order in self.orders: - Order.session.delete(order) + Order.query.session.delete(order) - Trade.session.delete(self) - Trade.session.flush() + Trade.query.session.delete(self) + Trade.query.session.flush() @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index f0048bb52..245f7cdab 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -48,8 +48,8 @@ class PairLocks(): active=True ) if PairLocks.use_db: - PairLock.session.add(lock) - PairLock.session.flush() + PairLock.query.session.add(lock) + PairLock.query.session.flush() else: PairLocks.locks.append(lock) @@ -99,7 +99,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock.session.flush() + PairLock.query.session.flush() @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1359729b9..59758a573 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -558,7 +558,7 @@ class RPC: # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) - Trade.session.flush() + Trade.query.session.flush() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} @@ -571,7 +571,7 @@ class RPC: raise RPCException('invalid argument') _exec_forcesell(trade) - Trade.session.flush() + Trade.query.session.flush() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} @@ -696,7 +696,7 @@ class RPC: lock.lock_end_time = datetime.now(timezone.utc) # session is always the same - PairLock.session.flush() + PairLock.query.session.flush() return self._rpc_locks() From ea0b47a7f9c1625e6ec651d656688e19bff41781 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 07:38:07 +0200 Subject: [PATCH 223/348] Replace test occurances of Trade.session with Trade.query.session --- tests/conftest.py | 2 +- tests/plugins/test_protections.py | 42 +++++++++++++++---------------- tests/rpc/test_rpc_apiserver.py | 12 ++++----- tests/test_freqtradebot.py | 35 +++++++++++--------------- tests/test_integration.py | 1 - tests/test_persistence.py | 8 +++--- 6 files changed, 46 insertions(+), 54 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3522ef02d..f8c1c5357 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -197,7 +197,7 @@ def create_mock_trades(fee, use_db: bool = True): """ def add_trade(trade): if use_db: - Trade.session.add(trade) + Trade.query.session.add(trade) else: LocalTrade.add_bt_trade(trade) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2e42c1be4..545387eaa 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -91,7 +91,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, )) @@ -100,12 +100,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() # This trade does not count, as it's closed too long ago - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'BCH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=250, min_ago_close=100, )) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=240, min_ago_close=30, )) @@ -114,7 +114,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'LTC/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=180, min_ago_close=30, )) @@ -148,7 +148,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, profit_rate=0.9, )) @@ -158,12 +158,12 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair assert not log_has_re(message, caplog) caplog.clear() # This trade does not count, as it's closed too long ago - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=250, min_ago_close=100, profit_rate=0.9, )) # Trade does not count for per pair stop as it's the wrong pair. - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=240, min_ago_close=30, profit_rate=0.9, )) @@ -178,7 +178,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair caplog.clear() # 2nd Trade that counts with correct pair - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=180, min_ago_close=30, profit_rate=0.9, )) @@ -203,7 +203,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=30, )) @@ -213,7 +213,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=205, min_ago_close=35, )) @@ -242,7 +242,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) @@ -253,7 +253,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) @@ -265,14 +265,14 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() # Add positive trade - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=1.15, )) assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC') - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=110, min_ago_close=20, profit_rate=0.8, )) @@ -300,15 +300,15 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.stop_per_pair('XRP/BTC') caplog.clear() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'NEO/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) @@ -316,7 +316,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=500, min_ago_close=400, profit_rate=0.9, )) @@ -326,7 +326,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, )) @@ -339,7 +339,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): assert not log_has_re(message, caplog) # Winning trade ... (should not lock, does not change drawdown!) - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=320, min_ago_close=410, profit_rate=1.5, )) @@ -349,7 +349,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): caplog.clear() # Add additional negative trade, causing a loss of > 15% - Trade.session.add(generate_mock_trade( + Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=0.8, )) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 20d32024f..180eefa08 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -510,7 +510,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 0 create_mock_trades(fee) - Trade.session.flush() + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) @@ -538,7 +538,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): assert_response(rc, 502) create_mock_trades(fee) - Trade.session.flush() + Trade.query.session.flush() ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() trades[1].stoploss_order_id = '1234' @@ -720,7 +720,7 @@ def test_api_performance(botclient, mocker, ticker, fee): ) trade.close_profit = trade.calc_profit_ratio() - Trade.session.add(trade) + Trade.query.session.add(trade) trade = Trade( pair='XRP/ETH', @@ -735,8 +735,8 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.391 ) trade.close_profit = trade.calc_profit_ratio() - Trade.session.add(trade) - Trade.session.flush() + Trade.query.session.add(trade) + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) @@ -764,7 +764,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): trades = Trade.get_open_trades() trades[0].open_order_id = None ftbot.exit_positions(trades) - Trade.session.flush() + Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 486c31090..c93f8b858 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -768,7 +768,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, assert pair not in default_conf['exchange']['pair_whitelist'] # create open trade not in whitelist - Trade.session.add(Trade( + Trade.query.session.add(Trade( pair=pair, stake_amount=0.001, fee_open=fee.return_value, @@ -778,7 +778,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, open_rate=0.01, exchange='bittrex', )) - Trade.session.add(Trade( + Trade.query.session.add(Trade( pair='ETH/BTC', stake_amount=0.001, fee_open=fee.return_value, @@ -1779,7 +1779,6 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) - Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf) trade = Trade( @@ -1805,7 +1804,6 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) - Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf) trade = Trade( @@ -1868,7 +1866,6 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) patch_exchange(mocker) - Trade.session = MagicMock() amount = limit_sell_order["amount"] freqtrade = get_patched_freqtradebot(mocker, default_conf) wallet_mock.reset_mock() @@ -2110,7 +2107,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # Ensure default is to return empty (so not mocked yet) freqtrade.check_handle_timedout() @@ -2161,7 +2158,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) # check it does cancel buy orders over the time limit @@ -2191,7 +2188,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() @@ -2218,7 +2215,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() @@ -2248,7 +2245,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ open_trade.close_profit_abs = 0.001 open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # Ensure default is false freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 @@ -2296,7 +2293,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, open_trade.close_profit_abs = 0.001 open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) # check it does cancel sell orders over the time limit @@ -2327,7 +2324,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade.is_open = False - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() @@ -2353,7 +2350,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit # note this is for a partially-complete buy order @@ -2386,7 +2383,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. freqtrade.check_handle_timedout() @@ -2426,7 +2423,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. freqtrade.check_handle_timedout() @@ -2463,7 +2460,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke ) freqtrade = FreqtradeBot(default_conf) - Trade.session.add(open_trade) + Trade.query.session.add(open_trade) freqtrade.check_handle_timedout() assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, " @@ -2486,7 +2483,6 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non freqtrade = FreqtradeBot(default_conf) freqtrade._notify_buy_cancel = MagicMock() - Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['filled'] = 0.0 @@ -2520,7 +2516,6 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel') freqtrade = FreqtradeBot(default_conf) - Trade.session = MagicMock() reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' @@ -2549,7 +2544,6 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) freqtrade._notify_buy_cancel = MagicMock() - Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['filled'] = 0.0 @@ -2812,7 +2806,6 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c freqtrade.enter_positions() trade = Trade.query.first() - Trade.session = MagicMock() PairLock.session = MagicMock() freqtrade.config['dry_run'] = False @@ -4422,7 +4415,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): open_rate=0.01, exchange='bittrex', ) - Trade.session.add(trade) + Trade.query.session.add(trade) freqtrade.reupdate_buy_order_fees(trade) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index 8e3bd251a..1c60faa7b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -89,7 +89,6 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, freqtrade.strategy.confirm_trade_entry.reset_mock() assert freqtrade.strategy.confirm_trade_exit.call_count == 0 wallets_mock.reset_mock() - Trade.session = MagicMock() trades = Trade.query.all() # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6a388327c..d53386287 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -403,7 +403,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='dry_run_buy_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) trade = Trade( pair='ETC/BTC', @@ -415,7 +415,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='dry_run_sell_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) # Simulate prod entry trade = Trade( @@ -428,7 +428,7 @@ def test_clean_dry_run_db(default_conf, fee): exchange='bittrex', open_order_id='prod_buy_12345' ) - Trade.session.add(trade) + Trade.query.session.add(trade) # We have 3 entries: 2 dry_run, 1 prod assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3 @@ -933,7 +933,7 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 - Trade.session.add(trade) + Trade.query.session.add(trade) # Lower stoploss Trade.stoploss_reinitialization(0.06) From e979f132e3933effcfd2e2fb0d742b1c559883a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Apr 2021 05:57:56 +0000 Subject: [PATCH 224/348] Bump python from 3.9.2-slim-buster to 3.9.3-slim-buster Bumps python from 3.9.2-slim-buster to 3.9.3-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b399174b..1f6e36b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.2-slim-buster as base +FROM python:3.9.3-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index eecd9fdc0..5e6763d4c 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.3-slim-buster as base # Setup env ENV LANG C.UTF-8 From af525818136efd4848a5b6e6e6d6c70653310d53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 08:22:01 +0200 Subject: [PATCH 225/348] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5e6763d4c..dcb008cb9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.3-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From 7132aefd608ab8495932f438bc66695d18d4d70f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 08:46:12 +0200 Subject: [PATCH 226/348] Rename Trade.session to Trade._session --- freqtrade/persistence/models.py | 8 ++++---- tests/test_persistence.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a82c047c3..a22e75e1e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -59,10 +59,10 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) - Trade.query = Trade.session.query_property() - Order.query = Trade.session.query_property() - PairLock.query = Trade.session.query_property() + Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) + Trade.query = Trade._session.query_property() + Order.query = Trade._session.query_property() + PairLock.query = Trade._session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d53386287..3336e4e66 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -18,8 +18,8 @@ from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url'], default_conf['dry_run']) - assert hasattr(Trade, 'session') - assert 'scoped_session' in type(Trade.session).__name__ + assert hasattr(Trade, '_session') + assert 'scoped_session' in type(Trade._session).__name__ def test_init_custom_db_url(default_conf, tmpdir): From dc406fe19f85197d9223342c7777a466caabb480 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 10:53:00 +0200 Subject: [PATCH 227/348] Fail in case of name and explicit space name collisions --- freqtrade/strategy/hyper.py | 4 ++++ tests/optimize/test_hyperopt.py | 2 -- tests/strategy/test_interface.py | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index e7f31e20d..0b7055f01 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -244,6 +244,10 @@ class HyperStrategyMixin(object): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): + if (category and attr_name.startswith(category + '_') + and attr.category is not None and attr.category != category): + raise OperationalException( + f'Inconclusive parameter name {attr_name}, category: {attr.category}.') if (category is None or category == attr.category or (attr_name.startswith(category + '_') and attr.category is None)): yield attr_name, attr diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 36b6f1229..c13da0d76 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1093,8 +1093,6 @@ def test_print_epoch_details(capsys): def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: - # mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) - # mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed del hyperopt_conf['hyperopt'] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 71f877cc3..3bfa691b4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -617,3 +617,8 @@ def test_auto_hyperopt_interface(default_conf): # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 + + strategy.sell_rsi = IntParameter([0, 10], default=5, space='buy') + + with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): + [x for x in strategy.enumerate_parameters('sell')] From c51839dc3be4c47a32394719eb3bfba0e94ff1a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 11:21:20 +0200 Subject: [PATCH 228/348] Make the logmessage for loaded parameters clearer --- freqtrade/strategy/hyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0b7055f01..709179997 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -263,7 +263,7 @@ class HyperStrategyMixin(object): if attr_name in params: if attr.load: attr.value = params[attr_name] - logger.info(f'{attr_name} = {attr.value}') + logger.info(f'Strategy Parameter: {attr_name} = {attr.value}') else: logger.warning(f'Parameter "{attr_name}" exists, but is disabled. ' f'Default value "{attr.value}" used.') From 3044aa18e60bb5d42d880d67b27a7bea0f937653 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 14:45:42 +0200 Subject: [PATCH 229/348] Add warning for hyperopt-parameters --- docs/hyperopt.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index db7a23f02..07cc963cf 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -305,6 +305,9 @@ There are four parameter types each suited for different purposes. * `optimize` - when set to `False` parameter will not be included in optimization process. Use these parameters to quickly prototype various ideas. +!!! Warning + Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. From 7b2a0d46cb19cb4c219d33e4ec1fc9cfac887a40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 15:38:25 +0200 Subject: [PATCH 230/348] Fix typo --- freqtrade/optimize/hyperopt_auto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index ed6f2d6f7..c4d6f1581 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -33,14 +33,14 @@ class HyperOptAuto(IHyperOpt): return populate_buy_trend def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - def populate_buy_trend(dataframe: DataFrame, metadata: dict): + def populate_sell_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) - return populate_buy_trend + return populate_sell_trend def _get_func(self, name) -> Callable: """ From 78a84f8081f8cd58f0ab7a74f2f40c9106dd615a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Apr 2021 15:38:33 +0200 Subject: [PATCH 231/348] Allow --hyperoptloss in addition to --hyperopt-loss --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index cea353109..4fac8ac72 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -267,7 +267,7 @@ AVAILABLE_CLI_OPTIONS = { default=1, ), "hyperopt_loss": Arg( - '--hyperopt-loss', + '--hyperopt-loss', '--hyperoptloss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' From c176e277f117badcbe495750707af79e07717b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:31:34 +0200 Subject: [PATCH 232/348] Add a REST endpoint for getting a specific trade --- freqtrade/rpc/api_server/api_v1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..6873c0c4c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -88,6 +88,11 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) +@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) +def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_status([tradeid])[0] + + @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete(tradeid) From ddba0d688e0b168054fda58b0267810ff13fad98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:32:55 +0200 Subject: [PATCH 233/348] Add new trade endpoint to docs --- docs/rest-api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index c41c3f24c..4e784b6af 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -125,6 +125,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. | `trades` | List last trades. +| `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. | `logs` | Shows last log messages. @@ -275,6 +276,10 @@ trades :param limit: Limits trades to the X last trades. No limit to get all the trades. +trade + Return specific trade. + :param tradeid: Specify which trade to get. + version Return the version of the bot. From fc78246bbc2a7f4260e1bf2bdd3d898c52c98110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:34:01 +0200 Subject: [PATCH 234/348] Some changes to rest-api docs --- docs/rest-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 4e784b6af..be3107fcb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -182,7 +182,7 @@ count Return the amount of open trades. daily - Return the amount of open trades. + Return the profits for each day, and amount of trades. delete_lock Delete (disable) lock from the database. @@ -215,7 +215,7 @@ locks logs Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. pair_candles Return live dataframe for . From 6633752fcb743f693105c9c56633c82817593592 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 05:29:34 +0000 Subject: [PATCH 235/348] Bump python from 3.9.3-slim-buster to 3.9.4-slim-buster Bumps python from 3.9.3-slim-buster to 3.9.4-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1f6e36b68..711f27990 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.3-slim-buster as base +FROM python:3.9.4-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/Dockerfile.armhf b/Dockerfile.armhf index dcb008cb9..b019b6096 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base +FROM --platform=linux/arm/v7 python:3.9.4-slim-buster as base # Setup env ENV LANG C.UTF-8 From 0550f261f1f48626c112ef830485ac5a843f9548 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:47:44 +0200 Subject: [PATCH 236/348] Add exchange_has validation --- freqtrade/commands/list_commands.py | 14 +++++++++----- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/common.py | 23 +++++++++++++++++++++++ freqtrade/exchange/exchange.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index d509bfaa5..fa4bc1066 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -13,7 +13,7 @@ from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException -from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active +from freqtrade.exchange import market_is_active, validate_exchanges from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -28,14 +28,18 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: :param args: Cli args from Arguments() :return: None """ - exchanges = ccxt_exchanges() if args['list_exchanges_all'] else available_exchanges() + exchanges = validate_exchanges(args['list_exchanges_all']) + if args['print_one_column']: - print('\n'.join(exchanges)) + print('\n'.join([e[0] for e in exchanges])) else: if args['list_exchanges_all']: - print(f"All exchanges supported by the ccxt library: {', '.join(exchanges)}") + print("All exchanges supported by the ccxt library:") else: - print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}") + print("Exchanges available for Freqtrade:") + exchanges = [e for e in exchanges if e[1] is not False] + + print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason'])) def _print_objs_tabular(objs: List, print_colorized: bool) -> None: diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 15ba7b9f6..0eedd25d8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,6 +12,6 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds) + timeframe_to_seconds, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index be0a1e483..90b70d67e 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -98,6 +98,29 @@ MAP_EXCHANGE_CHILDCLASS = { } +EXCHANGE_HAS_REQUIRED = [ + # Required / private + 'fetchOrder', + 'cancelOrder', + 'createOrder', + # 'createLimitOrder', 'createMarketOrder', + 'fetchBalance', + + # Public endpoints + 'loadMarkets', + 'fetchOHLCV', +] + +EXCHANGE_HAS_OPTIONAL = [ + # Private + 'fetchMyTrades', # Trades for order - fee detection + # Public + 'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker', # OR for pricing + 'fetchTickers', # For volumepairlist? + 'fetchTrades', # Downloading trades data +] + + def calculate_backoff(retrycount, max_retries): """ Calculate backoff diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 85c5b4c6d..768a02aa1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,7 +23,8 @@ from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, + EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -1337,6 +1338,32 @@ def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: return [x for x in exchanges if not is_exchange_bad(x)] +def validate_exchange(exchange: str) -> Tuple[bool, str]: + ex_mod = getattr(ccxt, exchange.lower())() + if not ex_mod or not ex_mod.has: + return False, '' + missing = [k for k in EXCHANGE_HAS_REQUIRED if not ex_mod.has.get(k)] + if missing: + return False, f"missing: {', '.join(missing)}" + + missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + if missing_opt: + return True, f"missing opt: {', '.join(missing_opt)}" + + return True, '' + + +def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]: + """ + :return: List of tuples with exchangename, valid, reason. + """ + exchanges = ccxt_exchanges() if all_exchanges else available_exchanges() + exchanges_valid = [ + (e, *validate_exchange(e)) for e in exchanges + ] + return exchanges_valid + + def timeframe_to_seconds(timeframe: str) -> int: """ Translates the timeframe interval value written in the human readable From 969d44a95298e20174514017c85a9925245699ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:49:16 +0200 Subject: [PATCH 237/348] Update Dockerfile.armhf --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b019b6096..dcb008cb9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM --platform=linux/arm/v7 python:3.9.4-slim-buster as base +FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base # Setup env ENV LANG C.UTF-8 From ddabfe0206c4fa93efb04942dc4c11c6a062d026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 07:57:27 +0200 Subject: [PATCH 238/348] adjust tests to match new exchangelist output --- tests/commands/test_commands.py | 10 +++++----- tests/test_main.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index e21ef4dd1..232fc4e2c 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -66,8 +66,8 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() assert re.match(r"Exchanges available for Freqtrade.*", captured.out) - assert re.match(r".*binance,.*", captured.out) - assert re.match(r".*bittrex,.*", captured.out) + assert re.search(r".*binance.*", captured.out) + assert re.search(r".*bittrex.*", captured.out) # Test with --one-column args = [ @@ -89,9 +89,9 @@ def test_list_exchanges(capsys): start_list_exchanges(get_args(args)) captured = capsys.readouterr() assert re.match(r"All exchanges supported by the ccxt library.*", captured.out) - assert re.match(r".*binance,.*", captured.out) - assert re.match(r".*bittrex,.*", captured.out) - assert re.match(r".*bitmex,.*", captured.out) + assert re.search(r".*binance.*", captured.out) + assert re.search(r".*bittrex.*", captured.out) + assert re.search(r".*bitmex.*", captured.out) # Test with --one-column --all args = [ diff --git a/tests/test_main.py b/tests/test_main.py index 70632aeaa..d52dcaf79 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -118,7 +118,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_operational_exception1(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch( - 'freqtrade.commands.list_commands.available_exchanges', + 'freqtrade.commands.list_commands.validate_exchanges', MagicMock(side_effect=ValueError('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) @@ -132,7 +132,7 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has('Fatal exception!', caplog) assert not log_has_re(r'SIGINT.*', caplog) mocker.patch( - 'freqtrade.commands.list_commands.available_exchanges', + 'freqtrade.commands.list_commands.validate_exchanges', MagicMock(side_effect=KeyboardInterrupt) ) with pytest.raises(SystemExit): From 142690c93068be58937aaef08dd9004871f237fb Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:05:03 +0200 Subject: [PATCH 239/348] resolves freqtrade/freqtrade#4650 --- config_binance.json.example | 99 --------------- docker-compose.yml | 2 +- freqtrade/constants.py | 4 +- .../plugins/pairlist/VolatilityFilter.py | 120 ++++++++++++++++++ 4 files changed, 123 insertions(+), 102 deletions(-) delete mode 100644 config_binance.json.example create mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/config_binance.json.example b/config_binance.json.example deleted file mode 100644 index 4fa615d6d..000000000 --- a/config_binance.json.example +++ /dev/null @@ -1,99 +0,0 @@ -{ - "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.05, - "tradable_balance_ratio": 0.99, - "fiat_display_currency": "USD", - "timeframe": "5m", - "dry_run": true, - "cancel_open_orders_on_exit": false, - "unfilledtimeout": { - "buy": 10, - "sell": 30 - }, - "bid_strategy": { - "ask_last_balance": 0.0, - "use_order_book": false, - "order_book_top": 1, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "ask_strategy": { - "use_order_book": false, - "order_book_min": 1, - "order_book_max": 1, - "use_sell_signal": true, - "sell_profit_only": false, - "ignore_roi_if_buy_signal": false - }, - "exchange": { - "name": "binance", - "key": "your_exchange_key", - "secret": "your_exchange_secret", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, - "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" - ], - "pair_blacklist": [ - "BNB/BTC" - ] - }, - "pairlists": [ - {"method": "StaticPairList"} - ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, - "telegram": { - "enabled": false, - "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id" - }, - "api_server": { - "enabled": false, - "listen_ip_address": "127.0.0.1", - "listen_port": 8080, - "verbosity": "error", - "jwt_secret_key": "somethingrandom", - "CORS_origins": [], - "username": "freqtrader", - "password": "SuperSecurePassword" - }, - "bot_name": "freqtrade", - "initial_state": "running", - "forcebuy_enable": false, - "internals": { - "process_throttle_secs": 5 - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 80e194ab2..71572140c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --strategy SampleStrategy + --strategy BinHV45.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..c4a360d18 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter'] + 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 @@ -416,4 +416,4 @@ PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list -TradeList = List[List] +TradeList = List[List] \ No newline at end of file diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py new file mode 100644 index 000000000..ea1ebeb29 --- /dev/null +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -0,0 +1,120 @@ +""" +Rate of change pairlist filter +""" +import logging +from copy import deepcopy +from typing import Any, Dict, List, Optional + +import sys +import arrow +from cachetools.ttl import TTLCache +from pandas import DataFrame +import numpy as np + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.plugins.pairlist.IPairList import IPairList + + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + ''' + Filters pairs by volatility + ''' + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('lookback_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0) + self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit('1d'): + raise OperationalException("VolatilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit('1d')})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with volatility range " + f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + result = False + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_avg = volatility_series.mean() + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result + + return result From 6f02acdbbd8d1f524e552ae9e24a775fbe7071b1 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:39:27 +0200 Subject: [PATCH 240/348] Revert "resolves freqtrade/freqtrade#4650" This reverts commit 142690c93068be58937aaef08dd9004871f237fb. --- config_binance.json.example | 99 +++++++++++++++ docker-compose.yml | 2 +- freqtrade/constants.py | 4 +- .../plugins/pairlist/VolatilityFilter.py | 120 ------------------ 4 files changed, 102 insertions(+), 123 deletions(-) create mode 100644 config_binance.json.example delete mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/config_binance.json.example b/config_binance.json.example new file mode 100644 index 000000000..4fa615d6d --- /dev/null +++ b/config_binance.json.example @@ -0,0 +1,99 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, + "bid_strategy": { + "ask_last_balance": 0.0, + "use_order_book": false, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 1, + "use_sell_signal": true, + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false + }, + "exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 200 + }, + "pair_whitelist": [ + "ALGO/BTC", + "ATOM/BTC", + "BAT/BTC", + "BCH/BTC", + "BRD/BTC", + "EOS/BTC", + "ETH/BTC", + "IOTA/BTC", + "LINK/BTC", + "LTC/BTC", + "NEO/BTC", + "NXS/BTC", + "XMR/BTC", + "XRP/BTC", + "XTZ/BTC" + ], + "pair_blacklist": [ + "BNB/BTC" + ] + }, + "pairlists": [ + {"method": "StaticPairList"} + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "jwt_secret_key": "somethingrandom", + "CORS_origins": [], + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 71572140c..80e194ab2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,4 +25,4 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --strategy BinHV45.py + --strategy SampleStrategy diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c4a360d18..3a2ed98e9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter', 'VolatilityFilter'] + 'SpreadFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 @@ -416,4 +416,4 @@ PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list -TradeList = List[List] \ No newline at end of file +TradeList = List[List] diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py deleted file mode 100644 index ea1ebeb29..000000000 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Rate of change pairlist filter -""" -import logging -from copy import deepcopy -from typing import Any, Dict, List, Optional - -import sys -import arrow -from cachetools.ttl import TTLCache -from pandas import DataFrame -import numpy as np - -from freqtrade.exceptions import OperationalException -from freqtrade.misc import plural -from freqtrade.plugins.pairlist.IPairList import IPairList - - - -logger = logging.getLogger(__name__) - - -class VolatilityFilter(IPairList): - ''' - Filters pairs by volatility - ''' - - def __init__(self, exchange, pairlistmanager, - config: Dict[str, Any], pairlistconfig: Dict[str, Any], - pairlist_pos: int) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - - self._days = pairlistconfig.get('lookback_days', 10) - self._min_volatility = pairlistconfig.get('min_volatility', 0) - self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) - self._refresh_period = pairlistconfig.get('refresh_period', 1440) - - self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) - - if self._days < 1: - raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") - if self._days > exchange.ohlcv_candle_limit('1d'): - raise OperationalException("VolatilityFilter requires lookback_days to not " - "exceed exchange max request size " - f"({exchange.ohlcv_candle_limit('1d')})") - - @property - def needstickers(self) -> bool: - """ - Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed - as tickers argument to filter_pairlist - """ - return False - - def short_desc(self) -> str: - """ - Short whitelist method description - used for startup-messages - """ - return (f"{self.name} - Filtering pairs with volatility range " - f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") - - def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: - """ - Validate trading range - :param pairlist: pairlist to filter or sort - :param tickers: Tickers (from exchange.get_tickers()). May be cached. - :return: new allowlist - """ - needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] - - since_ms = int(arrow.utcnow() - .floor('day') - .shift(days=-self._days - 1) - .float_timestamp) * 1000 - # Get all candles - candles = {} - if needed_pairs: - candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, - cache=False) - - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist - - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: - """ - Validate trading range - :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, false if it should be removed - """ - # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] - - result = False - if daily_candles is not None and not daily_candles.empty: - returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) - returns.fillna(0, inplace=True) - - volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) - volatility_avg = volatility_series.mean() - - if self._min_volatility <= volatility_avg <= self._max_volatility: - result = True - else: - self.log_once(f"Removed {pair} from whitelist, because volatility " - f"over {self._days} {plural(self._days, 'day')} " - f"is: {volatility_avg:.3f} " - f"which is not in the configured range of " - f"{self._min_volatility}-{self._max_volatility}.", - logger.info) - result = False - self._pair_cache[pair] = result - - return result From be770a89417b90a82d537ac5f26d2d76ee726a76 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:42:53 +0200 Subject: [PATCH 241/348] added VolatilityFilter resolves freqtrade#4650 --- freqtrade/constants.py | 2 +- .../plugins/pairlist/VolatilityFilter.py | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/pairlist/VolatilityFilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..b98161ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', - 'SpreadFilter'] + 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py new file mode 100644 index 000000000..97e86bab6 --- /dev/null +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -0,0 +1,120 @@ +""" +Rate of change pairlist filter +""" +import logging +from copy import deepcopy +from typing import Any, Dict, List, Optional + +import sys +import arrow +from cachetools.ttl import TTLCache +from pandas import DataFrame +import numpy as np + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.plugins.pairlist.IPairList import IPairList + + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + ''' + Filters pairs by volatility + ''' + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('lookback_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0) + self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") + if self._days > exchange.ohlcv_candle_limit('1d'): + raise OperationalException("VolatilityFilter requires lookback_days to not " + "exceed exchange max request size " + f"({exchange.ohlcv_candle_limit('1d')})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with volatility range " + f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + result = False + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_avg = volatility_series.mean() + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result + + return result \ No newline at end of file From 1733e24062339363084200e12f1bbda26f41dde0 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 10:44:13 +0200 Subject: [PATCH 242/348] pyLint adjustment resolves freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 97e86bab6..1913bfcc1 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -117,4 +117,5 @@ class VolatilityFilter(IPairList): result = False self._pair_cache[pair] = result - return result \ No newline at end of file + return result + \ No newline at end of file From 56ef3af42415164400a3f24bd1e9b2754e467491 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 11:59:58 +0200 Subject: [PATCH 243/348] Allow comments in pairs files --- freqtrade/configuration/configuration.py | 12 +++++------- freqtrade/configuration/load_config.py | 9 +++++++++ freqtrade/misc.py | 2 +- tests/test_configuration.py | 19 ++++++------------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index a40a4fd83..9acd532cc 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -11,10 +11,10 @@ from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir -from freqtrade.configuration.load_config import load_config_file +from freqtrade.configuration.load_config import load_config_file, load_file from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts, json_load +from freqtrade.misc import deep_merge_dicts from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode @@ -454,9 +454,8 @@ class Configuration: # or if pairs file is specified explicitely if not pairs_file.exists(): raise OperationalException(f'No pairs file found with path "{pairs_file}".') - with pairs_file.open('r') as f: - config['pairs'] = json_load(f) - config['pairs'].sort() + config['pairs'] = load_file(pairs_file) + config['pairs'].sort() return if 'config' in self.args and self.args['config']: @@ -466,7 +465,6 @@ class Configuration: # Fall back to /dl_path/pairs.json pairs_file = config['datadir'] / 'pairs.json' if pairs_file.exists(): - with pairs_file.open('r') as f: - config['pairs'] = json_load(f) + config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 726126034..1320a375f 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -38,6 +38,15 @@ def log_config_error_range(path: str, errmsg: str) -> str: return '' +def load_file(path: Path) -> Dict[str, Any]: + try: + with path.open('r') as file: + config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) + except FileNotFoundError: + raise OperationalException(f'File file "{path}" not found!') + return config + + def load_config_file(path: str) -> Dict[str, Any]: """ Loads a config file from the given path diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7bbc24056..6508363d6 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,7 +81,7 @@ def json_load(datafile: IO) -> Any: """ load data with rapidjson Use this to have a consistent experience, - sete number_mode to "NM_NATIVE" for greatest speed + set number_mode to "NM_NATIVE" for greatest speed """ return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 15fbab7f8..b8d38dce0 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1038,37 +1038,30 @@ def test_pairlist_resolving_with_config(mocker, default_conf): def test_pairlist_resolving_with_config_pl(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) - load_mock = mocker.patch("freqtrade.configuration.configuration.json_load", - MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) arglist = [ 'download-data', '--config', 'config.json', - '--pairs-file', 'pairs.json', + '--pairs-file', 'tests/testdata/pairs.json', ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() - - assert load_mock.call_count == 1 - assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert len(config['pairs']) == 23 + assert 'ETH/BTC' in config['pairs'] + assert 'XRP/BTC' in config['pairs'] assert config['exchange']['name'] == default_conf['exchange']['name'] def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) - mocker.patch("freqtrade.configuration.configuration.json_load", - MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) arglist = [ 'download-data', '--config', 'config.json', - '--pairs-file', 'pairs.json', + '--pairs-file', 'tests/testdata/pairs_doesnotexist.json', ] args = Arguments(arglist).get_parsed_arg() @@ -1081,7 +1074,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): def test_pairlist_resolving_fallback(mocker): mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) - mocker.patch("freqtrade.configuration.configuration.json_load", + mocker.patch("freqtrade.configuration.configuration.load_file", MagicMock(return_value=['XRP/BTC', 'ETH/BTC'])) arglist = [ 'download-data', From bf0886a839eeca7477ade9eeb5b2fa4bc6a28851 Mon Sep 17 00:00:00 2001 From: klara31 Date: Tue, 6 Apr 2021 18:35:30 +0200 Subject: [PATCH 244/348] Update constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3a2ed98e9..a3cf71553 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -176,7 +176,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, - 'sell_profit_offset': {'type': 'number', 'minimum': 0.0}, + 'sell_profit_offset': {'type': 'number', 'minimum': -100}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, From c40b811f19414042503514ff9fdb97dc062c88cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 19:35:17 +0200 Subject: [PATCH 245/348] flush after creating mock trades --- tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index f8c1c5357..4a2106a4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -220,6 +220,9 @@ def create_mock_trades(fee, use_db: bool = True): trade = mock_trade_6(fee) add_trade(trade) + if use_db: + Trade.query.session.flush() + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: From f37fbbf4e115c3373ad6ef0df660ace3c3c7b83f Mon Sep 17 00:00:00 2001 From: klara31 Date: Tue, 6 Apr 2021 19:47:48 +0200 Subject: [PATCH 246/348] Update constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a3cf71553..2d2f9658c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -176,7 +176,7 @@ CONF_SCHEMA = { 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'use_sell_signal': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'}, - 'sell_profit_offset': {'type': 'number', 'minimum': -100}, + 'sell_profit_offset': {'type': 'number'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'} } }, From 5ed7828446abcddc7adb93d81886f66ec8877c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 20:03:38 +0200 Subject: [PATCH 247/348] Remove hardcoded list of non-working exchanges --- freqtrade/exchange/common.py | 70 ------------------------------------ 1 file changed, 70 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 90b70d67e..694aa3aa2 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -18,78 +18,8 @@ BAD_EXCHANGES = { "bitmex": "Various reasons.", "bitstamp": "Does not provide history. " "Details in https://github.com/freqtrade/freqtrade/issues/1983", - "hitbtc": "This API cannot be used with Freqtrade. " - "Use `hitbtc2` exchange id to access this exchange.", "phemex": "Does not provide history. ", "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", - **dict.fromkeys([ - 'adara', - 'anxpro', - 'bigone', - 'coinbase', - 'coinexchange', - 'coinmarketcap', - 'lykke', - 'xbtce', - ], "Does not provide timeframes. ccxt fetchOHLCV: False"), - **dict.fromkeys([ - 'bcex', - 'bit2c', - 'bitbay', - 'bitflyer', - 'bitforex', - 'bithumb', - 'bitso', - 'bitstamp1', - 'bl3p', - 'braziliex', - 'btcbox', - 'btcchina', - 'btctradeim', - 'btctradeua', - 'bxinth', - 'chilebit', - 'coincheck', - 'coinegg', - 'coinfalcon', - 'coinfloor', - 'coingi', - 'coinmate', - 'coinone', - 'coinspot', - 'coolcoin', - 'crypton', - 'deribit', - 'exmo', - 'exx', - 'flowbtc', - 'foxbit', - 'fybse', - # 'hitbtc', - 'ice3x', - 'independentreserve', - 'indodax', - 'itbit', - 'lakebtc', - 'latoken', - 'liquid', - 'livecoin', - 'luno', - 'mixcoins', - 'negociecoins', - 'nova', - 'paymium', - 'southxchange', - 'stronghold', - 'surbitcoin', - 'therock', - 'tidex', - 'vaultoro', - 'vbtc', - 'virwox', - 'yobit', - 'zaif', - ], "Does not provide timeframes. ccxt fetchOHLCV: emulated"), } MAP_EXCHANGE_CHILDCLASS = { From b6599c1da9a75e4ac7dae47bd4d7844ea354d0a6 Mon Sep 17 00:00:00 2001 From: Aleksey Popov Date: Tue, 6 Apr 2021 20:10:52 +0200 Subject: [PATCH 248/348] Improve Kraken-specific config description. Added Warning after Kraken rate limit config in order to clearly highlight that it holds delay between requests instead of req\sec rate. --- docs/exchanges.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 4c7e44b06..1c5956088 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -44,6 +44,10 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine. It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient. +!!! Warning "rateLimit tuning" + Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate. + So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased. + ## Bittrex ### Order types From a3b4667f7c39b6b1cb137a245427441469a3e751 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Apr 2021 20:16:29 +0200 Subject: [PATCH 249/348] Update exchange validation to use "validate_exchange". --- freqtrade/configuration/check_exchange.py | 14 +++++++++----- freqtrade/exchange/__init__.py | 4 ++-- freqtrade/exchange/exchange.py | 15 +++++---------- tests/test_configuration.py | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index aa36de3ff..832caf153 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -2,8 +2,8 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad, - is_exchange_known_ccxt, is_exchange_officially_supported) +from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt, + is_exchange_officially_supported, validate_exchange) from freqtrade.state import RunMode @@ -57,9 +57,13 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: f'{", ".join(available_exchanges())}' ) - if check_for_bad and is_exchange_bad(exchange): - raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. ' - f'Reason: {get_exchange_bad_reason(exchange)}') + valid, reason = validate_exchange(exchange) + if not valid: + if check_for_bad: + raise OperationalException(f'Exchange "{exchange}" will not work with Freqtrade. ' + f'Reason: {reason}') + else: + logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') if is_exchange_officially_supported(exchange): logger.info(f'Exchange "{exchange}" is officially supported ' diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 0eedd25d8..8a5563623 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -8,10 +8,10 @@ from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, - get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, validate_exchanges) + timeframe_to_seconds, validate_exchange, + validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 768a02aa1..37d92e253 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1307,14 +1307,6 @@ class Exchange: self.calculate_fee_rate(order)) -def is_exchange_bad(exchange_name: str) -> bool: - return exchange_name in BAD_EXCHANGES - - -def get_exchange_bad_reason(exchange_name: str) -> str: - return BAD_EXCHANGES.get(exchange_name, "") - - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) @@ -1335,18 +1327,21 @@ def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list """ exchanges = ccxt_exchanges(ccxt_module) - return [x for x in exchanges if not is_exchange_bad(x)] + return [x for x in exchanges if validate_exchange(x)[0]] def validate_exchange(exchange: str) -> Tuple[bool, str]: ex_mod = getattr(ccxt, exchange.lower())() if not ex_mod or not ex_mod.has: return False, '' - missing = [k for k in EXCHANGE_HAS_REQUIRED if not ex_mod.has.get(k)] + missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True] if missing: return False, f"missing: {', '.join(missing)}" missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] + + if exchange.lower() in BAD_EXCHANGES: + return False, BAD_EXCHANGES.get(exchange.lower(), '') if missing_opt: return True, f"missing opt: {', '.join(missing_opt)}" diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 15fbab7f8..e480b7bc3 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -565,7 +565,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange, which known to have serious problems default_conf.get('exchange').update({'name': 'bitmex'}) with pytest.raises(OperationalException, - match=r"Exchange .* is known to not work with the bot yet.*"): + match=r"Exchange .* will not work with Freqtrade\..*"): check_exchange(default_conf) caplog.clear() From 187cf6dcd5217914cea52c3296afb9f58728d017 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 22:41:15 +0200 Subject: [PATCH 250/348] VolatilityFilter resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 6 +++--- tests/plugins/test_pairlist.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 1913bfcc1..1bb836e76 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -67,7 +67,7 @@ class VolatilityFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ - needed_pairs = [(p, '1h') for p in pairlist if p not in self._pair_cache] + needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] since_ms = int(arrow.utcnow() .floor('day') @@ -81,7 +81,7 @@ class VolatilityFilter(IPairList): if self._enabled: for p in deepcopy(pairlist): - daily_candles = candles[(p, '1h')] if (p, '1h') in candles else None + daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None if not self._validate_pair_loc(p, daily_candles): pairlist.remove(p) return pairlist @@ -102,7 +102,7 @@ class VolatilityFilter(IPairList): returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) returns.fillna(0, inplace=True) - volatility_series = returns.rolling(window=self._days*24).std()*np.sqrt(self._days*24) + volatility_series = returns.rolling(window=self._days).std()*np.sqrt(self._days) volatility_avg = volatility_series.mean() if self._min_volatility <= volatility_avg <= self._max_volatility: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 67cd96f5b..7d39014f1 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -407,6 +407,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), + ([{"method": "StaticPairList"}, + {"method": "VolatilityFilter", "lookback_days": 3, + "min_volatility": 0.002, "max_volatility": 0.004, "refresh_period": 1440}], + "BTC", ['ETH/BTC', 'TKN/BTC']) ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history, pairlists, base_currency, @@ -414,13 +418,19 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency + ohlcv_history_high_vola = ohlcv_history.copy() + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index==1, 'close'] = 0.00090 + ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, ('LTC/BTC', '1d'): ohlcv_history, ('XRP/BTC', '1d'): ohlcv_history, - ('HOT/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history_high_vola, } + + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -487,7 +497,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert log_has(logmsg, caplog) else: assert not log_has(logmsg, caplog) - + if pairlist["method"] == 'VolatilityFilter': + assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] From 9089323d266980c1d896c35341e3fd3e68dd8362 Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 22:46:36 +0200 Subject: [PATCH 251/348] resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 1bb836e76..5b50e04e4 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -1,5 +1,5 @@ """ -Rate of change pairlist filter +Volatility pairlist filter """ import logging from copy import deepcopy From 9772a93634a1c60be321cbb78a51cec52cd59a5c Mon Sep 17 00:00:00 2001 From: gbojen Date: Tue, 6 Apr 2021 23:11:40 +0200 Subject: [PATCH 252/348] resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 ++--- tests/plugins/test_pairlist.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 5b50e04e4..6ef3841f5 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -16,7 +16,6 @@ from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList - logger = logging.getLogger(__name__) @@ -58,7 +57,8 @@ class VolatilityFilter(IPairList): Short whitelist method description - used for startup-messages """ return (f"{self.name} - Filtering pairs with volatility range " - f"{self._min_volatility}-{self._max_volatility} the last {self._days} {plural(self._days, 'day')}.") + f"{self._min_volatility}-{self._max_volatility} " + f" the last {self._days} {plural(self._days, 'day')}.") def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ @@ -118,4 +118,3 @@ class VolatilityFilter(IPairList): self._pair_cache[pair] = result return result - \ No newline at end of file diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 7d39014f1..4db0b7098 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -419,7 +419,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t whitelist_conf['stake_currency'] = base_currency ohlcv_history_high_vola = ohlcv_history.copy() - ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index==1, 'close'] = 0.00090 + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090 ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, @@ -428,15 +428,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ('XRP/BTC', '1d'): ohlcv_history, ('HOT/BTC', '1d'): ohlcv_history_high_vola, } - - - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) if whitelist_result == 'static_in_the_middle': with pytest.raises(OperationalException, - match=r"StaticPairList can only be used in the first position " + match=r"StaticPairList only in the first position " r"in the list of Pairlist Handlers."): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return @@ -500,6 +497,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if pairlist["method"] == 'VolatilityFilter': assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) + def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] From 0f0607baecdbbc04378d546239265d2e22c52c02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:52:34 +0200 Subject: [PATCH 253/348] Fix rangeestability filter caching issue --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index a1430a223..6565e92c1 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -87,8 +87,9 @@ class RangeStabilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] + cached_res = self._pair_cache.get(pair, None) + if cached_res is not None: + return cached_res result = False if daily_candles is not None and not daily_candles.empty: From ac6bff536f8c5d5bc78b4cd4cdd499bba7af2e89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:55:11 +0200 Subject: [PATCH 254/348] Fix test failure with UI test if UI is deployed --- freqtrade/rpc/api_server/web_ui.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index 13d22a63e..a8c737e04 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -13,6 +13,11 @@ async def favicon(): return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico')) +@router_ui.get('/fallback_file.html', include_in_schema=False) +async def fallback(): + return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html')) + + @router_ui.get('/{rest_of_path:path}', include_in_schema=False) async def index_html(rest_of_path: str): """ From d2680f6cb8c63de6470864b675fccdcb888ff01d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 06:57:05 +0200 Subject: [PATCH 255/348] Remove telegram deprecation warning closes #4688 --- freqtrade/rpc/telegram.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b418c3dab..17ddd1c91 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -159,10 +159,10 @@ class Telegram(RPCHandler): for handle in handles: self._updater.dispatcher.add_handler(handle) self._updater.start_polling( - clean=True, bootstrap_retries=-1, timeout=30, read_latency=60, + drop_pending_updates=True, ) logger.info( 'rpc.telegram is listening for following commands: %s', diff --git a/setup.py b/setup.py index bf23fb999..54a2e01b5 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup(name='freqtrade', # from requirements.txt 'ccxt>=1.24.96', 'SQLAlchemy', - 'python-telegram-bot', + 'python-telegram-bot>=13.4', 'arrow>=0.17.0', 'cachetools', 'requests', From 7f8d90d34c37549937dd4ceb2fde565b63c03213 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 07:05:10 +0200 Subject: [PATCH 256/348] Update list-exchanges doc with new format --- docs/utils.md | 197 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index a84f068e9..8ef12e1c9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -253,13 +253,206 @@ optional arguments: * Example: see exchanges available for the bot: ``` $ freqtrade list-exchanges -Exchanges available for Freqtrade: _1btcxe, acx, allcoin, bequant, bibox, binance, binanceje, binanceus, bitbank, bitfinex, bitfinex2, bitkk, bitlish, bitmart, bittrex, bitz, bleutrade, btcalpha, btcmarkets, btcturk, buda, cex, cobinhood, coinbaseprime, coinbasepro, coinex, cointiger, coss, crex24, digifinex, dsx, dx, ethfinex, fcoin, fcoinjp, gateio, gdax, gemini, hitbtc2, huobipro, huobiru, idex, kkex, kraken, kucoin, kucoin2, kuna, lbank, mandala, mercado, oceanex, okcoincny, okcoinusd, okex, okex3, poloniex, rightbtc, theocean, tidebit, upbit, zb +Exchanges available for Freqtrade: +Exchange name Valid reason +--------------- ------- -------------------------------------------- +aax True +ascendex True missing opt: fetchMyTrades +bequant True +bibox True +bigone True +binance True +binanceus True +bitbank True missing opt: fetchTickers +bitcoincom True +bitfinex True +bitforex True missing opt: fetchMyTrades, fetchTickers +bitget True +bithumb True missing opt: fetchMyTrades +bitkk True missing opt: fetchMyTrades +bitmart True +bitmax True missing opt: fetchMyTrades +bitpanda True +bittrex True +bitvavo True +bitz True missing opt: fetchMyTrades +btcalpha True missing opt: fetchTicker, fetchTickers +btcmarkets True missing opt: fetchTickers +buda True missing opt: fetchMyTrades, fetchTickers +bw True missing opt: fetchMyTrades, fetchL2OrderBook +bybit True +bytetrade True +cdax True +cex True missing opt: fetchMyTrades +coinbaseprime True missing opt: fetchTickers +coinbasepro True missing opt: fetchTickers +coinex True +crex24 True +deribit True +digifinex True +equos True missing opt: fetchTicker, fetchTickers +eterbase True +fcoin True missing opt: fetchMyTrades, fetchTickers +fcoinjp True missing opt: fetchMyTrades, fetchTickers +ftx True +gateio True +gemini True +gopax True +hbtc True +hitbtc True +huobijp True +huobipro True +idex True +kraken True +kucoin True +lbank True missing opt: fetchMyTrades +mercado True missing opt: fetchTickers +ndax True missing opt: fetchTickers +novadax True +okcoin True +okex True +probit True +qtrade True +stex True +timex True +upbit True missing opt: fetchMyTrades +vcc True +zb True missing opt: fetchMyTrades + ``` +!!! Note "missing opt exchanges" + Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will). + * Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): ``` $ freqtrade list-exchanges -a -All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpro, bcex, bequant, bibox, bigone, binance, binanceje, binanceus, bit2c, bitbank, bitbay, bitfinex, bitfinex2, bitflyer, bitforex, bithumb, bitkk, bitlish, bitmart, bitmex, bitso, bitstamp, bitstamp1, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, cex, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, deribit, digifinex, dsx, dx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, gateio, gdax, gemini, hitbtc, hitbtc2, huobipro, huobiru, ice3x, idex, independentreserve, indodax, itbit, kkex, kraken, kucoin, kucoin2, kuna, lakebtc, latoken, lbank, liquid, livecoin, luno, lykke, mandala, mercado, mixcoins, negociecoins, nova, oceanex, okcoincny, okcoinusd, okex, okex3, paymium, poloniex, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, upbit, vaultoro, vbtc, virwox, xbtce, yobit, zaif, zb +All exchanges supported by the ccxt library: +Exchange name Valid reason +------------------ ------- --------------------------------------------------------------------------------------- +aax True +aofex False missing: fetchOrder +ascendex True missing opt: fetchMyTrades +bequant True +bibox True +bigone True +binance True +binanceus True +bit2c False missing: fetchOrder, fetchOHLCV +bitbank True missing opt: fetchTickers +bitbay False missing: fetchOrder +bitcoincom True +bitfinex True +bitfinex2 False missing: fetchOrder +bitflyer False missing: fetchOrder, fetchOHLCV +bitforex True missing opt: fetchMyTrades, fetchTickers +bitget True +bithumb True missing opt: fetchMyTrades +bitkk True missing opt: fetchMyTrades +bitmart True +bitmax True missing opt: fetchMyTrades +bitmex False Various reasons. +bitpanda True +bitso False missing: fetchOHLCV +bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983 +bitstamp1 False missing: fetchOrder, fetchOHLCV +bittrex True +bitvavo True +bitz True missing opt: fetchMyTrades +bl3p False missing: fetchOrder, fetchOHLCV +bleutrade False missing: fetchOrder +braziliex False missing: fetchOHLCV +btcalpha True missing opt: fetchTicker, fetchTickers +btcbox False missing: fetchOHLCV +btcmarkets True missing opt: fetchTickers +btctradeua False missing: fetchOrder, fetchOHLCV +btcturk False missing: fetchOrder +buda True missing opt: fetchMyTrades, fetchTickers +bw True missing opt: fetchMyTrades, fetchL2OrderBook +bybit True +bytetrade True +cdax True +cex True missing opt: fetchMyTrades +chilebit False missing: fetchOrder, fetchOHLCV +coinbase False missing: fetchOrder, cancelOrder, createOrder, fetchOHLCV +coinbaseprime True missing opt: fetchTickers +coinbasepro True missing opt: fetchTickers +coincheck False missing: fetchOrder, fetchOHLCV +coinegg False missing: fetchOHLCV +coinex True +coinfalcon False missing: fetchOHLCV +coinfloor False missing: fetchOrder, fetchOHLCV +coingi False missing: fetchOrder, fetchOHLCV +coinmarketcap False missing: fetchOrder, cancelOrder, createOrder, fetchBalance, fetchOHLCV +coinmate False missing: fetchOHLCV +coinone False missing: fetchOHLCV +coinspot False missing: fetchOrder, cancelOrder, fetchOHLCV +crex24 True +currencycom False missing: fetchOrder +delta False missing: fetchOrder +deribit True +digifinex True +equos True missing opt: fetchTicker, fetchTickers +eterbase True +exmo False missing: fetchOrder +exx False missing: fetchOHLCV +fcoin True missing opt: fetchMyTrades, fetchTickers +fcoinjp True missing opt: fetchMyTrades, fetchTickers +flowbtc False missing: fetchOrder, fetchOHLCV +foxbit False missing: fetchOrder, fetchOHLCV +ftx True +gateio True +gemini True +gopax True +hbtc True +hitbtc True +hollaex False missing: fetchOrder +huobijp True +huobipro True +idex True +independentreserve False missing: fetchOHLCV +indodax False missing: fetchOHLCV +itbit False missing: fetchOHLCV +kraken True +kucoin True +kuna False missing: fetchOHLCV +lakebtc False missing: fetchOrder, fetchOHLCV +latoken False missing: fetchOrder, fetchOHLCV +lbank True missing opt: fetchMyTrades +liquid False missing: fetchOHLCV +luno False missing: fetchOHLCV +lykke False missing: fetchOHLCV +mercado True missing opt: fetchTickers +mixcoins False missing: fetchOrder, fetchOHLCV +ndax True missing opt: fetchTickers +novadax True +oceanex False missing: fetchOHLCV +okcoin True +okex True +paymium False missing: fetchOrder, fetchOHLCV +phemex False Does not provide history. +poloniex False missing: fetchOrder +probit True +qtrade True +rightbtc False missing: fetchOrder +ripio False missing: fetchOHLCV +southxchange False missing: fetchOrder, fetchOHLCV +stex True +surbitcoin False missing: fetchOrder, fetchOHLCV +therock False missing: fetchOHLCV +tidebit False missing: fetchOrder +tidex False missing: fetchOHLCV +timex True +upbit True missing opt: fetchMyTrades +vbtc False missing: fetchOrder, fetchOHLCV +vcc True +wavesexchange False missing: fetchOrder +whitebit False missing: fetchOrder, cancelOrder, createOrder, fetchBalance +xbtce False missing: fetchOrder, fetchOHLCV +xena False missing: fetchOrder +yobit False missing: fetchOHLCV +zaif False missing: fetchOrder, fetchOHLCV +zb True missing opt: fetchMyTrades ``` ## List Timeframes From 17508efbbc88e89a7380cb6d14f12c5350e0ed5c Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 08:59:44 +0200 Subject: [PATCH 257/348] resolves freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 33 ++++++++++++++++++++++++++++++++- tests/plugins/test_pairlist.py | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 2653406e7..ad1ac6efc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -4,7 +4,7 @@ Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). -Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. +Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. @@ -29,6 +29,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) * [`RangeStabilityFilter`](#rangestabilityfilter) +* [`VolatilityFilter`](#volatilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -164,6 +165,29 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit !!! Tip This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. +#### VolatilityFilter + +Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. + +Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. + +This filter can be used to narrow down your pairs to a certain volatilty or avoid very volatile pairs. + +In the below example: +If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. + +```json +"pairlists": [ + { + "method": "VolatilityFilter", + "lookback_days": 10, + "min_volatilty": 0.20, + "max_volatilty": 0.30, + "refresh_period": 86400 + } +] +``` + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. @@ -189,6 +213,13 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "min_rate_of_change": 0.01, "refresh_period": 1440 }, + { + "method": "VolatilityFilter", + "lookback_days": 10, + "min_volatilty": 0.20, + "max_volatilty": 0.30, + "refresh_period": 86400 + }, {"method": "ShuffleFilter", "seed": 42} ], ``` diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 4db0b7098..bf225271f 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -433,7 +433,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if whitelist_result == 'static_in_the_middle': with pytest.raises(OperationalException, - match=r"StaticPairList only in the first position " + match=r"StaticPairList can only be used in the first position " r"in the list of Pairlist Handlers."): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return From 5ee879a747a5bbb916601c90af1e0cb25de56515 Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 10:15:51 +0200 Subject: [PATCH 258/348] isort resolves freqtrade/freqtrade#4650 --- freqtrade/plugins/pairlist/VolatilityFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 6ef3841f5..f8e380f56 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -2,14 +2,14 @@ Volatility pairlist filter """ import logging +import sys from copy import deepcopy from typing import Any, Dict, List, Optional -import sys import arrow +import numpy as np from cachetools.ttl import TTLCache from pandas import DataFrame -import numpy as np from freqtrade.exceptions import OperationalException from freqtrade.misc import plural From 4d30c32ad2bda337da4efb1f2d87bb76dc9693e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Apr 2021 17:10:20 +0200 Subject: [PATCH 259/348] Improve resiliancy of a test --- tests/conftest_trades.py | 4 +- tests/rpc/test_rpc_apiserver.py | 72 ++++++++++++++++----------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 8e4be9165..34fc58aee 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -241,7 +241,8 @@ def mock_trade_5(fee): open_rate=0.123, exchange='bittrex', strategy='SampleStrategy', - stoploss_order_id='prod_stoploss_3455' + stoploss_order_id='prod_stoploss_3455', + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') trade.orders.append(o) @@ -295,6 +296,7 @@ def mock_trade_6(fee): exchange='bittrex', strategy='SampleStrategy', open_order_id="prod_sell_6", + timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..d113a8802 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -753,25 +753,21 @@ def test_api_status(botclient, mocker, ticker, fee, markets): get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, - markets=PropertyMock(return_value=markets) + markets=PropertyMock(return_value=markets), + fetch_order=MagicMock(return_value={}), ) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json() == [] - - ftbot.enter_positions() - trades = Trade.get_open_trades() - trades[0].open_order_id = None - ftbot.exit_positions(trades) - Trade.query.session.flush() + create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) - assert len(rc.json()) == 1 - assert rc.json() == [{ - 'amount': 91.07468123, - 'amount_requested': 91.07468123, + assert len(rc.json()) == 4 + assert rc.json()[0] == { + 'amount': 123.0, + 'amount_requested': 123.0, 'base_currency': 'BTC', 'close_date': None, 'close_date_hum': None, @@ -780,37 +776,37 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate': None, - 'current_profit': -0.00408133, - 'current_profit_pct': -0.41, - 'current_profit_abs': -4.09e-06, - 'profit_ratio': -0.00408133, - 'profit_pct': -0.41, - 'profit_abs': -4.09e-06, + 'current_profit': ANY, + 'current_profit_pct': ANY, + 'current_profit_abs': ANY, + 'profit_ratio': ANY, + 'profit_pct': ANY, + 'profit_abs': ANY, 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, - 'open_date_hum': 'just now', + 'open_date_hum': ANY, 'open_timestamp': ANY, 'open_order': None, - 'open_rate': 1.098e-05, + 'open_rate': 0.123, 'pair': 'ETH/BTC', 'stake_amount': 0.001, - 'stop_loss_abs': 9.882e-06, - 'stop_loss_pct': -10.0, - 'stop_loss_ratio': -0.1, + 'stop_loss_abs': ANY, + 'stop_loss_pct': ANY, + 'stop_loss_ratio': ANY, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss_abs': 9.882e-06, - 'initial_stop_loss_pct': -10.0, - 'initial_stop_loss_ratio': -0.1, - 'stoploss_current_dist': -1.1080000000000002e-06, - 'stoploss_current_dist_ratio': -0.10081893, - 'stoploss_current_dist_pct': -10.08, - 'stoploss_entry_dist': -0.00010475, - 'stoploss_entry_dist_ratio': -0.10448878, + 'initial_stop_loss_abs': 0.0, + 'initial_stop_loss_pct': ANY, + 'initial_stop_loss_ratio': ANY, + 'stoploss_current_dist': ANY, + 'stoploss_current_dist_ratio': ANY, + 'stoploss_current_dist_pct': ANY, + 'stoploss_entry_dist': ANY, + 'stoploss_entry_dist_ratio': ANY, 'trade_id': 1, - 'close_rate_requested': None, + 'close_rate_requested': ANY, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, @@ -818,17 +814,17 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': True, - 'max_rate': 1.099e-05, - 'min_rate': 1.098e-05, - 'open_order_id': None, - 'open_rate_requested': 1.098e-05, - 'open_trade_value': 0.0010025, + 'max_rate': ANY, + 'min_rate': ANY, + 'open_order_id': 'dry_run_buy_12345', + 'open_rate_requested': ANY, + 'open_trade_value': 15.1668225, 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, 'exchange': 'bittrex', - }] + } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) @@ -836,7 +832,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) resp_values = rc.json() - assert len(resp_values) == 1 + assert len(resp_values) == 4 assert isnan(resp_values[0]['profit_abs']) From f8244d9d76610c96e0995754a934272bf94a536f Mon Sep 17 00:00:00 2001 From: gbojen Date: Wed, 7 Apr 2021 22:25:54 +0200 Subject: [PATCH 260/348] resolves freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 6 +++--- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ad1ac6efc..8c65753b6 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,7 +167,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. +Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. @@ -181,8 +181,8 @@ If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove { "method": "VolatilityFilter", "lookback_days": 10, - "min_volatilty": 0.20, - "max_volatilty": 0.30, + "min_volatility": 0.05, + "max_volatility": 0.50, "refresh_period": 86400 } ] diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index f8e380f56..400b1577d 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -94,8 +94,9 @@ class VolatilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - if pair in self._pair_cache: - return self._pair_cache[pair] + cached_res = self._pair_cache.get(pair, None) + if cached_res is not None: + return cached_res result = False if daily_candles is not None and not daily_candles.empty: From 862f69f895c4dd1774ba78ca9a221692ad9bb930 Mon Sep 17 00:00:00 2001 From: gbojen Date: Thu, 8 Apr 2021 16:43:38 +0200 Subject: [PATCH 261/348] removed typos --- docs/includes/pairlists.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8c65753b6..3aa2c63f4 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,14 +167,14 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatily is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). -Removes pairs where the average volatility over a `lookback_days` days is below `min_volatility` and above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. +This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. -This filter can be used to narrow down your pairs to a certain volatilty or avoid very volatile pairs. +This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: -If the volatilty over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. +If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. ```json "pairlists": [ From 5a5c5fccf236bf23612af897527fb7f86f588222 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 16:45:52 +0200 Subject: [PATCH 262/348] Add gitattributes file --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..00abd1d9d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.py eol=lf +*.sh eol=lf +*.ps1 eol=crlf From 74bf0b6399adf9766f8af6b2c68140723d0c3d51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 19:29:51 +0200 Subject: [PATCH 263/348] Fix typo in documentation --- docs/includes/pairlists.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 3aa2c63f4..d57757bbd 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -167,7 +167,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit #### VolatilityFilter -Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatilty of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. @@ -216,8 +216,8 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, { "method": "VolatilityFilter", "lookback_days": 10, - "min_volatilty": 0.20, - "max_volatilty": 0.30, + "min_volatility": 0.05, + "max_volatility": 0.50, "refresh_period": 86400 }, {"method": "ShuffleFilter", "seed": 42} From 898c24949bfcaee24e08198566afb15125679c4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:07:52 +0200 Subject: [PATCH 264/348] Add chown method to support docker --- freqtrade/commands/build_config_commands.py | 2 ++ .../configuration/directory_operations.py | 16 +++++++++++++ tests/test_directory_operations.py | 23 +++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 0dee480b3..03d095e12 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List from questionary import Separator, prompt +from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import OperationalException from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges @@ -216,6 +217,7 @@ def start_new_config(args: Dict[str, Any]) -> None: """ config_path = Path(args['config'][0]) + chown_user_directory(config_path.parent) if config_path.exists(): overwrite = ask_user_overwrite(config_path) if overwrite: diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 1ce8d1461..ca305c260 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -24,6 +24,21 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat return folder +def chown_user_directory(directory: Path) -> None: + """ + Use Sudo to change permissions of the home-directory if necessary + Only applies when running in docker! + """ + import os + if os.environ.get('FT_APP_ENV') == 'docker': + try: + import subprocess + subprocess.check_output( + ['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())]) + except Exception: + logger.warning(f"Could not chown {directory}") + + def create_userdata_dir(directory: str, create_dir: bool = False) -> Path: """ Create userdata directory structure. @@ -37,6 +52,7 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path: sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "logs", "notebooks", "plot", "strategies", ] folder = Path(directory) + chown_user_directory(folder) if not folder.is_dir(): if create_dir: folder.mkdir(parents=True) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index a8058c514..a11200526 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -1,11 +1,12 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name +import os from pathlib import Path from unittest.mock import MagicMock import pytest -from freqtrade.configuration.directory_operations import (copy_sample_files, create_datadir, - create_userdata_dir) +from freqtrade.configuration.directory_operations import (chown_user_directory, copy_sample_files, + create_datadir, create_userdata_dir) from freqtrade.exceptions import OperationalException from tests.conftest import log_has, log_has_re @@ -31,6 +32,24 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: assert str(x) == str(Path("/tmp/bar")) +def test_create_userdata_dir_and_chown(mocker, tmpdir, caplog) -> None: + sp_mock = mocker.patch('subprocess.check_output') + path = Path(tmpdir / 'bar') + assert not path.is_dir() + + x = create_userdata_dir(str(path), create_dir=True) + assert sp_mock.call_count == 0 + assert log_has(f'Created user-data directory: {path}', caplog) + assert isinstance(x, Path) + assert path.is_dir() + assert (path / 'data').is_dir() + + os.environ['FT_APP_ENV'] = 'docker' + chown_user_directory(path / 'data') + assert sp_mock.call_count == 1 + del os.environ['FT_APP_ENV'] + + def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) md = mocker.patch.object(Path, 'mkdir', MagicMock()) From 4eb251ce416cf3a3bf40071b8843814f38726cfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:17:53 +0200 Subject: [PATCH 265/348] Update dockerfiles to run as non-root --- Dockerfile | 26 ++++++++++++++++++-------- Dockerfile.armhf | 28 +++++++++++++++++----------- docker/Dockerfile.custom | 8 ++++---- docker/Dockerfile.develop | 4 ++-- docker/Dockerfile.jupyter | 2 +- docker/Dockerfile.plot | 2 +- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b399174b..ac48ea611 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,19 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" # Prepare environment -RUN mkdir /freqtrade +RUN mkdir /freqtrade \ + && apt update \ + && apt install -y sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + WORKDIR /freqtrade # Install dependencies @@ -24,7 +33,8 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-hyperopt.txt /freqtrade/ +COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ +USER ftuser RUN pip install --user --no-cache-dir numpy \ && pip install --user --no-cache-dir -r requirements-hyperopt.txt @@ -33,13 +43,13 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /root/.local /root/.local - - +COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +USER ftuser # Install and execute -COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir \ +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ && mkdir /freqtrade/user_data/ \ && freqtrade install-ui diff --git a/Dockerfile.armhf b/Dockerfile.armhf index eecd9fdc0..62ef165c0 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -5,15 +5,20 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PATH=/root/.local/bin:$PATH +ENV PATH=/home/ftuser/.local/bin:$PATH +ENV FT_APP_ENV="docker" # Prepare environment -RUN mkdir /freqtrade -WORKDIR /freqtrade +RUN mkdir /freqtrade \ + && apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \ + && apt-get clean \ + && useradd -u 1000 -G sudo -U -m ftuser \ + && chown ftuser:ftuser /freqtrade \ + # Allow sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -RUN apt-get update \ - && apt-get -y install libatlas3-base curl sqlite3 \ - && apt-get clean +WORKDIR /freqtrade # Install dependencies FROM base as python-deps @@ -37,13 +42,14 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /root/.local /root/.local +COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +USER ftuser # Install and execute -COPY . /freqtrade/ -RUN apt-get install -y libhdf5-serial-dev \ - && apt-get clean \ - && pip install -e . --no-cache-dir \ +COPY --chown=ftuser:ftuser . /freqtrade/ + +RUN pip install -e . --user --no-cache-dir \ + && mkdir /freqtrade/user_data/ \ && freqtrade install-ui ENTRYPOINT ["freqtrade"] diff --git a/docker/Dockerfile.custom b/docker/Dockerfile.custom index 10620e6b8..a7c599fa8 100644 --- a/docker/Dockerfile.custom +++ b/docker/Dockerfile.custom @@ -1,7 +1,7 @@ FROM freqtradeorg/freqtrade:develop -RUN apt-get update \ - && apt-get -y install git \ - && apt-get clean \ +RUN sudo apt-get update \ + && sudo apt-get -y install git \ + && sudo apt-get clean \ # The below dependency - pyti - serves as an example. Please use whatever you need! - && pip install pyti + && pip install --user pyti diff --git a/docker/Dockerfile.develop b/docker/Dockerfile.develop index cb49984e2..7c580f234 100644 --- a/docker/Dockerfile.develop +++ b/docker/Dockerfile.develop @@ -3,8 +3,8 @@ FROM freqtradeorg/freqtrade:develop # Install dependencies COPY requirements-dev.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements-dev.txt --no-cache-dir +RUN pip install numpy --user --no-cache-dir \ + && pip install -r requirements-dev.txt --user --no-cache-dir # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/docker/Dockerfile.jupyter b/docker/Dockerfile.jupyter index b7499eeef..7d603c667 100644 --- a/docker/Dockerfile.jupyter +++ b/docker/Dockerfile.jupyter @@ -1,7 +1,7 @@ FROM freqtradeorg/freqtrade:develop_plot -RUN pip install jupyterlab --no-cache-dir +RUN pip install jupyterlab --user --no-cache-dir # Empty the ENTRYPOINT to allow all commands ENTRYPOINT [] diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 40bc72bc5..d2fc3618a 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -4,4 +4,4 @@ FROM freqtradeorg/freqtrade:${sourceimage} # Install dependencies COPY requirements-plot.txt /freqtrade/ -RUN pip install -r requirements-plot.txt --no-cache-dir +RUN pip install -r requirements-plot.txt --user --no-cache-dir From 644dcc1641624700033b0865f8360a5264e51585 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Apr 2021 20:36:10 +0200 Subject: [PATCH 266/348] Only allow chown via sudo --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- docker/Dockerfile.custom | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index ac48ea611..a6dc9a991 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN mkdir /freqtrade \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers WORKDIR /freqtrade diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 62ef165c0..909c44eaa 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -16,7 +16,7 @@ RUN mkdir /freqtrade \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers WORKDIR /freqtrade diff --git a/docker/Dockerfile.custom b/docker/Dockerfile.custom index a7c599fa8..3b55fcb0e 100644 --- a/docker/Dockerfile.custom +++ b/docker/Dockerfile.custom @@ -1,7 +1,10 @@ FROM freqtradeorg/freqtrade:develop -RUN sudo apt-get update \ - && sudo apt-get -y install git \ - && sudo apt-get clean \ - # The below dependency - pyti - serves as an example. Please use whatever you need! - && pip install --user pyti +# Switch user to root if you must install something from apt +# Don't forget to switch the user back below! +# USER root + +# The below dependency - pyti - serves as an example. Please use whatever you need! +RUN pip install --user pyti + +# USER ftuser From 0b4b67e46b444167d18d37657521d7c212781c92 Mon Sep 17 00:00:00 2001 From: Brook Miles Date: Fri, 9 Apr 2021 10:36:03 +0900 Subject: [PATCH 267/348] add FAQ entries for shorting, futures, and options --- docs/faq.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 93b806dca..c890caf1a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,5 +1,19 @@ # Freqtrade FAQ +## Supported Markets + +Freqtrade supports spot trading only. + +### Can I open short positions? + +No, Freqtrade does not support trading with margin / leverage, and cannot open short positions. + +In some cases, your exchange may provide leveraged spot tokens which can be traded with Freqtrade eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD, etc... + +### Can I trade options or futures? + +No, options and futures trading are not supported. + ## Beginner Tips & Tricks * When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). From 4b2cec22ec645bd8a2ef0d75db1a3f2f0b586b1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 19:33:40 +0200 Subject: [PATCH 268/348] Chown .local dir --- .dockerignore | 12 ++++++++++-- Dockerfile | 2 +- Dockerfile.armhf | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 09f4c9f0c..889a4dfc7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,8 @@ .git .gitignore Dockerfile +Dockerfile.armhf .dockerignore -config.json* -*.sqlite .coveragerc .eggs .github @@ -13,4 +12,13 @@ CONTRIBUTING.md MANIFEST.in README.md freqtrade.service +freqtrade.egg-info + +config.json* +*.sqlite user_data +*.log + +.vscode +.mypy_cache +.ipynb_checkpoints diff --git a/Dockerfile b/Dockerfile index a6dc9a991..5d8bcdd41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local USER ftuser # Install and execute diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 909c44eaa..df1c4bc80 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -42,7 +42,7 @@ FROM base as runtime-image COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib -COPY --from=python-deps /home/ftuser/.local /home/ftuser/.local +COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local USER ftuser # Install and execute From 126127c1e11a09a4d4653c0ca9551ffc8cf35ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 21:28:38 +0200 Subject: [PATCH 269/348] Fix armHF image to use ftuser on install too --- Dockerfile.armhf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index df1c4bc80..332b91b35 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -33,7 +33,8 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt /freqtrade/ +COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ +USER ftuser RUN pip install --user --no-cache-dir numpy \ && pip install --user --no-cache-dir -r requirements.txt From 5f67400649d40637177a4a5071e364e94cdea0e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 21:58:15 +0200 Subject: [PATCH 270/348] Add SKDecimal Space --- freqtrade/optimize/decimalspace.py | 33 ++++++++++++++++++++++++++++++ freqtrade/strategy/hyper.py | 16 ++++----------- 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 freqtrade/optimize/decimalspace.py diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py new file mode 100644 index 000000000..fbc6a7af6 --- /dev/null +++ b/freqtrade/optimize/decimalspace.py @@ -0,0 +1,33 @@ +import numpy as np + +from skopt.space import Integer + + +class SKDecimal(Integer): + + def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None, + name=None, dtype=np.int64): + self.decimals = decimals + _low = int(low * pow(10, self.decimals)) + _high = int(high * pow(10, self.decimals)) + self.low_orig = low + self.high_orig = high + + super().__init__(_low, _high, prior, base, transform, name, dtype) + + def __repr__(self): + return "Decimal(low={}, high={}, decimals={}, prior='{}', transform='{}')".format( + self.low_orig, self.high_orig, self.decimals, self.prior, self.transform_) + + def __contains__(self, point): + if isinstance(point, list): + point = np.array(point) + return self.low_orig <= point <= self.high_orig + + def transform(self, Xt): + aa = [int(x * pow(10, self.decimals)) for x in Xt] + return super().transform(aa) + + def inverse_transform(self, Xt): + res = super().inverse_transform(Xt) + return [round(x * pow(0.1, self.decimals), self.decimals) for x in res] diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 709179997..9a7fc4ba4 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,6 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical + from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.exceptions import OperationalException @@ -168,22 +169,13 @@ class DecimalParameter(RealParameter): super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) - def get_space(self, name: str) -> 'Integer': + def get_space(self, name: str) -> 'SKDecimal': """ Create skopt optimization space. :param name: A name of parameter field. """ - low = int(self.opt_range[0] * pow(10, self._decimals)) - high = int(self.opt_range[1] * pow(10, self._decimals)) - return Integer(low, high, name=name, **self._space_params) - - def _set_value(self, value: int): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: An integer value. - """ - self.value = round(value * pow(0.1, self._decimals), self._decimals) + return SKDecimal(*self.opt_range, decimals=self._decimals, name=name, + **self._space_params) class CategoricalParameter(BaseParameter): From fedff1a75ac7de025449c9fecd884cb8c17f80c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 22:10:20 +0200 Subject: [PATCH 271/348] Fix failing test --- tests/strategy/test_interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3bfa691b4..0ee80e0c5 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -596,8 +596,6 @@ def test_hyperopt_parameters(): fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') assert isinstance(fltpar.get_space(''), Integer) assert fltpar.value == 1 - fltpar._set_value(2222) - assert fltpar.value == 2.222 catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy') From 34e47db18d4cd8bd44b1a8cd125a45c43c115dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Apr 2021 22:15:24 +0200 Subject: [PATCH 272/348] Test SKDecimal space --- freqtrade/optimize/decimalspace.py | 1 - tests/optimize/test_hyperopt.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py index fbc6a7af6..f5370b6d6 100644 --- a/freqtrade/optimize/decimalspace.py +++ b/freqtrade/optimize/decimalspace.py @@ -1,5 +1,4 @@ import numpy as np - from skopt.space import Integer diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c13da0d76..129fe53d9 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException +from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -1104,3 +1105,20 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) hyperopt.start() + + +def test_SKDecimal(): + space = SKDecimal(1, 2, decimals=2) + assert 1.5 in space + assert 2.5 not in space + assert space.low == 100 + assert space.high == 200 + + assert space.inverse_transform([200]) == [2.0] + assert space.inverse_transform([100]) == [1.0] + assert space.inverse_transform([150, 160]) == [1.5, 1.6] + + assert space.transform([1.5]) == [150] + assert space.transform([2.0]) == [200] + assert space.transform([1.0]) == [100] + assert space.transform([1.5, 1.6]) == [150, 160] From ea4b5d675df701d9cb0d6b36ada7cd9023408c97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 07:12:31 +0200 Subject: [PATCH 273/348] Don't explode low/high, but use explicit parameters --- freqtrade/strategy/hyper.py | 48 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 9a7fc4ba4..c72beba55 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -25,9 +25,8 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - opt_range: Sequence[Any] - def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, + def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. @@ -44,7 +43,6 @@ class BaseParameter(ABC): self.category = space self._space_params = kwargs self.value = default - self.opt_range = opt_range self.optimize = optimize self.load = load @@ -69,7 +67,6 @@ class BaseParameter(ABC): class IntParameter(BaseParameter): default: int value: int - opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -90,10 +87,12 @@ class IntParameter(BaseParameter): if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') - opt_range = low + self.low, self.high = low else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -101,13 +100,12 @@ class IntParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.opt_range, name=name, **self._space_params) + return Integer(low=self.low, high=self.high, name=name, **self._space_params) class RealParameter(BaseParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, optimize: bool = True, @@ -129,10 +127,11 @@ class RealParameter(BaseParameter): if high is None or isinstance(low, Sequence): if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - opt_range = low + self.low, self.high = low else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + self.low = low + self.high = high + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -140,13 +139,12 @@ class RealParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.opt_range, name=name, **self._space_params) + return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(RealParameter): +class DecimalParameter(BaseParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, decimals: int = 3, space: Optional[str] = None, @@ -162,11 +160,22 @@ class DecimalParameter(RealParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param optimize: Include parameter in hyperopt optimizations. :param load: Load parameter value from {space}_params. - :param kwargs: Extra parameters to skopt.space.Real. + :param kwargs: Extra parameters to skopt.space.Integer. """ self._decimals = decimals default = round(default, self._decimals) - super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, + + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'SKDecimal': @@ -174,7 +183,7 @@ class DecimalParameter(RealParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return SKDecimal(*self.opt_range, decimals=self._decimals, name=name, + return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name, **self._space_params) @@ -200,7 +209,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + self.opt_range = categories + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': From 83fbaf16c84d6395048e62956adf6509a8a70939 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 07:57:02 +0200 Subject: [PATCH 274/348] Extract numeric param validation and explosion --- freqtrade/strategy/hyper.py | 76 ++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c72beba55..35000d916 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -64,7 +64,43 @@ class BaseParameter(ABC): self.value = value -class IntParameter(BaseParameter): +class NumericParameter(BaseParameter): + """ Internal parameter used for Numeric purposes """ + float_or_int = Union[int, float] + default: float_or_int + value: float_or_int + + def __init__(self, low: Union[float_or_int, Sequence[float_or_int]], + high: Optional[float_or_int] = None, *, default: float_or_int, + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable numeric parameter. + Cannot be instantiated, but provides the validation for other numeric parameters + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. + :param default: A default value. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.*. + """ + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, + load=load, **kwargs) + + +class IntParameter(NumericParameter): default: int value: int @@ -82,17 +118,8 @@ class IntParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException('IntParameter space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -103,7 +130,7 @@ class IntParameter(BaseParameter): return Integer(low=self.low, high=self.high, name=name, **self._space_params) -class RealParameter(BaseParameter): +class RealParameter(NumericParameter): default: float value: float @@ -122,16 +149,7 @@ class RealParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -142,7 +160,7 @@ class RealParameter(BaseParameter): return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(BaseParameter): +class DecimalParameter(NumericParameter): default: float value: float @@ -165,17 +183,7 @@ class DecimalParameter(BaseParameter): self._decimals = decimals default = round(default, self._decimals) - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - self.low, self.high = low - else: - self.low = low - self.high = high - - super().__init__(default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'SKDecimal': From 9804e201146762f96752e18f41304b37bf47804a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 09:53:48 +0200 Subject: [PATCH 275/348] Don't use _set_value for autoOpt-Spaces --- freqtrade/optimize/hyperopt_auto.py | 4 ++-- freqtrade/strategy/hyper.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index c4d6f1581..f86204406 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -27,7 +27,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -37,7 +37,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_sell_trend diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 35000d916..3fedda974 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -50,19 +50,11 @@ class BaseParameter(ABC): return f'{self.__class__.__name__}({self.value})' @abstractmethod - def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: + def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ - def _set_value(self, value: Any): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: A numerical value. - """ - self.value = value - class NumericParameter(BaseParameter): """ Internal parameter used for Numeric purposes """ From ebbe47f38d884b139610d60db09708b32dcb6b71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 13:36:16 +0200 Subject: [PATCH 276/348] Simplify fiat convert and fix USD coingecko problem --- freqtrade/rpc/fiat_convert.py | 103 +++++++-------------------------- tests/rpc/test_fiat_convert.py | 78 +++---------------------- 2 files changed, 30 insertions(+), 151 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 4e26432d4..380070deb 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -4,9 +4,9 @@ e.g BTC to USD """ import logging -import time -from typing import Dict, List +from typing import Dict +from cachetools.ttl import TTLCache from pycoingecko import CoinGeckoAPI from freqtrade.constants import SUPPORTED_FIAT @@ -15,51 +15,6 @@ from freqtrade.constants import SUPPORTED_FIAT logger = logging.getLogger(__name__) -class CryptoFiat: - """ - Object to describe what is the price of Crypto-currency in a FIAT - """ - # Constants - CACHE_DURATION = 6 * 60 * 60 # 6 hours - - def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None: - """ - Create an object that will contains the price for a crypto-currency in fiat - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) - :param price: Price in FIAT - """ - - # Public attributes - self.crypto_symbol = None - self.fiat_symbol = None - self.price = 0.0 - - # Private attributes - self._expiration = 0.0 - - self.crypto_symbol = crypto_symbol.lower() - self.fiat_symbol = fiat_symbol.lower() - self.set_price(price=price) - - def set_price(self, price: float) -> None: - """ - Set the price of the Crypto-currency in FIAT and set the expiration time - :param price: Price of the current Crypto currency in the fiat - :return: None - """ - self.price = price - self._expiration = time.time() + self.CACHE_DURATION - - def is_expired(self) -> bool: - """ - Return if the current price is still valid or needs to be refreshed - :return: bool, true the price is expired and needs to be refreshed, false the price is - still valid - """ - return self._expiration - time.time() <= 0 - - class CryptoToFiatConverter: """ Main class to initiate Crypto to FIAT. @@ -84,7 +39,9 @@ class CryptoToFiatConverter: return CryptoToFiatConverter.__instance def __init__(self) -> None: - self._pairs: List[CryptoFiat] = [] + # Timeout: 6h + self._pair_price: TTLCache = TTLCache(maxsize=500, ttl=6 * 60 * 60) + self._load_cryptomap() def _load_cryptomap(self) -> None: @@ -118,49 +75,31 @@ class CryptoToFiatConverter: """ crypto_symbol = crypto_symbol.lower() fiat_symbol = fiat_symbol.lower() + inverse = False + if crypto_symbol == 'usd': + # usd corresponds to "uniswap-state-dollar" for coingecko. + # We'll therefore need to "swap" the currencies + logger.info(f"reversing Rates {crypto_symbol}, {fiat_symbol}") + crypto_symbol = fiat_symbol + fiat_symbol = 'usd' + inverse = True + + symbol = f"{crypto_symbol}/{fiat_symbol}" # Check if the fiat convertion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): raise ValueError(f'The fiat {fiat_symbol} is not supported.') - # Get the pair that interest us and return the price in fiat - for pair in self._pairs: - if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol: - # If the price is expired we refresh it, avoid to call the API all the time - if pair.is_expired(): - pair.set_price( - price=self._find_price( - crypto_symbol=pair.crypto_symbol, - fiat_symbol=pair.fiat_symbol - ) - ) + price = self._pair_price.get(symbol, None) - # return the last price we have for this pair - return pair.price - - # The pair does not exist, so we create it and return the price - return self._add_pair( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol, - price=self._find_price( + if not price: + price = self._find_price( crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol ) - ) - - def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float: - """ - :param crypto_symbol: Crypto-currency you want to convert (e.g BTC) - :param fiat_symbol: FIAT currency you want to convert to (e.g USD) - :return: price in FIAT - """ - self._pairs.append( - CryptoFiat( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol, - price=price - ) - ) + if inverse and price != 0.0: + price = 1 / price + self._pair_price[symbol] = price return price diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index ed21bc516..2d43addff 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -1,44 +1,15 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, # pragma pylint: disable=protected-access, C0103 -import time from unittest.mock import MagicMock import pytest from requests.exceptions import RequestException -from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter +from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from tests.conftest import log_has, log_has_re -def test_pair_convertion_object(): - pair_convertion = CryptoFiat( - crypto_symbol='btc', - fiat_symbol='usd', - price=12345.0 - ) - - # Check the cache duration is 6 hours - assert pair_convertion.CACHE_DURATION == 6 * 60 * 60 - - # Check a regular usage - assert pair_convertion.crypto_symbol == 'btc' - assert pair_convertion.fiat_symbol == 'usd' - assert pair_convertion.price == 12345.0 - assert pair_convertion.is_expired() is False - - # Update the expiration time (- 2 hours) and check the behavior - pair_convertion._expiration = time.time() - 2 * 60 * 60 - assert pair_convertion.is_expired() is True - - # Check set price behaviour - time_reference = time.time() + pair_convertion.CACHE_DURATION - pair_convertion.set_price(price=30000.123) - assert pair_convertion.is_expired() is False - assert pair_convertion._expiration >= time_reference - assert pair_convertion.price == 30000.123 - - def test_fiat_convert_is_supported(mocker): fiat_convert = CryptoToFiatConverter() assert fiat_convert._is_supported_fiat(fiat='USD') is True @@ -47,28 +18,6 @@ def test_fiat_convert_is_supported(mocker): assert fiat_convert._is_supported_fiat(fiat='ABC') is False -def test_fiat_convert_add_pair(mocker): - - fiat_convert = CryptoToFiatConverter() - - pair_len = len(fiat_convert._pairs) - assert pair_len == 0 - - fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0) - pair_len = len(fiat_convert._pairs) - assert pair_len == 1 - assert fiat_convert._pairs[0].crypto_symbol == 'btc' - assert fiat_convert._pairs[0].fiat_symbol == 'usd' - assert fiat_convert._pairs[0].price == 12345.0 - - fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2) - pair_len = len(fiat_convert._pairs) - assert pair_len == 2 - assert fiat_convert._pairs[1].crypto_symbol == 'btc' - assert fiat_convert._pairs[1].fiat_symbol == 'eur' - assert fiat_convert._pairs[1].price == 13000.2 - - def test_fiat_convert_find_price(mocker): fiat_convert = CryptoToFiatConverter() @@ -95,8 +44,8 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog): def test_fiat_convert_get_price(mocker): - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', - return_value=28000.0) + find_price = mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', + return_value=28000.0) fiat_convert = CryptoToFiatConverter() @@ -104,26 +53,17 @@ def test_fiat_convert_get_price(mocker): fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='US Dollar') # Check the value return by the method - pair_len = len(fiat_convert._pairs) + pair_len = len(fiat_convert._pair_price) assert pair_len == 0 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 - assert fiat_convert._pairs[0].crypto_symbol == 'btc' - assert fiat_convert._pairs[0].fiat_symbol == 'usd' - assert fiat_convert._pairs[0].price == 28000.0 - assert fiat_convert._pairs[0]._expiration != 0 - assert len(fiat_convert._pairs) == 1 + assert fiat_convert._pair_price['btc/usd'] == 28000.0 + assert len(fiat_convert._pair_price) == 1 + assert find_price.call_count == 1 # Verify the cached is used - fiat_convert._pairs[0].price = 9867.543 - expiration = fiat_convert._pairs[0]._expiration + fiat_convert._pair_price['btc/usd'] = 9867.543 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 9867.543 - assert fiat_convert._pairs[0]._expiration == expiration - - # Verify the cache expiration - expiration = time.time() - 2 * 60 * 60 - fiat_convert._pairs[0]._expiration = expiration - assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 28000.0 - assert fiat_convert._pairs[0]._expiration is not expiration + assert find_price.call_count == 1 def test_fiat_convert_same_currencies(mocker): From 37c2e037f11da94b7c55a567d30c6184830a3148 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 13:50:56 +0200 Subject: [PATCH 277/348] Rename dry_run_order to create_dry_run_order --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 8 ++++---- freqtrade/exchange/ftx.py | 2 +- freqtrade/exchange/kraken.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 26ec30a8a..0bcfa5e17 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -52,7 +52,7 @@ class Binance(Exchange): 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 37d92e253..7edace13c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -543,8 +543,8 @@ class Exchange: # See also #2575 at github. return max(min_stake_amounts) * amount_reserve_percent - def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order = { @@ -618,7 +618,7 @@ class Exchange: rate: float, time_in_force: str) -> Dict: if self._config['dry_run']: - dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, "buy", amount, rate) return dry_order params = self._params.copy() @@ -631,7 +631,7 @@ class Exchange: rate: float, time_in_force: str = 'gtc') -> Dict: if self._config['dry_run']: - dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, "sell", amount, rate) return dry_order params = self._params.copy() diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f05490cbb..6312759b9 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -53,7 +53,7 @@ class Ftx(Exchange): stop_price = self.price_to_precision(pair, stop_price) if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 724b11189..786f1b592 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -92,7 +92,7 @@ class Kraken(Exchange): stop_price = self.price_to_precision(pair, stop_price) if self._config['dry_run']: - dry_order = self.dry_run_order( + dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3439c7a09..202f1885f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -931,11 +931,11 @@ def test_exchange_has(default_conf, mocker): ("sell") ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_dry_run_order(default_conf, mocker, side, exchange_name): +def test_create_dry_run_order(default_conf, mocker, side, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - order = exchange.dry_run_order( + order = exchange.create_dry_run_order( pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) assert 'id' in order assert f'dry_run_{side}_' in order["id"] From 14e857423528b87e0cbfd46f1e3ad2264cda8ccd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:13:00 +0200 Subject: [PATCH 278/348] fetch_balance is never called in dry-run --- freqtrade/exchange/exchange.py | 4 ---- tests/exchange/test_exchange.py | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7edace13c..3224255d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -662,8 +662,6 @@ class Exchange: @retrier def get_balance(self, currency: str) -> float: - if self._config['dry_run']: - return self._config['dry_run_wallet'] # ccxt exception is already handled by get_balances balances = self.get_balances() @@ -675,8 +673,6 @@ class Exchange: @retrier def get_balances(self) -> dict: - if self._config['dry_run']: - return {} try: balances = self._api.fetch_balance() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 202f1885f..4ceba6eba 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1245,14 +1245,6 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert "timeInForce" not in api_mock.create_order.call_args[0][5] -def test_get_balance_dry_run(default_conf, mocker): - default_conf['dry_run'] = True - default_conf['dry_run_wallet'] = 999.9 - - exchange = get_patched_exchange(mocker, default_conf) - assert exchange.get_balance(currency='BTC') == 999.9 - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balance_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() @@ -1276,13 +1268,6 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -@pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_balances_dry_run(default_conf, mocker, exchange_name): - default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - assert exchange.get_balances() == {} - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balances_prod(default_conf, mocker, exchange_name): balance_item = { From 96a5b6555dd6a2c4f7fc62112d618f7c1d928843 Mon Sep 17 00:00:00 2001 From: gbojen Date: Sat, 10 Apr 2021 14:31:12 +0200 Subject: [PATCH 279/348] fix documentation inconsistency fixes freqtrade/freqtrade#4650 --- docs/includes/pairlists.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index d57757bbd..8688494cc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -174,7 +174,7 @@ This filter removes pairs if the average volatility over a `lookback_days` days This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: -If the volatility over the last 10 days is not in the range of 0.20-0.30, remove the pair from the whitelist. The filter is applied every 24h. +If the volatility over the last 10 days is not in the range of 0.05-0.50, remove the pair from the whitelist. The filter is applied every 24h. ```json "pairlists": [ @@ -190,7 +190,7 @@ If the volatility over the last 10 days is not in the range of 0.20-0.30, remove ### Full example of Pairlist Handlers -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. ```json "exchange": { From 579e68f31e122a3ebbd897371642a7ee299749c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:37:09 +0200 Subject: [PATCH 280/348] Reduce log verbosity when buying --- freqtrade/freqtradebot.py | 11 ++++------- tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a701e8db9..1ebf28ebd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -410,9 +410,7 @@ class FreqtradeBot(LoggingMixin): bid_strategy = self.config.get('bid_strategy', {}) if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): - logger.info( - f"Getting price from order book {bid_strategy['price_side'].capitalize()} side." - ) + order_book_top = bid_strategy.get('order_book_top', 1) order_book = self.exchange.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) @@ -425,7 +423,8 @@ class FreqtradeBot(LoggingMixin): f"Orderbook: {order_book}" ) raise PricingError from e - logger.info(f'...top {order_book_top} order book buy rate {rate_from_l2:.8f}') + logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side " + f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}") used_rate = rate_from_l2 else: logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") @@ -479,19 +478,17 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False - logger.info(f"Buy signal found: about create a new trade with stake_amount: " + logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): if self._check_depth_of_market_buy(pair, bid_check_dom): - logger.info(f'Executing Buy for {pair}.') return self.execute_buy(pair, stake_amount) else: return False - logger.info(f'Executing Buy for {pair}') return self.execute_buy(pair, stake_amount) else: return False diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c93f8b858..a7b9bb103 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -685,7 +685,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog + 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog ) From 4820b4b314096fc2529ad67f5d2dd2a8ccd431b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 14:52:34 +0200 Subject: [PATCH 281/348] Fix test failure --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a7b9bb103..c91015766 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -685,7 +685,8 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog + 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', + caplog ) From aaf9872ef37b5959ce1489da8470ce1dd534c2ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Apr 2021 19:53:00 +0200 Subject: [PATCH 282/348] Simplify webserver test --- tests/rpc/test_rpc_apiserver.py | 61 +++++++++++++-------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d113a8802..e72749715 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -416,10 +416,10 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json()["max"] == 1 # Create some test data - ftbot.enter_positions() + create_mock_trades(fee) rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) - assert rc.json()["current"] == 1 + assert rc.json()["current"] == 4 assert rc.json()["max"] == 1 ftbot.config['max_open_trades'] = float('inf') @@ -612,7 +612,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") -def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): +def test_api_profit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -627,48 +627,33 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert_response(rc, 200) assert rc.json()['trade_count'] == 0 - ftbot.enter_positions() - trade = Trade.query.first() - + create_mock_trades(fee) # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - rc = client_get(client, f"{BASE_URI}/profit") - assert_response(rc, 200) - # One open trade - assert rc.json()['trade_count'] == 1 - assert rc.json()['best_pair'] == '' - assert rc.json()['best_rate'] == 0 - - trade = Trade.query.first() - trade.update(limit_sell_order) - - trade.close_date = datetime.utcnow() - trade.is_open = False rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) assert rc.json() == {'avg_duration': ANY, - 'best_pair': 'ETH/BTC', - 'best_rate': 6.2, - 'first_trade_date': 'just now', + 'best_pair': 'XRP/BTC', + 'best_rate': 1.0, + 'first_trade_date': ANY, 'first_trade_timestamp': ANY, - 'latest_trade_date': 'just now', + 'latest_trade_date': '5 minutes ago', 'latest_trade_timestamp': ANY, - 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': 0.76748865, - 'profit_all_percent_mean': 6.2, - 'profit_all_ratio_mean': 0.06201058, - 'profit_all_percent_sum': 6.2, - 'profit_all_ratio_sum': 0.06201058, - 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': 0.76748865, - 'profit_closed_ratio_mean': 0.06201058, - 'profit_closed_percent_mean': 6.2, - 'profit_closed_ratio_sum': 0.06201058, - 'profit_closed_percent_sum': 6.2, - 'trade_count': 1, - 'closed_trade_count': 1, - 'winning_trades': 1, + 'profit_all_coin': -44.0631579, + 'profit_all_fiat': -543959.6842755, + 'profit_all_percent_mean': -66.41, + 'profit_all_ratio_mean': -0.6641100666666667, + 'profit_all_percent_sum': -398.47, + 'profit_all_ratio_sum': -3.9846604, + 'profit_closed_coin': 0.00073913, + 'profit_closed_fiat': 9.124559849999999, + 'profit_closed_ratio_mean': 0.0075, + 'profit_closed_percent_mean': 0.75, + 'profit_closed_ratio_sum': 0.015, + 'profit_closed_percent_sum': 1.5, + 'trade_count': 6, + 'closed_trade_count': 2, + 'winning_trades': 2, 'losing_trades': 0, } From 906c4e64d335e6d401eac2c85d3fa0f0b2319f46 Mon Sep 17 00:00:00 2001 From: Ugur Cem Ozturk Date: Sun, 11 Apr 2021 15:38:08 +0300 Subject: [PATCH 283/348] chore(readme): Fix markdown of docker manual Link to docker-compose was pointing to the one from develop branch. It's changed as with the stable docker-compose. --- docs/docker_quickstart.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index ca0515281..b133e33f0 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -22,10 +22,10 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co ### Docker quick start -Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. +Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -=== "PC/MAC/Linux" - ``` bash +#### PC/MAC/Linux + mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` + + +#### RaspberryPi -=== "RaspberryPi" - ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,7 +56,6 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -68,12 +67,11 @@ Create a new directory and place the [docker-compose file](https://github.com/fr The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -!!! Question "How to edit the bot configuration?" - You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +### Question: "How to edit the bot configuration?" +You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - -#### Adding a custom strategy +##### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -81,7 +79,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a The `SampleStrategy` is run by default. -!!! Warning "`SampleStrategy` is just a demo!" +#### Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -92,7 +90,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -!!! Warning "Default configuration" +#### Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -122,7 +120,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -!!! Warning "Check the Changelog" +#### Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -131,7 +129,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -!!! Note "`docker-compose run --rm`" +#### Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From 1b925ec4a9eff02f47be05263b472ae298c36631 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:11 +0000 Subject: [PATCH 284/348] Bump sqlalchemy from 1.4.5 to 1.4.7 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.5 to 1.4.7. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1877624cf..8bcb9608c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.46.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.5 +SQLAlchemy==1.4.7 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From 53bbb2b42c9e808252f8bdc7dd0a45d76c29f4bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:47 +0000 Subject: [PATCH 285/348] Bump ccxt from 1.46.38 to 1.47.47 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.46.38 to 1.47.47. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.46.38...1.47.47) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1877624cf..8705ef8e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.3 -ccxt==1.46.38 +ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From c19ebc0157cd7157099182e101aed8006ba463fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Apr 2021 05:26:58 +0000 Subject: [PATCH 286/348] Bump mkdocs-material from 7.1.0 to 7.1.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.0 to 7.1.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.0...7.1.1) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 3dbaea111..cfd63d1d0 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.0 +mkdocs-material==7.1.1 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From f1ac6853fc64270a32930e991d6d809c9576bc55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Apr 2021 11:11:53 +0200 Subject: [PATCH 287/348] Fix discord invite link --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index c890caf1a..7233a92fe 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -156,7 +156,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/MA9v74M). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: From d4dc05980c8b155dd8e7a1673387fdc20f505903 Mon Sep 17 00:00:00 2001 From: Chris van de Steeg Date: Mon, 12 Apr 2021 16:01:46 +0200 Subject: [PATCH 288/348] Update ftx.py Stoploss price should be set as param instead of passing it as price according to ccxt --- freqtrade/exchange/ftx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6312759b9..fdfd5a674 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -63,10 +63,11 @@ class Ftx(Exchange): # set orderPrice to place limit order, otherwise it's a market order params['orderPrice'] = limit_rate + params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', - amount=amount, price=stop_price, params=params) + amount=amount, params=params) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) return order From 1194d0c0f4ca3e6f7c595345849e839b0ff30a43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Apr 2021 20:03:37 +0200 Subject: [PATCH 289/348] Update brew before installing packages --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e15a5a89..102e6ed78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,7 @@ jobs: - name: Installation - macOS run: | + brew update brew install hdf5 c-blosc python -m pip install --upgrade pip export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH From 9a58a8534773393181012bc93930f1b138530c9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:17:11 +0200 Subject: [PATCH 290/348] Don't export "hum" date versions for trade objects. They are not used and have a rather high performance penalty due to using arrow.get --- freqtrade/persistence/models.py | 3 --- freqtrade/rpc/api_server/api_schemas.py | 2 -- freqtrade/rpc/telegram.py | 1 + tests/rpc/test_rpc.py | 4 ---- tests/rpc/test_rpc_apiserver.py | 4 ---- tests/rpc/test_rpc_telegram.py | 2 -- tests/test_persistence.py | 4 ---- 7 files changed, 1 insertion(+), 19 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a22e75e1e..8b4aa325a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -294,15 +294,12 @@ class LocalTrade(): 'fee_close_cost': self.fee_close_cost, 'fee_close_currency': self.fee_close_currency, - 'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, 'open_trade_value': round(self.open_trade_value, 8), - 'close_date_hum': (arrow.get(self.close_date).humanize() - if self.close_date else None), 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index eaca477d7..41de0134c 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -151,13 +151,11 @@ class TradeSchema(BaseModel): fee_close: Optional[float] fee_close_cost: Optional[float] fee_close_currency: Optional[str] - open_date_hum: str open_date: str open_timestamp: int open_rate: float open_rate_requested: Optional[float] open_trade_value: float - close_date_hum: Optional[str] close_date: Optional[str] close_timestamp: Optional[int] close_rate: Optional[float] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 17ddd1c91..a8c629149 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -294,6 +294,7 @@ class Telegram(RPCHandler): messages = [] for r in results: + r['open_date_hum'] = arrow.get(r['open_date']).humanize() lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 64918ed47..a97f6b65e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -53,7 +53,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, @@ -73,7 +72,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, @@ -121,7 +119,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, @@ -141,7 +138,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e72749715..2b6d96c61 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -755,7 +755,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'amount_requested': 123.0, 'base_currency': 'BTC', 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'close_profit': None, 'close_profit_pct': None, @@ -770,7 +769,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_fiat': ANY, 'current_rate': 1.099e-05, 'open_date': ANY, - 'open_date_hum': ANY, 'open_timestamp': ANY, 'open_order': None, 'open_rate': 0.123, @@ -922,11 +920,9 @@ def test_api_forcebuy(botclient, mocker, fee): 'amount_requested': 1, 'trade_id': 22, 'close_date': None, - 'close_date_hum': None, 'close_timestamp': None, 'close_rate': 0.265441, 'open_date': ANY, - 'open_date_hum': 'just now', 'open_timestamp': ANY, 'open_rate': 0.245441, 'pair': 'ETH/ETH', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 27babb1b7..34bf057cb 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -177,9 +177,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': arrow.utcnow(), - 'open_date_hum': arrow.utcnow().humanize, 'close_date': None, - 'close_date_hum': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3336e4e66..0a3d6858d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -799,11 +799,9 @@ def test_to_json(default_conf, fee): assert result == {'trade_id': None, 'pair': 'ETH/BTC', 'is_open': None, - 'open_date_hum': '2 hours ago', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_order_id': 'dry_run_buy_12345', - 'close_date_hum': None, 'close_date': None, 'close_timestamp': None, 'open_rate': 0.123, @@ -865,10 +863,8 @@ def test_to_json(default_conf, fee): assert result == {'trade_id': None, 'pair': 'XRP/BTC', - 'open_date_hum': '2 hours ago', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date_hum': 'an hour ago', 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, From 4b902d6eb8e0e17e94365bd4de2b6c8c256d8b24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:23:11 +0200 Subject: [PATCH 291/348] Don't use response-model on trades endpoint for now --- freqtrade/rpc/api_server/api_v1.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..663cc9ff4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -17,8 +17,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, - StrategyResponse, TradeResponse, Version, - WhitelistResponse) + StrategyResponse, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -83,7 +82,9 @@ def status(rpc: RPC = Depends(get_rpc)): return [] -@router.get('/trades', response_model=TradeResponse, tags=['info', 'trading']) +# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) +# on big databases. Correct response model: response_model=TradeResponse, +@router.get('/trades', tags=['info', 'trading']) def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) From 9b23be402114cdde6f4ed7d2468c3b90595a931d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:49:53 +0200 Subject: [PATCH 292/348] Return a copy from `current_whitelist` this avoids manipulating of the pair whitelist from within a strategy --- freqtrade/data/dataprovider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index a035b7c3b..b4dea0743 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -170,6 +170,6 @@ class DataProvider: """ if self._pairlists: - return self._pairlists.whitelist + return self._pairlists.whitelist.copy() else: raise OperationalException("Dataprovider was not initialized with a pairlist provider.") From f1cf56cc4277ec5c9f7184a58f29b9169200d984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 06:57:21 +0200 Subject: [PATCH 293/348] Update current_whitelist test --- tests/data/test_dataprovider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index ee2e551b6..6b33fa7f2 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -214,8 +214,8 @@ def test_current_whitelist(mocker, default_conf, tickers): pairlist.refresh_pairlist() assert dp.current_whitelist() == pairlist._whitelist - # The identity of the 2 lists should be identical - assert dp.current_whitelist() is pairlist._whitelist + # The identity of the 2 lists should not be identical, but a copy + assert dp.current_whitelist() is not pairlist._whitelist with pytest.raises(OperationalException): dp = DataProvider(default_conf, exchange) From 99e7ee12731533d91b2206e7f2df5008260ffb83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 08:26:26 +0200 Subject: [PATCH 294/348] Fix ftx stoploss creation test --- tests/exchange/test_ftx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 17cfb26fa..494d86e56 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -39,8 +39,9 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 - assert api_mock.create_order.call_args_list[0][1]['price'] == 190 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 190 assert api_mock.create_order.call_count == 1 @@ -55,8 +56,8 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 - assert api_mock.create_order.call_args_list[0][1]['price'] == 220 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, @@ -69,9 +70,9 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 - assert api_mock.create_order.call_args_list[0][1]['price'] == 220 assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 + assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 # test exception handling with pytest.raises(DependencyException): From e0f2bb6160f9478688601a0ffda7b35cb26439ca Mon Sep 17 00:00:00 2001 From: wr0ngc0degen Date: Tue, 13 Apr 2021 11:44:07 +0200 Subject: [PATCH 295/348] update conda dependencies to make compatible with tables package - restrict python version in conda's environment.yml to fixed installation issues due to current incompatibility of tables package with python 3.9 --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 938b5b6b8..f58434c15 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: # - defaults dependencies: # 1/4 req main - - python>=3.7 + - python>=3.7,<3.9 - numpy - pandas - pip From 37c8fd6ad799ed0320147304304acdad3e18e0b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 11:55:03 +0200 Subject: [PATCH 296/348] Remove arrow from models.py --- freqtrade/persistence/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8b4aa325a..49d3e2d62 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,7 +6,6 @@ from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional -import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError @@ -160,8 +159,8 @@ class Order(_DECL_BASE): if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False if order.get('filled', 0) > 0: - self.order_filled_date = arrow.utcnow().datetime - self.order_update_date = arrow.utcnow().datetime + self.order_filled_date = datetime.now(timezone.utc) + self.order_update_date = datetime.now(timezone.utc) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): From 638cd4e8f13b963b03beec0b311f520cf35f0185 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:04:22 +0200 Subject: [PATCH 297/348] Upgrade cleanup action to latest version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102e6ed78..4169661c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.2.2 + uses: rokroskar/workflow-run-cleanup-action@v0.3.2 if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From e4bb6b158294a0b6b36ac5b9be41f93c9bbedafe Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:28:07 +0200 Subject: [PATCH 298/348] Add kucoin exchange subclass Kucoin has some specific orderbook restrictions closes #4723 --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bittrex.py | 4 ---- freqtrade/exchange/exchange.py | 14 +++++++++++--- freqtrade/exchange/kucoin.py | 24 ++++++++++++++++++++++++ tests/exchange/test_ccxt_compat.py | 13 ++++++++++--- tests/exchange/test_exchange.py | 3 +++ 6 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 freqtrade/exchange/kucoin.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 8a5563623..889bb49c2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken +from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index fd7d47668..69e2f2b8d 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -12,10 +12,6 @@ class Bittrex(Exchange): """ Bittrex exchange class. Contains adjustments needed for Freqtrade to work with this exchange. - - Please note that this exchange is not included in the list of exchanges - officially supported by the Freqtrade development team. So some features - may still not work as expected. """ _ft_has: Dict = { diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3224255d0..e52e0e0d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -63,6 +63,7 @@ class Exchange: "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", "l2_limit_range": None, + "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} @@ -1154,14 +1155,20 @@ class Exchange: return self.fetch_order(order_id, pair) @staticmethod - def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]], + range_required: bool = True): """ Get next greater value in the list. Used by fetch_l2_order_book if the api only supports a limited range """ if not limit_range: return limit - return min([x for x in limit_range if limit <= x] + [max(limit_range)]) + + result = min([x for x in limit_range if limit <= x] + [max(limit_range)]) + if not range_required and limit > result: + # Range is not required - we can use None as parameter. + return None + return result @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: @@ -1171,7 +1178,8 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ - limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'], + self._ft_has['l2_limit_range_required']) try: return self._api.fetch_l2_order_book(pair, limit1) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py new file mode 100644 index 000000000..22886a1d8 --- /dev/null +++ b/freqtrade/exchange/kucoin.py @@ -0,0 +1,24 @@ +""" Kucoin exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Kucoin(Exchange): + """ + Kucoin exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "l2_limit_range": [20, 100], + "l2_limit_range_required": False, + } diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 870e6cabd..dce10da84 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -36,7 +36,12 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', - } + }, + 'kucoin': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, } @@ -100,14 +105,16 @@ class TestCCXTExchange(): assert 'asks' in l2 assert 'bids' in l2 l2_limit_range = exchange._ft_has['l2_limit_range'] + l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list(val, l2_limit_range) - if next_limit > 200: + next_limit = exchange.get_next_limit_in_list( + val, l2_limit_range, l2_limit_range_required) + if next_limit is None or next_limit > 200: # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4ceba6eba..db67d038c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1641,6 +1641,9 @@ def test_get_next_limit_in_list(): # Going over the limit ... assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 + # Without required range + assert Exchange.get_next_limit_in_list(2000, limit_range, False) is None + assert Exchange.get_next_limit_in_list(15, limit_range, False) == 20 assert Exchange.get_next_limit_in_list(21, None) == 21 assert Exchange.get_next_limit_in_list(100, None) == 100 From 521e48c94a5e5b18ba409d965757c7b8f509f08f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 13:55:08 +0200 Subject: [PATCH 299/348] Add doc section for Kucoin part of #4723 --- docs/exchanges.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 1c5956088..662f2b908 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -100,6 +100,18 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll } ``` +## Kucoin + +Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: + +```json +"exchange": { + "name": "kucoin", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "your_exchange_api_key_password", +``` + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From 82d66410f74b47f25cb4af60b702cad6fdebcae8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 19:20:57 +0200 Subject: [PATCH 300/348] Fix /performance output if multiple messages are necessary closes #4726 --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a8c629149..09b7b235c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -702,7 +702,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: - self._send_msg(output) + self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line From c2f35ce416cf8d6e6c5806dd1a3e624a124d3843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 20:09:22 +0200 Subject: [PATCH 301/348] /balance should use cached tickers when possible --- freqtrade/exchange/exchange.py | 18 ++++++++++++++++-- freqtrade/rpc/rpc.py | 2 +- tests/exchange/test_exchange.py | 8 ++++++++ tests/rpc/test_rpc.py | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e52e0e0d0..3627a07e4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async +from cachetools import TTLCache from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame @@ -84,6 +85,9 @@ class Exchange: # Timestamp of last markets refresh self._last_markets_refresh: int = 0 + # Cache for 10 minutes ... + self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} @@ -693,9 +697,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_tickers(self) -> Dict: + def get_tickers(self, cached: bool = False) -> Dict: + """ + :param cached: Allow cached result + :return: fetch_tickers result + """ + if cached: + tickers = self._fetch_tickers_cache.get('fetch_tickers') + if tickers: + return tickers try: - return self._api.fetch_tickers() + tickers = self._api.fetch_tickers() + self._fetch_tickers_cache['fetch_tickers'] = tickers + return tickers except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 59758a573..88f8b36db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -442,7 +442,7 @@ class RPC: output = [] total = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers() + tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index db67d038c..76095be2d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1319,6 +1319,14 @@ def test_get_tickers(default_conf, mocker, exchange_name): assert tickers['ETH/BTC']['ask'] == 1 assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 + assert api_mock.fetch_tickers.call_count == 1 + + api_mock.fetch_tickers.reset_mock() + + # Cached ticker should not call api again + tickers2 = exchange.get_tickers(cached=True) + assert tickers2 == tickers + assert api_mock.fetch_tickers.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a97f6b65e..199845545 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -569,6 +569,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) + assert tickers.call_count == 1 + assert tickers.call_args.kwargs['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From c316531c491c995c62bda57bda177669b8d41690 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 21:54:06 +0200 Subject: [PATCH 302/348] make tests 3.7 compatible --- tests/exchange/test_exchange.py | 2 ++ tests/rpc/test_rpc.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 76095be2d..882cf6b5a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1327,6 +1327,8 @@ def test_get_tickers(default_conf, mocker, exchange_name): tickers2 = exchange.get_tickers(cached=True) assert tickers2 == tickers assert api_mock.fetch_tickers.call_count == 0 + tickers2 = exchange.get_tickers(cached=False) + assert api_mock.fetch_tickers.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 199845545..a548505a7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args.kwargs['cached'] is True + assert tickers.call_args[1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From ba38e398e42359293dd13f00b7b44615771f5fd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 22:17:42 +0200 Subject: [PATCH 303/348] Add type hint --- freqtrade/exchange/exchange.py | 2 +- tests/rpc/test_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3627a07e4..3958f6838 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -86,7 +86,7 @@ class Exchange: self._last_markets_refresh: int = 0 # Cache for 10 minutes ... - self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a548505a7..63e09cbe1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args[1]['cached'] is True + assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From 862df2b431021d43f8ab5fcba198cff07250d336 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 19:43:32 +0200 Subject: [PATCH 304/348] Add blacklist recommendation for kucoin closes #4738 --- docs/exchanges.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 662f2b908..8797ade8c 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -7,10 +7,10 @@ This page combines common gotchas and informations which are exchange-specific a !!! Tip "Stoploss on Exchange" Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. -### Blacklists +### Binance Blacklist For Binance, please add `"BNB/"` to your blacklist to avoid issues. -Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. +Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. ### Binance sites @@ -112,6 +112,11 @@ Kucoin requries a passphrase for each api key, you will therefore need to add th "password": "your_exchange_api_key_password", ``` +### Kucoin Blacklists + +For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. +Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From e820814809f6bef69f1501f16fb797d841034dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:32:34 +0200 Subject: [PATCH 305/348] Default-stoploss-hyperopt should use decimal space, nto real --- freqtrade/optimize/hyperopt_interface.py | 3 ++- freqtrade/optimize/space/__init__.py | 4 ++++ freqtrade/optimize/{ => space}/decimalspace.py | 0 freqtrade/strategy/hyper.py | 2 +- freqtrade/templates/sample_hyperopt_advanced.py | 4 ++-- tests/optimize/test_hyperopt.py | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 freqtrade/optimize/space/__init__.py rename freqtrade/optimize/{ => space}/decimalspace.py (100%) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 633c8bdd5..fa28463e9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict +from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IStrategy @@ -167,7 +168,7 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] def generate_trailing_params(self, params: Dict) -> Dict: diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py new file mode 100644 index 000000000..bbdac4ab9 --- /dev/null +++ b/freqtrade/optimize/space/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa: F401 +from skopt.space import Categorical, Dimension, Integer, Real + +from .decimalspace import SKDecimal diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/space/decimalspace.py similarity index 100% rename from freqtrade/optimize/decimalspace.py rename to freqtrade/optimize/space/decimalspace.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 3fedda974..16b576a73 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,7 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical - from freqtrade.optimize.decimalspace import SKDecimal + from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7736570f7..32ba21716 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Dict, List import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -237,7 +237,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): 'stoploss' optimization hyperspace. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] @staticmethod diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 129fe53d9..59bc4aefb 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,10 +15,10 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException -from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, From 52c482cecfd3133a1e8f741f9b708eec55bd41f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:34:34 +0200 Subject: [PATCH 306/348] Convert trailing and roi defaults to skdecimal --- freqtrade/optimize/hyperopt_interface.py | 15 +++++++++------ freqtrade/templates/sample_hyperopt_advanced.py | 10 +++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index fa28463e9..1bb471b9c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ import math from abc import ABC from typing import Any, Callable, Dict, List -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -155,9 +155,12 @@ class IHyperOpt(ABC): Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'), - Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'), - Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + name='roi_p1'), + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + name='roi_p2'), + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + name='roi_p3'), ] def stoploss_space(self) -> List[Dimension]: @@ -198,14 +201,14 @@ class IHyperOpt(ABC): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 32ba21716..cc13b6ba3 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -223,9 +223,9 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(10, 120, name='roi_t1'), Integer(10, 60, name='roi_t2'), Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), + SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), + SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), + SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), ] @staticmethod @@ -256,14 +256,14 @@ class AdvancedSampleHyperOpt(IHyperOpt): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] From fa343b0484a5850ee9181a7a825ef9f3653d5d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:19:30 +0100 Subject: [PATCH 307/348] Fix get_min_pair_stake_amount formula --- freqtrade/exchange/exchange.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3958f6838..ea6bcb29f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -539,7 +539,9 @@ class Exchange: # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += abs(stoploss) + amount_reserve_percent = ( + amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) From 885096f2b3830e14bf8b1678ad31e696775ddfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:22:52 +0100 Subject: [PATCH 308/348] Update tests for get_min_pair_stake_amount --- tests/exchange/test_exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 882cf6b5a..3bfff50e8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -371,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * 1.1) + assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -383,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * 1.1) + assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -395,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * 1.1) + assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -407,10 +407,10 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * 1.1) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * 1.45) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -432,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) def test_set_sandbox(default_conf, mocker): From ce23d9dfeef32c1a67f0cfbcc28fd39ff4305b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:38:08 +0100 Subject: [PATCH 309/348] Fix test min stake amount --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3bfff50e8..4531bf816 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -410,7 +410,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) + assert isclose(result, max(8, 2 * 2) * 1.5) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) From c9c039d640b59e0794b7c54ff30a1f8412e6c86d Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 15 Apr 2021 15:21:28 +0200 Subject: [PATCH 310/348] remove `copy()` from `custom_info` example `set_index` automatically copies if not stated otherwise with `inplace=True` > inplacebool, default False If True, modifies the DataFrame in place (do not create a new object). from: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.set_index.html?highlight=set_index#pandas.DataFrame.set_index --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7fa824a5b..96c927965 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -57,7 +57,7 @@ class AwesomeStrategy(IStrategy): dataframe['atr'] = ta.ATR(dataframe) if self.dp.runmode.value in ('backtest', 'hyperopt'): # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') return dataframe ``` From 7142787256fd870193699929c377c0706a01599b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 15:41:35 +0200 Subject: [PATCH 311/348] Roll back unintended changes that break rendering --- docs/docker_quickstart.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index b133e33f0..9096000c1 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -14,7 +14,7 @@ To simplify running freqtrade, please install [`docker-compose`](https://docs.do ## Freqtrade with docker-compose -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. !!! Note - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. @@ -24,8 +24,8 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -#### PC/MAC/Linux - +=== "PC/MAC/Linux" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - - -#### RaspberryPi + ``` +=== "RaspberryPi" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,6 +56,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -67,11 +68,12 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -### Question: "How to edit the bot configuration?" -You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. +!!! Question "How to edit the bot configuration?" + You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -##### Adding a custom strategy + You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. + +#### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -79,7 +81,7 @@ You can also change the both Strategy and commands by editing the command sectio The `SampleStrategy` is run by default. -#### Warning "`SampleStrategy` is just a demo!" +!!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -90,7 +92,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -#### Warning "Default configuration" +!!! Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -120,7 +122,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -#### Warning "Check the Changelog" +!!! Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -129,7 +131,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -#### Note "`docker-compose run --rm`" +!!! Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From ce870bbcf7b7b4fe2dfd79884c2da33f225d4af3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 21:38:20 +0200 Subject: [PATCH 312/348] Use 3 decimals for ROI space --- freqtrade/optimize/hyperopt_interface.py | 10 +++++----- freqtrade/optimize/space/decimalspace.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 1bb471b9c..889854cad 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -140,7 +140,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -149,17 +149,17 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=3, name='roi_p1'), - SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=3, name='roi_p2'), - SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=3, name='roi_p3'), ] diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index f5370b6d6..643999cc1 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -9,8 +9,9 @@ class SKDecimal(Integer): self.decimals = decimals _low = int(low * pow(10, self.decimals)) _high = int(high * pow(10, self.decimals)) - self.low_orig = low - self.high_orig = high + # trunc to precision to avoid points out of space + self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals) + self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals) super().__init__(_low, _high, prior, base, transform, name, dtype) From 5e51ba6258c77a33e7b06ed132159ade64029c7d Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:38:00 +0100 Subject: [PATCH 313/348] fix flake8 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6bcb29f..ed7918b36 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -541,7 +541,7 @@ class Exchange: DEFAULT_AMOUNT_RESERVE_PERCENT) amount_reserve_percent = ( amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4531bf816..27f4d0db9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -432,7 +432,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) + assert round(result, 8) == round( + max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), + 8 + ) def test_set_sandbox(default_conf, mocker): From 01b303e0f95bf3c1dc4c7a375b82bcf2f29fc645 Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:58:07 +0100 Subject: [PATCH 314/348] Aligning the doc --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index eb3351b8f..0ade558f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -167,7 +167,7 @@ This exchange has also a limit on USD - where all orders must be > 10$ - which h To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). -With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). +With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take in account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`). To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. From 2011912a19620c337b37a777123e80f86f0c9ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 07:46:00 +0200 Subject: [PATCH 315/348] Adapt documentation to use 3 decimals only --- docs/hyperopt.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 07cc963cf..0e6bded92 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -460,23 +460,26 @@ As stated in the comment, you can also use it as the value of the `minimal_roi` #### Default ROI Search Space -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 3 digits after the decimal point): -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ------------- | -------- | ----------- | ---------- | ------------- | ------------ | ------------- | +| 1 | 0 | 0.011...0.119 | 0 | 0.03...0.31 | 0 | 0.068...0.711 | 0 | 0.121...1.258 | +| 2 | 2...8 | 0.007...0.042 | 10...40 | 0.02...0.11 | 120...480 | 0.045...0.252 | 2880...11520 | 0.081...0.446 | +| 3 | 4...20 | 0.003...0.015 | 20...100 | 0.01...0.04 | 240...1200 | 0.022...0.091 | 5760...28800 | 0.040...0.162 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -516,6 +519,9 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Trailing Stop results If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: @@ -551,6 +557,9 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Reproducible results The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. From 8ce5522a100f2058f7ba20a662fbfba3eb78cf71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 08:00:04 +0200 Subject: [PATCH 316/348] Add additional documentation for SKDecimal space --- docs/advanced-hyperopt.md | 24 +++++++++++++++++++++++- docs/hyperopt.md | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index cc71f39a7..c86978b80 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -79,9 +79,31 @@ class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. def stoploss_space(self): - return [Real(-0.05, -0.01, name='stoploss')] + return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')] ``` +## Space options + +For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: + +* `Categorical` - Pick from a list of categories (e.g. `Categorical(['a', 'b', 'c'], name="cat")`) +* `Integer` - Pick from a range of whole numbers (e.g. `Integer(1, 10, name='rsi')`) +* `SKDecimal` - Pick from a range of decimal numbers with limited precision (e.g. `SKDecimal(0.1, 0.5, decimals=3, name='adx')`). *Available only with freqtrade*. +* `Real` - Pick from a range of decimal numbers with full precision (e.g. `Real(0.1, 0.5, name='adx')` + +You can import all of these from `freqtrade.optimize.space`, although `Categorical`, `Integer` and `Real` are only aliases for their corresponding scikit-optimize Spaces. `SKDecimal` is provided by freqtrade for faster optimizations. + +``` python +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa +``` + +!!! Hint "SKDecimal vs. Real" + We recommend to use `SKDecimal` instead of the `Real` space in almost all cases. While the Real space provides full accuracy (up to ~16 decimal places) - this precision is rarely needed, and leads to unnecessary long hyperopt times. + + Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`). + + A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`). + --- ## Legacy Hyperopt diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0e6bded92..b073a73b6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -294,6 +294,7 @@ Based on the results, hyperopt will tell you which parameter combination produce ## Parameter types There are four parameter types each suited for different purposes. + * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. * `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. From e6936ae1352246125a66685abf9ac35d05fbce3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:16:29 +0200 Subject: [PATCH 317/348] Improve wording in docs --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b073a73b6..21cbadf7f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -479,7 +479,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Stoploss results @@ -521,7 +521,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Trailing Stop results @@ -559,7 +559,7 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Reproducible results From aeb81f90ff0cf375def82f21194c922baa4c4b76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:35:56 +0200 Subject: [PATCH 318/348] Implement errorhandling for /trade endpoint --- freqtrade/rpc/api_server/api_v1.py | 5 ++++- tests/rpc/test_rpc_apiserver.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6873c0c4c..02736aca6 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -90,7 +90,10 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_status([tradeid])[0] + try: + return rpc._rpc_trade_status([tradeid])[0] + except (RPCException, KeyError): + raise HTTPException(status_code=404, detail='Trade not found.') @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..760d78b03 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -522,6 +522,26 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 1 +def test_api_trade_single(botclient, mocker, fee, ticker, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + fetch_ticker=ticker, + ) + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc, 404) + assert rc.json()['detail'] == 'Trade not found.' + + create_mock_trades(fee) + Trade.query.session.flush() + + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc) + assert rc.json()['trade_id'] == 3 + + def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 5c579613e1cf8505f659210ecae518168a0f026b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:42:13 +0200 Subject: [PATCH 319/348] add /trade endpoint to rest_client script --- docs/rest-api.md | 12 ++++++++---- scripts/rest_client.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index be3107fcb..5c25e9eeb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -235,6 +235,9 @@ pair_history performance Return the performance of the different coins. +ping + simple ping + plot_config Return plot configuration if the strategy defines one. @@ -271,15 +274,16 @@ strategy :param strategy: Strategy class name +trade + Return specific trade + + :param trade_id: Specify which trade to get. + trades Return trades history. :param limit: Limits trades to the X last trades. No limit to get all the trades. -trade - Return specific trade. - :param tradeid: Specify which trade to get. - version Return the version of the bot. diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4d667879d..40b338ce8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -127,7 +127,7 @@ class FtRestClient(): return self._delete("locks/{}".format(lock_id)) def daily(self, days=None): - """Return the amount of open trades. + """Return the profits for each day, and amount of trades. :return: json object """ @@ -195,7 +195,7 @@ class FtRestClient(): def logs(self, limit=None): """Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. :return: json object """ return self._get("logs", params={"limit": limit} if limit else 0) @@ -208,6 +208,14 @@ class FtRestClient(): """ return self._get("trades", params={"limit": limit} if limit else 0) + def trade(self, trade_id): + """Return specific trade + + :param trade_id: Specify which trade to get. + :return: json object + """ + return self._get("trade/{}".format(trade_id)) + def delete_trade(self, trade_id): """Delete trade from the database. Tries to close open orders. Requires manual handling of this asset on the exchange. From 1eb9ce4227a0424b5153e8f47d1522fe2ac1de29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:47:32 +0200 Subject: [PATCH 320/348] Allow specifying pairs for optimize commands via `--pairs` --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/configuration/configuration.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9468a7f7d..9cf9992ce 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -17,7 +17,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", - "max_open_trades", "stake_amount", "fee"] + "max_open_trades", "stake_amount", "fee", "pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "enable_protections", "dry_run_wallet", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4fac8ac72..e49895de4 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -330,7 +330,7 @@ AVAILABLE_CLI_OPTIONS = { # Script options "pairs": Arg( '-p', '--pairs', - help='Show profits for only these pairs. Pairs are space-separated.', + help='Limit command to these pairs. Pairs are space-separated.', nargs='+', ), # Download data diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 9acd532cc..cc11f97c2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -445,6 +445,7 @@ class Configuration: """ if "pairs" in config: + config['exchange']['pair_whitelist'] = config['pairs'] return if "pairs_file" in self.args and self.args["pairs_file"]: From 6a9c47d15f7476f72dfb220e643a8801bcf5d058 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:48:24 +0200 Subject: [PATCH 321/348] Update docs with new options --- docs/backtesting.md | 6 +++++- docs/edge.md | 10 +++++++++- docs/hyperopt.md | 11 ++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e16225f94..ee9926f32 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -15,7 +15,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -37,6 +38,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). diff --git a/docs/edge.md b/docs/edge.md index 7f0a9cb2d..237ff36f6 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -215,8 +215,10 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] - [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] + [--fee FLOAT] [-p PAIRS [PAIRS ...]] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -224,6 +226,9 @@ optional arguments: Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -232,6 +237,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --stoplosses STOPLOSS_RANGE Defines a range of stoploss values against which edge will assess the strategy. The format is "min,max,step" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 21cbadf7f..51905e616 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,8 +44,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt NAME] + [--hyperopt-path PATH] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -69,6 +70,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --hyperopt NAME Specify hyperopt class name which will be used by the bot. --hyperopt-path PATH Specify additional lookup path for Hyperopt and @@ -105,7 +109,8 @@ optional arguments: reproducible hyperopt results. --min-trades INT Set minimal desired number of trades for evaluations in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function + --hyperopt-loss NAME, --hyperoptloss NAME + Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in From c8d3d449a3b0fee283999d5782939dc0bbc7ff5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:51:02 +0200 Subject: [PATCH 322/348] Add quick test for pair_whitelist overwrite --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a512bf58a..b2c883108 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1002,6 +1002,7 @@ def test_pairlist_resolving(): config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['pair_whitelist'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' From fbb90755395e205f2235229dc2eb14c25ee88c99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:53:03 +0200 Subject: [PATCH 323/348] Update util command structures too --- docs/data-download.md | 6 +++--- docs/plotting.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 04f444a8b..7a78334d5 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -30,7 +30,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. @@ -48,10 +48,10 @@ optional arguments: exchange/pairs/timeframes. --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. - (default: `json`). + (default: `None`). --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: - `jsongz`). + `None`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/plotting.md b/docs/plotting.md index 63afa16b6..5d454c414 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -37,7 +37,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --indicators1 INDICATORS1 [INDICATORS1 ...] Set indicators from your strategy you want in the @@ -90,6 +90,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` Example: @@ -244,7 +245,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --timerange TIMERANGE Specify what timerange of data to use. @@ -286,6 +287,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation. From 44bfb53668d8a5fd301e0c2187623a6e958589a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:29:34 +0200 Subject: [PATCH 324/348] Don't use current rate for closed trades --- freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 11 +++++++---- tests/rpc/test_rpc_apiserver.py | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 41de0134c..12bee1cf2 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -189,7 +189,6 @@ class OpenTradeSchema(TradeSchema): stoploss_current_dist_ratio: Optional[float] stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] - base_currency: str current_profit: float current_profit_abs: float current_profit_pct: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 88f8b36db..b86562e80 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -167,10 +167,13 @@ class RPC: if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user - try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except (ExchangeError, PricingError): - current_rate = NAN + if trade.is_open: + try: + current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + except (ExchangeError, PricingError): + current_rate = NAN + else: + current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3d27922d8..d610906d5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -773,7 +773,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert rc.json()[0] == { 'amount': 123.0, 'amount_requested': 123.0, - 'base_currency': 'BTC', 'close_date': None, 'close_timestamp': None, 'close_profit': None, From 0737e3fa2230ba27dee588476fa589691123554e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:48:29 +0200 Subject: [PATCH 325/348] Clarify refresh_period section for volumepairlist part of #4689 --- docs/includes/pairlists.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8688494cc..85d157e75 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -60,6 +60,8 @@ When used in the chain of Pairlist Handlers in a non-leading position (after Sta When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). +The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. +Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data. `VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: @@ -90,6 +92,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter Sorts pairs by past trade performance, as follows: + 1. Positive performance. 2. No closed trades yet. 3. Negative performance. From 296ea30cc341345bd87a46f99bbe99e511481ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:35 +0000 Subject: [PATCH 326/348] Bump pytest-asyncio from 0.14.0 to 0.15.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.14.0...v0.15.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cd93f2433..6ddbffb74 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 pytest==6.2.3 -pytest-asyncio==0.14.0 +pytest-asyncio==0.15.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 From 8d2e6954a1fabb833b6e75a5609a4993aeac90bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:41 +0000 Subject: [PATCH 327/348] Bump flake8 from 3.9.0 to 3.9.1 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.0 to 3.9.1. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.0...3.9.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index cd93f2433..35c57769a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.9.0 +flake8==3.9.1 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 From 05246e6637637590909e9e34d0a3a813cc14aa5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:52 +0000 Subject: [PATCH 328/348] Bump pandas from 1.2.3 to 1.2.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.3...v1.2.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..c6051463c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.2 -pandas==1.2.3 +pandas==1.2.4 ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels From 59d02f3f039f4e696ac78f7b2eeb3d7888020224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:27 +0000 Subject: [PATCH 329/348] Bump sqlalchemy from 1.4.7 to 1.4.9 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.7 to 1.4.9. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..5159d69e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.7 +SQLAlchemy==1.4.9 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From b94de3030a11610d48aae70f9e9f182e098c0096 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:33 +0000 Subject: [PATCH 330/348] Bump mkdocs-material from 7.1.1 to 7.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.1...7.1.2) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index cfd63d1d0..4d7082a7f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.1 +mkdocs-material==7.1.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 9407dbcf87eea8a511642045b2910f54de448f49 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 07:46:39 +0200 Subject: [PATCH 331/348] Add freqtrade powered by ccxt --- README.md | 2 +- docs/assets/ccxt-logo.svg | 3 ++ docs/assets/freqtrade_poweredby.svg | 44 +++++++++++++++++++++++++++++ docs/index.md | 5 ++-- 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 docs/assets/ccxt-logo.svg create mode 100644 docs/assets/freqtrade_poweredby.svg diff --git a/README.md b/README.md index c3a665c47..916f9cf17 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Freqtrade +# ![freqtrade](docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) diff --git a/docs/assets/ccxt-logo.svg b/docs/assets/ccxt-logo.svg new file mode 100644 index 000000000..e52682546 --- /dev/null +++ b/docs/assets/ccxt-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg new file mode 100644 index 000000000..1041f87ab --- /dev/null +++ b/docs/assets/freqtrade_poweredby.svg @@ -0,0 +1,44 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + Freqtrade + + + + + poweredby + + diff --git a/docs/index.md b/docs/index.md index 61f2276c3..c2b6d5629 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ -# Freqtrade +![freqtrade](assets/freqtrade_poweredby.svg) + [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) @@ -39,7 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Kraken](https://kraken.com/) -- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +- [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested From 66b3ecfeed6023794d9710dde825f49d46ed0bd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 08:32:17 +0200 Subject: [PATCH 332/348] Remove faulty font-family in svg --- docs/assets/freqtrade_poweredby.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 1041f87ab..71d165cbf 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,7 +34,7 @@ - Freqtrade + Freqtrade From 0ddc68b37d82bb7ee64ce0851cd050bce0b3539f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 09:41:49 +0000 Subject: [PATCH 333/348] Bump ccxt from 1.47.47 to 1.48.22 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.47.47 to 1.48.22. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.47.47...1.48.22) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d71cd1c05..a89eb2383 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.47.47 +ccxt==1.48.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From a2acb54e7efc90fa2ce8af85284518cb840d26fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 15:15:40 +0200 Subject: [PATCH 334/348] Clarify comments in pairlist --- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlist/StaticPairList.py | 2 +- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 184feff9e..c4a9c3e40 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -85,7 +85,7 @@ class IPairList(LoggingMixin, ABC): position in the chain. :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ raise OperationalException("This Pairlist Handler should not be used " diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index c5ced48c9..13d30fc47 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -46,7 +46,7 @@ class StaticPairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ if self._allow_inactive: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index dd8fc64fd..e85fb1805 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -67,7 +67,7 @@ class VolumePairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ # Generate dynamic whitelist From 75612496d7294ca6b0ccd89a62cb16562109d307 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:01:39 +0200 Subject: [PATCH 335/348] Improve poweredBy logo spacing --- docs/assets/freqtrade_poweredby.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 71d165cbf..957ec6401 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,11 +34,11 @@ - Freqtrade + Freqtrade - poweredby + poweredby From c9e901cf325756707e51e4dbda2f060b2fed827b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:31:37 +0200 Subject: [PATCH 336/348] Move wallet tasks to test_wallets --- tests/test_freqtradebot.py | 59 -------------------------------------- tests/test_wallets.py | 59 +++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c91015766..0634df9e4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -207,65 +207,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b freqtrade.get_free_open_trades()) -def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - - -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), - ]) -def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), - get_fee=fee - ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 - conf['max_open_trades'] = 2 - conf['tradable_balance_ratio'] = balance_ratio - - freqtrade = FreqtradeBot(conf) - patch_get_signal(freqtrade) - - # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create 2 trades, order amount should be None - freqtrade.execute_buy('LTC/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - # set max_open_trades = None, so do not trade - conf['max_open_trades'] = 0 - freqtrade = FreqtradeBot(conf) - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - def test_edge_called_in_process(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_edge(mocker) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index b7aead0c4..e6e41bab1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,12 @@ # pragma pylint: disable=missing-docstring +from copy import deepcopy from unittest.mock import MagicMock -from tests.conftest import get_patched_freqtradebot +import pytest + +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException +from tests.conftest import get_patched_freqtradebot, patch_wallet def test_sync_wallet_at_boot(mocker, default_conf): @@ -106,3 +111,55 @@ def test_sync_wallet_missing_data(mocker, default_conf): assert freqtrade.wallets._wallets['GAS'].used is None assert freqtrade.wallets._wallets['GAS'].total == 0.260739 assert freqtrade.wallets.get_free('GAS') == 0.260739 + + +def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: + patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + + +@pytest.mark.parametrize("balance_ratio,result1", [ + (1, 0.005), + (0.99, 0.00495), + (0.50, 0.0025), +]) +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, + limit_buy_order_open, fee, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + buy=MagicMock(return_value=limit_buy_order_open), + get_fee=fee + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT + conf['dry_run_wallet'] = 0.01 + conf['max_open_trades'] = 2 + conf['tradable_balance_ratio'] = balance_ratio + + freqtrade = get_patched_freqtradebot(mocker, conf) + + # no open trades, order amount should be 'balance / max_open_trades' + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' + freqtrade.execute_buy('ETH/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create 2 trades, order amount should be None + freqtrade.execute_buy('LTC/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + assert result == 0 + + # set max_open_trades = None, so do not trade + freqtrade.config['max_open_trades'] = 0 + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + assert result == 0 From bd7e535e42ccca777697f497d0e9264698c4421a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:58:47 +0200 Subject: [PATCH 337/348] Use "human" amounts in stake_amount tests --- tests/test_wallets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index e6e41bab1..562957790 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -122,9 +122,9 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), + (1, 50), + (0.99, 49.5), + (0.50, 25), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, limit_buy_order_open, fee, mocker) -> None: @@ -137,29 +137,29 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r conf = deepcopy(default_conf) conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 + conf['dry_run_wallet'] = 100 conf['max_open_trades'] = 2 conf['tradable_balance_ratio'] = balance_ratio freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) + freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) assert result == 0 From 2254f65fa7f8c32d2d23d559fa57f297bf424dd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 12:54:22 +0200 Subject: [PATCH 338/348] use binance intests instead of bittrex --- tests/commands/test_commands.py | 4 +- tests/config_test_comments.json | 2 +- tests/conftest.py | 8 ++-- tests/conftest_trades.py | 12 ++--- tests/plugins/test_protections.py | 2 +- tests/rpc/test_rpc.py | 4 +- tests/rpc/test_rpc_apiserver.py | 8 ++-- tests/rpc/test_rpc_manager.py | 2 +- tests/rpc/test_rpc_telegram.py | 22 ++++----- tests/rpc/test_rpc_webhook.py | 10 ++-- tests/strategy/test_interface.py | 8 ++-- tests/test_freqtradebot.py | 77 ++++++++++++++++++++----------- tests/test_persistence.py | 54 +++++++++++----------- 13 files changed, 117 insertions(+), 96 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 232fc4e2c..d86bced5d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -116,7 +116,7 @@ def test_list_timeframes(mocker, capsys): '1h': 'hour', '1d': 'day', } - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') args = [ "list-timeframes", ] @@ -201,7 +201,7 @@ def test_list_markets(mocker, markets, capsys): api_mock = MagicMock() api_mock.markets = markets - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') # Test with no --config args = [ diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 4f201f86c..48a087dec 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -59,7 +59,7 @@ } }, "exchange": { - "name": "bittrex", + "name": "binance", "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", diff --git a/tests/conftest.py b/tests/conftest.py index 4a2106a4d..cc4fe91f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,7 +79,7 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: +def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -98,7 +98,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', +def get_patched_exchange(mocker, config, api_mock=None, id='binance', mock_markets=True) -> Exchange: patch_exchange(mocker, api_mock, id, mock_markets) config['exchange']['name'] = id @@ -293,7 +293,7 @@ def get_default_conf(testdatadir): "order_book_max": 1 }, "exchange": { - "name": "bittrex", + "name": "binance", "enabled": True, "key": "key", "secret": "secret", @@ -1765,7 +1765,7 @@ def open_trade(): return Trade( pair='ETH/BTC', open_rate=0.00001099, - exchange='bittrex', + exchange='binance', open_order_id='123456789', amount=90.99181073, fee_open=0.0, diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 34fc58aee..b92b51144 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -31,7 +31,7 @@ def mock_trade_1(fee): is_open=True, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -84,7 +84,7 @@ def mock_trade_2(fee): close_rate=0.128, close_profit=0.005, close_profit_abs=0.000584127, - exchange='bittrex', + exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', @@ -144,7 +144,7 @@ def mock_trade_3(fee): close_rate=0.06, close_profit=0.01, close_profit_abs=0.000155, - exchange='bittrex', + exchange='binance', is_open=False, strategy='DefaultStrategy', timeframe=5, @@ -187,7 +187,7 @@ def mock_trade_4(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -239,7 +239,7 @@ def mock_trade_5(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', stoploss_order_id='prod_stoploss_3455', timeframe=5, @@ -293,7 +293,7 @@ def mock_trade_6(fee): fee_close=fee.return_value, is_open=True, open_rate=0.15, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', open_order_id="prod_sell_6", timeframe=5, diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 545387eaa..a39301145 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -27,7 +27,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, - exchange='bittrex', + exchange='binance', ) trade.recalc_open_trade_value() if not is_open: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 63e09cbe1..6d31e7635 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -106,7 +106,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -172,7 +172,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d610906d5..6505629eb 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -468,7 +468,7 @@ def test_api_show_config(botclient, mocker): rc = client_get(client, f"{BASE_URI}/show_config") assert_response(rc) assert 'dry_run' in rc.json() - assert rc.json()['exchange'] == 'bittrex' + assert rc.json()['exchange'] == 'binance' assert rc.json()['timeframe'] == '5m' assert rc.json()['timeframe_ms'] == 300000 assert rc.json()['timeframe_min'] == 5 @@ -825,7 +825,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -916,7 +916,7 @@ def test_api_forcebuy(botclient, mocker, fee): pair='ETH/ETH', amount=1, amount_requested=1, - exchange='bittrex', + exchange='binance', stake_amount=1, open_rate=0.245441, open_order_id="123456", @@ -979,7 +979,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 3068e9764..6996c932b 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -140,7 +140,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) assert telegram_mock.call_count == 3 - assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] + assert "*Exchange:* `binance`" in telegram_mock.call_args_list[1][0][0]['status'] telegram_mock.reset_mock() default_conf['dry_run'] = True diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 34bf057cb..ba32dc385 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -688,7 +688,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.173e-05, @@ -749,7 +749,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.043e-05, @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.099e-05, @@ -1178,7 +1178,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1187,7 +1187,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1197,7 +1197,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1213,7 +1213,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \ + == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1242,11 +1242,11 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) - assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' + assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* ' 'Cancelling open buy Order for ETH/BTC (#1). ' 'Reason: cancelled due to timeout.') @@ -1393,7 +1393,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1405,7 +1405,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 5361cd947..62818ecbb 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -59,7 +59,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -80,7 +80,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -101,7 +101,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -127,7 +127,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -184,7 +184,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'order_type': 'limit', diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 0ee80e0c5..78fa368e4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -219,7 +219,7 @@ def test_min_roi_reached(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -258,7 +258,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -293,7 +293,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -346,7 +346,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) trade.adjust_min_max_rates(trade.open_rate) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0634df9e4..433cce170 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -362,7 +362,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -621,7 +621,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.stake_amount == default_conf['stake_amount'] assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert trade.open_rate == 0.00001098 assert trade.amount == 91.07468123 @@ -718,7 +718,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', )) Trade.query.session.add(Trade( pair='ETH/BTC', @@ -728,7 +728,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=12, open_rate=0.001, - exchange='bittrex', + exchange='binance', )) assert pair not in freqtrade.active_pair_whitelist @@ -969,7 +969,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None return_value=limit_buy_order['amount']) stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1001,6 +1001,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss ) freqtrade = FreqtradeBot(default_conf) @@ -1025,7 +1028,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1038,7 +1041,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1064,14 +1067,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False mocker.patch( - 'freqtrade.exchange.Exchange.stoploss', + 'freqtrade.exchange.Binance.stoploss', side_effect=ExchangeError() ) trade.is_open = True @@ -1083,9 +1086,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1095,7 +1098,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.is_open = False stoploss.reset_mock() mocker.patch('freqtrade.exchange.Exchange.fetch_order') - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1115,6 +1118,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), stoploss=MagicMock(side_effect=ExchangeError()), ) @@ -1149,6 +1155,9 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value=limit_buy_order_open), sell=sell_mock, get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) @@ -1194,6 +1203,9 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, sell=sell_mock, get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=MagicMock(side_effect=InsufficientFundsError()), ) patch_get_signal(freqtrade) @@ -1231,6 +1243,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1271,7 +1286,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1286,8 +1301,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1334,6 +1349,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1369,9 +1387,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1380,8 +1398,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) + cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) + mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1403,6 +1421,9 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1443,7 +1464,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1457,8 +1478,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -2603,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert { 'trade_id': 1, 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -2653,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.044e-05, @@ -2710,7 +2731,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.08801e-05, @@ -2916,7 +2937,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -3899,7 +3920,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert len(Trade.query.all()) == 1 @@ -4355,7 +4376,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', ) Trade.query.session.add(trade) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0a3d6858d..3b90f368f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -64,7 +64,7 @@ def test_init_dryrun_db(default_conf, tmpdir): @pytest.mark.usefixtures("init_persistence") -def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): +def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ On this test we will buy and sell a crypto currency. @@ -102,7 +102,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None assert trade.close_profit is None @@ -142,7 +142,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().datetime, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -177,7 +177,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -205,7 +205,7 @@ def test_trade_close(limit_buy_order, limit_sell_order, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, - exchange='bittrex', + exchange='binance', ) assert trade.close_profit is None assert trade.close_date is None @@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -250,7 +250,7 @@ def test_update_open_order(limit_buy_order): amount=5, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None @@ -274,7 +274,7 @@ def test_update_invalid_order(limit_buy_order): open_rate=0.001, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -290,7 +290,7 @@ def test_calc_open_trade_value(limit_buy_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'open_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -311,7 +311,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'close_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -336,7 +336,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -370,7 +370,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -400,7 +400,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) Trade.query.session.add(trade) @@ -412,7 +412,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_sell_12345' ) Trade.query.session.add(trade) @@ -425,7 +425,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345' ) Trade.query.session.add(trade) @@ -463,7 +463,7 @@ def test_migrate_old(mocker, default_conf, fee): );""" insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee, open_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee}, + VALUES ('binance', 'BTC_ETC', 1, '123123', {fee}, 0.00258580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -472,7 +472,7 @@ def test_migrate_old(mocker, default_conf, fee): ) insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee, open_rate, close_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 0, {fee}, + VALUES ('binance', 'BTC_ETC', 0, {fee}, 0.00258580, 0.00268580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -500,7 +500,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.amount_requested == amount assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" - assert trade.exchange == "bittrex" + assert trade.exchange == "binance" assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 @@ -694,7 +694,7 @@ def test_adjust_stop_loss(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -746,7 +746,7 @@ def test_adjust_min_max_rates(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -790,7 +790,7 @@ def test_to_json(default_conf, fee): fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) result = trade.to_json() @@ -841,7 +841,7 @@ def test_to_json(default_conf, fee): 'max_rate': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } # Simulate dry_run entries @@ -856,7 +856,7 @@ def test_to_json(default_conf, fee): close_date=arrow.utcnow().shift(hours=-1).datetime, open_rate=0.123, close_rate=0.125, - exchange='bittrex', + exchange='binance', ) result = trade.to_json() assert isinstance(result, dict) @@ -906,7 +906,7 @@ def test_to_json(default_conf, fee): 'sell_order_status': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } @@ -919,7 +919,7 @@ def test_stoploss_reinitialization(default_conf, fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -978,7 +978,7 @@ def test_update_fee(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -1017,7 +1017,7 @@ def test_fee_updated(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) From 71b017e7c34c837b13e039740154cd0896d7bf79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:53:16 +0200 Subject: [PATCH 339/348] Simplify webhook test --- tests/rpc/test_rpc_webhook.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 62818ecbb..bfb9cbb01 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -25,6 +25,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhooksell": { "value1": "Selling {pair}", "value2": "limit {limit:8f}", @@ -35,6 +40,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -76,8 +86,8 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) # Test buy cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() + msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Binance', @@ -97,8 +107,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) # Test sell - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -123,8 +132,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) # Test sell cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'exchange': 'Binance', From fecd5c582b81e191b82a5d90834b0592eb1ffbe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:58:29 +0200 Subject: [PATCH 340/348] Add buy and sell fill notifications closes #3542 --- config_full.json.example | 2 ++ docs/webhook-config.md | 46 +++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 28 ++++++++++++++++--- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 8 ++++++ freqtrade/rpc/webhook.py | 4 +++ tests/rpc/test_rpc_webhook.py | 51 +++++++++++++++++++++++++++++++++-- 7 files changed, 136 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 717797933..973afe2c8 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -163,7 +163,9 @@ "warning": "on", "startup": "on", "buy": "on", + "buy_fill": "on", "sell": "on", + "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" } diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2e41ad2cc..8ce6edc18 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT). "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "" }, "webhooksell": { "value1": "Selling {pair}", @@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT). "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}.", + "value3": "" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -91,6 +101,21 @@ Possible parameters are: * `order_type` * `current_rate` +### Webhookbuyfill + +The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `open_rate` +* `amount` +* `open_date` +* `stake_amount` +* `stake_currency` +* `fiat_currency` + ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. @@ -103,6 +128,27 @@ Possible parameters are: * `limit` * `amount` * `open_rate` +* `profit_amount` +* `profit_ratio` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` +* `open_date` +* `close_date` + +### Webhooksellfill + +The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `gain` +* `close_rate` +* `amount` +* `open_rate` * `current_rate` * `profit_amount` * `profit_ratio` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..68f98ec21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -675,6 +675,21 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def _notify_buy_fill(self, trade: Trade) -> None: + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'open_rate': trade.open_rate, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + } + self.rpc.send_msg(msg) + # # SELL / exit positions / close trades logic and methods # @@ -1212,19 +1227,20 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_sell(self, trade: Trade, order_type: str) -> None: + def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occured. """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.get_sell_rate(trade.pair, False) + current_rate = self.get_sell_rate(trade.pair, False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill + else RPCMessageType.SELL_NOTIFICATION), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1233,6 +1249,7 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'amount': trade.amount, 'open_rate': trade.open_rate, + 'close_rate': trade.close_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, @@ -1344,9 +1361,14 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() + elif trade.open_order_id is None: + # Buy fill + self._notify_buy_fill(trade) + return False def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..bf0b88f6c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -35,8 +35,10 @@ class RPCMessageType(Enum): WARNING_NOTIFICATION = 'warning' STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' + BUY_FILL_NOTIFICATION = 'buy_fill' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' + SELL_FILL_NOTIFICATION = 'sell_fill' SELL_CANCEL_NOTIFICATION = 'sell_cancel' def __repr__(self): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 09b7b235c..4dceeb46c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -209,6 +209,10 @@ class Telegram(RPCHandler): "Cancelling open buy Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) @@ -240,6 +244,10 @@ class Telegram(RPCHandler): message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 5a30a9be8..c7e012af5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -49,8 +49,12 @@ class Webhook(RPCHandler): valuedict = self._config['webhook'].get('webhookbuy', None) elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhookbuycancel', None) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookbuyfill', None) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksellfill', None) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index bfb9cbb01..38d2fe539 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -43,7 +43,7 @@ def get_webhook_dict() -> dict: "webhooksellfill": { "value1": "Sell Order for {pair} filled", "value2": "at {close_rate:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "" }, "webhookstatus": { "value1": "Status: {status}", @@ -59,7 +59,7 @@ def test__init__(mocker, default_conf): assert webhook._config == default_conf -def test_send_msg(default_conf, mocker): +def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -106,6 +106,27 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + # Test buy fill + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'open_rate': 0.005, + 'stake_amount': 0.8, + 'stake_amount_fiat': 500, + 'stake_currency': 'BTC', + 'fiat_currency': 'EUR' + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) # Test sell msg_mock.reset_mock() msg = { @@ -156,6 +177,32 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) + # Test Sell fill + msg_mock.reset_mock() + msg = { + 'type': RPCMessageType.SELL_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': "profit", + 'close_rate': 0.005, + 'amount': 0.8, + 'order_type': 'limit', + 'open_rate': 0.004, + 'current_rate': 0.005, + 'profit_amount': 0.001, + 'profit_ratio': 0.20, + 'stake_currency': 'BTC', + 'sell_reason': SellType.STOP_LOSS.value + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) + for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.STARTUP_NOTIFICATION]: From 8800a097700077955bc5149d567c1df92e1f9445 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 21:32:04 +0200 Subject: [PATCH 341/348] Don't send double-notifications for stoploss fills --- freqtrade/freqtradebot.py | 3 ++- tests/rpc/test_rpc_telegram.py | 13 ++++++++----- tests/test_freqtradebot.py | 23 +++++++++++++++-------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68f98ec21..76212bf97 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,7 +1361,8 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - self._notify_sell(trade, '', True) + if not stoploss_order: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ba32dc385..a3c823aac 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -683,7 +683,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -703,6 +703,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -743,7 +744,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { @@ -764,6 +765,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -794,9 +796,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for each trade 3 times - assert msg_mock.call_count == 8 - msg = msg_mock.call_args_list[1][0][0] + # Called for each trade 4 times + assert msg_mock.call_count == 12 + msg = msg_mock.call_args_list[2][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -815,6 +817,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == msg diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..39c3f0561 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1710,6 +1710,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_rate=0.01, open_date=arrow.utcnow().datetime, amount=11, + exchange="binance", ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) @@ -2262,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 @@ -2319,7 +2320,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # note this is for a partially-complete buy order freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 @@ -2354,7 +2355,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap assert log_has_re(r"Applying fee on amount for Trade.*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2394,7 +2395,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert log_has_re(r"Could not update trade amount: .*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2639,6 +2640,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2689,6 +2691,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2746,7 +2749,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, - + 'close_rate': ANY, } == last_msg @@ -2830,7 +2833,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke trade = Trade.query.first() assert trade assert cancel_order.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, @@ -2898,7 +2901,10 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.stoploss_order_id is None assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2932,7 +2938,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert not trade.is_open assert trade.close_profit == 0.0620716 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -2952,6 +2958,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg From 0341ac5a55bc0a985776c38137489bc81960e80a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:41:58 +0200 Subject: [PATCH 342/348] rename RPC message types --- freqtrade/freqtradebot.py | 16 ++--- freqtrade/rpc/rpc.py | 18 ++--- freqtrade/rpc/rpc_manager.py | 8 +-- freqtrade/rpc/telegram.py | 117 +++++++++++++++++---------------- freqtrade/rpc/webhook.py | 18 ++--- tests/rpc/test_rpc_manager.py | 6 +- tests/rpc/test_rpc_telegram.py | 28 ++++---- tests/rpc/test_rpc_webhook.py | 28 ++++---- tests/test_freqtradebot.py | 14 ++-- 9 files changed, 128 insertions(+), 125 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 76212bf97..c48ea851e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -113,7 +113,7 @@ class FreqtradeBot(LoggingMixin): via RPC about changes in the bot status. """ self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': msg }) @@ -205,7 +205,7 @@ class FreqtradeBot(LoggingMixin): if len(open_trades) != 0: msg = { - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': f"{len(open_trades)} open trades active.\n\n" f"Handle these trades manually on {self.exchange.name}, " f"or '/start' the bot again and use '/stopbuy' " @@ -634,7 +634,7 @@ class FreqtradeBot(LoggingMixin): """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -658,7 +658,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -678,7 +678,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy_fill(self, trade: Trade) -> None: msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'type': RPCMessageType.BUY_FILL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'open_rate': trade.open_rate, @@ -1239,8 +1239,8 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill - else RPCMessageType.SELL_NOTIFICATION), + 'type': (RPCMessageType.SELL_FILL if fill + else RPCMessageType.SELL), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1284,7 +1284,7 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index bf0b88f6c..e5c0dffba 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -31,15 +31,15 @@ logger = logging.getLogger(__name__) class RPCMessageType(Enum): - STATUS_NOTIFICATION = 'status' - WARNING_NOTIFICATION = 'warning' - STARTUP_NOTIFICATION = 'startup' - BUY_NOTIFICATION = 'buy' - BUY_FILL_NOTIFICATION = 'buy_fill' - BUY_CANCEL_NOTIFICATION = 'buy_cancel' - SELL_NOTIFICATION = 'sell' - SELL_FILL_NOTIFICATION = 'sell_fill' - SELL_CANCEL_NOTIFICATION = 'sell_cancel' + STATUS = 'status' + WARNING = 'warning' + STARTUP = 'startup' + BUY = 'buy' + BUY_FILL = 'buy_fill' + BUY_CANCEL = 'buy_cancel' + SELL = 'sell' + SELL_FILL = 'sell_fill' + SELL_CANCEL = 'sell_cancel' def __repr__(self): return self.value diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7977d68de..f819b55b4 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -67,7 +67,7 @@ class RPCManager: def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'Dry run is enabled. All trades are simulated.' }) stake_currency = config['stake_currency'] @@ -79,7 +79,7 @@ class RPCManager: exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'*Exchange:* `{exchange_name}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' @@ -88,13 +88,13 @@ class RPCManager: f'*Strategy:* `{strategy_name}`' }) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) if len(protections.name_list) > 0: prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()]) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Using Protections: \n{prots}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4dceeb46c..778baea3c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -176,6 +176,53 @@ class Telegram(RPCHandler): """ self._updater.stop() + def _format_buy_msg(self, msg: Dict[str, Any]) -> str: + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") + + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + message += ")`" + return message + + def _format_sell_msg(self, msg: Dict[str, Any]) -> str: + msg['amount'] = round(msg['amount'], 8) + msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) + msg['duration'] = msg['close_date'].replace( + microsecond=0) - msg['open_date'].replace(microsecond=0) + msg['duration_min'] = msg['duration'].total_seconds() / 60 + + msg['emoji'] = self._get_sell_emoji(msg) + + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) + + # Check if all sell properties are available. + # This might not be the case if the message origin is triggered by /forcesell + if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) + and self._rpc._fiat_converter): + msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) + message += (' `({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) + return message + def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ @@ -186,75 +233,31 @@ class Telegram(RPCHandler): # Notification disabled return - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._rpc._fiat_converter: - msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) - else: - msg['stake_amount_fiat'] = 0 + if msg['type'] == RPCMessageType.BUY: + message = self._format_buy_msg(msg) - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" - f" (#{msg['trade_id']})\n" - f"*Amount:* `{msg['amount']:.8f}`\n" - f"*Open Rate:* `{msg['limit']:.8f}`\n" - f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") - - if msg.get('fiat_currency', None): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - message += ")`" - - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell' message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open buy Order for {pair} (#{trade_id}). " + "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' + message = ("\N{LARGE CIRCLE} *{exchange}:* " "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: - msg['amount'] = round(msg['amount'], 8) - msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) - msg['duration'] = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 + elif msg['type'] == RPCMessageType.SELL: + message = self._format_sell_msg(msg) - msg['emoji'] = self._get_sell_emoji(msg) - - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) - - # Check if all sell properties are available. - # This might not be the case if the message origin is triggered by /forcesell - if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._rpc._fiat_converter): - msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - message += (' `({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) - - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: - message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) - - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: - message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) - - elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + elif msg['type'] == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: + elif msg['type'] == RPCMessageType.WARNING: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: + elif msg['type'] == RPCMessageType.STARTUP: message = '{status}'.format(**msg) else: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index c7e012af5..24e1348f1 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -45,21 +45,21 @@ class Webhook(RPCHandler): """ Send a message to telegram channel """ try: - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + if msg['type'] == RPCMessageType.BUY: valuedict = self._config['webhook'].get('webhookbuy', None) - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_CANCEL: valuedict = self._config['webhook'].get('webhookbuycancel', None) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_FILL: valuedict = self._config['webhook'].get('webhookbuyfill', None) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL: valuedict = self._config['webhook'].get('webhooksell', None) - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_FILL: valuedict = self._config['webhook'].get('webhooksellfill', None) - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_CANCEL: valuedict = self._config['webhook'].get('webhooksellcancel', None) - elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION): + elif msg['type'] in (RPCMessageType.STATUS, + RPCMessageType.STARTUP, + RPCMessageType.WARNING): valuedict = self._config['webhook'].get('webhookstatus', None) else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 6996c932b..69a757fcf 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -71,7 +71,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -86,7 +86,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -124,7 +124,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] - rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, + rpc_manager.send_msg({'type': RPCMessageType.STARTUP, 'status': 'TestMessage'}) assert log_has( "Message type 'startup' not implemented by handler webhook.", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a3c823aac..accb94d34 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -686,7 +686,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -748,7 +748,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert msg_mock.call_count == 12 msg = msg_mock.call_args_list[2][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1198,7 +1198,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1243,7 +1243,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1261,7 +1261,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1291,7 +1291,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1328,7 +1328,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1340,7 +1340,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1357,7 +1357,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'running' }) assert msg_mock.call_args[0][0] == '*Status:* `running`' @@ -1366,7 +1366,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: def test_warning_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'message' }) assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' @@ -1375,7 +1375,7 @@ def test_warning_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': '*Custom:* `Hello World`' }) assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' @@ -1394,7 +1394,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1420,7 +1420,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 38d2fe539..0560f8d53 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -68,7 +68,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -89,7 +89,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -110,8 +110,8 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.BUY_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'open_rate': 0.005, 'stake_amount': 0.8, @@ -130,7 +130,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -155,7 +155,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell cancel msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -180,8 +180,8 @@ def test_send_msg_webhook(default_conf, mocker): # Test Sell fill msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.SELL_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'close_rate': 0.005, @@ -203,9 +203,9 @@ def test_send_msg_webhook(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) - for msgtype in [RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION]: + for msgtype in [RPCMessageType.STATUS, + RPCMessageType.WARNING, + RPCMessageType.STARTUP]: # Test notification msg = { 'type': msgtype, @@ -228,8 +228,8 @@ def test_exception_send_msg(default_conf, mocker, caplog): del default_conf["webhook"]["webhookbuy"] webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) - assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", + webhook.send_msg({'type': RPCMessageType.BUY}) + assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() @@ -238,7 +238,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 39c3f0561..25239d503 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2624,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'trade_id': 1, - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -2674,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2732,7 +2732,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2902,9 +2902,9 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 - assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION - assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION - assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2941,7 +2941,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', From d740aae8ca96a221a744b69fb16f708b8a632623 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:49:29 +0200 Subject: [PATCH 343/348] Default fill notifications to off --- freqtrade/constants.py | 14 ++++++++++++-- freqtrade/freqtradebot.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7b955c37d..aea6e1ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -246,14 +246,24 @@ CONF_SCHEMA = { 'balance_dust_level': {'type': 'number', 'minimum': 0.0}, 'notification_settings': { 'type': 'object', + 'default': {}, 'properties': { 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS} + 'buy_fill': {'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, + 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_fill': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, } } }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c48ea851e..ad55b38f8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,12 +1361,12 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - if not stoploss_order: + if not stoploss_order and not trade.open_order_id: self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() - elif trade.open_order_id is None: + elif not trade.open_order_id: # Buy fill self._notify_buy_fill(trade) From efbe0843be6a05d623a77c8e91adfce2158ca9f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 07:57:34 +0200 Subject: [PATCH 344/348] Add documentation for fill messages --- docs/telegram-usage.md | 9 ++++++++- tests/rpc/test_rpc_telegram.py | 8 ++++---- tests/test_freqtradebot.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 377977892..824cb17c7 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -82,12 +82,19 @@ Example configuration showing the different settings: "buy": "silent", "sell": "on", "buy_cancel": "silent", - "sell_cancel": "on" + "sell_cancel": "on", + "buy_fill": "off", + "sell_fill": "off" }, "balance_dust_level": 0.01 }, ``` +`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. +`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. +`*_fill` notifications are off by default and must be explicitly enabled. + + `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. ## Create a custom keyboard (command shortcut buttons) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index accb94d34..718c1d3a0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1335,8 +1335,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange.') msg_mock.reset_mock() telegram.send_msg({ @@ -1347,8 +1347,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: timeout.') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 25239d503..44791f928 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2263,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 From f821ef5aec8fb8321b75bfbe9ab33e0eed380ef5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 19:36:30 +0200 Subject: [PATCH 345/348] Final finetunings of rpc_fill messages --- freqtrade/rpc/telegram.py | 2 +- tests/conftest.py | 3 ++- tests/rpc/test_rpc_telegram.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 778baea3c..ffe7a7ceb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,7 +242,7 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " diff --git a/tests/conftest.py b/tests/conftest.py index cc4fe91f0..788586134 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -314,7 +314,8 @@ def get_default_conf(testdatadir): "telegram": { "enabled": True, "token": "token", - "chat_id": "0" + "chat_id": "0", + "notification_settings": {}, }, "datadir": str(testdatadir), "initial_state": "running", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 718c1d3a0..d72ba36ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1254,6 +1254,25 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'Reason: cancelled due to timeout.') +def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['buy_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.BUY_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'open_rate': 200, + 'stake_amount': 100, + 'amount': 0.5, + 'open_date': arrow.utcnow().datetime + }) + assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* ' + 'Buy order for ETH/USDT (#1) filled for 200.') + + def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From fd110c7d625fc8b5bc12445fe94998eb1f10eb31 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Tue, 20 Apr 2021 11:50:53 -0600 Subject: [PATCH 346/348] The error that it prints says the contrary to what was evaluated. ex. Trading stopped due to Max Drawdown 0.79 < 0.2 within 48 candles --- freqtrade/plugins/protections/max_drawdown_protection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d1c6b192d..67e204039 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -61,7 +61,7 @@ class MaxDrawdown(IProtection): if drawdown > self._max_allowed_drawdown: self.log_once( - f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" + f"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}" f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) From cfa9315e2a3dd1706bca0f09a4e1e48315912ec3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 20:29:53 +0200 Subject: [PATCH 347/348] Prevent out of candle ROI sells --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..a1d4a2578 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -239,7 +239,7 @@ class Backtesting: # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - return max(close_rate, sell_row[LOW_IDX]) + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... From 9f6f3e0862b27693706ecbb44be0856834d05717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:41:18 +0200 Subject: [PATCH 348/348] Address ZeroDivisionExceptiond closes #4764 closes #4617 --- freqtrade/persistence/models.py | 2 ++ tests/test_persistence.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49d3e2d62..e7fd488c7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -547,6 +547,8 @@ class LocalTrade(): rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) + if self.open_trade_value == 0.0: + return 0.0 profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3b90f368f..dad0e275e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -388,6 +388,9 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): # Test with a custom fee rate on the close trade assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + trade.open_trade_value = 0.0 + assert trade.calc_profit_ratio(fee=0.003) == 0.0 + @pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee):