commit
949f469a7f
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,10 +92,19 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
|
|||||||
if current_profit == -1:
|
if current_profit == -1:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
stoploss = 1-((1+open_relative_stop)/(1+current_profit))
|
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))
|
||||||
|
|
||||||
# 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
|
||||||
return max(stoploss, 0.0)
|
if for_short:
|
||||||
|
return min(stoploss, 0.0)
|
||||||
|
else:
|
||||||
|
return max(stoploss, 0.0)
|
||||||
|
|
||||||
|
|
||||||
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
|
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
|
||||||
|
@ -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) }}
|
||||||
|
379
freqtrade/templates/sample_short_strategy.py
Normal file
379
freqtrade/templates/sample_short_strategy.py
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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):
|
||||||
|
@ -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': {
|
||||||
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
|
@ -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):
|
||||||
|
181
tests/strategy/strats/strategy_test_v3.py
Normal file
181
tests/strategy/strats/strategy_test_v3.py
Normal 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
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user