Merge pull request #5378 from samgermain/lev-strat

Lev-strat
This commit is contained in:
Matthias 2021-09-27 19:16:00 +02:00 committed by GitHub
commit 949f469a7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1318 additions and 439 deletions

View File

@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"

View File

@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
# Run backtest # Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"

View File

@ -539,9 +539,10 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool: time_in_force: str, current_time: datetime,
side: str, **kwargs) -> bool:
""" """
Called right before placing a buy order. Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
network requests in this method. network requests in this method.
@ -549,12 +550,13 @@ class AwesomeStrategy(IStrategy):
When not implemented by a strategy, returns True (always confirming). When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought. :param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market. :param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded. :param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
: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. :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. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process
@ -617,7 +619,7 @@ It is possible to manage your risk by reducing or increasing stake amount when p
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float: side: str, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze() current_candle = dataframe.iloc[-1].squeeze()
@ -642,6 +644,34 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an
!!! Tip !!! Tip
Returning `0` or `None` will prevent trades from being placed. Returning `0` or `None` will prevent trades from being placed.
## Leverage Callback
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
Assuming a capital of 500USDT, a trade with leverage=3 would result in a position with 500 x 3 = 1500 USDT.
Values that are above `max_leverage` will be adjusted to `max_leverage`.
For markets / exchanges that don't support leverage, this method is ignored.
``` python
class AwesomeStrategy(IStrategy):
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
: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
```
--- ---
## Derived strategies ## Derived strategies

View File

@ -31,6 +31,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'profit_ratio', 'profit_abs', 'sell_reason', 'profit_ratio', 'profit_abs', 'sell_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag']
# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?)
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:

View File

@ -159,7 +159,8 @@ class Edge:
logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..') f'({(max_date - min_date).days} days)..')
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] # TODO-lev: Should edge support shorts? needs to be investigated further...
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long']
trades: list = [] trades: list = []
for pair, pair_data in preprocessed.items(): for pair, pair_data in preprocessed.items():
@ -167,8 +168,13 @@ class Edge:
pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True) pair_data = pair_data.reset_index(drop=True)
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_exit(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() dataframe=self.strategy.advise_entry(
dataframe=pair_data,
metadata={'pair': pair}
),
metadata={'pair': pair}
)[headers].copy()
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range) trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
@ -382,8 +388,8 @@ class Edge:
return final return final
def _find_trades_for_stoploss_range(self, df, pair, stoploss_range): def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
buy_column = df['buy'].values buy_column = df['enter_long'].values
sell_column = df['sell'].values sell_column = df['exit_long'].values
date_column = df['date'].values date_column = df['date'].values
ohlc_columns = df[['open', 'high', 'low', 'close']].values ohlc_columns = df[['open', 'high', 'low', 'close']].values

View File

@ -4,6 +4,6 @@ from freqtrade.enums.collateral import Collateral
from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType from freqtrade.enums.selltype import SellType
from freqtrade.enums.signaltype import SignalTagType, SignalType from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
from freqtrade.enums.state import State from freqtrade.enums.state import State
from freqtrade.enums.tradingmode import TradingMode from freqtrade.enums.tradingmode import TradingMode

View File

@ -5,12 +5,19 @@ class SignalType(Enum):
""" """
Enum to distinguish between enter and exit signals Enum to distinguish between enter and exit signals
""" """
BUY = "buy" ENTER_LONG = "enter_long"
SELL = "sell" EXIT_LONG = "exit_long"
ENTER_SHORT = "enter_short"
EXIT_SHORT = "exit_short"
class SignalTagType(Enum): class SignalTagType(Enum):
""" """
Enum for signal columns Enum for signal columns
""" """
BUY_TAG = "buy_tag" ENTER_TAG = "enter_tag"
class SignalDirection(Enum):
LONG = 'long'
SHORT = 'short'

View File

