Merge branch 'develop' into pr/Axel-CH/5347
This commit is contained in:
@@ -193,7 +193,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
selections['exchange'] = render_template(
|
||||
templatefile=f"subtemplates/exchange_{exchange_template}.j2",
|
||||
arguments=selections
|
||||
)
|
||||
)
|
||||
except TemplateNotFound:
|
||||
selections['exchange'] = render_template(
|
||||
templatefile="subtemplates/exchange_generic.j2",
|
||||
|
@@ -218,7 +218,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"spaces": Arg(
|
||||
'--spaces',
|
||||
help='Specify which parameters to hyperopt. Space-separated list.',
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'],
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
|
||||
nargs='+',
|
||||
default='default',
|
||||
),
|
||||
|
@@ -38,15 +38,15 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
indicators = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/indicators_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/indicators_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
buy_trend = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
sell_trend = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
plot_config = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/plot_config_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2",
|
||||
@@ -97,19 +97,19 @@ def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: st
|
||||
buy_guards = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
sell_guards = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
buy_space = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
sell_space = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
|
||||
strategy_text = render_template(templatefile='base_hyperopt.py.j2',
|
||||
arguments={"hyperopt": hyperopt_name,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict
|
||||
|
||||
from colorama import init as colorama_init
|
||||
|
||||
@@ -28,30 +28,12 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
no_details = config.get('hyperopt_list_no_details', False)
|
||||
no_header = False
|
||||
|
||||
filteroptions = {
|
||||
'only_best': config.get('hyperopt_list_best', False),
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||
}
|
||||
|
||||
results_file = get_latest_hyperopt_file(
|
||||
config['user_data_dir'] / 'hyperopt_results',
|
||||
config.get('hyperoptexportfilename'))
|
||||
|
||||
# Previous evaluations
|
||||
epochs = HyperoptTools.load_previous_results(results_file)
|
||||
total_epochs = len(epochs)
|
||||
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||
|
||||
if print_colorized:
|
||||
colorama_init(autoreset=True)
|
||||
@@ -59,7 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
if not export_csv:
|
||||
try:
|
||||
print(HyperoptTools.get_result_table(config, epochs, total_epochs,
|
||||
not filteroptions['only_best'],
|
||||
not config.get('hyperopt_list_best', False),
|
||||
print_colorized, 0))
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
@@ -71,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
|
||||
if epochs and export_csv:
|
||||
HyperoptTools.export_csv_file(
|
||||
config, epochs, total_epochs, not filteroptions['only_best'], export_csv
|
||||
config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
|
||||
)
|
||||
|
||||
|
||||
@@ -91,26 +73,9 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
|
||||
n = config.get('hyperopt_show_index', -1)
|
||||
|
||||
filteroptions = {
|
||||
'only_best': config.get('hyperopt_list_best', False),
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None)
|
||||
}
|
||||
|
||||
# Previous evaluations
|
||||
epochs = HyperoptTools.load_previous_results(results_file)
|
||||
total_epochs = len(epochs)
|
||||
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||
filtered_epochs = len(epochs)
|
||||
|
||||
if n > filtered_epochs:
|
||||
@@ -137,138 +102,3 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
|
||||
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
||||
header_str="Epoch details")
|
||||
|
||||
|
||||
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
"""
|
||||
Filter our items from the list of hyperopt results
|
||||
TODO: after 2021.5 remove all "legacy" mode queries.
|
||||
"""
|
||||
if filteroptions['only_best']:
|
||||
epochs = [x for x in epochs if x['is_best']]
|
||||
if filteroptions['only_profitable']:
|
||||
epochs = [x for x in epochs if x['results_metrics'].get(
|
||||
'profit', x['results_metrics'].get('profit_total', 0)) > 0]
|
||||
|
||||
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
||||
|
||||
logger.info(f"{len(epochs)} " +
|
||||
("best " if filteroptions['only_best'] else "") +
|
||||
("profitable " if filteroptions['only_profitable'] else "") +
|
||||
"epochs found.")
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
|
||||
"""
|
||||
Filter epochs with trade-counts > trades
|
||||
"""
|
||||
return [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get(
|
||||
'trade_count', x['results_metrics'].get('total_trades', 0)
|
||||
) > trade_count
|
||||
]
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_trades'] > 0:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
|
||||
|
||||
if filteroptions['filter_max_trades'] > 0:
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get(
|
||||
'trade_count', x['results_metrics'].get('total_trades')
|
||||
) < filteroptions['filter_max_trades']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
def get_duration_value(x):
|
||||
# Duration in minutes ...
|
||||
if 'duration' in x['results_metrics']:
|
||||
return x['results_metrics']['duration']
|
||||
else:
|
||||
# New mode
|
||||
if 'holding_avg_s' in x['results_metrics']:
|
||||
avg = x['results_metrics']['holding_avg_s']
|
||||
return avg // 60
|
||||
raise OperationalException(
|
||||
"Holding-average not available. Please omit the filter on average time, "
|
||||
"or rerun hyperopt with this version")
|
||||
|
||||
if filteroptions['filter_min_avg_time'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if get_duration_value(x) > filteroptions['filter_min_avg_time']
|
||||
]
|
||||
if filteroptions['filter_max_avg_time'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if get_duration_value(x) < filteroptions['filter_max_avg_time']
|
||||
]
|
||||
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_avg_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get(
|
||||
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
|
||||
) > filteroptions['filter_min_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_max_avg_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get(
|
||||
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
|
||||
) < filteroptions['filter_max_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_min_total_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get(
|
||||
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
||||
) > filteroptions['filter_min_total_profit']
|
||||
]
|
||||
if filteroptions['filter_max_total_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get(
|
||||
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
||||
) < filteroptions['filter_max_total_profit']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_objective'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
||||
if filteroptions['filter_max_objective'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
||||
|
||||
return epochs
|
||||
|
@@ -51,10 +51,10 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
|
||||
if not is_exchange_known_ccxt(exchange):
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" is not known to the ccxt library '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are available for Freqtrade: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
f'Exchange "{exchange}" is not known to the ccxt library '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are available for Freqtrade: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
valid, reason = validate_exchange(exchange)
|
||||
|
@@ -115,7 +115,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||
if conf.get('stoploss') == 0.0:
|
||||
raise OperationalException(
|
||||
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
|
||||
)
|
||||
)
|
||||
# Skip if trailing stoploss is not activated
|
||||
if not conf.get('trailing_stop', False):
|
||||
return
|
||||
@@ -180,7 +180,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
||||
raise OperationalException(
|
||||
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}"
|
||||
)
|
||||
)
|
||||
|
||||
if ('lookback_period' in prot and 'lookback_period_candles' in prot):
|
||||
raise OperationalException(
|
||||
|
@@ -108,5 +108,8 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
raise OperationalException(
|
||||
"Both 'timeframe' and 'ticker_interval' detected."
|
||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||
)
|
||||
)
|
||||
config['timeframe'] = config['ticker_interval']
|
||||
|
||||
if 'protections' in config:
|
||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||
|
@@ -280,7 +280,7 @@ CONF_SCHEMA = {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
|
@@ -151,7 +151,7 @@ class Edge:
|
||||
# Fake run-mode to Edge
|
||||
prior_rm = self.config['runmode']
|
||||
self.config['runmode'] = RunMode.EDGE
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
self.config['runmode'] = prior_rm
|
||||
|
||||
# Print timeframe
|
||||
@@ -231,12 +231,12 @@ class Edge:
|
||||
'Minimum expectancy and minimum winrate are met only for %s,'
|
||||
' so other pairs are filtered out.',
|
||||
self._final_pairs
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
'Edge removed all pairs as no pair with minimum expectancy '
|
||||
'and minimum winrate was found !'
|
||||
)
|
||||
)
|
||||
|
||||
return self._final_pairs
|
||||
|
||||
@@ -247,7 +247,7 @@ class Edge:
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
||||
final.append({
|
||||
'Pair': pair,
|
||||
'Winrate': info.winrate,
|
||||
|
@@ -618,6 +618,8 @@ class Exchange:
|
||||
if self.exchange_has('fetchL2OrderBook'):
|
||||
ob = self.fetch_l2_order_book(pair, 20)
|
||||
ob_type = 'asks' if side == 'buy' else 'bids'
|
||||
slippage = 0.05
|
||||
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
||||
|
||||
remaining_amount = amount
|
||||
filled_amount = 0
|
||||
@@ -626,7 +628,9 @@ class Exchange:
|
||||
book_entry_coin_volume = book_entry[1]
|
||||
if remaining_amount > 0:
|
||||
if remaining_amount < book_entry_coin_volume:
|
||||
# Orderbook at this slot bigger than remaining amount
|
||||
filled_amount += remaining_amount * book_entry_price
|
||||
break
|
||||
else:
|
||||
filled_amount += book_entry_coin_volume * book_entry_price
|
||||
remaining_amount -= book_entry_coin_volume
|
||||
@@ -635,7 +639,14 @@ class Exchange:
|
||||
else:
|
||||
# If remaining_amount wasn't consumed completely (break was not called)
|
||||
filled_amount += remaining_amount * book_entry_price
|
||||
forecast_avg_filled_price = filled_amount / amount
|
||||
forecast_avg_filled_price = max(filled_amount, 0) / amount
|
||||
# Limit max. slippage to specified value
|
||||
if side == 'buy':
|
||||
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
||||
|
||||
else:
|
||||
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
|
||||
|
||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||
|
||||
return rate
|
||||
|
@@ -44,7 +44,7 @@ def main(sysargv: List[str] = None) -> None:
|
||||
"as `freqtrade trade [options...]`.\n"
|
||||
"To see the full list of options available, please use "
|
||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||
)
|
||||
)
|
||||
|
||||
except SystemExit as e:
|
||||
return_code = e
|
||||
|
@@ -130,6 +130,9 @@ class Backtesting:
|
||||
self.abort = False
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
LoggingMixin.show_output = True
|
||||
PairLocks.use_db = True
|
||||
Trade.use_db = True
|
||||
@@ -146,6 +149,8 @@ class Backtesting:
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
if self.config.get('enable_protections', False):
|
||||
conf = self.config
|
||||
if hasattr(strategy, 'protections'):
|
||||
@@ -194,6 +199,7 @@ class Backtesting:
|
||||
Trade.reset_trades()
|
||||
self.rejected_trades = 0
|
||||
self.dataprovider.clear_cache()
|
||||
self._load_protections(self.strategy)
|
||||
|
||||
def check_abort(self):
|
||||
"""
|
||||
@@ -212,7 +218,7 @@ class Backtesting:
|
||||
"""
|
||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||
# and eventually change the constants for indexes at the top
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag']
|
||||
data: Dict = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
@@ -220,13 +226,10 @@ class Backtesting:
|
||||
for pair, pair_data in processed.items():
|
||||
self.check_abort()
|
||||
self.progress.increment()
|
||||
has_buy_tag = 'buy_tag' in pair_data
|
||||
headers = headers + ['buy_tag'] if has_buy_tag else headers
|
||||
if not pair_data.empty:
|
||||
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
||||
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
||||
if has_buy_tag:
|
||||
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||
|
||||
df_analyzed = self.strategy.advise_sell(
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
|
||||
@@ -237,14 +240,13 @@ class Backtesting:
|
||||
# from the previous candle
|
||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
||||
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
||||
if has_buy_tag:
|
||||
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||
|
||||
df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
|
||||
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||
|
||||
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
|
||||
|
||||
# Convert from Pandas to list for performance reasons
|
||||
# (Looping Pandas is slow.)
|
||||
data[pair] = df_analyzed[headers].values.tolist()
|
||||
@@ -317,14 +319,14 @@ class Backtesting:
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||
|
||||
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
|
||||
sell_candle_time, sell_row[BUY_IDX],
|
||||
sell_row[SELL_IDX],
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||
|
||||
if sell.sell_flag:
|
||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||
trade.close_date = sell_candle_time
|
||||
trade.sell_reason = sell.sell_reason
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
@@ -336,7 +338,7 @@ class Backtesting:
|
||||
rate=closerate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=sell.sell_reason,
|
||||
current_time=sell_row[DATE_IDX].to_pydatetime()):
|
||||
current_time=sell_candle_time):
|
||||
return None
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
@@ -460,6 +462,8 @@ class Backtesting:
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
try:
|
||||
# Row is treated as "current incomplete candle".
|
||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||
row = data[pair][row_index]
|
||||
except IndexError:
|
||||
# missing Data for one pair at the end.
|
||||
@@ -471,8 +475,8 @@ class Backtesting:
|
||||
continue
|
||||
|
||||
row_index += 1
|
||||
self.dataprovider._set_dataframe_max_index(row_index)
|
||||
indexes[pair] = row_index
|
||||
self.dataprovider._set_dataframe_max_index(row_index)
|
||||
|
||||
# without positionstacking, we can only have one open trade per pair.
|
||||
# max_open_trades must be respected
|
||||
@@ -496,7 +500,7 @@ class Backtesting:
|
||||
open_trades[pair].append(trade)
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
for trade in open_trades[pair]:
|
||||
for trade in list(open_trades[pair]):
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occurred
|
||||
@@ -527,7 +531,8 @@ class Backtesting:
|
||||
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||
}
|
||||
|
||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
|
||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
|
||||
timerange: TimeRange):
|
||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
@@ -546,7 +551,7 @@ class Backtesting:
|
||||
max_open_trades = 0
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)
|
||||
|
@@ -66,6 +66,7 @@ class Hyperopt:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.buy_space: List[Dimension] = []
|
||||
self.sell_space: List[Dimension] = []
|
||||
self.protection_space: List[Dimension] = []
|
||||
self.roi_space: List[Dimension] = []
|
||||
self.stoploss_space: List[Dimension] = []
|
||||
self.trailing_space: List[Dimension] = []
|
||||
@@ -191,6 +192,8 @@ class Hyperopt:
|
||||
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
result['protection'] = {p.name: params.get(p.name) for p in self.protection_space}
|
||||
if HyperoptTools.has_space(self.config, 'roi'):
|
||||
result['roi'] = {str(k): v for k, v in
|
||||
self.custom_hyperopt.generate_roi_table(params).items()}
|
||||
@@ -241,6 +244,12 @@ class Hyperopt:
|
||||
"""
|
||||
Assign the dimensions in the hyperoptimization space.
|
||||
"""
|
||||
if self.auto_hyperopt and HyperoptTools.has_space(self.config, 'protection'):
|
||||
# Protections can only be optimized when using the Parameter interface
|
||||
logger.debug("Hyperopt has 'protection' space")
|
||||
# Enable Protections if protection space is selected.
|
||||
self.config['enable_protections'] = True
|
||||
self.protection_space = self.custom_hyperopt.protection_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
logger.debug("Hyperopt has 'buy' space")
|
||||
@@ -261,8 +270,8 @@ class Hyperopt:
|
||||
if HyperoptTools.has_space(self.config, 'trailing'):
|
||||
logger.debug("Hyperopt has 'trailing' space")
|
||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.roi_space +
|
||||
self.stoploss_space + self.trailing_space)
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||
|
||||
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
||||
"""
|
||||
@@ -282,6 +291,12 @@ class Hyperopt:
|
||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'protection'):
|
||||
for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'):
|
||||
if attr.optimize:
|
||||
# noinspection PyProtectedMember
|
||||
attr.value = params_dict[attr_name]
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'roi'):
|
||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||
@@ -379,7 +394,7 @@ class Hyperopt:
|
||||
data, timerange = self.backtesting.load_bt_data()
|
||||
logger.info("Dataload complete. Calculating indicators")
|
||||
|
||||
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
|
||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe to get correct dates for output.
|
||||
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)
|
||||
@@ -444,9 +459,9 @@ class Hyperopt:
|
||||
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
|
||||
]
|
||||
with progressbar.ProgressBar(
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
EVALS = ceil(self.total_epochs / jobs)
|
||||
for i in range(EVALS):
|
||||
# Correct the number of epochs to be processed for the last
|
||||
|
@@ -73,6 +73,9 @@ class HyperOptAuto(IHyperOpt):
|
||||
def sell_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('sell', 'sell_indicator_space')
|
||||
|
||||
def protection_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('protection', 'indicator_space')
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
return self._get_func('generate_roi_table')(params)
|
||||
|
||||
|
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True) -> List:
|
||||
"""
|
||||
Filter our items from the list of hyperopt results
|
||||
"""
|
||||
if filteroptions['only_best']:
|
||||
epochs = [x for x in epochs if x['is_best']]
|
||||
if filteroptions['only_profitable']:
|
||||
epochs = [x for x in epochs
|
||||
if x['results_metrics'].get('profit_total', 0) > 0]
|
||||
|
||||
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
||||
|
||||
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
||||
if log:
|
||||
logger.info(f"{len(epochs)} " +
|
||||
("best " if filteroptions['only_best'] else "") +
|
||||
("profitable " if filteroptions['only_profitable'] else "") +
|
||||
"epochs found.")
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
|
||||
"""
|
||||
Filter epochs with trade-counts > trades
|
||||
"""
|
||||
return [
|
||||
x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count
|
||||
]
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_trades'] > 0:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
|
||||
|
||||
if filteroptions['filter_max_trades'] > 0:
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
def get_duration_value(x):
|
||||
# Duration in minutes ...
|
||||
if 'holding_avg_s' in x['results_metrics']:
|
||||
avg = x['results_metrics']['holding_avg_s']
|
||||
return avg // 60
|
||||
raise OperationalException(
|
||||
"Holding-average not available. Please omit the filter on average time, "
|
||||
"or rerun hyperopt with this version")
|
||||
|
||||
if filteroptions['filter_min_avg_time'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if get_duration_value(x) > filteroptions['filter_min_avg_time']
|
||||
]
|
||||
if filteroptions['filter_max_avg_time'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if get_duration_value(x) < filteroptions['filter_max_avg_time']
|
||||
]
|
||||
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_avg_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_mean', 0) * 100
|
||||
> filteroptions['filter_min_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_max_avg_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_mean', 0) * 100
|
||||
< filteroptions['filter_max_avg_profit']
|
||||
]
|
||||
if filteroptions['filter_min_total_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_total_abs', 0)
|
||||
> filteroptions['filter_min_total_profit']
|
||||
]
|
||||
if filteroptions['filter_max_total_profit'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
epochs = [
|
||||
x for x in epochs
|
||||
if x['results_metrics'].get('profit_total_abs', 0)
|
||||
< filteroptions['filter_max_total_profit']
|
||||
]
|
||||
return epochs
|
||||
|
||||
|
||||
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
||||
|
||||
if filteroptions['filter_min_objective'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
||||
if filteroptions['filter_max_objective'] is not None:
|
||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||
|
||||
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
||||
|
||||
return epochs
|
@@ -57,6 +57,13 @@ class IHyperOpt(ABC):
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
||||
|
||||
def protection_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create a protection space.
|
||||
Only supported by the Parameter interface.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('indicator_space', 'protection'))
|
||||
|
||||
def indicator_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Create an indicator space.
|
||||
|
@@ -4,7 +4,7 @@ import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import rapidjson
|
||||
@@ -15,6 +15,7 @@ from pandas import isna, json_normalize
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
||||
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -82,53 +83,77 @@ class HyperoptTools():
|
||||
"""
|
||||
Tell if the space value is contained in the configuration
|
||||
"""
|
||||
# The 'trailing' space is not included in the 'default' set of spaces
|
||||
if space == 'trailing':
|
||||
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||
if space in ('trailing', 'protection'):
|
||||
return any(s in config['spaces'] for s in [space, 'all'])
|
||||
else:
|
||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||
|
||||
@staticmethod
|
||||
def _read_results_pickle(results_file: Path) -> List:
|
||||
def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
|
||||
"""
|
||||
Read hyperopt results from pickle file
|
||||
LEGACY method - new files are written as json and cannot be read with this method.
|
||||
"""
|
||||
from joblib import load
|
||||
|
||||
logger.info(f"Reading pickled epochs from '{results_file}'")
|
||||
data = load(results_file)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _read_results(results_file: Path) -> List:
|
||||
"""
|
||||
Read hyperopt results from file
|
||||
Stream hyperopt results from file
|
||||
"""
|
||||
import rapidjson
|
||||
logger.info(f"Reading epochs from '{results_file}'")
|
||||
with results_file.open('r') as f:
|
||||
data = [rapidjson.loads(line) for line in f]
|
||||
return data
|
||||
data = []
|
||||
for line in f:
|
||||
data += [rapidjson.loads(line)]
|
||||
if len(data) >= batch_size:
|
||||
yield data
|
||||
data = []
|
||||
yield data
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(results_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
epochs: List = []
|
||||
def _test_hyperopt_results_exist(results_file) -> bool:
|
||||
if results_file.is_file() and results_file.stat().st_size > 0:
|
||||
if results_file.suffix == '.pickle':
|
||||
epochs = HyperoptTools._read_results_pickle(results_file)
|
||||
else:
|
||||
epochs = HyperoptTools._read_results(results_file)
|
||||
# Detection of some old format, without 'is_best' field saved
|
||||
if epochs[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"Legacy hyperopt results are no longer supported."
|
||||
"Please rerun hyperopt or use an older version to load this file."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# No file found.
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]:
|
||||
filteroptions = {
|
||||
'only_best': config.get('hyperopt_list_best', False),
|
||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||
}
|
||||
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
||||
# No file found.
|
||||
return [], 0
|
||||
|
||||
epochs = []
|
||||
total_epochs = 0
|
||||
for epochs_tmp in HyperoptTools._read_results(results_file):
|
||||
if total_epochs == 0 and epochs_tmp[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"The file with HyperoptTools results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
|
||||
return epochs
|
||||
total_epochs += len(epochs_tmp)
|
||||
epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
|
||||
|
||||
logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
|
||||
|
||||
# Final filter run ...
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
|
||||
|
||||
return epochs, total_epochs
|
||||
|
||||
@staticmethod
|
||||
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
@@ -149,7 +174,7 @@ class HyperoptTools():
|
||||
|
||||
if print_json:
|
||||
result_dict: Dict = {}
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
|
||||
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
|
||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
|
||||
@@ -158,6 +183,8 @@ class HyperoptTools():
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'protection',
|
||||
"Protection hyperspace params:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||
@@ -203,7 +230,7 @@ class HyperoptTools():
|
||||
elif space == "roi":
|
||||
result = result[:-1] + f'{appendix}\n'
|
||||
minimal_roi_result = rapidjson.dumps({
|
||||
str(k): v for k, v in (space_params or no_params).items()
|
||||
str(k): v for k, v in (space_params or no_params).items()
|
||||
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == "trailing":
|
||||
@@ -431,21 +458,14 @@ class HyperoptTools():
|
||||
trials['Best'] = ''
|
||||
trials['Stake currency'] = config['stake_currency']
|
||||
|
||||
if 'results_metrics.total_trades' in trials:
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
perc_multi = 100
|
||||
else:
|
||||
perc_multi = 1
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.avg_profit', 'results_metrics.median_profit',
|
||||
'results_metrics.total_profit',
|
||||
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
perc_multi = 100
|
||||
|
||||
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
|
||||
trials = trials[base_metrics + param_metrics]
|
||||
|
||||
@@ -473,11 +493,6 @@ class HyperoptTools():
|
||||
trials['Avg profit'] = trials['Avg profit'].apply(
|
||||
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
|
||||
)
|
||||
if perc_multi == 1:
|
||||
trials['Avg duration'] = trials['Avg duration'].apply(
|
||||
lambda x: f'{x:,.1f} m' if isinstance(
|
||||
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
|
||||
)
|
||||
trials['Objective'] = trials['Objective'].apply(
|
||||
lambda x: f'{x:,.5f}' if x != 100000 else ""
|
||||
)
|
||||
|
@@ -31,7 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent,
|
||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
||||
).with_suffix(recordfilename.suffix)
|
||||
).with_suffix(recordfilename.suffix)
|
||||
file_dump_json(filename, stats)
|
||||
|
||||
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||
@@ -173,7 +173,7 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append(_generate_result_line(
|
||||
results['results'], results['config']['dry_run_wallet'], strategy)
|
||||
)
|
||||
)
|
||||
try:
|
||||
max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'],
|
||||
value_col='profit_ratio')
|
||||
@@ -604,7 +604,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])
|
||||
stake_amount = round_coin_value(
|
||||
strat_results['stake_amount'], strat_results['stake_currency']
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
|
||||
message = ("No trades made. "
|
||||
f"Your starting balance was {start_balance}, "
|
||||
|
@@ -161,7 +161,7 @@ class Order(_DECL_BASE):
|
||||
self.ft_is_open = True
|
||||
if self.status in ('closed', 'canceled', 'cancelled'):
|
||||
self.ft_is_open = False
|
||||
if order.get('filled', 0) > 0:
|
||||
if (order.get('filled', 0.0) or 0.0) > 0:
|
||||
self.order_filled_date = datetime.now(timezone.utc)
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
@@ -354,12 +354,12 @@ class LocalTrade():
|
||||
LocalTrade.trades_open = []
|
||||
LocalTrade.total_profit = 0
|
||||
|
||||
def adjust_min_max_rates(self, current_price: float) -> None:
|
||||
def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
|
||||
"""
|
||||
Adjust the max_rate and min_rate.
|
||||
"""
|
||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
||||
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
|
||||
|
||||
def _set_new_stoploss(self, new_loss: float, stoploss: float):
|
||||
"""Assign new stop value"""
|
||||
|
@@ -334,8 +334,8 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
|
||||
)
|
||||
elif indicator_b not in data:
|
||||
logger.info(
|
||||
'fill_to: "%s" ignored. Reason: This indicator is not '
|
||||
'in your strategy.', indicator_b
|
||||
'fill_to: "%s" ignored. Reason: This indicator is not '
|
||||
'in your strategy.', indicator_b
|
||||
)
|
||||
return fig
|
||||
|
||||
|
@@ -144,7 +144,7 @@ class IPairList(LoggingMixin, ABC):
|
||||
markets = self._exchange.markets
|
||||
if not markets:
|
||||
raise OperationalException(
|
||||
'Markets not loaded. Make sure that exchange is initialized correctly.')
|
||||
'Markets not loaded. Make sure that exchange is initialized correctly.')
|
||||
|
||||
sanitized_whitelist: List[str] = []
|
||||
for pair in pairlist:
|
||||
|
@@ -120,9 +120,9 @@ class VolumePairList(IPairList):
|
||||
# Use fresh pairlist
|
||||
# Check if pair quote currency equals to the stake currency.
|
||||
filtered_tickers = [
|
||||
v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and v[self._sort_key] is not None)]
|
||||
v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and v[self._sort_key] is not None)]
|
||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||
|
||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||
@@ -197,7 +197,7 @@ class VolumePairList(IPairList):
|
||||
|
||||
if self._min_value > 0:
|
||||
filtered_tickers = [
|
||||
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
|
||||
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
|
||||
|
||||
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])
|
||||
|
||||
|
@@ -28,13 +28,13 @@ class PairListManager():
|
||||
self._tickers_needed = False
|
||||
for pairlist_handler_config in self._config.get('pairlists', None):
|
||||
pairlist_handler = PairListResolver.load_pairlist(
|
||||
pairlist_handler_config['method'],
|
||||
exchange=exchange,
|
||||
pairlistmanager=self,
|
||||
config=config,
|
||||
pairlistconfig=pairlist_handler_config,
|
||||
pairlist_pos=len(self._pairlist_handlers)
|
||||
)
|
||||
pairlist_handler_config['method'],
|
||||
exchange=exchange,
|
||||
pairlistmanager=self,
|
||||
config=config,
|
||||
pairlistconfig=pairlist_handler_config,
|
||||
pairlist_pos=len(self._pairlist_handlers)
|
||||
)
|
||||
self._tickers_needed |= pairlist_handler.needstickers
|
||||
self._pairlist_handlers.append(pairlist_handler)
|
||||
|
||||
|
@@ -25,19 +25,22 @@ class IProtection(LoggingMixin, ABC):
|
||||
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._protection_config = protection_config
|
||||
self._stop_duration_candles: Optional[int] = None
|
||||
self._lookback_period_candles: Optional[int] = None
|
||||
|
||||
tf_in_min = timeframe_to_minutes(config['timeframe'])
|
||||
if 'stop_duration_candles' in protection_config:
|
||||
self._stop_duration_candles = protection_config.get('stop_duration_candles', 1)
|
||||
self._stop_duration_candles = int(protection_config.get('stop_duration_candles', 1))
|
||||
self._stop_duration = (tf_in_min * self._stop_duration_candles)
|
||||
else:
|
||||
self._stop_duration_candles = None
|
||||
self._stop_duration = protection_config.get('stop_duration', 60)
|
||||
if 'lookback_period_candles' in protection_config:
|
||||
self._lookback_period_candles = protection_config.get('lookback_period_candles', 1)
|
||||
self._lookback_period_candles = int(protection_config.get('lookback_period_candles', 1))
|
||||
self._lookback_period = tf_in_min * self._lookback_period_candles
|
||||
else:
|
||||
self._lookback_period_candles = None
|
||||
self._lookback_period = protection_config.get('lookback_period', 60)
|
||||
self._lookback_period = int(protection_config.get('lookback_period', 60))
|
||||
|
||||
LoggingMixin.__init__(self, logger)
|
||||
|
||||
|
@@ -54,9 +54,9 @@ class StoplossGuard(IProtection):
|
||||
|
||||
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 (
|
||||
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
|
||||
SellType.STOPLOSS_ON_EXCHANGE.value)
|
||||
and trade.close_profit and trade.close_profit < 0)]
|
||||
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
|
||||
SellType.STOPLOSS_ON_EXCHANGE.value)
|
||||
and trade.close_profit and trade.close_profit < 0)]
|
||||
|
||||
if len(trades) < self._trade_limit:
|
||||
return False, None, None
|
||||
|
@@ -8,6 +8,3 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from freqtrade.resolvers.pairlist_resolver import PairListResolver
|
||||
from freqtrade.resolvers.protection_resolver import ProtectionResolver
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
|
||||
|
||||
|
@@ -50,7 +50,7 @@ class StrategyResolver(IResolver):
|
||||
if 'timeframe' not in config:
|
||||
logger.warning(
|
||||
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
||||
)
|
||||
)
|
||||
strategy.timeframe = strategy.ticker_interval
|
||||
|
||||
if strategy._ft_params_from_file:
|
||||
@@ -119,7 +119,7 @@ class StrategyResolver(IResolver):
|
||||
- default (if not None)
|
||||
"""
|
||||
if (attribute in config
|
||||
and not isinstance(getattr(type(strategy), 'my_property', None), property)):
|
||||
and not isinstance(getattr(type(strategy), attribute, None), property)):
|
||||
# Ensure Properties are not overwritten
|
||||
setattr(strategy, attribute, config[attribute])
|
||||
logger.info("Override strategy '%s' with value in config file: %s.",
|
||||
|
@@ -199,8 +199,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
|
||||
config=Depends(get_config)):
|
||||
config = deepcopy(config)
|
||||
config.update({
|
||||
'strategy': strategy,
|
||||
})
|
||||
'strategy': strategy,
|
||||
})
|
||||
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
|
||||
|
||||
|
||||
|
@@ -62,7 +62,7 @@ class CryptoToFiatConverter:
|
||||
# If the request is not a 429 error we want to raise the normal error
|
||||
logger.error(
|
||||
"Could not load FIAT Cryptocurrency map for the following problem: {}".format(
|
||||
request_exception
|
||||
request_exception
|
||||
)
|
||||
)
|
||||
except (Exception) as exception:
|
||||
|
@@ -15,6 +15,7 @@ class RPCManager:
|
||||
"""
|
||||
Class to manage RPC objects (Telegram, API, ...)
|
||||
"""
|
||||
|
||||
def __init__(self, freqtrade) -> None:
|
||||
""" Initializes all enabled rpc modules """
|
||||
self.registered_modules: List[RPCHandler] = []
|
||||
|
@@ -77,7 +77,6 @@ class Telegram(RPCHandler):
|
||||
""" This class handles all telegram communication """
|
||||
|
||||
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
|
||||
|
||||
"""
|
||||
Init the Telegram call, and init the super class RPCHandler
|
||||
:param rpc: instance of RPC Helper class
|
||||
@@ -270,7 +269,7 @@ class Telegram(RPCHandler):
|
||||
noti = ''
|
||||
if msg_type == RPCMessageType.SELL:
|
||||
sell_noti = self._config['telegram'] \
|
||||
.get('notification_settings', {}).get(str(msg_type), {})
|
||||
.get('notification_settings', {}).get(str(msg_type), {})
|
||||
# For backward compatibility sell still can be string
|
||||
if isinstance(sell_noti, str):
|
||||
noti = sell_noti
|
||||
@@ -278,7 +277,7 @@ class Telegram(RPCHandler):
|
||||
noti = sell_noti.get(str(msg['sell_reason']), default_noti)
|
||||
else:
|
||||
noti = self._config['telegram'] \
|
||||
.get('notification_settings', {}).get(str(msg_type), default_noti)
|
||||
.get('notification_settings', {}).get(str(msg_type), default_noti)
|
||||
|
||||
if noti == 'off':
|
||||
logger.info(f"Notification '{msg_type}' not sent.")
|
||||
@@ -541,7 +540,7 @@ class Telegram(RPCHandler):
|
||||
f"`{first_trade_date}`\n"
|
||||
f"*Latest Trade opened:* `{latest_trade_date}\n`"
|
||||
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
|
||||
)
|
||||
)
|
||||
if stats['closed_trade_count'] > 0:
|
||||
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
||||
@@ -576,13 +575,14 @@ class Telegram(RPCHandler):
|
||||
sell_reasons_msg = tabulate(
|
||||
sell_reasons_tabulate,
|
||||
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
||||
)
|
||||
)
|
||||
durations = stats['durations']
|
||||
duration_msg = tabulate([
|
||||
['Wins', str(timedelta(seconds=durations['wins']))
|
||||
if durations['wins'] != 'N/A' else 'N/A'],
|
||||
['Losses', str(timedelta(seconds=durations['losses']))
|
||||
if durations['losses'] != 'N/A' else 'N/A']
|
||||
duration_msg = tabulate(
|
||||
[
|
||||
['Wins', str(timedelta(seconds=durations['wins']))
|
||||
if durations['wins'] != 'N/A' else 'N/A'],
|
||||
['Losses', str(timedelta(seconds=durations['losses']))
|
||||
if durations['losses'] != 'N/A' else 'N/A']
|
||||
],
|
||||
headers=['', 'Avg. Duration']
|
||||
)
|
||||
@@ -1100,7 +1100,7 @@ class Telegram(RPCHandler):
|
||||
if reload_able:
|
||||
reply_markup = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Refresh", callback_data=callback_path)],
|
||||
])
|
||||
])
|
||||
else:
|
||||
reply_markup = InlineKeyboardMarkup([[]])
|
||||
msg += "\nUpdated: {}".format(datetime.now().ctime())
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter,
|
||||
RealParameter)
|
||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
||||
|
@@ -270,6 +270,28 @@ class CategoricalParameter(BaseParameter):
|
||||
return [self.value]
|
||||
|
||||
|
||||
class BooleanParameter(CategoricalParameter):
|
||||
|
||||
def __init__(self, *, default: Optional[Any] = None,
|
||||
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
|
||||
"""
|
||||
Initialize hyperopt-optimizable Boolean Parameter.
|
||||
It's a shortcut to `CategoricalParameter([True, False])`.
|
||||
:param default: A default value. If not specified, first item from specified space will be
|
||||
used.
|
||||
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
|
||||
parameter field
|
||||
name is prefixed with 'buy_' or 'sell_'.
|
||||
:param optimize: Include parameter in hyperopt optimizations.
|
||||
:param load: Load parameter value from {space}_params.
|
||||
:param kwargs: Extra parameters to skopt.space.Categorical.
|
||||
"""
|
||||
|
||||
categories = [True, False]
|
||||
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class HyperStrategyMixin(object):
|
||||
"""
|
||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||
@@ -283,6 +305,7 @@ class HyperStrategyMixin(object):
|
||||
self.config = config
|
||||
self.ft_buy_params: List[BaseParameter] = []
|
||||
self.ft_sell_params: List[BaseParameter] = []
|
||||
self.ft_protection_params: List[BaseParameter] = []
|
||||
|
||||
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
|
||||
|
||||
@@ -292,11 +315,12 @@ class HyperStrategyMixin(object):
|
||||
:param category:
|
||||
:return:
|
||||
"""
|
||||
if category not in ('buy', 'sell', None):
|
||||
raise OperationalException('Category must be one of: "buy", "sell", None.')
|
||||
if category not in ('buy', 'sell', 'protection', None):
|
||||
raise OperationalException(
|
||||
'Category must be one of: "buy", "sell", "protection", None.')
|
||||
|
||||
if category is None:
|
||||
params = self.ft_buy_params + self.ft_sell_params
|
||||
params = self.ft_buy_params + self.ft_sell_params + self.ft_protection_params
|
||||
else:
|
||||
params = getattr(self, f"ft_{category}_params")
|
||||
|
||||
@@ -324,9 +348,10 @@ class HyperStrategyMixin(object):
|
||||
params: Dict = {
|
||||
'buy': list(cls.detect_parameters('buy')),
|
||||
'sell': list(cls.detect_parameters('sell')),
|
||||
'protection': list(cls.detect_parameters('protection')),
|
||||
}
|
||||
params.update({
|
||||
'count': len(params['buy'] + params['sell'])
|
||||
'count': len(params['buy'] + params['sell'] + params['protection'])
|
||||
})
|
||||
|
||||
return params
|
||||
@@ -340,9 +365,12 @@ class HyperStrategyMixin(object):
|
||||
self._ft_params_from_file = params
|
||||
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {}))
|
||||
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {}))
|
||||
protection_params = deep_merge_dicts(params.get('protection', {}),
|
||||
getattr(self, 'protection_params', {}))
|
||||
|
||||
self._load_params(buy_params, 'buy', hyperopt)
|
||||
self._load_params(sell_params, 'sell', hyperopt)
|
||||
self._load_params(protection_params, 'protection', hyperopt)
|
||||
|
||||
def load_params_from_file(self) -> Dict:
|
||||
filename_str = getattr(self, '__file__', '')
|
||||
@@ -397,7 +425,8 @@ class HyperStrategyMixin(object):
|
||||
"""
|
||||
params = {
|
||||
'buy': {},
|
||||
'sell': {}
|
||||
'sell': {},
|
||||
'protection': {},
|
||||
}
|
||||
for name, p in self.enumerate_parameters():
|
||||
if not p.optimize or not p.in_space:
|
||||
|
@@ -605,7 +605,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
current_rate = rate
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
trade.adjust_min_max_rates(high or current_rate)
|
||||
trade.adjust_min_max_rates(high or current_rate, low or current_rate)
|
||||
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
@@ -769,7 +769,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Populates indicators for given candle (OHLCV) data (for multiple pairs)
|
||||
Does not run advise_buy or advise_sell!
|
||||
|
@@ -38,7 +38,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
||||
informative['date_merge'] = (
|
||||
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
||||
"This would create new rows, and can throw off your regular indicators.")
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"ask_strategy": {
|
||||
"price_side": "ask",
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"order_book_top": 1
|
||||
},
|
||||
{{ exchange | indent(4) }},
|
||||
"pairlists": [
|
||||
|
@@ -6,8 +6,8 @@ import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import IStrategy
|
||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
|
@@ -6,8 +6,8 @@ import numpy as np # noqa
|
||||
import pandas as pd # noqa
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import IStrategy
|
||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
|
Reference in New Issue
Block a user