Merge branch 'develop' into pr/eatrisno/4308
This commit is contained in:
@@ -167,8 +167,9 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"export": Arg(
|
||||
'--export',
|
||||
help='Export backtest results, argument are: trades. '
|
||||
'Example: `--export=trades`',
|
||||
help='Export backtest results (default: trades).',
|
||||
choices=constants.EXPORT_OPTIONS,
|
||||
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
'--export-filename',
|
||||
|
@@ -12,6 +12,7 @@ PROCESS_THROTTLE_SECS = 5 # sec
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
TIMEOUT_UNITS = ['minutes', 'seconds']
|
||||
EXPORT_OPTIONS = ['none', 'trades']
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||
@@ -308,6 +309,7 @@ CONF_SCHEMA = {
|
||||
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
|
||||
},
|
||||
'db_url': {'type': 'string'},
|
||||
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'forcebuy_enable': {'type': 'boolean'},
|
||||
'disable_dataframe_checks': {'type': 'boolean'},
|
||||
|
@@ -225,6 +225,22 @@ class Backtesting:
|
||||
# sell at open price.
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||
# pessimistic price movement, which is moving just enough to arm stoploss and
|
||||
# immediately going down to stop price.
|
||||
if (sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0
|
||||
and self.strategy.trailing_stop_positive):
|
||||
if self.strategy.trailing_only_offset_is_reached:
|
||||
# Worst case: price reaches stop_positive_offset and dives down.
|
||||
stop_rate = (sell_row[OPEN_IDX] *
|
||||
(1 + abs(self.strategy.trailing_stop_positive_offset) -
|
||||
abs(self.strategy.trailing_stop_positive)))
|
||||
else:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive))
|
||||
assert stop_rate < sell_row[HIGH_IDX]
|
||||
return stop_rate
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
@@ -520,7 +536,7 @@ class Backtesting:
|
||||
stats = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if self.config.get('export', False):
|
||||
if self.config.get('export', 'none') == 'trades':
|
||||
store_backtest_stats(self.config['exportfilename'], stats)
|
||||
|
||||
# Show backtest results
|
||||
|
@@ -556,7 +556,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
('Backtesting to', strat_results['backtest_end']),
|
||||
('Max open trades', strat_results['max_open_trades']),
|
||||
('', ''), # Empty line to improve readability
|
||||
('Total trades', strat_results['total_trades']),
|
||||
('Total/Daily Avg Trades',
|
||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
|
||||
('Starting balance', round_coin_value(strat_results['starting_balance'],
|
||||
strat_results['stake_currency'])),
|
||||
('Final balance', round_coin_value(strat_results['final_balance'],
|
||||
@@ -564,7 +565,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
|
||||
('Trades per day', strat_results['trades_per_day']),
|
||||
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||
|
@@ -58,6 +58,9 @@ class IResolver:
|
||||
# Generate spec based on absolute path
|
||||
# Pass object_name as first argument to have logging print a reasonable name.
|
||||
spec = importlib.util.spec_from_file_location(object_name or "", str(module_path))
|
||||
if not spec:
|
||||
return iter([None])
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||
@@ -91,6 +94,9 @@ class IResolver:
|
||||
if not str(entry).endswith('.py'):
|
||||
logger.debug('Ignoring %s', entry)
|
||||
continue
|
||||
if entry.is_symlink() and not entry.is_file():
|
||||
logger.debug('Ignoring broken symlink %s', entry)
|
||||
continue
|
||||
module_path = entry.resolve()
|
||||
|
||||
obj = next(cls._get_valid_object(module_path, object_name), None)
|
||||
|
@@ -60,7 +60,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||
)
|
||||
return wrapper
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
'Executing handler: %s for chat_id: %s',
|
||||
command_handler.__name__,
|
||||
chat_id
|
||||
|
@@ -77,14 +77,13 @@ class Webhook(RPCHandler):
|
||||
def _send_msg(self, payload: dict) -> None:
|
||||
"""do the actual call to the webhook"""
|
||||
|
||||
if self._format == 'form':
|
||||
kwargs = {'data': payload}
|
||||
elif self._format == 'json':
|
||||
kwargs = {'json': payload}
|
||||
else:
|
||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||
|
||||
try:
|
||||
post(self._url, **kwargs)
|
||||
if self._format == 'form':
|
||||
post(self._url, data=payload)
|
||||
elif self._format == 'json':
|
||||
post(self._url, json=payload)
|
||||
else:
|
||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||
|
||||
except RequestException as exc:
|
||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||
|
@@ -524,15 +524,14 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param force_stoploss: Externally provided stoploss
|
||||
:return: True if trade should be sold, False otherwise
|
||||
"""
|
||||
# Set current rate to low for backtesting sell
|
||||
current_rate = low or rate
|
||||
current_rate = rate
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
trade.adjust_min_max_rates(high or current_rate)
|
||||
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
force_stoploss=force_stoploss, high=high)
|
||||
force_stoploss=force_stoploss, low=low, high=high)
|
||||
|
||||
# Set current rate to high for backtesting sell
|
||||
current_rate = high or rate
|
||||
@@ -599,18 +598,21 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||
current_time: datetime, current_profit: float,
|
||||
force_stoploss: float, high: float = None) -> SellCheckTuple:
|
||||
force_stoploss: float, low: float = None,
|
||||
high: float = None) -> SellCheckTuple:
|
||||
"""
|
||||
Based on current profit of the trade and configured (trailing) stoploss,
|
||||
decides to sell or not
|
||||
:param current_profit: current profit as ratio
|
||||
:param low: Low value of this candle, only set in backtesting
|
||||
:param high: High value of this candle, only set in backtesting
|
||||
"""
|
||||
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
|
||||
|
||||
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
||||
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
|
||||
|
||||
if self.use_custom_stoploss:
|
||||
if self.use_custom_stoploss and trade.stop_loss < (low or current_rate):
|
||||
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
|
||||
)(pair=trade.pair, trade=trade,
|
||||
current_time=current_time,
|
||||
@@ -623,7 +625,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
logger.warning("CustomStoploss function did not return valid stoploss")
|
||||
|
||||
if self.trailing_stop:
|
||||
if self.trailing_stop and trade.stop_loss < (low or current_rate):
|
||||
# trailing stoploss handling
|
||||
sl_offset = self.trailing_stop_positive_offset
|
||||
|
||||
@@ -643,7 +645,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||
# regular stoploss handling.
|
||||
if ((trade.stop_loss >= current_rate) and
|
||||
if ((trade.stop_loss >= (low or current_rate)) and
|
||||
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
||||
|
||||
sell_type = SellType.STOP_LOSS
|
||||
@@ -652,7 +654,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
if trade.initial_stop_loss != trade.stop_loss:
|
||||
sell_type = SellType.TRAILING_STOP_LOSS
|
||||
logger.debug(
|
||||
f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, "
|
||||
f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
|
||||
f"stoploss is {trade.stop_loss:.6f}, "
|
||||
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
||||
f"trade opened at {trade.open_rate:.6f}")
|
||||
|
Reference in New Issue
Block a user