mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 01:21:11 +00:00
ruff format: optimize
This commit is contained in:
parent
2c60985e2d
commit
801ab4acc9
|
@ -17,19 +17,23 @@ def get_strategy_run_id(strategy) -> str:
|
|||
config = deepcopy(strategy.config)
|
||||
|
||||
# Options that have no impact on results of individual backtest.
|
||||
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
|
||||
not_important_keys = ("strategy_list", "original_config", "telegram", "api_server")
|
||||
for k in not_important_keys:
|
||||
if k in config:
|
||||
del config[k]
|
||||
|
||||
# Explicitly allow NaN values (e.g. max_open_trades).
|
||||
# as it does not matter for getting the hash.
|
||||
digest.update(rapidjson.dumps(config, default=str,
|
||||
number_mode=rapidjson.NM_NAN).encode('utf-8'))
|
||||
digest.update(
|
||||
rapidjson.dumps(config, default=str, number_mode=rapidjson.NM_NAN).encode("utf-8")
|
||||
)
|
||||
# Include _ft_params_from_file - so changing parameter files cause cache eviction
|
||||
digest.update(rapidjson.dumps(
|
||||
strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8'))
|
||||
with Path(strategy.__file__).open('rb') as fp:
|
||||
digest.update(
|
||||
rapidjson.dumps(
|
||||
strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN
|
||||
).encode("utf-8")
|
||||
)
|
||||
with Path(strategy.__file__).open("rb") as fp:
|
||||
digest.update(fp.read())
|
||||
return digest.hexdigest().lower()
|
||||
|
||||
|
@ -37,4 +41,4 @@ def get_strategy_run_id(strategy) -> str:
|
|||
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
|
||||
"""Return metadata filename for specified backtest results file."""
|
||||
filename = Path(filename)
|
||||
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')
|
||||
return filename.parent / Path(f"{filename.stem}.meta{filename.suffix}")
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -25,7 +25,6 @@ class VarHolder:
|
|||
|
||||
|
||||
class BaseAnalysis:
|
||||
|
||||
def __init__(self, config: Dict[str, Any], strategy_obj: Dict):
|
||||
self.failed_bias_check = True
|
||||
self.full_varHolder = VarHolder()
|
||||
|
@ -34,7 +33,7 @@ class BaseAnalysis:
|
|||
|
||||
# pull variables the scope of the lookahead_analysis-instance
|
||||
self.local_config = deepcopy(config)
|
||||
self.local_config['strategy'] = strategy_obj['name']
|
||||
self.local_config["strategy"] = strategy_obj["name"]
|
||||
self.strategy_obj = strategy_obj
|
||||
|
||||
@staticmethod
|
||||
|
@ -46,7 +45,7 @@ class BaseAnalysis:
|
|||
self.full_varHolder = VarHolder()
|
||||
|
||||
# define datetime in human-readable format
|
||||
parsed_timerange = TimeRange.parse_timerange(self.local_config['timerange'])
|
||||
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
|
||||
|
||||
if parsed_timerange.startdt is None:
|
||||
self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=timezone.utc)
|
||||
|
@ -58,9 +57,8 @@ class BaseAnalysis:
|
|||
else:
|
||||
self.full_varHolder.to_dt = parsed_timerange.stopdt
|
||||
|
||||
self.prepare_data(self.full_varHolder, self.local_config['pairs'])
|
||||
self.prepare_data(self.full_varHolder, self.local_config["pairs"])
|
||||
|
||||
def start(self) -> None:
|
||||
|
||||
# first make a single backtest
|
||||
self.fill_full_varholder()
|
||||
|
|
|
@ -25,8 +25,9 @@ class BTProgress:
|
|||
"""
|
||||
Get progress as ratio, capped to be between 0 and 1 (to avoid small calculation errors).
|
||||
"""
|
||||
return max(min(round(self._progress / self._max_steps, 5)
|
||||
if self._max_steps > 0 else 0, 1), 0)
|
||||
return max(
|
||||
min(round(self._progress / self._max_steps, 5) if self._max_steps > 0 else 0, 1), 0
|
||||
)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"""
|
||||
This module contains the edge backtesting interface
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from freqtrade import constants
|
||||
|
@ -30,8 +31,8 @@ class EdgeCli:
|
|||
self.config = config
|
||||
|
||||
# Ensure using dry-run
|
||||
self.config['dry_run'] = True
|
||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.config["dry_run"] = True
|
||||
self.config["stake_amount"] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config)
|
||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||
self.strategy.dp = DataProvider(config, self.exchange)
|
||||
|
@ -42,12 +43,13 @@ class EdgeCli:
|
|||
# Set refresh_pairs to false for edge-cli (it must be true for edge)
|
||||
self.edge._refresh_pairs = False
|
||||
|
||||
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
self.edge._timerange = TimeRange.parse_timerange(
|
||||
None if self.config.get("timerange") is None else str(self.config.get("timerange"))
|
||||
)
|
||||
self.strategy.ft_bot_start()
|
||||
|
||||
def start(self) -> None:
|
||||
result = self.edge.calculate(self.config['exchange']['pair_whitelist'])
|
||||
result = self.edge.calculate(self.config["exchange"]["pair_whitelist"])
|
||||
if result:
|
||||
print('') # blank line for readability
|
||||
print("") # blank line for readability
|
||||
print(generate_edge_table(self.edge._cached_pairs))
|
||||
|
|
|
@ -16,28 +16,34 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
|||
"""
|
||||
Generate floatformat (goes in line with _generate_result_line())
|
||||
"""
|
||||
return ['s', 'd', '.2f', f'.{decimals_per_coin(stake_currency)}f',
|
||||
'.2f', 'd', 's', 's']
|
||||
return ["s", "d", ".2f", f".{decimals_per_coin(stake_currency)}f", ".2f", "d", "s", "s"]
|
||||
|
||||
|
||||
def _get_line_header(first_column: str, stake_currency: str,
|
||||
direction: str = 'Entries') -> List[str]:
|
||||
def _get_line_header(
|
||||
first_column: str, stake_currency: str, direction: str = "Entries"
|
||||
) -> List[str]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [first_column, direction, 'Avg Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Win Draw Loss Win%']
|
||||
return [
|
||||
first_column,
|
||||
direction,
|
||||
"Avg Profit %",
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Tot Profit %",
|
||||
"Avg Duration",
|
||||
"Win Draw Loss Win%",
|
||||
]
|
||||
|
||||
|
||||
def generate_wins_draws_losses(wins, draws, losses):
|
||||
if wins > 0 and losses == 0:
|
||||
wl_ratio = '100'
|
||||
wl_ratio = "100"
|
||||
elif wins == 0:
|
||||
wl_ratio = '0'
|
||||
wl_ratio = "0"
|
||||
else:
|
||||
wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
|
||||
return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}'
|
||||
wl_ratio = f"{100.0 / (wins + draws + losses) * wins:.1f}" if losses > 0 else "100"
|
||||
return f"{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}"
|
||||
|
||||
|
||||
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
|
@ -48,16 +54,22 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
|
|||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
|
||||
headers = _get_line_header('Pair', stake_currency)
|
||||
headers = _get_line_header("Pair", stake_currency)
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
output = [[
|
||||
t['key'], t['trades'], t['profit_mean_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 pair_results]
|
||||
output = [
|
||||
[
|
||||
t["key"],
|
||||
t["trades"],
|
||||
t["profit_mean_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 pair_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")
|
||||
return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
|
@ -67,34 +79,35 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
|||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
fallback: str = ''
|
||||
if (tag_type == "enter_tag"):
|
||||
fallback: str = ""
|
||||
if tag_type == "enter_tag":
|
||||
headers = _get_line_header("TAG", stake_currency)
|
||||
else:
|
||||
headers = _get_line_header("Exit Reason", stake_currency, 'Exits')
|
||||
fallback = 'exit_reason'
|
||||
headers = _get_line_header("Exit Reason", stake_currency, "Exits")
|
||||
fallback = "exit_reason"
|
||||
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
output = [
|
||||
[
|
||||
t['key'] if t.get('key') is not None and len(
|
||||
str(t['key'])) > 0 else t.get(fallback, "OTHER"),
|
||||
t['trades'],
|
||||
t['profit_mean_pct'],
|
||||
t['profit_total_abs'],
|
||||
t['profit_total_pct'],
|
||||
t.get('duration_avg'),
|
||||
generate_wins_draws_losses(
|
||||
t['wins'],
|
||||
t['draws'],
|
||||
t['losses'])] for t in tag_results]
|
||||
t["key"]
|
||||
if t.get("key") is not None and len(str(t["key"])) > 0
|
||||
else t.get(fallback, "OTHER"),
|
||||
t["trades"],
|
||||
t["profit_mean_pct"],
|
||||
t["profit_total_abs"],
|
||||
t["profit_total_pct"],
|
||||
t.get("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")
|
||||
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:
|
||||
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
|
||||
|
@ -103,15 +116,21 @@ def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
|
|||
"""
|
||||
headers = [
|
||||
period.capitalize(),
|
||||
f'Tot Profit {stake_currency}',
|
||||
'Wins',
|
||||
'Draws',
|
||||
'Losses',
|
||||
f"Tot Profit {stake_currency}",
|
||||
"Wins",
|
||||
"Draws",
|
||||
"Losses",
|
||||
]
|
||||
output = [
|
||||
[
|
||||
d["date"],
|
||||
fmt_coin(d["profit_abs"], stake_currency, False),
|
||||
d["wins"],
|
||||
d["draws"],
|
||||
d["loses"],
|
||||
]
|
||||
for d in days_breakdown_stats
|
||||
]
|
||||
output = [[
|
||||
d['date'], fmt_coin(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")
|
||||
|
||||
|
||||
|
@ -123,263 +142,352 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
|||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
headers = _get_line_header('Strategy', stake_currency)
|
||||
headers = _get_line_header("Strategy", stake_currency)
|
||||
# _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
|
||||
# therefore we slip this column in only for strategy summary here.
|
||||
headers.append('Drawdown')
|
||||
headers.append("Drawdown")
|
||||
|
||||
# Align drawdown string on the center two space separator.
|
||||
if 'max_drawdown_account' in strategy_results[0]:
|
||||
if "max_drawdown_account" in strategy_results[0]:
|
||||
drawdown = [f'{t["max_drawdown_account"] * 100:.2f}' for t in strategy_results]
|
||||
else:
|
||||
# Support for prior backtest results
|
||||
drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results]
|
||||
|
||||
dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results])
|
||||
dd_pad_abs = max([len(t["max_drawdown_abs"]) for t in strategy_results])
|
||||
dd_pad_per = max([len(dd) for dd in drawdown])
|
||||
drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%'
|
||||
for t, dd in zip(strategy_results, drawdown)]
|
||||
drawdown = [
|
||||
f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency} {dd:>{dd_pad_per}}%'
|
||||
for t, dd in zip(strategy_results, drawdown)
|
||||
]
|
||||
|
||||
output = [[
|
||||
t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'],
|
||||
t['profit_total_pct'], t['duration_avg'],
|
||||
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
|
||||
for t, drawdown in zip(strategy_results, drawdown)]
|
||||
output = [
|
||||
[
|
||||
t["key"],
|
||||
t["trades"],
|
||||
t["profit_mean_pct"],
|
||||
t["profit_total_abs"],
|
||||
t["profit_total_pct"],
|
||||
t["duration_avg"],
|
||||
generate_wins_draws_losses(t["wins"], t["draws"], t["losses"]),
|
||||
drawdown,
|
||||
]
|
||||
for t, drawdown in zip(strategy_results, drawdown)
|
||||
]
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(output, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
|
||||
|
||||
def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
if len(strat_results['trades']) > 0:
|
||||
best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
|
||||
worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
|
||||
if len(strat_results["trades"]) > 0:
|
||||
best_trade = max(strat_results["trades"], key=lambda x: x["profit_ratio"])
|
||||
worst_trade = min(strat_results["trades"], key=lambda x: x["profit_ratio"])
|
||||
|
||||
short_metrics = [
|
||||
('', ''), # Empty line to improve readability
|
||||
('Long / Short',
|
||||
f"{strat_results.get('trade_count_long', 'total_trades')} / "
|
||||
f"{strat_results.get('trade_count_short', 0)}"),
|
||||
('Total profit Long %', f"{strat_results['profit_total_long']:.2%}"),
|
||||
('Total profit Short %', f"{strat_results['profit_total_short']:.2%}"),
|
||||
('Absolute profit Long', fmt_coin(strat_results['profit_total_long_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Absolute profit Short', fmt_coin(strat_results['profit_total_short_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
] if strat_results.get('trade_count_short', 0) > 0 else []
|
||||
short_metrics = (
|
||||
[
|
||||
("", ""), # Empty line to improve readability
|
||||
(
|
||||
"Long / Short",
|
||||
f"{strat_results.get('trade_count_long', 'total_trades')} / "
|
||||
f"{strat_results.get('trade_count_short', 0)}",
|
||||
),
|
||||
("Total profit Long %", f"{strat_results['profit_total_long']:.2%}"),
|
||||
("Total profit Short %", f"{strat_results['profit_total_short']:.2%}"),
|
||||
(
|
||||
"Absolute profit Long",
|
||||
fmt_coin(
|
||||
strat_results["profit_total_long_abs"], strat_results["stake_currency"]
|
||||
),
|
||||
),
|
||||
(
|
||||
"Absolute profit Short",
|
||||
fmt_coin(
|
||||
strat_results["profit_total_short_abs"], strat_results["stake_currency"]
|
||||
),
|
||||
),
|
||||
]
|
||||
if strat_results.get("trade_count_short", 0) > 0
|
||||
else []
|
||||
)
|
||||
|
||||
drawdown_metrics = []
|
||||
if 'max_relative_drawdown' in strat_results:
|
||||
if "max_relative_drawdown" in strat_results:
|
||||
# Compatibility to show old hyperopt results
|
||||
drawdown_metrics.append(
|
||||
('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}")
|
||||
("Max % of account underwater", f"{strat_results['max_relative_drawdown']:.2%}")
|
||||
)
|
||||
drawdown_metrics.extend([
|
||||
('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
|
||||
if 'max_drawdown_account' in strat_results else (
|
||||
'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
||||
('Absolute Drawdown', fmt_coin(strat_results['max_drawdown_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown high', fmt_coin(strat_results['max_drawdown_high'],
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown low', fmt_coin(strat_results['max_drawdown_low'],
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown Start', strat_results['drawdown_start']),
|
||||
('Drawdown End', strat_results['drawdown_end']),
|
||||
])
|
||||
drawdown_metrics.extend(
|
||||
[
|
||||
("Absolute Drawdown (Account)", f"{strat_results['max_drawdown_account']:.2%}")
|
||||
if "max_drawdown_account" in strat_results
|
||||
else ("Drawdown", f"{strat_results['max_drawdown']:.2%}"),
|
||||
(
|
||||
"Absolute Drawdown",
|
||||
fmt_coin(strat_results["max_drawdown_abs"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Drawdown high",
|
||||
fmt_coin(strat_results["max_drawdown_high"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Drawdown low",
|
||||
fmt_coin(strat_results["max_drawdown_low"], strat_results["stake_currency"]),
|
||||
),
|
||||
("Drawdown Start", strat_results["drawdown_start"]),
|
||||
("Drawdown End", strat_results["drawdown_end"]),
|
||||
]
|
||||
)
|
||||
|
||||
entry_adjustment_metrics = [
|
||||
('Canceled Trade Entries', strat_results.get('canceled_trade_entries', 'N/A')),
|
||||
('Canceled Entry Orders', strat_results.get('canceled_entry_orders', 'N/A')),
|
||||
('Replaced Entry Orders', strat_results.get('replaced_entry_orders', 'N/A')),
|
||||
] if strat_results.get('canceled_entry_orders', 0) > 0 else []
|
||||
entry_adjustment_metrics = (
|
||||
[
|
||||
("Canceled Trade Entries", strat_results.get("canceled_trade_entries", "N/A")),
|
||||
("Canceled Entry Orders", strat_results.get("canceled_entry_orders", "N/A")),
|
||||
("Replaced Entry Orders", strat_results.get("replaced_entry_orders", "N/A")),
|
||||
]
|
||||
if strat_results.get("canceled_entry_orders", 0) > 0
|
||||
else []
|
||||
)
|
||||
|
||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
||||
# command stores these results and newer version of freqtrade must be able to handle old
|
||||
# results with missing new fields.
|
||||
metrics = [
|
||||
('Backtesting from', strat_results['backtest_start']),
|
||||
('Backtesting to', strat_results['backtest_end']),
|
||||
('Max open trades', strat_results['max_open_trades']),
|
||||
('', ''), # Empty line to improve readability
|
||||
('Total/Daily Avg Trades',
|
||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
|
||||
|
||||
('Starting balance', fmt_coin(strat_results['starting_balance'],
|
||||
strat_results['stake_currency'])),
|
||||
('Final balance', fmt_coin(strat_results['final_balance'],
|
||||
strat_results['stake_currency'])),
|
||||
('Absolute profit ', fmt_coin(strat_results['profit_total_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
||||
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
|
||||
('Sharpe', f"{strat_results['sharpe']:.2f}" if 'sharpe' in strat_results else 'N/A'),
|
||||
('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
|
||||
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
|
||||
in strat_results else 'N/A'),
|
||||
('Expectancy (Ratio)', (
|
||||
f"{strat_results['expectancy']:.2f} ({strat_results['expectancy_ratio']:.2f})" if
|
||||
'expectancy_ratio' in strat_results else 'N/A')),
|
||||
('Avg. daily profit %',
|
||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||
('Avg. stake amount', fmt_coin(strat_results['avg_stake_amount'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total trade volume', fmt_coin(strat_results['total_volume'],
|
||||
strat_results['stake_currency'])),
|
||||
("Backtesting from", strat_results["backtest_start"]),
|
||||
("Backtesting to", strat_results["backtest_end"]),
|
||||
("Max open trades", strat_results["max_open_trades"]),
|
||||
("", ""), # Empty line to improve readability
|
||||
(
|
||||
"Total/Daily Avg Trades",
|
||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}",
|
||||
),
|
||||
(
|
||||
"Starting balance",
|
||||
fmt_coin(strat_results["starting_balance"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Final balance",
|
||||
fmt_coin(strat_results["final_balance"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Absolute profit ",
|
||||
fmt_coin(strat_results["profit_total_abs"], strat_results["stake_currency"]),
|
||||
),
|
||||
("Total profit %", f"{strat_results['profit_total']:.2%}"),
|
||||
("CAGR %", f"{strat_results['cagr']:.2%}" if "cagr" in strat_results else "N/A"),
|
||||
("Sortino", f"{strat_results['sortino']:.2f}" if "sortino" in strat_results else "N/A"),
|
||||
("Sharpe", f"{strat_results['sharpe']:.2f}" if "sharpe" in strat_results else "N/A"),
|
||||
("Calmar", f"{strat_results['calmar']:.2f}" if "calmar" in strat_results else "N/A"),
|
||||
(
|
||||
"Profit factor",
|
||||
f'{strat_results["profit_factor"]:.2f}'
|
||||
if "profit_factor" in strat_results
|
||||
else "N/A",
|
||||
),
|
||||
(
|
||||
"Expectancy (Ratio)",
|
||||
(
|
||||
f"{strat_results['expectancy']:.2f} ({strat_results['expectancy_ratio']:.2f})"
|
||||
if "expectancy_ratio" in strat_results
|
||||
else "N/A"
|
||||
),
|
||||
),
|
||||
(
|
||||
"Avg. daily profit %",
|
||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}",
|
||||
),
|
||||
(
|
||||
"Avg. stake amount",
|
||||
fmt_coin(strat_results["avg_stake_amount"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Total trade volume",
|
||||
fmt_coin(strat_results["total_volume"], strat_results["stake_currency"]),
|
||||
),
|
||||
*short_metrics,
|
||||
('', ''), # Empty line to improve readability
|
||||
('Best Pair', f"{strat_results['best_pair']['key']} "
|
||||
f"{strat_results['best_pair']['profit_total']:.2%}"),
|
||||
('Worst Pair', f"{strat_results['worst_pair']['key']} "
|
||||
f"{strat_results['worst_pair']['profit_total']:.2%}"),
|
||||
('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
|
||||
('Worst trade', f"{worst_trade['pair']} "
|
||||
f"{worst_trade['profit_ratio']:.2%}"),
|
||||
|
||||
('Best day', fmt_coin(strat_results['backtest_best_day_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Worst day', fmt_coin(strat_results['backtest_worst_day_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Days win/draw/lose', f"{strat_results['winning_days']} / "
|
||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||
('Max Consecutive Wins / Loss',
|
||||
f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}"
|
||||
if 'max_consecutive_losses' in strat_results else 'N/A'),
|
||||
('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
|
||||
('Entry/Exit Timeouts',
|
||||
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
||||
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
|
||||
("", ""), # Empty line to improve readability
|
||||
(
|
||||
"Best Pair",
|
||||
f"{strat_results['best_pair']['key']} "
|
||||
f"{strat_results['best_pair']['profit_total']:.2%}",
|
||||
),
|
||||
(
|
||||
"Worst Pair",
|
||||
f"{strat_results['worst_pair']['key']} "
|
||||
f"{strat_results['worst_pair']['profit_total']:.2%}",
|
||||
),
|
||||
("Best trade", f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
|
||||
("Worst trade", f"{worst_trade['pair']} " f"{worst_trade['profit_ratio']:.2%}"),
|
||||
(
|
||||
"Best day",
|
||||
fmt_coin(strat_results["backtest_best_day_abs"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Worst day",
|
||||
fmt_coin(strat_results["backtest_worst_day_abs"], strat_results["stake_currency"]),
|
||||
),
|
||||
(
|
||||
"Days win/draw/lose",
|
||||
f"{strat_results['winning_days']} / "
|
||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}",
|
||||
),
|
||||
("Avg. Duration Winners", f"{strat_results['winner_holding_avg']}"),
|
||||
("Avg. Duration Loser", f"{strat_results['loser_holding_avg']}"),
|
||||
(
|
||||
"Max Consecutive Wins / Loss",
|
||||
f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}"
|
||||
if "max_consecutive_losses" in strat_results
|
||||
else "N/A",
|
||||
),
|
||||
("Rejected Entry signals", strat_results.get("rejected_signals", "N/A")),
|
||||
(
|
||||
"Entry/Exit Timeouts",
|
||||
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
||||
f"{strat_results.get('timedout_exit_orders', 'N/A')}",
|
||||
),
|
||||
*entry_adjustment_metrics,
|
||||
('', ''), # Empty line to improve readability
|
||||
|
||||
('Min balance', fmt_coin(strat_results['csum_min'], strat_results['stake_currency'])),
|
||||
('Max balance', fmt_coin(strat_results['csum_max'], strat_results['stake_currency'])),
|
||||
|
||||
("", ""), # Empty line to improve readability
|
||||
("Min balance", fmt_coin(strat_results["csum_min"], strat_results["stake_currency"])),
|
||||
("Max balance", fmt_coin(strat_results["csum_max"], strat_results["stake_currency"])),
|
||||
*drawdown_metrics,
|
||||
('Market change', f"{strat_results['market_change']:.2%}"),
|
||||
("Market change", f"{strat_results['market_change']:.2%}"),
|
||||
]
|
||||
|
||||
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
||||
else:
|
||||
start_balance = fmt_coin(strat_results['starting_balance'], strat_results['stake_currency'])
|
||||
stake_amount = fmt_coin(
|
||||
strat_results['stake_amount'], strat_results['stake_currency']
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
start_balance = fmt_coin(strat_results["starting_balance"], strat_results["stake_currency"])
|
||||
stake_amount = (
|
||||
fmt_coin(strat_results["stake_amount"], strat_results["stake_currency"])
|
||||
if strat_results["stake_amount"] != UNLIMITED_STAKE_AMOUNT
|
||||
else "unlimited"
|
||||
)
|
||||
|
||||
message = ("No trades made. "
|
||||
f"Your starting balance was {start_balance}, "
|
||||
f"and your stake was {stake_amount}."
|
||||
)
|
||||
message = (
|
||||
"No trades made. "
|
||||
f"Your starting balance was {start_balance}, "
|
||||
f"and your stake was {stake_amount}."
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
|
||||
backtest_breakdown: List[str]):
|
||||
def show_backtest_result(
|
||||
strategy: str, results: Dict[str, Any], stake_currency: str, backtest_breakdown: List[str]
|
||||
):
|
||||
"""
|
||||
Print results for one strategy
|
||||
"""
|
||||
# Print results
|
||||
print(f"Result for strategy {strategy}")
|
||||
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
|
||||
table = text_table_bt_results(results["results_per_pair"], stake_currency=stake_currency)
|
||||
if isinstance(table, str):
|
||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(" BACKTESTING REPORT ".center(len(table.splitlines()[0]), "="))
|
||||
print(table)
|
||||
|
||||
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
|
||||
table = text_table_bt_results(results["left_open_trades"], stake_currency=stake_currency)
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(" LEFT OPEN TRADES REPORT ".center(len(table.splitlines()[0]), "="))
|
||||
print(table)
|
||||
|
||||
if (enter_tags := results.get('results_per_enter_tag')) is not None:
|
||||
if (enter_tags := results.get("results_per_enter_tag")) is not None:
|
||||
table = text_table_tags("enter_tag", enter_tags, stake_currency)
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(" ENTER TAG STATS ".center(len(table.splitlines()[0]), "="))
|
||||
print(table)
|
||||
|
||||
if (exit_reasons := results.get('exit_reason_summary')) is not None:
|
||||
if (exit_reasons := results.get("exit_reason_summary")) is not None:
|
||||
table = text_table_tags("exit_tag", exit_reasons, stake_currency)
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
|
||||
print(" EXIT REASON STATS ".center(len(table.splitlines()[0]), "="))
|
||||
print(table)
|
||||
|
||||
for period in backtest_breakdown:
|
||||
if period in results.get('periodic_breakdown', {}):
|
||||
days_breakdown_stats = results['periodic_breakdown'][period]
|
||||
if period in results.get("periodic_breakdown", {}):
|
||||
days_breakdown_stats = results["periodic_breakdown"][period]
|
||||
else:
|
||||
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)
|
||||
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(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]), '='))
|
||||
print(" SUMMARY METRICS ".center(len(table.splitlines()[0]), "="))
|
||||
print(table)
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
print("=" * len(table.splitlines()[0]))
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def show_backtest_results(config: Config, backtest_stats: BacktestResultType):
|
||||
stake_currency = config['stake_currency']
|
||||
stake_currency = config["stake_currency"]
|
||||
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
for strategy, results in backtest_stats["strategy"].items():
|
||||
show_backtest_result(
|
||||
strategy, results, stake_currency,
|
||||
config.get('backtest_breakdown', []))
|
||||
strategy, results, stake_currency, config.get("backtest_breakdown", [])
|
||||
)
|
||||
|
||||
if len(backtest_stats['strategy']) > 0:
|
||||
if len(backtest_stats["strategy"]) > 0:
|
||||
# Print Strategy summary table
|
||||
|
||||
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
|
||||
print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |"
|
||||
f" Max open trades : {results['max_open_trades']}")
|
||||
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
|
||||
table = text_table_strategy(backtest_stats["strategy_comparison"], stake_currency)
|
||||
print(
|
||||
f"Backtested {results['backtest_start']} -> {results['backtest_end']} |"
|
||||
f" Max open trades : {results['max_open_trades']}"
|
||||
)
|
||||
print(" STRATEGY SUMMARY ".center(len(table.splitlines()[0]), "="))
|
||||
print(table)
|
||||
print('=' * len(table.splitlines()[0]))
|
||||
print('\nFor more details, please look at the detail tables above')
|
||||
print("=" * len(table.splitlines()[0]))
|
||||
print("\nFor more details, please look at the detail tables above")
|
||||
|
||||
|
||||
def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType):
|
||||
if config.get('backtest_show_pair_list', False):
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
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':
|
||||
for result in results["results_per_pair"]:
|
||||
if result["key"] != "TOTAL":
|
||||
print(f'"{result["key"]}", // {result["profit_mean"]:.2%}')
|
||||
print("]")
|
||||
|
||||
|
||||
def generate_edge_table(results: dict) -> str:
|
||||
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
|
||||
floatfmt = ("s", ".10g", ".2f", ".2f", ".2f", ".2f", "d", "d", "d")
|
||||
tabular_data = []
|
||||
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
|
||||
'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
|
||||
'Average Duration (min)']
|
||||
headers = [
|
||||
"Pair",
|
||||
"Stoploss",
|
||||
"Win Rate",
|
||||
"Risk Reward Ratio",
|
||||
"Required Risk Reward",
|
||||
"Expectancy",
|
||||
"Total Number of Trades",
|
||||
"Average Duration (min)",
|
||||
]
|
||||
|
||||
for result in results.items():
|
||||
if result[1].nb_trades > 0:
|
||||
tabular_data.append([
|
||||
result[0],
|
||||
result[1].stoploss,
|
||||
result[1].winrate,
|
||||
result[1].risk_reward_ratio,
|
||||
result[1].required_risk_reward,
|
||||
result[1].expectancy,
|
||||
result[1].nb_trades,
|
||||
round(result[1].avg_trade_duration)
|
||||
])
|
||||
tabular_data.append(
|
||||
[
|
||||
result[0],
|
||||
result[1].stoploss,
|
||||
result[1].winrate,
|
||||
result[1].risk_reward_ratio,
|
||||
result[1].required_risk_reward,
|
||||
result[1].expectancy,
|
||||
result[1].nb_trades,
|
||||
round(result[1].avg_trade_duration),
|
||||
]
|
||||
)
|
||||
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(tabular_data, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||
return tabulate(
|
||||
tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right"
|
||||
)
|
||||
|
|
|
@ -22,17 +22,21 @@ def _generate_filename(recordfilename: Path, appendix: str, suffix: str) -> Path
|
|||
:return: Generated filename as a Path object
|
||||
"""
|
||||
if recordfilename.is_dir():
|
||||
filename = (recordfilename / f'backtest-result-{appendix}').with_suffix(suffix)
|
||||
filename = (recordfilename / f"backtest-result-{appendix}").with_suffix(suffix)
|
||||
else:
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent, f'{recordfilename.stem}-{appendix}'
|
||||
recordfilename.parent, f"{recordfilename.stem}-{appendix}"
|
||||
).with_suffix(suffix)
|
||||
return filename
|
||||
|
||||
|
||||
def store_backtest_stats(
|
||||
recordfilename: Path, stats: BacktestResultType, dtappendix: str, *,
|
||||
market_change_data: Optional[DataFrame] = None) -> Path:
|
||||
recordfilename: Path,
|
||||
stats: BacktestResultType,
|
||||
dtappendix: str,
|
||||
*,
|
||||
market_change_data: Optional[DataFrame] = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Stores backtest results
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
|
@ -41,32 +45,33 @@ def store_backtest_stats(
|
|||
:param stats: Dataframe containing the backtesting statistics
|
||||
:param dtappendix: Datetime to use for the filename
|
||||
"""
|
||||
filename = _generate_filename(recordfilename, dtappendix, '.json')
|
||||
filename = _generate_filename(recordfilename, dtappendix, ".json")
|
||||
|
||||
# Store metadata separately.
|
||||
file_dump_json(get_backtest_metadata_filename(filename), stats['metadata'])
|
||||
file_dump_json(get_backtest_metadata_filename(filename), stats["metadata"])
|
||||
# Don't mutate the original stats dict.
|
||||
stats_copy = {
|
||||
'strategy': stats['strategy'],
|
||||
'strategy_comparison': stats['strategy_comparison'],
|
||||
"strategy": stats["strategy"],
|
||||
"strategy_comparison": stats["strategy_comparison"],
|
||||
}
|
||||
|
||||
file_dump_json(filename, stats_copy)
|
||||
|
||||
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
||||
file_dump_json(latest_filename, {"latest_backtest": str(filename.name)})
|
||||
|
||||
if market_change_data is not None:
|
||||
filename_mc = _generate_filename(recordfilename, f"{dtappendix}_market_change", '.feather')
|
||||
filename_mc = _generate_filename(recordfilename, f"{dtappendix}_market_change", ".feather")
|
||||
market_change_data.reset_index().to_feather(
|
||||
filename_mc, compression_level=9, compression='lz4')
|
||||
filename_mc, compression_level=9, compression="lz4"
|
||||
)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def _store_backtest_analysis_data(
|
||||
recordfilename: Path, data: Dict[str, Dict],
|
||||
dtappendix: str, name: str) -> Path:
|
||||
recordfilename: Path, data: Dict[str, Dict], dtappendix: str, name: str
|
||||
) -> Path:
|
||||
"""
|
||||
Stores backtest trade candles for analysis
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
|
@ -77,7 +82,7 @@ def _store_backtest_analysis_data(
|
|||
:param dtappendix: Datetime to use for the filename
|
||||
:param name: Name to use for the file, e.g. signals, rejected
|
||||
"""
|
||||
filename = _generate_filename(recordfilename, f"{dtappendix}_{name}", '.pkl')
|
||||
filename = _generate_filename(recordfilename, f"{dtappendix}_{name}", ".pkl")
|
||||
|
||||
file_dump_joblib(filename, data)
|
||||
|
||||
|
@ -85,7 +90,7 @@ def _store_backtest_analysis_data(
|
|||
|
||||
|
||||
def store_backtest_analysis_results(
|
||||
recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict],
|
||||
dtappendix: str) -> None:
|
||||
recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], dtappendix: str
|
||||
) -> None:
|
||||
_store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
|
||||
_store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
|
||||
|
|
|
@ -24,43 +24,45 @@ from freqtrade.util import decimals_per_coin, fmt_coin
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_trade_signal_candles(preprocessed_df: Dict[str, DataFrame],
|
||||
bt_results: Dict[str, Any]) -> DataFrame:
|
||||
def generate_trade_signal_candles(
|
||||
preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any]
|
||||
) -> DataFrame:
|
||||
signal_candles_only = {}
|
||||
for pair in preprocessed_df.keys():
|
||||
signal_candles_only_df = DataFrame()
|
||||
|
||||
pairdf = preprocessed_df[pair]
|
||||
resdf = bt_results['results']
|
||||
resdf = bt_results["results"]
|
||||
pairresults = resdf.loc[(resdf["pair"] == pair)]
|
||||
|
||||
if pairdf.shape[0] > 0:
|
||||
for t, v in pairresults.open_date.items():
|
||||
allinds = pairdf.loc[(pairdf['date'] < v)]
|
||||
allinds = pairdf.loc[(pairdf["date"] < v)]
|
||||
signal_inds = allinds.iloc[[-1]]
|
||||
signal_candles_only_df = concat([
|
||||
signal_candles_only_df.infer_objects(),
|
||||
signal_inds.infer_objects()])
|
||||
signal_candles_only_df = concat(
|
||||
[signal_candles_only_df.infer_objects(), signal_inds.infer_objects()]
|
||||
)
|
||||
|
||||
signal_candles_only[pair] = signal_candles_only_df
|
||||
return signal_candles_only
|
||||
|
||||
|
||||
def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame],
|
||||
rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||
def generate_rejected_signals(
|
||||
preprocessed_df: Dict[str, DataFrame], rejected_dict: Dict[str, DataFrame]
|
||||
) -> Dict[str, DataFrame]:
|
||||
rejected_candles_only = {}
|
||||
for pair, signals in rejected_dict.items():
|
||||
rejected_signals_only_df = DataFrame()
|
||||
pairdf = preprocessed_df[pair]
|
||||
|
||||
for t in signals:
|
||||
data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy()
|
||||
data_df_row['pair'] = pair
|
||||
data_df_row['enter_tag'] = t[1]
|
||||
data_df_row = pairdf.loc[(pairdf["date"] == t[0])].copy()
|
||||
data_df_row["pair"] = pair
|
||||
data_df_row["enter_tag"] = t[1]
|
||||
|
||||
rejected_signals_only_df = concat([
|
||||
rejected_signals_only_df.infer_objects(),
|
||||
data_df_row.infer_objects()])
|
||||
rejected_signals_only_df = concat(
|
||||
[rejected_signals_only_df.infer_objects(), data_df_row.infer_objects()]
|
||||
)
|
||||
|
||||
rejected_candles_only[pair] = rejected_signals_only_df
|
||||
return rejected_candles_only
|
||||
|
@ -70,39 +72,45 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column
|
|||
"""
|
||||
Generate one result dict, with "first_column" as key.
|
||||
"""
|
||||
profit_sum = result['profit_ratio'].sum()
|
||||
profit_sum = result["profit_ratio"].sum()
|
||||
# (end-capital - starting capital) / starting capital
|
||||
profit_total = result['profit_abs'].sum() / starting_balance
|
||||
profit_total = result["profit_abs"].sum() / starting_balance
|
||||
|
||||
return {
|
||||
'key': first_column,
|
||||
'trades': len(result),
|
||||
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
|
||||
'profit_mean_pct': round(result['profit_ratio'].mean() * 100.0, 2
|
||||
) if len(result) > 0 else 0.0,
|
||||
'profit_sum': profit_sum,
|
||||
'profit_sum_pct': round(profit_sum * 100.0, 2),
|
||||
'profit_total_abs': result['profit_abs'].sum(),
|
||||
'profit_total': profit_total,
|
||||
'profit_total_pct': round(profit_total * 100.0, 2),
|
||||
'duration_avg': str(timedelta(
|
||||
minutes=round(result['trade_duration'].mean()))
|
||||
) if not result.empty else '0:00',
|
||||
"key": first_column,
|
||||
"trades": len(result),
|
||||
"profit_mean": result["profit_ratio"].mean() if len(result) > 0 else 0.0,
|
||||
"profit_mean_pct": round(result["profit_ratio"].mean() * 100.0, 2)
|
||||
if len(result) > 0
|
||||
else 0.0,
|
||||
"profit_sum": profit_sum,
|
||||
"profit_sum_pct": round(profit_sum * 100.0, 2),
|
||||
"profit_total_abs": result["profit_abs"].sum(),
|
||||
"profit_total": profit_total,
|
||||
"profit_total_pct": round(profit_total * 100.0, 2),
|
||||
"duration_avg": str(timedelta(minutes=round(result["trade_duration"].mean())))
|
||||
if not result.empty
|
||||
else "0:00",
|
||||
# 'duration_max': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].max()))
|
||||
# ) if not result.empty else '0:00',
|
||||
# 'duration_min': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].min()))
|
||||
# ) if not result.empty else '0:00',
|
||||
'wins': len(result[result['profit_abs'] > 0]),
|
||||
'draws': len(result[result['profit_abs'] == 0]),
|
||||
'losses': len(result[result['profit_abs'] < 0]),
|
||||
'winrate': len(result[result['profit_abs'] > 0]) / len(result) if len(result) else 0.0,
|
||||
"wins": len(result[result["profit_abs"] > 0]),
|
||||
"draws": len(result[result["profit_abs"] == 0]),
|
||||
"losses": len(result[result["profit_abs"] < 0]),
|
||||
"winrate": len(result[result["profit_abs"] > 0]) / len(result) if len(result) else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def generate_pair_metrics(pairlist: List[str], stake_currency: str, starting_balance: int,
|
||||
results: DataFrame, skip_nan: bool = False) -> List[Dict]:
|
||||
def generate_pair_metrics(
|
||||
pairlist: List[str],
|
||||
stake_currency: str,
|
||||
starting_balance: int,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False,
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Generates and returns a list for the given backtest data and the results dataframe
|
||||
:param pairlist: Pairlist used
|
||||
|
@ -116,24 +124,23 @@ def generate_pair_metrics(pairlist: List[str], stake_currency: str, starting_bal
|
|||
tabular_data = []
|
||||
|
||||
for pair in pairlist:
|
||||
result = results[results['pair'] == pair]
|
||||
if skip_nan and result['profit_abs'].isnull().all():
|
||||
result = results[results["pair"] == pair]
|
||||
if skip_nan and result["profit_abs"].isnull().all():
|
||||
continue
|
||||
|
||||
tabular_data.append(_generate_result_line(result, starting_balance, pair))
|
||||
|
||||
# Sort by total profit %:
|
||||
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||
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'))
|
||||
tabular_data.append(_generate_result_line(results, starting_balance, "TOTAL"))
|
||||
return tabular_data
|
||||
|
||||
|
||||
def generate_tag_metrics(tag_type: str,
|
||||
starting_balance: int,
|
||||
results: DataFrame,
|
||||
skip_nan: bool = False) -> List[Dict]:
|
||||
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
|
||||
|
@ -147,16 +154,16 @@ def generate_tag_metrics(tag_type: str,
|
|||
if tag_type in results.columns:
|
||||
for tag, count in results[tag_type].value_counts().items():
|
||||
result = results[results[tag_type] == tag]
|
||||
if skip_nan and result['profit_abs'].isnull().all():
|
||||
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)
|
||||
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'))
|
||||
tabular_data.append(_generate_result_line(results, starting_balance, "TOTAL"))
|
||||
return tabular_data
|
||||
else:
|
||||
return []
|
||||
|
@ -171,51 +178,52 @@ def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]:
|
|||
|
||||
tabular_data = []
|
||||
for strategy, result in bt_stats.items():
|
||||
tabular_data.append(deepcopy(result['results_per_pair'][-1]))
|
||||
tabular_data.append(deepcopy(result["results_per_pair"][-1]))
|
||||
# Update "key" to strategy (results_per_pair has it as "Total").
|
||||
tabular_data[-1]['key'] = strategy
|
||||
tabular_data[-1]['max_drawdown_account'] = result['max_drawdown_account']
|
||||
tabular_data[-1]['max_drawdown_abs'] = fmt_coin(
|
||||
result['max_drawdown_abs'], result['stake_currency'], False)
|
||||
tabular_data[-1]["key"] = strategy
|
||||
tabular_data[-1]["max_drawdown_account"] = result["max_drawdown_account"]
|
||||
tabular_data[-1]["max_drawdown_abs"] = fmt_coin(
|
||||
result["max_drawdown_abs"], result["stake_currency"], False
|
||||
)
|
||||
return tabular_data
|
||||
|
||||
|
||||
def _get_resample_from_period(period: str) -> str:
|
||||
if period == 'day':
|
||||
return '1d'
|
||||
if period == 'week':
|
||||
if period == "day":
|
||||
return "1d"
|
||||
if period == "week":
|
||||
# Weekly defaulting to Monday.
|
||||
return '1W-MON'
|
||||
if period == 'month':
|
||||
return '1ME'
|
||||
return "1W-MON"
|
||||
if period == "month":
|
||||
return "1ME"
|
||||
raise ValueError(f"Period {period} is not supported.")
|
||||
|
||||
|
||||
def generate_periodic_breakdown_stats(
|
||||
trade_list: Union[List, DataFrame], period: str) -> List[Dict[str, Any]]:
|
||||
|
||||
trade_list: Union[List, DataFrame], period: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list)
|
||||
if len(results) == 0:
|
||||
return []
|
||||
results['close_date'] = to_datetime(results['close_date'], utc=True)
|
||||
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')
|
||||
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)
|
||||
trades = (wins + draws + loses)
|
||||
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)
|
||||
trades = wins + draws + loses
|
||||
stats.append(
|
||||
{
|
||||
'date': name.strftime('%d/%m/%Y'),
|
||||
'date_ts': int(name.to_pydatetime().timestamp() * 1000),
|
||||
'profit_abs': profit_abs,
|
||||
'wins': wins,
|
||||
'draws': draws,
|
||||
'loses': loses,
|
||||
'winrate': wins / trades if trades else 0.0,
|
||||
"date": name.strftime("%d/%m/%Y"),
|
||||
"date_ts": int(name.to_pydatetime().timestamp() * 1000),
|
||||
"profit_abs": profit_abs,
|
||||
"wins": wins,
|
||||
"draws": draws,
|
||||
"loses": loses,
|
||||
"winrate": wins / trades if trades else 0.0,
|
||||
}
|
||||
)
|
||||
return stats
|
||||
|
@ -235,74 +243,83 @@ def calc_streak(dataframe: DataFrame) -> Tuple[int, int]:
|
|||
:return: Tuple containing consecutive wins and losses
|
||||
"""
|
||||
|
||||
df = Series(np.where(dataframe['profit_ratio'] > 0, 'win', 'loss')).to_frame('result')
|
||||
df['streaks'] = df['result'].ne(df['result'].shift()).cumsum().rename('streaks')
|
||||
df['counter'] = df['streaks'].groupby(df['streaks']).cumcount() + 1
|
||||
res = df.groupby(df['result']).max()
|
||||
df = Series(np.where(dataframe["profit_ratio"] > 0, "win", "loss")).to_frame("result")
|
||||
df["streaks"] = df["result"].ne(df["result"].shift()).cumsum().rename("streaks")
|
||||
df["counter"] = df["streaks"].groupby(df["streaks"]).cumcount() + 1
|
||||
res = df.groupby(df["result"]).max()
|
||||
#
|
||||
cons_wins = int(res.loc['win', 'counter']) if 'win' in res.index else 0
|
||||
cons_losses = int(res.loc['loss', 'counter']) if 'loss' in res.index else 0
|
||||
cons_wins = int(res.loc["win", "counter"]) if "win" in res.index else 0
|
||||
cons_losses = int(res.loc["loss", "counter"]) if "loss" in res.index else 0
|
||||
return cons_wins, cons_losses
|
||||
|
||||
|
||||
def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
""" Generate overall trade statistics """
|
||||
"""Generate overall trade statistics"""
|
||||
if len(results) == 0:
|
||||
return {
|
||||
'wins': 0,
|
||||
'losses': 0,
|
||||
'draws': 0,
|
||||
'winrate': 0,
|
||||
'holding_avg': timedelta(),
|
||||
'winner_holding_avg': timedelta(),
|
||||
'loser_holding_avg': timedelta(),
|
||||
'max_consecutive_wins': 0,
|
||||
'max_consecutive_losses': 0,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0,
|
||||
"winrate": 0,
|
||||
"holding_avg": timedelta(),
|
||||
"winner_holding_avg": timedelta(),
|
||||
"loser_holding_avg": timedelta(),
|
||||
"max_consecutive_wins": 0,
|
||||
"max_consecutive_losses": 0,
|
||||
}
|
||||
|
||||
winning_trades = results.loc[results['profit_ratio'] > 0]
|
||||
draw_trades = results.loc[results['profit_ratio'] == 0]
|
||||
losing_trades = results.loc[results['profit_ratio'] < 0]
|
||||
winning_trades = results.loc[results["profit_ratio"] > 0]
|
||||
draw_trades = results.loc[results["profit_ratio"] == 0]
|
||||
losing_trades = results.loc[results["profit_ratio"] < 0]
|
||||
|
||||
holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
|
||||
if not results.empty else timedelta())
|
||||
winner_holding_avg = (timedelta(minutes=round(winning_trades['trade_duration'].mean()))
|
||||
if not winning_trades.empty else timedelta())
|
||||
loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean()))
|
||||
if not losing_trades.empty else timedelta())
|
||||
holding_avg = (
|
||||
timedelta(minutes=round(results["trade_duration"].mean()))
|
||||
if not results.empty
|
||||
else timedelta()
|
||||
)
|
||||
winner_holding_avg = (
|
||||
timedelta(minutes=round(winning_trades["trade_duration"].mean()))
|
||||
if not winning_trades.empty
|
||||
else timedelta()
|
||||
)
|
||||
loser_holding_avg = (
|
||||
timedelta(minutes=round(losing_trades["trade_duration"].mean()))
|
||||
if not losing_trades.empty
|
||||
else timedelta()
|
||||
)
|
||||
winstreak, loss_streak = calc_streak(results)
|
||||
|
||||
return {
|
||||
'wins': len(winning_trades),
|
||||
'losses': len(losing_trades),
|
||||
'draws': len(draw_trades),
|
||||
'winrate': len(winning_trades) / len(results) if len(results) else 0.0,
|
||||
'holding_avg': holding_avg,
|
||||
'holding_avg_s': holding_avg.total_seconds(),
|
||||
'winner_holding_avg': winner_holding_avg,
|
||||
'winner_holding_avg_s': winner_holding_avg.total_seconds(),
|
||||
'loser_holding_avg': loser_holding_avg,
|
||||
'loser_holding_avg_s': loser_holding_avg.total_seconds(),
|
||||
'max_consecutive_wins': winstreak,
|
||||
'max_consecutive_losses': loss_streak,
|
||||
"wins": len(winning_trades),
|
||||
"losses": len(losing_trades),
|
||||
"draws": len(draw_trades),
|
||||
"winrate": len(winning_trades) / len(results) if len(results) else 0.0,
|
||||
"holding_avg": holding_avg,
|
||||
"holding_avg_s": holding_avg.total_seconds(),
|
||||
"winner_holding_avg": winner_holding_avg,
|
||||
"winner_holding_avg_s": winner_holding_avg.total_seconds(),
|
||||
"loser_holding_avg": loser_holding_avg,
|
||||
"loser_holding_avg_s": loser_holding_avg.total_seconds(),
|
||||
"max_consecutive_wins": winstreak,
|
||||
"max_consecutive_losses": loss_streak,
|
||||
}
|
||||
|
||||
|
||||
def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
""" Generate daily statistics """
|
||||
"""Generate daily statistics"""
|
||||
if len(results) == 0:
|
||||
return {
|
||||
'backtest_best_day': 0,
|
||||
'backtest_worst_day': 0,
|
||||
'backtest_best_day_abs': 0,
|
||||
'backtest_worst_day_abs': 0,
|
||||
'winning_days': 0,
|
||||
'draw_days': 0,
|
||||
'losing_days': 0,
|
||||
'daily_profit_list': [],
|
||||
"backtest_best_day": 0,
|
||||
"backtest_worst_day": 0,
|
||||
"backtest_best_day_abs": 0,
|
||||
"backtest_worst_day_abs": 0,
|
||||
"winning_days": 0,
|
||||
"draw_days": 0,
|
||||
"losing_days": 0,
|
||||
"daily_profit_list": [],
|
||||
}
|
||||
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
|
||||
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
|
||||
daily_profit_rel = results.resample("1d", on="close_date")["profit_ratio"].sum()
|
||||
daily_profit = results.resample("1d", on="close_date")["profit_abs"].sum().round(10)
|
||||
worst_rel = min(daily_profit_rel)
|
||||
best_rel = max(daily_profit_rel)
|
||||
worst = min(daily_profit)
|
||||
|
@ -313,24 +330,26 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
|||
daily_profit_list = [(str(idx.date()), val) for idx, val in daily_profit.items()]
|
||||
|
||||
return {
|
||||
'backtest_best_day': best_rel,
|
||||
'backtest_worst_day': worst_rel,
|
||||
'backtest_best_day_abs': best,
|
||||
'backtest_worst_day_abs': worst,
|
||||
'winning_days': winning_days,
|
||||
'draw_days': draw_days,
|
||||
'losing_days': losing_days,
|
||||
'daily_profit': daily_profit_list,
|
||||
"backtest_best_day": best_rel,
|
||||
"backtest_worst_day": worst_rel,
|
||||
"backtest_best_day_abs": best,
|
||||
"backtest_worst_day_abs": worst,
|
||||
"winning_days": winning_days,
|
||||
"draw_days": draw_days,
|
||||
"losing_days": losing_days,
|
||||
"daily_profit": daily_profit_list,
|
||||
}
|
||||
|
||||
|
||||
def generate_strategy_stats(pairlist: List[str],
|
||||
strategy: str,
|
||||
content: Dict[str, Any],
|
||||
min_date: datetime, max_date: datetime,
|
||||
market_change: float,
|
||||
is_hyperopt: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
def generate_strategy_stats(
|
||||
pairlist: List[str],
|
||||
strategy: str,
|
||||
content: Dict[str, Any],
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
market_change: float,
|
||||
is_hyperopt: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
:param pairlist: List of pairs to backtest
|
||||
:param strategy: Strategy name
|
||||
|
@ -341,175 +360,197 @@ def generate_strategy_stats(pairlist: List[str],
|
|||
:param market_change: float indicating the market change
|
||||
:return: Dictionary containing results per strategy and a strategy summary.
|
||||
"""
|
||||
results: Dict[str, DataFrame] = content['results']
|
||||
results: Dict[str, DataFrame] = content["results"]
|
||||
if not isinstance(results, DataFrame):
|
||||
return {}
|
||||
config = content['config']
|
||||
max_open_trades = min(config['max_open_trades'], len(pairlist))
|
||||
start_balance = config['dry_run_wallet']
|
||||
stake_currency = config['stake_currency']
|
||||
config = content["config"]
|
||||
max_open_trades = min(config["max_open_trades"], len(pairlist))
|
||||
start_balance = config["dry_run_wallet"]
|
||||
stake_currency = config["stake_currency"]
|
||||
|
||||
pair_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
|
||||
starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
pair_results = generate_pair_metrics(
|
||||
pairlist,
|
||||
stake_currency=stake_currency,
|
||||
starting_balance=start_balance,
|
||||
results=results,
|
||||
skip_nan=False,
|
||||
)
|
||||
|
||||
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
enter_tag_results = generate_tag_metrics(
|
||||
"enter_tag", starting_balance=start_balance, results=results, skip_nan=False
|
||||
)
|
||||
exit_reason_stats = generate_tag_metrics(
|
||||
"exit_reason", starting_balance=start_balance, results=results, skip_nan=False
|
||||
)
|
||||
left_open_results = generate_pair_metrics(
|
||||
pairlist, stake_currency=stake_currency, starting_balance=start_balance,
|
||||
results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True)
|
||||
pairlist,
|
||||
stake_currency=stake_currency,
|
||||
starting_balance=start_balance,
|
||||
results=results.loc[results["exit_reason"] == "force_exit"],
|
||||
skip_nan=True,
|
||||
)
|
||||
|
||||
daily_stats = generate_daily_stats(results)
|
||||
trade_stats = generate_trading_stats(results)
|
||||
|
||||
periodic_breakdown = {}
|
||||
if not is_hyperopt:
|
||||
periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)}
|
||||
periodic_breakdown = {"periodic_breakdown": generate_all_periodic_breakdown_stats(results)}
|
||||
|
||||
best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
|
||||
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
|
||||
best_pair = (
|
||||
max(
|
||||
[pair for pair in pair_results if pair["key"] != "TOTAL"], key=lambda x: x["profit_sum"]
|
||||
)
|
||||
if len(pair_results) > 1
|
||||
else None
|
||||
)
|
||||
worst_pair = (
|
||||
min(
|
||||
[pair for pair in pair_results if pair["key"] != "TOTAL"], key=lambda x: x["profit_sum"]
|
||||
)
|
||||
if len(pair_results) > 1
|
||||
else None
|
||||
)
|
||||
winning_profit = results.loc[results["profit_abs"] > 0, "profit_abs"].sum()
|
||||
losing_profit = results.loc[results["profit_abs"] < 0, "profit_abs"].sum()
|
||||
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
|
||||
|
||||
expectancy, expectancy_ratio = calculate_expectancy(results)
|
||||
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_enter_tag': enter_tag_results,
|
||||
'exit_reason_summary': exit_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
|
||||
'total_trades': len(results),
|
||||
'trade_count_long': len(results.loc[~results['is_short']]),
|
||||
'trade_count_short': len(results.loc[results['is_short']]),
|
||||
'total_volume': float(results['stake_amount'].sum()),
|
||||
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
|
||||
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
|
||||
'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
|
||||
'profit_total': results['profit_abs'].sum() / start_balance,
|
||||
'profit_total_long': results.loc[~results['is_short'], 'profit_abs'].sum() / start_balance,
|
||||
'profit_total_short': results.loc[results['is_short'], 'profit_abs'].sum() / start_balance,
|
||||
'profit_total_abs': results['profit_abs'].sum(),
|
||||
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
|
||||
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
|
||||
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
|
||||
'expectancy': expectancy,
|
||||
'expectancy_ratio': expectancy_ratio,
|
||||
'sortino': calculate_sortino(results, min_date, max_date, start_balance),
|
||||
'sharpe': calculate_sharpe(results, min_date, max_date, start_balance),
|
||||
'calmar': calculate_calmar(results, min_date, max_date, start_balance),
|
||||
'profit_factor': profit_factor,
|
||||
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
||||
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
'backtest_end_ts': int(max_date.timestamp() * 1000),
|
||||
'backtest_days': backtest_days,
|
||||
|
||||
'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),
|
||||
'market_change': market_change,
|
||||
'pairlist': pairlist,
|
||||
'stake_amount': config['stake_amount'],
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
'starting_balance': start_balance,
|
||||
'dry_run_wallet': start_balance,
|
||||
'final_balance': content['final_balance'],
|
||||
'rejected_signals': content['rejected_signals'],
|
||||
'timedout_entry_orders': content['timedout_entry_orders'],
|
||||
'timedout_exit_orders': content['timedout_exit_orders'],
|
||||
'canceled_trade_entries': content['canceled_trade_entries'],
|
||||
'canceled_entry_orders': content['canceled_entry_orders'],
|
||||
'replaced_entry_orders': content['replaced_entry_orders'],
|
||||
'max_open_trades': max_open_trades,
|
||||
'max_open_trades_setting': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'timeframe': config['timeframe'],
|
||||
'timeframe_detail': config.get('timeframe_detail', ''),
|
||||
'timerange': config.get('timerange', ''),
|
||||
'enable_protections': config.get('enable_protections', False),
|
||||
'strategy_name': strategy,
|
||||
"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_enter_tag": enter_tag_results,
|
||||
"exit_reason_summary": exit_reason_stats,
|
||||
"left_open_trades": left_open_results,
|
||||
"total_trades": len(results),
|
||||
"trade_count_long": len(results.loc[~results["is_short"]]),
|
||||
"trade_count_short": len(results.loc[results["is_short"]]),
|
||||
"total_volume": float(results["stake_amount"].sum()),
|
||||
"avg_stake_amount": results["stake_amount"].mean() if len(results) > 0 else 0,
|
||||
"profit_mean": results["profit_ratio"].mean() if len(results) > 0 else 0,
|
||||
"profit_median": results["profit_ratio"].median() if len(results) > 0 else 0,
|
||||
"profit_total": results["profit_abs"].sum() / start_balance,
|
||||
"profit_total_long": results.loc[~results["is_short"], "profit_abs"].sum() / start_balance,
|
||||
"profit_total_short": results.loc[results["is_short"], "profit_abs"].sum() / start_balance,
|
||||
"profit_total_abs": results["profit_abs"].sum(),
|
||||
"profit_total_long_abs": results.loc[~results["is_short"], "profit_abs"].sum(),
|
||||
"profit_total_short_abs": results.loc[results["is_short"], "profit_abs"].sum(),
|
||||
"cagr": calculate_cagr(backtest_days, start_balance, content["final_balance"]),
|
||||
"expectancy": expectancy,
|
||||
"expectancy_ratio": expectancy_ratio,
|
||||
"sortino": calculate_sortino(results, min_date, max_date, start_balance),
|
||||
"sharpe": calculate_sharpe(results, min_date, max_date, start_balance),
|
||||
"calmar": calculate_calmar(results, min_date, max_date, start_balance),
|
||||
"profit_factor": profit_factor,
|
||||
"backtest_start": min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
"backtest_start_ts": int(min_date.timestamp() * 1000),
|
||||
"backtest_end": max_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
"backtest_end_ts": int(max_date.timestamp() * 1000),
|
||||
"backtest_days": backtest_days,
|
||||
"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),
|
||||
"market_change": market_change,
|
||||
"pairlist": pairlist,
|
||||
"stake_amount": config["stake_amount"],
|
||||
"stake_currency": config["stake_currency"],
|
||||
"stake_currency_decimals": decimals_per_coin(config["stake_currency"]),
|
||||
"starting_balance": start_balance,
|
||||
"dry_run_wallet": start_balance,
|
||||
"final_balance": content["final_balance"],
|
||||
"rejected_signals": content["rejected_signals"],
|
||||
"timedout_entry_orders": content["timedout_entry_orders"],
|
||||
"timedout_exit_orders": content["timedout_exit_orders"],
|
||||
"canceled_trade_entries": content["canceled_trade_entries"],
|
||||
"canceled_entry_orders": content["canceled_entry_orders"],
|
||||
"replaced_entry_orders": content["replaced_entry_orders"],
|
||||
"max_open_trades": max_open_trades,
|
||||
"max_open_trades_setting": (
|
||||
config["max_open_trades"] if config["max_open_trades"] != float("inf") else -1
|
||||
),
|
||||
"timeframe": config["timeframe"],
|
||||
"timeframe_detail": config.get("timeframe_detail", ""),
|
||||
"timerange": config.get("timerange", ""),
|
||||
"enable_protections": config.get("enable_protections", False),
|
||||
"strategy_name": strategy,
|
||||
# Parameters relevant for backtesting
|
||||
'stoploss': config['stoploss'],
|
||||
'trailing_stop': config.get('trailing_stop', False),
|
||||
'trailing_stop_positive': config.get('trailing_stop_positive'),
|
||||
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
|
||||
'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_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'],
|
||||
"stoploss": config["stoploss"],
|
||||
"trailing_stop": config.get("trailing_stop", False),
|
||||
"trailing_stop_positive": config.get("trailing_stop_positive"),
|
||||
"trailing_stop_positive_offset": config.get("trailing_stop_positive_offset", 0.0),
|
||||
"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_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"],
|
||||
**periodic_breakdown,
|
||||
**daily_stats,
|
||||
**trade_stats
|
||||
**trade_stats,
|
||||
}
|
||||
|
||||
try:
|
||||
max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown(
|
||||
results, value_col='profit_ratio')
|
||||
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
|
||||
max_drawdown) = calculate_max_drawdown(
|
||||
results, value_col='profit_abs', starting_balance=start_balance)
|
||||
results, value_col="profit_ratio"
|
||||
)
|
||||
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, max_drawdown) = (
|
||||
calculate_max_drawdown(results, value_col="profit_abs", starting_balance=start_balance)
|
||||
)
|
||||
# max_relative_drawdown = Underwater
|
||||
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
|
||||
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
|
||||
results, value_col="profit_abs", starting_balance=start_balance, relative=True
|
||||
)
|
||||
|
||||
strat_stats.update({
|
||||
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
|
||||
'max_drawdown_account': max_drawdown,
|
||||
'max_relative_drawdown': max_relative_drawdown,
|
||||
'max_drawdown_abs': drawdown_abs,
|
||||
'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
|
||||
'drawdown_start_ts': drawdown_start.timestamp() * 1000,
|
||||
'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT),
|
||||
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
|
||||
|
||||
'max_drawdown_low': low_val,
|
||||
'max_drawdown_high': high_val,
|
||||
})
|
||||
strat_stats.update(
|
||||
{
|
||||
"max_drawdown": max_drawdown_legacy, # Deprecated - do not use
|
||||
"max_drawdown_account": max_drawdown,
|
||||
"max_relative_drawdown": max_relative_drawdown,
|
||||
"max_drawdown_abs": drawdown_abs,
|
||||
"drawdown_start": drawdown_start.strftime(DATETIME_PRINT_FORMAT),
|
||||
"drawdown_start_ts": drawdown_start.timestamp() * 1000,
|
||||
"drawdown_end": drawdown_end.strftime(DATETIME_PRINT_FORMAT),
|
||||
"drawdown_end_ts": drawdown_end.timestamp() * 1000,
|
||||
"max_drawdown_low": low_val,
|
||||
"max_drawdown_high": high_val,
|
||||
}
|
||||
)
|
||||
|
||||
csum_min, csum_max = calculate_csum(results, start_balance)
|
||||
strat_stats.update({
|
||||
'csum_min': csum_min,
|
||||
'csum_max': csum_max
|
||||
})
|
||||
strat_stats.update({"csum_min": csum_min, "csum_max": csum_max})
|
||||
|
||||
except ValueError:
|
||||
strat_stats.update({
|
||||
'max_drawdown': 0.0,
|
||||
'max_drawdown_account': 0.0,
|
||||
'max_relative_drawdown': 0.0,
|
||||
'max_drawdown_abs': 0.0,
|
||||
'max_drawdown_low': 0.0,
|
||||
'max_drawdown_high': 0.0,
|
||||
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||
'drawdown_start_ts': 0,
|
||||
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||
'drawdown_end_ts': 0,
|
||||
'csum_min': 0,
|
||||
'csum_max': 0
|
||||
})
|
||||
strat_stats.update(
|
||||
{
|
||||
"max_drawdown": 0.0,
|
||||
"max_drawdown_account": 0.0,
|
||||
"max_relative_drawdown": 0.0,
|
||||
"max_drawdown_abs": 0.0,
|
||||
"max_drawdown_low": 0.0,
|
||||
"max_drawdown_high": 0.0,
|
||||
"drawdown_start": datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||
"drawdown_start_ts": 0,
|
||||
"drawdown_end": datetime(1970, 1, 1, tzinfo=timezone.utc),
|
||||
"drawdown_end_ts": 0,
|
||||
"csum_min": 0,
|
||||
"csum_max": 0,
|
||||
}
|
||||
)
|
||||
|
||||
return strat_stats
|
||||
|
||||
|
||||
def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
||||
all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]],
|
||||
min_date: datetime, max_date: datetime
|
||||
) -> BacktestResultType:
|
||||
def generate_backtest_stats(
|
||||
btdata: Dict[str, DataFrame],
|
||||
all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]],
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
) -> BacktestResultType:
|
||||
"""
|
||||
:param btdata: Backtest data
|
||||
:param all_results: backtest result - dictionary in the form:
|
||||
|
@ -519,29 +560,30 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||
:return: Dictionary containing results per strategy and a strategy summary.
|
||||
"""
|
||||
result: BacktestResultType = {
|
||||
'metadata': {},
|
||||
'strategy': {},
|
||||
'strategy_comparison': [],
|
||||
"metadata": {},
|
||||
"strategy": {},
|
||||
"strategy_comparison": [],
|
||||
}
|
||||
market_change = calculate_market_change(btdata, 'close')
|
||||
market_change = calculate_market_change(btdata, "close")
|
||||
metadata = {}
|
||||
pairlist = list(btdata.keys())
|
||||
for strategy, content in all_results.items():
|
||||
strat_stats = generate_strategy_stats(pairlist, strategy, content,
|
||||
min_date, max_date, market_change=market_change)
|
||||
strat_stats = generate_strategy_stats(
|
||||
pairlist, strategy, content, min_date, max_date, market_change=market_change
|
||||
)
|
||||
metadata[strategy] = {
|
||||
'run_id': content['run_id'],
|
||||
'backtest_start_time': content['backtest_start_time'],
|
||||
'timeframe': content['config']['timeframe'],
|
||||
'timeframe_detail': content['config'].get('timeframe_detail', None),
|
||||
'backtest_start_ts': int(min_date.timestamp()),
|
||||
'backtest_end_ts': int(max_date.timestamp()),
|
||||
"run_id": content["run_id"],
|
||||
"backtest_start_time": content["backtest_start_time"],
|
||||
"timeframe": content["config"]["timeframe"],
|
||||
"timeframe_detail": content["config"].get("timeframe_detail", None),
|
||||
"backtest_start_ts": int(min_date.timestamp()),
|
||||
"backtest_end_ts": int(max_date.timestamp()),
|
||||
}
|
||||
result['strategy'][strategy] = strat_stats
|
||||
result["strategy"][strategy] = strat_stats
|
||||
|
||||
strategy_results = generate_strategy_comparison(bt_stats=result['strategy'])
|
||||
strategy_results = generate_strategy_comparison(bt_stats=result["strategy"])
|
||||
|
||||
result['metadata'] = metadata
|
||||
result['strategy_comparison'] = strategy_results
|
||||
result["metadata"] = metadata
|
||||
result["strategy_comparison"] = strategy_results
|
||||
|
||||
return result
|
||||
|
|
|
@ -3,9 +3,17 @@ from skopt.space import Integer
|
|||
|
||||
|
||||
class SKDecimal(Integer):
|
||||
|
||||
def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None,
|
||||
name=None, dtype=np.int64):
|
||||
def __init__(
|
||||
self,
|
||||
low,
|
||||
high,
|
||||
decimals=3,
|
||||
prior="uniform",
|
||||
base=10,
|
||||
transform=None,
|
||||
name=None,
|
||||
dtype=np.int64,
|
||||
):
|
||||
self.decimals = decimals
|
||||
|
||||
self.pow_dot_one = pow(0.1, self.decimals)
|
||||
|
|
Loading…
Reference in New Issue
Block a user