diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index f265bfe61..7b8769f0c 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -365,7 +365,7 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, - entry_tag: Optional[str], **kwargs) -> float: + entry_tag: Optional[str], side: str, **kwargs) -> float: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 40c1ee760..5721537c6 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -18,6 +18,7 @@ You can use the quick summary as checklist. Please refer to the detailed section * New `side` argument to callbacks without trade object * [`custom_stake_amount`](#custom-stake-amount) * [`confirm_trade_entry`](#confirm_trade_entry) + * [`custom_entry_price`](#custom_entry_price) * [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit) * Dataframe columns: * [`buy` -> `enter_long`](#populate_buy_trend) @@ -227,6 +228,26 @@ class AwesomeStrategy(IStrategy): return True ``` +### `custom_entry_price` + +New string argument `side` - which can be either `"long"` or `"short"`. + +``` python hl_lines="3" +class AwesomeStrategy(IStrategy): + def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, + entry_tag: Optional[str], **kwargs) -> float: + return proposed_rate +``` + +After: + +``` python hl_lines="3" +class AwesomeStrategy(IStrategy): + def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, + entry_tag: Optional[str], side: str, **kwargs) -> float: + return proposed_rate +``` + ### Adjust trade position changes While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a06e2771f..d927f03d7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -3,7 +3,7 @@ """ bot constants """ -from typing import List, Tuple +from typing import List, Literal, Tuple from freqtrade.enums import CandleType @@ -487,3 +487,6 @@ ListPairsWithTimeframes = List[PairWithTimeframe] # Type for trades list TradeList = List[List] + +LongShort = Literal['long', 'short'] +EntryExit = Literal['entry', 'exit'] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e77c48abc..1a8cae62c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, - ListPairsWithTimeframes, PairWithTimeframe) + EntryExit, ListPairsWithTimeframes, PairWithTimeframe) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, @@ -1429,7 +1429,7 @@ class Exchange: raise OperationalException(e) from e def get_rate(self, pair: str, refresh: bool, # noqa: max-complexity: 13 - side: Literal['entry', 'exit'], is_short: bool, + side: EntryExit, is_short: bool, order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float: """ Calculates bid/ask target diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 39989a0d4..87e33f0e6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -8,12 +8,13 @@ from datetime import datetime, time, timezone from decimal import Decimal from math import isclose from threading import Lock -from typing import Any, Dict, List, Literal, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from schedule import Scheduler from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency +from freqtrade.constants import LongShort from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge @@ -611,7 +612,7 @@ class FreqtradeBot(LoggingMixin): time_in_force = self.strategy.order_time_in_force['entry'] [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] - trade_side: Literal['long', 'short'] = 'short' if is_short else 'long' + trade_side: LongShort = 'short' if is_short else 'long' pos_adjust = trade is not None enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( @@ -765,7 +766,7 @@ class FreqtradeBot(LoggingMixin): def get_valid_enter_price_and_stake( self, pair: str, price: Optional[float], stake_amount: float, - trade_side: Literal['long', 'short'], + trade_side: LongShort, entry_tag: Optional[str], trade: Optional[Trade] ) -> Tuple[float, float, float]: @@ -779,7 +780,9 @@ 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, entry_tag=entry_tag) + proposed_rate=proposed_enter_rate, entry_tag=entry_tag, + side=trade_side, + ) enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3d59a8edc..993393b49 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, LongShort from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes @@ -655,7 +655,7 @@ class Backtesting: def get_valid_price_and_stake( self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float], - direction: str, current_time: datetime, entry_tag: Optional[str], + direction: LongShort, current_time: datetime, entry_tag: Optional[str], trade: Optional[LocalTrade], order_type: str ) -> Tuple[float, float, float, float]: @@ -663,7 +663,9 @@ class Backtesting: propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=propose_rate)( pair=pair, current_time=current_time, - proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate + proposed_rate=propose_rate, entry_tag=entry_tag, + side=direction, + ) # default value is the open rate # We can't place orders higher than current high (otherwise it'd be a stop limit buy) # which freqtrade does not support in live. if direction == "short": @@ -714,7 +716,7 @@ class Backtesting: return propose_rate, stake_amount_val, leverage, min_stake_amount - def _enter_trade(self, pair: str, row: Tuple, direction: str, + def _enter_trade(self, pair: str, row: Tuple, direction: LongShort, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: @@ -847,7 +849,7 @@ class Backtesting: self.rejected_trades += 1 return False - def check_for_trade_entry(self, row) -> Optional[str]: + def check_for_trade_entry(self, row) -> Optional[LongShort]: enter_long = row[LONG_IDX] == 1 exit_long = row[ELONG_IDX] == 1 enter_short = self._can_short and row[SHORT_IDX] == 1 diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 03bd4041d..c5f153266 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -339,7 +339,7 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float, - entry_tag: Optional[str], **kwargs) -> float: + entry_tag: Optional[str], side: str, **kwargs) -> float: """ Custom entry price logic, returning the new entry price. @@ -351,6 +351,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param proposed_rate: Rate, calculated based on pricing settings in exit_pricing. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New entry price value if provided """ diff --git a/requirements-dev.txt b/requirements-dev.txt index 063cfaa45..65de55b76 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,12 +17,12 @@ isort==5.10.1 time-machine==2.6.0 # Convert jupyter notebooks to markdown documents -nbconvert==6.4.4 +nbconvert==6.4.5 # mypy types types-cachetools==5.0.0 types-filelock==3.2.5 -types-requests==2.27.15 +types-requests==2.27.16 types-tabulate==0.8.6 # Extensions to datetime library diff --git a/requirements.txt b/requirements.txt index d305f91b4..eee88001d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ numpy==1.22.3 -pandas==1.4.1 +pandas==1.4.2 pandas-ta==0.3.14b -ccxt==1.77.45 +ccxt==1.77.98 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.2 aiohttp==3.8.1 -SQLAlchemy==1.4.32 +SQLAlchemy==1.4.34 python-telegram-bot==13.11 arrow==1.2.2 cachetools==4.2.2 @@ -31,7 +31,7 @@ python-rapidjson==1.6 sdnotify==0.3.2 # API Server -fastapi==0.75.0 +fastapi==0.75.1 uvicorn==0.17.6 pyjwt==2.3.0 aiofiles==0.8.0