Merge branch 'develop' into partial_sell2
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'
|
||||
|
||||
@@ -94,8 +94,8 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
||||
:raise: OperationalException if config validation failed
|
||||
"""
|
||||
if (not conf.get('edge', {}).get('enabled')
|
||||
and conf.get('max_open_trades') == float('inf')
|
||||
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
||||
and conf.get('max_open_trades') == float('inf')
|
||||
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT):
|
||||
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -243,7 +244,9 @@ def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||
def _validate_order_types(conf: Dict[str, Any]) -> None:
|
||||
|
||||
order_types = conf.get('order_types', {})
|
||||
if any(x in order_types for x in ['buy', 'sell', 'emergencysell', 'forcebuy', 'forcesell']):
|
||||
old_order_types = ['buy', 'sell', 'emergencysell', 'forcebuy',
|
||||
'forcesell', 'emergencyexit', 'forceexit', 'forceentry']
|
||||
if any(x in order_types for x in old_order_types):
|
||||
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||
raise OperationalException(
|
||||
"Please migrate your order_types settings to use the new wording.")
|
||||
@@ -255,9 +258,12 @@ def _validate_order_types(conf: Dict[str, Any]) -> None:
|
||||
for o, n in [
|
||||
('buy', 'entry'),
|
||||
('sell', 'exit'),
|
||||
('emergencysell', 'emergencyexit'),
|
||||
('forcesell', 'forceexit'),
|
||||
('forcebuy', 'forceentry'),
|
||||
('emergencysell', 'emergency_exit'),
|
||||
('forcesell', 'force_exit'),
|
||||
('forcebuy', 'force_entry'),
|
||||
('emergencyexit', 'emergency_exit'),
|
||||
('forceexit', 'force_exit'),
|
||||
('forceentry', 'force_entry'),
|
||||
]:
|
||||
|
||||
process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
|
||||
@@ -312,3 +318,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')
|
||||
|
||||
@@ -12,7 +12,7 @@ from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
||||
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
||||
from freqtrade.configuration.load_config import load_config_file, load_file
|
||||
from freqtrade.configuration.load_config import load_file, load_from_files
|
||||
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import setup_logging
|
||||
@@ -55,45 +55,28 @@ class Configuration:
|
||||
:param files: List of file paths
|
||||
:return: configuration dictionary
|
||||
"""
|
||||
# Keep this method as staticmethod, so it can be used from interactive environments
|
||||
c = Configuration({'config': files}, RunMode.OTHER)
|
||||
return c.get_config()
|
||||
|
||||
def load_from_files(self, files: List[str]) -> Dict[str, Any]:
|
||||
|
||||
# Keep this method as staticmethod, so it can be used from interactive environments
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
if not files:
|
||||
return deepcopy(constants.MINIMAL_CONFIG)
|
||||
|
||||
# We expect here a list of config filenames
|
||||
for path in files:
|
||||
logger.info(f'Using config: {path} ...')
|
||||
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(load_config_file(path), config)
|
||||
|
||||
# Load environment variables
|
||||
env_data = enironment_vars_to_dict()
|
||||
config = deep_merge_dicts(env_data, config)
|
||||
|
||||
config['config_files'] = files
|
||||
# Normalize config
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
|
||||
if 'pairlists' not in config:
|
||||
config['pairlists'] = []
|
||||
|
||||
return config
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load the bot configuration
|
||||
:return: Configuration dictionary
|
||||
"""
|
||||
# Load all configs
|
||||
config: Dict[str, Any] = self.load_from_files(self.args.get("config", []))
|
||||
config: Dict[str, Any] = load_from_files(self.args.get("config", []))
|
||||
|
||||
# Load environment variables
|
||||
env_data = enironment_vars_to_dict()
|
||||
config = deep_merge_dicts(env_data, config)
|
||||
|
||||
# Normalize config
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
|
||||
if 'pairlists' not in config:
|
||||
config['pairlists'] = []
|
||||
|
||||
# Keep a copy of the original configuration file
|
||||
config['original_config'] = deepcopy(config)
|
||||
@@ -164,8 +147,8 @@ class Configuration:
|
||||
config.update({'db_url': self.args['db_url']})
|
||||
logger.info('Parameter --db-url detected ...')
|
||||
|
||||
if config.get('forcebuy_enable', False):
|
||||
logger.warning('`forcebuy` RPC message enabled.')
|
||||
if config.get('force_entry_enable', False):
|
||||
logger.warning('`force_entry_enable` RPC message enabled.')
|
||||
|
||||
# Support for sd_notify
|
||||
if 'sd_notify' in self.args and self.args['sd_notify']:
|
||||
@@ -433,8 +416,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,17 +48,18 @@ 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_1 = f"{section_old}.{name_old}" if section_old else f"{name_old}"
|
||||
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||
logger.warning(
|
||||
"DEPRECATED: "
|
||||
f"The `{section_old}.{name_old}` setting is deprecated and "
|
||||
f"The `{section_1}` setting is deprecated and "
|
||||
"will be removed in the next versions of Freqtrade. "
|
||||
f"Please use the `{section_2}` setting in your configuration instead."
|
||||
)
|
||||
@@ -72,16 +74,12 @@ 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')
|
||||
|
||||
process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable')
|
||||
|
||||
# New settings
|
||||
if config.get('telegram'):
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
|
||||
@@ -109,13 +107,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(
|
||||
|
||||
@@ -4,12 +4,15 @@ This module contain functions to load the configuration file
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import rapidjson
|
||||
|
||||
from freqtrade.constants import MINIMAL_CONFIG
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -70,3 +73,21 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def load_from_files(files: List[str]) -> Dict[str, Any]:
|
||||
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
if not files:
|
||||
return deepcopy(MINIMAL_CONFIG)
|
||||
|
||||
# We expect here a list of config filenames
|
||||
for path in files:
|
||||
logger.info(f'Using config: {path} ...')
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(load_config_file(path), config)
|
||||
|
||||
config['config_files'] = files
|
||||
|
||||
return config
|
||||
|
||||
@@ -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},
|
||||
@@ -216,9 +216,9 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'forceexit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'forceentry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencyexit': {
|
||||
'force_exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'force_entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergency_exit': {
|
||||
'type': 'string',
|
||||
'enum': ORDERTYPE_POSSIBILITIES,
|
||||
'default': 'market'},
|
||||
@@ -358,7 +358,7 @@ CONF_SCHEMA = {
|
||||
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
|
||||
'disableparamexport': {'type': 'boolean'},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'forcebuy_enable': {'type': 'boolean'},
|
||||
'force_entry_enable': {'type': 'boolean'},
|
||||
'disable_dataframe_checks': {'type': 'boolean'},
|
||||
'internals': {
|
||||
'type': 'object',
|
||||
|
||||
@@ -192,7 +192,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
|
||||
# Protect from collisions with forceexit.
|
||||
# Protect from collisions with force_exit.
|
||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||
# while exiting is in process, since telegram messages arrive in an different thread.
|
||||
with self._exit_lock:
|
||||
@@ -331,12 +331,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)
|
||||
@@ -365,7 +365,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:
|
||||
@@ -570,9 +570,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}"
|
||||
@@ -619,6 +619,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair, price, stake_amount, trade_side, enter_tag, trade)
|
||||
|
||||
if not stake_amount:
|
||||
logger.info(f"No stake amount to enter a trade for {pair}.")
|
||||
return False
|
||||
|
||||
if pos_adjust:
|
||||
@@ -949,8 +950,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)
|
||||
|
||||
@@ -1159,7 +1160,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)
|
||||
|
||||
@@ -1200,7 +1201,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:
|
||||
@@ -1239,7 +1240,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
|
||||
@@ -1270,7 +1271,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()
|
||||
@@ -1405,7 +1406,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_type = ordertype or self.strategy.order_types[exit_type]
|
||||
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergencyexit", "market")
|
||||
order_type = self.strategy.order_types.get("emergency_exit", "market")
|
||||
|
||||
amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
@@ -1618,7 +1619,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
if order.get('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:
|
||||
@@ -511,7 +510,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:
|
||||
@@ -520,15 +519,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:
|
||||
@@ -536,7 +535,7 @@ class Backtesting:
|
||||
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
try:
|
||||
close_rate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
close_rate = self._get_close_rate(row, trade, sell, trade_dur)
|
||||
except ValueError:
|
||||
return None
|
||||
# call the custom exit price,with default value as previous close_rate
|
||||
@@ -553,9 +552,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:
|
||||
close_rate = min(close_rate, sell_row[HIGH_IDX])
|
||||
close_rate = min(close_rate, row[HIGH_IDX])
|
||||
else:
|
||||
close_rate = max(close_rate, sell_row[LOW_IDX])
|
||||
close_rate = max(close_rate, row[LOW_IDX])
|
||||
# Confirm trade exit:
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
|
||||
@@ -571,13 +570,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]
|
||||
|
||||
return self._exit_trade(trade, sell_row, close_rate)
|
||||
|
||||
@@ -612,8 +611,8 @@ class Backtesting:
|
||||
trade.orders.append(order)
|
||||
return trade
|
||||
|
||||
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(
|
||||
@@ -634,13 +633,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():
|
||||
@@ -651,7 +650,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],
|
||||
@@ -794,8 +793,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,
|
||||
@@ -877,7 +876,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.
|
||||
@@ -992,7 +991,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
|
||||
}
|
||||
|
||||
@@ -374,6 +374,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:
|
||||
@@ -414,7 +420,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,
|
||||
@@ -604,7 +610,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
|
||||
@@ -697,7 +703,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:
|
||||
@@ -714,7 +720,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
|
||||
@@ -960,7 +966,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.
|
||||
|
||||
@@ -140,9 +140,9 @@ class UnfilledTimeout(BaseModel):
|
||||
class OrderTypes(BaseModel):
|
||||
entry: OrderTypeValues
|
||||
exit: OrderTypeValues
|
||||
emergencyexit: Optional[OrderTypeValues]
|
||||
forceexit: Optional[OrderTypeValues]
|
||||
forceentry: Optional[OrderTypeValues]
|
||||
emergency_exit: Optional[OrderTypeValues]
|
||||
force_exit: Optional[OrderTypeValues]
|
||||
force_entry: Optional[OrderTypeValues]
|
||||
stoploss: OrderTypeValues
|
||||
stoploss_on_exchange: bool
|
||||
stoploss_on_exchange_interval: Optional[int]
|
||||
@@ -174,7 +174,7 @@ class ShowConfig(BaseModel):
|
||||
timeframe_min: int
|
||||
exchange: str
|
||||
strategy: Optional[str]
|
||||
forcebuy_enabled: bool
|
||||
force_entry_enable: bool
|
||||
exit_pricing: Dict[str, Any]
|
||||
entry_pricing: Dict[str, Any]
|
||||
bot_name: str
|
||||
|
||||
@@ -135,13 +135,13 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
||||
return resp
|
||||
|
||||
|
||||
# /forcebuy is deprecated with short addition. use ForceEntry instead
|
||||
# /forcebuy is deprecated with short addition. use /forceentry instead
|
||||
@router.post('/forceenter', response_model=ForceEnterResponse, tags=['trading'])
|
||||
@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
|
||||
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
stake_amount = payload.stakeamount if payload.stakeamount else None
|
||||
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
|
||||
entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry'
|
||||
|
||||
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
|
||||
order_type=ordertype, stake_amount=stake_amount,
|
||||
@@ -154,11 +154,12 @@ def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."})
|
||||
|
||||
|
||||
# /forcesell is deprecated with short addition. use /forceexit instead
|
||||
@router.post('/forceexit', response_model=ResultMsg, tags=['trading'])
|
||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||
def forcesell(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
return rpc._rpc_forceexit(payload.tradeid, ordertype)
|
||||
return rpc._rpc_force_exit(payload.tradeid, ordertype)
|
||||
|
||||
|
||||
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -136,7 +136,7 @@ class RPC:
|
||||
) if 'timeframe' in config else 0,
|
||||
'exchange': config['exchange']['name'],
|
||||
'strategy': config['strategy'],
|
||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||
'force_entry_enable': config.get('force_entry_enable', False),
|
||||
'exit_pricing': config.get('exit_pricing', {}),
|
||||
'entry_pricing': config.get('entry_pricing', {}),
|
||||
'state': str(botstate),
|
||||
@@ -684,7 +684,7 @@ class RPC:
|
||||
|
||||
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||
|
||||
def _rpc_forceexit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||
"""
|
||||
Handler for forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
@@ -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'])
|
||||
|
||||
@@ -709,7 +709,7 @@ class RPC:
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
|
||||
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
|
||||
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||
"forceexit", self._freqtrade.strategy.order_types["exit"])
|
||||
"force_exit", self._freqtrade.strategy.order_types["exit"])
|
||||
|
||||
self._freqtrade.execute_trade_exit(
|
||||
trade, current_rate, exit_check, ordertype=order_type)
|
||||
@@ -732,7 +732,7 @@ class RPC:
|
||||
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
|
||||
).first()
|
||||
if not trade:
|
||||
logger.warning('forceexit: Invalid argument received')
|
||||
logger.warning('force_exit: Invalid argument received')
|
||||
raise RPCException('invalid argument')
|
||||
|
||||
_exec_forcesell(trade)
|
||||
@@ -744,14 +744,14 @@ class RPC:
|
||||
order_type: Optional[str] = None,
|
||||
order_side: SignalDirection = SignalDirection.LONG,
|
||||
stake_amount: Optional[float] = None,
|
||||
enter_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
|
||||
enter_tag: Optional[str] = 'force_entry') -> Optional[Trade]:
|
||||
"""
|
||||
Handler for forcebuy <asset> <price>
|
||||
Buys a pair trade at the given or current price
|
||||
"""
|
||||
|
||||
if not self._freqtrade.config.get('forcebuy_enable', False):
|
||||
raise RPCException('Forceentry not enabled.')
|
||||
if not self._freqtrade.config.get('force_entry_enable', False):
|
||||
raise RPCException('Force_entry not enabled.')
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('trader is not running')
|
||||
@@ -781,7 +781,7 @@ class RPC:
|
||||
# execute buy
|
||||
if not order_type:
|
||||
order_type = self._freqtrade.strategy.order_types.get(
|
||||
'forceentry', self._freqtrade.strategy.order_types['entry'])
|
||||
'force_entry', self._freqtrade.strategy.order_types['entry'])
|
||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
||||
ordertype=order_type, trade=trade,
|
||||
is_short=is_short,
|
||||
@@ -791,7 +791,7 @@ class RPC:
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
return trade
|
||||
else:
|
||||
return None
|
||||
raise RPCException(f'Failed to enter position for {pair}.')
|
||||
|
||||
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
|
||||
"""
|
||||
|
||||
@@ -153,11 +153,11 @@ class Telegram(RPCHandler):
|
||||
CommandHandler('balance', self._balance),
|
||||
CommandHandler('start', self._start),
|
||||
CommandHandler('stop', self._stop),
|
||||
CommandHandler(['forcesell', 'forceexit'], self._forceexit),
|
||||
CommandHandler(['forcesell', 'forceexit', 'fx'], self._force_exit),
|
||||
CommandHandler(['forcebuy', 'forcelong'], partial(
|
||||
self._forceenter, order_side=SignalDirection.LONG)),
|
||||
self._force_enter, order_side=SignalDirection.LONG)),
|
||||
CommandHandler('forceshort', partial(
|
||||
self._forceenter, order_side=SignalDirection.SHORT)),
|
||||
self._force_enter, order_side=SignalDirection.SHORT)),
|
||||
CommandHandler('trades', self._trades),
|
||||
CommandHandler('delete', self._delete_trade),
|
||||
CommandHandler('performance', self._performance),
|
||||
@@ -197,7 +197,7 @@ class Telegram(RPCHandler):
|
||||
pattern='update_exit_reason_performance'),
|
||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||
CallbackQueryHandler(self._forceenter_inline),
|
||||
CallbackQueryHandler(self._force_enter_inline),
|
||||
]
|
||||
for handle in handles:
|
||||
self._updater.dispatcher.add_handler(handle)
|
||||
@@ -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 ""
|
||||
@@ -974,7 +974,7 @@ class Telegram(RPCHandler):
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _forceexit(self, update: Update, context: CallbackContext) -> None:
|
||||
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
@@ -988,20 +988,20 @@ class Telegram(RPCHandler):
|
||||
self._send_msg("You must specify a trade-id or 'all'.")
|
||||
return
|
||||
try:
|
||||
msg = self._rpc._rpc_forceexit(trade_id)
|
||||
self._send_msg('Forceexit Result: `{result}`'.format(**msg))
|
||||
msg = self._rpc._rpc_force_exit(trade_id)
|
||||
self._send_msg('Force_exit Result: `{result}`'.format(**msg))
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
if pair != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _forceenter_inline(self, update: Update, _: CallbackContext) -> None:
|
||||
def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
|
||||
if update.callback_query:
|
||||
query = update.callback_query
|
||||
if query.data and '_||_' in query.data:
|
||||
@@ -1009,7 +1009,7 @@ class Telegram(RPCHandler):
|
||||
order_side = SignalDirection(side)
|
||||
query.answer()
|
||||
query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
|
||||
self._forceenter_action(pair, None, order_side)
|
||||
self._force_enter_action(pair, None, order_side)
|
||||
|
||||
@staticmethod
|
||||
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
|
||||
@@ -1017,7 +1017,7 @@ class Telegram(RPCHandler):
|
||||
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||
|
||||
@authorized_only
|
||||
def _forceenter(
|
||||
def _force_enter(
|
||||
self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None:
|
||||
"""
|
||||
Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
|
||||
@@ -1029,7 +1029,7 @@ class Telegram(RPCHandler):
|
||||
if context.args:
|
||||
pair = context.args[0]
|
||||
price = float(context.args[1]) if len(context.args) > 1 else None
|
||||
self._forceenter_action(pair, price, order_side)
|
||||
self._force_enter_action(pair, price, order_side)
|
||||
else:
|
||||
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
||||
pair_buttons = [
|
||||
@@ -1407,23 +1407,25 @@ class Telegram(RPCHandler):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
forceenter_text = ("*/forcelong <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||
"Optionally takes a rate at which to buy "
|
||||
"(only applies to limit orders).` \n"
|
||||
)
|
||||
force_enter_text = ("*/forcelong <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||
"Optionally takes a rate at which to buy "
|
||||
"(only applies to limit orders).` \n"
|
||||
)
|
||||
if self._rpc._freqtrade.trading_mode != TradingMode.SPOT:
|
||||
forceenter_text += ("*/forceshort <pair> [<rate>]:* `Instantly shorts the given pair. "
|
||||
"Optionally takes a rate at which to sell "
|
||||
"(only applies to limit orders).` \n")
|
||||
force_enter_text += ("*/forceshort <pair> [<rate>]:* `Instantly shorts the given pair. "
|
||||
"Optionally takes a rate at which to sell "
|
||||
"(only applies to limit orders).` \n")
|
||||
message = (
|
||||
"_BotControl_\n"
|
||||
"------------\n"
|
||||
"*/start:* `Starts the trader`\n"
|
||||
"*/stop:* Stops the trader\n"
|
||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||
# TODO: forceenter forceshort forcelong missing
|
||||
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
||||
"regardless of profit`\n"
|
||||
f"{forceenter_text if self._config.get('forcebuy_enable', False) else ''}"
|
||||
"*/fe <trade_id>|all:* `Alias to /forceexit`"
|
||||
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
|
||||
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||
"*/whitelist:* `Show current whitelist` \n"
|
||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||
|
||||
@@ -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:
|
||||
@@ -1049,7 +1049,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:
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"force_entry_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"order_types": {
|
||||
"entry": "limit",
|
||||
"exit": "limit",
|
||||
"emergencyexit": "limit",
|
||||
"emergency_exit": "limit",
|
||||
"stoploss": "limit",
|
||||
"stoploss_on_exchange": false
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user