Merge branch 'develop' into renaming-forceentry-forceexit
This commit is contained in:
@@ -202,6 +202,8 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
if not answers:
|
||||
# Interrupted questionary sessions return an empty dict.
|
||||
raise OperationalException("User interrupted interactive questions.")
|
||||
# Ensure default is set for non-futures exchanges
|
||||
answers['trading_mode'] = answers.get('trading_mode', "spot")
|
||||
answers['margin_mode'] = (
|
||||
'isolated'
|
||||
if answers.get('trading_mode') == 'futures'
|
||||
|
@@ -154,9 +154,9 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||
if not conf.get('edge', {}).get('enabled'):
|
||||
return
|
||||
|
||||
if not conf.get('use_sell_signal', True):
|
||||
if not conf.get('use_exit_signal', True):
|
||||
raise OperationalException(
|
||||
"Edge requires `use_sell_signal` to be True, otherwise no sells will happen."
|
||||
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
||||
)
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
||||
_validate_order_types(conf)
|
||||
_validate_unfilledtimeout(conf)
|
||||
_validate_pricing_rules(conf)
|
||||
_strategy_settings(conf)
|
||||
|
||||
|
||||
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||
@@ -315,3 +316,12 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
|
||||
else:
|
||||
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj)
|
||||
del conf['ask_strategy']
|
||||
|
||||
|
||||
def _strategy_settings(conf: Dict[str, Any]) -> None:
|
||||
|
||||
process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal')
|
||||
process_deprecated_setting(conf, None, 'sell_profit_only', None, 'exit_profit_only')
|
||||
process_deprecated_setting(conf, None, 'sell_profit_offset', None, 'exit_profit_offset')
|
||||
process_deprecated_setting(conf, None, 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_entry_signal')
|
||||
|
@@ -433,8 +433,9 @@ class Configuration:
|
||||
logstring='Detected --new-pairs-days: {}')
|
||||
self._args_to_config(config, argname='trading_mode',
|
||||
logstring='Detected --trading-mode: {}')
|
||||
config['candle_type_def'] = CandleType.get_default(config.get('trading_mode', 'spot'))
|
||||
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot'))
|
||||
config['candle_type_def'] = CandleType.get_default(
|
||||
config.get('trading_mode', 'spot') or 'spot')
|
||||
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot') or 'spot')
|
||||
self._args_to_config(config, argname='candle_types',
|
||||
logstring='Detected --candle-types: {}')
|
||||
|
||||
|
@@ -12,14 +12,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_conflicting_settings(config: Dict[str, Any],
|
||||
section_old: str, name_old: str,
|
||||
section_old: Optional[str], name_old: str,
|
||||
section_new: Optional[str], name_new: str) -> None:
|
||||
section_new_config = config.get(section_new, {}) if section_new else config
|
||||
section_old_config = config.get(section_old, {})
|
||||
section_old_config = config.get(section_old, {}) if section_old else config
|
||||
if name_new in section_new_config and name_old in section_old_config:
|
||||
new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||
old_name = f"{section_old}.{name_old}" if section_old else f"{name_old}"
|
||||
raise OperationalException(
|
||||
f"Conflicting settings `{new_name}` and `{section_old}.{name_old}` "
|
||||
f"Conflicting settings `{new_name}` and `{old_name}` "
|
||||
"(DEPRECATED) detected in the configuration file. "
|
||||
"This deprecated setting will be removed in the next versions of Freqtrade. "
|
||||
f"Please delete it from your configuration and use the `{new_name}` "
|
||||
@@ -47,11 +48,11 @@ def process_removed_setting(config: Dict[str, Any],
|
||||
|
||||
|
||||
def process_deprecated_setting(config: Dict[str, Any],
|
||||
section_old: str, name_old: str,
|
||||
section_old: Optional[str], name_old: str,
|
||||
section_new: Optional[str], name_new: str
|
||||
) -> None:
|
||||
check_conflicting_settings(config, section_old, name_old, section_new, name_new)
|
||||
section_old_config = config.get(section_old, {})
|
||||
section_old_config = config.get(section_old, {}) if section_old else config
|
||||
|
||||
if name_old in section_old_config:
|
||||
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||
@@ -72,14 +73,7 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
# Kept for future deprecated / moved settings
|
||||
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
|
||||
# 'experimental', 'use_sell_signal')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
|
||||
None, 'use_sell_signal')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
|
||||
None, 'sell_profit_only')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_offset',
|
||||
None, 'sell_profit_offset')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_buy_signal')
|
||||
|
||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
|
||||
None, 'ignore_buying_expired_candle_after')
|
||||
# New settings
|
||||
@@ -109,13 +103,18 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
'webhook', 'webhookexitfill')
|
||||
|
||||
# Legacy way - having them in experimental ...
|
||||
process_removed_setting(config, 'experimental', 'use_sell_signal',
|
||||
None, 'use_sell_signal')
|
||||
process_removed_setting(config, 'experimental', 'sell_profit_only',
|
||||
None, 'sell_profit_only')
|
||||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_buy_signal')
|
||||
|
||||
process_removed_setting(config, 'experimental', 'use_sell_signal', None, 'use_exit_signal')
|
||||
process_removed_setting(config, 'experimental', 'sell_profit_only', None, 'exit_profit_only')
|
||||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_entry_signal')
|
||||
|
||||
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'exit_sell_signal')
|
||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only')
|
||||
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset',
|
||||
None, 'exit_profit_offset')
|
||||
process_removed_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_entry_signal')
|
||||
if (config.get('edge', {}).get('enabled', False)
|
||||
and 'capital_available_percentage' in config.get('edge', {})):
|
||||
raise OperationalException(
|
||||
|
@@ -86,8 +86,8 @@ SUPPORTED_FIAT = [
|
||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
||||
"BTC", "ETH", "XRP", "LTC", "BCH"
|
||||
"RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
|
||||
"USD", "BTC", "ETH", "XRP", "LTC", "BCH"
|
||||
]
|
||||
|
||||
MINIMAL_CONFIG = {
|
||||
@@ -149,10 +149,10 @@ CONF_SCHEMA = {
|
||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_only_offset_is_reached': {'type': 'boolean'},
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'},
|
||||
'sell_profit_offset': {'type': 'number'},
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||
'use_exit_signal': {'type': 'boolean'},
|
||||
'exit_profit_only': {'type': 'boolean'},
|
||||
'exit_profit_offset': {'type': 'number'},
|
||||
'ignore_roi_if_entry_signal': {'type': 'boolean'},
|
||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
||||
|
@@ -330,12 +330,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||
for trade in trades:
|
||||
if trade.is_open and not trade.fee_updated(trade.enter_side):
|
||||
order = trade.select_order(trade.enter_side, False)
|
||||
open_order = trade.select_order(trade.enter_side, True)
|
||||
if trade.is_open and not trade.fee_updated(trade.entry_side):
|
||||
order = trade.select_order(trade.entry_side, False)
|
||||
open_order = trade.select_order(trade.entry_side, True)
|
||||
if order and open_order is None:
|
||||
logger.info(
|
||||
f"Updating {trade.enter_side}-fee on trade {trade}"
|
||||
f"Updating {trade.entry_side}-fee on trade {trade}"
|
||||
f"for order {order.order_id}."
|
||||
)
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
@@ -364,7 +364,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
elif order.ft_order_side == trade.enter_side:
|
||||
elif order.ft_order_side == trade.entry_side:
|
||||
if fo and fo['status'] == 'open':
|
||||
trade.open_order_id = order.order_id
|
||||
if fo:
|
||||
@@ -549,9 +549,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||
|
||||
enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
|
||||
entry_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
|
||||
exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
|
||||
bids_ask_delta = enter_side / exit_side
|
||||
bids_ask_delta = entry_side / exit_side
|
||||
|
||||
bids = f"Bids: {order_book_bids}"
|
||||
asks = f"Asks: {order_book_asks}"
|
||||
@@ -926,8 +926,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
exit_tag = None
|
||||
exit_signal_type = "exit_short" if trade.is_short else "exit_long"
|
||||
|
||||
if (self.config.get('use_sell_signal', True) or
|
||||
self.config.get('ignore_roi_if_buy_signal', False)):
|
||||
if (self.config.get('use_exit_signal', True) or
|
||||
self.config.get('ignore_roi_if_entry_signal', False)):
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
self.strategy.timeframe)
|
||||
|
||||
@@ -1136,7 +1136,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
continue
|
||||
|
||||
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||
is_entering = order['side'] == trade.enter_side
|
||||
is_entering = order['side'] == trade.entry_side
|
||||
not_closed = order['status'] == 'open' or fully_cancelled
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
|
||||
@@ -1177,7 +1177,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
|
||||
if order['side'] == trade.enter_side:
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
|
||||
elif order['side'] == trade.exit_side:
|
||||
@@ -1216,7 +1216,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
corder = order
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
|
||||
side = trade.enter_side.capitalize()
|
||||
side = trade.entry_side.capitalize()
|
||||
logger.info('%s order %s for %s.', side, reason, trade)
|
||||
|
||||
# Using filled to determine the filled amount
|
||||
@@ -1247,7 +1247,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||
|
||||
trade.open_order_id = None
|
||||
logger.info(f'Partial {trade.enter_side} order timeout for {trade}.')
|
||||
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
self.wallets.update()
|
||||
@@ -1577,7 +1577,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
# If a entry order was closed, force update on stoploss on exchange
|
||||
if order.get('side', None) == trade.enter_side:
|
||||
if order.get('side', None) == trade.entry_side:
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
# TODO: Margin will need to use interest_rate as well.
|
||||
# interest_rate = self.exchange.get_interest_rate()
|
||||
|
@@ -349,20 +349,20 @@ class Backtesting:
|
||||
data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else []
|
||||
return data
|
||||
|
||||
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
||||
def _get_close_rate(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
"""
|
||||
# Special handling if high or low hit STOP_LOSS or ROI
|
||||
if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
||||
return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur)
|
||||
return self._get_close_rate_for_stoploss(row, trade, sell, trade_dur)
|
||||
elif sell.exit_type == (ExitType.ROI):
|
||||
return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur)
|
||||
return self._get_close_rate_for_roi(row, trade, sell, trade_dur)
|
||||
else:
|
||||
return sell_row[OPEN_IDX]
|
||||
return row[OPEN_IDX]
|
||||
|
||||
def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
||||
def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
# our stoploss was already lower than candle high,
|
||||
# possibly due to a cancelled trade exit.
|
||||
@@ -371,11 +371,11 @@ class Backtesting:
|
||||
leverage = trade.leverage or 1.0
|
||||
side_1 = -1 if is_short else 1
|
||||
if is_short:
|
||||
if trade.stop_loss < sell_row[LOW_IDX]:
|
||||
return sell_row[OPEN_IDX]
|
||||
if trade.stop_loss < row[LOW_IDX]:
|
||||
return row[OPEN_IDX]
|
||||
else:
|
||||
if trade.stop_loss > sell_row[HIGH_IDX]:
|
||||
return sell_row[OPEN_IDX]
|
||||
if trade.stop_loss > row[HIGH_IDX]:
|
||||
return 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
|
||||
@@ -388,29 +388,28 @@ class Backtesting:
|
||||
and self.strategy.trailing_stop_positive
|
||||
):
|
||||
# Worst case: price reaches stop_positive_offset and dives down.
|
||||
stop_rate = (sell_row[OPEN_IDX] *
|
||||
stop_rate = (row[OPEN_IDX] *
|
||||
(1 + side_1 * abs(self.strategy.trailing_stop_positive_offset) -
|
||||
side_1 * abs(self.strategy.trailing_stop_positive / leverage)))
|
||||
else:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 -
|
||||
side_1 * abs(trade.stop_loss_pct / leverage))
|
||||
stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(trade.stop_loss_pct / leverage))
|
||||
if is_short:
|
||||
assert stop_rate > sell_row[LOW_IDX]
|
||||
assert stop_rate > row[LOW_IDX]
|
||||
else:
|
||||
assert stop_rate < sell_row[HIGH_IDX]
|
||||
assert stop_rate < row[HIGH_IDX]
|
||||
|
||||
# Limit lower-end to candle low to avoid sells below the low.
|
||||
# This still remains "worst case" - but "worst realistic case".
|
||||
if is_short:
|
||||
return min(sell_row[HIGH_IDX], stop_rate)
|
||||
return min(row[HIGH_IDX], stop_rate)
|
||||
else:
|
||||
return max(sell_row[LOW_IDX], stop_rate)
|
||||
return max(row[LOW_IDX], stop_rate)
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
|
||||
def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
||||
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
is_short = trade.is_short or False
|
||||
leverage = trade.leverage or 1.0
|
||||
@@ -421,38 +420,38 @@ class Backtesting:
|
||||
# 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_IDX]
|
||||
return row[OPEN_IDX]
|
||||
|
||||
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
|
||||
roi_rate = trade.open_rate * roi / leverage
|
||||
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
|
||||
close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1)
|
||||
if is_short:
|
||||
is_new_roi = sell_row[OPEN_IDX] < close_rate
|
||||
is_new_roi = row[OPEN_IDX] < close_rate
|
||||
else:
|
||||
is_new_roi = sell_row[OPEN_IDX] > close_rate
|
||||
is_new_roi = row[OPEN_IDX] > close_rate
|
||||
if (trade_dur > 0 and trade_dur == roi_entry
|
||||
and roi_entry % self.timeframe_min == 0
|
||||
and is_new_roi):
|
||||
# new ROI entry came into effect.
|
||||
# use Open rate if open_rate > calculated sell rate
|
||||
return sell_row[OPEN_IDX]
|
||||
return row[OPEN_IDX]
|
||||
|
||||
if (trade_dur == 0 and (
|
||||
(
|
||||
is_short
|
||||
# Red candle (for longs)
|
||||
and sell_row[OPEN_IDX] < sell_row[CLOSE_IDX] # Red candle
|
||||
and trade.open_rate > sell_row[OPEN_IDX] # trade-open above open_rate
|
||||
and close_rate < sell_row[CLOSE_IDX] # closes below close
|
||||
and row[OPEN_IDX] < row[CLOSE_IDX] # Red candle
|
||||
and trade.open_rate > row[OPEN_IDX] # trade-open above open_rate
|
||||
and close_rate < row[CLOSE_IDX] # closes below close
|
||||
)
|
||||
or
|
||||
(
|
||||
not is_short
|
||||
# green candle (for shorts)
|
||||
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # green candle
|
||||
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate
|
||||
and close_rate > sell_row[CLOSE_IDX] # closes above close
|
||||
and row[OPEN_IDX] > row[CLOSE_IDX] # green candle
|
||||
and trade.open_rate < row[OPEN_IDX] # trade-open below open_rate
|
||||
and close_rate > row[CLOSE_IDX] # closes above close
|
||||
)
|
||||
)):
|
||||
# ROI on opening candles with custom pricing can only
|
||||
@@ -464,11 +463,11 @@ class Backtesting:
|
||||
# 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 min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
||||
return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX])
|
||||
|
||||
else:
|
||||
# This should not be reached...
|
||||
return sell_row[OPEN_IDX]
|
||||
return row[OPEN_IDX]
|
||||
|
||||
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
|
||||
) -> LocalTrade:
|
||||
@@ -498,7 +497,7 @@ class Backtesting:
|
||||
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||
|
||||
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
||||
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
# Check if we need to adjust our current positions
|
||||
if self.strategy.position_adjustment_enable:
|
||||
@@ -507,15 +506,15 @@ class Backtesting:
|
||||
entry_count = trade.nr_of_successful_entries
|
||||
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
|
||||
if check_adjust_entry:
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row)
|
||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||
|
||||
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
|
||||
enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX]
|
||||
exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX]
|
||||
sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||
exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||
sell = self.strategy.should_exit(
|
||||
trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore
|
||||
trade, row[OPEN_IDX], sell_candle_time, # type: ignore
|
||||
enter=enter, exit_=exit_,
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]
|
||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||
)
|
||||
|
||||
if sell.exit_flag:
|
||||
@@ -523,7 +522,7 @@ class Backtesting:
|
||||
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
try:
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
closerate = self._get_close_rate(row, trade, sell, trade_dur)
|
||||
except ValueError:
|
||||
return None
|
||||
# call the custom exit price,with default value as previous closerate
|
||||
@@ -540,9 +539,9 @@ class Backtesting:
|
||||
# We can't place orders lower than current low.
|
||||
# freqtrade does not support this in live, and the order would fill immediately
|
||||
if trade.is_short:
|
||||
closerate = min(closerate, sell_row[HIGH_IDX])
|
||||
closerate = min(closerate, row[HIGH_IDX])
|
||||
else:
|
||||
closerate = max(closerate, sell_row[LOW_IDX])
|
||||
closerate = max(closerate, row[LOW_IDX])
|
||||
# Confirm trade exit:
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
|
||||
@@ -558,13 +557,13 @@ class Backtesting:
|
||||
trade.exit_reason = sell.exit_reason
|
||||
|
||||
# Checks and adds an exit tag, after checking that the length of the
|
||||
# sell_row has the length for an exit tag column
|
||||
# row has the length for an exit tag column
|
||||
if(
|
||||
len(sell_row) > EXIT_TAG_IDX
|
||||
and sell_row[EXIT_TAG_IDX] is not None
|
||||
and len(sell_row[EXIT_TAG_IDX]) > 0
|
||||
len(row) > EXIT_TAG_IDX
|
||||
and row[EXIT_TAG_IDX] is not None
|
||||
and len(row[EXIT_TAG_IDX]) > 0
|
||||
):
|
||||
trade.exit_reason = sell_row[EXIT_TAG_IDX]
|
||||
trade.exit_reason = row[EXIT_TAG_IDX]
|
||||
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
@@ -592,8 +591,8 @@ class Backtesting:
|
||||
|
||||
return None
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
||||
sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||
@@ -614,13 +613,13 @@ class Backtesting:
|
||||
].copy()
|
||||
if len(detail_data) == 0:
|
||||
# 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)
|
||||
detail_data.loc[:, 'enter_long'] = sell_row[LONG_IDX]
|
||||
detail_data.loc[:, 'exit_long'] = sell_row[ELONG_IDX]
|
||||
detail_data.loc[:, 'enter_short'] = sell_row[SHORT_IDX]
|
||||
detail_data.loc[:, 'exit_short'] = sell_row[ESHORT_IDX]
|
||||
detail_data.loc[:, 'enter_tag'] = sell_row[ENTER_TAG_IDX]
|
||||
detail_data.loc[:, 'exit_tag'] = sell_row[EXIT_TAG_IDX]
|
||||
return self._get_sell_trade_entry_for_candle(trade, row)
|
||||
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
||||
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
||||
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
||||
detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
|
||||
detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
|
||||
detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
|
||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
||||
for det_row in detail_data[headers].values.tolist():
|
||||
@@ -631,7 +630,7 @@ class Backtesting:
|
||||
return None
|
||||
|
||||
else:
|
||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||
return self._get_sell_trade_entry_for_candle(trade, row)
|
||||
|
||||
def get_valid_price_and_stake(
|
||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
||||
@@ -774,8 +773,8 @@ class Backtesting:
|
||||
ft_pair=trade.pair,
|
||||
order_id=str(self.order_id_counter),
|
||||
symbol=trade.pair,
|
||||
ft_order_side=trade.enter_side,
|
||||
side=trade.enter_side,
|
||||
ft_order_side=trade.entry_side,
|
||||
side=trade.entry_side,
|
||||
order_type=order_type,
|
||||
status="open",
|
||||
order_date=current_time,
|
||||
@@ -857,7 +856,7 @@ class Backtesting:
|
||||
|
||||
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
|
||||
if timedout:
|
||||
if order.side == trade.enter_side:
|
||||
if order.side == trade.entry_side:
|
||||
self.timedout_entry_orders += 1
|
||||
if trade.nr_of_successful_entries == 0:
|
||||
# Remove trade due to entry timeout expiration.
|
||||
@@ -972,7 +971,7 @@ class Backtesting:
|
||||
|
||||
for trade in list(open_trades[pair]):
|
||||
# 3. Process entry orders.
|
||||
order = trade.select_order(trade.enter_side, is_open=True)
|
||||
order = trade.select_order(trade.entry_side, is_open=True)
|
||||
if order and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time)
|
||||
trade.open_order_id = None
|
||||
|
@@ -114,8 +114,8 @@ class Hyperopt:
|
||||
self.position_stacking = self.config.get('position_stacking', False)
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
# Make sure use_sell_signal is enabled
|
||||
self.config['use_sell_signal'] = True
|
||||
# Make sure use_exit_signal is enabled
|
||||
self.config['use_exit_signal'] = True
|
||||
|
||||
self.print_all = self.config.get('print_all', False)
|
||||
self.hyperopt_table_header = 0
|
||||
|
@@ -460,10 +460,10 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
|
||||
'use_custom_stoploss': config.get('use_custom_stoploss', False),
|
||||
'minimal_roi': config['minimal_roi'],
|
||||
'use_sell_signal': config['use_sell_signal'],
|
||||
'sell_profit_only': config['sell_profit_only'],
|
||||
'sell_profit_offset': config['sell_profit_offset'],
|
||||
'ignore_roi_if_buy_signal': config['ignore_roi_if_buy_signal'],
|
||||
'use_exit_signal': config['use_exit_signal'],
|
||||
'exit_profit_only': config['exit_profit_only'],
|
||||
'exit_profit_offset': config['exit_profit_offset'],
|
||||
'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'],
|
||||
**daily_stats,
|
||||
**trade_stats
|
||||
}
|
||||
|
@@ -372,6 +372,12 @@ class LocalTrade():
|
||||
|
||||
@property
|
||||
def enter_side(self) -> str:
|
||||
""" DEPRECATED, please use entry_side instead"""
|
||||
# TODO: Please remove me after 2022.5
|
||||
return self.entry_side
|
||||
|
||||
@property
|
||||
def entry_side(self) -> str:
|
||||
if self.is_short:
|
||||
return "sell"
|
||||
else:
|
||||
@@ -412,7 +418,7 @@ class LocalTrade():
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
orders = [order.to_json(self.enter_side) for order in filled_orders]
|
||||
orders = [order.to_json(self.entry_side) for order in filled_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@@ -601,7 +607,7 @@ class LocalTrade():
|
||||
|
||||
logger.info(f'Updating trade (id={self.id}) ...')
|
||||
|
||||
if order.ft_order_side == self.enter_side:
|
||||
if order.ft_order_side == self.entry_side:
|
||||
# Update open rate and actual amount
|
||||
self.open_rate = order.safe_price
|
||||
self.amount = order.safe_amount_after_fee
|
||||
@@ -650,7 +656,7 @@ class LocalTrade():
|
||||
"""
|
||||
Update Fee parameters. Only acts once per side
|
||||
"""
|
||||
if self.enter_side == side and self.fee_open_currency is None:
|
||||
if self.entry_side == side and self.fee_open_currency is None:
|
||||
self.fee_open_cost = fee_cost
|
||||
self.fee_open_currency = fee_currency
|
||||
if fee_rate is not None:
|
||||
@@ -667,7 +673,7 @@ class LocalTrade():
|
||||
"""
|
||||
Verify if this side (buy / sell) has already been updated
|
||||
"""
|
||||
if self.enter_side == side:
|
||||
if self.entry_side == side:
|
||||
return self.fee_open_currency is not None
|
||||
elif self.exit_side == side:
|
||||
return self.fee_close_currency is not None
|
||||
@@ -840,7 +846,7 @@ class LocalTrade():
|
||||
def recalc_trade_from_orders(self):
|
||||
# We need at least 2 entry orders for averaging amounts and rates.
|
||||
# TODO: this condition could probably be removed
|
||||
if len(self.select_filled_orders(self.enter_side)) < 2:
|
||||
if len(self.select_filled_orders(self.entry_side)) < 2:
|
||||
self.stake_amount = self.amount * self.open_rate / self.leverage
|
||||
|
||||
# Just in case, still recalc open trade value
|
||||
@@ -851,7 +857,7 @@ class LocalTrade():
|
||||
total_stake = 0.0
|
||||
for o in self.orders:
|
||||
if (o.ft_is_open or
|
||||
(o.ft_order_side != self.enter_side) or
|
||||
(o.ft_order_side != self.entry_side) or
|
||||
(o.status not in NON_OPEN_EXCHANGE_STATES)):
|
||||
continue
|
||||
|
||||
@@ -919,7 +925,7 @@ class LocalTrade():
|
||||
:return: int count of entry orders that have been filled for this trade.
|
||||
"""
|
||||
|
||||
return len(self.select_filled_orders(self.enter_side))
|
||||
return len(self.select_filled_orders(self.entry_side))
|
||||
|
||||
@property
|
||||
def nr_of_successful_exits(self) -> int:
|
||||
|
@@ -85,10 +85,10 @@ class StrategyResolver(IResolver):
|
||||
("protections", None),
|
||||
("startup_candle_count", None),
|
||||
("unfilledtimeout", None),
|
||||
("use_sell_signal", True),
|
||||
("sell_profit_only", False),
|
||||
("ignore_roi_if_buy_signal", False),
|
||||
("sell_profit_offset", 0.0),
|
||||
("use_exit_signal", True),
|
||||
("exit_profit_only", False),
|
||||
("ignore_roi_if_entry_signal", False),
|
||||
("exit_profit_offset", 0.0),
|
||||
("disable_dataframe_checks", False),
|
||||
("ignore_buying_expired_candle_after", 0),
|
||||
("position_adjustment_enable", False),
|
||||
@@ -173,6 +173,12 @@ class StrategyResolver(IResolver):
|
||||
def validate_strategy(strategy: IStrategy) -> IStrategy:
|
||||
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||
# Require new method
|
||||
warn_deprecated_setting(strategy, 'sell_profit_only', 'exit_profit_only', True)
|
||||
warn_deprecated_setting(strategy, 'sell_profit_offset', 'exit_profit_offset', True)
|
||||
warn_deprecated_setting(strategy, 'use_sell_signal', 'use_exit_signal', True)
|
||||
warn_deprecated_setting(strategy, 'ignore_roi_if_buy_signal',
|
||||
'ignore_roi_if_entry_signal', True)
|
||||
|
||||
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
|
||||
raise OperationalException("`populate_entry_trend` must be implemented.")
|
||||
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
|
||||
@@ -187,9 +193,16 @@ class StrategyResolver(IResolver):
|
||||
if check_override(strategy, IStrategy, 'custom_sell'):
|
||||
raise OperationalException(
|
||||
"Please migrate your implementation of `custom_sell` to `custom_exit`.")
|
||||
|
||||
else:
|
||||
# TODO: Implementing one of the following methods should show a deprecation warning
|
||||
# buy_trend and sell_trend, custom_sell
|
||||
warn_deprecated_setting(strategy, 'sell_profit_only', 'exit_profit_only')
|
||||
warn_deprecated_setting(strategy, 'sell_profit_offset', 'exit_profit_offset')
|
||||
warn_deprecated_setting(strategy, 'use_sell_signal', 'use_exit_signal')
|
||||
warn_deprecated_setting(strategy, 'ignore_roi_if_buy_signal',
|
||||
'ignore_roi_if_entry_signal')
|
||||
|
||||
if (
|
||||
not check_override(strategy, IStrategy, 'populate_buy_trend')
|
||||
and not check_override(strategy, IStrategy, 'populate_entry_trend')
|
||||
@@ -262,6 +275,15 @@ class StrategyResolver(IResolver):
|
||||
)
|
||||
|
||||
|
||||
def warn_deprecated_setting(strategy: IStrategy, old: str, new: str, error=False):
|
||||
if hasattr(strategy, old):
|
||||
errormsg = f"DEPRECATED: Using '{old}' moved to '{new}'."
|
||||
if error:
|
||||
raise OperationalException(errormsg)
|
||||
logger.warning(errormsg)
|
||||
setattr(strategy, new, getattr(strategy, f'{old}'))
|
||||
|
||||
|
||||
def check_override(object, parentclass, attribute):
|
||||
"""
|
||||
Checks if a object overrides the parent class attribute.
|
||||
|
@@ -86,7 +86,7 @@ class CryptoToFiatConverter:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
|
||||
found = [x for x in self._coinlistings if x['symbol'].lower() == crypto_symbol]
|
||||
|
||||
if crypto_symbol in coingecko_mapping.keys():
|
||||
found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]]
|
||||
|
@@ -695,7 +695,7 @@ class RPC:
|
||||
if trade.open_order_id:
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
|
||||
if order['side'] == trade.enter_side:
|
||||
if order['side'] == trade.entry_side:
|
||||
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||
trade, order, CANCEL_REASON['FORCE_EXIT'])
|
||||
|
||||
|
@@ -233,11 +233,11 @@ class Telegram(RPCHandler):
|
||||
is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL]
|
||||
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
|
||||
|
||||
enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
|
||||
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
|
||||
else {'enter': 'Short', 'entered': 'Shorted'})
|
||||
message = (
|
||||
f"{emoji} *{msg['exchange']}:*"
|
||||
f" {enter_side['entered'] if is_fill else enter_side['enter']} {msg['pair']}"
|
||||
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
||||
f" (#{msg['trade_id']})\n"
|
||||
)
|
||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
||||
|
@@ -90,10 +90,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# run "populate_indicators" only for new candle
|
||||
process_only_new_candles: bool = False
|
||||
|
||||
use_sell_signal: bool
|
||||
sell_profit_only: bool
|
||||
sell_profit_offset: float
|
||||
ignore_roi_if_buy_signal: bool
|
||||
use_exit_signal: bool
|
||||
exit_profit_only: bool
|
||||
exit_profit_offset: float
|
||||
ignore_roi_if_entry_signal: bool
|
||||
|
||||
# Position adjustment is disabled by default
|
||||
position_adjustment_enable: bool = False
|
||||
@@ -871,7 +871,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
|
||||
roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
|
||||
roi_reached = (not (enter and self.ignore_roi_if_entry_signal)
|
||||
and self.min_roi_reached(trade=trade, current_profit=current_profit,
|
||||
current_time=current_time))
|
||||
|
||||
@@ -881,10 +881,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
current_rate = rate
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
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
|
||||
if (self.exit_profit_only and current_profit <= self.exit_profit_offset):
|
||||
# exit_profit_only and profit doesn't reach the offset - ignore sell signal
|
||||
pass
|
||||
elif self.use_sell_signal and not enter:
|
||||
elif self.use_exit_signal and not enter:
|
||||
if exit_:
|
||||
exit_signal = ExitType.EXIT_SIGNAL
|
||||
else:
|
||||
@@ -1044,7 +1044,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
FT Internal method.
|
||||
Check if timeout is active, and if the order is still open and timed out
|
||||
"""
|
||||
side = 'entry' if order.ft_order_side == trade.enter_side else 'exit'
|
||||
side = 'entry' if order.ft_order_side == trade.entry_side else 'exit'
|
||||
|
||||
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
||||
if timeout is not None:
|
||||
|
@@ -65,9 +65,9 @@ class {{ strategy }}(IStrategy):
|
||||
process_only_new_candles = False
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_sell_signal = True
|
||||
sell_profit_only = False
|
||||
ignore_roi_if_buy_signal = False
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = False
|
||||
|
||||
# Number of candles the strategy requires before producing valid signals
|
||||
startup_candle_count: int = 30
|
||||
|
@@ -65,9 +65,9 @@ class SampleStrategy(IStrategy):
|
||||
process_only_new_candles = False
|
||||
|
||||
# These values can be overridden in the config.
|
||||
use_sell_signal = True
|
||||
sell_profit_only = False
|
||||
ignore_roi_if_buy_signal = False
|
||||
use_exit_signal = True
|
||||
exit_profit_only = False
|
||||
ignore_roi_if_entry_signal = False
|
||||
|
||||
# Hyperoptable parameters
|
||||
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
|
||||
|
Reference in New Issue
Block a user