@ -422,24 +422,25 @@ class FreqtradeBot(LoggingMixin):
return False return False
# running get_signal on historical data fetched # running get_signal on historical data fetched
(buy, sell, buy_tag) = self.strategy.get_signal( (signal, enter_tag) = self.strategy.get_entry_signal(
pair, pair, self.strategy.timeframe, analyzed_df
self.strategy.timeframe,
analyzed_df
) )
if buy and not sell: if signal:
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
# TODO-lev: Does the below need to be adjusted for shorts?
if self._check_depth_of_market_buy(pair, bid_check_dom): if self._check_depth_of_market_buy(pair, bid_check_dom):
return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) # TODO-lev: pass in "enter" as side.
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
else: else:
return False return False
return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
else: else:
return False return False
@ -468,7 +469,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
@ -501,7 +502,9 @@ class FreqtradeBot(LoggingMixin):
default_retval=stake_amount)( default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount, current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount) min_stake=min_stake_amount, max_stake=max_stake_amount, side='long')
# TODO-lev: Add non-hardcoded "side" parameter
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount: if not stake_amount:
@ -518,9 +521,12 @@ class FreqtradeBot(LoggingMixin):
order_type = self.strategy.order_types.get('forcebuy', order_type) order_type = self.strategy.order_types.get('forcebuy', order_type)
# TODO-lev: Will this work for shorting? # TODO-lev: Will this work for shorting?
# TODO-lev: Add non-hardcoded "side" parameter
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
side='long'
):
logger.info(f"User requested abortion of buying {pair}") logger.info(f"User requested abortion of buying {pair}")
return False return False
amount = self.exchange.amount_to_precision(pair, amount) amount = self.exchange.amount_to_precision(pair, amount)
@ -579,7 +585,8 @@ class FreqtradeBot(LoggingMixin):
exchange=self.exchange.id, exchange=self.exchange.id,
open_order_id=order_id, open_order_id=order_id,
strategy=self.strategy.get_strategy_name(), strategy=self.strategy.get_strategy_name(),
buy_tag=buy_tag, # TODO-lev: compatibility layer for buy_tag (!)
buy_tag=enter_tag,
timeframe=timeframe_to_minutes(self.config['timeframe']) timeframe=timeframe_to_minutes(self.config['timeframe'])
) )
trade.orders.append(order_obj) trade.orders.append(order_obj)
@ -703,22 +710,23 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
(buy, sell) = (False, False) (enter, exit_) = (False, False)
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
if (self.config.get('use_sell_signal', True) or if (self.config.get('use_sell_signal', True) or
self.config.get('ignore_roi_if_buy_signal', False)): self.config.get('ignore_roi_if_buy_signal', False)):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe) self.strategy.timeframe)
(buy, sell, _) = self.strategy.get_signal( (enter, exit_) = self.strategy.get_exit_signal(
trade.pair, trade.pair,
self.strategy.timeframe, self.strategy.timeframe,
analyzed_df analyzed_df, is_short=trade.is_short
) )
logger.debug('checking sell') # TODO-lev: side should depend on trade side.
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
if self._check_and_execute_exit(trade, exit_rate, buy, sell): if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
return True return True
logger.debug('Found no sell signal for %s.', trade) logger.debug('Found no sell signal for %s.', trade)
@ -864,18 +872,18 @@ class FreqtradeBot(LoggingMixin):
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def _check_and_execute_exit(self, trade: Trade, exit_rate: float, def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
buy: bool, sell: bool) -> bool: enter: bool, exit_: bool) -> bool:
""" """
Check and execute exit Check and execute trade exit
""" """
should_sell = self.strategy.should_sell( should_exit: SellCheckTuple = self.strategy.should_exit(
trade, exit_rate, datetime.now(timezone.utc), buy, sell, trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
if should_sell.sell_flag: if should_exit.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}')
self.execute_trade_exit(trade, exit_rate, should_sell) self.execute_trade_exit(trade, exit_rate, should_exit)
return True return True
return False return False

View File

@ -37,13 +37,15 @@ logger = logging.getLogger(__name__)
# Indexes for backtest tuples # Indexes for backtest tuples
DATE_IDX = 0 DATE_IDX = 0
BUY_IDX = 1 OPEN_IDX = 1
OPEN_IDX = 2 HIGH_IDX = 2
CLOSE_IDX = 3 LOW_IDX = 3
SELL_IDX = 4 CLOSE_IDX = 4
LOW_IDX = 5 LONG_IDX = 5
HIGH_IDX = 6 ELONG_IDX = 6 # Exit long
BUY_TAG_IDX = 7 SHORT_IDX = 7
ESHORT_IDX = 8 # Exit short
ENTER_TAG_IDX = 9
class Backtesting: class Backtesting:
@ -64,8 +66,8 @@ class Backtesting:
config['dry_run'] = True config['dry_run'] = True
self.strategylist: List[IStrategy] = [] self.strategylist: List[IStrategy] = []
self.all_results: Dict[str, Dict] = {} self.all_results: Dict[str, Dict] = {}
self._exchange_name = self.config['exchange']['name']
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
self.dataprovider = DataProvider(self.config, None) self.dataprovider = DataProvider(self.config, None)
if self.config.get('strategy_list', None): if self.config.get('strategy_list', None):
@ -136,6 +138,10 @@ class Backtesting:
self.config['startup_candle_count'] = self.required_startup self.config['startup_candle_count'] = self.required_startup
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
# TODO-lev: This should come from the configuration setting or better a
# TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange
self._can_short = False
self.progress = BTProgress() self.progress = BTProgress()
self.abort = False self.abort = False
@ -245,7 +251,8 @@ class Backtesting:
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple # Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top # and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag'] headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag']
data: Dict = {} data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed)) self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -253,13 +260,13 @@ class Backtesting:
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
self.check_abort() self.check_abort()
self.progress.increment() self.progress.increment()
if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
df_analyzed = self.strategy.advise_sell( if not pair_data.empty:
self.strategy.advise_buy(pair_data, {'pair': pair}), # Cleanup from prior runs
pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore')
df_analyzed = self.strategy.advise_exit(
self.strategy.advise_entry(pair_data, {'pair': pair}),
{'pair': pair} {'pair': pair}
).copy() ).copy()
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
@ -267,9 +274,11 @@ class Backtesting:
startup_candles=self.required_startup) startup_candles=self.required_startup)
# To avoid using data from future, we use buy/sell signals shifted # To avoid using data from future, we use buy/sell signals shifted
# from the previous candle # from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) for col in headers[5:]:
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) if col in df_analyzed.columns:
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) df_analyzed.loc[:, col] = df_analyzed.loc[:, col].shift(1)
else:
df_analyzed.loc[:, col] = 0 if col != 'enter_tag' else None
# Update dataprovider cache # Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
@ -350,10 +359,13 @@ class Backtesting:
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
sell_row: Tuple) -> Optional[LocalTrade]: sell_row: Tuple) -> Optional[LocalTrade]:
sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX]
sell_candle_time, sell_row[BUY_IDX], exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX]
sell_row[SELL_IDX], sell = self.strategy.should_exit(
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore
enter=enter, exit_=exit_,
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]
)
if sell.sell_flag: if sell.sell_flag:
trade.close_date = sell_candle_time trade.close_date = sell_candle_time
@ -389,9 +401,12 @@ class Backtesting:
if len(detail_data) == 0: if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle # Fall back to "regular" data if no detail data was found for this candle
return self._get_sell_trade_entry_for_candle(trade, sell_row) return self._get_sell_trade_entry_for_candle(trade, sell_row)
detail_data['buy'] = sell_row[BUY_IDX] detail_data['enter_long'] = sell_row[LONG_IDX]
detail_data['sell'] = sell_row[SELL_IDX] detail_data['exit_long'] = sell_row[ELONG_IDX]
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] detail_data['enter_short'] = sell_row[SHORT_IDX]
detail_data['exit_short'] = sell_row[ESHORT_IDX]
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short']
for det_row in detail_data[headers].values.tolist(): for det_row in detail_data[headers].values.tolist():
res = self._get_sell_trade_entry_for_candle(trade, det_row) res = self._get_sell_trade_entry_for_candle(trade, det_row)
if res: if res:
@ -402,7 +417,7 @@ class Backtesting:
else: else:
return self._get_sell_trade_entry_for_candle(trade, sell_row) return self._get_sell_trade_entry_for_candle(trade, sell_row)
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: def _enter_trade(self, pair: str, row: List, direction: str) -> Optional[LocalTrade]:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException: except DependencyException:
@ -414,7 +429,8 @@ class Backtesting:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)( default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
side=direction)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount: if not stake_amount:
@ -425,12 +441,13 @@ class Backtesting:
# Confirm trade entry: # Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime(),
side=direction):
return None return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
# Enter trade # Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1 has_enter_tag = len(row) >= ENTER_TAG_IDX + 1
trade = LocalTrade( trade = LocalTrade(
pair=pair, pair=pair,
open_rate=row[OPEN_IDX], open_rate=row[OPEN_IDX],
@ -440,8 +457,9 @@ class Backtesting:
fee_open=self.fee, fee_open=self.fee,
fee_close=self.fee, fee_close=self.fee,
is_open=True, is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None,
exchange='backtesting', exchange=self._exchange_name,
is_short=(direction == 'short'),
) )
return trade return trade
return None return None
@ -475,6 +493,20 @@ class Backtesting:
self.rejected_trades += 1 self.rejected_trades += 1
return False return False
def check_for_trade_entry(self, row) -> Optional[str]:
enter_long = row[LONG_IDX] == 1
exit_long = row[ELONG_IDX] == 1
enter_short = self._can_short and row[SHORT_IDX] == 1
exit_short = self._can_short and row[ESHORT_IDX] == 1
if enter_long == 1 and not any([exit_long, enter_short]):
# Long
return 'long'
if enter_short == 1 and not any([exit_short, enter_long]):
# Short
return 'short'
return None
def backtest(self, processed: Dict, def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False, max_open_trades: int = 0, position_stacking: bool = False,
@ -537,15 +569,15 @@ class Backtesting:
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected # max_open_trades must be respected
# don't open on the last row # don't open on the last row
trade_dir = self.check_for_trade_entry(row)
if ( if (
(position_stacking or len(open_trades[pair]) == 0) (position_stacking or len(open_trades[pair]) == 0)
and self.trade_slot_available(max_open_trades, open_trade_count_start) and self.trade_slot_available(max_open_trades, open_trade_count_start)
and tmp != end_date and tmp != end_date
and row[BUY_IDX] == 1 and trade_dir is not None
and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
): ):
trade = self._enter_trade(pair, row) trade = self._enter_trade(pair, row, trade_dir)
if trade: if trade:
# TODO: hacky workaround to avoid opening > max_open_trades # TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct # This emulates previous behaviour - not sure if this is correct

View File

@ -386,8 +386,9 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
) )
fig.add_trace(candles, 1, 1) fig.add_trace(candles, 1, 1)
if 'buy' in data.columns: # TODO-lev: Needs short equivalent
df_buy = data[data['buy'] == 1] if 'enter_long' in data.columns:
df_buy = data[data['enter_long'] == 1]
if len(df_buy) > 0: if len(df_buy) > 0:
buys = go.Scatter( buys = go.Scatter(
x=df_buy.date, x=df_buy.date,
@ -405,8 +406,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
else: else:
logger.warning("No buy-signals found.") logger.warning("No buy-signals found.")
if 'sell' in data.columns: if 'exit_long' in data.columns:
df_sell = data[data['sell'] == 1] df_sell = data[data['exit_long'] == 1]
if len(df_sell) > 0: if len(df_sell) > 0:
sells = go.Scatter( sells = go.Scatter(
x=df_sell.date, x=df_sell.date,

View File

@ -13,7 +13,7 @@ from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import SellType, SignalTagType, SignalType from freqtrade.enums import SellType, SignalDirection, SignalTagType, SignalType
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
@ -187,7 +187,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
""" """
Check buy enter timeout function callback. Check buy timeout function callback.
This method can be used to override the enter-timeout. This method can be used to override the enter-timeout.
It is called whenever a limit entry order has been created, It is called whenever a limit entry order has been created,
and is not yet fully filled. and is not yet fully filled.
@ -231,7 +231,8 @@ class IStrategy(ABC, HyperStrategyMixin):
pass pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, **kwargs) -> bool: time_in_force: str, current_time: datetime,
side: str, **kwargs) -> bool:
""" """
Called right before placing a entry order. Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -247,6 +248,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
: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. :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. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process
@ -366,10 +368,9 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float: side: str, **kwargs) -> float:
""" """
Customize stake size for each new trade. This method is not called when edge module is Customize stake size for each new trade.
enabled.
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
@ -377,10 +378,28 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot. :param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :param max_stake: Balance available for trading.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake. :return: A stake size, which is between min_stake and max_stake.
""" """
return proposed_stake return proposed_stake
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is not called when edge module is
enabled.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
: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
def informative_pairs(self) -> ListPairsWithTimeframes: def informative_pairs(self) -> ListPairsWithTimeframes:
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
@ -471,8 +490,8 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
logger.debug("TA Analysis Launched") logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata) dataframe = self.advise_entry(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata) dataframe = self.advise_exit(dataframe, metadata)
return dataframe return dataframe
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -497,9 +516,11 @@ class IStrategy(ABC, HyperStrategyMixin):
self.dp._set_cached_df(pair, self.timeframe, dataframe) self.dp._set_cached_df(pair, self.timeframe, dataframe)
else: else:
logger.debug("Skipping TA Analysis for already analyzed candle") logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0 dataframe[SignalType.ENTER_LONG.value] = 0
dataframe['sell'] = 0 dataframe[SignalType.EXIT_LONG.value] = 0
dataframe['buy_tag'] = None dataframe[SignalType.ENTER_SHORT.value] = 0
dataframe[SignalType.EXIT_SHORT.value] = 0
dataframe[SignalTagType.ENTER_TAG.value] = None
# Other Defs in strategy that want to be called every loop here # Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata) # twitter_sell = self.watch_twitter_feed(dataframe, metadata)
@ -558,8 +579,8 @@ class IStrategy(ABC, HyperStrategyMixin):
message = "" message = ""
if dataframe is None: if dataframe is None:
message = "No dataframe returned (return statement missing?)." message = "No dataframe returned (return statement missing?)."
elif 'buy' not in dataframe: elif 'enter_long' not in dataframe:
message = "Buy column not set." message = "enter_long/buy column not set."
elif df_len != len(dataframe): elif df_len != len(dataframe):
message = message_template.format("length") message = message_template.format("length")
elif df_close != dataframe["close"].iloc[-1]: elif df_close != dataframe["close"].iloc[-1]:
@ -572,12 +593,12 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
raise StrategyError(message) raise StrategyError(message)
def get_signal( def get_latest_candle(
self, self,
pair: str, pair: str,
timeframe: str, timeframe: str,
dataframe: DataFrame dataframe: DataFrame,
) -> Tuple[bool, bool, Optional[str]]: ) -> Tuple[Optional[DataFrame], Optional[arrow.Arrow]]:
""" """
Calculates current signal based based on the entry order or exit order Calculates current signal based based on the entry order or exit order
columns of the dataframe. columns of the dataframe.
@ -585,12 +606,11 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
:param timeframe: timeframe to use :param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from. :param dataframe: Analyzed dataframe to get signal from.
:return: (Buy, Sell)/(Short, Exit_short) A bool-tuple indicating :return: (None, None) or (Dataframe, latest_date) - corresponding to the last candle
(buy/sell)/(short/exit_short) signal
""" """
if not isinstance(dataframe, DataFrame) or dataframe.empty: if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning(f'Empty candle (OHLCV) data for pair {pair}') logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
return False, False, None return None, None
latest_date = dataframe['date'].max() latest_date = dataframe['date'].max()
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
@ -605,27 +625,89 @@ class IStrategy(ABC, HyperStrategyMixin):
'Outdated history for pair %s. Last tick is %s minutes old', 'Outdated history for pair %s. Last tick is %s minutes old',
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60) pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
) )
return False, False, None return None, None
return latest, latest_date
enter = latest[SignalType.BUY.value] == 1 def get_exit_signal(
self,
pair: str,
timeframe: str,
dataframe: DataFrame,
is_short: bool = None
) -> Tuple[bool, bool]:
"""
Calculates current exit signal based based on the buy/short or sell/exit_short
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.
"""
latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe)
if latest is None:
return False, False
exit = False if is_short:
if SignalType.SELL.value in latest: enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
exit = latest[SignalType.SELL.value] == 1 exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
else:
enter = latest[SignalType.ENTER_LONG.value] == 1
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) "
f"enter={enter} exit={exit_}")
return enter, exit_
def get_entry_signal(
self,
pair: str,
timeframe: str,
dataframe: DataFrame,
) -> Tuple[Optional[SignalDirection], Optional[str]]:
"""
Calculates current entry signal based based on the buy/short or sell/exit_short
columns of the dataframe.
Used by Bot to get the signal to buy, sell, short, or exit_short
: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)
if latest is None or latest_date is None:
return None, None
enter_long = latest[SignalType.ENTER_LONG.value] == 1
exit_long = latest.get(SignalType.EXIT_LONG.value, 0) == 1
enter_short = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
enter_signal: Optional[SignalDirection] = None
enter_tag_value: Optional[str] = None
if enter_long == 1 and not any([exit_long, enter_short]):
enter_signal = SignalDirection.LONG
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
if enter_short == 1 and not any([exit_short, enter_long]):
enter_signal = SignalDirection.SHORT
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'], pair, str(enter), str(exit))
timeframe_seconds = timeframe_to_seconds(timeframe) timeframe_seconds = timeframe_to_seconds(timeframe)
if self.ignore_expired_candle( if self.ignore_expired_candle(
latest_date=latest_date, latest_date=latest_date.datetime,
current_time=datetime.now(timezone.utc), current_time=datetime.now(timezone.utc),
timeframe_seconds=timeframe_seconds, timeframe_seconds=timeframe_seconds,
enter=enter enter=bool(enter_signal)
): ):
return False, exit, buy_tag return None, enter_tag_value
return enter, exit, buy_tag
logger.debug(f"entry trigger: {latest['date']} (pair={pair}) "
f"enter={enter_long} enter_tag_value={enter_tag_value}")
return enter_signal, enter_tag_value
def ignore_expired_candle( def ignore_expired_candle(
self, self,
@ -640,8 +722,9 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return False return False
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, def should_exit(self, trade: Trade, rate: float, date: datetime, *,
sell: bool, low: float = None, high: float = None, enter: bool, exit_: bool,
low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
""" """
This function evaluates if one of the conditions required to trigger an exit order This function evaluates if one of the conditions required to trigger an exit order
@ -651,6 +734,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param force_stoploss: Externally provided stoploss :param force_stoploss: Externally provided stoploss
:return: True if trade should be exited, False otherwise :return: True if trade should be exited, False otherwise
""" """
current_rate = rate current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
@ -665,7 +749,7 @@ class IStrategy(ABC, HyperStrategyMixin):
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi. # if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and self.ignore_roi_if_buy_signal) roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=date))
@ -678,10 +762,11 @@ class IStrategy(ABC, HyperStrategyMixin):
if (self.sell_profit_only and current_profit <= self.sell_profit_offset): if (self.sell_profit_only and current_profit <= self.sell_profit_offset):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal # sell_profit_only and profit doesn't reach the offset - ignore sell signal
pass pass
elif self.use_sell_signal and not buy: elif self.use_sell_signal and not enter:
if sell: if exit_:
sell_signal = SellType.SELL_SIGNAL sell_signal = SellType.SELL_SIGNAL
else: else:
trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
current_profit=current_profit) current_profit=current_profit)
@ -689,9 +774,9 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_signal = SellType.CUSTOM_SELL sell_signal = SellType.CUSTOM_SELL
if isinstance(custom_reason, str): if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
logger.warning(f'Custom sell reason returned from custom_sell is too ' logger.warning(f'Custom {trade_type} reason returned from '
f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' f'custom_{trade_type} is too long and was trimmed'
f'characters.') f'to {CUSTOM_SELL_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
else: else:
custom_reason = None custom_reason = None
@ -737,7 +822,12 @@ class IStrategy(ABC, HyperStrategyMixin):
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if self.use_custom_stoploss and trade.stop_loss < (low or current_rate): dir_correct = (trade.stop_loss < (low or current_rate)
if not trade.is_short else
trade.stop_loss > (high or current_rate)
)
if self.use_custom_stoploss and dir_correct:
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
)(pair=trade.pair, trade=trade, )(pair=trade.pair, trade=trade,
current_time=current_time, current_time=current_time,
@ -755,6 +845,7 @@ class IStrategy(ABC, HyperStrategyMixin):
sl_offset = self.trailing_stop_positive_offset sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting. # Make sure current_profit is calculated using high for backtesting.
# TODO-lev: Check this function - high / low usage must be inversed for short trades!
high_profit = current_profit if not high else trade.calc_profit_ratio(high) high_profit = current_profit if not high else trade.calc_profit_ratio(high)
# Don't update stoploss if trailing_only_offset_is_reached is true. # Don't update stoploss if trailing_only_offset_is_reached is true.
@ -821,7 +912,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Populates indicators for given candle (OHLCV) data (for multiple pairs) Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advise_buy or advise_sell! Does not run advise_entry or advise_exit!
Used by optimize operations only, not during dry / live runs. Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run. Using .copy() to get a fresh copy of the dataframe for every strategy run.
Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show.
@ -853,7 +944,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return self.populate_indicators(dataframe, metadata) return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the entry order signal for the given dataframe Based on TA indicators, populates the entry order signal for the given dataframe
This method should not be overridden. This method should not be overridden.
@ -868,11 +959,15 @@ class IStrategy(ABC, HyperStrategyMixin):
if self._buy_fun_len == 2: if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see " warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore df = self.populate_buy_trend(dataframe) # type: ignore
else: else:
return self.populate_buy_trend(dataframe, metadata) df = self.populate_buy_trend(dataframe, metadata)
if 'enter_long' not in df.columns:
df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns')
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return df
def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the exit order signal for the given dataframe Based on TA indicators, populates the exit order signal for the given dataframe
This method should not be overridden. This method should not be overridden.
@ -886,6 +981,9 @@ class IStrategy(ABC, HyperStrategyMixin):
if self._sell_fun_len == 2: if self._sell_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see " warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore df = self.populate_sell_trend(dataframe) # type: ignore
else: else:
return self.populate_sell_trend(dataframe, metadata) df = self.populate_sell_trend(dataframe, metadata)
if 'exit_long' not in df.columns:
df = df.rename({'sell': 'exit_long'}, axis='columns')
return df

View File

@ -1,5 +1,6 @@
import pandas as pd import pandas as pd
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@ -66,7 +67,11 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
return dataframe return dataframe
def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: def stoploss_from_open(
open_relative_stop: float,
current_profit: float,
for_short: bool = False
) -> float:
""" """
Given the current profit, and a desired stop loss value relative to the open price, Given the current profit, and a desired stop loss value relative to the open price,
@ -87,9 +92,18 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
if current_profit == -1: if current_profit == -1:
return 1 return 1
if for_short is True:
# TODO-lev: How would this be calculated for short
raise OperationalException(
"Freqtrade hasn't figured out how to calculated stoploss on shorts")
# stoploss = 1-((1+open_relative_stop)/(1+current_profit))
else:
stoploss = 1-((1+open_relative_stop)/(1+current_profit)) stoploss = 1-((1+open_relative_stop)/(1+current_profit))
# negative stoploss values indicate the requested stop price is higher than the current price # negative stoploss values indicate the requested stop price is higher than the current price
if for_short:
return min(stoploss, 0.0)
else:
return max(stoploss, 0.0) return max(stoploss, 0.0)

View File

@ -122,7 +122,7 @@ class {{ strategy }}(IStrategy):
{{ buy_trend | indent(16) }} {{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'buy'] = 1 'enter_long'] = 1
return dataframe return dataframe
@ -138,6 +138,6 @@ class {{ strategy }}(IStrategy):
{{ sell_trend | indent(16) }} {{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'sell'] = 1 'exit_long'] = 1
return dataframe return dataframe
{{ additional_methods | indent(4) }} {{ additional_methods | indent(4) }}

View File

@ -0,0 +1,379 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
# This class is a sample. Feel free to customize it.
class SampleStrategy(IStrategy):
"""
This is a sample strategy to inspire you.
More information in https://www.freqtrade.io/en/latest/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy
- Add any lib you need to build your strategy
You must keep:
- the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend
You should keep:
- timeframe, minimal_roi, stoploss, trailing_*
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 2
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {
"60": 0.01,
"30": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy.
# This attribute will be overridden if the config file contains "stoploss".
stoploss = -0.10
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Hyperoptable parameters
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
# Optimal timeframe for the strategy.
timeframe = '5m'
# Run "populate_indicators()" only for new candle.
process_only_new_candles = False
# These values can be overridden in the "ask_strategy" section in the config.
use_sell_signal = True
sell_profit_only = False
ignore_roi_if_buy_signal = False
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 30
# Optional order type mapping.
order_types = {
'buy': 'limit',
'sell': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
# Optional order time in force.
order_time_in_force = {
'buy': 'gtc',
'sell': 'gtc'
}
plot_config = {
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
},
'subplots': {
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
},
"RSI": {
'rsi': {'color': 'red'},
}
}
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
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 []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
: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
"""
# Momentum Indicators
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# # Plus Directional Indicator / Movement
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
# dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# # Minus Directional Indicator / Movement
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
# dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# # Aroon, Aroon Oscillator
# aroon = ta.AROON(dataframe)
# dataframe['aroonup'] = aroon['aroonup']
# dataframe['aroondown'] = aroon['aroondown']
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
# # Awesome Oscillator
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# # Keltner Channel
# keltner = qtpylib.keltner_channel(dataframe)
# dataframe["kc_upperband"] = keltner["upper"]
# dataframe["kc_lowerband"] = keltner["lower"]
# dataframe["kc_middleband"] = keltner["mid"]
# dataframe["kc_percent"] = (
# (dataframe["close"] - dataframe["kc_lowerband"]) /
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"])
# )
# dataframe["kc_width"] = (
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"]
# )
# # Ultimate Oscillator
# dataframe['uo'] = ta.ULTOSC(dataframe)
# # Commodity Channel Index: values [Oversold:-100, Overbought:100]
# dataframe['cci'] = ta.CCI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
# rsi = 0.1 * (dataframe['rsi'] - 50)
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
# # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# # Stochastic Slow
# stoch = ta.STOCH(dataframe)
# dataframe['slowd'] = stoch['slowd']
# dataframe['slowk'] = stoch['slowk']
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# # Stochastic RSI
# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this.
# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results.
# stoch_rsi = ta.STOCHRSI(dataframe)
# dataframe['fastd_rsi'] = stoch_rsi['fastd']
# dataframe['fastk_rsi'] = stoch_rsi['fastk']
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# # ROC
# dataframe['roc'] = ta.ROC(dataframe)
# Overlap Studies
# ------------------------------------
# Bollinger Bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
dataframe["bb_percent"] = (
(dataframe["close"] - dataframe["bb_lowerband"]) /
(dataframe["bb_upperband"] - dataframe["bb_lowerband"])
)
dataframe["bb_width"] = (
(dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]
)
# Bollinger Bands - Weighted (EMA based instead of SMA)
# weighted_bollinger = qtpylib.weighted_bollinger_bands(
# qtpylib.typical_price(dataframe), window=20, stds=2
# )
# dataframe["wbb_upperband"] = weighted_bollinger["upper"]
# dataframe["wbb_lowerband"] = weighted_bollinger["lower"]
# dataframe["wbb_middleband"] = weighted_bollinger["mid"]
# dataframe["wbb_percent"] = (
# (dataframe["close"] - dataframe["wbb_lowerband"]) /
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"])
# )
# dataframe["wbb_width"] = (
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) /
# dataframe["wbb_middleband"]
# )
# # EMA - Exponential Moving Average
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# # SMA - Simple Moving Average
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3)
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5)
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10)
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21)
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50)
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100)
# Parabolic SAR
dataframe['sar'] = ta.SAR(dataframe)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
# # Hammer: values [0, 100]
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# # Inverted Hammer: values [0, 100]
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# # Dragonfly Doji: values [0, 100]
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# # Piercing Line: values [0, 100]
# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# # Morningstar: values [0, 100]
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# # Three White Soldiers: values [0, 100]
# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
# # Hanging Man: values [0, 100]
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# # Shooting Star: values [0, 100]
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# # Gravestone Doji: values [0, 100]
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# # Dark Cloud Cover: values [0, 100]
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# # Evening Doji Star: values [0, 100]
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# # Evening Star: values [0, 100]
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
# # Three Line Strike: values [0, -100, 100]
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# # Spinning Top: values [0, -100, 100]
# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# # Engulfing: values [0, -100, 100]
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# # Harami: values [0, -100, 100]
# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# # Three Outside Up/Down: values [0, -100, 100]
# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# # Three Inside Up/Down: values [0, -100, 100]
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
# # Chart type
# # ------------------------------------
# # Heikin Ashi Strategy
# heikinashi = qtpylib.heikinashi(dataframe)
# dataframe['ha_open'] = heikinashi['open']
# dataframe['ha_close'] = heikinashi['close']
# dataframe['ha_high'] = heikinashi['high']
# dataframe['ha_low'] = heikinashi['low']
# Retrieve best bid and best ask from the orderbook
# ------------------------------------
"""
# first check if dataprovider is available
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
"""
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'enter_short'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column
"""
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
# Guard: tema below BB middle
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
return dataframe

View File

@ -352,7 +352,7 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'buy'] = 1 'enter_long'] = 1
return dataframe return dataframe
@ -371,5 +371,5 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'sell'] = 1 'exit_long'] = 1
return dataframe return dataframe

