Merge remote-tracking branch 'upstream/develop' into backtest_refactor-2
This commit is contained in:
commit
7e343221ec
@ -194,7 +194,10 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- Buys happen at open-price
|
||||
- Sell signal sells happen at open-price of the following candle
|
||||
- Low happens before high for stoploss, protecting capital first.
|
||||
- ROI sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%)
|
||||
- ROI
|
||||
- sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%)
|
||||
- sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit
|
||||
- Forcesells caused by `<N>=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
||||
- Stoploss sells happen exactly at stoploss price, even if low was lower
|
||||
- Trailing stoploss
|
||||
- High happens first - adjusting stoploss
|
||||
|
@ -169,6 +169,9 @@ This parameter can be set in either Strategy or Configuration file. If you use i
|
||||
`minimal_roi` value from the strategy file.
|
||||
If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal roi is disabled unless your trade generates 1000% profit.
|
||||
|
||||
!!! Note "Special case to forcesell after a specific time"
|
||||
A special case presents using `"<N>": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell.
|
||||
|
||||
### Understand stoploss
|
||||
|
||||
Go to the [stoploss documentation](stoploss.md) for more details.
|
||||
|
@ -6,8 +6,12 @@ algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
||||
and still take a long time.
|
||||
|
||||
In general, the search for best parameters starts with a few random combinations and then uses Bayesian search with a
|
||||
ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace
|
||||
that minimizes the value of the [loss function](#loss-functions).
|
||||
|
||||
Hyperopt requires historic data to be available, just as backtesting does.
|
||||
To learn how to get data for the pairs and exchange you're interrested in, head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||
To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||
|
||||
!!! Bug
|
||||
Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||
@ -170,10 +174,6 @@ with different value combinations. It will then use the given historical data an
|
||||
buys based on the buy signals generated with the above function and based on the results
|
||||
it will end with telling you which paramter combination produced the best profits.
|
||||
|
||||
The search for best parameters starts with a few random combinations and then uses a
|
||||
regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination
|
||||
that minimizes the value of the [loss function](#loss-functions).
|
||||
|
||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||
add it to the `populate_indicators()` method in your custom hyperopt file.
|
||||
@ -284,6 +284,16 @@ number).
|
||||
You can also enable position stacking in the configuration file by explicitly setting
|
||||
`"position_stacking"=true`.
|
||||
|
||||
### Reproducible results
|
||||
|
||||
The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperopt output.
|
||||
|
||||
The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results.
|
||||
|
||||
If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used.
|
||||
|
||||
If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyperoptimization results with same random state value used.
|
||||
|
||||
## Understand the Hyperopt Result
|
||||
|
||||
Once Hyperopt is completed you can use the result to create a new strategy.
|
||||
|
@ -80,7 +80,7 @@ class Edge:
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee()
|
||||
self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0])
|
||||
|
||||
def calculate(self) -> bool:
|
||||
pairs = self.config['exchange']['pair_whitelist']
|
||||
|
@ -921,7 +921,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
|
||||
def get_fee(self, symbol, type='', side='', amount=1,
|
||||
price=1, taker_or_maker='maker') -> float:
|
||||
try:
|
||||
# validate that markets are loaded before trying to get fee
|
||||
|
@ -65,7 +65,7 @@ class Backtesting:
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee()
|
||||
self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0])
|
||||
|
||||
if self.config.get('runmode') != RunMode.HYPEROPT:
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
@ -278,6 +278,45 @@ class Backtesting:
|
||||
ticker[pair] = [x for x in pair_data[headers].itertuples()]
|
||||
return ticker
|
||||
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
"""
|
||||
# Special handling if high or low hit STOP_LOSS or ROI
|
||||
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
|
||||
if roi is not None:
|
||||
if roi == -1 and roi_entry % self.timeframe_mins == 0:
|
||||
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
|
||||
# If that entry is a multiple of the timeframe (so on candle open)
|
||||
# - we'll use open instead of close
|
||||
return sell_row.open
|
||||
|
||||
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||
close_rate = - (trade.open_rate * roi + trade.open_rate *
|
||||
(1 + trade.fee_open)) / (trade.fee_close - 1)
|
||||
|
||||
if (trade_dur > 0 and trade_dur == roi_entry
|
||||
and roi_entry % self.timeframe_mins == 0
|
||||
and sell_row.open > close_rate):
|
||||
# new ROI entry came into effect.
|
||||
# use Open rate if open_rate > calculated sell rate
|
||||
return sell_row.open
|
||||
|
||||
# Use the maximum between close_rate and low as we
|
||||
# cannot sell outside of a candle.
|
||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||
return max(close_rate, sell_row.low)
|
||||
|
||||
else:
|
||||
# This should not be reached...
|
||||
return sell_row.open
|
||||
else:
|
||||
return sell_row.open
|
||||
|
||||
def _get_sell_trade_entry(
|
||||
self, pair: str, buy_row: DataFrame,
|
||||
partial_ticker: List, trade_count_lock: Dict,
|
||||
@ -304,26 +343,7 @@ class Backtesting:
|
||||
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
||||
if sell.sell_flag:
|
||||
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
|
||||
# Special handling if high or low hit STOP_LOSS or ROI
|
||||
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
# Set close_rate to stoploss
|
||||
closerate = trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
roi = self.strategy.min_roi_reached_entry(trade_dur)
|
||||
if roi is not None:
|
||||
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||
closerate = - (trade.open_rate * roi + trade.open_rate *
|
||||
(1 + trade.fee_open)) / (trade.fee_close - 1)
|
||||
|
||||
# Use the maximum between closerate and low as we
|
||||
# cannot sell outside of a candle.
|
||||
# Applies when using {"xx": -1} as roi to force sells after xx minutes
|
||||
closerate = max(closerate, sell_row.low)
|
||||
else:
|
||||
# This should not be reached...
|
||||
closerate = sell_row.open
|
||||
else:
|
||||
closerate = sell_row.open
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
|
||||
return BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_percent(rate=closerate),
|
||||
|
@ -6,6 +6,7 @@ This module contains the hyperopt logic
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
@ -434,7 +435,7 @@ class Hyperopt:
|
||||
acq_optimizer="auto",
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||
random_state=self.config.get('hyperopt_random_state', None),
|
||||
random_state=self.random_state,
|
||||
)
|
||||
|
||||
def fix_optimizer_models_list(self):
|
||||
@ -473,7 +474,13 @@ class Hyperopt:
|
||||
logger.info(f"Loaded {len(trials)} previous evaluations from disk.")
|
||||
return trials
|
||||
|
||||
def _set_random_state(self, random_state: Optional[int]) -> int:
|
||||
return random_state or random.randint(1, 2**16 - 1)
|
||||
|
||||
def start(self) -> None:
|
||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
||||
logger.info(f"Using optimizer random state: {self.random_state}")
|
||||
|
||||
data, timerange = self.backtesting.load_bt_data()
|
||||
|
||||
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
|
@ -587,14 +587,25 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
val = self._rpc_show_config()
|
||||
if val['trailing_stop']:
|
||||
sl_info = (
|
||||
f"*Initial Stoploss:* `{val['stoploss']}`\n"
|
||||
f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n"
|
||||
f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n"
|
||||
f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n"
|
||||
)
|
||||
|
||||
else:
|
||||
sl_info = f"*Stoploss:* `{val['stoploss']}`\n"
|
||||
|
||||
self._send_msg(
|
||||
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
|
||||
f"*Exchange:* `{val['exchange']}`\n"
|
||||
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
||||
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
||||
f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n"
|
||||
f"{sl_info}"
|
||||
f"*Ticker Interval:* `{val['ticker_interval']}`\n"
|
||||
f"*Strategy:* `{val['strategy']}`'"
|
||||
f"*Strategy:* `{val['strategy']}`"
|
||||
)
|
||||
|
||||
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||
|
@ -400,7 +400,7 @@ class IStrategy(ABC):
|
||||
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]:
|
||||
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
|
||||
"""
|
||||
Based on trade duration defines the ROI entry that may have been reached.
|
||||
:param trade_dur: trade duration in minutes
|
||||
@ -409,9 +409,9 @@ class IStrategy(ABC):
|
||||
# Get highest entry in ROI dict where key <= trade-duration
|
||||
roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys()))
|
||||
if not roi_list:
|
||||
return None
|
||||
return None, None
|
||||
roi_entry = max(roi_list)
|
||||
return self.minimal_roi[roi_entry]
|
||||
return roi_entry, self.minimal_roi[roi_entry]
|
||||
|
||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||
"""
|
||||
@ -421,7 +421,7 @@ class IStrategy(ABC):
|
||||
"""
|
||||
# Check if time matches and current rate is above threshold
|
||||
trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60)
|
||||
roi = self.min_roi_reached_entry(trade_dur)
|
||||
_, roi = self.min_roi_reached_entry(trade_dur)
|
||||
if roi is None:
|
||||
return False
|
||||
else:
|
||||
|
@ -334,7 +334,7 @@ def test_process_expectancy(mocker, edge_conf):
|
||||
edge_conf['edge']['min_trade_number'] = 2
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
def get_fee():
|
||||
def get_fee(*args, **kwargs):
|
||||
return 0.001
|
||||
|
||||
freqtrade.exchange.get_fee = get_fee
|
||||
|
@ -1646,10 +1646,10 @@ def test_get_fee(default_conf, mocker, exchange_name):
|
||||
})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
assert exchange.get_fee() == 0.025
|
||||
assert exchange.get_fee('ETH/BTC') == 0.025
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
'get_fee', 'calculate_fee')
|
||||
'get_fee', 'calculate_fee', symbol="ETH/BTC")
|
||||
|
||||
|
||||
def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker):
|
||||
|
@ -265,6 +265,69 @@ tc16 = BTContainer(data=[
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 17: Buy, hold for 120 mins, then forcesell using roi=-1
|
||||
# Causes negative profit even though sell-reason is ROI.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval.
|
||||
tc17 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5050, 6172, 0, 0],
|
||||
[3, 4980, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1)
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
|
||||
# Test 18: Buy, hold for 120 mins, then drop ROI to 1%, causing a sell in candle 3.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# uses open_rate as sell-price
|
||||
tc18 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5200, 6172, 0, 0],
|
||||
[3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Sell on ROI (sells on open)
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# uses calculated ROI (1%) as sell rate, otherwise identical to tc18
|
||||
tc19 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5200, 6172, 0, 0],
|
||||
[3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4550, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration)
|
||||
# uses calculated ROI (1%) as sell rate, otherwise identical to tc18
|
||||
tc20 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5200, 6172, 0, 0],
|
||||
[3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4550, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
tc1,
|
||||
@ -283,6 +346,10 @@ TESTS = [
|
||||
tc14,
|
||||
tc15,
|
||||
tc16,
|
||||
tc17,
|
||||
tc18,
|
||||
tc19,
|
||||
tc20,
|
||||
]
|
||||
|
||||
|
||||
|
@ -1177,6 +1177,16 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
|
||||
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
|
||||
assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0]
|
||||
assert '*Strategy:* `DefaultStrategy`' 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()
|
||||
freqtradebot.config['trailing_stop'] = True
|
||||
telegram._show_config(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
|
||||
assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0]
|
||||
assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0]
|
||||
assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||
|
Loading…
Reference in New Issue
Block a user