From a43c088448f4f95fe57caaa68ded70803f7fa368 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 07:11:59 +0100 Subject: [PATCH 01/25] Allow @informative in webserver mode --- freqtrade/rpc/api_server/api_v1.py | 8 +++++--- freqtrade/rpc/api_server/deps.py | 10 ++++++++++ freqtrade/rpc/api_server/webserver.py | 2 ++ freqtrade/rpc/rpc.py | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 1c1ff39df..648ec6c57 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -20,7 +20,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac Stats, StatusMsg, StrategyListResponse, StrategyResponse, SysInfo, Version, WhitelistResponse) -from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional +from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -217,12 +217,14 @@ def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Dep @router.get('/pair_history', response_model=PairHistory, tags=['candle data']) def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, - config=Depends(get_config)): + config=Depends(get_config), exchange=Depends(get_exchange)): + # The initial call to this endpoint can be slow, as it may need to initialize + # the exchange class. config = deepcopy(config) config.update({ 'strategy': strategy, }) - return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) + return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange, exchange) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index 16f9a78c0..b428d9c6d 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,5 +1,7 @@ from typing import Any, Dict, Iterator, Optional +from fastapi import Depends + from freqtrade.persistence import Trade from freqtrade.rpc.rpc import RPC, RPCException @@ -28,3 +30,11 @@ def get_config() -> Dict[str, Any]: def get_api_config() -> Dict[str, Any]: return ApiServer._config['api_server'] + + +def get_exchange(config=Depends(get_config)): + if not ApiServer._exchange: + from freqtrade.resolvers import ExchangeResolver + ApiServer._exchange = ExchangeResolver.load_exchange( + config['exchange']['name'], config) + return ApiServer._exchange diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 235063191..63812f52f 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -41,6 +41,8 @@ class ApiServer(RPCHandler): _has_rpc: bool = False _bgtask_running: bool = False _config: Dict[str, Any] = {} + # Exchange - only available in webserver mode. + _exchange = None def __new__(cls, *args, **kwargs): """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e969aa0a7..2232496de 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -995,7 +995,7 @@ class RPC: @staticmethod def _rpc_analysed_history_full(config, pair: str, timeframe: str, - timerange: str) -> Dict[str, Any]: + timerange: str, exchange) -> Dict[str, Any]: timerange_parsed = TimeRange.parse_timerange(timerange) _data = load_data( @@ -1010,7 +1010,7 @@ class RPC: from freqtrade.data.dataprovider import DataProvider from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) - strategy.dp = DataProvider(config, exchange=None, pairlists=None) + strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) From 314a5448811a0723d5f941c096eac7b02747522a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 08:08:50 +0100 Subject: [PATCH 02/25] Add Failing test for get_strategy_run_id Fails because max_open_trades is "inf" emulates behaviour of `max_open_trades=-1` when loading the configuration) --- tests/optimize/test_backtesting.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index edd0faba3..4caf4aec1 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -21,6 +21,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.enums import RunMode, SellType from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.misc import get_strategy_run_id from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver @@ -1357,3 +1358,13 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda for line in exists: assert log_has(line, caplog) + + +def test_get_strategy_run_id(default_conf_usdt): + default_conf_usdt.update({ + 'strategy': 'StrategyTestV2', + 'max_open_trades': float('inf') + }) + strategy = StrategyResolver.load_strategy(default_conf_usdt) + x = get_strategy_run_id(strategy) + assert isinstance(x, str) From 9ecd7400c8316469fd3ae981dc86e4732a991df7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 08:10:09 +0100 Subject: [PATCH 03/25] Allow NaN when calculating digests --- freqtrade/misc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index f09e5ee47..2a27f1660 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -248,8 +248,10 @@ def get_strategy_run_id(strategy) -> str: if k in config: del config[k] + # Explicitly allow NaN values (e.g. max_open_trades). + # as it does not matter for getting the hash. digest.update(rapidjson.dumps(config, default=str, - number_mode=rapidjson.NM_NATIVE).encode('utf-8')) + number_mode=rapidjson.NM_NAN).encode('utf-8')) with open(strategy.__file__, 'rb') as fp: digest.update(fp.read()) return digest.hexdigest().lower() From a35b0b519a18d57b9b28e954cdcebf8763885e25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 13:26:02 +0100 Subject: [PATCH 04/25] Update forcebuy endpoint to support stake-amount closes #6223 --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 7 +++++-- freqtrade/rpc/rpc.py | 11 ++++++----- tests/rpc/test_rpc.py | 7 ++++++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 10f181bb6..bbd858795 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -277,6 +277,7 @@ class ForceBuyPayload(BaseModel): pair: str price: Optional[float] ordertype: Optional[OrderTypeValues] + stakeamount: Optional[float] class ForceSellPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 648ec6c57..4c430dd46 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -31,7 +31,8 @@ logger = logging.getLogger(__name__) # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. # 1.11: forcebuy and forcesell accept ordertype # 1.12: add blacklist delete endpoint -API_VERSION = 1.12 +# 1.13: forcebuy supports stake_amount +API_VERSION = 1.13 # Public API, requires no auth. router_public = APIRouter() @@ -134,7 +135,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None - trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype) + stake_amount = payload.stakeamount if payload.stakeamount else None + + trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2232496de..c78ff1079 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -707,8 +707,8 @@ class RPC: self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} - def _rpc_forcebuy(self, pair: str, price: Optional[float], - order_type: Optional[str] = None) -> Optional[Trade]: + def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None, + stake_amount: Optional[float] = None) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price @@ -733,14 +733,15 @@ class RPC: if not self._freqtrade.strategy.position_adjustment_enable: raise RPCException(f'position for {pair} already open - id: {trade.id}') - # gen stake amount - stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) + if not stake_amount: + # gen stake amount + stake_amount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy if not order_type: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) - if self._freqtrade.execute_entry(pair, stakeamount, price, + if self._freqtrade.execute_entry(pair, stake_amount, price, ordertype=order_type, trade=trade): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index fd43df019..27c509c94 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1108,9 +1108,14 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> with pytest.raises(RPCException, match=r'Wrong pair selected. Only pairs with stake-currency.*'): rpc._rpc_forcebuy('LTC/ETH', 0.0001) - pair = 'XRP/BTC' + + # Test with defined stake_amount + pair = 'LTC/BTC' + trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit', stake_amount=0.05) + assert trade.stake_amount == 0.05 # Test not buying + pair = 'XRP/BTC' freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot.config['stake_amount'] = 0 patch_get_signal(freqtradebot) From 82f0d4d05684f1576f99cec2bea128208f9ba42b Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Sat, 22 Jan 2022 14:03:12 +0000 Subject: [PATCH 05/25] set stoploss at trade creation --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ae4001f5f..9cfeedd75 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -521,6 +521,7 @@ class Backtesting: exchange='backtesting', orders=[] ) + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) order = Order( ft_is_open=False, From 7bef9a9b3ec8593dac0701e7c5f8df6d77b5d4e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 15:59:10 +0100 Subject: [PATCH 06/25] Extract timeout handling from freqtradebot class --- freqtrade/freqtradebot.py | 32 ++++++-------------------------- freqtrade/strategy/interface.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8980fb91c..e8f24864f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -9,8 +9,6 @@ from math import isclose from threading import Lock from typing import Any, Dict, List, Optional, Tuple -import arrow - from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe @@ -959,20 +957,6 @@ class FreqtradeBot(LoggingMixin): return True return False - def _check_timed_out(self, side: str, order: dict) -> bool: - """ - Check if timeout is active, and if the order is still open and timed out - """ - timeout = self.config.get('unfilledtimeout', {}).get(side) - ordertime = arrow.get(order['datetime']).datetime - if timeout is not None: - timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') - timeout_kwargs = {timeout_unit: -timeout} - timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime - return (order['status'] == 'open' and order['side'] == side - and ordertime < timeout_threshold) - return False - def check_handle_timedout(self) -> None: """ Check if any orders are timed out and cancel if necessary @@ -993,20 +977,16 @@ class FreqtradeBot(LoggingMixin): if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled - or self._check_timed_out('buy', order) - or strategy_safe_wrapper(self.strategy.check_buy_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): + or self.strategy.ft_check_timed_out( + 'buy', trade, order, datetime.now(timezone.utc)) + )): self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled - or self._check_timed_out('sell', order) - or strategy_safe_wrapper(self.strategy.check_sell_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): + or self.strategy.ft_check_timed_out( + 'sell', trade, order, datetime.now(timezone.utc))) + ): self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c8fb24da1..d073e8dfa 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -852,6 +852,28 @@ class IStrategy(ABC, HyperStrategyMixin): else: return current_profit > roi + def ft_check_timed_out(self, side: str, trade: Trade, order: Dict, + current_time: datetime) -> bool: + """ + FT Internal method. + Check if timeout is active, and if the order is still open and timed out + """ + timeout = self.config.get('unfilledtimeout', {}).get(side) + ordertime = arrow.get(order['datetime']).datetime + if timeout is not None: + timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') + timeout_kwargs = {timeout_unit: -timeout} + timeout_threshold = current_time + timedelta(**timeout_kwargs) + timedout = (order['status'] == 'open' and order['side'] == side + and ordertime < timeout_threshold) + if timedout: + return True + time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout' + + return strategy_safe_wrapper(getattr(self, time_method), + default_retval=False)( + pair=trade.pair, trade=trade, order=order) + def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Populates indicators for given candle (OHLCV) data (for multiple pairs) From 56daafd6b7f89280a7f92df0a856326d5515adf4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 16:31:59 +0100 Subject: [PATCH 07/25] Use realistic date for dry-run orders --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ef9e395f5..bfff7d06c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -614,7 +614,7 @@ class Exchange: 'side': side, 'filled': 0, 'remaining': _amount, - 'datetime': arrow.utcnow().isoformat(), + 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'timestamp': arrow.utcnow().int_timestamp * 1000, 'status': "closed" if ordertype == "market" else "open", 'fee': None, From 821a9d9cdc31ee5d97883eb46dbfa221d56ee83e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 16:05:13 +0100 Subject: [PATCH 08/25] Add current_time to check_timeout functions for convenience --- docs/strategy-callbacks.md | 20 +++++++++++--------- freqtrade/strategy/interface.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 6edc549a4..5e03776c7 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -413,7 +413,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, timezone +from datetime import datetime, timedelta from freqtrade.persistence import Trade class AwesomeStrategy(IStrategy): @@ -426,22 +426,24 @@ class AwesomeStrategy(IStrategy): 'sell': 60 * 25 } - def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, + current_time: datetime, **kwargs) -> bool: + if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < current_time - 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_utc < datetime.now(timezone.utc) - timedelta(minutes=5): + def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: + if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): return True - elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): + elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3): return True - elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): + elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24): return True return False ``` diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d073e8dfa..ac1285025 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -188,7 +188,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: + def check_buy_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: """ Check buy timeout function callback. This method can be used to override the buy-timeout. @@ -201,12 +202,14 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: Pair the trade is for :param trade: trade object. :param order: Order dictionary as returned from CCXT. + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is cancelled. """ return False - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: + def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: """ Check sell timeout function callback. This method can be used to override the sell-timeout. @@ -219,6 +222,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: Pair the trade is for :param trade: trade object. :param order: Order dictionary as returned from CCXT. + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the sell-order is cancelled. """ @@ -872,7 +876,8 @@ class IStrategy(ABC, HyperStrategyMixin): return strategy_safe_wrapper(getattr(self, time_method), default_retval=False)( - pair=trade.pair, trade=trade, order=order) + pair=trade.pair, trade=trade, order=order, + current_time=current_time) def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ From 8c79d5573918320fe20f36f3a82160c87f092ea0 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 23 Jan 2022 17:44:16 +0200 Subject: [PATCH 09/25] Fix issue #6268 --- freqtrade/persistence/models.py | 19 ++++++++++--------- tests/test_persistence.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 98a5329ba..ba5d11caf 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -577,18 +577,19 @@ class LocalTrade(): total_amount = 0.0 total_stake = 0.0 - for temp_order in self.orders: - if (temp_order.ft_is_open or - (temp_order.ft_order_side != 'buy') or - (temp_order.status not in NON_OPEN_EXCHANGE_STATES)): + for o in self.orders: + if (o.ft_is_open or + (o.ft_order_side != 'buy') or + (o.status not in NON_OPEN_EXCHANGE_STATES)): continue - tmp_amount = temp_order.amount - if temp_order.filled is not None: - tmp_amount = temp_order.filled - if tmp_amount > 0.0 and temp_order.average is not None: + tmp_amount = o.amount + tmp_price = o.average or o.price or 0.0 + if o.filled is not None: + tmp_amount = o.filled + if tmp_amount > 0.0 and tmp_price is not None: total_amount += tmp_amount - total_stake += temp_order.average * tmp_amount + total_stake += tmp_price * tmp_amount if total_amount > 0: self.open_rate = total_stake / total_amount diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d98238f6f..06afa4e81 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1661,6 +1661,33 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_buys == 2 + # Check with 1 order + order_noavg = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o1_rate, + average=None, + filled=o1_amount, + remaining=0, + cost=o1_amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(order_noavg) + trade.recalc_trade_from_orders() + + # Calling recalc with single initial order should not change anything + assert trade.amount == 3 * o1_amount + assert trade.stake_amount == 3 * o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == 3 * o1_fee_cost + assert trade.open_trade_value == 3 * o1_trade_val + assert trade.nr_of_successful_buys == 3 @pytest.mark.usefixtures("init_persistence") From 51b94889b27e7453b815afb2a988cee1469481af Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 23 Jan 2022 17:56:41 +0200 Subject: [PATCH 10/25] Just in case also check for closed to avoid counting in canceled orders. --- freqtrade/persistence/models.py | 2 +- tests/test_integration.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ba5d11caf..c3aabd928 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -587,7 +587,7 @@ class LocalTrade(): tmp_price = o.average or o.price or 0.0 if o.filled is not None: tmp_amount = o.filled - if tmp_amount > 0.0 and tmp_price is not None: + if tmp_amount > 0.0 and tmp_price is not None and o.status == 'closed': total_amount += tmp_amount total_stake += tmp_price * tmp_amount diff --git a/tests/test_integration.py b/tests/test_integration.py index 356ea5c62..ed38f1fec 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -243,6 +243,8 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 2 + for o in trade.orders: + assert o.status == "closed" assert trade.stake_amount == 120 # Open-rate averaged between 2.0 and 2.0 * 0.995 @@ -258,7 +260,6 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid'] assert trade.amount == trade.orders[0].amount + trade.orders[1].amount - assert trade.nr_of_successful_buys == 2 # Sell From 09db4bcaddb2ef85a7bf28105cc1392fda9d087c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jan 2022 17:00:00 +0100 Subject: [PATCH 11/25] Don't use strings, use methods directly --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index ac1285025..16d8ee818 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -872,9 +872,9 @@ class IStrategy(ABC, HyperStrategyMixin): and ordertime < timeout_threshold) if timedout: return True - time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout' + time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout - return strategy_safe_wrapper(getattr(self, time_method), + return strategy_safe_wrapper(time_method, default_retval=False)( pair=trade.pair, trade=trade, order=order, current_time=current_time) From 6613e3757aabea128e54cdfffbd7511df883ba4f Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 23 Jan 2022 18:09:57 +0200 Subject: [PATCH 12/25] Additional fix --- freqtrade/persistence/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index c3aabd928..76886d8ae 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -584,10 +584,10 @@ class LocalTrade(): continue tmp_amount = o.amount - tmp_price = o.average or o.price or 0.0 + tmp_price = o.average or o.price if o.filled is not None: tmp_amount = o.filled - if tmp_amount > 0.0 and tmp_price is not None and o.status == 'closed': + if tmp_amount > 0.0 and tmp_price is not None: total_amount += tmp_amount total_stake += tmp_price * tmp_amount From 6492e1cd7657f3c83c44a85ce540fa6ffc5abee6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jan 2022 17:42:18 +0100 Subject: [PATCH 13/25] Investigate random test failure --- tests/optimize/test_backtesting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4caf4aec1..bc408a059 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -763,6 +763,8 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: + # Debug output for random test failure + print(f"{contour}, {numres}") assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres From daee59f4f1ccc5936f440a49031f2ce168dca45c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jan 2022 19:20:10 +0100 Subject: [PATCH 14/25] Reorder interface methods --- freqtrade/strategy/interface.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 16d8ee818..12755d6bc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -188,6 +188,15 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote resource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + pass + def check_buy_timeout(self, pair: str, trade: Trade, order: dict, current_time: datetime, **kwargs) -> bool: """ @@ -228,15 +237,6 @@ class IStrategy(ABC, HyperStrategyMixin): """ return False - def bot_loop_start(self, **kwargs) -> None: - """ - Called at the start of the bot iteration (one loop). - Might be used to perform pair-independent tasks - (e.g. gather some remote resource for comparison) - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - """ - pass - def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, **kwargs) -> bool: """ From e67a54f7a979e8bb873875aac3e6a9fac06d40d3 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 23 Jan 2022 20:51:15 +0200 Subject: [PATCH 15/25] Fix missing order time info in backtesting. --- freqtrade/optimize/backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9cfeedd75..ea6a4082d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -531,6 +531,9 @@ class Backtesting: side="buy", order_type="market", status="closed", + order_date=row[DATE_IDX].to_pydatetime(), + order_filled_date=row[DATE_IDX].to_pydatetime(), + order_update_date=row[DATE_IDX].to_pydatetime(), price=propose_rate, average=propose_rate, amount=amount, From 451eca51c83e78c11b508f04a003efb6f62b352d Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 23 Jan 2022 20:58:25 +0200 Subject: [PATCH 16/25] Optimise the multiple usages of the same timestamp. --- freqtrade/optimize/backtesting.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ea6a4082d..8d5dc77d6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -462,11 +462,11 @@ class Backtesting: def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: - + current_time = row[DATE_IDX].to_pydatetime() # let's call the custom entry price, using the open price as default price propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( - pair=pair, current_time=row[DATE_IDX].to_pydatetime(), + pair=pair, current_time=current_time, proposed_rate=row[OPEN_IDX]) # default value is the open rate # Move rate to within the candle's low/high rate @@ -484,7 +484,7 @@ class Backtesting: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( - pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate, + pair=pair, current_time=current_time, current_rate=propose_rate, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) @@ -500,7 +500,7 @@ class Backtesting: if not pos_adjust: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, - time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): + time_in_force=time_in_force, current_time=current_time): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): @@ -511,7 +511,7 @@ class Backtesting: trade = LocalTrade( pair=pair, open_rate=propose_rate, - open_date=row[DATE_IDX].to_pydatetime(), + open_date=current_time, stake_amount=stake_amount, amount=amount, fee_open=self.fee, @@ -531,9 +531,9 @@ class Backtesting: side="buy", order_type="market", status="closed", - order_date=row[DATE_IDX].to_pydatetime(), - order_filled_date=row[DATE_IDX].to_pydatetime(), - order_update_date=row[DATE_IDX].to_pydatetime(), + order_date=current_time, + order_filled_date=current_time, + order_update_date=current_time, price=propose_rate, average=propose_rate, amount=amount, From 138fd9440aa105ca5e826ba710ec2ed934a8f71a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jan 2022 03:01:03 +0000 Subject: [PATCH 17/25] Bump types-python-dateutil from 2.8.7 to 2.8.8 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.7 to 2.8.8. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... 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 7773ff01a..6e6b27b88 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,4 +26,4 @@ types-requests==2.27.7 types-tabulate==0.8.5 # Extensions to datetime library -types-python-dateutil==2.8.7 \ No newline at end of file +types-python-dateutil==2.8.8 \ No newline at end of file From 013262f7e2055b2d0561d35b5ce5a222b959a544 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jan 2022 03:01:07 +0000 Subject: [PATCH 18/25] Bump mkdocs-material from 8.1.7 to 8.1.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.7 to 8.1.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.7...8.1.8) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... 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 15124b543..0368c90ff 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.1.7 +mkdocs-material==8.1.8 mdx_truly_sane_lists==1.2 pymdown-extensions==9.1 From 12fabba7841552092b49d3b9f88c6b65fe73b9c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jan 2022 03:01:12 +0000 Subject: [PATCH 19/25] Bump pytest-asyncio from 0.17.1 to 0.17.2 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.17.1 to 0.17.2. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.17.1...v0.17.2) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-patch ... 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 7773ff01a..ae9b68d74 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==4.0.1 flake8-tidy-imports==4.6.0 mypy==0.931 pytest==6.2.5 -pytest-asyncio==0.17.1 +pytest-asyncio==0.17.2 pytest-cov==3.0.0 pytest-mock==3.6.1 pytest-random-order==1.0.4 From 95c7d4868455a17204d980b9649332ae8157f20e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jan 2022 03:01:22 +0000 Subject: [PATCH 20/25] Bump sqlalchemy from 1.4.29 to 1.4.31 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.29 to 1.4.31. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ddbda38ad..1cc6db7a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ ccxt==1.68.20 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 -SQLAlchemy==1.4.29 +SQLAlchemy==1.4.31 python-telegram-bot==13.10 arrow==1.2.1 cachetools==4.2.2 From cf79cef7bab8379b2784604b303f445ff81c13d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jan 2022 03:01:24 +0000 Subject: [PATCH 21/25] Bump types-filelock from 3.2.4 to 3.2.5 Bumps [types-filelock](https://github.com/python/typeshed) from 3.2.4 to 3.2.5. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-filelock dependency-type: direct:development update-type: version-update:semver-patch ... 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 7773ff01a..e16d7891e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,7 +21,7 @@ nbconvert==6.4.0 # mypy types types-cachetools==4.2.9 -types-filelock==3.2.4 +types-filelock==3.2.5 types-requests==2.27.7 types-tabulate==0.8.5 From b8413410d1e8d0f314bcf9b0475aebb9097938fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jan 2022 03:01:33 +0000 Subject: [PATCH 22/25] Bump fastapi from 0.72.0 to 0.73.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.72.0 to 0.73.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.72.0...0.73.0) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ddbda38ad..12004df59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ python-rapidjson==1.5 sdnotify==0.3.2 # API Server -fastapi==0.72.0 +fastapi==0.73.0 uvicorn==0.17.0 pyjwt==2.3.0 aiofiles==0.8.0 From e252830229e536328c0d5bdf3afc13ceb4d04ffb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jan 2022 19:32:38 +0100 Subject: [PATCH 23/25] Add entry_tag to "entry" callbacks --- docs/strategy-callbacks.md | 9 +++++---- freqtrade/freqtradebot.py | 10 ++++++---- freqtrade/optimize/backtesting.py | 13 ++++++++----- freqtrade/strategy/interface.py | 10 +++++++--- .../subtemplates/strategy_methods_advanced.j2 | 7 +++++-- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 5e03776c7..e83fee46f 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -362,8 +362,8 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods - def custom_entry_price(self, pair: str, current_time: datetime, - proposed_rate, **kwargs) -> float: + def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, + entry_tag: Optional[str], **kwargs) -> float: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) @@ -502,7 +502,8 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -620,7 +621,7 @@ class DigDeeperStrategy(IStrategy): # This is called when placing the initial order (opening trade) def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + entry_tag: Optional[str], **kwargs) -> float: # We need to leave most of the funds for possible further DCA orders # This also applies to fixed stakes diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e8f24864f..972b6f6b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -531,7 +531,7 @@ class FreqtradeBot(LoggingMixin): pos_adjust = trade is not None enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake( - pair, price, stake_amount, trade) + pair, price, stake_amount, buy_tag, trade) if not stake_amount: return False @@ -550,7 +550,8 @@ class FreqtradeBot(LoggingMixin): if not pos_adjust and not strategy_safe_wrapper( self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc), + entry_tag=buy_tag): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) @@ -660,6 +661,7 @@ class FreqtradeBot(LoggingMixin): def get_valid_enter_price_and_stake( self, pair: str, price: Optional[float], stake_amount: float, + entry_tag: Optional[str], trade: Optional[Trade]) -> Tuple[float, float]: if price: enter_limit_requested = price @@ -669,7 +671,7 @@ class FreqtradeBot(LoggingMixin): custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=proposed_enter_rate)( pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_enter_rate) + proposed_rate=proposed_enter_rate, entry_tag=entry_tag) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) if not enter_limit_requested: @@ -682,7 +684,7 @@ class FreqtradeBot(LoggingMixin): default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) + min_stake=min_stake_amount, max_stake=max_stake_amount, entry_tag=entry_tag) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) return enter_limit_requested, stake_amount diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8d5dc77d6..8e52a62fa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -462,12 +462,14 @@ class Backtesting: def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: + current_time = row[DATE_IDX].to_pydatetime() + entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None # let's call the custom entry price, using the open price as default price propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( pair=pair, current_time=current_time, - proposed_rate=row[OPEN_IDX]) # default value is the open rate + proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate # Move rate to within the candle's low/high rate propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX]) @@ -485,7 +487,8 @@ class Backtesting: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=current_time, current_rate=propose_rate, - proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) + proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount, + entry_tag=entry_tag) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) @@ -500,14 +503,14 @@ class Backtesting: if not pos_adjust: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, - time_in_force=time_in_force, current_time=current_time): + time_in_force=time_in_force, current_time=current_time, + entry_tag=entry_tag): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): amount = round(stake_amount / propose_rate, 8) if trade is None: # Enter trade - has_buy_tag = len(row) >= BUY_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=propose_rate, @@ -517,7 +520,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, - buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, + buy_tag=entry_tag, exchange='backtesting', orders=[] ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 12755d6bc..0ce53c5cc 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -238,7 +238,8 @@ class IStrategy(ABC, HyperStrategyMixin): return False def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -254,6 +255,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process @@ -311,7 +313,7 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, - **kwargs) -> float: + entry_tag: Optional[str], **kwargs) -> float: """ Custom entry price logic, returning the new entry price. @@ -322,6 +324,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New entry price value if provided """ @@ -373,7 +376,7 @@ class IStrategy(ABC, HyperStrategyMixin): def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + entry_tag: Optional[str], **kwargs) -> float: """ Customize stake size for each new trade. This method is not called when edge module is enabled. @@ -384,6 +387,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index fb467ecaa..aed57fd20 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -14,7 +14,7 @@ def bot_loop_start(self, **kwargs) -> None: def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + entry_tag: Optional[str], **kwargs) -> float: """ Customize stake size for each new trade. This method is not called when edge module is enabled. @@ -25,6 +25,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake @@ -78,7 +79,8 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: 'datetime', **kwargs) -> bool: + time_in_force: str, current_time: 'datetime', entry_tag: Optional[str], + **kwargs) -> bool: """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or @@ -94,6 +96,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process From 194a5ce3cca52e3944af19ece509382efa29e8f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jan 2022 19:40:12 +0100 Subject: [PATCH 24/25] Update advanced strategy template with missing methods --- freqtrade/strategy/interface.py | 1 + .../subtemplates/strategy_methods_advanced.j2 | 65 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0ce53c5cc..6f139026e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -398,6 +398,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ Custom trade adjustment logic, returning the stake amount that a trade should be increased. This means extra buy orders with additional fees. + Only called when `position_adjustment_enable` is set to True. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index aed57fd20..db12094ed 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,9 +12,47 @@ def bot_loop_start(self, **kwargs) -> None: """ pass +def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate: float, + entry_tag: 'Optional[str]', **kwargs) -> float: + """ + Custom entry price logic, returning the new entry price. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None, orderbook is used to set entry price + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New entry price value if provided + """ + return proposed_rate + +def custom_exit_price(self, pair: str, trade: 'Trade', + current_time: 'datetime', proposed_rate: float, + current_profit: float, **kwargs) -> float: + """ + Custom exit price logic, returning the new exit price. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None, orderbook is used to set exit price + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param proposed_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New exit price value if provided + """ + return proposed_rate + def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - entry_tag: Optional[str], **kwargs) -> float: + entry_tag: 'Optional[str]', **kwargs) -> float: """ Customize stake size for each new trade. This method is not called when edge module is enabled. @@ -79,7 +117,7 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: 'datetime', entry_tag: Optional[str], + time_in_force: str, current_time: 'datetime', entry_tag: 'Optional[str]', **kwargs) -> bool: """ Called right before placing a buy order. @@ -170,3 +208,26 @@ def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) - :return bool: When True is returned, then the sell-order is cancelled. """ return False + +def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', + current_rate: float, current_profit: float, min_stake: float, + max_stake: float, **kwargs) -> 'Optional[float]': + """ + Custom trade adjustment logic, returning the stake amount that a trade should be increased. + This means extra buy orders with additional fees. + Only called when `position_adjustment_enable` is set to True. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None + + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Current buy rate. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param min_stake: Minimal stake size allowed by exchange. + :param max_stake: Balance available for trading. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: Stake amount to adjust your trade + """ + return None From 381bda1e4a5c12e099e9d799021fca0d8fab96c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Jan 2022 19:43:41 +0100 Subject: [PATCH 25/25] Update test to add new argument --- tests/strategy/test_default_strategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 6426ebe5f..a2d7df720 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -37,7 +37,7 @@ def test_strategy_test_v2(result, fee): assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.utcnow()) is True + current_time=datetime.utcnow(), entry_tag=None) is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi', current_time=datetime.utcnow()) is True