View File

@ -12,12 +12,11 @@ def bot_loop_start(self, **kwargs) -> None:
""" """
pass pass
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float: side: str, **kwargs) -> float:
""" """
Customize stake size for each new trade. This method is not called when edge module is Customize stake size for each new trade.
enabled.
:param pair: Pair that's currently analyzed :param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
@ -25,6 +24,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
:param proposed_stake: A stake amount proposed by the bot. :param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :param max_stake: Balance available for trading.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake. :return: A stake size, which is between min_stake and max_stake.
""" """
return proposed_stake return proposed_stake
@ -80,9 +80,10 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
return None return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: 'datetime', **kwargs) -> bool: time_in_force: str, current_time: datetime,
side: str, **kwargs) -> bool:
""" """
Called right before placing a buy order. Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
network requests in this method. network requests in this method.
@ -90,12 +91,13 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
When not implemented by a strategy, returns True (always confirming). When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought. :param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market. :param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded. :param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
: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. :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. :return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process False aborts the process

View File

@ -19,8 +19,8 @@ from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_in
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has,
patched_configuration_load_config_file) log_has_re, patch_exchange, patched_configuration_load_config_file)
from tests.conftest_trades import MOCK_TRADE_COUNT from tests.conftest_trades import MOCK_TRADE_COUNT
@ -774,7 +774,7 @@ def test_start_list_strategies(mocker, caplog, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestStrategyLegacyV1" in captured.out assert "TestStrategyLegacyV1" in captured.out
assert "legacy_strategy_v1.py" not in captured.out assert "legacy_strategy_v1.py" not in captured.out
assert "StrategyTestV2" in captured.out assert CURRENT_TEST_STRATEGY in captured.out
# Test regular output # Test regular output
args = [ args = [
@ -789,7 +789,7 @@ def test_start_list_strategies(mocker, caplog, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestStrategyLegacyV1" in captured.out assert "TestStrategyLegacyV1" in captured.out
assert "legacy_strategy_v1.py" in captured.out assert "legacy_strategy_v1.py" in captured.out
assert "StrategyTestV2" in captured.out assert CURRENT_TEST_STRATEGY in captured.out
# Test color output # Test color output
args = [ args = [
@ -803,7 +803,7 @@ def test_start_list_strategies(mocker, caplog, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "TestStrategyLegacyV1" in captured.out assert "TestStrategyLegacyV1" in captured.out
assert "legacy_strategy_v1.py" in captured.out assert "legacy_strategy_v1.py" in captured.out
assert "StrategyTestV2" in captured.out assert CURRENT_TEST_STRATEGY in captured.out
assert "LOAD FAILED" in captured.out assert "LOAD FAILED" in captured.out

View File

@ -6,7 +6,7 @@ from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Optional, Tuple
from unittest.mock import MagicMock, Mock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock
import arrow import arrow
@ -19,6 +19,7 @@ from freqtrade.commands import Arguments
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.enums import Collateral, RunMode, TradingMode
from freqtrade.enums.signaltype import SignalDirection
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.persistence import LocalTrade, Trade, init_db
@ -34,6 +35,9 @@ logging.getLogger('').setLevel(logging.INFO)
# Do not mask numpy errors as warnings that no one read, raise the exсeption # Do not mask numpy errors as warnings that no one read, raise the exсeption
np.seterr(all='raise') np.seterr(all='raise')
CURRENT_TEST_STRATEGY = 'StrategyTestV3'
TRADE_SIDES = ('long', 'short')
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption('--longrun', action='store_true', dest="longrun", parser.addoption('--longrun', action='store_true', dest="longrun",
@ -201,13 +205,35 @@ def get_patched_worker(mocker, config) -> Worker:
return Worker(args=None, config=config) return Worker(args=None, config=config)
def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None: def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False,
enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None:
""" """
:param mocker: mocker to patch IStrategy class :param mocker: mocker to patch IStrategy class
:param value: which value IStrategy.get_signal() must return :param value: which value IStrategy.get_signal() must return
(buy, sell, buy_tag)
:return: None :return: None
""" """
freqtrade.strategy.get_signal = lambda e, s, x: value # returns (Signal-direction, signaname)
def patched_get_entry_signal(*args, **kwargs):
direction = None
if enter_long and not any([exit_long, enter_short]):
direction = SignalDirection.LONG
if enter_short and not any([exit_short, enter_long]):
direction = SignalDirection.SHORT
return direction, enter_tag
freqtrade.strategy.get_entry_signal = patched_get_entry_signal
def patched_get_exit_signal(pair, timeframe, dataframe, is_short):
if is_short:
return enter_short, exit_short
else:
return enter_long, exit_long
# returns (enter, exit)
freqtrade.strategy.get_exit_signal = patched_get_exit_signal
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
@ -383,7 +409,7 @@ def get_default_conf(testdatadir):
"user_data_dir": Path("user_data"), "user_data_dir": Path("user_data"),
"verbosity": 3, "verbosity": 3,
"strategy_path": str(Path(__file__).parent / "strategy" / "strats"), "strategy_path": str(Path(__file__).parent / "strategy" / "strats"),
"strategy": "StrategyTestV2", "strategy": CURRENT_TEST_STRATEGY,
"disableparamexport": True, "disableparamexport": True,
"internals": {}, "internals": {},
"export": "none", "export": "none",

View File

@ -33,7 +33,7 @@ def mock_trade_1(fee):
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
open_order_id='dry_run_buy_12345', open_order_id='dry_run_buy_12345',
strategy='StrategyTestV2', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy')
@ -87,7 +87,7 @@ def mock_trade_2(fee):
exchange='binance', exchange='binance',
is_open=False, is_open=False,
open_order_id='dry_run_sell_12345', open_order_id='dry_run_sell_12345',
strategy='StrategyTestV2', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
sell_reason='sell_signal', sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
@ -146,7 +146,7 @@ def mock_trade_3(fee):
close_profit_abs=0.000155, close_profit_abs=0.000155,
exchange='binance', exchange='binance',
is_open=False, is_open=False,
strategy='StrategyTestV2', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
sell_reason='roi', sell_reason='roi',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
@ -189,7 +189,7 @@ def mock_trade_4(fee):
open_rate=0.123, open_rate=0.123,
exchange='binance', exchange='binance',
open_order_id='prod_buy_12345', open_order_id='prod_buy_12345',
strategy='StrategyTestV2', strategy='StrategyTestV3',
timeframe=5, timeframe=5,
) )
o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy')

View File

@ -16,7 +16,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_
get_latest_hyperopt_file, load_backtest_data, load_trades, get_latest_hyperopt_file, load_backtest_data, load_trades,
load_trades_from_db) load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.history import load_data, load_pair_history
from tests.conftest import create_mock_trades from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
from tests.conftest_trades import MOCK_TRADE_COUNT from tests.conftest_trades import MOCK_TRADE_COUNT
@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
for col in BT_DATA_COLUMNS: for col in BT_DATA_COLUMNS:
if col not in ['index', 'open_at_end']: if col not in ['index', 'open_at_end']:
assert col in trades.columns assert col in trades.columns
trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='StrategyTestV2') trades = load_trades_from_db(db_url=default_conf['db_url'], strategy=CURRENT_TEST_STRATEGY)
assert len(trades) == 4 assert len(trades) == 4
trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy')
assert len(trades) == 0 assert len(trades) == 0
@ -186,7 +186,7 @@ def test_load_trades(default_conf, mocker):
db_url=default_conf.get('db_url'), db_url=default_conf.get('db_url'),
exportfilename=default_conf.get('exportfilename'), exportfilename=default_conf.get('exportfilename'),
no_trades=False, no_trades=False,
strategy="StrategyTestV2", strategy=CURRENT_TEST_STRATEGY,
) )
assert db_mock.call_count == 1 assert db_mock.call_count == 1

View File

@ -26,7 +26,8 @@ from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHa
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re,
patch_exchange)
# Change this if modifying UNITTEST/BTC testdatafile # Change this if modifying UNITTEST/BTC testdatafile
@ -380,7 +381,7 @@ def test_file_dump_json_tofile(testdatadir) -> None:
def test_get_timerange(default_conf, mocker, testdatadir) -> None: def test_get_timerange(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'strategy': 'StrategyTestV2'}) default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.advise_all_indicators( data = strategy.advise_all_indicators(
@ -398,7 +399,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None:
def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'strategy': 'StrategyTestV2'}) default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.advise_all_indicators( data = strategy.advise_all_indicators(
@ -422,7 +423,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None: def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'strategy': 'StrategyTestV2'}) default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange('index', 'index', 200, 250) timerange = TimeRange('index', 'index', 200, 250)

View File

@ -18,7 +18,7 @@ class BTrade(NamedTuple):
sell_reason: SellType sell_reason: SellType
open_tick: int open_tick: int
close_tick: int close_tick: int
buy_tag: Optional[str] = None enter_tag: Optional[str] = None
class BTContainer(NamedTuple): class BTContainer(NamedTuple):
@ -44,14 +44,18 @@ def _get_frame_time_from_offset(offset):
def _build_backtest_dataframe(data): def _build_backtest_dataframe(data):
columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell'] columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'enter_long', 'exit_long',
columns = columns + ['buy_tag'] if len(data[0]) == 9 else columns 'enter_short', 'exit_short']
if len(data[0]) == 8:
# No short columns
data = [d + [0, 0] for d in data]
columns = columns + ['enter_tag'] if len(data[0]) == 11 else columns
frame = DataFrame.from_records(data, columns=columns) frame = DataFrame.from_records(data, columns=columns)
frame['date'] = frame['date'].apply(_get_frame_time_from_offset) frame['date'] = frame['date'].apply(_get_frame_time_from_offset)
# Ensure floats are in place # Ensure floats are in place
for column in ['open', 'high', 'low', 'close', 'volume']: for column in ['open', 'high', 'low', 'close', 'volume']:
frame[column] = frame[column].astype('float64') frame[column] = frame[column].astype('float64')
if 'buy_tag' not in columns: if 'enter_tag' not in columns:
frame['buy_tag'] = None frame['enter_tag'] = None
return frame return frame

