Skip to content

Commit

Permalink
[SQUASH] Address PR comments.
Browse files Browse the repository at this point in the history
  • Loading branch information
rokups committed Sep 18, 2021
1 parent bb6ae68 commit e88c470
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 149 deletions.
19 changes: 8 additions & 11 deletions docs/strategy-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -652,9 +652,7 @@ In some situations it may be confusing to deal with stops relative to current ra

??? Example "Returning a stoploss using absolute price from the custom stoploss function"

Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).

If we want a stop price at $107 price we can call `stoploss_from_absolute(107, current_rate)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`.

``` python

Expand All @@ -664,18 +662,17 @@ In some situations it may be confusing to deal with stops relative to current ra

class AwesomeStrategy(IStrategy):

# ... populate_* methods

use_custom_stoploss = True

def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
return dataframe

def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:

# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_absolute(trade.open_rate * 1.07, current_rate)

return 1
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
candle = dataframe.iloc[-1].squeeze()
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)

```

Expand Down
5 changes: 3 additions & 2 deletions freqtrade/strategy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
IntParameter, RealParameter)
from freqtrade.strategy.informative_decorator import informative
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_helper import (informative, merge_informative_pair,
stoploss_from_absolute, stoploss_from_open)
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
stoploss_from_open)
134 changes: 134 additions & 0 deletions freqtrade/strategy/informative_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from typing import Any, Callable, NamedTuple, Optional, Union

from mypy_extensions import KwArg
from pandas import DataFrame

from freqtrade.exceptions import OperationalException
from freqtrade.strategy.strategy_helper import merge_informative_pair


PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]


class InformativeData(NamedTuple):
asset: Optional[str]
timeframe: str
fmt: Union[str, Callable[[KwArg(str)], str], None]
ffill: bool


def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
"""
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
define informative indicators.
Example usage:
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
current pair.
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
specified, defaults to:
* {base}_{column}_{timeframe} if asset is specified and quote currency does match stake
currency.
* {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match
stake currency.
* {column}_{timeframe} if asset is not specified.
Format string supports these format variables:
* {asset} - full name of the asset, for example 'BTC/USDT'.
* {base} - base currency in lower case, for example 'eth'.
* {BASE} - same as {base}, except in upper case.
* {quote} - quote currency in lower case, for example 'usdt'.
* {QUOTE} - same as {quote}, except in upper case.
* {column} - name of dataframe column.
* {timeframe} - timeframe of informative dataframe.
:param ffill: ffill dataframe after merging informative pair.
"""
_asset = asset
_timeframe = timeframe
_fmt = fmt
_ffill = ffill

def decorator(fn: PopulateIndicators):
informative_pairs = getattr(fn, '_ft_informative', [])
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
setattr(fn, '_ft_informative', informative_pairs)
return fn
return decorator


def _format_pair_name(config, pair: str) -> str:
return pair.format(stake_currency=config['stake_currency'],
stake=config['stake_currency']).upper()


def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
inf_data: InformativeData,
populate_indicators: PopulateIndicators):
asset = inf_data.asset or ''
timeframe = inf_data.timeframe
fmt = inf_data.fmt
config = strategy.config

if asset:
# Insert stake currency if needed.
asset = _format_pair_name(config, asset)
else:
# Not specifying an asset will define informative dataframe for current pair.
asset = metadata['pair']

if '/' in asset:
base, quote = asset.split('/')
else:
# When futures are supported this may need reevaluation.
# base, quote = asset, None
raise OperationalException('Not implemented.')

# Default format. This optimizes for the common case: informative pairs using same stake
# currency. When quote currency matches stake currency, column name will omit base currency.
# This allows easily reconfiguring strategy to use different base currency. In a rare case
# where it is desired to keep quote currency in column name at all times user should specify
# fmt='{base}_{quote}_{column}_{timeframe}' format or similar.
if not fmt:
fmt = '{column}_{timeframe}' # Informatives of current pair
if quote != config['stake_currency']:
fmt = '{quote}_' + fmt # Informatives of different quote currency
if inf_data.asset:
fmt = '{base}_' + fmt # Informatives of other pair

inf_metadata = {'pair': asset, 'timeframe': timeframe}
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)

formatter: Any = None
if callable(fmt):
formatter = fmt # A custom user-specified formatter function.
else:
formatter = fmt.format # A default string formatter.

fmt_args = {
'BASE': base.upper(),
'QUOTE': quote.upper(),
'base': base.lower(),
'quote': quote.lower(),
'asset': asset,
'timeframe': timeframe,
}
inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args),
inplace=True)

date_column = formatter(column='date', **fmt_args)
if date_column in dataframe.columns:
raise OperationalException(f'Duplicate column name {date_column} exists in '
f'dataframe! Ensure column names are unique!')
dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe,
ffill=inf_data.ffill, append_timeframe=False,
date_column=date_column)
return dataframe
11 changes: 7 additions & 4 deletions freqtrade/strategy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.persistence import PairLocks, Trade
from freqtrade.strategy.hyper import HyperStrategyMixin
from freqtrade.strategy.strategy_helper import (InformativeData, PopulateIndicators,
_create_and_merge_informative_pair,
_format_pair_name)
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
_create_and_merge_informative_pair,
_format_pair_name)
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets

