2018-01-28 05:26:57 +00:00
|
|
|
"""
|
|
|
|
IStrategy interface
|
|
|
|
This module defines the interface to apply for strategies
|
|
|
|
"""
|
2018-07-16 05:11:17 +00:00
|
|
|
import logging
|
2020-02-06 19:26:04 +00:00
|
|
|
import warnings
|
2018-07-22 15:39:35 +00:00
|
|
|
from abc import ABC, abstractmethod
|
2019-08-12 17:50:22 +00:00
|
|
|
from datetime import datetime, timezone
|
2018-07-16 05:11:17 +00:00
|
|
|
from enum import Enum
|
2020-05-16 08:28:36 +00:00
|
|
|
from typing import Dict, NamedTuple, Optional, Tuple
|
2018-03-17 21:44:47 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
import arrow
|
2018-01-15 08:35:11 +00:00
|
|
|
from pandas import DataFrame
|
|
|
|
|
2018-12-26 13:32:17 +00:00
|
|
|
from freqtrade.data.dataprovider import DataProvider
|
2020-02-06 19:26:04 +00:00
|
|
|
from freqtrade.exceptions import StrategyError
|
2019-04-09 09:27:35 +00:00
|
|
|
from freqtrade.exchange import timeframe_to_minutes
|
2018-07-16 05:11:17 +00:00
|
|
|
from freqtrade.persistence import Trade
|
2020-02-06 19:26:04 +00:00
|
|
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
2020-05-18 18:59:50 +00:00
|
|
|
from freqtrade.typing import ListPairsWithTimeframes
|
2018-12-26 13:32:17 +00:00
|
|
|
from freqtrade.wallets import Wallets
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2020-05-16 08:28:36 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class SignalType(Enum):
|
|
|
|
"""
|
|
|
|
Enum to distinguish between buy and sell signals
|
|
|
|
"""
|
|
|
|
BUY = "buy"
|
|
|
|
SELL = "sell"
|
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2018-07-11 17:22:34 +00:00
|
|
|
class SellType(Enum):
|
|
|
|
"""
|
|
|
|
Enum to distinguish between sell reasons
|
|
|
|
"""
|
|
|
|
ROI = "roi"
|
|
|
|
STOP_LOSS = "stop_loss"
|
2018-11-26 17:28:13 +00:00
|
|
|
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
2018-07-11 17:22:34 +00:00
|
|
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
|
|
|
SELL_SIGNAL = "sell_signal"
|
2018-07-11 17:57:01 +00:00
|
|
|
FORCE_SELL = "force_sell"
|
2019-09-01 07:07:09 +00:00
|
|
|
EMERGENCY_SELL = "emergency_sell"
|
2018-07-11 17:59:30 +00:00
|
|
|
NONE = ""
|
2018-07-11 17:22:34 +00:00
|
|
|
|
|
|
|
|
2018-07-12 20:21:52 +00:00
|
|
|
class SellCheckTuple(NamedTuple):
|
|
|
|
"""
|
|
|
|
NamedTuple for Sell type + reason
|
|
|
|
"""
|
|
|
|
sell_flag: bool
|
|
|
|
sell_type: SellType
|
|
|
|
|
|
|
|
|
2018-01-15 08:35:11 +00:00
|
|
|
class IStrategy(ABC):
|
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
|
2020-03-08 10:35:31 +00:00
|
|
|
ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy
|
2018-01-28 05:26:57 +00:00
|
|
|
"""
|
2019-08-26 17:44:33 +00:00
|
|
|
# Strategy interface version
|
|
|
|
# Default to version 2
|
2019-08-26 18:16:03 +00:00
|
|
|
# Version 1 is the initial interface without metadata dict
|
2019-08-26 17:44:33 +00:00
|
|
|
# Version 2 populate_* include metadata dict
|
2019-08-26 18:16:03 +00:00
|
|
|
INTERFACE_VERSION: int = 2
|
2018-01-15 08:35:11 +00:00
|
|
|
|
2018-07-23 16:38:21 +00:00
|
|
|
_populate_fun_len: int = 0
|
|
|
|
_buy_fun_len: int = 0
|
|
|
|
_sell_fun_len: int = 0
|
2018-06-15 03:27:41 +00:00
|
|
|
# associated minimal roi
|
2018-05-31 19:59:22 +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
|
|
|
|
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
|
2019-01-05 06:10:25 +00:00
|
|
|
|
2018-06-15 03:27:41 +00:00
|
|
|
# associated ticker interval
|
2018-05-31 19:59:22 +00:00
|
|
|
ticker_interval: str
|
|
|
|
|
2018-11-15 05:58:24 +00:00
|
|
|
# Optional order types
|
|
|
|
order_types: Dict = {
|
|
|
|
'buy': 'limit',
|
|
|
|
'sell': 'limit',
|
2018-11-25 16:22:56 +00:00
|
|
|
'stoploss': 'limit',
|
2019-01-08 11:39:53 +00:00
|
|
|
'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 = {
|
|
|
|
'buy': 'gtc',
|
|
|
|
'sell': 'gtc',
|
|
|
|
}
|
|
|
|
|
2018-08-09 17:24:00 +00:00
|
|
|
# run "populate_indicators" only for new candle
|
2018-09-01 17:52:40 +00:00
|
|
|
process_only_new_candles: bool = False
|
2018-08-09 17:24:00 +00:00
|
|
|
|
2019-10-20 09:17:01 +00:00
|
|
|
# Count of candles the strategy requires before producing valid signals
|
|
|
|
startup_candle_count: int = 0
|
|
|
|
|
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.
|
2019-11-21 01:59:38 +00:00
|
|
|
dp: Optional[DataProvider] = None
|
|
|
|
wallets: Optional[Wallets] = None
|
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
|
|
|
|
2018-07-17 07:47:15 +00:00
|
|
|
def __init__(self, config: dict) -> 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] = {}
|
2019-08-12 14:29:09 +00:00
|
|
|
self._pair_locked_until: Dict[str, datetime] = {}
|
2018-07-16 05:11:17 +00:00
|
|
|
|
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
|
|
|
"""
|
|
|
|
Populate indicators that will be used in the Buy and Sell 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
|
|
|
|
"""
|
|
|
|
|
2018-07-22 15:39:35 +00:00
|
|
|
@abstractmethod
|
2018-07-29 18:36:03 +00:00
|
|
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-01-15 08:35:11 +00:00
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the buy 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-01-15 08:35:11 +00:00
|
|
|
:return: DataFrame with buy column
|
|
|
|
"""
|
|
|
|
|
2018-07-22 15:39:35 +00:00
|
|
|
@abstractmethod
|
2018-07-29 18:36:03 +00:00
|
|
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
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
|
|
|
"""
|
2018-07-12 18:38:14 +00:00
|
|
|
|
2020-03-01 08:43:20 +00:00
|
|
|
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
2020-02-06 19:30:17 +00:00
|
|
|
"""
|
|
|
|
Check buy timeout function callback.
|
|
|
|
This method can be used to override the buy-timeout.
|
|
|
|
It is called whenever a limit buy order has been created,
|
|
|
|
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
|
|
|
|
:param trade: trade object.
|
|
|
|
:param order: Order dictionary as returned from CCXT.
|
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
|
|
:return bool: When True is returned, then the buy-order is cancelled.
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
2020-03-01 08:43:20 +00:00
|
|
|
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
2020-02-21 19:27:13 +00:00
|
|
|
"""
|
|
|
|
Check sell timeout function callback.
|
|
|
|
This method can be used to override the sell-timeout.
|
|
|
|
It is called whenever a limit sell order has been created,
|
|
|
|
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
|
|
|
|
:param trade: trade object.
|
|
|
|
:param order: Order dictionary as returned from CCXT.
|
|
|
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
|
|
|
:return bool: When True is returned, then the sell-order is cancelled.
|
|
|
|
"""
|
|
|
|
return False
|
|
|
|
|
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.
|
2019-01-21 19:22:27 +00:00
|
|
|
These pair/interval combinations are non-tradeable, unless they are part
|
|
|
|
of the whitelist as well.
|
|
|
|
For more information, please consult the documentation
|
|
|
|
:return: List of tuples in the format (pair, interval)
|
|
|
|
Sample: return [("ETH/USDT", "5m"),
|
|
|
|
("BTC/USDT", "15m"),
|
|
|
|
]
|
|
|
|
"""
|
|
|
|
return []
|
|
|
|
|
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
|
|
|
|
2019-08-12 14:29:09 +00:00
|
|
|
def lock_pair(self, pair: str, until: datetime) -> None:
|
|
|
|
"""
|
|
|
|
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)`
|
2019-08-12 14:29:09 +00:00
|
|
|
"""
|
2019-12-22 08:46:00 +00:00
|
|
|
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
|
|
|
self._pair_locked_until[pair] = until
|
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
if pair in self._pair_locked_until:
|
|
|
|
del self._pair_locked_until[pair]
|
2019-08-12 17:50:22 +00:00
|
|
|
|
|
|
|
def is_pair_locked(self, pair: str) -> bool:
|
|
|
|
"""
|
|
|
|
Checks if a pair is currently locked
|
|
|
|
"""
|
|
|
|
if pair not in self._pair_locked_until:
|
|
|
|
return False
|
|
|
|
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
|
2019-08-12 14:29:09 +00:00
|
|
|
|
2018-12-11 18:47:48 +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
|
2018-07-16 05:11:17 +00:00
|
|
|
add several TA indicators and buy signal to it
|
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")
|
|
|
|
dataframe = self.advise_indicators(dataframe, metadata)
|
|
|
|
dataframe = self.advise_buy(dataframe, metadata)
|
|
|
|
dataframe = self.advise_sell(dataframe, metadata)
|
|
|
|
return dataframe
|
|
|
|
|
2019-08-04 10:55:03 +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
|
|
|
"""
|
2018-08-09 11:02:41 +00:00
|
|
|
pair = str(metadata.get('pair'))
|
2018-08-03 07:33:34 +00:00
|
|
|
|
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
|
2018-09-01 17:52:40 +00:00
|
|
|
if (not self.process_only_new_candles or
|
2018-09-01 17:50:45 +00:00
|
|
|
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
2018-08-03 07:33:34 +00:00
|
|
|
# Defs that only make change on new candle data.
|
2019-08-04 08:20:31 +00:00
|
|
|
dataframe = self.analyze_ticker(dataframe, metadata)
|
2018-09-01 17:50:45 +00:00
|
|
|
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
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")
|
2018-08-09 17:53:47 +00:00
|
|
|
dataframe['buy'] = 0
|
|
|
|
dataframe['sell'] = 0
|
2018-08-03 07:33:34 +00:00
|
|
|
|
|
|
|
# Other Defs in strategy that want to be called every loop here
|
|
|
|
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
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
|
|
|
|
|
2020-03-24 12:54:46 +00:00
|
|
|
@staticmethod
|
2020-03-26 06:05:30 +00:00
|
|
|
def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
|
2020-03-24 12:54:46 +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
|
|
|
|
|
|
|
@staticmethod
|
2020-03-26 06:05:30 +00:00
|
|
|
def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
|
2020-03-24 12:54:46 +00:00
|
|
|
""" make sure data is unmodified """
|
2020-03-26 06:05:30 +00:00
|
|
|
message = ""
|
|
|
|
if df_len != len(dataframe):
|
|
|
|
message = "length"
|
|
|
|
elif df_close != dataframe["close"].iloc[-1]:
|
|
|
|
message = "last close price"
|
|
|
|
elif df_date != dataframe["date"].iloc[-1]:
|
|
|
|
message = "last date"
|
|
|
|
if message:
|
2020-04-19 04:58:44 +00:00
|
|
|
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
|
2020-03-24 12:54:46 +00:00
|
|
|
|
|
|
|
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
|
|
|
Calculates current signal based several technical analysis indicators
|
|
|
|
:param pair: pair in format ANT/BTC
|
|
|
|
:param interval: Interval to use (in min)
|
2018-12-11 18:47:48 +00:00
|
|
|
:param dataframe: Dataframe to analyze
|
2018-07-16 05:11:17 +00:00
|
|
|
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
|
|
|
"""
|
2018-12-12 18:35:51 +00:00
|
|
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
2020-03-08 10:35:31 +00:00
|
|
|
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
|
2018-07-16 05:11:17 +00:00
|
|
|
return False, False
|
|
|
|
|
2020-03-24 12:54:46 +00:00
|
|
|
latest_date = dataframe['date'].max()
|
2018-07-16 05:11:17 +00:00
|
|
|
try:
|
2020-03-24 12:54:46 +00:00
|
|
|
df_len, df_close, df_date = self.preserve_df(dataframe)
|
2020-02-06 19:26:04 +00:00
|
|
|
dataframe = strategy_safe_wrapper(
|
|
|
|
self._analyze_ticker_internal, message=""
|
|
|
|
)(dataframe, {'pair': pair})
|
2020-03-24 12:54:46 +00:00
|
|
|
self.assert_df(dataframe, df_len, df_close, df_date)
|
2020-02-06 19:26:04 +00:00
|
|
|
except StrategyError as error:
|
2020-03-15 13:56:14 +00:00
|
|
|
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
|
2020-02-06 19:26:04 +00:00
|
|
|
|
2018-07-16 05:11:17 +00:00
|
|
|
return False, False
|
|
|
|
|
|
|
|
if dataframe.empty:
|
|
|
|
logger.warning('Empty dataframe for pair %s', pair)
|
|
|
|
return False, False
|
|
|
|
|
2020-03-24 12:54:46 +00:00
|
|
|
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
|
2018-07-16 05:11:17 +00:00
|
|
|
|
|
|
|
# Check if dataframe is out of date
|
|
|
|
signal_date = arrow.get(latest['date'])
|
2019-04-04 17:56:40 +00:00
|
|
|
interval_minutes = timeframe_to_minutes(interval)
|
2018-08-20 18:01:57 +00:00
|
|
|
offset = self.config.get('exchange', {}).get('outdated_offset', 5)
|
2018-08-07 07:25:21 +00:00
|
|
|
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
|
2018-07-16 05:11:17 +00:00
|
|
|
logger.warning(
|
|
|
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
|
|
|
pair,
|
|
|
|
(arrow.utcnow() - signal_date).seconds // 60
|
|
|
|
)
|
|
|
|
return False, False
|
|
|
|
|
|
|
|
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
|
|
|
logger.debug(
|
|
|
|
'trigger: %s (pair=%s) buy=%s sell=%s',
|
|
|
|
latest['date'],
|
|
|
|
pair,
|
|
|
|
str(buy),
|
|
|
|
str(sell)
|
|
|
|
)
|
|
|
|
return buy, sell
|
|
|
|
|
2018-07-11 17:57:01 +00:00
|
|
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
2018-11-07 17:15:04 +00:00
|
|
|
sell: bool, low: float = None, high: float = None,
|
|
|
|
force_stoploss: float = 0) -> SellCheckTuple:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2019-08-14 04:07:03 +00:00
|
|
|
This function evaluates if one of the conditions required to trigger a sell
|
2019-08-12 17:50:22 +00:00
|
|
|
has been reached, which can either be a stop-loss, ROI or sell-signal.
|
2019-03-17 15:02:13 +00:00
|
|
|
:param low: Only used during backtesting to simulate stoploss
|
|
|
|
:param high: Only used during backtesting, to simulate ROI
|
|
|
|
:param force_stoploss: Externally provided stoploss
|
2018-07-16 05:11:17 +00:00
|
|
|
:return: True if trade should be sold, False otherwise
|
|
|
|
"""
|
2018-08-16 09:31:41 +00:00
|
|
|
# Set current rate to low for backtesting sell
|
2018-10-30 19:23:31 +00:00
|
|
|
current_rate = low or rate
|
2019-12-17 07:53:30 +00:00
|
|
|
current_profit = trade.calc_profit_ratio(current_rate)
|
2018-11-19 19:02:26 +00:00
|
|
|
|
2019-03-17 15:03:44 +00:00
|
|
|
trade.adjust_min_max_rates(high or current_rate)
|
2019-03-16 18:54:34 +00:00
|
|
|
|
2019-01-16 13:49:47 +00:00
|
|
|
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
|
|
|
current_time=date, current_profit=current_profit,
|
2019-03-17 15:03:44 +00:00
|
|
|
force_stoploss=force_stoploss, high=high)
|
2018-11-22 15:24:40 +00:00
|
|
|
|
2018-07-12 20:21:52 +00:00
|
|
|
if stoplossflag.sell_flag:
|
2019-09-11 22:21:14 +00:00
|
|
|
logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
|
|
|
|
f"sell_type={stoplossflag.sell_type}")
|
2018-07-12 20:21:52 +00:00
|
|
|
return stoplossflag
|
2018-11-22 15:24:40 +00:00
|
|
|
|
2019-03-17 15:02:13 +00:00
|
|
|
# Set current rate to high for backtesting sell
|
2018-10-30 19:23:31 +00:00
|
|
|
current_rate = high or rate
|
2019-12-17 07:53:30 +00:00
|
|
|
current_profit = trade.calc_profit_ratio(current_rate)
|
2019-10-05 10:29:59 +00:00
|
|
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-10-05 10:29:59 +00:00
|
|
|
if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False):
|
2019-09-12 17:58:10 +00:00
|
|
|
# This one is noisy, commented out
|
|
|
|
# logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False")
|
2018-07-12 20:21:52 +00:00
|
|
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
|
|
|
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
|
|
|
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
2019-09-11 22:21:14 +00:00
|
|
|
logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
|
|
|
|
f"sell_type=SellType.ROI")
|
2018-07-12 20:21:52 +00:00
|
|
|
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-10-05 10:29:59 +00:00
|
|
|
if config_ask_strategy.get('sell_profit_only', False):
|
2019-09-12 17:58:10 +00:00
|
|
|
# This one is noisy, commented out
|
|
|
|
# logger.debug(f"{trade.pair} - Checking if trade is profitable...")
|
2018-07-16 05:11:17 +00:00
|
|
|
if trade.calc_profit(rate=rate) <= 0:
|
2019-09-12 17:58:10 +00:00
|
|
|
# This one is noisy, commented out
|
|
|
|
# logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False")
|
2018-07-12 20:21:52 +00:00
|
|
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
2019-09-10 07:42:45 +00:00
|
|
|
|
2019-10-05 10:29:59 +00:00
|
|
|
if sell and not buy and config_ask_strategy.get('use_sell_signal', True):
|
2019-09-11 22:21:14 +00:00
|
|
|
logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
|
|
|
|
f"sell_type=SellType.SELL_SIGNAL")
|
2018-07-12 20:21:52 +00:00
|
|
|
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-09-10 07:42:45 +00:00
|
|
|
# This one is noisy, commented out...
|
2019-09-12 17:58:10 +00:00
|
|
|
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
|
2018-07-12 20:21:52 +00:00
|
|
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-03-23 15:51:36 +00:00
|
|
|
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
|
|
|
current_time: datetime, current_profit: float,
|
|
|
|
force_stoploss: float, high: float = None) -> SellCheckTuple:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
|
|
|
Based on current profit of the trade and configured (trailing) stoploss,
|
|
|
|
decides to sell or not
|
2020-02-28 09:36:39 +00:00
|
|
|
:param current_profit: current profit as ratio
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
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
|
|
|
|
2019-10-11 06:55:31 +00:00
|
|
|
if self.trailing_stop:
|
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
|
2018-07-16 19:23:35 +00:00
|
|
|
|
2019-06-13 18:04:52 +00:00
|
|
|
# Make sure current_profit is calculated using high for backtesting.
|
2019-12-17 07:53:30 +00:00
|
|
|
high_profit = current_profit if not high else trade.calc_profit_ratio(high)
|
2019-06-13 18:04:52 +00:00
|
|
|
|
2019-03-23 15:48:17 +00:00
|
|
|
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
2019-10-11 06:55:31 +00:00
|
|
|
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
|
2019-10-13 07:54:03 +00:00
|
|
|
# Specific handling for trailing_stop_positive
|
2019-10-11 19:59:13 +00:00
|
|
|
if self.trailing_stop_positive is not None and high_profit > sl_offset:
|
2019-10-11 07:05:21 +00:00
|
|
|
stop_loss_value = self.trailing_stop_positive
|
2019-09-11 20:32:08 +00:00
|
|
|
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
2019-03-23 15:48:17 +00:00
|
|
|
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
|
2018-07-16 05:11:17 +00:00
|
|
|
|
2019-03-17 15:03:44 +00:00
|
|
|
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
2018-07-16 05:11:17 +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.
|
2019-03-23 15:21:58 +00:00
|
|
|
if ((self.stoploss is not None) and
|
|
|
|
(trade.stop_loss >= current_rate) and
|
2020-01-20 19:14:40 +00:00
|
|
|
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
2019-03-23 15:21:58 +00:00
|
|
|
|
2019-09-10 07:42:45 +00:00
|
|
|
sell_type = SellType.STOP_LOSS
|
2019-06-02 11:27:31 +00:00
|
|
|
|
|
|
|
# If initial stoploss is not the same as current one then it is trailing.
|
|
|
|
if trade.initial_stop_loss != trade.stop_loss:
|
2019-09-10 07:42:45 +00:00
|
|
|
sell_type = SellType.TRAILING_STOP_LOSS
|
2019-03-23 15:21:58 +00:00
|
|
|
logger.debug(
|
2019-09-11 20:32:08 +00:00
|
|
|
f"{trade.pair} - HIT STOP: current price at {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}, "
|
2019-03-23 15:21:58 +00:00
|
|
|
f"trade opened at {trade.open_rate:.6f}")
|
2019-09-11 20:32:08 +00:00
|
|
|
logger.debug(f"{trade.pair} - Trailing stop saved "
|
2019-09-10 07:42:45 +00:00
|
|
|
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
2019-03-23 15:21:58 +00:00
|
|
|
|
2019-09-10 07:42:45 +00:00
|
|
|
return SellCheckTuple(sell_flag=True, sell_type=sell_type)
|
2019-03-23 15:21:58 +00:00
|
|
|
|
2018-07-12 20:21:52 +00:00
|
|
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.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
|
|
|
|
roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys()))
|
|
|
|
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,
|
|
|
|
decides whether bot should sell.
|
|
|
|
:param current_profit: current profit as ratio
|
2019-06-23 20:10:37 +00:00
|
|
|
:return: True if bot should sell at current rate
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
|
|
|
# Check if time matches and current rate is above threshold
|
2019-06-23 16:23:51 +00:00
|
|
|
trade_dur = int((current_time.timestamp() - trade.open_date.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
|
|
|
|
2020-03-08 10:35:31 +00:00
|
|
|
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2020-03-08 10:35:31 +00:00
|
|
|
Creates a dataframe and populates indicators for given candle (OHLCV) data
|
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.
|
|
|
|
Has positive effects on memory usage for whatever reason - also when
|
|
|
|
using only one strategy.
|
2018-07-16 05:11:17 +00:00
|
|
|
"""
|
2019-11-04 19:19:43 +00:00
|
|
|
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair})
|
2020-03-08 10:35:31 +00:00
|
|
|
for pair, pair_data in data.items()}
|
2018-06-15 03:27:41 +00:00
|
|
|
|
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
|
|
|
"""
|
|
|
|
Populate indicators that will be used in the Buy and Sell 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')}.")
|
2018-07-23 16:38:21 +00:00
|
|
|
if self._populate_fun_len == 2:
|
2018-07-22 15:39:35 +00:00
|
|
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
|
|
|
"the current function headers!", DeprecationWarning)
|
|
|
|
return self.populate_indicators(dataframe) # type: ignore
|
|
|
|
else:
|
2018-07-29 18:36:03 +00:00
|
|
|
return self.populate_indicators(dataframe, metadata)
|
2018-06-15 03:27:41 +00:00
|
|
|
|
2018-07-29 18:36:03 +00:00
|
|
|
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-06-15 03:27:41 +00:00
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the buy 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
|
2018-07-29 18:36:03 +00:00
|
|
|
:param pair: Additional information, like the currently traded pair
|
2018-06-15 03:27:41 +00:00
|
|
|
:return: DataFrame with buy column
|
|
|
|
"""
|
2019-06-11 07:42:14 +00:00
|
|
|
logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.")
|
2018-07-23 16:38:21 +00:00
|
|
|
if self._buy_fun_len == 2:
|
2018-07-22 15:39:35 +00:00
|
|
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
|
|
|
"the current function headers!", DeprecationWarning)
|
|
|
|
return self.populate_buy_trend(dataframe) # type: ignore
|
|
|
|
else:
|
2018-07-29 18:36:03 +00:00
|
|
|
return self.populate_buy_trend(dataframe, metadata)
|
2018-06-15 03:27:41 +00:00
|
|
|
|
2018-07-29 18:36:03 +00:00
|
|
|
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
2018-06-15 03:27:41 +00:00
|
|
|
"""
|
|
|
|
Based on TA indicators, populates the sell 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
|
2018-07-29 18:36:03 +00:00
|
|
|
:param pair: Additional information, like the currently traded pair
|
2018-06-15 03:27:41 +00:00
|
|
|
:return: DataFrame with sell column
|
|
|
|
"""
|
2019-06-11 07:42:14 +00:00
|
|
|
logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.")
|
2018-07-23 16:38:21 +00:00
|
|
|
if self._sell_fun_len == 2:
|
2018-07-22 15:39:35 +00:00
|
|
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
|
|
|
"the current function headers!", DeprecationWarning)
|
|
|
|
return self.populate_sell_trend(dataframe) # type: ignore
|
|
|
|
else:
|
2018-07-29 18:36:03 +00:00
|
|
|
return self.populate_sell_trend(dataframe, metadata)
|