View File

@ -519,12 +519,12 @@ tc32 = BTContainer(data=[
# Test 33: trailing_stop should be triggered immediately on trade open candle. # Test 33: trailing_stop should be triggered immediately on trade open candle.
# stop-loss: 1%, ROI: 10% (should not apply) # stop-loss: 1%, ROI: 10% (should not apply)
tc33 = BTContainer(data=[ tc33 = BTContainer(data=[
# D O H L C V B S BT # D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0, 'buy_signal_01'], [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0, 'buy_signal_01'],
[1, 5000, 5500, 5000, 4900, 6172, 0, 0, None], # enter trade (signal on last candle) and stop [1, 5000, 5500, 5000, 4900, 6172, 0, 0, 0, 0, None], # enter trade and stop
[2, 4900, 5250, 4500, 5100, 6172, 0, 0, None], [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0, None],
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, None], [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0, None],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0, None]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0, None]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01, use_custom_stoploss=True, trailing_stop_positive=0.01, use_custom_stoploss=True,
@ -532,7 +532,7 @@ tc33 = BTContainer(data=[
sell_reason=SellType.TRAILING_STOP_LOSS, sell_reason=SellType.TRAILING_STOP_LOSS,
open_tick=1, open_tick=1,
close_tick=1, close_tick=1,
buy_tag='buy_signal_01' enter_tag='buy_signal_01'
)] )]
) )
@ -571,6 +571,7 @@ TESTS = [
tc31, tc31,
tc32, tc32,
tc33, tc33,
# TODO-lev: Add tests for short here
] ]
@ -597,8 +598,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.required_startup = 0 backtesting.required_startup = 0
backtesting.strategy.advise_buy = lambda a, m: frame backtesting.strategy.advise_entry = lambda a, m: frame
backtesting.strategy.advise_sell = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -620,6 +621,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
for c, trade in enumerate(data.trades): for c, trade in enumerate(data.trades):
res = results.iloc[c] res = results.iloc[c]
assert res.sell_reason == trade.sell_reason.value assert res.sell_reason == trade.sell_reason.value
assert res.buy_tag == trade.buy_tag assert res.buy_tag == trade.enter_tag
assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick)

View File