Expand Down Expand Up @@ -121,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# Class level variables (intentional) containing
# the dataprovider (dp) (access to other candles, historic data, ...)
# and wallets - access to the current balance.
dp: DataProvider
dp: Optional[DataProvider]
wallets: Optional[Wallets] = None
# Filled from configuration
stake_currency: str
Expand Down Expand Up @@ -408,6 +408,9 @@ def gather_informative_pairs(self) -> ListPairsWithTimeframes:
pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe)
informative_pairs.append(pair_tf)
else:
if not self.dp:
raise OperationalException('@informative decorator with unspecified asset '
'requires DataProvider instance.')
for pair in self.dp.current_whitelist():
informative_pairs.append((pair, inf_data.timeframe))
return list(set(informative_pairs))
Expand Down
132 changes: 0 additions & 132 deletions freqtrade/strategy/strategy_helper.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
from typing import Any, Callable, NamedTuple, Optional, Union

import pandas as pd
from mypy_extensions import KwArg
from pandas import DataFrame

from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes


PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]


class InformativeData(NamedTuple):
asset: Optional[str]
timeframe: str
fmt: Union[str, Callable[[KwArg(str)], str], None]
ffill: bool


def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
timeframe: str, timeframe_inf: str, ffill: bool = True,
append_timeframe: bool = True,
Expand Down Expand Up @@ -117,120 +102,3 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
:return: Positive stop loss value relative to current price
"""
return 1 - (stop_rate / current_rate)


def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
"""
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
define informative indicators.
Example usage:
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
current pair.
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
specified, defaults to:
* {base}_{column}_{timeframe} if asset is specified and quote currency does match stake
currency.
* {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match
stake currency.
* {column}_{timeframe} if asset is not specified.
Format string supports these format variables:
* {asset} - full name of the asset, for example 'BTC/USDT'.
* {base} - base currency in lower case, for example 'eth'.
* {BASE} - same as {base}, except in upper case.
* {quote} - quote currency in lower case, for example 'usdt'.
* {QUOTE} - same as {quote}, except in upper case.
* {column} - name of dataframe column.
* {timeframe} - timeframe of informative dataframe.
:param ffill: ffill dataframe after merging informative pair.
"""
_asset = asset
_timeframe = timeframe
_fmt = fmt
_ffill = ffill

def decorator(fn: PopulateIndicators):
informative_pairs = getattr(fn, '_ft_informative', [])
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
setattr(fn, '_ft_informative', informative_pairs)
return fn
return decorator


def _format_pair_name(config, pair: str) -> str:
return pair.format(stake_currency=config['stake_currency'],
stake=config['stake_currency']).upper()


def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
inf_data: InformativeData,
populate_indicators: PopulateIndicators):
asset = inf_data.asset or ''
timeframe = inf_data.timeframe
fmt = inf_data.fmt
config = strategy.config

if asset:
# Insert stake currency if needed.
asset = _format_pair_name(config, asset)
else:
# Not specifying an asset will define informative dataframe for current pair.
asset = metadata['pair']

if '/' in asset:
base, quote = asset.split('/')
else:
# When futures are supported this may need reevaluation.
# base, quote = asset, None
raise OperationalException('Not implemented.')

# Default format. This optimizes for the common case: informative pairs using same stake
# currency. When quote currency matches stake currency, column name will omit base currency.
# This allows easily reconfiguring strategy to use different base currency. In a rare case
# where it is desired to keep quote currency in column name at all times user should specify
# fmt='{base}_{quote}_{column}_{timeframe}' format or similar.
if not fmt:
fmt = '{column}_{timeframe}' # Informatives of current pair
if quote != config['stake_currency']:
fmt = '{quote}_' + fmt # Informatives of different quote currency
if inf_data.asset:
fmt = '{base}_' + fmt # Informatives of other pair

inf_metadata = {'pair': asset, 'timeframe': timeframe}
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)

formatter: Any = None
if callable(fmt):
formatter = fmt # A custom user-specified formatter function.
else:
formatter = fmt.format # A default string formatter.

fmt_args = {
'BASE': base.upper(),
'QUOTE': quote.upper(),
'base': base.lower(),
'quote': quote.lower(),
'asset': asset,
'timeframe': timeframe,
}
inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args),
inplace=True)

date_column = formatter(column='date', **fmt_args)
if date_column in dataframe.columns:
raise OperationalException(f'Duplicate column name {date_column} exists in '
f'dataframe! Ensure column names are unique!')
dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe,
ffill=inf_data.ffill, append_timeframe=False,
date_column=date_column)
return dataframe

0 comments on commit e88c470

Please sign in to comment.