Merge branch 'develop' into pr/GluTbl/5756
This commit is contained in:
@@ -44,6 +44,7 @@ SELL_IDX = 4
|
||||
LOW_IDX = 5
|
||||
HIGH_IDX = 6
|
||||
BUY_TAG_IDX = 7
|
||||
EXIT_TAG_IDX = 8
|
||||
|
||||
|
||||
class Backtesting:
|
||||
@@ -66,7 +67,7 @@ class Backtesting:
|
||||
self.all_results: Dict[str, Dict] = {}
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
self.dataprovider = DataProvider(self.config, None)
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
|
||||
if self.config.get('strategy_list', None):
|
||||
for strat in list(self.config['strategy_list']):
|
||||
@@ -88,7 +89,8 @@ class Backtesting:
|
||||
self.init_backtest_detail()
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
raise OperationalException("VolumePairList not allowed for backtesting. "
|
||||
"Please use StaticPairlist instead.")
|
||||
if 'PerformanceFilter' in self.pairlists.name_list:
|
||||
raise OperationalException("PerformanceFilter not allowed for backtesting.")
|
||||
|
||||
@@ -247,7 +249,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', 'buy_tag']
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag']
|
||||
data: Dict = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
@@ -259,6 +261,7 @@ class Backtesting:
|
||||
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
||||
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
||||
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||
pair_data.loc[:, 'exit_tag'] = None # cleanup if exit_tag is exist
|
||||
|
||||
df_analyzed = self.strategy.advise_sell(
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
|
||||
@@ -270,6 +273,7 @@ class Backtesting:
|
||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
||||
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
||||
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||
df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1)
|
||||
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||
@@ -312,7 +316,9 @@ class Backtesting:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
||||
assert stop_rate < sell_row[HIGH_IDX]
|
||||
return stop_rate
|
||||
# Limit lower-end to candle low to avoid sells below the low.
|
||||
# This still remains "worst case" - but "worst realistic case".
|
||||
return max(sell_row[LOW_IDX], stop_rate)
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
@@ -357,7 +363,7 @@ class Backtesting:
|
||||
|
||||
if sell.sell_flag:
|
||||
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)
|
||||
# call the custom exit price,with default value as previous closerate
|
||||
@@ -378,6 +384,17 @@ class Backtesting:
|
||||
current_time=sell_candle_time):
|
||||
return None
|
||||
|
||||
trade.sell_reason = sell.sell_reason
|
||||
|
||||
# Checks and adds an exit tag, after checking that the length of the
|
||||
# sell_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
|
||||
):
|
||||
trade.sell_reason = sell_row[EXIT_TAG_IDX]
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
return trade
|
||||
|
||||
@@ -392,7 +409,7 @@ class Backtesting:
|
||||
detail_data = detail_data.loc[
|
||||
(detail_data['date'] >= sell_candle_time) &
|
||||
(detail_data['date'] < sell_candle_end)
|
||||
].copy()
|
||||
].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)
|
||||
@@ -427,7 +444,7 @@ class Backtesting:
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
|
||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
if not stake_amount:
|
||||
return None
|
||||
|
@@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INITIAL_POINTS = 5
|
||||
INITIAL_POINTS = 30
|
||||
|
||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||
# in the skopt model queue, to optimize memory consumption
|
||||
|
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
64
freqtrade/optimize/hyperopt_loss_calmar.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
CalmarHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from math import sqrt as msqrt
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class CalmarHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation uses the Calmar Ratio calculation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Dict,
|
||||
processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args,
|
||||
**kwargs
|
||||
) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results.
|
||||
|
||||
Uses Calmar Ratio calculation.
|
||||
"""
|
||||
total_profit = backtest_stats["profit_total"]
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_returns_mean = total_profit.sum() / days_period * 100
|
||||
|
||||
# calculate max drawdown
|
||||
try:
|
||||
_, _, _, high_val, low_val = calculate_max_drawdown(
|
||||
results, value_col="profit_abs"
|
||||
)
|
||||
max_drawdown = (high_val - low_val) / high_val
|
||||
except ValueError:
|
||||
max_drawdown = 0
|
||||
|
||||
if max_drawdown != 0:
|
||||
calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
|
||||
else:
|
||||
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||
calmar_ratio = -20.0
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return -calmar_ratio
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import io
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
@@ -64,10 +63,11 @@ class HyperoptTools():
|
||||
'export_time': datetime.now(timezone.utc),
|
||||
}
|
||||
logger.info(f"Dumping parameters to {filename}")
|
||||
rapidjson.dump(final_params, filename.open('w'), indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
with filename.open('w') as f:
|
||||
rapidjson.dump(final_params, f, indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
||||
@@ -284,10 +284,10 @@ class HyperoptTools():
|
||||
return (f"{results_metrics['total_trades']:6d} trades. "
|
||||
f"{results_metrics['wins']}/{results_metrics['draws']}"
|
||||
f"/{results_metrics['losses']} Wins/Draws/Losses. "
|
||||
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
|
||||
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
|
||||
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
|
||||
f"({results_metrics['profit_total'] * 100: 7.2f}%). "
|
||||
f"Avg profit {results_metrics['profit_mean']:7.2%}. "
|
||||
f"Median profit {results_metrics['profit_median']:7.2%}. "
|
||||
f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
|
||||
f"({results_metrics['profit_total']:8.2%}). "
|
||||
f"Avg duration {results_metrics['holding_avg']} min."
|
||||
)
|
||||
|
||||
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from numpy import int64
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, to_datetime
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
||||
@@ -46,11 +46,11 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
||||
'.2f', 'd', 's', 's']
|
||||
|
||||
|
||||
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
||||
def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
|
||||
return [first_column, direction, 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Win Draw Loss Win%']
|
||||
|
||||
@@ -127,6 +127,38 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
|
||||
return tabular_data
|
||||
|
||||
|
||||
def generate_tag_metrics(tag_type: str,
|
||||
starting_balance: int,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False) -> List[Dict]:
|
||||
"""
|
||||
Generates and returns a list of metrics for the given tag trades and the results dataframe
|
||||
:param starting_balance: Starting balance
|
||||
:param results: Dataframe containing the backtest results
|
||||
:param skip_nan: Print "left open" open trades
|
||||
:return: List of Dicts containing the metrics per pair
|
||||
"""
|
||||
|
||||
tabular_data = []
|
||||
|
||||
if tag_type in results.columns:
|
||||
for tag, count in results[tag_type].value_counts().iteritems():
|
||||
result = results[results[tag_type] == tag]
|
||||
if skip_nan and result['profit_abs'].isnull().all():
|
||||
continue
|
||||
|
||||
tabular_data.append(_generate_result_line(result, starting_balance, tag))
|
||||
|
||||
# Sort by total profit %:
|
||||
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||
|
||||
# Append Total
|
||||
tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
|
||||
return tabular_data
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
@@ -189,7 +221,6 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
|
||||
|
||||
|
||||
def generate_edge_table(results: dict) -> str:
|
||||
|
||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
||||
tabular_data = []
|
||||
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
||||
@@ -214,6 +245,41 @@ def generate_edge_table(results: dict) -> str:
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
||||
|
||||
|
||||
def _get_resample_from_period(period: str) -> str:
|
||||
if period == 'day':
|
||||
return '1d'
|
||||
if period == 'week':
|
||||
return '1w'
|
||||
if period == 'month':
|
||||
return '1M'
|
||||
raise ValueError(f"Period {period} is not supported.")
|
||||
|
||||
|
||||
def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]:
|
||||
results = DataFrame.from_records(trade_list)
|
||||
if len(results) == 0:
|
||||
return []
|
||||
results['close_date'] = to_datetime(results['close_date'], utc=True)
|
||||
resample_period = _get_resample_from_period(period)
|
||||
resampled = results.resample(resample_period, on='close_date')
|
||||
stats = []
|
||||
for name, day in resampled:
|
||||
profit_abs = day['profit_abs'].sum().round(10)
|
||||
wins = sum(day['profit_abs'] > 0)
|
||||
draws = sum(day['profit_abs'] == 0)
|
||||
loses = sum(day['profit_abs'] < 0)
|
||||
stats.append(
|
||||
{
|
||||
'date': name.strftime('%d/%m/%Y'),
|
||||
'profit_abs': profit_abs,
|
||||
'wins': wins,
|
||||
'draws': draws,
|
||||
'loses': loses
|
||||
}
|
||||
)
|
||||
return stats
|
||||
|
||||
|
||||
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
""" Generate overall trade statistics """
|
||||
if len(results) == 0:
|
||||
@@ -313,6 +379,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||
starting_balance=starting_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||
results=results)
|
||||
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
|
||||
@@ -329,15 +399,18 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
||||
|
||||
backtest_days = (max_date - min_date).days
|
||||
backtest_days = (max_date - min_date).days or 1
|
||||
strat_stats = {
|
||||
'trades': results.to_dict(orient='records'),
|
||||
'locks': [lock.to_json() for lock in content['locks']],
|
||||
'best_pair': best_pair,
|
||||
'worst_pair': worst_pair,
|
||||
'results_per_pair': pair_results,
|
||||
'results_per_buy_tag': buy_tag_results,
|
||||
'sell_reason_summary': sell_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
# 'days_breakdown_stats': days_breakdown_stats,
|
||||
|
||||
'total_trades': len(results),
|
||||
'total_volume': float(results['stake_amount'].sum()),
|
||||
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
|
||||
@@ -354,7 +427,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'backtest_run_start_ts': content['backtest_start_time'],
|
||||
'backtest_run_end_ts': content['backtest_end_time'],
|
||||
|
||||
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
||||
'trades_per_day': round(len(results) / backtest_days, 2),
|
||||
'market_change': market_change,
|
||||
'pairlist': list(btdata.keys()),
|
||||
'stake_amount': config['stake_amount'],
|
||||
@@ -506,6 +579,59 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
"""
|
||||
Generates and returns a text table for the given backtest data and the results dataframe
|
||||
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
if(tag_type == "buy_tag"):
|
||||
headers = _get_line_header("TAG", stake_currency)
|
||||
else:
|
||||
headers = _get_line_header("TAG", stake_currency, 'Sells')
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
output = [
|
||||
[
|
||||
t['key'] if t['key'] is not None and len(
|
||||
t['key']) > 0 else "OTHER",
|
||||
t['trades'],
|
||||
t['profit_mean_pct'],
|
||||
t['profit_sum_pct'],
|
||||
t['profit_total_abs'],
|
||||
t['profit_total_pct'],
|
||||
t['duration_avg'],
|
||||
_generate_wins_draws_losses(
|
||||
t['wins'],
|
||||
t['draws'],
|
||||
t['losses'])] for t in tag_results]
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(output, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
||||
stake_currency: str, period: str) -> str:
|
||||
"""
|
||||
Generate small table with Backtest results by days
|
||||
:param days_breakdown_stats: Days breakdown metrics
|
||||
:param stake_currency: Stakecurrency used
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
headers = [
|
||||
period.capitalize(),
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Wins',
|
||||
'Draws',
|
||||
'Losses',
|
||||
]
|
||||
output = [[
|
||||
d['date'], round_coin_value(d['profit_abs'], stake_currency, False),
|
||||
d['wins'], d['draws'], d['loses'],
|
||||
] for d in days_breakdown_stats]
|
||||
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||
"""
|
||||
Generate summary table per strategy
|
||||
@@ -557,19 +683,22 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])),
|
||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
|
||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||
('Trades per day', strat_results['trades_per_day']),
|
||||
('Avg. daily profit %',
|
||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||
strat_results['stake_currency'])),
|
||||
('', ''), # Empty line to improve readability
|
||||
('Best Pair', f"{strat_results['best_pair']['key']} "
|
||||
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
|
||||
f"{strat_results['best_pair']['profit_sum']:.2%}"),
|
||||
('Worst Pair', f"{strat_results['worst_pair']['key']} "
|
||||
f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"),
|
||||
('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"),
|
||||
f"{strat_results['worst_pair']['profit_sum']:.2%}"),
|
||||
('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
|
||||
('Worst trade', f"{worst_trade['pair']} "
|
||||
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
|
||||
f"{worst_trade['profit_ratio']:.2%}"),
|
||||
|
||||
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
@@ -587,7 +716,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
('Max balance', round_coin_value(strat_results['csum_max'],
|
||||
strat_results['stake_currency'])),
|
||||
|
||||
('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
|
||||
('Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
||||
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
||||
@@ -596,7 +725,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown Start', strat_results['drawdown_start']),
|
||||
('Drawdown End', strat_results['drawdown_end']),
|
||||
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
|
||||
('Market change', f"{strat_results['market_change']:.2%}"),
|
||||
]
|
||||
|
||||
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
||||
@@ -614,7 +743,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
return message
|
||||
|
||||
|
||||
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str):
|
||||
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
|
||||
backtest_breakdown=[]):
|
||||
"""
|
||||
Print results for one strategy
|
||||
"""
|
||||
@@ -625,6 +755,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
if results.get('results_per_buy_tag') is not None:
|
||||
table = text_table_tags(
|
||||
"buy_tag",
|
||||
results['results_per_buy_tag'],
|
||||
stake_currency=stake_currency)
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
|
||||
stake_currency=stake_currency)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
@@ -636,6 +776,15 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
for period in backtest_breakdown:
|
||||
days_breakdown_stats = generate_periodic_breakdown_stats(
|
||||
trade_list=results['trades'], period=period)
|
||||
table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
|
||||
stake_currency=stake_currency, period=period)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(f' {period.upper()} BREAKDOWN '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
table = text_table_add_metrics(results)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
|
||||
@@ -643,6 +792,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
|
||||
print()
|
||||
|
||||
|
||||
@@ -650,7 +800,9 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||
stake_currency = config['stake_currency']
|
||||
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
show_backtest_result(strategy, results, stake_currency)
|
||||
show_backtest_result(
|
||||
strategy, results, stake_currency,
|
||||
config.get('backtest_breakdown', []))
|
||||
|
||||
if len(backtest_stats['strategy']) > 1:
|
||||
# Print Strategy summary table
|
||||
@@ -662,3 +814,13 @@ def show_backtest_results(config: Dict, backtest_stats: Dict):
|
||||
print(table)
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
print('\nFor more details, please look at the detail tables above')
|
||||
|
||||
|
||||
def show_sorted_pairlist(config: Dict, backtest_stats: Dict):
|
||||
if config.get('backtest_show_pair_list', False):
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
print(f"Pairs for Strategy {strategy}: \n[")
|
||||
for result in results['results_per_pair']:
|
||||
if result["key"] != 'TOTAL':
|
||||
print(f'"{result["key"]}", // {result["profit_mean"]:.2%}')
|
||||
print("]")
|
||||
|
@@ -7,11 +7,15 @@ class SKDecimal(Integer):
|
||||
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
|
||||
name=None, dtype=np.int64):
|
||||
self.decimals = decimals
|
||||
_low = int(low * pow(10, self.decimals))
|
||||
_high = int(high * pow(10, self.decimals))
|
||||
|
||||
self.pow_dot_one = pow(0.1, self.decimals)
|
||||
self.pow_ten = pow(10, self.decimals)
|
||||
|
||||
_low = int(low * self.pow_ten)
|
||||
_high = int(high * self.pow_ten)
|
||||
# trunc to precision to avoid points out of space
|
||||
self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals)
|
||||
self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals)
|
||||
self.low_orig = round(_low * self.pow_dot_one, self.decimals)
|
||||
self.high_orig = round(_high * self.pow_dot_one, self.decimals)
|
||||
|
||||
super().__init__(_low, _high, prior, base, transform, name, dtype)
|
||||
|
||||
@@ -25,9 +29,9 @@ class SKDecimal(Integer):
|
||||
return self.low_orig <= point <= self.high_orig
|
||||
|
||||
def transform(self, Xt):
|
||||
aa = [int(x * pow(10, self.decimals)) for x in Xt]
|
||||
return super().transform(aa)
|
||||
return super().transform([int(v * self.pow_ten) for v in Xt])
|
||||
|
||||
def inverse_transform(self, Xt):
|
||||
res = super().inverse_transform(Xt)
|
||||
return [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||
# equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
|
||||
return [int(v) / self.pow_ten for v in res]
|
||||
|
Reference in New Issue
Block a user