@ -22,7 +22,7 @@ from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade from freqtrade.persistence import LocalTrade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
@ -123,12 +123,14 @@ def _trend(signals, buy_value, sell_value):
n = len(signals['low']) n = len(signals['low'])
buy = np.zeros(n) buy = np.zeros(n)
sell = np.zeros(n) sell = np.zeros(n)
for i in range(0, len(signals['buy'])): for i in range(0, len(signals['date'])):
if random.random() > 0.5: # Both buy and sell signals at same timeframe if random.random() > 0.5: # Both buy and sell signals at same timeframe
buy[i] = buy_value buy[i] = buy_value
sell[i] = sell_value sell[i] = sell_value
signals['buy'] = buy signals['enter_long'] = buy
signals['sell'] = sell signals['exit_long'] = sell
signals['enter_short'] = 0
signals['exit_short'] = 0
return signals return signals
@ -143,8 +145,10 @@ def _trend_alternate(dataframe=None, metadata=None):
buy[i] = 1 buy[i] = 1
else: else:
sell[i] = 1 sell[i] = 1
signals['buy'] = buy signals['enter_long'] = buy
signals['sell'] = sell signals['exit_long'] = sell
signals['enter_short'] = 0
signals['exit_short'] = 0
return dataframe return dataframe
@ -155,7 +159,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--export', 'none' '--export', 'none'
] ]
@ -190,7 +194,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--timeframe', '1m', '--timeframe', '1m',
'--enable-position-stacking', '--enable-position-stacking',
@ -240,7 +244,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog)
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '2' '--starting-balance', '2'
] ]
@ -251,7 +255,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog)
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '0.5' '--starting-balance', '0.5'
] ]
@ -269,7 +273,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
] ]
pargs = get_args(args) pargs = get_args(args)
start_backtesting(pargs) start_backtesting(pargs)
@ -291,8 +295,8 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.timeframe == '5m' assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.advise_all_indicators) assert callable(backtesting.strategy.advise_all_indicators)
assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_entry)
assert callable(backtesting.strategy.advise_sell) assert callable(backtesting.strategy.advise_exit)
assert isinstance(backtesting.strategy.dp, DataProvider) assert isinstance(backtesting.strategy.dp, DataProvider)
get_fee.assert_called() get_fee.assert_called()
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
@ -302,7 +306,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
del default_conf['timeframe'] del default_conf['timeframe']
default_conf['strategy_list'] = ['StrategyTestV2', default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY,
'SampleStrategy'] 'SampleStrategy']
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
@ -340,7 +344,6 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
assert len(processed['UNITTEST/BTC']) == 102 assert len(processed['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same # Load strategy to compare the result between Backtesting function and strategy are the same
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
processed2 = strategy.advise_all_indicators(data) processed2 = strategy.advise_all_indicators(data)
@ -482,7 +485,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf) Backtesting(default_conf)
# Multiple strategies # Multiple strategies
default_conf['strategy_list'] = ['StrategyTestV2', 'TestStrategyLegacyV1'] default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1']
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match='PrecisionFilter not allowed for backtesting multiple strategies.'): match='PrecisionFilter not allowed for backtesting multiple strategies.'):
Backtesting(default_conf) Backtesting(default_conf)
@ -508,41 +511,47 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
0.0012, # High 0.0012, # High
'', # Buy Signal Name '', # Buy Signal Name
] ]
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert isinstance(trade, LocalTrade) assert isinstance(trade, LocalTrade)
assert trade.stake_amount == 495 assert trade.stake_amount == 495
# Fake 2 trades, so there's not enough amount for the next trade left. # Fake 2 trades, so there's not enough amount for the next trade left.
LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade)
LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade)
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert trade is None assert trade is None
LocalTrade.trades_open.pop() LocalTrade.trades_open.pop()
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert trade is not None assert trade is not None
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5 backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert trade assert trade
assert trade.stake_amount == 123.5 assert trade.stake_amount == 123.5
# In case of error - use proposed stake # In case of error - use proposed stake
backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert trade assert trade
assert trade.stake_amount == 495 assert trade.stake_amount == 495
assert trade.is_short is False
trade = backtesting._enter_trade(pair, row=row, direction='short')
assert trade
assert trade.stake_amount == 495
assert trade.is_short is True
# Stake-amount too high! # Stake-amount too high!
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert trade is None assert trade is None
# Stake-amount throwing error # Stake-amount throwing error
mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount",
side_effect=DependencyException) side_effect=DependencyException)
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert trade is None assert trade is None
backtesting.cleanup() backtesting.cleanup()
@ -560,47 +569,54 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
pair = 'UNITTEST/BTC' pair = 'UNITTEST/BTC'
row = [ row = [
pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=55, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=55, tzinfo=timezone.utc),
1, # Buy
200, # Open 200, # Open
201, # Close
0, # Sell
195, # Low
201.5, # High 201.5, # High
'', # Buy Signal Name 195, # Low
201, # Close
1, # enter_long
0, # exit_long
0, # enter_short
0, # exit_hsort
'', # Long Signal Name
'', # Short Signal Name
] ]
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert isinstance(trade, LocalTrade) assert isinstance(trade, LocalTrade)
row_sell = [ row_sell = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
0, # Buy
200, # Open 200, # Open
201, # Close
0, # Sell
195, # Low
210.5, # High 210.5, # High
'', # Buy Signal Name 195, # Low
201, # Close
0, # enter_long
0, # exit_long
0, # enter_short
0, # exit_short
'', # long Signal Name
'', # Short Signal Name
] ]
row_detail = pd.DataFrame( row_detail = pd.DataFrame(
[ [
[ [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc),
1, 200, 199, 0, 197, 200.1, '', 200, 200.1, 197, 199, 1, 0, 0, 0, '', '',
], [ ], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc),
0, 199, 199.5, 0, 199, 199.7, '', 199, 199.7, 199, 199.5, 0, 0, 0, 0, '', ''
], [ ], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc),
0, 199.5, 200.5, 0, 199, 200.8, '', 199.5, 200.8, 199, 200.9, 0, 0, 0, 0, '', ''
], [ ], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc),
0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?) 200.5, 210.5, 193, 210.5, 0, 0, 0, 0, '', '' # ROI sell (?)
], [ ], [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc),
0, 200, 199, 0, 193, 200.1, '', 200, 200.1, 193, 199, 0, 0, 0, 0, '', ''
], ],
], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"] ], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'long_tag', 'short_tag']
) )
# No data available. # No data available.
@ -610,11 +626,12 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
# Enter new trade # Enter new trade
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row, direction='long')
assert isinstance(trade, LocalTrade) assert isinstance(trade, LocalTrade)
# Assign empty ... no result. # Assign empty ... no result.
backtesting.detail_data[pair] = pd.DataFrame( backtesting.detail_data[pair] = pd.DataFrame(
[], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]) [], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'long_tag', 'short_tag'])
res = backtesting._get_sell_trade_entry(trade, row) res = backtesting._get_sell_trade_entry(trade, row)
assert res is None assert res is None
@ -785,7 +802,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
# Override the default buy trend function in our StrategyTestV2 # Override the default buy trend function in our StrategyTest
def fun(dataframe=None, pair=None): def fun(dataframe=None, pair=None):
buy_value = 1 buy_value = 1
sell_value = 1 sell_value = 1
@ -794,14 +811,14 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_entry = fun # Override
backtesting.strategy.advise_sell = fun # Override backtesting.strategy.advise_exit = fun # Override
result = backtesting.backtest(**backtest_conf) result = backtesting.backtest(**backtest_conf)
assert result['results'].empty assert result['results'].empty
def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_only_sell(mocker, default_conf, testdatadir):
# Override the default buy trend function in our StrategyTestV2 # Override the default buy trend function in our StrategyTest
def fun(dataframe=None, pair=None): def fun(dataframe=None, pair=None):
buy_value = 0 buy_value = 0
sell_value = 1 sell_value = 1
@ -810,8 +827,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_entry = fun # Override
backtesting.strategy.advise_sell = fun # Override backtesting.strategy.advise_exit = fun # Override
result = backtesting.backtest(**backtest_conf) result = backtesting.backtest(**backtest_conf)
assert result['results'].empty assert result['results'].empty
@ -825,8 +842,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.required_startup = 0 backtesting.required_startup = 0
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_entry = _trend_alternate # Override
backtesting.strategy.advise_sell = _trend_alternate # Override backtesting.strategy.advise_exit = _trend_alternate # Override
result = backtesting.backtest(**backtest_conf) result = backtesting.backtest(**backtest_conf)
# 200 candles in backtest data # 200 candles in backtest data
# won't buy on first (shifted by 1) # won't buy on first (shifted by 1)
@ -857,8 +874,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
multi = 20 multi = 20
else: else:
multi = 18 multi = 18
dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) dataframe['enter_long'] = np.where(dataframe.index % multi == 0, 1, 0)
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) dataframe['exit_long'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
dataframe['enter_short'] = 0
dataframe['exit_short'] = 0
return dataframe return dataframe
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
@ -877,8 +896,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_entry = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override backtesting.strategy.advise_exit = _trend_alternate_hold # Override
processed = backtesting.strategy.advise_all_indicators(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
@ -928,7 +947,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
args = [ args = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--datadir', str(testdatadir), '--datadir', str(testdatadir),
'--timeframe', '1m', '--timeframe', '1m',
'--timerange', '1510694220-1510700340', '--timerange', '1510694220-1510700340',
@ -999,7 +1018,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--strategy-list', '--strategy-list',
'StrategyTestV2', CURRENT_TEST_STRATEGY,
'TestStrategyLegacyV1', 'TestStrategyLegacyV1',
] ]
args = get_args(args) args = get_args(args)
@ -1022,7 +1041,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days).', 'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...', 'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy StrategyTestV2', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
'Running backtesting for Strategy TestStrategyLegacyV1', 'Running backtesting for Strategy TestStrategyLegacyV1',
] ]
@ -1103,7 +1122,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--strategy-list', '--strategy-list',
'StrategyTestV2', CURRENT_TEST_STRATEGY,
'TestStrategyLegacyV1', 'TestStrategyLegacyV1',
] ]
args = get_args(args) args = get_args(args)
@ -1120,7 +1139,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'Backtesting with data from 2017-11-14 21:17:00 ' 'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:58:00 (0 days).', 'up to 2017-11-14 22:58:00 (0 days).',
'Parameter --enable-position-stacking detected ...', 'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy StrategyTestV2', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
'Running backtesting for Strategy TestStrategyLegacyV1', 'Running backtesting for Strategy TestStrategyLegacyV1',
] ]
@ -1208,7 +1227,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'--timeframe', '5m', '--timeframe', '5m',
'--timeframe-detail', '1m', '--timeframe-detail', '1m',
'--strategy-list', '--strategy-list',
'StrategyTestV2' CURRENT_TEST_STRATEGY
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
@ -1222,7 +1241,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'up to 2019-10-13 11:10:00 (2 days).', 'up to 2019-10-13 11:10:00 (2 days).',
'Backtesting with data from 2019-10-11 01:40:00 ' 'Backtesting with data from 2019-10-11 01:40:00 '
'up to 2019-10-13 11:10:00 (2 days).', 'up to 2019-10-13 11:10:00 (2 days).',
'Running backtesting for Strategy StrategyTestV2', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
] ]
for line in exists: for line in exists:

View File

@ -6,7 +6,7 @@ from unittest.mock import MagicMock
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.optimize.edge_cli import EdgeCli
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
@ -16,7 +16,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca
args = [ args = [
'edge', 'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
] ]
config = setup_optimize_configuration(get_args(args), RunMode.EDGE) config = setup_optimize_configuration(get_args(args), RunMode.EDGE)
@ -46,7 +46,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
args = [ args = [
'edge', 'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--timeframe', '1m', '--timeframe', '1m',
'--timerange', ':100', '--timerange', ':100',
@ -80,7 +80,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
args = [ args = [
'edge', 'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
] ]
pargs = get_args(args) pargs = get_args(args)
start_edge(pargs) start_edge(pargs)

View File

@ -18,10 +18,13 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.optimize.space import SKDecimal from freqtrade.optimize.space import SKDecimal
from freqtrade.strategy.hyper import IntParameter from freqtrade.strategy.hyper import IntParameter
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
# TODO-lev: This file
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -122,7 +125,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
args = [ args = [
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--stake-amount', '1', '--stake-amount', '1',
'--starting-balance', '0.5' '--starting-balance', '0.5'
] ]
@ -315,8 +318,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
# Should be called for historical candle data # Should be called for historical candle data
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -695,8 +698,8 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -769,8 +772,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
assert dumper.called assert dumper.called
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")
@ -818,8 +821,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
assert dumper.called assert dumper.called
assert dumper.call_count == 1 assert dumper.call_count == 1
assert dumper2.call_count == 1 assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades") assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking") assert hasattr(hyperopt, "position_stacking")

View File

@ -10,7 +10,7 @@ import rapidjson
from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.constants import FTHYPT_FILEVERSION
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from tests.conftest import log_has from tests.conftest import CURRENT_TEST_STRATEGY, log_has
# Functions for recurrent object patching # Functions for recurrent object patching
@ -167,9 +167,9 @@ def test__pprint_dict():
def test_get_strategy_filename(default_conf): def test_get_strategy_filename(default_conf):
x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV2') x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV3')
assert isinstance(x, Path) assert isinstance(x, Path)
assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v2.py' assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py'
x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy') x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy')
assert x is None assert x is None
@ -177,7 +177,7 @@ def test_get_strategy_filename(default_conf):
def test_export_params(tmpdir): def test_export_params(tmpdir):
filename = Path(tmpdir) / "StrategyTestV2.json" filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json"
assert not filename.is_file() assert not filename.is_file()
params = { params = {
"params_details": { "params_details": {
@ -205,12 +205,12 @@ def test_export_params(tmpdir):
} }
} }
HyperoptTools.export_params(params, "StrategyTestV2", filename) HyperoptTools.export_params(params, CURRENT_TEST_STRATEGY, filename)
assert filename.is_file() assert filename.is_file()
content = rapidjson.load(filename.open('r')) content = rapidjson.load(filename.open('r'))
assert content['strategy_name'] == 'StrategyTestV2' assert content['strategy_name'] == CURRENT_TEST_STRATEGY
assert 'params' in content assert 'params' in content
assert "buy" in content["params"] assert "buy" in content["params"]
assert "sell" in content["params"] assert "sell" in content["params"]
@ -223,7 +223,7 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker):
default_conf['disableparamexport'] = False default_conf['disableparamexport'] = False
export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params")
filename = Path(tmpdir) / "StrategyTestV2.json" filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json"
assert not filename.is_file() assert not filename.is_file()
params = { params = {
"params_details": { "params_details": {
@ -252,17 +252,17 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker):
FTHYPT_FILEVERSION: 2, FTHYPT_FILEVERSION: 2,
} }
HyperoptTools.try_export_params(default_conf, "StrategyTestV222", params) HyperoptTools.try_export_params(default_conf, "StrategyTestVXXX", params)
assert log_has("Strategy not found, not exporting parameter file.", caplog) assert log_has("Strategy not found, not exporting parameter file.", caplog)
assert export_mock.call_count == 0 assert export_mock.call_count == 0
caplog.clear() caplog.clear()
HyperoptTools.try_export_params(default_conf, "StrategyTestV2", params) HyperoptTools.try_export_params(default_conf, CURRENT_TEST_STRATEGY, params)
assert export_mock.call_count == 1 assert export_mock.call_count == 1
assert export_mock.call_args_list[0][0][1] == 'StrategyTestV2' assert export_mock.call_args_list[0][0][1] == CURRENT_TEST_STRATEGY
assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v2.json' assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v3.json'
def test_params_print(capsys): def test_params_print(capsys):

View File

@ -21,6 +21,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera
text_table_bt_results, text_table_sell_reason, text_table_bt_results, text_table_sell_reason,
text_table_strategy) text_table_strategy)
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
from tests.conftest import CURRENT_TEST_STRATEGY
from tests.data.test_history import _backup_file, _clean_test_file from tests.data.test_history import _backup_file, _clean_test_file
@ -52,7 +53,7 @@ def test_text_table_bt_results():
def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
default_conf.update({'strategy': 'StrategyTestV2'}) default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
results = {'DefStrat': { results = {'DefStrat': {

View File

@ -24,8 +24,8 @@ from freqtrade.rpc import RPC
from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server import ApiServer
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro,
log_has_re, patch_get_signal) get_patched_freqtradebot, log_has, log_has_re, patch_get_signal)
BASE_URI = "/api/v1" BASE_URI = "/api/v1"
@ -885,7 +885,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'open_trade_value': 15.1668225, 'open_trade_value': 15.1668225,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None, 'sell_order_status': None,
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'buy_tag': None, 'buy_tag': None,
'timeframe': 5, 'timeframe': 5,
'exchange': 'binance', 'exchange': 'binance',
@ -990,7 +990,7 @@ def test_api_forcebuy(botclient, mocker, fee):
close_rate=0.265441, close_rate=0.265441,
id=22, id=22,
timeframe=5, timeframe=5,
strategy="StrategyTestV2" strategy=CURRENT_TEST_STRATEGY
)) ))
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
@ -1040,7 +1040,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'open_trade_value': 0.24605460, 'open_trade_value': 0.24605460,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None, 'sell_order_status': None,
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'buy_tag': None, 'buy_tag': None,
'timeframe': 5, 'timeframe': 5,
'exchange': 'binance', 'exchange': 'binance',
@ -1107,7 +1107,7 @@ def test_api_pair_candles(botclient, ohlcv_history):
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
assert_response(rc) assert_response(rc)
assert 'strategy' in rc.json() assert 'strategy' in rc.json()
assert rc.json()['strategy'] == 'StrategyTestV2' assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY
assert 'columns' in rc.json() assert 'columns' in rc.json()
assert 'data_start_ts' in rc.json() assert 'data_start_ts' in rc.json()
assert 'data_start' in rc.json() assert 'data_start' in rc.json()
@ -1145,19 +1145,19 @@ def test_api_pair_history(botclient, ohlcv_history):
# No pair # No pair
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?timeframe={timeframe}" f"{BASE_URI}/pair_history?timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=StrategyTestV2") f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 422) assert_response(rc, 422)
# No Timeframe # No Timeframe
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC"
"&timerange=20180111-20180112&strategy=StrategyTestV2") f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 422) assert_response(rc, 422)
# No timerange # No timerange
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&strategy=StrategyTestV2") f"&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 422) assert_response(rc, 422)
# No strategy # No strategy
@ -1169,14 +1169,14 @@ def test_api_pair_history(botclient, ohlcv_history):
# Working # Working
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20180111-20180112&strategy=StrategyTestV2") f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 200) assert_response(rc, 200)
assert rc.json()['length'] == 289 assert rc.json()['length'] == 289
assert len(rc.json()['data']) == rc.json()['length'] assert len(rc.json()['data']) == rc.json()['length']
assert 'columns' in rc.json() assert 'columns' in rc.json()
assert 'data' in rc.json() assert 'data' in rc.json()
assert rc.json()['pair'] == 'UNITTEST/BTC' assert rc.json()['pair'] == 'UNITTEST/BTC'
assert rc.json()['strategy'] == 'StrategyTestV2' assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY
assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00'
assert rc.json()['data_start_ts'] == 1515628800000 assert rc.json()['data_start_ts'] == 1515628800000
assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00'
@ -1185,7 +1185,7 @@ def test_api_pair_history(botclient, ohlcv_history):
# No data found # No data found
rc = client_get(client, rc = client_get(client,
f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}"
"&timerange=20200111-20200112&strategy=StrategyTestV2") f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}")
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json()['error'] == ("Error querying /api/v1/pair_history: " assert rc.json()['error'] == ("Error querying /api/v1/pair_history: "
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
@ -1226,19 +1226,20 @@ def test_api_strategies(botclient):
'HyperoptableStrategy', 'HyperoptableStrategy',
'InformativeDecoratorTest', 'InformativeDecoratorTest',
'StrategyTestV2', 'StrategyTestV2',
'TestStrategyLegacyV1' 'StrategyTestV3',
'TestStrategyLegacyV1',
]} ]}
def test_api_strategy(botclient): def test_api_strategy(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/strategy/StrategyTestV2") rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}")
assert_response(rc) assert_response(rc)
assert rc.json()['strategy'] == 'StrategyTestV2' assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY
data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v2.py").read_text() data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text()
assert rc.json()['code'] == data assert rc.json()['code'] == data
rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") rc = client_get(client, f"{BASE_URI}/strategy/NoStrat")
@ -1295,7 +1296,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog):
# start backtesting # start backtesting
data = { data = {
"strategy": "StrategyTestV2", "strategy": CURRENT_TEST_STRATEGY,
"timeframe": "5m", "timeframe": "5m",
"timerange": "20180110-20180111", "timerange": "20180110-20180111",
"max_open_trades": 3, "max_open_trades": 3,

View File

@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
patch_exchange, patch_get_signal, patch_whitelist) log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
class DummyCls(Telegram): class DummyCls(Telegram):
@ -1238,7 +1238,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0]
assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
@ -1247,7 +1247,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0]
assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]

