Merge branch 'develop' into verify_date_on_new_candle_on_get_signal
This commit is contained in:
@@ -3,18 +3,21 @@ IStrategy interface
|
||||
This module defines the interface to apply for strategies
|
||||
"""
|
||||
import logging
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||
import warnings
|
||||
from typing import Dict, NamedTuple, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.typing import ListPairsWithTimeframes
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
|
||||
@@ -59,7 +62,7 @@ class IStrategy(ABC):
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||
ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy
|
||||
"""
|
||||
# Strategy interface version
|
||||
# Default to version 2
|
||||
@@ -125,7 +128,7 @@ class IStrategy(ABC):
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param dataframe: DataFrame with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
@@ -148,7 +151,43 @@ class IStrategy(ABC):
|
||||
:return: DataFrame with sell column
|
||||
"""
|
||||
|
||||
def informative_pairs(self) -> List[Tuple[str, str]]:
|
||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||
"""
|
||||
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
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||
"""
|
||||
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
|
||||
|
||||
def informative_pairs(self) -> ListPairsWithTimeframes:
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
These pair/interval combinations are non-tradeable, unless they are part
|
||||
@@ -200,11 +239,11 @@ class IStrategy(ABC):
|
||||
|
||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
:param dataframe: Dataframe containing ticker data
|
||||
:param dataframe: Dataframe containing data from exchange
|
||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||
:return: DataFrame with ticker data and indicator data
|
||||
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
||||
"""
|
||||
logger.debug("TA Analysis Launched")
|
||||
dataframe = self.advise_indicators(dataframe, metadata)
|
||||
@@ -214,12 +253,12 @@ class IStrategy(ABC):
|
||||
|
||||
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
Parses the given candle (OHLCV) data and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
|
||||
:param dataframe: Dataframe containing ticker data
|
||||
:param dataframe: Dataframe containing data from exchange
|
||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||
:return: DataFrame with ticker data and indicator data
|
||||
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
|
||||
"""
|
||||
pair = str(metadata.get('pair'))
|
||||
|
||||
@@ -241,8 +280,25 @@ class IStrategy(ABC):
|
||||
|
||||
return dataframe
|
||||
|
||||
def get_signal(self, pair: str, interval: str,
|
||||
dataframe: DataFrame) -> Tuple[bool, bool]:
|
||||
@staticmethod
|
||||
def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
|
||||
""" keep some data for dataframes """
|
||||
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
|
||||
|
||||
@staticmethod
|
||||
def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
|
||||
""" make sure data is unmodified """
|
||||
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:
|
||||
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
|
||||
|
||||
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
|
||||
"""
|
||||
Calculates current signal based several technical analysis indicators
|
||||
:param pair: pair in format ANT/BTC
|
||||
@@ -251,31 +307,27 @@ class IStrategy(ABC):
|
||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||
"""
|
||||
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||
logger.warning('Empty ticker history for pair %s', pair)
|
||||
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
latest_date = dataframe['date'].max()
|
||||
try:
|
||||
dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
'Unable to analyze ticker for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
return False, False
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
df_len, df_close, df_date = self.preserve_df(dataframe)
|
||||
dataframe = strategy_safe_wrapper(
|
||||
self._analyze_ticker_internal, message=""
|
||||
)(dataframe, {'pair': pair})
|
||||
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 False, False
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
latest = dataframe.iloc[-1]
|
||||
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
|
||||
|
||||
signal_date = arrow.get(latest['date'])
|
||||
interval_minutes = timeframe_to_minutes(interval)
|
||||
|
||||
@@ -446,19 +498,22 @@ class IStrategy(ABC):
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def tickerdata_to_dataframe(self, tickerdata: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Creates a dataframe and populates indicators for given ticker data
|
||||
Creates a dataframe and populates indicators for given candle (OHLCV) data
|
||||
Used by optimize operations only, not during dry / live runs.
|
||||
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.
|
||||
"""
|
||||
return {pair: self.advise_indicators(pair_data, {'pair': pair})
|
||||
for pair, pair_data in tickerdata.items()}
|
||||
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair})
|
||||
for pair, pair_data in data.items()}
|
||||
|
||||
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
This method should not be overridden.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param dataframe: Dataframe with data from the exchange
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
35
freqtrade/strategy/strategy_wrapper.py
Normal file
35
freqtrade/strategy/strategy_wrapper.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
|
||||
from freqtrade.exceptions import StrategyError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def strategy_safe_wrapper(f, message: str = "", default_retval=None):
|
||||
"""
|
||||
Wrapper around user-provided methods and functions.
|
||||
Caches all exceptions and returns either the default_retval (if it's not None) or raises
|
||||
a StrategyError exception, which then needs to be handled by the calling method.
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
f"{message}"
|
||||
f"Strategy caused the following exception: {error}"
|
||||
f"{f}"
|
||||
)
|
||||
if default_retval is None:
|
||||
raise StrategyError(str(error)) from error
|
||||
return default_retval
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
f"{message}"
|
||||
f"Unexpected error {error} calling {f}"
|
||||
)
|
||||
if default_retval is None:
|
||||
raise StrategyError(str(error)) from error
|
||||
return default_retval
|
||||
|
||||
return wrapper
|
Reference in New Issue
Block a user