Merge branch 'develop' into pr/cyberjunky/6615
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,25 +74,51 @@ 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')
|
||||
|
||||
# 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_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable')
|
||||
|
||||
# New settings
|
||||
if config.get('telegram'):
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
|
||||
'notification_settings', 'exit')
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill',
|
||||
'notification_settings', 'exit_fill')
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel',
|
||||
'notification_settings', 'exit_cancel')
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy',
|
||||
'notification_settings', 'entry')
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_fill',
|
||||
'notification_settings', 'entry_fill')
|
||||
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_cancel',
|
||||
'notification_settings', 'entry_cancel')
|
||||
if config.get('webhook'):
|
||||
process_deprecated_setting(config, 'webhook', 'webhookbuy', 'webhook', 'webhookentry')
|
||||
process_deprecated_setting(config, 'webhook', 'webhookbuycancel',
|
||||
'webhook', 'webhookentrycancel')
|
||||
process_deprecated_setting(config, 'webhook', 'webhookbuyfill',
|
||||
'webhook', 'webhookentryfill')
|
||||
process_deprecated_setting(config, 'webhook', 'webhooksell', 'webhook', 'webhookexit')
|
||||
process_deprecated_setting(config, 'webhook', 'webhooksellcancel',
|
||||
'webhook', 'webhookexitcancel')
|
||||
process_deprecated_setting(config, 'webhook', 'webhooksellfill',
|
||||
'webhook', 'webhookexitfill')
|
||||
|
||||
# Legacy way - having them in experimental ...
|
||||
|
||||
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
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
bot constants
|
||||
"""
|
||||
from typing import List, Tuple
|
||||
from typing import List, Literal, Tuple
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
@@ -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'},
|
||||
@@ -285,21 +285,21 @@ CONF_SCHEMA = {
|
||||
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'buy_fill': {'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
'sell': {
|
||||
'entry': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'entry_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'entry_fill': {'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
'exit': {
|
||||
'type': ['string', 'object'],
|
||||
'additionalProperties': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS
|
||||
}
|
||||
},
|
||||
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'sell_fill': {
|
||||
'exit_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'exit_fill': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
@@ -327,12 +327,12 @@ CONF_SCHEMA = {
|
||||
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||
'retries': {'type': 'integer', 'minimum': 0},
|
||||
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||
'webhookbuy': {'type': 'object'},
|
||||
'webhookbuycancel': {'type': 'object'},
|
||||
'webhookbuyfill': {'type': 'object'},
|
||||
'webhooksell': {'type': 'object'},
|
||||
'webhooksellcancel': {'type': 'object'},
|
||||
'webhooksellfill': {'type': 'object'},
|
||||
'webhookentry': {'type': 'object'},
|
||||
'webhookentrycancel': {'type': 'object'},
|
||||
'webhookentryfill': {'type': 'object'},
|
||||
'webhookexit': {'type': 'object'},
|
||||
'webhookexitcancel': {'type': 'object'},
|
||||
'webhookexitfill': {'type': 'object'},
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
@@ -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',
|
||||
@@ -478,7 +478,7 @@ CANCEL_REASON = {
|
||||
"FULLY_CANCELLED": "fully cancelled",
|
||||
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
|
||||
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
|
||||
"FORCE_SELL": "forcesold",
|
||||
"FORCE_EXIT": "forcesold",
|
||||
}
|
||||
|
||||
# List of pairs with their timeframes
|
||||
@@ -487,3 +487,6 @@ ListPairsWithTimeframes = List[PairWithTimeframe]
|
||||
|
||||
# Type for trades list
|
||||
TradeList = List[List]
|
||||
|
||||
LongShort = Literal['long', 'short']
|
||||
EntryExit = Literal['entry', 'exit']
|
||||
|
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||
'open_rate', 'close_rate',
|
||||
'fee_open', 'fee_close', 'trade_duration',
|
||||
'profit_ratio', 'profit_abs', 'sell_reason',
|
||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||
'is_short'
|
||||
|
@@ -470,7 +470,7 @@ class Edge:
|
||||
if len(ohlc_columns) - 1 < exit_index:
|
||||
break
|
||||
|
||||
exit_type = ExitType.SELL_SIGNAL
|
||||
exit_type = ExitType.EXIT_SIGNAL
|
||||
exit_price = ohlc_columns[exit_index, 0]
|
||||
|
||||
trade = {'pair': pair,
|
||||
|
@@ -3,16 +3,16 @@ from enum import Enum
|
||||
|
||||
class ExitType(Enum):
|
||||
"""
|
||||
Enum to distinguish between sell reasons
|
||||
Enum to distinguish between exit reasons
|
||||
"""
|
||||
ROI = "roi"
|
||||
STOP_LOSS = "stop_loss"
|
||||
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||
SELL_SIGNAL = "sell_signal"
|
||||
FORCE_SELL = "force_sell"
|
||||
EMERGENCY_SELL = "emergency_sell"
|
||||
CUSTOM_SELL = "custom_sell"
|
||||
EXIT_SIGNAL = "exit_signal"
|
||||
FORCE_EXIT = "force_exit"
|
||||
EMERGENCY_EXIT = "emergency_exit"
|
||||
CUSTOM_EXIT = "custom_exit"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
|
@@ -6,19 +6,13 @@ class RPCMessageType(Enum):
|
||||
WARNING = 'warning'
|
||||
STARTUP = 'startup'
|
||||
|
||||
BUY = 'buy'
|
||||
BUY_FILL = 'buy_fill'
|
||||
BUY_CANCEL = 'buy_cancel'
|
||||
ENTRY = 'entry'
|
||||
ENTRY_FILL = 'entry_fill'
|
||||
ENTRY_CANCEL = 'entry_cancel'
|
||||
|
||||
SHORT = 'short'
|
||||
SHORT_FILL = 'short_fill'
|
||||
SHORT_CANCEL = 'short_cancel'
|
||||
|
||||
# TODO: The below messagetypes should be renamed to "exit"!
|
||||
# Careful - has an impact on webhooks, therefore needs proper communication
|
||||
SELL = 'sell'
|
||||
SELL_FILL = 'sell_fill'
|
||||
SELL_CANCEL = 'sell_cancel'
|
||||
EXIT = 'exit'
|
||||
EXIT_FILL = 'exit_fill'
|
||||
EXIT_CANCEL = 'exit_cancel'
|
||||
|
||||
PROTECTION_TRIGGER = 'protection_trigger'
|
||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||
|
@@ -102,7 +102,7 @@ def calculate_backoff(retrycount, max_retries):
|
||||
def retrier_async(f):
|
||||
async def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
kucoin = args[0].name == "Kucoin" # Check if the exchange is KuCoin.
|
||||
kucoin = args[0].name == "KuCoin" # Check if the exchange is KuCoin.
|
||||
try:
|
||||
return await f(*args, **kwargs)
|
||||
except TemporaryError as ex:
|
||||
|
@@ -20,7 +20,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
|
||||
ListPairsWithTimeframes, PairWithTimeframe)
|
||||
EntryExit, ListPairsWithTimeframes, PairWithTimeframe)
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
@@ -1429,7 +1429,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_rate(self, pair: str, refresh: bool,
|
||||
side: Literal['entry', 'exit'], is_short: bool) -> float:
|
||||
side: EntryExit, is_short: bool) -> float:
|
||||
"""
|
||||
Calculates bid/ask target
|
||||
bid rate - between current ask price and last price
|
||||
|
@@ -7,12 +7,13 @@ import traceback
|
||||
from datetime import datetime, time, timezone
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from schedule import Scheduler
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.constants import LongShort
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
@@ -190,7 +191,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:
|
||||
@@ -329,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)
|
||||
@@ -363,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:
|
||||
@@ -548,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}"
|
||||
@@ -590,7 +591,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
time_in_force = self.strategy.order_time_in_force['entry']
|
||||
|
||||
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
|
||||
trade_side: Literal['long', 'short'] = 'short' if is_short else 'long'
|
||||
trade_side: LongShort = 'short' if is_short else 'long'
|
||||
pos_adjust = trade is not None
|
||||
|
||||
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
|
||||
@@ -746,7 +747,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def get_valid_enter_price_and_stake(
|
||||
self, pair: str, price: Optional[float], stake_amount: float,
|
||||
trade_side: Literal['long', 'short'],
|
||||
trade_side: LongShort,
|
||||
entry_tag: Optional[str],
|
||||
trade: Optional[Trade]
|
||||
) -> Tuple[float, float, float]:
|
||||
@@ -760,7 +761,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=proposed_enter_rate)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
proposed_rate=proposed_enter_rate, entry_tag=entry_tag)
|
||||
proposed_rate=proposed_enter_rate, entry_tag=entry_tag,
|
||||
side=trade_side,
|
||||
)
|
||||
|
||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||
|
||||
@@ -816,10 +819,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Sends rpc notification when a entry order occurred.
|
||||
"""
|
||||
if fill:
|
||||
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
||||
else:
|
||||
msg_type = RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY
|
||||
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
|
||||
open_rate = safe_value_fallback(order, 'average', 'price')
|
||||
if open_rate is None:
|
||||
open_rate = trade.open_rate
|
||||
@@ -858,10 +858,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
|
||||
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': msg_type,
|
||||
'type': RPCMessageType.ENTRY_CANCEL,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
@@ -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)
|
||||
|
||||
@@ -978,7 +978,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Exiting the trade forcefully')
|
||||
self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.EMERGENCY_SELL))
|
||||
exit_type=ExitType.EMERGENCY_EXIT))
|
||||
|
||||
except ExchangeError:
|
||||
trade.stoploss_order_id = None
|
||||
@@ -1010,7 +1010,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
|
||||
trade.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||
stoploss_order=True)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
@@ -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)
|
||||
|
||||
@@ -1159,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
try:
|
||||
self.execute_trade_exit(
|
||||
trade, order.get('price'),
|
||||
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_SELL))
|
||||
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
|
||||
except DependencyException as exception:
|
||||
logger.warning(
|
||||
f'Unable to emergency sell trade {trade.pair}: {exception}')
|
||||
@@ -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()
|
||||
@@ -1286,7 +1286,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.sell_reason = None
|
||||
trade.exit_reason = None
|
||||
cancelled = True
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
@@ -1377,9 +1377,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
|
||||
order_type = ordertype or self.strategy.order_types[exit_type]
|
||||
if exit_check.exit_type == ExitType.EMERGENCY_SELL:
|
||||
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, trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['exit']
|
||||
@@ -1414,9 +1414,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.orders.append(order_obj)
|
||||
|
||||
trade.open_order_id = order['id']
|
||||
trade.sell_order_status = ''
|
||||
trade.exit_order_status = ''
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = exit_tag or exit_check.exit_reason
|
||||
trade.exit_reason = exit_tag or exit_check.exit_reason
|
||||
|
||||
# Lock pair for one candle to prevent immediate re-trading
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
@@ -1443,8 +1443,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
'type': (RPCMessageType.SELL_FILL if fill
|
||||
else RPCMessageType.SELL),
|
||||
'type': (RPCMessageType.EXIT_FILL if fill
|
||||
else RPCMessageType.EXIT),
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
@@ -1461,7 +1461,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'sell_reason': trade.exit_reason, # Deprecated
|
||||
'exit_reason': trade.exit_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
@@ -1480,10 +1481,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occurred.
|
||||
"""
|
||||
if trade.sell_order_status == reason:
|
||||
if trade.exit_order_status == reason:
|
||||
return
|
||||
else:
|
||||
trade.sell_order_status = reason
|
||||
trade.exit_order_status = reason
|
||||
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
@@ -1493,7 +1494,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_CANCEL,
|
||||
'type': RPCMessageType.EXIT_CANCEL,
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
@@ -1509,7 +1510,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'sell_reason': trade.exit_reason, # Deprecated
|
||||
'exit_reason': trade.exit_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
@@ -1575,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()
|
||||
|
@@ -14,7 +14,7 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LongShort
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
||||
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||
@@ -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,13 +522,13 @@ 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
|
||||
current_profit = trade.calc_profit_ratio(closerate)
|
||||
order_type = self.strategy.order_types['exit']
|
||||
if sell.exit_type in (ExitType.SELL_SIGNAL, ExitType.CUSTOM_SELL):
|
||||
if sell.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
|
||||
# Custom exit pricing only for sell-signals
|
||||
if order_type == 'limit':
|
||||
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||
@@ -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']
|
||||
|
||||
@@ -555,16 +554,16 @@ class Backtesting:
|
||||
current_time=sell_candle_time):
|
||||
return None
|
||||
|
||||
trade.sell_reason = sell.exit_reason
|
||||
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.sell_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,11 +630,11 @@ 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],
|
||||
direction: str, current_time: datetime, entry_tag: Optional[str],
|
||||
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
||||
trade: Optional[LocalTrade], order_type: str
|
||||
) -> Tuple[float, float, float, float]:
|
||||
|
||||
@@ -643,7 +642,9 @@ class Backtesting:
|
||||
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=propose_rate)(
|
||||
pair=pair, current_time=current_time,
|
||||
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
|
||||
proposed_rate=propose_rate, entry_tag=entry_tag,
|
||||
side=direction,
|
||||
) # default value is the open rate
|
||||
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
|
||||
# which freqtrade does not support in live.
|
||||
if direction == "short":
|
||||
@@ -694,7 +695,7 @@ class Backtesting:
|
||||
|
||||
return propose_rate, stake_amount_val, leverage, min_stake_amount
|
||||
|
||||
def _enter_trade(self, pair: str, row: Tuple, direction: str,
|
||||
def _enter_trade(self, pair: str, row: Tuple, direction: LongShort,
|
||||
stake_amount: Optional[float] = None,
|
||||
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
|
||||
|
||||
@@ -772,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,
|
||||
@@ -810,7 +811,7 @@ class Backtesting:
|
||||
sell_row = data[pair][-1]
|
||||
|
||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||
trade.sell_reason = ExitType.FORCE_SELL.value
|
||||
trade.exit_reason = ExitType.FORCE_EXIT.value
|
||||
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
# Deepcopy object to have wallets update correctly
|
||||
@@ -827,7 +828,7 @@ class Backtesting:
|
||||
self.rejected_trades += 1
|
||||
return False
|
||||
|
||||
def check_for_trade_entry(self, row) -> Optional[str]:
|
||||
def check_for_trade_entry(self, row) -> Optional[LongShort]:
|
||||
enter_long = row[LONG_IDX] == 1
|
||||
exit_long = row[ELONG_IDX] == 1
|
||||
enter_short = self._can_short and row[SHORT_IDX] == 1
|
||||
@@ -855,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.
|
||||
@@ -970,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
|
||||
|
@@ -166,7 +166,7 @@ def generate_tag_metrics(tag_type: str,
|
||||
return []
|
||||
|
||||
|
||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param max_open_trades: Max_open_trades parameter
|
||||
@@ -175,8 +175,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
|
||||
"""
|
||||
tabular_data = []
|
||||
|
||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||
result = results.loc[results['sell_reason'] == reason]
|
||||
for reason, count in results['exit_reason'].value_counts().iteritems():
|
||||
result = results.loc[results['exit_reason'] == reason]
|
||||
|
||||
profit_mean = result['profit_ratio'].mean()
|
||||
profit_sum = result['profit_ratio'].sum()
|
||||
@@ -184,7 +184,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
|
||||
|
||||
tabular_data.append(
|
||||
{
|
||||
'sell_reason': reason,
|
||||
'exit_reason': reason,
|
||||
'trades': count,
|
||||
'wins': len(result[result['profit_abs'] > 0]),
|
||||
'draws': len(result[result['profit_abs'] == 0]),
|
||||
@@ -382,7 +382,7 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
exit_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||
exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades,
|
||||
results=results)
|
||||
left_open_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
|
||||
starting_balance=start_balance,
|
||||
@@ -406,7 +406,7 @@ def generate_strategy_stats(pairlist: List[str],
|
||||
'worst_pair': worst_pair,
|
||||
'results_per_pair': pair_results,
|
||||
'results_per_enter_tag': enter_tag_results,
|
||||
'sell_reason_summary': exit_reason_stats,
|
||||
'exit_reason_summary': exit_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
# 'days_breakdown_stats': days_breakdown_stats,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -572,7 +572,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_exit_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
:param sell_reason_stats: Exit reason metrics
|
||||
@@ -590,12 +590,12 @@ def text_table_exit_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
|
||||
]
|
||||
|
||||
output = [[
|
||||
t['sell_reason'], t['trades'],
|
||||
t.get('exit_reason', t.get('sell_reason')), t['trades'],
|
||||
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
|
||||
t['profit_mean_pct'], t['profit_sum_pct'],
|
||||
round_coin_value(t['profit_total_abs'], stake_currency, False),
|
||||
t['profit_total_pct'],
|
||||
] for t in sell_reason_stats]
|
||||
] for t in exit_reason_stats]
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
@@ -813,7 +813,8 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_exit_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||
exit_reasons = results.get('exit_reason_summary', results.get('sell_reason_summary'))
|
||||
table = text_table_exit_reason(exit_reason_stats=exit_reasons,
|
||||
stake_currency=stake_currency)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||
|
@@ -74,7 +74,7 @@ def migrate_trades_and_orders_table(
|
||||
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
|
||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||
min_rate = get_column_def(cols, 'min_rate', 'null')
|
||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||
exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
|
||||
strategy = get_column_def(cols, 'strategy', 'null')
|
||||
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
|
||||
|
||||
@@ -104,7 +104,8 @@ def migrate_trades_and_orders_table(
|
||||
close_profit_abs = get_column_def(
|
||||
cols, 'close_profit_abs',
|
||||
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
|
||||
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
|
||||
exit_order_status = get_column_def(cols, 'exit_order_status',
|
||||
get_column_def(cols, 'sell_order_status', 'null'))
|
||||
amount_requested = get_column_def(cols, 'amount_requested', 'amount')
|
||||
|
||||
# Schema migration necessary
|
||||
@@ -136,7 +137,7 @@ def migrate_trades_and_orders_table(
|
||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||
stoploss_order_id, stoploss_last_update,
|
||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, enter_tag,
|
||||
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
|
||||
timeframe, open_trade_value, close_profit_abs,
|
||||
trading_mode, leverage, liquidation_price, is_short,
|
||||
interest_rate, funding_fees
|
||||
@@ -152,8 +153,14 @@ def migrate_trades_and_orders_table(
|
||||
{initial_stop_loss} initial_stop_loss,
|
||||
{initial_stop_loss_pct} initial_stop_loss_pct,
|
||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||
{sell_order_status} sell_order_status,
|
||||
{max_rate} max_rate, {min_rate} min_rate,
|
||||
case when {exit_reason} == 'sell_signal' then 'exit_signal'
|
||||
when {exit_reason} == 'custom_sell' then 'custom_exit'
|
||||
when {exit_reason} == 'force_sell' then 'force_exit'
|
||||
when {exit_reason} == 'emergency_sell' then 'emergency_exit'
|
||||
else {exit_reason}
|
||||
end exit_reason,
|
||||
{exit_order_status} exit_order_status,
|
||||
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
|
||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
|
||||
@@ -234,7 +241,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
# Migrates both trades and orders table!
|
||||
# if ('orders' not in previous_tables
|
||||
# or not has_column(cols_orders, 'leverage')):
|
||||
if not has_column(cols, 'liquidation_price'):
|
||||
if not has_column(cols, 'exit_order_status'):
|
||||
logger.info(f"Running database migration for trades - "
|
||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||
migrate_trades_and_orders_table(
|
||||
|
@@ -316,8 +316,8 @@ class LocalTrade():
|
||||
max_rate: float = 0.0
|
||||
# Lowest price reached
|
||||
min_rate: float = 0.0
|
||||
sell_reason: str = ''
|
||||
sell_order_status: str = ''
|
||||
exit_reason: str = ''
|
||||
exit_order_status: str = ''
|
||||
strategy: str = ''
|
||||
enter_tag: Optional[str] = None
|
||||
timeframe: Optional[int] = None
|
||||
@@ -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,
|
||||
@@ -459,8 +465,9 @@ class LocalTrade():
|
||||
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||
'profit_abs': self.close_profit_abs,
|
||||
|
||||
'sell_reason': self.sell_reason,
|
||||
'sell_order_status': self.sell_order_status,
|
||||
'sell_reason': self.exit_reason, # Deprecated
|
||||
'exit_reason': self.exit_reason,
|
||||
'exit_order_status': self.exit_order_status,
|
||||
'stop_loss_abs': self.stop_loss,
|
||||
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||
@@ -600,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
|
||||
@@ -618,7 +625,7 @@ class LocalTrade():
|
||||
elif order.ft_order_side == 'stoploss':
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
self.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
|
||||
if self.is_open:
|
||||
logger.info(f'{order.order_type.upper()} is hit for {self}.')
|
||||
self.close(order.safe_price)
|
||||
@@ -636,7 +643,7 @@ class LocalTrade():
|
||||
self.close_profit = self.calc_profit_ratio()
|
||||
self.close_profit_abs = self.calc_profit()
|
||||
self.is_open = False
|
||||
self.sell_order_status = 'closed'
|
||||
self.exit_order_status = 'closed'
|
||||
self.open_order_id = None
|
||||
if show_msg:
|
||||
logger.info(
|
||||
@@ -649,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:
|
||||
@@ -666,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
|
||||
@@ -839,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
|
||||
@@ -850,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
|
||||
|
||||
@@ -918,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:
|
||||
@@ -947,6 +954,11 @@ class LocalTrade():
|
||||
"""
|
||||
return len(self.select_filled_orders('sell'))
|
||||
|
||||
@property
|
||||
def sell_reason(self) -> str:
|
||||
""" DEPRECATED! Please use exit_reason instead."""
|
||||
return self.exit_reason
|
||||
|
||||
@staticmethod
|
||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
||||
open_date: datetime = None, close_date: datetime = None,
|
||||
@@ -1076,8 +1088,8 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
# Lowest price reached
|
||||
min_rate = Column(Float, nullable=True)
|
||||
sell_reason = Column(String(100), nullable=True)
|
||||
sell_order_status = Column(String(100), nullable=True)
|
||||
exit_reason = Column(String(100), nullable=True)
|
||||
exit_order_status = Column(String(100), nullable=True)
|
||||
strategy = Column(String(100), nullable=True)
|
||||
enter_tag = Column(String(100), nullable=True)
|
||||
timeframe = Column(Integer, nullable=True)
|
||||
@@ -1283,9 +1295,9 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def get_exit_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on sell reason performance
|
||||
Returns List of dicts containing all Trades, based on exit reason performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
@@ -1295,30 +1307,30 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
sell_tag_perf = Trade.query.with_entities(
|
||||
Trade.sell_reason,
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.sell_reason) \
|
||||
.group_by(Trade.exit_reason) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
'sell_reason': sell_reason if sell_reason is not None else "Other",
|
||||
'exit_reason': exit_reason if exit_reason is not None else "Other",
|
||||
'profit_ratio': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for sell_reason, profit, profit_abs, count in sell_tag_perf
|
||||
for exit_reason, profit, profit_abs, count in sell_tag_perf
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance
|
||||
Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
@@ -1330,7 +1342,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
mix_tag_perf = Trade.query.with_entities(
|
||||
Trade.id,
|
||||
Trade.enter_tag,
|
||||
Trade.sell_reason,
|
||||
Trade.exit_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
@@ -1340,12 +1352,12 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
.all()
|
||||
|
||||
return_list: List[Dict] = []
|
||||
for id, enter_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
enter_tag = enter_tag if enter_tag is not None else "Other"
|
||||
sell_reason = sell_reason if sell_reason is not None else "Other"
|
||||
exit_reason = exit_reason if exit_reason is not None else "Other"
|
||||
|
||||
if(sell_reason is not None and enter_tag is not None):
|
||||
mix_tag = enter_tag + " " + sell_reason
|
||||
if(exit_reason is not None and enter_tag is not None):
|
||||
mix_tag = enter_tag + " " + exit_reason
|
||||
i = 0
|
||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||
return_list.append({'mix_tag': mix_tag,
|
||||
|
@@ -240,7 +240,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
||||
trades['desc'] = trades.apply(
|
||||
lambda row: f"{row['profit_ratio']:.2%}, " +
|
||||
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
|
||||
f"{row['sell_reason']}, " +
|
||||
f"{row['exit_reason']}, " +
|
||||
f"{row['trade_duration']} min",
|
||||
axis=1)
|
||||
trade_buys = go.Scatter(
|
||||
|
@@ -41,19 +41,9 @@ class StoplossGuard(IProtection):
|
||||
Evaluate recent trades
|
||||
"""
|
||||
look_back_until = date_now - timedelta(minutes=self._lookback_period)
|
||||
# filters = [
|
||||
# Trade.is_open.is_(False),
|
||||
# Trade.close_date > look_back_until,
|
||||
# or_(Trade.sell_reason == ExitType.STOP_LOSS.value,
|
||||
# and_(Trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value,
|
||||
# Trade.close_profit < 0))
|
||||
# ]
|
||||
# if pair:
|
||||
# filters.append(Trade.pair == pair)
|
||||
# trades = Trade.get_trades(filters).all()
|
||||
|
||||
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
||||
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
|
||||
trades = [trade for trade in trades1 if (str(trade.exit_reason) in (
|
||||
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
|
||||
ExitType.STOPLOSS_ON_EXCHANGE.value)
|
||||
and trade.close_profit and trade.close_profit < 0)]
|
||||
|
@@ -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.
|
||||
|
@@ -113,7 +113,7 @@ class SellReason(BaseModel):
|
||||
|
||||
|
||||
class Stats(BaseModel):
|
||||
sell_reasons: Dict[str, SellReason]
|
||||
exit_reasons: Dict[str, SellReason]
|
||||
durations: Dict[str, Optional[float]]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -235,8 +235,9 @@ class TradeSchema(BaseModel):
|
||||
profit_pct: Optional[float]
|
||||
profit_abs: Optional[float]
|
||||
profit_fiat: Optional[float]
|
||||
sell_reason: Optional[str]
|
||||
sell_order_status: Optional[str]
|
||||
sell_reason: Optional[str] # Deprecated
|
||||
exit_reason: Optional[str]
|
||||
exit_order_status: Optional[str]
|
||||
stop_loss_abs: Optional[float]
|
||||
stop_loss_ratio: Optional[float]
|
||||
stop_loss_pct: Optional[float]
|
||||
|
@@ -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),
|
||||
@@ -428,13 +428,13 @@ class RPC:
|
||||
return 'losses'
|
||||
else:
|
||||
return 'draws'
|
||||
trades = trades = Trade.get_trades([Trade.is_open.is_(False)])
|
||||
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)])
|
||||
# Sell reason
|
||||
sell_reasons = {}
|
||||
exit_reasons = {}
|
||||
for trade in trades:
|
||||
if trade.sell_reason not in sell_reasons:
|
||||
sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
|
||||
sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1
|
||||
if trade.exit_reason not in exit_reasons:
|
||||
exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
|
||||
exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
|
||||
|
||||
# Duration
|
||||
dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
|
||||
@@ -448,7 +448,7 @@ class RPC:
|
||||
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
|
||||
|
||||
durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
|
||||
return {'sell_reasons': sell_reasons, 'durations': durations}
|
||||
return {'exit_reasons': exit_reasons, 'durations': durations}
|
||||
|
||||
def _rpc_trade_statistics(
|
||||
self, stake_currency: str, fiat_display_currency: str,
|
||||
@@ -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,21 +695,21 @@ 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_SELL'])
|
||||
trade, order, CANCEL_REASON['FORCE_EXIT'])
|
||||
|
||||
if order['side'] == trade.exit_side:
|
||||
# Cancel order - so it is placed anew with a fresh price.
|
||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
|
||||
|
||||
if not fully_canceled:
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
|
||||
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL)
|
||||
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,
|
||||
@@ -848,16 +848,16 @@ class RPC:
|
||||
"""
|
||||
return Trade.get_enter_tag_performance(pair)
|
||||
|
||||
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for sell reason performance.
|
||||
Handler for exit reason performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
return Trade.get_sell_reason_performance(pair)
|
||||
return Trade.get_exit_reason_performance(pair)
|
||||
|
||||
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for mix tag (enter_tag + sell_reason) performance.
|
||||
Handler for mix tag (enter_tag + exit_reason) performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
mix_tags = Trade.get_mix_tag_performance(pair)
|
||||
|
@@ -108,7 +108,8 @@ class Telegram(RPCHandler):
|
||||
# this needs refactoring of the whole telegram module (same
|
||||
# problem in _help()).
|
||||
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
|
||||
r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags',
|
||||
r'/trades$', r'/performance$', r'/buys', r'/entries',
|
||||
r'/sells', r'/exits', r'/mix_tags',
|
||||
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
|
||||
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||
@@ -153,16 +154,16 @@ 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),
|
||||
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
|
||||
CommandHandler('sells', self._sell_reason_performance),
|
||||
CommandHandler(['sells', 'exits'], self._exit_reason_performance),
|
||||
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||
CommandHandler('stats', self._stats),
|
||||
CommandHandler('daily', self._daily),
|
||||
@@ -193,11 +194,11 @@ class Telegram(RPCHandler):
|
||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||
CallbackQueryHandler(self._enter_tag_performance,
|
||||
pattern='update_enter_tag_performance'),
|
||||
CallbackQueryHandler(self._sell_reason_performance,
|
||||
pattern='update_sell_reason_performance'),
|
||||
CallbackQueryHandler(self._exit_reason_performance,
|
||||
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, pattern=r"\S+\/\S+"),
|
||||
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
|
||||
CallbackQueryHandler(self._forceexit_inline, pattern=r"[0-9]+\s\S+\/\S+")
|
||||
]
|
||||
for handle in handles:
|
||||
@@ -225,21 +226,20 @@ class Telegram(RPCHandler):
|
||||
# This can take up to `timeout` from the call to `start_polling`.
|
||||
self._updater.stop()
|
||||
|
||||
def _format_buy_msg(self, msg: Dict[str, Any]) -> str:
|
||||
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
|
||||
if self._rpc._fiat_converter:
|
||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
else:
|
||||
msg['stake_amount_fiat'] = 0
|
||||
is_fill = msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]
|
||||
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['type']
|
||||
in [RPCMessageType.BUY_FILL, RPCMessageType.BUY]
|
||||
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 ""
|
||||
@@ -247,9 +247,9 @@ class Telegram(RPCHandler):
|
||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
||||
message += f"*Leverage:* `{msg['leverage']}`\n"
|
||||
|
||||
if msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]:
|
||||
if msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
||||
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||
elif msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]:
|
||||
elif msg['type'] in [RPCMessageType.ENTRY]:
|
||||
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
|
||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
|
||||
@@ -261,7 +261,7 @@ class Telegram(RPCHandler):
|
||||
message += ")`"
|
||||
return message
|
||||
|
||||
def _format_sell_msg(self, msg: Dict[str, Any]) -> str:
|
||||
def _format_exit_msg(self, msg: Dict[str, Any]) -> str:
|
||||
msg['amount'] = round(msg['amount'], 8)
|
||||
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
|
||||
msg['duration'] = msg['close_date'].replace(
|
||||
@@ -285,41 +285,38 @@ class Telegram(RPCHandler):
|
||||
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
|
||||
else:
|
||||
msg['profit_extra'] = ''
|
||||
is_fill = msg['type'] == RPCMessageType.SELL_FILL
|
||||
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
||||
message = (
|
||||
f"{msg['emoji']} *{msg['exchange']}:* "
|
||||
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
||||
f"*Exit Reason:* `{msg['sell_reason']}`\n"
|
||||
f"*Exit Reason:* `{msg['exit_reason']}`\n"
|
||||
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||
f"*Direction:* `{msg['direction']}`\n"
|
||||
f"{msg['leverage_text']}"
|
||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||
)
|
||||
if msg['type'] == RPCMessageType.SELL:
|
||||
if msg['type'] == RPCMessageType.EXIT:
|
||||
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
f"*Close Rate:* `{msg['limit']:.8f}`")
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_FILL:
|
||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
|
||||
|
||||
return message
|
||||
|
||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL, RPCMessageType.SHORT,
|
||||
RPCMessageType.SHORT_FILL]:
|
||||
message = self._format_buy_msg(msg)
|
||||
if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]:
|
||||
message = self._format_entry_msg(msg)
|
||||
|
||||
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
|
||||
message = self._format_sell_msg(msg)
|
||||
elif msg_type in [RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]:
|
||||
message = self._format_exit_msg(msg)
|
||||
|
||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL,
|
||||
RPCMessageType.SELL_CANCEL):
|
||||
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.BUY_CANCEL,
|
||||
RPCMessageType.SHORT_CANCEL] else 'exit'
|
||||
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
|
||||
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
|
||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||
"Cancelling {message_side} Order for {pair} (#{trade_id}). "
|
||||
"Reason: {reason}.".format(**msg))
|
||||
@@ -356,14 +353,14 @@ class Telegram(RPCHandler):
|
||||
|
||||
msg_type = msg['type']
|
||||
noti = ''
|
||||
if msg_type == RPCMessageType.SELL:
|
||||
if msg_type == RPCMessageType.EXIT:
|
||||
sell_noti = self._config['telegram'] \
|
||||
.get('notification_settings', {}).get(str(msg_type), {})
|
||||
# For backward compatibility sell still can be string
|
||||
if isinstance(sell_noti, str):
|
||||
noti = sell_noti
|
||||
else:
|
||||
noti = sell_noti.get(str(msg['sell_reason']), default_noti)
|
||||
noti = sell_noti.get(str(msg['exit_reason']), default_noti)
|
||||
else:
|
||||
noti = self._config['telegram'] \
|
||||
.get('notification_settings', {}).get(str(msg_type), default_noti)
|
||||
@@ -386,7 +383,7 @@ class Telegram(RPCHandler):
|
||||
return "\N{ROCKET}"
|
||||
elif float(msg['profit_percent']) >= 0.0:
|
||||
return "\N{EIGHT SPOKED ASTERISK}"
|
||||
elif msg['sell_reason'] == "stop_loss":
|
||||
elif msg['exit_reason'] == "stop_loss":
|
||||
return "\N{WARNING SIGN}"
|
||||
else:
|
||||
return "\N{CROSS MARK}"
|
||||
@@ -468,7 +465,7 @@ class Telegram(RPCHandler):
|
||||
for r in results:
|
||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
|
||||
r['sell_reason'] = r.get('sell_reason', "")
|
||||
r['exit_reason'] = r.get('exit_reason', "")
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
("` (since {open_date_hum})`" if r['is_open'] else ""),
|
||||
@@ -477,7 +474,7 @@ class Telegram(RPCHandler):
|
||||
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
||||
"*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "",
|
||||
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
|
||||
]
|
||||
|
||||
if position_adjust:
|
||||
@@ -507,8 +504,8 @@ class Telegram(RPCHandler):
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
if r['sell_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
||||
if r['exit_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`")
|
||||
else:
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
|
||||
@@ -769,27 +766,27 @@ class Telegram(RPCHandler):
|
||||
'stop_loss': 'Stoploss',
|
||||
'trailing_stop_loss': 'Trail. Stop',
|
||||
'stoploss_on_exchange': 'Stoploss',
|
||||
'sell_signal': 'Sell Signal',
|
||||
'force_sell': 'Forcesell',
|
||||
'emergency_sell': 'Emergency Sell',
|
||||
'exit_signal': 'Exit Signal',
|
||||
'force_exit': 'Force Exit',
|
||||
'emergency_exit': 'Emergency Exit',
|
||||
}
|
||||
sell_reasons_tabulate = [
|
||||
exit_reasons_tabulate = [
|
||||
[
|
||||
reason_map.get(reason, reason),
|
||||
sum(count.values()),
|
||||
count['wins'],
|
||||
count['losses']
|
||||
] for reason, count in stats['sell_reasons'].items()
|
||||
] for reason, count in stats['exit_reasons'].items()
|
||||
]
|
||||
sell_reasons_msg = 'No trades yet.'
|
||||
for reason in chunks(sell_reasons_tabulate, 25):
|
||||
sell_reasons_msg = tabulate(
|
||||
exit_reasons_msg = 'No trades yet.'
|
||||
for reason in chunks(exit_reasons_tabulate, 25):
|
||||
exit_reasons_msg = tabulate(
|
||||
reason,
|
||||
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
||||
headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
|
||||
)
|
||||
if len(sell_reasons_tabulate) > 25:
|
||||
self._send_msg(sell_reasons_msg, ParseMode.MARKDOWN)
|
||||
sell_reasons_msg = ''
|
||||
if len(exit_reasons_tabulate) > 25:
|
||||
self._send_msg(exit_reasons_msg, ParseMode.MARKDOWN)
|
||||
exit_reasons_msg = ''
|
||||
|
||||
durations = stats['durations']
|
||||
duration_msg = tabulate(
|
||||
@@ -801,7 +798,7 @@ class Telegram(RPCHandler):
|
||||
],
|
||||
headers=['', 'Avg. Duration']
|
||||
)
|
||||
msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""")
|
||||
msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""")
|
||||
|
||||
self._send_msg(msg, ParseMode.MARKDOWN)
|
||||
|
||||
@@ -931,7 +928,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
|
||||
@@ -974,14 +971,14 @@ class Telegram(RPCHandler):
|
||||
query.edit_message_text(text=f"Manually exiting: {query.data}")
|
||||
self._forceexit_action(trade_id)
|
||||
|
||||
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:
|
||||
@@ -989,7 +986,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],
|
||||
@@ -1002,7 +999,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>
|
||||
@@ -1015,7 +1012,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 = [
|
||||
@@ -1132,7 +1129,7 @@ class Telegram(RPCHandler):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_enter_tag_performance(pair)
|
||||
output = "<b>Buy Tag Performance:</b>\n"
|
||||
output = "<b>Entry Tag Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['enter_tag']}\t"
|
||||
@@ -1153,7 +1150,7 @@ class Telegram(RPCHandler):
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _sell_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /sells.
|
||||
Shows a performance statistic from finished trades
|
||||
@@ -1166,11 +1163,11 @@ class Telegram(RPCHandler):
|
||||
if context.args and isinstance(context.args[0], str):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_sell_reason_performance(pair)
|
||||
output = "<b>Sell Reason Performance:</b>\n"
|
||||
trades = self._rpc._rpc_exit_reason_performance(pair)
|
||||
output = "<b>Exit Reason Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['sell_reason']}\t"
|
||||
f"{i+1}.\t <code>{trade['exit_reason']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
@@ -1182,7 +1179,7 @@ class Telegram(RPCHandler):
|
||||
output += stat_line
|
||||
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_sell_reason_performance",
|
||||
reload_able=True, callback_path="update_exit_reason_performance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
@@ -1393,23 +1390,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 "
|
||||
@@ -1438,8 +1437,8 @@ class Telegram(RPCHandler):
|
||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the exit reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined entry tag + exit reason performance`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
||||
"over the last n days`\n"
|
||||
|
@@ -43,23 +43,23 @@ class Webhook(RPCHandler):
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
""" Send a message to telegram channel """
|
||||
try:
|
||||
|
||||
if msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]:
|
||||
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||
elif msg['type'] in [RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL]:
|
||||
valuedict = self._config['webhook'].get('webhookbuycancel', None)
|
||||
elif msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]:
|
||||
valuedict = self._config['webhook'].get('webhookbuyfill', None)
|
||||
elif msg['type'] == RPCMessageType.SELL:
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_FILL:
|
||||
valuedict = self._config['webhook'].get('webhooksellfill', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_CANCEL:
|
||||
valuedict = self._config['webhook'].get('webhooksellcancel', None)
|
||||
whconfig = self._config['webhook']
|
||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||
valuedict = whconfig.get('webhookentry', None)
|
||||
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
||||
valuedict = whconfig.get('webhookentrycancel', None)
|
||||
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
||||
valuedict = whconfig.get('webhookentryfill', None)
|
||||
elif msg['type'] == RPCMessageType.EXIT:
|
||||
valuedict = whconfig.get('webhookexit', None)
|
||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||
valuedict = whconfig.get('webhookexitfill', None)
|
||||
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
||||
valuedict = whconfig.get('webhookexitcancel', None)
|
||||
elif msg['type'] in (RPCMessageType.STATUS,
|
||||
RPCMessageType.STARTUP,
|
||||
RPCMessageType.WARNING):
|
||||
valuedict = self._config['webhook'].get('webhookstatus', None)
|
||||
valuedict = whconfig.get('webhookstatus', None)
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||
if not valuedict:
|
||||
|
@@ -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
|
||||
@@ -308,10 +308,10 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param exit_reason: Exit reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'sell_signal', 'force_sell', 'emergency_sell']
|
||||
'exit_signal', 'force_exit', 'emergency_exit']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True, then the sell-order/exit_short-order is placed on the exchange.
|
||||
:return bool: When True, then the exit-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
return True
|
||||
@@ -339,7 +339,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
return self.stoploss
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
"""
|
||||
Custom entry price logic, returning the new entry price.
|
||||
|
||||
@@ -351,6 +351,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
"""
|
||||
@@ -396,7 +397,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return: To execute exit, return a string with custom sell reason or True. Otherwise return
|
||||
:return: To execute exit, return a string with custom exit reason or True. Otherwise return
|
||||
None or False.
|
||||
"""
|
||||
return None
|
||||
@@ -420,7 +421,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return: To execute exit, return a string with custom sell reason or True. Otherwise return
|
||||
:return: To execute exit, return a string with custom exit reason or True. Otherwise return
|
||||
None or False.
|
||||
"""
|
||||
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
|
||||
@@ -634,8 +635,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
dataframe[SignalTagType.ENTER_TAG.value] = None
|
||||
dataframe[SignalTagType.EXIT_TAG.value] = None
|
||||
|
||||
# Other Defs in strategy that want to be called every loop here
|
||||
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
||||
logger.debug("Loop Analysis Launched")
|
||||
|
||||
return dataframe
|
||||
@@ -716,7 +715,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
Calculates current signal based based on the entry order or exit order
|
||||
columns of the dataframe.
|
||||
Used by Bot to get the signal to buy, sell, short, or exit_short
|
||||
Used by Bot to get the signal to enter, or exit
|
||||
:param pair: pair in format ANT/BTC
|
||||
:param timeframe: timeframe to use
|
||||
:param dataframe: Analyzed dataframe to get signal from.
|
||||
@@ -750,7 +749,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
is_short: bool = None
|
||||
) -> Tuple[bool, bool, Optional[str]]:
|
||||
"""
|
||||
Calculates current exit signal based based on the buy/short or sell/exit_short
|
||||
Calculates current exit signal based based on the dataframe
|
||||
columns of the dataframe.
|
||||
Used by Bot to get the signal to exit.
|
||||
depending on is_short, looks at "short" or "long" columns.
|
||||
@@ -787,9 +786,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
dataframe: DataFrame,
|
||||
) -> Tuple[Optional[SignalDirection], Optional[str]]:
|
||||
"""
|
||||
Calculates current entry signal based based on the buy/short or sell/exit_short
|
||||
Calculates current entry signal based based on the dataframe signals
|
||||
columns of the dataframe.
|
||||
Used by Bot to get the signal to buy, sell, short, or exit_short
|
||||
Used by Bot to get the signal to enter trades.
|
||||
:param pair: pair in format ANT/BTC
|
||||
:param timeframe: timeframe to use
|
||||
:param dataframe: Analyzed dataframe to get signal from.
|
||||
@@ -867,34 +866,34 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
current_profit=current_profit,
|
||||
force_stoploss=force_stoploss, low=low, high=high)
|
||||
|
||||
# Set current rate to high for backtesting sell
|
||||
# Set current rate to high for backtesting exits
|
||||
current_rate = (low if trade.is_short else high) or rate
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
|
||||
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))
|
||||
|
||||
sell_signal = ExitType.NONE
|
||||
exit_signal = ExitType.NONE
|
||||
custom_reason = ''
|
||||
# use provided rate in backtesting, not high/low.
|
||||
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_:
|
||||
sell_signal = ExitType.SELL_SIGNAL
|
||||
exit_signal = ExitType.EXIT_SIGNAL
|
||||
else:
|
||||
trade_type = "exit_short" if trade.is_short else "sell"
|
||||
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
|
||||
pair=trade.pair, trade=trade, current_time=current_time,
|
||||
current_rate=current_rate, current_profit=current_profit)
|
||||
if custom_reason:
|
||||
sell_signal = ExitType.CUSTOM_SELL
|
||||
exit_signal = ExitType.CUSTOM_EXIT
|
||||
if isinstance(custom_reason, str):
|
||||
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
|
||||
logger.warning(f'Custom {trade_type} reason returned from '
|
||||
@@ -903,23 +902,23 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
|
||||
else:
|
||||
custom_reason = None
|
||||
if sell_signal in (ExitType.CUSTOM_SELL, ExitType.SELL_SIGNAL):
|
||||
if exit_signal in (ExitType.CUSTOM_EXIT, ExitType.EXIT_SIGNAL):
|
||||
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||
f"sell_type=ExitType.{sell_signal.name}" +
|
||||
f"exit_type=ExitType.{exit_signal.name}" +
|
||||
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||
return ExitCheckTuple(exit_type=sell_signal, exit_reason=custom_reason)
|
||||
return ExitCheckTuple(exit_type=exit_signal, exit_reason=custom_reason)
|
||||
|
||||
# Sequence:
|
||||
# Exit-signal
|
||||
# ROI (if not stoploss)
|
||||
# Stoploss
|
||||
if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
|
||||
logger.debug(f"{trade.pair} - Required profit reached. sell_type=ExitType.ROI")
|
||||
logger.debug(f"{trade.pair} - Required profit reached. exit_type=ExitType.ROI")
|
||||
return ExitCheckTuple(exit_type=ExitType.ROI)
|
||||
|
||||
if stoplossflag.exit_flag:
|
||||
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.exit_type}")
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
||||
return stoplossflag
|
||||
|
||||
# This one is noisy, commented out...
|
||||
@@ -988,11 +987,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
if ((sl_higher_long or sl_lower_short) and
|
||||
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
||||
|
||||
sell_type = ExitType.STOP_LOSS
|
||||
exit_type = ExitType.STOP_LOSS
|
||||
|
||||
# If initial stoploss is not the same as current one then it is trailing.
|
||||
if trade.initial_stop_loss != trade.stop_loss:
|
||||
sell_type = ExitType.TRAILING_STOP_LOSS
|
||||
exit_type = ExitType.TRAILING_STOP_LOSS
|
||||
logger.debug(
|
||||
f"{trade.pair} - HIT STOP: current price at "
|
||||
f"{((high if trade.is_short else low) or current_rate):.6f}, "
|
||||
@@ -1007,7 +1006,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
||||
f"{new_stoploss:.6f}")
|
||||
|
||||
return ExitCheckTuple(exit_type=sell_type)
|
||||
return ExitCheckTuple(exit_type=exit_type)
|
||||
|
||||
return ExitCheckTuple(exit_type=ExitType.NONE)
|
||||
|
||||
@@ -1027,9 +1026,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||
"""
|
||||
Based on trade duration, current profit of the trade and ROI configuration,
|
||||
decides whether bot should sell.
|
||||
decides whether bot should exit.
|
||||
:param current_profit: current profit as ratio
|
||||
:return: True if bot should sell at current rate
|
||||
:return: True if bot should exit at current rate
|
||||
"""
|
||||
# Check if time matches and current rate is above threshold
|
||||
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
|
||||
@@ -1045,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:
|
||||
@@ -1128,7 +1127,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information dictionary, with details like the
|
||||
currently traded pair
|
||||
:return: DataFrame with sell column
|
||||
:return: DataFrame with exit column
|
||||
"""
|
||||
|
||||
logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.")
|
||||
|
@@ -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)
|
||||
|
@@ -187,7 +187,7 @@
|
||||
"trades = load_backtest_data(backtest_dir)\n",
|
||||
"\n",
|
||||
"# Show value-counts per pair\n",
|
||||
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
|
||||
"trades.groupby(\"pair\")[\"exit_reason\"].value_counts()"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -257,7 +257,7 @@
|
||||
"trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
|
||||
"\n",
|
||||
"# Display results\n",
|
||||
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
|
||||
"trades.groupby(\"pair\")[\"exit_reason\"].value_counts()"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"order_types": {
|
||||
"entry": "limit",
|
||||
"exit": "limit",
|
||||
"emergencyexit": "limit",
|
||||
"emergency_exit": "limit",
|
||||
"stoploss": "limit",
|
||||
"stoploss_on_exchange": false
|
||||
},
|
||||
|
@@ -95,7 +95,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
|
||||
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
|
||||
"""
|
||||
Custom sell signal logic indicating that specified position should be sold. Returning a
|
||||
Custom exit signal logic indicating that specified position should be sold. Returning a
|
||||
string or True from this method is equal to setting sell signal on a candle at specified
|
||||
time. This method is not called when sell signal is set.
|
||||
|
||||
@@ -103,7 +103,7 @@ def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
|
||||
example you could implement a sell relative to the candle when the trade was opened,
|
||||
or a custom 1:2 risk-reward ROI.
|
||||
|
||||
Custom sell reason max length is 64. Exceeding characters will be removed.
|
||||
Custom exit reason max length is 64. Exceeding characters will be removed.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: trade object.
|
||||
@@ -111,7 +111,7 @@ def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', curre
|
||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return: To execute sell, return a string with custom sell reason or True. Otherwise return
|
||||
:return: To execute sell, return a string with custom exit reason or True. Otherwise return
|
||||
None or False.
|
||||
"""
|
||||
return None
|
||||
@@ -162,10 +162,10 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param exit_reason: Exit reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'sell_signal', 'force_sell', 'emergency_sell']
|
||||
'exit_signal', 'force_exit', 'emergency_exit']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
||||
:return bool: When True is returned, then the exit-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
return True
|
||||
@@ -206,7 +206,7 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the sell-order is cancelled.
|
||||
:return bool: When True is returned, then the exit-order is cancelled.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
Reference in New Issue
Block a user