View File

@ -2,8 +2,7 @@
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy import informative, merge_informative_pair from freqtrade.strategy import IStrategy, informative, merge_informative_pair
from freqtrade.strategy.interface import IStrategy
class InformativeDecoratorTest(IStrategy): class InformativeDecoratorTest(IStrategy):

View File

@ -4,7 +4,7 @@
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import IStrategy
# -------------------------------- # --------------------------------

View File

@ -4,7 +4,7 @@ import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import IStrategy
class StrategyTestV2(IStrategy): class StrategyTestV2(IStrategy):

View File

@ -0,0 +1,181 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from datetime import datetime
import talib.abstract as ta
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
RealParameter)
class StrategyTestV3(IStrategy):
"""
Strategy used by tests freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
"""
INTERFACE_VERSION = 3
# Minimal ROI designed for the strategy
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
stoploss = -0.10
# Optimal timeframe for the strategy
timeframe = '5m'
# Optional order type mapping
order_types = {
'buy': 'limit',
'sell': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False
}
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 20
# Optional time in force for orders
order_time_in_force = {
'buy': 'gtc',
'sell': 'gtc',
}
buy_params = {
'buy_rsi': 35,
# Intentionally not specified, so "default" is tested
# 'buy_plusdi': 0.4
}
sell_params = {
'sell_rsi': 74,
'sell_minusdi': 0.4
}
buy_rsi = IntParameter([0, 50], default=30, space='buy')
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
load=False)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO-lev: Can we make this work with protection tests?
# TODO-lev: (Would replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
def informative_pairs(self):
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Minus Directional Indicator / Movement
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] < self.buy_rsi.value) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > self.buy_plusdi.value)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > self.buy_plusdi.value)
),
'enter_long'] = 1
dataframe.loc[
(
qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value)
),
'enter_short'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > self.sell_minusdi.value)
),
'exit_long'] = 1
dataframe.loc[
(
qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)
),
'exit_short'] = 1
# TODO-lev: Add short logic
return dataframe
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
# Return 3.0 in all cases.
# Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly.
return 3.0

View File

@ -4,20 +4,20 @@ from pandas import DataFrame
from freqtrade.persistence.models import Trade from freqtrade.persistence.models import Trade
from .strats.strategy_test_v2 import StrategyTestV2 from .strats.strategy_test_v3 import StrategyTestV3
def test_strategy_test_v2_structure(): def test_strategy_test_v2_structure():
assert hasattr(StrategyTestV2, 'minimal_roi') assert hasattr(StrategyTestV3, 'minimal_roi')
assert hasattr(StrategyTestV2, 'stoploss') assert hasattr(StrategyTestV3, 'stoploss')
assert hasattr(StrategyTestV2, 'timeframe') assert hasattr(StrategyTestV3, 'timeframe')
assert hasattr(StrategyTestV2, 'populate_indicators') assert hasattr(StrategyTestV3, 'populate_indicators')
assert hasattr(StrategyTestV2, 'populate_buy_trend') assert hasattr(StrategyTestV3, 'populate_buy_trend')
assert hasattr(StrategyTestV2, 'populate_sell_trend') assert hasattr(StrategyTestV3, 'populate_sell_trend')
def test_strategy_test_v2(result, fee): def test_strategy_test_v2(result, fee):
strategy = StrategyTestV2({}) strategy = StrategyTestV3({})
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
assert type(strategy.minimal_roi) is dict assert type(strategy.minimal_roi) is dict
@ -37,10 +37,11 @@ def test_strategy_test_v2(result, fee):
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', rate=20000, time_in_force='gtc',
current_time=datetime.utcnow()) is True current_time=datetime.utcnow(), side='long') is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi', rate=20000, time_in_force='gtc', sell_reason='roi',
current_time=datetime.utcnow()) is True current_time=datetime.utcnow()) is True
# TODO-lev: Test for shorts?
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
current_rate=20_000, current_profit=0.05) == strategy.stoploss current_rate=20_000, current_profit=0.05) == strategy.stoploss

View File

