2018-01-28 05:26:57 +00:00
|
|
|
"""
|
|
|
|
IStrategy interface
|
|
|
|
This module defines the interface to apply for strategies
|
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
import logging
|
2018-07-22 15:39:35 +00:00
|
|
|
from abc import ABC, abstractmethod
|
2021-01-05 06:06:53 +00:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2022-08-31 01:21:34 +00:00
|
|
|
from typing import Dict, List, Optional, Tuple, Union
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
from pandas import DataFrame
|
|
|
|
|
2023-04-11 05:26:38 +00:00
|
|
|
from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes
|
2018-12-26 13:32:17 +00:00
|
|
|
from freqtrade.data.dataprovider import DataProvider
|
2024-05-12 13:18:32 +00:00
|
|
|
from freqtrade.enums import (
|
|
|
|
CandleType,
|
|
|
|
ExitCheckTuple,
|
|
|
|
ExitType,
|
|
|
|
MarketDirection,
|
|
|
|
RunMode,
|
|
|
|
SignalDirection,
|
|
|
|
SignalTagType,
|
|
|
|
SignalType,
|
|
|
|
TradingMode,
|
|
|
|
)
|
2020-08-24 09:44:32 +00:00
|
|
|
from freqtrade.exceptions import OperationalException, StrategyError
|
2022-05-08 15:45:20 +00:00
|
|
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
2022-08-31 16:40:26 +00:00
|
|
|
from freqtrade.misc import remove_entry_exit_signals
|
2023-09-16 17:58:59 +00:00
|
|
|
from freqtrade.persistence import Order, PairLocks, Trade
|
2021-03-24 14:03:38 +00:00
|
|
|
from freqtrade.strategy.hyper import HyperStrategyMixin
|
2024-05-12 13:18:32 +00:00
|
|
|
from freqtrade.strategy.informative_decorator import (
|
|
|
|
InformativeData,
|
|
|
|
PopulateIndicators,
|
|
|
|
_create_and_merge_informative_pair,
|
|
|
|
_format_pair_name,
|
|
|
|
)
|
2020-02-06 19:26:04 +00:00
|
|
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
2023-05-14 08:32:18 +00:00
|
|
|
from freqtrade.util import dt_now
|
2018-12-26 13:32:17 +00:00
|
|
|
from freqtrade.wallets import Wallets
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2020-09-28 17:39:41 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-03-24 14:03:38 +00:00
|
|
|
class IStrategy(ABC, HyperStrategyMixin):
|
2018-01-28 05:26:57 +00:00
|
|
|
"""
|
|
|
|
Interface for freqtrade strategies
|
|
|
|
Defines the mandatory structure must follow any custom strategies
|
|
|
|
|
|
|
|
Attributes you can use:
|
|
|
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
|
|
|
stoploss -> float: optimal stoploss designed for the strategy
|
2022-03-20 08:00:53 +00:00
|
|
|
timeframe -> str: value of the timeframe to use with the strategy
|
2018-01-28 05:26:57 +00:00
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
|
2019-08-26 17:44:33 +00:00
|
|
|
# Strategy interface version
|
|
|
|
# Default to version 2
|
2022-04-25 05:01:27 +00:00
|
|
|
# Version 1 is the initial interface without metadata dict - deprecated and no longer supported.
|
2019-08-26 17:44:33 +00:00
|
|
|
# Version 2 populate_* include metadata dict
|
2022-03-05 13:26:18 +00:00
|
|
|
# Version 3 - First version with short and leverage support
|
|
|
|
INTERFACE_VERSION: int = 3
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2021-10-27 04:29:35 +00:00
|
|
|
_ft_params_from_file: Dict
|
2018-06-15 03:27:41 +00:00
|
|
|
# associated minimal roi
|
2023-05-27 17:39:00 +00:00
|
|
|
minimal_roi: Dict = {}
|
2018-06-15 03:27:41 +00:00
|
|
|
|
|
|
|
# associated stoploss
|
2018-05-31 19:59:22 +00:00
|
|
|
stoploss: float
|
2018-06-15 03:27:41 +00:00
|
|
|
|
2023-01-04 09:34:44 +00:00
|
|
|
# max open trades for the strategy
|
2024-05-12 14:41:08 +00:00
|
|
|
max_open_trades: IntOrInf
|
2023-01-04 09:34:44 +00:00
|
|
|
|
2019-01-05 06:10:25 +00:00
|
|
|
# trailing stoploss
|
|
|
|
trailing_stop: bool = False
|
2019-10-11 19:59:13 +00:00
|
|
|
trailing_stop_positive: Optional[float] = None
|
2019-10-11 06:55:31 +00:00
|
|
|
trailing_stop_positive_offset: float = 0.0
|
2019-03-12 14:43:53 +00:00
|
|
|
trailing_only_offset_is_reached = False
|
2020-12-20 10:12:22 +00:00
|
|
|
use_custom_stoploss: bool = False
|
2019-01-05 06:10:25 +00:00
|
|
|
|
2022-03-12 06:00:57 +00:00
|
|
|
# Can this strategy go short?
|
2022-03-11 05:59:28 +00:00
|
|
|
can_short: bool = False
|
|
|
|
|
2020-06-02 07:50:56 +00:00
|
|
|
# associated timeframe
|
|
|
|
timeframe: str
|
2018-05-31 19:59:22 +00:00
|
|
|
|
2018-11-15 05:58:24 +00:00
|
|
|
# Optional order types
|
|
|
|
order_types: Dict = {
|
2024-05-12 14:41:08 +00:00
|
|
|
"entry": "limit",
|
|
|
|
"exit": "limit",
|
|
|
|
"stoploss": "limit",
|
|
|
|
"stoploss_on_exchange": False,
|
|
|
|
"stoploss_on_exchange_interval": 60,
|
2018-11-15 05:58:24 +00:00
|
|
|
}
|
|
|
|
|
2018-11-25 21:02:59 +00:00
|
|
|
# Optional time in force
|
|
|
|
order_time_in_force: Dict = {
|
2024-05-12 14:41:08 +00:00
|
|
|
"entry": "GTC",
|
|
|
|
"exit": "GTC",
|
2018-11-25 21:02:59 +00:00
|
|
|
}
|
|
|
|
|
2018-08-09 17:24:00 +00:00
|
|
|
# run "populate_indicators" only for new candle
|
2022-05-23 08:24:58 +00:00
|
|
|
process_only_new_candles: bool = True
|
2018-08-09 17:24:00 +00:00
|
|
|
|
2022-04-05 18:07:58 +00:00
|
|
|
use_exit_signal: bool
|
2022-04-05 18:00:35 +00:00
|
|
|
exit_profit_only: bool
|
2022-04-05 18:03:20 +00:00
|
|
|
exit_profit_offset: float
|
2022-04-05 18:20:51 +00:00
|
|
|
ignore_roi_if_entry_signal: bool
|
2021-06-26 15:15:05 +00:00
|
|
|
|
2021-12-24 10:38:43 +00:00
|
|
|
# Position adjustment is disabled by default
|
|
|
|
position_adjustment_enable: bool = False
|
2022-01-27 15:57:50 +00:00
|
|
|
max_entry_position_adjustment: int = -1
|
2021-12-24 10:38:43 +00:00
|
|
|
|
2021-01-12 06:30:39 +00:00
|
|
|
# Number of seconds after which the candle will no longer result in a buy on expired candles
|
2021-01-04 19:49:24 +00:00
|
|
|
ignore_buying_expired_candle_after: int = 0
|
|
|
|
|
2020-05-29 17:37:18 +00:00
|
|
|
# Disable checking the dataframe (converts the error into a warning message)
|
|
|
|
disable_dataframe_checks: bool = False
|
|
|
|
|
2019-10-20 09:17:01 +00:00
|
|
|
# Count of candles the strategy requires before producing valid signals
|
|
|
|
startup_candle_count: int = 0
|
|
|
|
|
2021-01-06 15:37:09 +00:00
|
|
|
# Protections
|
2021-06-17 19:01:22 +00:00
|
|
|
protections: List = []
|
2021-01-06 15:37:09 +00:00
|
|
|
|
2018-12-26 13:32:17 +00:00
|
|
|
# Class level variables (intentional) containing
|
|
|
|
# the dataprovider (dp) (access to other candles, historic data, ...)
|
|
|
|
# and wallets - access to the current balance.
|
2022-04-25 09:12:35 +00:00
|
|
|
dp: DataProvider
|
2019-11-21 01:59:38 +00:00
|
|
|
wallets: Optional[Wallets] = None
|
2021-08-31 05:18:32 +00:00
|
|
|
# Filled from configuration
|
|
|
|
stake_currency: str
|
2020-10-02 04:41:28 +00:00
|
|
|
# container variable for strategy source code
|
2024-05-12 14:41:08 +00:00
|
|
|
__source__: str = ""
|
2018-12-26 13:32:17 +00:00
|
|
|
|
2020-01-04 11:54:58 +00:00
|
|
|
# Definition of plot_config. See plotting documentation for more details.
|
|
|
|
plot_config: Dict = {}
|
2020-01-04 10:14:00 +00:00
|
|
|
|
2023-02-11 23:31:25 +00:00
|
|
|
# A self set parameter that represents the market direction. filled from configuration
|
|
|
|
market_direction: MarketDirection = MarketDirection.NONE
|
|
|
|
|
2022-09-18 11:31:52 +00:00
|
|
|
def __init__(self, config: Config) -> None:
|
2018-07-16 05:11:17 +00:00
|
|
|
self.config = config
|
2019-01-21 18:28:53 +00:00
|
|
|
# Dict to determine if analysis is necessary
|
|
|
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
2021-03-26 12:25:17 +00:00
|
|
|
super().__init__(config)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2021-09-19 23:44:12 +00:00
|
|
|
# Gather informative pairs from @informative-decorated methods.
|
|
|
|
self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = []
|
|
|
|
for attr_name in dir(self.__class__):
|
|
|
|
cls_method = getattr(self.__class__, attr_name)
|
|
|
|
if not callable(cls_method):
|
|
|
|
continue
|
2024-05-12 14:41:08 +00:00
|
|
|
informative_data_list = getattr(cls_method, "_ft_informative", None)
|
2021-09-19 23:44:12 +00:00
|
|
|
if not isinstance(informative_data_list, list):
|
|
|
|
# Type check is required because mocker would return a mock object that evaluates to
|
|
|
|
# True, confusing this code.
|
|
|
|
continue
|
|
|
|
strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe)
|
|
|
|
for informative_data in informative_data_list:
|
|
|
|
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
|
2024-05-12 14:41:08 +00:00
|
|
|
raise OperationalException(
|
|
|
|
"Informative timeframe must be equal or higher than strategy timeframe!"
|
|
|
|
)
|
2022-01-28 15:58:07 +00:00
|
|
|
if not informative_data.candle_type:
|
2024-05-12 14:41:08 +00:00
|
|
|
informative_data.candle_type = config["candle_type_def"]
|
2021-09-19 23:44:12 +00:00
|
|
|
self._ft_informative.append((informative_data, cls_method))
|
|
|
|
|
2022-07-23 13:58:31 +00:00
|
|
|
def load_freqAI_model(self) -> None:
|
2024-05-12 14:41:08 +00:00
|
|
|
if self.config.get("freqai", {}).get("enabled", False):
|
2022-07-23 13:58:31 +00:00
|
|
|
# Import here to avoid importing this if freqAI is disabled
|
2022-08-26 13:30:01 +00:00
|
|
|
from freqtrade.freqai.utils import download_all_data_for_training
|
2022-07-23 13:58:31 +00:00
|
|
|
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
2024-05-12 14:41:08 +00:00
|
|
|
|
2022-07-23 13:58:31 +00:00
|
|
|
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
|
2022-08-13 07:27:56 +00:00
|
|
|
self.freqai_info = self.config["freqai"]
|
2022-08-17 13:18:44 +00:00
|
|
|
|
|
|
|
# download the desired data in dry/live
|
2024-05-12 14:41:08 +00:00
|
|
|
if self.config.get("runmode") in (RunMode.DRY_RUN, RunMode.LIVE):
|
2022-08-17 13:18:44 +00:00
|
|
|
logger.info(
|
|
|
|
"Downloading all training data for all pairs in whitelist and "
|
2022-08-26 16:51:42 +00:00
|
|
|
"corr_pairlist, this may take a while if the data is not "
|
|
|
|
"already on disk."
|
2022-08-17 13:18:44 +00:00
|
|
|
)
|
2022-08-26 11:56:44 +00:00
|
|
|
download_all_data_for_training(self.dp, self.config)
|
2022-08-13 07:53:18 +00:00
|
|
|
else:
|
|
|
|
# Gracious failures if freqAI is disabled but "start" is called.
|
2023-06-29 12:16:10 +00:00
|
|
|
class DummyClass:
|
2022-08-13 07:53:18 +00:00
|
|
|
def start(self, *args, **kwargs):
|
|
|
|
raise OperationalException(
|
2024-05-12 14:41:08 +00:00
|
|
|
"freqAI is not enabled. "
|
|
|
|
"Please enable it in your config to use this strategy."
|
|
|
|
)
|
2022-09-03 19:24:14 +00:00
|
|
|
|
2022-09-03 20:10:23 +00:00
|
|
|
def shutdown(self, *args, **kwargs):
|
2022-09-03 19:24:14 +00:00
|
|
|
pass
|
|
|
|
|
2022-08-13 07:53:18 +00:00
|
|
|
self.freqai = DummyClass() # type: ignore
|
2022-07-23 13:58:31 +00:00
|
|
|
|
2022-05-25 18:01:21 +00:00
|
|
|
def ft_bot_start(self, **kwargs) -> None:
|
|
|
|
"""
|
|
|
|
Strategy init - runs after dataprovider has been added.
|
|
|
|
Must call bot_start()
|
|
|
|
"""
|
2022-07-23 13:58:31 +00:00
|
|
|
self.load_freqAI_model()
|
|
|
|
|
2022-05-25 18:01:21 +00:00
|
|
|
strategy_safe_wrapper(self.bot_start)()
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
self.ft_load_hyper_params(self.config.get("runmode") == RunMode.HYPEROPT)
|
2022-05-30 05:07:47 +00:00
|
|
|
|
2022-09-03 19:24:14 +00:00
|
|
|
def ft_bot_cleanup(self) -> None:
|
|
|
|
"""
|
|
|
|
Clean up FreqAI and child threads
|
|
|
|
"""
|
|
|
|
self.freqai.shutdown()
|
|
|
|
|
2018-07-22 15:39:35 +00:00
|
|
|
@abstractmethod
|
2018-07-29 18:36:03 +00:00
|
|
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2021-08-08 09:38:34 +00:00
|
|
|
Populate indicators that will be used in the Buy, Sell, Short, Exit_short strategy
|
2020-03-08 10:35:31 +00:00
|
|
|
:param dataframe: DataFrame with data from the exchange
|
2018-07-29 18:36:03 +00:00
|
|
|
:param metadata: Additional information, like the currently traded pair
|
2018-01-15 08:35:11 +00:00
|
|
|
:return: a Dataframe with all mandatory indicators for the strategies
|
|
|
|
"""
|
2021-04-30 12:30:37 +00:00
|
|
|
return dataframe
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2021-08-18 10:19:17 +00:00
|
|
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2022-03-12 08:31:14 +00:00
|
|
|
DEPRECATED - please migrate to populate_entry_trend
|
2018-01-15 08:35:11 +00:00
|
|
|
:param dataframe: DataFrame
|
2018-07-29 18:36:03 +00:00
|
|
|
:param metadata: Additional information, like the currently traded pair
|
2018-01-15 08:35:11 +00:00
|
|
|
:return: DataFrame with buy column
|
|
|
|
"""
|
2021-04-30 12:30:37 +00:00
|
|
|
return dataframe
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2022-03-12 08:31:14 +00:00
|
|
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the entry signal for the given dataframe
|
|
|
|
:param dataframe: DataFrame
|
|
|
|
:param metadata: Additional information, like the currently traded pair
|
|
|
|
:return: DataFrame with entry columns populated
|
|
|
|
"""
|
|
|
|
return self.populate_buy_trend(dataframe, metadata)
|
|
|
|
|
2021-08-18 10:19:17 +00:00
|
|
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2022-03-12 08:31:14 +00:00
|
|
|
DEPRECATED - please migrate to populate_exit_trend
|
2018-01-15 08:35:11 +00:00
|
|
|
Based on TA indicators, populates the sell signal for the given dataframe
|
|
|
|
:param dataframe: DataFrame
|
2018-07-29 18:36:03 +00:00
|
|
|
:param metadata: Additional information, like the currently traded pair
|
2018-03-25 18:24:56 +00:00
|
|
|
:return: DataFrame with sell column
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
2021-04-30 12:30:37 +00:00
|
|
|
return dataframe
|
2018-07-12 18:38:14 +00:00
|
|
|
|
2022-03-12 08:31:14 +00:00
|
|
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the exit signal for the given dataframe
|
|
|
|
:param dataframe: DataFrame
|
|
|
|
:param metadata: Additional information, like the currently traded pair
|
|
|
|
:return: DataFrame with exit columns populated
|
|
|
|
"""
|
|
|
|
return self.populate_sell_trend(dataframe, metadata)
|
|
|
|
|
2022-04-01 14:39:56 +00:00
|
|
|
def bot_start(self, **kwargs) -> None:
|
|
|
|
"""
|
|
|
|
Called only once after bot instantiation.
|
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2023-03-26 09:21:18 +00:00
|
|
|
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
2022-01-23 18:20:10 +00:00
|
|
|
"""
|
|
|
|
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)
|
2023-03-26 09:21:18 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-01-23 18:20:10 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def check_buy_timeout(
|
|
|
|
self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs
|
|
|
|
) -> bool:
|
2020-02-06 19:30:17 +00:00
|
|
|
"""
|
2022-03-25 18:46:56 +00:00
|
|
|
DEPRECATED: Please use `check_entry_timeout` instead.
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def check_entry_timeout(
|
|
|
|
self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs
|
|
|
|
) -> bool:
|
2022-03-25 18:46:56 +00:00
|
|
|
"""
|
|
|
|
Check entry timeout function callback.
|
2022-04-16 04:47:56 +00:00
|
|
|
This method can be used to override the entry-timeout.
|
2021-09-09 08:10:12 +00:00
|
|
|
It is called whenever a limit entry order has been created,
|
2020-02-06 19:30:17 +00:00
|
|
|
and is not yet fully filled.
|
|
|
|
Configuration options in `unfilledtimeout` will be verified before this,
|
|
|
|
so ensure to set these timeouts high enough.
|
|
|
|
|
|
|
|
When not implemented by a strategy, this simply returns False.
|
|
|
|
:param pair: Pair the trade is for
|
2022-04-16 04:47:56 +00:00
|
|
|
:param trade: Trade object.
|
|
|
|
:param order: Order object.
|
2022-01-22 15:05:13 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2020-02-06 19:30:17 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
2021-09-09 08:10:12 +00:00
|
|
|
:return bool: When True is returned, then the entry order is cancelled.
|
2020-02-06 19:30:17 +00:00
|
|
|
"""
|
2022-03-25 18:46:56 +00:00
|
|
|
return self.check_buy_timeout(
|
2024-05-12 14:41:08 +00:00
|
|
|
pair=pair, trade=trade, order=order, current_time=current_time
|
|
|
|
)
|
2020-02-06 19:30:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def check_sell_timeout(
|
|
|
|
self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs
|
|
|
|
) -> bool:
|
2020-02-21 19:27:13 +00:00
|
|
|
"""
|
2022-03-25 18:46:56 +00:00
|
|
|
DEPRECATED: Please use `check_exit_timeout` instead.
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def check_exit_timeout(
|
|
|
|
self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs
|
|
|
|
) -> bool:
|
2022-03-25 18:46:56 +00:00
|
|
|
"""
|
2022-04-16 04:47:56 +00:00
|
|
|
Check exit timeout function callback.
|
2021-08-08 09:38:34 +00:00
|
|
|
This method can be used to override the exit-timeout.
|
2022-04-16 04:47:56 +00:00
|
|
|
It is called whenever a limit exit order has been created,
|
|
|
|
and is not yet fully filled.
|
2020-02-21 19:27:13 +00:00
|
|
|
Configuration options in `unfilledtimeout` will be verified before this,
|
|
|
|
so ensure to set these timeouts high enough.
|
|
|
|
|
|
|
|
When not implemented by a strategy, this simply returns False.
|
|
|
|
:param pair: Pair the trade is for
|
2022-04-16 04:47:56 +00:00
|
|
|
:param trade: Trade object.
|
|
|
|
:param order: Order object
|
2022-01-22 15:05:13 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2020-02-21 19:27:13 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
2022-04-16 04:47:56 +00:00
|
|
|
:return bool: When True is returned, then the exit-order is cancelled.
|
2020-02-21 19:27:13 +00:00
|
|
|
"""
|
2022-03-26 10:55:11 +00:00
|
|
|
return self.check_sell_timeout(
|
2024-05-12 14:41:08 +00:00
|
|
|
pair=pair, trade=trade, order=order, current_time=current_time
|
|
|
|
)
|
2020-02-21 19:27:13 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
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],
|
|
|
|
side: str,
|
|
|
|
**kwargs,
|
|
|
|
) -> bool:
|
2020-06-14 08:08:19 +00:00
|
|
|
"""
|
2021-09-09 08:10:12 +00:00
|
|
|
Called right before placing a entry order.
|
2020-06-14 08:08:19 +00:00
|
|
|
Timing for this function is critical, so avoid doing heavy computations or
|
2020-06-14 08:49:15 +00:00
|
|
|
network requests in this method.
|
2020-06-14 08:08:19 +00:00
|
|
|
|
|
|
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
|
|
|
|
|
|
When not implemented by a strategy, returns True (always confirming).
|
|
|
|
|
2021-08-08 09:38:34 +00:00
|
|
|
:param pair: Pair that's about to be bought/shorted.
|
2020-06-14 08:08:19 +00:00
|
|
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
2022-05-31 15:52:45 +00:00
|
|
|
:param amount: Amount in target (base) currency that's going to be traded.
|
2020-06-14 08:08:19 +00:00
|
|
|
:param rate: Rate that's going to be used when using limit orders
|
2022-06-09 17:41:08 +00:00
|
|
|
or current rate for market orders.
|
2020-06-14 08:08:19 +00:00
|
|
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
2021-05-02 09:20:25 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-01-23 18:32:38 +00:00
|
|
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
2021-09-26 17:32:24 +00:00
|
|
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
2020-06-14 08:08:19 +00:00
|
|
|
: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
|
|
|
|
"""
|
|
|
|
return True
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def confirm_trade_exit(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
trade: Trade,
|
|
|
|
order_type: str,
|
|
|
|
amount: float,
|
|
|
|
rate: float,
|
|
|
|
time_in_force: str,
|
|
|
|
exit_reason: str,
|
|
|
|
current_time: datetime,
|
|
|
|
**kwargs,
|
|
|
|
) -> bool:
|
2020-06-14 08:08:19 +00:00
|
|
|
"""
|
2021-09-09 08:10:12 +00:00
|
|
|
Called right before placing a regular exit order.
|
2020-06-14 08:08:19 +00:00
|
|
|
Timing for this function is critical, so avoid doing heavy computations or
|
2020-06-14 08:49:15 +00:00
|
|
|
network requests in this method.
|
2020-06-14 08:08:19 +00:00
|
|
|
|
|
|
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
|
|
|
|
|
|
When not implemented by a strategy, returns True (always confirming).
|
|
|
|
|
2021-08-08 09:38:34 +00:00
|
|
|
:param pair: Pair for trade that's about to be exited.
|
2020-06-14 08:08:19 +00:00
|
|
|
:param trade: trade object.
|
|
|
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
2022-05-31 15:52:45 +00:00
|
|
|
:param amount: Amount in base currency.
|
2020-06-14 08:08:19 +00:00
|
|
|
:param rate: Rate that's going to be used when using limit orders
|
2022-06-09 17:41:08 +00:00
|
|
|
or current rate for market orders.
|
2020-06-14 08:08:19 +00:00
|
|
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
2022-03-25 05:37:40 +00:00
|
|
|
:param exit_reason: Exit reason.
|
2020-06-14 08:08:19 +00:00
|
|
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
2022-04-04 15:10:02 +00:00
|
|
|
'exit_signal', 'force_exit', 'emergency_exit']
|
2021-05-02 09:20:25 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2020-06-14 08:08:19 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
2022-04-04 14:59:27 +00:00
|
|
|
:return bool: When True, then the exit-order is placed on the exchange.
|
2020-06-14 08:08:19 +00:00
|
|
|
False aborts the process
|
|
|
|
"""
|
|
|
|
return True
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def order_filled(
|
|
|
|
self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs
|
|
|
|
) -> None:
|
2024-03-18 19:46:47 +00:00
|
|
|
"""
|
2024-03-28 05:49:02 +00:00
|
|
|
Called right after an order fills.
|
|
|
|
Will be called for all order types (entry, exit, stoploss, position adjustment).
|
|
|
|
:param pair: Pair for trade
|
2024-03-18 19:46:47 +00:00
|
|
|
:param trade: trade object.
|
2024-03-19 00:47:27 +00:00
|
|
|
:param order: Order object.
|
2024-03-18 19:46:47 +00:00
|
|
|
: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.
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def custom_stoploss(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
current_profit: float,
|
|
|
|
after_fill: bool,
|
|
|
|
**kwargs,
|
|
|
|
) -> Optional[float]:
|
2020-12-09 06:45:41 +00:00
|
|
|
"""
|
2020-12-19 12:18:06 +00:00
|
|
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
|
|
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
2020-12-09 06:45:41 +00:00
|
|
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
|
|
|
|
|
|
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
|
|
|
2023-08-13 08:32:37 +00:00
|
|
|
When not implemented by a strategy, returns the initial stoploss value.
|
2020-12-20 10:12:22 +00:00
|
|
|
Only called when use_custom_stoploss is set to True.
|
2020-12-09 06:45:41 +00:00
|
|
|
|
2020-12-31 08:48:49 +00:00
|
|
|
:param pair: Pair that's currently analyzed
|
2020-12-09 06:45:41 +00:00
|
|
|
:param trade: trade object.
|
2020-12-19 10:58:42 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2020-12-12 06:06:16 +00:00
|
|
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
2023-08-13 09:09:26 +00:00
|
|
|
:param after_fill: True if the stoploss is called after the order was filled.
|
2020-12-09 06:45:41 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
2021-06-25 13:45:49 +00:00
|
|
|
:return float: New stoploss value, relative to the current_rate
|
2020-12-09 06:45:41 +00:00
|
|
|
"""
|
|
|
|
return self.stoploss
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def custom_entry_price(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
trade: Optional[Trade],
|
|
|
|
current_time: datetime,
|
|
|
|
proposed_rate: float,
|
|
|
|
entry_tag: Optional[str],
|
|
|
|
side: str,
|
|
|
|
**kwargs,
|
|
|
|
) -> float:
|
2021-07-31 04:05:45 +00:00
|
|
|
"""
|
|
|
|
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
|
2023-09-17 07:17:07 +00:00
|
|
|
:param trade: trade object (None for initial entries).
|
2021-07-31 04:05:45 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2022-01-23 18:32:38 +00:00
|
|
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
2022-04-04 14:48:27 +00:00
|
|
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
2021-07-31 04:05:45 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
|
|
:return float: New entry price value if provided
|
|
|
|
"""
|
2021-08-01 06:09:59 +00:00
|
|
|
return proposed_rate
|
2021-07-31 04:05:45 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def custom_exit_price(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
proposed_rate: float,
|
|
|
|
current_profit: float,
|
|
|
|
exit_tag: Optional[str],
|
|
|
|
**kwargs,
|
|
|
|
) -> float:
|
2021-08-04 22:54:17 +00:00
|
|
|
"""
|
|
|
|
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
|
2021-08-04 23:09:55 +00:00
|
|
|
:param trade: trade object.
|
2021-08-04 22:54:17 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2021-08-04 23:09:55 +00:00
|
|
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
2022-04-26 06:39:15 +00:00
|
|
|
:param exit_tag: Exit reason.
|
2021-08-04 22:54:17 +00:00
|
|
|
: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
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def custom_sell(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
current_profit: float,
|
|
|
|
**kwargs,
|
|
|
|
) -> Optional[Union[str, bool]]:
|
2021-04-17 07:49:09 +00:00
|
|
|
"""
|
2022-03-12 10:15:27 +00:00
|
|
|
DEPRECATED - please use custom_exit instead.
|
2021-08-08 09:38:34 +00:00
|
|
|
Custom exit signal logic indicating that specified position should be sold. Returning a
|
|
|
|
string or True from this method is equal to setting exit signal on a candle at specified
|
|
|
|
time. This method is not called when exit signal is set.
|
2021-04-17 07:49:09 +00:00
|
|
|
|
2021-08-08 09:38:34 +00:00
|
|
|
This method should be overridden to create exit signals that depend on trade parameters. For
|
|
|
|
example you could implement an exit relative to the candle when the trade was opened,
|
2021-08-04 04:46:21 +00:00
|
|
|
or a custom 1:2 risk-reward ROI.
|
2021-04-17 07:49:09 +00:00
|
|
|
|
2021-08-08 09:38:34 +00:00
|
|
|
Custom exit reason max length is 64. Exceeding characters will be removed.
|
2021-04-20 08:17:00 +00:00
|
|
|
|
2021-04-17 07:49:09 +00:00
|
|
|
:param pair: Pair that's currently analyzed
|
|
|
|
:param trade: trade object.
|
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2021-04-17 07:49:09 +00:00
|
|
|
: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.
|
2022-04-03 17:36:32 +00:00
|
|
|
:return: To execute exit, return a string with custom exit reason or True. Otherwise return
|
2021-04-20 08:17:00 +00:00
|
|
|
None or False.
|
2021-04-17 07:49:09 +00:00
|
|
|
"""
|
2021-04-20 08:17:00 +00:00
|
|
|
return None
|
2021-04-17 07:49:09 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def custom_exit(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
current_profit: float,
|
|
|
|
**kwargs,
|
|
|
|
) -> Optional[Union[str, bool]]:
|
2022-03-12 10:15:27 +00:00
|
|
|
"""
|
|
|
|
Custom exit signal logic indicating that specified position should be sold. Returning a
|
|
|
|
string or True from this method is equal to setting exit signal on a candle at specified
|
|
|
|
time. This method is not called when exit signal is set.
|
|
|
|
|
|
|
|
This method should be overridden to create exit signals that depend on trade parameters. For
|
|
|
|
example you could implement an exit relative to the candle when the trade was opened,
|
|
|
|
or a custom 1:2 risk-reward ROI.
|
|
|
|
|
|
|
|
Custom exit reason max length is 64. Exceeding characters will be removed.
|
|
|
|
|
|
|
|
:param pair: Pair that's currently analyzed
|
|
|
|
:param trade: trade object.
|
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2022-03-12 10:15:27 +00:00
|
|
|
: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.
|
2022-04-03 17:36:32 +00:00
|
|
|
:return: To execute exit, return a string with custom exit reason or True. Otherwise return
|
2022-03-12 10:15:27 +00:00
|
|
|
None or False.
|
|
|
|
"""
|
|
|
|
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def custom_stake_amount(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
proposed_stake: float,
|
|
|
|
min_stake: Optional[float],
|
|
|
|
max_stake: float,
|
|
|
|
leverage: float,
|
|
|
|
entry_tag: Optional[str],
|
|
|
|
side: str,
|
|
|
|
**kwargs,
|
|
|
|
) -> float:
|
2021-07-11 09:20:31 +00:00
|
|
|
"""
|
2021-09-22 18:14:52 +00:00
|
|
|
Customize stake size for each new trade.
|
2021-07-11 09:20:31 +00:00
|
|
|
|
|
|
|
:param pair: Pair that's currently analyzed
|
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2021-07-11 09:20:31 +00:00
|
|
|
: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.
|
2022-07-08 17:44:20 +00:00
|
|
|
:param leverage: Leverage selected for this trade.
|
2022-01-23 18:32:38 +00:00
|
|
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
2021-09-26 17:35:54 +00:00
|
|
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
2021-07-11 09:20:31 +00:00
|
|
|
:return: A stake size, which is between min_stake and max_stake.
|
|
|
|
"""
|
|
|
|
return proposed_stake
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def adjust_trade_position(
|
|
|
|
self,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
current_profit: float,
|
|
|
|
min_stake: Optional[float],
|
|
|
|
max_stake: float,
|
|
|
|
current_entry_rate: float,
|
|
|
|
current_exit_rate: float,
|
|
|
|
current_entry_profit: float,
|
|
|
|
current_exit_profit: float,
|
|
|
|
**kwargs,
|
|
|
|
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
|
2021-12-07 09:16:11 +00:00
|
|
|
"""
|
2022-07-31 12:19:04 +00:00
|
|
|
Custom trade adjustment logic, returning the stake amount that a trade should be
|
|
|
|
increased or decreased.
|
2023-09-14 16:27:41 +00:00
|
|
|
This means extra entry or exit orders with additional fees.
|
2022-01-23 18:40:12 +00:00
|
|
|
Only called when `position_adjustment_enable` is set to True.
|
2021-12-07 09:16:11 +00:00
|
|
|
|
|
|
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
|
|
|
|
2021-12-11 15:14:04 +00:00
|
|
|
When not implemented by a strategy, returns None
|
2021-12-07 09:16:11 +00:00
|
|
|
|
|
|
|
:param trade: trade object.
|
|
|
|
:param current_time: datetime object, containing the current datetime
|
2023-09-14 16:27:41 +00:00
|
|
|
:param current_rate: Current entry rate (same as current_entry_profit)
|
|
|
|
:param current_profit: Current profit (as ratio), calculated based on current_rate
|
|
|
|
(same as current_entry_profit).
|
2022-07-31 12:19:04 +00:00
|
|
|
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
|
|
|
|
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
|
|
|
|
:param current_entry_rate: Current rate using entry pricing.
|
|
|
|
:param current_exit_rate: Current rate using exit pricing.
|
|
|
|
:param current_entry_profit: Current profit using entry pricing.
|
|
|
|
:param current_exit_profit: Current profit using exit pricing.
|
2021-12-07 09:16:11 +00:00
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
2022-07-31 12:19:04 +00:00
|
|
|
:return float: Stake amount to adjust your trade,
|
|
|
|
Positive values to increase position, Negative values to decrease position.
|
|
|
|
Return None for no action.
|
2024-01-28 19:05:40 +00:00
|
|
|
Optionally, return a tuple with a 2nd element with an order reason
|
2021-12-07 09:16:11 +00:00
|
|
|
"""
|
2021-12-09 12:47:44 +00:00
|
|
|
return None
|
2021-12-07 09:16:11 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def adjust_entry_price(
|
|
|
|
self,
|
|
|
|
trade: Trade,
|
|
|
|
order: Optional[Order],
|
|
|
|
pair: str,
|
|
|
|
current_time: datetime,
|
|
|
|
proposed_rate: float,
|
|
|
|
current_order_rate: float,
|
|
|
|
entry_tag: Optional[str],
|
|
|
|
side: str,
|
|
|
|
**kwargs,
|
|
|
|
) -> float:
|
2022-04-16 12:04:22 +00:00
|
|
|
"""
|
2022-04-18 18:17:39 +00:00
|
|
|
Entry price re-adjustment logic, returning the user desired limit price.
|
2022-05-06 04:23:06 +00:00
|
|
|
This only executes when a order was already placed, still open (unfilled fully or partially)
|
2022-04-18 18:17:39 +00:00
|
|
|
and not timed out on subsequent candles after entry trigger.
|
2022-04-16 12:04:22 +00:00
|
|
|
|
2022-04-28 21:10:17 +00:00
|
|
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
2022-04-16 12:04:22 +00:00
|
|
|
|
2022-04-28 21:10:17 +00:00
|
|
|
When not implemented by a strategy, returns current_order_rate as default.
|
|
|
|
If current_order_rate is returned then the existing order is maintained.
|
2022-04-18 18:17:39 +00:00
|
|
|
If None is returned then order gets canceled but not replaced by a new one.
|
|
|
|
|
2022-04-16 12:04:22 +00:00
|
|
|
:param pair: Pair that's currently analyzed
|
|
|
|
:param trade: Trade object.
|
2022-04-18 18:17:39 +00:00
|
|
|
:param order: Order object
|
2022-04-16 12:04:22 +00:00
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-04-28 21:10:17 +00:00
|
|
|
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
|
|
|
:param current_order_rate: Rate of the existing order in place.
|
2022-04-16 12:04:22 +00:00
|
|
|
: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
|
|
|
|
|
|
|
|
"""
|
2022-04-28 21:10:17 +00:00
|
|
|
return current_order_rate
|
2022-04-16 12:04:22 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def leverage(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
proposed_leverage: float,
|
|
|
|
max_leverage: float,
|
|
|
|
entry_tag: Optional[str],
|
|
|
|
side: str,
|
|
|
|
**kwargs,
|
|
|
|
) -> float:
|
2021-09-22 18:14:52 +00:00
|
|
|
"""
|
2022-02-23 19:19:52 +00:00
|
|
|
Customize leverage for each new trade. This method is only called in futures mode.
|
2021-09-22 18:14:52 +00:00
|
|
|
|
|
|
|
:param pair: Pair that's currently analyzed
|
|
|
|
:param current_time: datetime object, containing the current datetime
|
2022-03-27 16:03:49 +00:00
|
|
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
2021-09-22 18:14:52 +00:00
|
|
|
:param proposed_leverage: A leverage proposed by the bot.
|
|
|
|
:param max_leverage: Max leverage allowed on this pair
|
2022-06-05 08:21:06 +00:00
|
|
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
2021-09-22 18:14:52 +00:00
|
|
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
|
|
|
:return: A leverage amount, which is between 1.0 and max_leverage.
|
|
|
|
"""
|
|
|
|
return 1.0
|
|
|
|
|
2020-05-16 08:09:50 +00:00
|
|
|
def informative_pairs(self) -> ListPairsWithTimeframes:
|
2019-01-21 19:22:27 +00:00
|
|
|
"""
|
2019-01-26 18:22:45 +00:00
|
|
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
2021-06-25 13:45:49 +00:00
|
|
|
These pair/interval combinations are non-tradable, unless they are part
|
2019-01-21 19:22:27 +00:00
|
|
|
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 []
|
|
|
|
|
2021-12-04 13:49:45 +00:00
|
|
|
def version(self) -> Optional[str]:
|
2021-11-20 16:26:07 +00:00
|
|
|
"""
|
|
|
|
Returns version of the strategy.
|
|
|
|
"""
|
|
|
|
return None
|
2019-01-21 19:22:27 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def populate_any_indicators(
|
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
df: DataFrame,
|
|
|
|
tf: str,
|
|
|
|
informative: Optional[DataFrame] = None,
|
|
|
|
set_generalized_indicators: bool = False,
|
|
|
|
) -> DataFrame:
|
2022-05-09 13:25:00 +00:00
|
|
|
"""
|
2022-12-27 14:37:01 +00:00
|
|
|
DEPRECATED - USE FEATURE ENGINEERING FUNCTIONS INSTEAD
|
2022-05-09 13:25:00 +00:00
|
|
|
Function designed to automatically generate, name and merge features
|
|
|
|
from user indicated timeframes in the configuration file. User can add
|
|
|
|
additional features here, but must follow the naming convention.
|
2022-07-22 15:46:14 +00:00
|
|
|
This method is *only* used in FreqaiDataKitchen class and therefore
|
|
|
|
it is only called if FreqAI is active.
|
2022-07-24 14:51:48 +00:00
|
|
|
:param pair: pair to be used as informative
|
|
|
|
:param df: strategy dataframe which will receive merges from informatives
|
|
|
|
:param tf: timeframe of the dataframe which will modify the feature names
|
|
|
|
:param informative: the dataframe associated with the informative pair
|
2022-05-09 13:25:00 +00:00
|
|
|
"""
|
|
|
|
return df
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def feature_engineering_expand_all(
|
|
|
|
self, dataframe: DataFrame, period: int, metadata: Dict, **kwargs
|
|
|
|
) -> DataFrame:
|
2022-12-27 14:37:01 +00:00
|
|
|
"""
|
2022-12-28 12:25:40 +00:00
|
|
|
*Only functional with FreqAI enabled strategies*
|
|
|
|
This function will automatically expand the defined features on the config defined
|
|
|
|
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
|
|
|
|
`include_corr_pairs`. In other words, a single feature defined in this function
|
|
|
|
will automatically expand to a total of
|
|
|
|
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
|
|
|
|
`include_corr_pairs` numbers of features added to the model.
|
|
|
|
|
|
|
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
|
|
|
|
|
|
|
More details on how these config defined parameters accelerate feature engineering
|
|
|
|
in the documentation at:
|
|
|
|
|
|
|
|
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
|
|
|
|
|
|
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
|
|
|
|
2023-02-04 19:04:16 +00:00
|
|
|
:param dataframe: strategy dataframe which will receive the features
|
2022-12-27 14:37:01 +00:00
|
|
|
:param period: period of the indicator - usage example:
|
2023-02-04 15:56:36 +00:00
|
|
|
:param metadata: metadata of current pair
|
2022-12-27 14:37:01 +00:00
|
|
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
|
|
|
"""
|
|
|
|
return dataframe
|
|
|
|
|
2023-04-23 17:30:30 +00:00
|
|
|
def feature_engineering_expand_basic(
|
2024-05-12 14:41:08 +00:00
|
|
|
self, dataframe: DataFrame, metadata: Dict, **kwargs
|
|
|
|
) -> DataFrame:
|
2022-12-27 14:37:01 +00:00
|
|
|
"""
|
2022-12-28 12:25:40 +00:00
|
|
|
*Only functional with FreqAI enabled strategies*
|
|
|
|
This function will automatically expand the defined features on the config defined
|
|
|
|
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
|
|
|
In other words, a single feature defined in this function
|
|
|
|
will automatically expand to a total of
|
|
|
|
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
|
|
|
|
numbers of features added to the model.
|
|
|
|
|
|
|
|
Features defined here will *not* be automatically duplicated on user defined
|
|
|
|
`indicator_periods_candles`
|
|
|
|
|
|
|
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
|
|
|
|
|
|
|
More details on how these config defined parameters accelerate feature engineering
|
|
|
|
in the documentation at:
|
|
|
|
|
|
|
|
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
|
|
|
|
|
|
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
|
|
|
|
2023-02-04 19:04:16 +00:00
|
|
|
:param dataframe: strategy dataframe which will receive the features
|
2023-02-04 15:56:36 +00:00
|
|
|
:param metadata: metadata of current pair
|
2022-12-27 14:37:01 +00:00
|
|
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
2022-12-28 12:25:40 +00:00
|
|
|
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
|
2022-12-27 14:37:01 +00:00
|
|
|
"""
|
|
|
|
return dataframe
|
|
|
|
|
2023-04-23 17:36:17 +00:00
|
|
|
def feature_engineering_standard(
|
2024-05-12 14:41:08 +00:00
|
|
|
self, dataframe: DataFrame, metadata: Dict, **kwargs
|
|
|
|
) -> DataFrame:
|
2022-12-27 14:37:01 +00:00
|
|
|
"""
|
2022-12-28 12:25:40 +00:00
|
|
|
*Only functional with FreqAI enabled strategies*
|
|
|
|
This optional function will be called once with the dataframe of the base timeframe.
|
|
|
|
This is the final function to be called, which means that the dataframe entering this
|
|
|
|
function will contain all the features and columns created by all other
|
|
|
|
freqai_feature_engineering_* functions.
|
|
|
|
|
|
|
|
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
|
|
|
|
This function is a good place for any feature that should not be auto-expanded upon
|
|
|
|
(e.g. day of the week).
|
|
|
|
|
|
|
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
|
|
|
|
|
|
|
More details about feature engineering available:
|
|
|
|
|
|
|
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
|
|
|
|
2023-02-04 19:04:16 +00:00
|
|
|
:param dataframe: strategy dataframe which will receive the features
|
2023-02-04 15:56:36 +00:00
|
|
|
:param metadata: metadata of current pair
|
2022-12-27 14:37:01 +00:00
|
|
|
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
|
|
|
"""
|
|
|
|
return dataframe
|
|
|
|
|
2023-04-23 17:30:30 +00:00
|
|
|
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
|
2022-12-27 14:37:01 +00:00
|
|
|
"""
|
2022-12-28 12:25:40 +00:00
|
|
|
*Only functional with FreqAI enabled strategies*
|
2022-12-27 14:37:01 +00:00
|
|
|
Required function to set the targets for the model.
|
2022-12-28 12:25:40 +00:00
|
|
|
All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
|
|
|
|
|
|
|
More details about feature engineering available:
|
|
|
|
|
|
|
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
|
|
|
|
2023-02-04 19:04:16 +00:00
|
|
|
:param dataframe: strategy dataframe which will receive the targets
|
2023-02-04 15:56:36 +00:00
|
|
|
:param metadata: metadata of current pair
|
2022-12-27 14:37:01 +00:00
|
|
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
|
|
|
"""
|
|
|
|
return dataframe
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
###
|
|
|
|
# END - Intended to be overridden by strategy
|
|
|
|
###
|
2020-06-13 05:09:44 +00:00
|
|
|
|
2023-08-14 14:12:04 +00:00
|
|
|
_ft_stop_uses_after_fill = False
|
|
|
|
|
2024-01-28 19:21:47 +00:00
|
|
|
def _adjust_trade_position_internal(
|
2024-05-12 14:41:08 +00:00
|
|
|
self,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_rate: float,
|
|
|
|
current_profit: float,
|
|
|
|
min_stake: Optional[float],
|
|
|
|
max_stake: float,
|
|
|
|
current_entry_rate: float,
|
|
|
|
current_exit_rate: float,
|
|
|
|
current_entry_profit: float,
|
|
|
|
current_exit_profit: float,
|
|
|
|
**kwargs,
|
2024-01-28 21:42:03 +00:00
|
|
|
) -> Tuple[Optional[float], str]:
|
2024-01-28 19:21:47 +00:00
|
|
|
"""
|
|
|
|
wrapper around adjust_trade_position to handle the return value
|
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
resp = strategy_safe_wrapper(
|
|
|
|
self.adjust_trade_position, default_retval=(None, ""), supress_error=True
|
|
|
|
)(
|
|
|
|
trade=trade,
|
|
|
|
current_time=current_time,
|
|
|
|
current_rate=current_rate,
|
|
|
|
current_profit=current_profit,
|
|
|
|
min_stake=min_stake,
|
|
|
|
max_stake=max_stake,
|
|
|
|
current_entry_rate=current_entry_rate,
|
|
|
|
current_exit_rate=current_exit_rate,
|
|
|
|
current_entry_profit=current_entry_profit,
|
|
|
|
current_exit_profit=current_exit_profit,
|
|
|
|
**kwargs,
|
2024-01-28 19:21:47 +00:00
|
|
|
)
|
2024-05-12 14:41:08 +00:00
|
|
|
order_tag = ""
|
2024-01-28 19:21:47 +00:00
|
|
|
if isinstance(resp, tuple):
|
|
|
|
if len(resp) >= 1:
|
|
|
|
stake_amount = resp[0]
|
|
|
|
if len(resp) > 1:
|
2024-05-12 14:41:08 +00:00
|
|
|
order_tag = resp[1] or ""
|
2024-01-28 19:21:47 +00:00
|
|
|
else:
|
|
|
|
stake_amount = resp
|
|
|
|
return stake_amount, order_tag
|
|
|
|
|
2022-09-17 08:11:04 +00:00
|
|
|
def __informative_pairs_freqai(self) -> ListPairsWithTimeframes:
|
|
|
|
"""
|
|
|
|
Create informative-pairs needed for FreqAI
|
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
if self.config.get("freqai", {}).get("enabled", False):
|
2022-09-17 08:11:04 +00:00
|
|
|
whitelist_pairs = self.dp.current_whitelist()
|
2024-05-12 14:41:08 +00:00
|
|
|
candle_type = self.config.get("candle_type_def", CandleType.SPOT)
|
2022-09-17 08:11:04 +00:00
|
|
|
corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
|
|
|
|
informative_pairs = []
|
|
|
|
for tf in self.config["freqai"]["feature_parameters"]["include_timeframes"]:
|
|
|
|
for pair in set(whitelist_pairs + corr_pairs):
|
|
|
|
informative_pairs.append((pair, tf, candle_type))
|
|
|
|
return informative_pairs
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
2021-09-19 23:44:12 +00:00
|
|
|
def gather_informative_pairs(self) -> ListPairsWithTimeframes:
|
|
|
|
"""
|
|
|
|
Internal method which gathers all informative pairs (user or automatically defined).
|
|
|
|
"""
|
|
|
|
informative_pairs = self.informative_pairs()
|
2021-11-28 14:43:04 +00:00
|
|
|
# Compatibility code for 2 tuple informative pairs
|
2021-12-03 13:11:24 +00:00
|
|
|
informative_pairs = [
|
2024-05-12 14:41:08 +00:00
|
|
|
(
|
|
|
|
p[0],
|
|
|
|
p[1],
|
2024-05-13 17:49:15 +00:00
|
|
|
(
|
|
|
|
CandleType.from_string(p[2])
|
|
|
|
if len(p) > 2 and p[2] != ""
|
|
|
|
else self.config.get("candle_type_def", CandleType.SPOT)
|
|
|
|
),
|
2024-05-12 14:41:08 +00:00
|
|
|
)
|
|
|
|
for p in informative_pairs
|
|
|
|
]
|
2021-09-19 23:44:12 +00:00
|
|
|
for inf_data, _ in self._ft_informative:
|
2022-01-28 15:58:07 +00:00
|
|
|
# Get default candle type if not provided explicitly.
|
2024-05-12 14:41:08 +00:00
|
|
|
candle_type = (
|
|
|
|
inf_data.candle_type
|
|
|
|
if inf_data.candle_type
|
|
|
|
else self.config.get("candle_type_def", CandleType.SPOT)
|
|
|
|
)
|
2021-09-19 23:44:12 +00:00
|
|
|
if inf_data.asset:
|
2023-11-01 10:01:55 +00:00
|
|
|
if any(s in inf_data.asset for s in ("{BASE}", "{base}")):
|
|
|
|
for pair in self.dp.current_whitelist():
|
|
|
|
pair_tf = (
|
|
|
|
_format_pair_name(self.config, inf_data.asset, self.dp.market(pair)),
|
|
|
|
inf_data.timeframe,
|
|
|
|
candle_type,
|
|
|
|
)
|
|
|
|
informative_pairs.append(pair_tf)
|
|
|
|
|
|
|
|
else:
|
|
|
|
pair_tf = (
|
|
|
|
_format_pair_name(self.config, inf_data.asset),
|
|
|
|
inf_data.timeframe,
|
|
|
|
candle_type,
|
|
|
|
)
|
|
|
|
informative_pairs.append(pair_tf)
|
2021-09-19 23:44:12 +00:00
|
|
|
else:
|
|
|
|
for pair in self.dp.current_whitelist():
|
2022-01-28 15:58:07 +00:00
|
|
|
informative_pairs.append((pair, inf_data.timeframe, candle_type))
|
2022-09-17 08:11:04 +00:00
|
|
|
informative_pairs.extend(self.__informative_pairs_freqai())
|
2021-09-19 23:44:12 +00:00
|
|
|
return list(set(informative_pairs))
|
|
|
|
|
2018-07-12 18:38:14 +00:00
|
|
|
def get_strategy_name(self) -> str:
|
|
|
|
"""
|
|
|
|
Returns strategy class name
|
|
|
|
"""
|
2018-07-19 17:41:42 +00:00
|
|
|
return self.__class__.__name__
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def lock_pair(
|
|
|
|
self, pair: str, until: datetime, reason: Optional[str] = None, side: str = "*"
|
|
|
|
) -> None:
|
2019-08-12 14:29:09 +00:00
|
|
|
"""
|
|
|
|
Locks pair until a given timestamp happens.
|
|
|
|
Locked pairs are not analyzed, and are prevented from opening new trades.
|
2019-12-22 08:46:00 +00:00
|
|
|
Locks can only count up (allowing users to lock pairs for a longer period of time).
|
|
|
|
To remove a lock from a pair, use `unlock_pair()`
|
2019-08-12 17:50:22 +00:00
|
|
|
:param pair: Pair to lock
|
|
|
|
:param until: datetime in UTC until the pair should be blocked from opening new trades.
|
|
|
|
Needs to be timezone aware `datetime.now(timezone.utc)`
|
2020-10-21 17:35:57 +00:00
|
|
|
:param reason: Optional string explaining why the pair was locked.
|
2022-04-24 12:38:23 +00:00
|
|
|
:param side: Side to check, can be long, short or '*'
|
2019-08-12 14:29:09 +00:00
|
|
|
"""
|
2022-04-24 09:23:26 +00:00
|
|
|
PairLocks.lock_pair(pair, until, reason, side=side)
|
2019-12-22 08:46:00 +00:00
|
|
|
|
2020-02-02 04:00:40 +00:00
|
|
|
def unlock_pair(self, pair: str) -> None:
|
2019-12-22 08:46:00 +00:00
|
|
|
"""
|
|
|
|
Unlocks a pair previously locked using lock_pair.
|
|
|
|
Not used by freqtrade itself, but intended to be used if users lock pairs
|
|
|
|
manually from within the strategy, to allow an easy way to unlock pairs.
|
|
|
|
:param pair: Unlock pair to allow trading again
|
|
|
|
"""
|
2020-10-25 09:54:30 +00:00
|
|
|
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
|
2019-08-12 17:50:22 +00:00
|
|
|
|
2021-10-25 22:04:40 +00:00
|
|
|
def unlock_reason(self, reason: str) -> None:
|
|
|
|
"""
|
|
|
|
Unlocks all pairs previously locked using lock_pair with specified reason.
|
|
|
|
Not used by freqtrade itself, but intended to be used if users lock pairs
|
|
|
|
manually from within the strategy, to allow an easy way to unlock pairs.
|
|
|
|
:param reason: Unlock pairs to allow trading again
|
|
|
|
"""
|
|
|
|
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def is_pair_locked(
|
|
|
|
self, pair: str, *, candle_date: Optional[datetime] = None, side: str = "*"
|
|
|
|
) -> bool:
|
2019-08-12 17:50:22 +00:00
|
|
|
"""
|
|
|
|
Checks if a pair is currently locked
|
2020-08-24 15:18:57 +00:00
|
|
|
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
|
|
|
|
and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
|
2021-09-09 08:10:12 +00:00
|
|
|
of 2 seconds for an entry order to happen on an old signal.
|
2021-06-25 17:13:31 +00:00
|
|
|
:param pair: "Pair to check"
|
2020-08-24 15:18:57 +00:00
|
|
|
:param candle_date: Date of the last candle. Optional, defaults to current date
|
2022-04-24 12:10:25 +00:00
|
|
|
:param side: Side to check, can be long, short or '*'
|
2020-08-24 15:31:00 +00:00
|
|
|
:returns: locking state of the pair in question.
|
2019-08-12 17:50:22 +00:00
|
|
|
"""
|
2020-10-17 09:28:34 +00:00
|
|
|
|
2020-08-24 09:09:09 +00:00
|
|
|
if not candle_date:
|
2020-10-17 09:28:34 +00:00
|
|
|
# Simple call ...
|
2022-04-24 12:10:25 +00:00
|
|
|
return PairLocks.is_pair_locked(pair, side=side)
|
2020-08-24 09:09:09 +00:00
|
|
|
else:
|
|
|
|
lock_time = timeframe_to_next_date(self.timeframe, candle_date)
|
2022-04-24 12:10:25 +00:00
|
|
|
return PairLocks.is_pair_locked(pair, lock_time, side=side)
|
2019-08-12 14:29:09 +00:00
|
|
|
|
2022-09-08 05:01:37 +00:00
|
|
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2020-03-08 10:35:31 +00:00
|
|
|
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
2021-09-09 08:10:12 +00:00
|
|
|
add several TA indicators and entry order signal to it
|
2023-07-21 18:27:52 +00:00
|
|
|
Should only be used in live.
|
2020-03-08 10:35:31 +00:00
|
|
|
:param dataframe: Dataframe containing data from exchange
|
2019-08-04 08:20:31 +00:00
|
|
|
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
2020-03-08 10:35:31 +00:00
|
|
|
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
2019-08-04 08:20:31 +00:00
|
|
|
"""
|
|
|
|
logger.debug("TA Analysis Launched")
|
2022-08-31 17:43:02 +00:00
|
|
|
dataframe = self.advise_indicators(dataframe, metadata)
|
2021-09-22 18:42:31 +00:00
|
|
|
dataframe = self.advise_entry(dataframe, metadata)
|
|
|
|
dataframe = self.advise_exit(dataframe, metadata)
|
2019-08-04 08:20:31 +00:00
|
|
|
return dataframe
|
|
|
|
|
2022-09-08 05:01:37 +00:00
|
|
|
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2019-08-04 08:20:31 +00:00
|
|
|
"""
|
2020-03-08 10:35:31 +00:00
|
|
|
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
2019-08-04 08:20:31 +00:00
|
|
|
add several TA indicators and buy signal to it
|
2019-08-04 08:21:22 +00:00
|
|
|
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
|
2020-03-08 10:35:31 +00:00
|
|
|
:param dataframe: Dataframe containing data from exchange
|
2019-08-04 08:21:22 +00:00
|
|
|
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
2020-03-08 10:35:31 +00:00
|
|
|
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
pair = str(metadata.get("pair"))
|
2018-08-03 07:33:34 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
new_candle = self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]["date"]
|
2018-09-01 17:53:49 +00:00
|
|
|
# Test if seen this pair and last candle before.
|
2018-12-13 18:43:17 +00:00
|
|
|
# always run if process_only_new_candles is set to false
|
2022-10-26 18:22:58 +00:00
|
|
|
if not self.process_only_new_candles or new_candle:
|
2018-08-03 07:33:34 +00:00
|
|
|
# Defs that only make change on new candle data.
|
2022-08-31 17:43:02 +00:00
|
|
|
dataframe = self.analyze_ticker(dataframe, metadata)
|
2022-08-22 04:45:36 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]["date"]
|
2022-08-22 04:45:36 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
candle_type = self.config.get("candle_type_def", CandleType.SPOT)
|
2022-08-22 04:45:36 +00:00
|
|
|
self.dp._set_cached_df(pair, self.timeframe, dataframe, candle_type=candle_type)
|
2022-10-26 18:22:58 +00:00
|
|
|
self.dp._emit_df((pair, self.timeframe, candle_type), dataframe, new_candle)
|
2022-08-22 04:45:36 +00:00
|
|
|
|
2018-08-03 07:33:34 +00:00
|
|
|
else:
|
2019-02-13 09:42:39 +00:00
|
|
|
logger.debug("Skipping TA Analysis for already analyzed candle")
|
2022-08-31 01:21:34 +00:00
|
|
|
dataframe = remove_entry_exit_signals(dataframe)
|
2018-08-03 07:33:34 +00:00
|
|
|
|
2019-02-10 18:02:53 +00:00
|
|
|
logger.debug("Loop Analysis Launched")
|
2018-08-03 07:33:34 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
return dataframe
|
|
|
|
|
2022-09-08 05:01:37 +00:00
|
|
|
def analyze_pair(self, pair: str) -> None:
|
2020-06-13 16:06:52 +00:00
|
|
|
"""
|
|
|
|
Fetch data for this pair from dataprovider and analyze.
|
|
|
|
Stores the dataframe into the dataprovider.
|
|
|
|
The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`.
|
|
|
|
:param pair: Pair to analyze.
|
|
|
|
"""
|
2022-09-08 05:01:37 +00:00
|
|
|
dataframe = self.dp.ohlcv(
|
2024-05-12 14:41:08 +00:00
|
|
|
pair, self.timeframe, candle_type=self.config.get("candle_type_def", CandleType.SPOT)
|
2022-09-08 05:01:37 +00:00
|
|
|
)
|
2020-06-13 16:06:52 +00:00
|
|
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.warning("Empty candle (OHLCV) data for pair %s", pair)
|
2020-06-13 16:06:52 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
df_len, df_close, df_date = self.preserve_df(dataframe)
|
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
dataframe = strategy_safe_wrapper(self._analyze_ticker_internal, message="")(
|
|
|
|
dataframe, {"pair": pair}
|
|
|
|
)
|
2020-06-13 16:06:52 +00:00
|
|
|
|
|
|
|
self.assert_df(dataframe, df_len, df_close, df_date)
|
|
|
|
except StrategyError as error:
|
|
|
|
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
|
|
|
|
return
|
|
|
|
|
|
|
|
if dataframe.empty:
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.warning("Empty dataframe for pair %s", pair)
|
2020-06-13 16:06:52 +00:00
|
|
|
return
|
|
|
|
|
2022-09-08 05:01:37 +00:00
|
|
|
def analyze(self, pairs: List[str]) -> None:
|
2020-06-18 05:05:06 +00:00
|
|
|
"""
|
|
|
|
Analyze all pairs using analyze_pair().
|
|
|
|
:param pairs: List of pairs to analyze
|
|
|
|
"""
|
2020-06-13 16:06:52 +00:00
|
|
|
for pair in pairs:
|
2022-09-04 16:22:10 +00:00
|
|
|
self.analyze_pair(pair)
|
2020-06-13 16:06:52 +00:00
|
|
|
|
2022-09-08 05:01:37 +00:00
|
|
|
@staticmethod
|
2020-03-26 06:05:30 +00:00
|
|
|
def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
|
2024-05-12 14:41:08 +00:00
|
|
|
"""keep some data for dataframes"""
|
2020-03-26 06:05:30 +00:00
|
|
|
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
|
2020-03-24 12:54:46 +00:00
|
|
|
|
2020-05-29 17:37:18 +00:00
|
|
|
def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
|
2020-06-18 05:05:06 +00:00
|
|
|
"""
|
|
|
|
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
|
|
|
|
"""
|
2021-06-19 17:32:29 +00:00
|
|
|
message_template = "Dataframe returned from strategy has mismatching {}."
|
2020-03-26 06:05:30 +00:00
|
|
|
message = ""
|
2021-06-19 17:32:29 +00:00
|
|
|
if dataframe is None:
|
|
|
|
message = "No dataframe returned (return statement missing?)."
|
2024-05-12 14:41:08 +00:00
|
|
|
elif "enter_long" not in dataframe:
|
2021-09-21 17:14:14 +00:00
|
|
|
message = "enter_long/buy column not set."
|
2021-06-19 17:32:29 +00:00
|
|
|
elif df_len != len(dataframe):
|
|
|
|
message = message_template.format("length")
|
2020-03-26 06:05:30 +00:00
|
|
|
elif df_close != dataframe["close"].iloc[-1]:
|
2021-06-19 17:32:29 +00:00
|
|
|
message = message_template.format("last close price")
|
2020-03-26 06:05:30 +00:00
|
|
|
elif df_date != dataframe["date"].iloc[-1]:
|
2021-06-19 17:32:29 +00:00
|
|
|
message = message_template.format("last date")
|
2020-03-26 06:05:30 +00:00
|
|
|
if message:
|
2020-05-29 17:37:18 +00:00
|
|
|
if self.disable_dataframe_checks:
|
2021-06-19 17:32:29 +00:00
|
|
|
logger.warning(message)
|
2020-05-29 17:37:18 +00:00
|
|
|
else:
|
2021-06-19 17:32:29 +00:00
|
|
|
raise StrategyError(message)
|
2020-03-24 12:54:46 +00:00
|
|
|
|
2021-08-24 18:24:51 +00:00
|
|
|
def get_latest_candle(
|
2021-07-21 19:23:34 +00:00
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
timeframe: str,
|
2021-08-08 09:38:34 +00:00
|
|
|
dataframe: DataFrame,
|
2023-05-14 08:32:18 +00:00
|
|
|
) -> Tuple[Optional[DataFrame], Optional[datetime]]:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2021-09-09 08:10:12 +00:00
|
|
|
Calculates current signal based based on the entry order or exit order
|
2021-08-18 12:23:44 +00:00
|
|
|
columns of the dataframe.
|
2022-04-05 04:50:44 +00:00
|
|
|
Used by Bot to get the signal to enter, or exit
|
2018-07-16 05:11:17 +00:00
|
|
|
:param pair: pair in format ANT/BTC
|
2020-06-13 16:06:52 +00:00
|
|
|
:param timeframe: timeframe to use
|
2020-06-18 06:01:09 +00:00
|
|
|
:param dataframe: Analyzed dataframe to get signal from.
|
2021-08-24 18:24:51 +00:00
|
|
|
:return: (None, None) or (Dataframe, latest_date) - corresponding to the last candle
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2018-12-12 18:35:51 +00:00
|
|
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.warning(f"Empty candle (OHLCV) data for pair {pair}")
|
2021-08-24 18:40:35 +00:00
|
|
|
return None, None
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
latest_date = dataframe["date"].max()
|
|
|
|
latest = dataframe.loc[dataframe["date"] == latest_date].iloc[-1]
|
2023-05-14 16:31:09 +00:00
|
|
|
# Explicitly convert to datetime object to ensure the below comparison does not fail
|
2023-05-14 08:32:18 +00:00
|
|
|
latest_date = latest_date.to_pydatetime()
|
2020-03-11 16:28:03 +00:00
|
|
|
|
2020-03-11 15:34:23 +00:00
|
|
|
# Check if dataframe is out of date
|
2020-06-13 16:06:52 +00:00
|
|
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
2024-05-12 14:41:08 +00:00
|
|
|
offset = self.config.get("exchange", {}).get("outdated_offset", 5)
|
2023-05-14 08:32:18 +00:00
|
|
|
if latest_date < (dt_now() - timedelta(minutes=timeframe_minutes * 2 + offset)):
|
2018-07-16 05:11:17 +00:00
|
|
|
logger.warning(
|
2024-05-12 14:41:08 +00:00
|
|
|
"Outdated history for pair %s. Last tick is %s minutes old",
|
|
|
|
pair,
|
|
|
|
int((dt_now() - latest_date).total_seconds() // 60),
|
2018-07-16 05:11:17 +00:00
|
|
|
)
|
2021-08-24 18:24:51 +00:00
|
|
|
return None, None
|
|
|
|
return latest, latest_date
|
|
|
|
|
|
|
|
def get_exit_signal(
|
2024-05-12 14:41:08 +00:00
|
|
|
self, pair: str, timeframe: str, dataframe: DataFrame, is_short: Optional[bool] = None
|
2021-11-06 14:24:52 +00:00
|
|
|
) -> Tuple[bool, bool, Optional[str]]:
|
2021-08-24 18:24:51 +00:00
|
|
|
"""
|
2022-04-05 04:50:44 +00:00
|
|
|
Calculates current exit signal based based on the dataframe
|
2021-08-24 18:24:51 +00:00
|
|
|
columns of the dataframe.
|
|
|
|
Used by Bot to get the signal to exit.
|
|
|
|
depending on is_short, looks at "short" or "long" columns.
|
|
|
|
:param pair: pair in format ANT/BTC
|
|
|
|
:param timeframe: timeframe to use
|
|
|
|
:param dataframe: Analyzed dataframe to get signal from.
|
|
|
|
:param is_short: Indicating existing trade direction.
|
|
|
|
:return: (enter, exit) A bool-tuple with enter / exit values.
|
|
|
|
"""
|
2024-01-24 19:31:38 +00:00
|
|
|
latest, _latest_date = self.get_latest_candle(pair, timeframe, dataframe)
|
2021-08-24 18:24:51 +00:00
|
|
|
if latest is None:
|
2021-11-06 14:24:52 +00:00
|
|
|
return False, False, None
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2021-08-24 18:24:51 +00:00
|
|
|
if is_short:
|
2021-09-04 18:23:51 +00:00
|
|
|
enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
|
|
|
|
exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
|
2021-08-02 18:17:58 +00:00
|
|
|
|
2021-08-24 18:24:51 +00:00
|
|
|
else:
|
2021-09-04 18:23:51 +00:00
|
|
|
enter = latest[SignalType.ENTER_LONG.value] == 1
|
|
|
|
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
|
2021-10-18 20:56:41 +00:00
|
|
|
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
2022-01-16 07:04:39 +00:00
|
|
|
# Tags can be None, which does not resolve to False.
|
2024-05-12 14:41:08 +00:00
|
|
|
exit_tag = exit_tag if isinstance(exit_tag, str) and exit_tag != "nan" else None
|
2021-08-24 18:24:51 +00:00
|
|
|
|
2024-05-12 15:51:21 +00:00
|
|
|
logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) enter={enter} exit={exit_}")
|
2021-08-08 09:38:34 +00:00
|
|
|
|
2021-11-06 14:24:52 +00:00
|
|
|
return enter, exit_, exit_tag
|
2021-08-02 18:17:58 +00:00
|
|
|
|
2021-08-25 04:43:58 +00:00
|
|
|
def get_entry_signal(
|
2021-08-24 18:24:51 +00:00
|
|
|
self,
|
|
|
|
pair: str,
|
|
|
|
timeframe: str,
|
|
|
|
dataframe: DataFrame,
|
|
|
|
) -> Tuple[Optional[SignalDirection], Optional[str]]:
|
|
|
|
"""
|
2022-04-05 04:50:44 +00:00
|
|
|
Calculates current entry signal based based on the dataframe signals
|
2021-08-24 18:24:51 +00:00
|
|
|
columns of the dataframe.
|
2022-04-05 04:50:44 +00:00
|
|
|
Used by Bot to get the signal to enter trades.
|
2021-08-24 18:24:51 +00:00
|
|
|
:param pair: pair in format ANT/BTC
|
|
|
|
:param timeframe: timeframe to use
|
|
|
|
:param dataframe: Analyzed dataframe to get signal from.
|
|
|
|
:return: (SignalDirection, entry_tag)
|
|
|
|
"""
|
|
|
|
latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe)
|
2021-08-24 18:40:35 +00:00
|
|
|
if latest is None or latest_date is None:
|
|
|
|
return None, None
|
2021-08-02 18:17:58 +00:00
|
|
|
|
2021-08-25 04:43:58 +00:00
|
|
|
enter_long = latest[SignalType.ENTER_LONG.value] == 1
|
|
|
|
exit_long = latest.get(SignalType.EXIT_LONG.value, 0) == 1
|
2022-03-12 07:58:54 +00:00
|
|
|
enter_short = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
|
2021-08-25 04:43:58 +00:00
|
|
|
exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
|
2021-08-24 18:24:51 +00:00
|
|
|
|
|
|
|
enter_signal: Optional[SignalDirection] = None
|
2023-10-30 18:05:15 +00:00
|
|
|
enter_tag: Optional[str] = None
|
2021-08-24 18:24:51 +00:00
|
|
|
if enter_long == 1 and not any([exit_long, enter_short]):
|
|
|
|
enter_signal = SignalDirection.LONG
|
2023-10-30 18:05:15 +00:00
|
|
|
enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None)
|
2024-05-12 14:41:08 +00:00
|
|
|
if (
|
|
|
|
self.config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT
|
|
|
|
and self.can_short
|
|
|
|
and enter_short == 1
|
|
|
|
and not any([exit_short, enter_long])
|
|
|
|
):
|
2021-08-24 18:24:51 +00:00
|
|
|
enter_signal = SignalDirection.SHORT
|
2023-10-30 18:05:15 +00:00
|
|
|
enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None)
|
2021-07-22 18:25:15 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
enter_tag = enter_tag if isinstance(enter_tag, str) and enter_tag != "nan" else None
|
2022-01-22 16:25:21 +00:00
|
|
|
|
2021-01-05 13:49:35 +00:00
|
|
|
timeframe_seconds = timeframe_to_seconds(timeframe)
|
2021-08-24 18:24:51 +00:00
|
|
|
|
2021-08-18 12:03:44 +00:00
|
|
|
if self.ignore_expired_candle(
|
2023-05-14 08:32:18 +00:00
|
|
|
latest_date=latest_date,
|
|
|
|
current_time=dt_now(),
|
2021-08-18 12:03:44 +00:00
|
|
|
timeframe_seconds=timeframe_seconds,
|
2024-05-12 14:41:08 +00:00
|
|
|
enter=bool(enter_signal),
|
2021-08-18 12:03:44 +00:00
|
|
|
):
|
2023-10-30 18:05:15 +00:00
|
|
|
return None, enter_tag
|
2021-08-24 18:24:51 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.debug(
|
|
|
|
f"entry trigger: {latest['date']} (pair={pair}) "
|
|
|
|
f"enter={enter_long} enter_tag_value={enter_tag}"
|
|
|
|
)
|
2023-10-30 18:05:15 +00:00
|
|
|
return enter_signal, enter_tag
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2021-08-18 12:03:44 +00:00
|
|
|
def ignore_expired_candle(
|
2024-05-12 14:41:08 +00:00
|
|
|
self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, enter: bool
|
2021-08-18 12:03:44 +00:00
|
|
|
):
|
2021-08-08 09:38:34 +00:00
|
|
|
if self.ignore_buying_expired_candle_after and enter:
|
2021-01-07 06:51:49 +00:00
|
|
|
time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds))
|
2021-01-05 08:07:46 +00:00
|
|
|
return time_delta.total_seconds() > self.ignore_buying_expired_candle_after
|
|
|
|
else:
|
|
|
|
return False
|
2021-01-04 19:49:24 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def should_exit(
|
|
|
|
self,
|
|
|
|
trade: Trade,
|
|
|
|
rate: float,
|
|
|
|
current_time: datetime,
|
|
|
|
*,
|
|
|
|
enter: bool,
|
|
|
|
exit_: bool,
|
|
|
|
low: Optional[float] = None,
|
|
|
|
high: Optional[float] = None,
|
|
|
|
force_stoploss: float = 0,
|
|
|
|
) -> List[ExitCheckTuple]:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2021-09-09 08:10:12 +00:00
|
|
|
This function evaluates if one of the conditions required to trigger an exit order
|
2021-08-08 09:38:34 +00:00
|
|
|
has been reached, which can either be a stop-loss, ROI or exit-signal.
|
|
|
|
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
|
|
|
|
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
|
2019-03-17 15:02:13 +00:00
|
|
|
:param force_stoploss: Externally provided stoploss
|
2022-05-22 08:15:58 +00:00
|
|
|
:return: List of exit reasons - or empty list.
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2022-05-22 08:15:58 +00:00
|
|
|
exits: List[ExitCheckTuple] = []
|
2021-06-13 14:37:11 +00:00
|
|
|
current_rate = rate
|
2019-12-17 07:53:30 +00:00
|
|
|
current_profit = trade.calc_profit_ratio(current_rate)
|
2023-06-05 18:19:10 +00:00
|
|
|
current_profit_best = current_profit
|
|
|
|
if low is not None or high is not None:
|
|
|
|
# Set current rate to high for backtesting ROI exits
|
|
|
|
current_rate_best = (low if trade.is_short else high) or rate
|
|
|
|
current_profit_best = trade.calc_profit_ratio(current_rate_best)
|
2018-11-19 19:02:26 +00:00
|
|
|
|
2021-08-09 17:38:56 +00:00
|
|
|
trade.adjust_min_max_rates(high or current_rate, low or current_rate)
|
2019-03-16 18:54:34 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
stoplossflag = self.ft_stoploss_reached(
|
|
|
|
current_rate=current_rate,
|
|
|
|
trade=trade,
|
|
|
|
current_time=current_time,
|
|
|
|
current_profit=current_profit,
|
|
|
|
force_stoploss=force_stoploss,
|
|
|
|
low=low,
|
|
|
|
high=high,
|
|
|
|
)
|
2020-11-27 07:16:11 +00:00
|
|
|
|
2021-08-08 09:38:34 +00:00
|
|
|
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
|
2024-05-12 14:41:08 +00:00
|
|
|
roi_reached = not (enter and self.ignore_roi_if_entry_signal) and self.min_roi_reached(
|
|
|
|
trade=trade, current_profit=current_profit_best, current_time=current_time
|
|
|
|
)
|
2020-11-27 08:18:03 +00:00
|
|
|
|
2022-04-03 17:22:59 +00:00
|
|
|
exit_signal = ExitType.NONE
|
2024-05-12 14:41:08 +00:00
|
|
|
custom_reason = ""
|
2021-05-14 05:18:10 +00:00
|
|
|
|
2022-04-09 15:17:49 +00:00
|
|
|
if self.use_exit_signal:
|
|
|
|
if exit_ and not enter:
|
2022-04-04 15:10:02 +00:00
|
|
|
exit_signal = ExitType.EXIT_SIGNAL
|
2021-04-20 08:17:00 +00:00
|
|
|
else:
|
2022-05-07 12:53:51 +00:00
|
|
|
reason_cust = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
2024-05-12 14:41:08 +00:00
|
|
|
pair=trade.pair,
|
|
|
|
trade=trade,
|
|
|
|
current_time=current_time,
|
|
|
|
current_rate=current_rate,
|
|
|
|
current_profit=current_profit,
|
|
|
|
)
|
2022-05-07 12:53:51 +00:00
|
|
|
if reason_cust:
|
2022-04-04 15:04:43 +00:00
|
|
|
exit_signal = ExitType.CUSTOM_EXIT
|
2022-05-07 12:53:51 +00:00
|
|
|
if isinstance(reason_cust, str):
|
|
|
|
custom_reason = reason_cust
|
2023-04-11 05:26:38 +00:00
|
|
|
if len(reason_cust) > CUSTOM_TAG_MAX_LENGTH:
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.warning(
|
|
|
|
f"Custom exit reason returned from "
|
|
|
|
f"custom_exit is too long and was trimmed"
|
|
|
|
f"to {CUSTOM_TAG_MAX_LENGTH} characters."
|
|
|
|
)
|
2023-04-11 05:26:38 +00:00
|
|
|
custom_reason = reason_cust[:CUSTOM_TAG_MAX_LENGTH]
|
2021-04-21 06:37:16 +00:00
|
|
|
else:
|
2024-05-12 14:41:08 +00:00
|
|
|
custom_reason = ""
|
|
|
|
if exit_signal == ExitType.CUSTOM_EXIT or (
|
|
|
|
exit_signal == ExitType.EXIT_SIGNAL
|
|
|
|
and (not self.exit_profit_only or current_profit > self.exit_profit_offset)
|
2022-04-09 14:50:38 +00:00
|
|
|
):
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.debug(
|
|
|
|
f"{trade.pair} - Sell signal received. "
|
|
|
|
f"exit_type=ExitType.{exit_signal.name}"
|
|
|
|
+ (f", custom_reason={custom_reason}" if custom_reason else "")
|
|
|
|
)
|
2022-05-22 08:15:58 +00:00
|
|
|
exits.append(ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason))
|
2020-11-27 08:18:03 +00:00
|
|
|
|
|
|
|
# Sequence:
|
2021-08-08 09:38:34 +00:00
|
|
|
# Exit-signal
|
2020-11-27 08:18:03 +00:00
|
|
|
# Stoploss
|
2022-05-22 09:01:18 +00:00
|
|
|
# ROI
|
|
|
|
# Trailing stoploss
|
|
|
|
|
2022-07-28 18:35:23 +00:00
|
|
|
if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION):
|
2022-05-22 09:01:18 +00:00
|
|
|
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
|
|
|
exits.append(stoplossflag)
|
|
|
|
|
|
|
|
if roi_reached:
|
2022-04-03 09:18:36 +00:00
|
|
|
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
2022-05-22 08:15:58 +00:00
|
|
|
exits.append(ExitCheckTuple(exit_type=ExitType.ROI))
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2022-05-22 09:01:18 +00:00
|
|
|
if stoplossflag.exit_type == ExitType.TRAILING_STOP_LOSS:
|
|
|
|
logger.debug(f"{trade.pair} - Trailing stoploss hit.")
|
2022-05-22 08:15:58 +00:00
|
|
|
exits.append(stoplossflag)
|
2020-11-27 08:18:03 +00:00
|
|
|
|
2022-05-22 08:15:58 +00:00
|
|
|
return exits
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def ft_stoploss_adjust(
|
|
|
|
self,
|
|
|
|
current_rate: float,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_profit: float,
|
|
|
|
force_stoploss: float,
|
|
|
|
low: Optional[float] = None,
|
|
|
|
high: Optional[float] = None,
|
|
|
|
after_fill: bool = False,
|
|
|
|
) -> None:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2023-02-13 18:53:04 +00:00
|
|
|
Adjust stop-loss dynamically if configured to do so.
|
2020-02-28 09:36:39 +00:00
|
|
|
:param current_profit: current profit as ratio
|
2021-06-13 14:37:11 +00:00
|
|
|
:param low: Low value of this candle, only set in backtesting
|
|
|
|
:param high: High value of this candle, only set in backtesting
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2023-08-14 14:12:04 +00:00
|
|
|
if after_fill and not self._ft_stop_uses_after_fill:
|
|
|
|
# Skip if the strategy doesn't support after fill.
|
|
|
|
return
|
|
|
|
|
2019-03-23 15:23:32 +00:00
|
|
|
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-03-23 15:48:17 +00:00
|
|
|
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
2019-03-23 15:23:32 +00:00
|
|
|
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
dir_correct = (
|
|
|
|
trade.stop_loss < (low or current_rate)
|
|
|
|
if not trade.is_short
|
|
|
|
else trade.stop_loss > (high or current_rate)
|
|
|
|
)
|
2021-08-08 09:38:34 +00:00
|
|
|
|
2022-10-22 10:52:13 +00:00
|
|
|
# Make sure current_profit is calculated using high for backtesting.
|
2024-05-12 14:41:08 +00:00
|
|
|
bound = low if trade.is_short else high
|
2022-10-22 10:52:13 +00:00
|
|
|
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
|
2021-08-08 09:38:34 +00:00
|
|
|
if self.use_custom_stoploss and dir_correct:
|
2023-08-14 14:46:25 +00:00
|
|
|
stop_loss_value_custom = strategy_safe_wrapper(
|
|
|
|
self.custom_stoploss, default_retval=None, supress_error=True
|
2024-05-12 14:41:08 +00:00
|
|
|
)(
|
|
|
|
pair=trade.pair,
|
|
|
|
trade=trade,
|
|
|
|
current_time=current_time,
|
|
|
|
current_rate=(bound or current_rate),
|
|
|
|
current_profit=bound_profit,
|
|
|
|
after_fill=after_fill,
|
|
|
|
)
|
2020-12-12 06:06:16 +00:00
|
|
|
# Sanity check - error cases will return None
|
2023-08-14 14:46:25 +00:00
|
|
|
if stop_loss_value_custom:
|
|
|
|
stop_loss_value = stop_loss_value_custom
|
2024-05-12 14:41:08 +00:00
|
|
|
trade.adjust_stop_loss(
|
|
|
|
bound or current_rate, stop_loss_value, allow_refresh=after_fill
|
|
|
|
)
|
2020-12-12 06:06:16 +00:00
|
|
|
else:
|
2023-08-14 14:20:54 +00:00
|
|
|
logger.debug("CustomStoploss function did not return valid stoploss")
|
2020-12-12 06:06:16 +00:00
|
|
|
|
2022-10-22 09:45:21 +00:00
|
|
|
if self.trailing_stop and dir_correct:
|
2019-03-23 15:51:36 +00:00
|
|
|
# trailing stoploss handling
|
2019-10-11 06:55:31 +00:00
|
|
|
sl_offset = self.trailing_stop_positive_offset
|
2019-06-13 18:04:52 +00:00
|
|
|
# Make sure current_profit is calculated using high for backtesting.
|
|
|
|
|
2019-03-23 15:48:17 +00:00
|
|
|
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
2021-10-17 07:46:39 +00:00
|
|
|
if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
|
2019-10-13 07:54:03 +00:00
|
|
|
# Specific handling for trailing_stop_positive
|
2021-10-17 07:46:39 +00:00
|
|
|
if self.trailing_stop_positive is not None and bound_profit > sl_offset:
|
2019-10-11 07:05:21 +00:00
|
|
|
stop_loss_value = self.trailing_stop_positive
|
2024-05-12 14:41:08 +00:00
|
|
|
logger.debug(
|
|
|
|
f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
|
|
|
f"offset: {sl_offset:.4g} profit: {bound_profit:.2%}"
|
|
|
|
)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2021-10-09 20:39:11 +00:00
|
|
|
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def ft_stoploss_reached(
|
|
|
|
self,
|
|
|
|
current_rate: float,
|
|
|
|
trade: Trade,
|
|
|
|
current_time: datetime,
|
|
|
|
current_profit: float,
|
|
|
|
force_stoploss: float,
|
|
|
|
low: Optional[float] = None,
|
|
|
|
high: Optional[float] = None,
|
|
|
|
) -> ExitCheckTuple:
|
2023-02-13 18:53:04 +00:00
|
|
|
"""
|
|
|
|
Based on current profit of the trade and configured (trailing) stoploss,
|
|
|
|
decides to exit or not
|
|
|
|
:param current_profit: current profit as ratio
|
|
|
|
:param low: Low value of this candle, only set in backtesting
|
|
|
|
:param high: High value of this candle, only set in backtesting
|
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
self.ft_stoploss_adjust(
|
|
|
|
current_rate, trade, current_time, current_profit, force_stoploss, low, high
|
|
|
|
)
|
2023-02-13 18:53:04 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
sl_higher_long = trade.stop_loss >= (low or current_rate) and not trade.is_short
|
|
|
|
sl_lower_short = trade.stop_loss <= (high or current_rate) and trade.is_short
|
|
|
|
liq_higher_long = (
|
|
|
|
trade.liquidation_price
|
|
|
|
and trade.liquidation_price >= (low or current_rate)
|
|
|
|
and not trade.is_short
|
|
|
|
)
|
|
|
|
liq_lower_short = (
|
|
|
|
trade.liquidation_price
|
|
|
|
and trade.liquidation_price <= (high or current_rate)
|
|
|
|
and trade.is_short
|
|
|
|
)
|
2022-07-28 18:35:23 +00:00
|
|
|
|
2019-03-23 15:21:58 +00:00
|
|
|
# evaluate if the stoploss was hit if stoploss is not on exchange
|
2020-01-20 19:14:40 +00:00
|
|
|
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
|
|
|
# regular stoploss handling.
|
2024-05-12 14:41:08 +00:00
|
|
|
if (sl_higher_long or sl_lower_short) and (
|
|
|
|
not self.order_types.get("stoploss_on_exchange") or self.config["dry_run"]
|
|
|
|
):
|
2022-04-03 09:18:36 +00:00
|
|
|
exit_type = ExitType.STOP_LOSS
|
2019-06-02 11:27:31 +00:00
|
|
|
|
|
|
|
# If initial stoploss is not the same as current one then it is trailing.
|
2023-08-14 13:57:47 +00:00
|
|
|
if trade.is_stop_loss_trailing:
|
2022-04-03 09:18:36 +00:00
|
|
|
exit_type = ExitType.TRAILING_STOP_LOSS
|
2019-03-23 15:21:58 +00:00
|
|
|
logger.debug(
|
2021-10-09 20:39:11 +00:00
|
|
|
f"{trade.pair} - HIT STOP: current price at "
|
|
|
|
f"{((high if trade.is_short else low) or current_rate):.6f}, "
|
2019-09-10 07:42:45 +00:00
|
|
|
f"stoploss is {trade.stop_loss:.6f}, "
|
|
|
|
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
2024-05-12 14:41:08 +00:00
|
|
|
f"trade opened at {trade.open_rate:.6f}"
|
|
|
|
)
|
2019-03-23 15:21:58 +00:00
|
|
|
|
2022-04-03 09:18:36 +00:00
|
|
|
return ExitCheckTuple(exit_type=exit_type)
|
2019-03-23 15:21:58 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
if liq_higher_long or liq_lower_short:
|
2023-10-22 07:24:28 +00:00
|
|
|
logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION")
|
|
|
|
return ExitCheckTuple(exit_type=ExitType.LIQUIDATION)
|
|
|
|
|
2022-03-25 05:55:37 +00:00
|
|
|
return ExitCheckTuple(exit_type=ExitType.NONE)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-12-07 14:18:12 +00:00
|
|
|
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2019-06-23 16:23:51 +00:00
|
|
|
Based on trade duration defines the ROI entry that may have been reached.
|
|
|
|
:param trade_dur: trade duration in minutes
|
|
|
|
:return: minimal ROI entry value or None if none proper ROI entry was found.
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2019-06-20 00:26:25 +00:00
|
|
|
# Get highest entry in ROI dict where key <= trade-duration
|
2023-05-27 17:57:12 +00:00
|
|
|
roi_list = [x for x in self.minimal_roi.keys() if x <= trade_dur]
|
2019-06-20 00:26:25 +00:00
|
|
|
if not roi_list:
|
2019-12-07 14:18:12 +00:00
|
|
|
return None, None
|
2019-06-20 00:26:25 +00:00
|
|
|
roi_entry = max(roi_list)
|
2019-12-07 14:18:12 +00:00
|
|
|
return roi_entry, self.minimal_roi[roi_entry]
|
2019-06-23 16:23:51 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
|
|
|
"""
|
2020-02-28 09:36:39 +00:00
|
|
|
Based on trade duration, current profit of the trade and ROI configuration,
|
2022-04-05 04:50:44 +00:00
|
|
|
decides whether bot should exit.
|
2020-02-28 09:36:39 +00:00
|
|
|
:param current_profit: current profit as ratio
|
2022-04-05 04:50:44 +00:00
|
|
|
:return: True if bot should exit at current rate
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
|
|
|
# Check if time matches and current rate is above threshold
|
2021-02-07 09:20:43 +00:00
|
|
|
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
|
2019-12-07 14:18:12 +00:00
|
|
|
_, roi = self.min_roi_reached_entry(trade_dur)
|
2019-06-23 16:23:51 +00:00
|
|
|
if roi is None:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return current_profit > roi
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
def ft_check_timed_out(self, trade: Trade, order: Order, current_time: datetime) -> bool:
|
2022-01-22 14:59:10 +00:00
|
|
|
"""
|
|
|
|
FT Internal method.
|
|
|
|
Check if timeout is active, and if the order is still open and timed out
|
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
side = "entry" if order.ft_order_side == trade.entry_side else "exit"
|
2022-03-26 10:55:11 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
timeout = self.config.get("unfilledtimeout", {}).get(side)
|
2022-01-22 14:59:10 +00:00
|
|
|
if timeout is not None:
|
2024-05-12 14:41:08 +00:00
|
|
|
timeout_unit = self.config.get("unfilledtimeout", {}).get("unit", "minutes")
|
2022-01-22 14:59:10 +00:00
|
|
|
timeout_kwargs = {timeout_unit: -timeout}
|
|
|
|
timeout_threshold = current_time + timedelta(**timeout_kwargs)
|
2024-05-12 14:41:08 +00:00
|
|
|
timedout = order.status == "open" and order.order_date_utc < timeout_threshold
|
2022-01-22 14:59:10 +00:00
|
|
|
if timedout:
|
|
|
|
return True
|
2024-05-12 14:41:08 +00:00
|
|
|
time_method = (
|
|
|
|
self.check_exit_timeout
|
|
|
|
if order.ft_order_side == trade.exit_side
|
|
|
|
else self.check_entry_timeout
|
|
|
|
)
|
2022-01-22 14:59:10 +00:00
|
|
|
|
2024-05-12 14:41:08 +00:00
|
|
|
return strategy_safe_wrapper(time_method, default_retval=False)(
|
|
|
|
pair=trade.pair, trade=trade, order=order, current_time=current_time
|
|
|
|
)
|
2022-01-22 14:59:10 +00:00
|
|
|
|
2021-08-09 12:53:18 +00:00
|
|
|
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2020-06-13 05:09:44 +00:00
|
|
|
Populates indicators for given candle (OHLCV) data (for multiple pairs)
|
2021-09-22 18:42:31 +00:00
|
|
|
Does not run advise_entry or advise_exit!
|
2019-10-27 09:56:38 +00:00
|
|
|
Used by optimize operations only, not during dry / live runs.
|
2020-04-02 18:17:54 +00:00
|
|
|
Using .copy() to get a fresh copy of the dataframe for every strategy run.
|
2021-09-15 17:56:12 +00:00
|
|
|
Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show.
|
2020-04-02 18:17:54 +00:00
|
|
|
Has positive effects on memory usage for whatever reason - also when
|
|
|
|
using only one strategy.
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2024-05-12 14:41:08 +00:00
|
|
|
return {
|
|
|
|
pair: self.advise_indicators(pair_data.copy(), {"pair": pair}).copy()
|
|
|
|
for pair, pair_data in data.items()
|
|
|
|
}
|
2018-06-15 03:27:41 +00:00
|
|
|
|
2023-07-21 18:27:52 +00:00
|
|
|
def ft_advise_signals(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
|
|
"""
|
|
|
|
Call advise_entry and advise_exit and return the resulting dataframe.
|
|
|
|
:param dataframe: Dataframe containing data from exchange, as well as pre-calculated
|
|
|
|
indicators
|
|
|
|
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
|
|
|
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
dataframe = self.advise_entry(dataframe, metadata)
|
|
|
|
dataframe = self.advise_exit(dataframe, metadata)
|
|
|
|
return dataframe
|
|
|
|
|
2018-07-29 18:36:03 +00:00
|
|
|
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-06-15 03:27:41 +00:00
|
|
|
"""
|
2021-08-08 09:38:34 +00:00
|
|
|
Populate indicators that will be used in the Buy, Sell, short, exit_short strategy
|
2018-07-22 15:39:35 +00:00
|
|
|
This method should not be overridden.
|
2020-03-08 10:35:31 +00:00
|
|
|
:param dataframe: Dataframe with data from the exchange
|
2018-07-29 18:36:03 +00:00
|
|
|
:param metadata: Additional information, like the currently traded pair
|
2018-06-15 03:27:41 +00:00
|
|
|
:return: a Dataframe with all mandatory indicators for the strategies
|
|
|
|
"""
|
2019-06-11 07:42:14 +00:00
|
|
|
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
2021-09-19 23:44:12 +00:00
|
|
|
|
|
|
|
# call populate_indicators_Nm() which were tagged with @informative decorator.
|
|
|
|
for inf_data, populate_fn in self._ft_informative:
|
|
|
|
dataframe = _create_and_merge_informative_pair(
|
2024-05-12 14:41:08 +00:00
|
|
|
self, dataframe, metadata, inf_data, populate_fn
|
|
|
|
)
|
2021-09-19 23:44:12 +00:00
|
|
|
|
2022-04-25 05:01:27 +00:00
|
|
|
return self.populate_indicators(dataframe, metadata)
|
2018-06-15 03:27:41 +00:00
|
|
|
|
2021-09-22 18:42:31 +00:00
|
|
|
def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-06-15 03:27:41 +00:00
|
|
|
"""
|
2021-09-09 08:10:12 +00:00
|
|
|
Based on TA indicators, populates the entry order signal for the given dataframe
|
2018-07-22 15:39:35 +00:00
|
|
|
This method should not be overridden.
|
2018-06-15 03:27:41 +00:00
|
|
|
:param dataframe: DataFrame
|
2021-06-25 17:13:31 +00:00
|
|
|
:param metadata: Additional information dictionary, with details like the
|
|
|
|
currently traded pair
|
2018-06-15 03:27:41 +00:00
|
|
|
:return: DataFrame with buy column
|
|
|
|
"""
|
2021-01-04 19:49:24 +00:00
|
|
|
|
2021-08-18 10:19:17 +00:00
|
|
|
logger.debug(f"Populating enter signals for pair {metadata.get('pair')}.")
|
2024-01-14 13:33:47 +00:00
|
|
|
# Initialize column to work around Pandas bug #56503.
|
2024-05-12 14:41:08 +00:00
|
|
|
dataframe.loc[:, "enter_tag"] = ""
|
2022-04-25 05:01:27 +00:00
|
|
|
df = self.populate_entry_trend(dataframe, metadata)
|
2024-05-12 14:41:08 +00:00
|
|
|
if "enter_long" not in df.columns:
|
|
|
|
df = df.rename({"buy": "enter_long", "buy_tag": "enter_tag"}, axis="columns")
|
2021-08-23 19:12:46 +00:00
|
|
|
|
2021-09-22 18:48:05 +00:00
|
|
|
return df
|
2018-06-15 03:27:41 +00:00
|
|
|
|
2021-09-22 18:42:31 +00:00
|
|
|
def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-06-15 03:27:41 +00:00
|
|
|
"""
|
2021-09-09 08:10:12 +00:00
|
|
|
Based on TA indicators, populates the exit order signal for the given dataframe
|
2018-07-22 15:39:35 +00:00
|
|
|
This method should not be overridden.
|
2018-06-15 03:27:41 +00:00
|
|
|
:param dataframe: DataFrame
|
2021-06-25 17:13:31 +00:00
|
|
|
:param metadata: Additional information dictionary, with details like the
|
|
|
|
currently traded pair
|
2022-04-05 04:50:44 +00:00
|
|
|
:return: DataFrame with exit column
|
2018-06-15 03:27:41 +00:00
|
|
|
"""
|
2024-01-14 13:33:47 +00:00
|
|
|
# Initialize column to work around Pandas bug #56503.
|
2024-05-12 14:41:08 +00:00
|
|
|
dataframe.loc[:, "exit_tag"] = ""
|
2021-08-18 10:19:17 +00:00
|
|
|
logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.")
|
2022-04-25 05:01:27 +00:00
|
|
|
df = self.populate_exit_trend(dataframe, metadata)
|
2024-05-12 14:41:08 +00:00
|
|
|
if "exit_long" not in df.columns:
|
|
|
|
df = df.rename({"sell": "exit_long"}, axis="columns")
|
2021-09-22 18:48:05 +00:00
|
|
|
return df
|