@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.enums import SellType from freqtrade.enums import SellType
from freqtrade.enums.signaltype import SignalDirection
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.optimize.space import SKDecimal from freqtrade.optimize.space import SKDecimal
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
@ -20,38 +21,68 @@ from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, Categoric
DecimalParameter, IntParameter, RealParameter) DecimalParameter, IntParameter, RealParameter)
from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import log_has, log_has_re from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
from .strats.strategy_test_v2 import StrategyTestV2 from .strats.strategy_test_v3 import StrategyTestV3
# Avoid to reinit the same object again and again # Avoid to reinit the same object again and again
_STRATEGY = StrategyTestV2(config={}) _STRATEGY = StrategyTestV3(config={})
_STRATEGY.dp = DataProvider({}, None, None) _STRATEGY.dp = DataProvider({}, None, None)
def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_returns_latest_signal(ohlcv_history):
ohlcv_history.loc[1, 'date'] = arrow.utcnow() ohlcv_history.loc[1, 'date'] = arrow.utcnow()
# Take a copy to correctly modify the call # Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy() mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0 mocked_history['enter_long'] = 0
mocked_history['buy'] = 0 mocked_history['exit_long'] = 0
mocked_history.loc[1, 'sell'] = 1 mocked_history['enter_short'] = 0
mocked_history['exit_short'] = 0
mocked_history.loc[1, 'exit_long'] = 1
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None)
mocked_history.loc[1, 'sell'] = 0 assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, True)
mocked_history.loc[1, 'buy'] = 1 assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
mocked_history.loc[1, 'exit_long'] = 0
mocked_history.loc[1, 'enter_long'] = 1
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history
mocked_history.loc[1, 'sell'] = 0 ) == (SignalDirection.LONG, None)
mocked_history.loc[1, 'buy'] = 0 assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
mocked_history.loc[1, 'exit_long'] = 0
mocked_history.loc[1, 'enter_long'] = 0
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None)
mocked_history.loc[1, 'sell'] = 0 assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False)
mocked_history.loc[1, 'buy'] = 1 assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' mocked_history.loc[1, 'exit_long'] = 0
mocked_history.loc[1, 'enter_long'] = 1
mocked_history.loc[1, 'enter_tag'] = 'buy_signal_01'
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') assert _STRATEGY.get_entry_signal(
'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01')
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
mocked_history.loc[1, 'exit_long'] = 0
mocked_history.loc[1, 'enter_long'] = 0
mocked_history.loc[1, 'enter_short'] = 1
mocked_history.loc[1, 'exit_short'] = 0
mocked_history.loc[1, 'enter_tag'] = 'sell_signal_01'
assert _STRATEGY.get_entry_signal(
'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, 'sell_signal_01')
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (True, False)
mocked_history.loc[1, 'enter_short'] = 0
mocked_history.loc[1, 'exit_short'] = 1
assert _STRATEGY.get_entry_signal(
'ETH/BTC', '5m', mocked_history) == (None, None)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False)
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, True)
def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
@ -67,18 +98,18 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
assert log_has('Empty dataframe for pair ETH/BTC', caplog) assert log_has('Empty dataframe for pair ETH/BTC', caplog)
def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_empty(default_conf, caplog):
assert (False, False, None) == _STRATEGY.get_signal( assert (None, None) == _STRATEGY.get_latest_candle(
'foo', default_conf['timeframe'], DataFrame() 'foo', default_conf['timeframe'], DataFrame()
) )
assert log_has('Empty candle (OHLCV) data for pair foo', caplog) assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear() caplog.clear()
assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) assert (None, None) == _STRATEGY.get_latest_candle('bar', default_conf['timeframe'], None)
assert log_has('Empty candle (OHLCV) data for pair bar', caplog) assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
caplog.clear() caplog.clear()
assert (False, False, None) == _STRATEGY.get_signal( assert (None, None) == _STRATEGY.get_latest_candle(
'baz', 'baz',
default_conf['timeframe'], default_conf['timeframe'],
DataFrame([]) DataFrame([])
@ -86,7 +117,7 @@ def test_get_signal_empty(default_conf, mocker, caplog):
assert log_has('Empty candle (OHLCV) data for pair baz', caplog) assert log_has('Empty candle (OHLCV) data for pair baz', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_exception_valueerror(mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
mocker.patch.object( mocker.patch.object(
@ -111,14 +142,14 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
# Take a copy to correctly modify the call # Take a copy to correctly modify the call
mocked_history = ohlcv_history.copy() mocked_history = ohlcv_history.copy()
mocked_history['sell'] = 0 mocked_history['exit_long'] = 0
mocked_history['buy'] = 0 mocked_history['enter_long'] = 0
mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'enter_long'] = 1
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, 'assert_df') mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False, None) == _STRATEGY.get_signal( assert (None, None) == _STRATEGY.get_latest_candle(
'xyz', 'xyz',
default_conf['timeframe'], default_conf['timeframe'],
mocked_history mocked_history
@ -134,13 +165,13 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
mocked_history = ohlcv_history.copy() mocked_history = ohlcv_history.copy()
# Intentionally don't set sell column # Intentionally don't set sell column
# mocked_history['sell'] = 0 # mocked_history['sell'] = 0
mocked_history['buy'] = 0 mocked_history['enter_long'] = 0
mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'enter_long'] = 1
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, 'assert_df') mocker.patch.object(_STRATEGY, 'assert_df')
assert (True, False, None) == _STRATEGY.get_signal( assert (SignalDirection.LONG, None) == _STRATEGY.get_entry_signal(
'xyz', 'xyz',
default_conf['timeframe'], default_conf['timeframe'],
mocked_history mocked_history
@ -148,7 +179,6 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
def test_ignore_expired_candle(default_conf): def test_ignore_expired_candle(default_conf):
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.ignore_buying_expired_candle_after = 60 strategy.ignore_buying_expired_candle_after = 60
@ -195,8 +225,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history):
def test_assert_df(ohlcv_history, caplog): def test_assert_df(ohlcv_history, caplog):
df_len = len(ohlcv_history) - 1 df_len = len(ohlcv_history) - 1
ohlcv_history.loc[:, 'buy'] = 0 ohlcv_history.loc[:, 'enter_long'] = 0
ohlcv_history.loc[:, 'sell'] = 0 ohlcv_history.loc[:, 'exit_long'] = 0
# Ensure it's running when passed correctly # Ensure it's running when passed correctly
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date'])
@ -219,8 +249,8 @@ def test_assert_df(ohlcv_history, caplog):
_STRATEGY.assert_df(None, len(ohlcv_history), _STRATEGY.assert_df(None, len(ohlcv_history),
ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date'])
with pytest.raises(StrategyError, with pytest.raises(StrategyError,
match="Buy column not set"): match="enter_long/buy column not set."):
_STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history.drop('enter_long', axis=1), len(ohlcv_history),
ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date'])
_STRATEGY.disable_dataframe_checks = True _STRATEGY.disable_dataframe_checks = True
@ -233,7 +263,6 @@ def test_assert_df(ohlcv_history, caplog):
def test_advise_all_indicators(default_conf, testdatadir) -> None: def test_advise_all_indicators(default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
@ -244,7 +273,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None:
def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators')
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
@ -262,7 +290,6 @@ def test_min_roi_reached(default_conf, fee) -> None:
min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1}, min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1},
{0: 0.1, 20: 0.05, 55: 0.01}] {0: 0.1, 20: 0.05, 55: 0.01}]
for roi in min_roi_list: for roi in min_roi_list:
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = roi strategy.minimal_roi = roi
trade = Trade( trade = Trade(
@ -301,7 +328,6 @@ def test_min_roi_reached2(default_conf, fee) -> None:
}, },
] ]
for roi in min_roi_list: for roi in min_roi_list:
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = roi strategy.minimal_roi = roi
trade = Trade( trade = Trade(
@ -336,7 +362,6 @@ def test_min_roi_reached3(default_conf, fee) -> None:
30: 0.05, 30: 0.05,
55: 0.30, 55: 0.30,
} }
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = min_roi strategy.minimal_roi = min_roi
trade = Trade( trade = Trade(
@ -389,8 +414,6 @@ def test_min_roi_reached3(default_conf, fee) -> None:
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom,
profit2, adjusted2, expected2, custom_stop) -> None: profit2, adjusted2, expected2, custom_stop) -> None:
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
@ -437,8 +460,6 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
def test_custom_sell(default_conf, fee, caplog) -> None: def test_custom_sell(default_conf, fee, caplog) -> None:
default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade( trade = Trade(
pair='ETH/BTC', pair='ETH/BTC',
@ -452,50 +473,84 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
) )
now = arrow.utcnow().datetime now = arrow.utcnow().datetime
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False,
low=None, high=None)
assert res.sell_flag is False assert res.sell_flag is False
assert res.sell_type == SellType.NONE assert res.sell_type == SellType.NONE
strategy.custom_sell = MagicMock(return_value=True) strategy.custom_sell = MagicMock(return_value=True)
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False,
low=None, high=None)
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_reason == 'custom_sell' assert res.sell_reason == 'custom_sell'
strategy.custom_sell = MagicMock(return_value='hello world') strategy.custom_sell = MagicMock(return_value='hello world')
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False,
low=None, high=None)
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_reason == 'hello world' assert res.sell_reason == 'hello world'
caplog.clear() caplog.clear()
strategy.custom_sell = MagicMock(return_value='h' * 100) strategy.custom_sell = MagicMock(return_value='h' * 100)
res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False,
low=None, high=None)
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_reason == 'h' * 64 assert res.sell_reason == 'h' * 64
assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog)
@pytest.mark.parametrize('side', TRADE_SIDES)
def test_leverage_callback(default_conf, side) -> None:
default_conf['strategy'] = 'StrategyTestV2'
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.leverage(
pair='XRP/USDT',
current_time=datetime.now(timezone.utc),
current_rate=2.2,
proposed_leverage=1.0,
max_leverage=5.0,
side=side,
) == 1
default_conf['strategy'] = CURRENT_TEST_STRATEGY
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.leverage(
pair='XRP/USDT',
current_time=datetime.now(timezone.utc),
current_rate=2.2,
proposed_leverage=1.0,
max_leverage=5.0,
side=side,
) == 3
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) entry_mock = MagicMock(side_effect=lambda x, meta: x)
sell_mock = MagicMock(side_effect=lambda x, meta: x) exit_mock = MagicMock(side_effect=lambda x, meta: x)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy', 'freqtrade.strategy.interface.IStrategy',
advise_indicators=ind_mock, advise_indicators=ind_mock,
advise_buy=buy_mock, advise_entry=entry_mock,
advise_sell=sell_mock, advise_exit=exit_mock,
) )
strategy = StrategyTestV2({}) strategy = StrategyTestV3({})
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert entry_mock.call_count == 1
assert buy_mock.call_count == 1 assert entry_mock.call_count == 1
assert log_has('TA Analysis Launched', caplog) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
@ -504,8 +559,8 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 2 assert ind_mock.call_count == 2
assert buy_mock.call_count == 2 assert entry_mock.call_count == 2
assert buy_mock.call_count == 2 assert entry_mock.call_count == 2
assert log_has('TA Analysis Launched', caplog) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
@ -513,16 +568,16 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) entry_mock = MagicMock(side_effect=lambda x, meta: x)
sell_mock = MagicMock(side_effect=lambda x, meta: x) exit_mock = MagicMock(side_effect=lambda x, meta: x)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy', 'freqtrade.strategy.interface.IStrategy',
advise_indicators=ind_mock, advise_indicators=ind_mock,
advise_buy=buy_mock, advise_entry=entry_mock,
advise_sell=sell_mock, advise_exit=exit_mock,
) )
strategy = StrategyTestV2({}) strategy = StrategyTestV3({})
strategy.dp = DataProvider({}, None, None) strategy.dp = DataProvider({}, None, None)
strategy.process_only_new_candles = True strategy.process_only_new_candles = True
@ -532,8 +587,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
assert 'close' in ret.columns assert 'close' in ret.columns
assert isinstance(ret, DataFrame) assert isinstance(ret, DataFrame)
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert entry_mock.call_count == 1
assert buy_mock.call_count == 1 assert entry_mock.call_count == 1
assert log_has('TA Analysis Launched', caplog) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.clear() caplog.clear()
@ -541,20 +596,19 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert entry_mock.call_count == 1
assert buy_mock.call_count == 1 assert entry_mock.call_count == 1
# only skipped analyze adds buy and sell columns, otherwise it's all mocked # only skipped analyze adds buy and sell columns, otherwise it's all mocked
assert 'buy' in ret.columns assert 'enter_long' in ret.columns
assert 'sell' in ret.columns assert 'exit_long' in ret.columns
assert ret['buy'].sum() == 0 assert ret['enter_long'].sum() == 0
assert ret['sell'].sum() == 0 assert ret['exit_long'].sum() == 0
assert not log_has('TA Analysis Launched', caplog) assert not log_has('TA Analysis Launched', caplog)
assert log_has('Skipping TA Analysis for already analyzed candle', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_is_pair_locked(default_conf): def test_is_pair_locked(default_conf):
default_conf.update({'strategy': 'StrategyTestV2'})
PairLocks.timeframe = default_conf['timeframe'] PairLocks.timeframe = default_conf['timeframe']
PairLocks.use_db = True PairLocks.use_db = True
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)

View File

@ -10,7 +10,7 @@ from pandas import DataFrame
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from tests.conftest import log_has, log_has_re from tests.conftest import CURRENT_TEST_STRATEGY, log_has, log_has_re
def test_search_strategy(): def test_search_strategy():
@ -18,7 +18,7 @@ def test_search_strategy():
s, _ = StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_name='StrategyTestV2', object_name=CURRENT_TEST_STRATEGY,
add_source=True, add_source=True,
) )
assert issubclass(s, IStrategy) assert issubclass(s, IStrategy)
@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed():
directory = Path(__file__).parent / "strats" directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
assert isinstance(strategies, list) assert isinstance(strategies, list)
assert len(strategies) == 4 assert len(strategies) == 5
assert isinstance(strategies[0], dict) assert isinstance(strategies[0], dict)
@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed():
directory = Path(__file__).parent / "strats" directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
assert isinstance(strategies, list) assert isinstance(strategies, list)
assert len(strategies) == 5 assert len(strategies) == 6
# with enum_failed=True search_all_objects() shall find 2 good strategies # with enum_failed=True search_all_objects() shall find 2 good strategies
# and 1 which fails to load # and 1 which fails to load
assert len([x for x in strategies if x['class'] is not None]) == 4 assert len([x for x in strategies if x['class'] is not None]) == 5
assert len([x for x in strategies if x['class'] is None]) == 1 assert len([x for x in strategies if x['class'] is None]) == 1
@ -74,10 +74,10 @@ def test_load_strategy_base64(result, caplog, default_conf):
def test_load_strategy_invalid_directory(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf):
default_conf['strategy'] = 'StrategyTestV2' default_conf['strategy'] = 'StrategyTestV3'
extra_dir = Path.cwd() / 'some/path' extra_dir = Path.cwd() / 'some/path'
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
StrategyResolver._load_strategy('StrategyTestV2', config=default_conf, StrategyResolver._load_strategy(CURRENT_TEST_STRATEGY, config=default_conf,
extra_dir=extra_dir) extra_dir=extra_dir)
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
@ -99,8 +99,10 @@ def test_load_strategy_noname(default_conf):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
def test_strategy(result, default_conf): @pytest.mark.filterwarnings("ignore:deprecated")
default_conf.update({'strategy': 'StrategyTestV2'}) @pytest.mark.parametrize('strategy_name', ['StrategyTestV2', 'TestStrategyLegacyV1'])
def test_strategy_pre_v3(result, default_conf, strategy_name):
default_conf.update({'strategy': strategy_name})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
@ -117,17 +119,19 @@ def test_strategy(result, default_conf):
df_indicators = strategy.advise_indicators(result, metadata=metadata) df_indicators = strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators assert 'adx' in df_indicators
dataframe = strategy.advise_buy(df_indicators, metadata=metadata) dataframe = strategy.advise_entry(df_indicators, metadata=metadata)
assert 'buy' in dataframe.columns assert 'buy' not in dataframe.columns
assert 'enter_long' in dataframe.columns
dataframe = strategy.advise_sell(df_indicators, metadata=metadata) dataframe = strategy.advise_exit(df_indicators, metadata=metadata)
assert 'sell' in dataframe.columns assert 'sell' not in dataframe.columns
assert 'exit_long' in dataframe.columns
def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'minimal_roi': { 'minimal_roi': {
"20": 0.1, "20": 0.1,
"0": 0.5 "0": 0.5
@ -144,7 +148,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf):
def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'stoploss': -0.5 'stoploss': -0.5
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -156,7 +160,7 @@ def test_strategy_override_stoploss(caplog, default_conf):
def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'trailing_stop': True 'trailing_stop': True
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -169,7 +173,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf):
def test_strategy_override_trailing_stop_positive(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'trailing_stop_positive': -0.1, 'trailing_stop_positive': -0.1,
'trailing_stop_positive_offset': -0.2 'trailing_stop_positive_offset': -0.2
@ -189,7 +193,7 @@ def test_strategy_override_timeframe(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'timeframe': 60, 'timeframe': 60,
'stake_currency': 'ETH' 'stake_currency': 'ETH'
}) })
@ -205,7 +209,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'process_only_new_candles': True 'process_only_new_candles': True
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -225,7 +229,7 @@ def test_strategy_override_order_types(caplog, default_conf):
'stoploss_on_exchange': True, 'stoploss_on_exchange': True,
} }
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'order_types': order_types 'order_types': order_types
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -239,12 +243,12 @@ def test_strategy_override_order_types(caplog, default_conf):
" 'stoploss_on_exchange': True}.", caplog) " 'stoploss_on_exchange': True}.", caplog)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'order_types': {'buy': 'market'} 'order_types': {'buy': 'market'}
}) })
# Raise error for invalid configuration # Raise error for invalid configuration
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'StrategyTestV2'. " match=r"Impossible to load Strategy '" + CURRENT_TEST_STRATEGY + "'. "
r"Order-types mapping is incomplete."): r"Order-types mapping is incomplete."):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
@ -258,7 +262,7 @@ def test_strategy_override_order_tif(caplog, default_conf):
} }
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'order_time_in_force': order_time_in_force 'order_time_in_force': order_time_in_force
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -271,20 +275,20 @@ def test_strategy_override_order_tif(caplog, default_conf):
" {'buy': 'fok', 'sell': 'gtc'}.", caplog) " {'buy': 'fok', 'sell': 'gtc'}.", caplog)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'order_time_in_force': {'buy': 'fok'} 'order_time_in_force': {'buy': 'fok'}
}) })
# Raise error for invalid configuration # Raise error for invalid configuration
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'StrategyTestV2'. " match=f"Impossible to load Strategy '{CURRENT_TEST_STRATEGY}'. "
r"Order-time-in-force mapping is incomplete."): "Order-time-in-force mapping is incomplete."):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_signal(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.use_sell_signal assert strategy.use_sell_signal
@ -294,7 +298,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
assert default_conf['use_sell_signal'] assert default_conf['use_sell_signal']
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'use_sell_signal': False, 'use_sell_signal': False,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -307,7 +311,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf):
def test_strategy_override_use_sell_profit_only(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
assert not strategy.sell_profit_only assert not strategy.sell_profit_only
@ -317,7 +321,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf):
assert not default_conf['sell_profit_only'] assert not default_conf['sell_profit_only']
default_conf.update({ default_conf.update({
'strategy': 'StrategyTestV2', 'strategy': CURRENT_TEST_STRATEGY,
'sell_profit_only': True, 'sell_profit_only': True,
}) })
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -345,7 +349,7 @@ def test_deprecate_populate_indicators(result, default_conf):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) strategy.advise_entry(indicators, {'pair': 'ETH/BTC'})
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -354,7 +358,7 @@ def test_deprecate_populate_indicators(result, default_conf):
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) strategy.advise_exit(indicators, {'pair': 'ETH_BTC'})
assert len(w) == 1 assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning) assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \ assert "deprecated - check out the Sample strategy to see the current function headers!" \
@ -362,7 +366,7 @@ def test_deprecate_populate_indicators(result, default_conf):
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): def test_call_deprecated_function(result, default_conf, caplog):
default_location = Path(__file__).parent / "strats" default_location = Path(__file__).parent / "strats"
del default_conf['timeframe'] del default_conf['timeframe']
default_conf.update({'strategy': 'TestStrategyLegacyV1', default_conf.update({'strategy': 'TestStrategyLegacyV1',
@ -382,19 +386,19 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog):
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns assert 'adx' in indicator_df.columns
enterdf = strategy.advise_buy(result, metadata=metadata) enterdf = strategy.advise_entry(result, metadata=metadata)
assert isinstance(enterdf, DataFrame) assert isinstance(enterdf, DataFrame)
assert 'buy' in enterdf.columns assert 'enter_long' in enterdf.columns
exitdf = strategy.advise_sell(result, metadata=metadata) exitdf = strategy.advise_exit(result, metadata=metadata)
assert isinstance(exitdf, DataFrame) assert isinstance(exitdf, DataFrame)
assert 'sell' in exitdf assert 'exit_long' in exitdf
assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.",
caplog) caplog)
def test_strategy_interface_versioning(result, monkeypatch, default_conf): def test_strategy_interface_versioning(result, default_conf):
default_conf.update({'strategy': 'StrategyTestV2'}) default_conf.update({'strategy': 'StrategyTestV2'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
@ -409,10 +413,13 @@ def test_strategy_interface_versioning(result, monkeypatch, default_conf):
assert isinstance(indicator_df, DataFrame) assert isinstance(indicator_df, DataFrame)
assert 'adx' in indicator_df.columns assert 'adx' in indicator_df.columns
enterdf = strategy.advise_buy(result, metadata=metadata) enterdf = strategy.advise_entry(result, metadata=metadata)
assert isinstance(enterdf, DataFrame) assert isinstance(enterdf, DataFrame)
assert 'buy' in enterdf.columns
exitdf = strategy.advise_sell(result, metadata=metadata) assert 'buy' not in enterdf.columns
assert 'enter_long' in enterdf.columns
exitdf = strategy.advise_exit(result, metadata=metadata)
assert isinstance(exitdf, DataFrame) assert isinstance(exitdf, DataFrame)
assert 'sell' in exitdf assert 'sell' not in exitdf
assert 'exit_long' in exitdf

View File

@ -7,6 +7,7 @@ import pytest
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive
from tests.conftest import CURRENT_TEST_STRATEGY
# Parse common command-line-arguments. Used for all tools # Parse common command-line-arguments. Used for all tools
@ -123,7 +124,7 @@ def test_parse_args_backtesting_custom() -> None:
'-c', 'test_conf.json', '-c', 'test_conf.json',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--strategy-list', '--strategy-list',
'StrategyTestV2', CURRENT_TEST_STRATEGY,
'SampleStrategy' 'SampleStrategy'
] ]
call_args = Arguments(args).get_parsed_arg() call_args = Arguments(args).get_parsed_arg()

View File

@ -23,7 +23,8 @@ from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre
from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re,
patched_configuration_load_config_file)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -403,7 +404,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
arglist = [ arglist = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
] ]
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
@ -440,7 +441,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
arglist = [ arglist = [
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'StrategyTestV2', '--strategy', CURRENT_TEST_STRATEGY,
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--userdir', "/tmp/freqtrade", '--userdir', "/tmp/freqtrade",
'--ticker-interval', '1m', '--ticker-interval', '1m',
@ -497,7 +498,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--export', 'trades', '--export', 'trades',
'--strategy-list', '--strategy-list',
'StrategyTestV2', CURRENT_TEST_STRATEGY,
'TestStrategy' 'TestStrategy'
] ]

View File

@ -234,7 +234,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker,
# stoploss shoud be hit # stoploss shoud be hit
assert freqtrade.handle_trade(trade) is not ignore_strat_sl assert freqtrade.handle_trade(trade) is not ignore_strat_sl
if not ignore_strat_sl: if not ignore_strat_sl:
assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value assert trade.sell_reason == SellType.STOP_LOSS.value
@ -427,7 +427,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
) )
default_conf['stake_amount'] = 10 default_conf['stake_amount'] = 10
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade, value=(False, False, None)) patch_get_signal(freqtrade, enter_long=False)
Trade.query = MagicMock() Trade.query = MagicMock()
Trade.query.filter = MagicMock() Trade.query.filter = MagicMock()
@ -648,9 +648,10 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
refresh_latest_ohlcv=refresh_mock, refresh_latest_ohlcv=refresh_mock,
) )
inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")])
mocker.patch( mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy.get_signal', 'freqtrade.strategy.interface.IStrategy',
return_value=(False, False, '') get_exit_signal=MagicMock(return_value=(False, False)),
get_entry_signal=MagicMock(return_value=(None, None))
) )
mocker.patch('time.sleep', return_value=None) mocker.patch('time.sleep', return_value=None)
@ -1802,7 +1803,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi
assert trade.is_open is True assert trade.is_open is True
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.open_order_id == limit_sell_order['id'] assert trade.open_order_id == limit_sell_order['id']
@ -1830,7 +1831,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open,
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade, value=(True, True, None)) patch_get_signal(freqtrade, enter_long=True, exit_long=True)
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions() freqtrade.enter_positions()
@ -1849,7 +1850,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open,
assert trades[0].is_open is True assert trades[0].is_open is True
# Buy and Sell are not triggering, so doing nothing ... # Buy and Sell are not triggering, so doing nothing ...
patch_get_signal(freqtrade, value=(False, False, None)) patch_get_signal(freqtrade, enter_long=False)
assert freqtrade.handle_trade(trades[0]) is False assert freqtrade.handle_trade(trades[0]) is False
trades = Trade.query.all() trades = Trade.query.all()
nb_trades = len(trades) nb_trades = len(trades)
@ -1857,7 +1858,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open,
assert trades[0].is_open is True assert trades[0].is_open is True
# Buy and Sell are triggering, so doing nothing ... # Buy and Sell are triggering, so doing nothing ...
patch_get_signal(freqtrade, value=(True, True, None)) patch_get_signal(freqtrade, enter_long=True, exit_long=True)
assert freqtrade.handle_trade(trades[0]) is False assert freqtrade.handle_trade(trades[0]) is False
trades = Trade.query.all() trades = Trade.query.all()
nb_trades = len(trades) nb_trades = len(trades)
@ -1865,7 +1866,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open,
assert trades[0].is_open is True assert trades[0].is_open is True
# Sell is triggering, guess what : we are Selling! # Sell is triggering, guess what : we are Selling!
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
trades = Trade.query.all() trades = Trade.query.all()
assert freqtrade.handle_trade(trades[0]) is True assert freqtrade.handle_trade(trades[0]) is True
@ -1899,7 +1900,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
# we might just want to check if we are in a sell condition without # we might just want to check if we are in a sell condition without
# executing # executing
# if ROI is reached we must sell # if ROI is reached we must sell
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) assert freqtrade.handle_trade(trade)
assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI",
caplog) caplog)
@ -1928,10 +1929,10 @@ def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
patch_get_signal(freqtrade, value=(False, False, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=False)
assert not freqtrade.handle_trade(trade) assert not freqtrade.handle_trade(trade)
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) assert freqtrade.handle_trade(trade)
assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL",
caplog) caplog)
@ -3058,7 +3059,7 @@ def test_sell_profit_only(
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is handle_first assert freqtrade.handle_trade(trade) is handle_first
if handle_second: if handle_second:
@ -3095,7 +3096,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
trade = Trade.query.first() trade = Trade.query.first()
amnt = trade.amount amnt = trade.amount
trade.update(limit_buy_order) trade.update(limit_buy_order)
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
@ -3203,11 +3204,11 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(True, True, None)) patch_get_signal(freqtrade, enter_long=True, exit_long=True)
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
# Test if buy-signal is absent (should sell due to roi = true) # Test if buy-signal is absent (should sell due to roi = true)
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.ROI.value assert trade.sell_reason == SellType.ROI.value
@ -3484,11 +3485,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
# Sell due to min_roi_reached # Sell due to min_roi_reached
patch_get_signal(freqtrade, value=(True, True, None)) patch_get_signal(freqtrade, enter_long=True, exit_long=True)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
# Test if buy-signal is absent # Test if buy-signal is absent
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value assert trade.sell_reason == SellType.SELL_SIGNAL.value
@ -4016,7 +4017,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o
freqtrade.wallets.update() freqtrade.wallets.update()
assert trade.is_open is True assert trade.is_open is True
patch_get_signal(freqtrade, value=(False, True, None)) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0]

View File

@ -72,7 +72,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
create_stoploss_order=MagicMock(return_value=True), create_stoploss_order=MagicMock(return_value=True),
_notify_exit=MagicMock(), _notify_exit=MagicMock(),
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000)) mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000))
@ -163,7 +163,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
SellCheckTuple(sell_type=SellType.NONE), SellCheckTuple(sell_type=SellType.NONE),
SellCheckTuple(sell_type=SellType.NONE)] SellCheckTuple(sell_type=SellType.NONE)]
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtrade) rpc = RPC(freqtrade)

View File

@ -201,8 +201,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t
timerange = TimeRange(None, 'line', 0, -1000) timerange = TimeRange(None, 'line', 0, -1000)
data = history.load_pair_history(pair=pair, timeframe='1m', data = history.load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange) datadir=testdatadir, timerange=timerange)
data['buy'] = 0 data['enter_long'] = 0
data['sell'] = 0 data['exit_long'] = 0
indicators1 = [] indicators1 = []
indicators2 = [] indicators2 = []
@ -261,12 +261,12 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir)
buy = find_trace_in_fig_data(figure.data, "buy") buy = find_trace_in_fig_data(figure.data, "buy")
assert isinstance(buy, go.Scatter) assert isinstance(buy, go.Scatter)
# All buy-signals should be plotted # All buy-signals should be plotted
assert int(data.buy.sum()) == len(buy.x) assert int(data['enter_long'].sum()) == len(buy.x)
sell = find_trace_in_fig_data(figure.data, "sell") sell = find_trace_in_fig_data(figure.data, "sell")
assert isinstance(sell, go.Scatter) assert isinstance(sell, go.Scatter)
# All buy-signals should be plotted # All buy-signals should be plotted
assert int(data.sell.sum()) == len(sell.x) assert int(data['exit_long'].sum()) == len(sell.x)
assert find_trace_in_fig_data(figure.data, "Bollinger Band") assert find_trace_in_fig_data(figure.data, "Bollinger Band")