2020-06-28 07:27:19 +00:00
|
|
|
import re
|
2022-04-19 12:08:01 +00:00
|
|
|
from datetime import timedelta
|
2020-03-15 14:36:53 +00:00
|
|
|
from pathlib import Path
|
2022-09-23 05:09:34 +00:00
|
|
|
from shutil import copyfile
|
2020-03-15 14:36:53 +00:00
|
|
|
|
2022-04-20 12:38:52 +00:00
|
|
|
import joblib
|
2020-01-02 06:33:55 +00:00
|
|
|
import pandas as pd
|
2020-05-25 05:14:21 +00:00
|
|
|
import pytest
|
2020-09-28 17:43:15 +00:00
|
|
|
|
2020-06-26 18:08:45 +00:00
|
|
|
from freqtrade.configuration import TimeRange
|
2023-04-24 07:41:36 +00:00
|
|
|
from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
2020-06-26 18:08:45 +00:00
|
|
|
from freqtrade.data import history
|
2021-10-30 15:05:12 +00:00
|
|
|
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
|
|
|
load_backtest_stats)
|
2020-06-28 07:27:19 +00:00
|
|
|
from freqtrade.edge import PairInfo
|
2022-03-25 05:55:37 +00:00
|
|
|
from freqtrade.enums import ExitType
|
2023-06-25 14:59:53 +00:00
|
|
|
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats,
|
|
|
|
generate_edge_table, generate_exit_reason_stats,
|
|
|
|
generate_pair_metrics,
|
2021-10-21 05:42:19 +00:00
|
|
|
generate_periodic_breakdown_stats,
|
2021-04-29 18:17:13 +00:00
|
|
|
generate_strategy_comparison,
|
2021-10-30 15:05:12 +00:00
|
|
|
generate_trading_stats, show_sorted_pairlist,
|
2023-03-19 13:56:41 +00:00
|
|
|
store_backtest_analysis_results,
|
2021-10-30 15:05:12 +00:00
|
|
|
store_backtest_stats, text_table_bt_results,
|
2022-03-22 05:43:37 +00:00
|
|
|
text_table_exit_reason, text_table_strategy)
|
2023-07-24 04:36:16 +00:00
|
|
|
from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period,
|
2023-07-24 05:09:11 +00:00
|
|
|
calc_streak)
|
2020-09-25 04:37:40 +00:00
|
|
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
2023-05-14 09:19:36 +00:00
|
|
|
from freqtrade.util import dt_ts
|
2023-05-14 15:46:56 +00:00
|
|
|
from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc
|
2021-09-21 18:18:14 +00:00
|
|
|
from tests.conftest import CURRENT_TEST_STRATEGY
|
2022-09-23 05:09:34 +00:00
|
|
|
from tests.data.test_history import _clean_test_file
|
|
|
|
|
|
|
|
|
|
|
|
def _backup_file(file: Path, copy_file: bool = False) -> None:
|
|
|
|
"""
|
|
|
|
Backup existing file to avoid deleting the user file
|
|
|
|
:param file: complete path to the file
|
|
|
|
:param copy_file: keep file in place too.
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
file_swp = str(file) + '.swp'
|
|
|
|
if file.is_file():
|
|
|
|
file.rename(file_swp)
|
|
|
|
|
|
|
|
if copy_file:
|
|
|
|
copyfile(file_swp, file)
|
2020-01-02 06:33:55 +00:00
|
|
|
|
|
|
|
|
2020-09-26 12:55:12 +00:00
|
|
|
def test_text_table_bt_results():
|
2020-01-02 06:33:55 +00:00
|
|
|
|
|
|
|
results = pd.DataFrame(
|
|
|
|
{
|
2021-05-23 07:15:36 +00:00
|
|
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
|
|
|
'profit_ratio': [0.1, 0.2, -0.05],
|
|
|
|
'profit_abs': [0.2, 0.4, -0.1],
|
|
|
|
'trade_duration': [10, 30, 20],
|
2020-01-02 06:33:55 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
result_str = (
|
2022-09-09 17:58:55 +00:00
|
|
|
'| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | '
|
|
|
|
'Tot Profit % | Avg Duration | Win Draw Loss Win% |\n'
|
|
|
|
'|---------+-----------+----------------+----------------+------------------+'
|
|
|
|
'----------------+----------------+-------------------------|\n'
|
|
|
|
'| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | '
|
|
|
|
'12.50 | 0:20:00 | 2 0 1 66.7 |\n'
|
|
|
|
'| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | '
|
|
|
|
'12.50 | 0:20:00 | 2 0 1 66.7 |'
|
2020-01-02 06:33:55 +00:00
|
|
|
)
|
2020-05-25 17:34:46 +00:00
|
|
|
|
2022-01-07 07:44:11 +00:00
|
|
|
pair_results = generate_pair_metrics(['ETH/BTC'], stake_currency='BTC',
|
2021-02-13 08:01:05 +00:00
|
|
|
starting_balance=4, results=results)
|
2020-06-07 09:35:02 +00:00
|
|
|
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
|
2020-01-02 06:33:55 +00:00
|
|
|
|
|
|
|
|
2023-11-05 15:25:23 +00:00
|
|
|
def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
|
2021-09-21 18:18:14 +00:00
|
|
|
default_conf.update({'strategy': CURRENT_TEST_STRATEGY})
|
2020-09-25 04:37:40 +00:00
|
|
|
StrategyResolver.load_strategy(default_conf)
|
|
|
|
|
2020-09-25 18:39:00 +00:00
|
|
|
results = {'DefStrat': {
|
|
|
|
'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
|
|
|
|
"UNITTEST/BTC", "UNITTEST/BTC"],
|
2021-01-23 12:02:48 +00:00
|
|
|
"profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780],
|
2020-09-25 18:39:00 +00:00
|
|
|
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
|
2023-05-14 15:46:56 +00:00
|
|
|
"open_date": [dt_utc(2017, 11, 14, 19, 32, 00),
|
|
|
|
dt_utc(2017, 11, 14, 21, 36, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 12, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 44, 00)],
|
|
|
|
"close_date": [dt_utc(2017, 11, 14, 21, 35, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 10, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 43, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 58, 00)],
|
2020-09-25 18:39:00 +00:00
|
|
|
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
|
|
|
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
|
|
|
|
"trade_duration": [123, 34, 31, 14],
|
2021-01-23 11:50:20 +00:00
|
|
|
"is_open": [False, False, False, True],
|
2021-11-18 19:34:59 +00:00
|
|
|
"is_short": [False, False, False, False],
|
2021-02-14 12:08:49 +00:00
|
|
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
2022-03-24 19:33:47 +00:00
|
|
|
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
|
2022-04-04 14:59:27 +00:00
|
|
|
ExitType.ROI, ExitType.FORCE_EXIT]
|
2020-09-25 18:39:00 +00:00
|
|
|
}),
|
2020-12-02 06:42:39 +00:00
|
|
|
'config': default_conf,
|
2021-01-13 06:47:03 +00:00
|
|
|
'locks': [],
|
2021-02-13 08:01:05 +00:00
|
|
|
'final_balance': 1000.02,
|
2021-05-23 07:46:51 +00:00
|
|
|
'rejected_signals': 20,
|
2022-02-07 18:33:22 +00:00
|
|
|
'timedout_entry_orders': 0,
|
|
|
|
'timedout_exit_orders': 0,
|
2022-05-16 22:42:48 +00:00
|
|
|
'canceled_trade_entries': 0,
|
2022-05-17 11:08:35 +00:00
|
|
|
'canceled_entry_orders': 0,
|
|
|
|
'replaced_entry_orders': 0,
|
2023-05-14 09:19:36 +00:00
|
|
|
'backtest_start_time': dt_ts() // 1000,
|
|
|
|
'backtest_end_time': dt_ts() // 1000,
|
2022-01-06 09:53:11 +00:00
|
|
|
'run_id': '123',
|
2021-01-13 06:47:03 +00:00
|
|
|
}
|
2020-09-25 18:39:00 +00:00
|
|
|
}
|
2020-06-26 18:08:45 +00:00
|
|
|
timerange = TimeRange.parse_timerange('1510688220-1510700340')
|
2023-05-14 15:46:56 +00:00
|
|
|
min_date = dt_from_ts(1510688220)
|
|
|
|
max_date = dt_from_ts(1510700340)
|
2020-06-26 18:08:45 +00:00
|
|
|
btdata = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
|
|
|
|
fill_up_missing=True)
|
|
|
|
|
2020-09-25 18:39:00 +00:00
|
|
|
stats = generate_backtest_stats(btdata, results, min_date, max_date)
|
2020-06-26 18:08:45 +00:00
|
|
|
assert isinstance(stats, dict)
|
|
|
|
assert 'strategy' in stats
|
|
|
|
assert 'DefStrat' in stats['strategy']
|
|
|
|
assert 'strategy_comparison' in stats
|
|
|
|
strat_stats = stats['strategy']['DefStrat']
|
2021-05-11 18:30:37 +00:00
|
|
|
assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT)
|
|
|
|
assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT)
|
2020-09-25 18:39:00 +00:00
|
|
|
assert strat_stats['total_trades'] == len(results['DefStrat']['results'])
|
2020-06-26 18:08:45 +00:00
|
|
|
# Above sample had no loosing trade
|
2022-01-04 15:16:08 +00:00
|
|
|
assert strat_stats['max_drawdown_account'] == 0.0
|
2020-06-26 18:08:45 +00:00
|
|
|
|
2021-02-27 09:14:25 +00:00
|
|
|
# Retry with losing trade
|
2020-09-25 18:39:00 +00:00
|
|
|
results = {'DefStrat': {
|
|
|
|
'results': pd.DataFrame(
|
|
|
|
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
|
2021-01-23 12:02:48 +00:00
|
|
|
"profit_ratio": [0.003312, 0.010801, -0.013803, 0.002780],
|
2020-09-25 18:39:00 +00:00
|
|
|
"profit_abs": [0.000003, 0.000011, -0.000014, 0.000003],
|
2023-05-14 15:46:56 +00:00
|
|
|
"open_date": [dt_utc(2017, 11, 14, 19, 32, 00),
|
|
|
|
dt_utc(2017, 11, 14, 21, 36, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 12, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 44, 00)],
|
|
|
|
"close_date": [dt_utc(2017, 11, 14, 21, 35, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 10, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 43, 00),
|
|
|
|
dt_utc(2017, 11, 14, 22, 58, 00)],
|
2020-09-25 18:39:00 +00:00
|
|
|
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
|
|
|
"close_rate": [0.002546, 0.003014, 0.0032903, 0.003217],
|
|
|
|
"trade_duration": [123, 34, 31, 14],
|
2021-02-27 09:14:25 +00:00
|
|
|
"is_open": [False, False, False, True],
|
2021-11-18 19:34:59 +00:00
|
|
|
"is_short": [False, False, False, False],
|
2021-02-27 09:14:25 +00:00
|
|
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
2022-03-24 19:33:47 +00:00
|
|
|
"exit_reason": [ExitType.ROI, ExitType.ROI,
|
2022-04-04 14:59:27 +00:00
|
|
|
ExitType.STOP_LOSS, ExitType.FORCE_EXIT]
|
2020-09-25 18:39:00 +00:00
|
|
|
}),
|
2021-02-27 09:14:25 +00:00
|
|
|
'config': default_conf,
|
|
|
|
'locks': [],
|
|
|
|
'final_balance': 1000.02,
|
2021-05-23 07:46:51 +00:00
|
|
|
'rejected_signals': 20,
|
2022-02-07 18:33:22 +00:00
|
|
|
'timedout_entry_orders': 0,
|
|
|
|
'timedout_exit_orders': 0,
|
2022-05-16 22:42:48 +00:00
|
|
|
'canceled_trade_entries': 0,
|
2022-05-17 11:08:35 +00:00
|
|
|
'canceled_entry_orders': 0,
|
|
|
|
'replaced_entry_orders': 0,
|
2023-05-14 09:19:36 +00:00
|
|
|
'backtest_start_time': dt_ts() // 1000,
|
|
|
|
'backtest_end_time': dt_ts() // 1000,
|
2022-01-06 09:53:11 +00:00
|
|
|
'run_id': '124',
|
2021-02-27 09:14:25 +00:00
|
|
|
}
|
2020-09-25 18:39:00 +00:00
|
|
|
}
|
2020-06-26 18:08:45 +00:00
|
|
|
|
2021-02-27 09:14:25 +00:00
|
|
|
stats = generate_backtest_stats(btdata, results, min_date, max_date)
|
|
|
|
assert isinstance(stats, dict)
|
|
|
|
assert 'strategy' in stats
|
|
|
|
assert 'DefStrat' in stats['strategy']
|
|
|
|
assert 'strategy_comparison' in stats
|
|
|
|
strat_stats = stats['strategy']['DefStrat']
|
|
|
|
|
2022-01-04 15:16:08 +00:00
|
|
|
assert pytest.approx(strat_stats['max_drawdown_account']) == 1.399999e-08
|
2021-05-11 18:30:37 +00:00
|
|
|
assert strat_stats['drawdown_start'] == '2017-11-14 22:10:00'
|
|
|
|
assert strat_stats['drawdown_end'] == '2017-11-14 22:43:00'
|
2021-02-27 09:14:25 +00:00
|
|
|
assert strat_stats['drawdown_end_ts'] == 1510699380000
|
|
|
|
assert strat_stats['drawdown_start_ts'] == 1510697400000
|
2020-06-28 07:04:19 +00:00
|
|
|
assert strat_stats['pairlist'] == ['UNITTEST/BTC']
|
2020-06-26 18:08:45 +00:00
|
|
|
|
2020-06-27 17:48:45 +00:00
|
|
|
# Test storing stats
|
2023-11-05 15:25:23 +00:00
|
|
|
filename = tmp_path / 'btresult.json'
|
|
|
|
filename_last = tmp_path / LAST_BT_RESULT_FN
|
2020-06-27 17:48:45 +00:00
|
|
|
_backup_file(filename_last, copy_file=True)
|
|
|
|
assert not filename.is_file()
|
|
|
|
|
2022-06-15 04:53:52 +00:00
|
|
|
store_backtest_stats(filename, stats, '2022_01_01_15_05_13')
|
2020-06-27 17:48:45 +00:00
|
|
|
|
|
|
|
# get real Filename (it's btresult-<date>.json)
|
|
|
|
last_fn = get_latest_backtest_filename(filename_last.parent)
|
|
|
|
assert re.match(r"btresult-.*\.json", last_fn)
|
|
|
|
|
2023-11-05 15:25:23 +00:00
|
|
|
filename1 = tmp_path / last_fn
|
2020-06-27 17:48:45 +00:00
|
|
|
assert filename1.is_file()
|
|
|
|
content = filename1.read_text()
|
2022-01-04 15:16:08 +00:00
|
|
|
assert 'max_drawdown_account' in content
|
2020-06-27 17:48:45 +00:00
|
|
|
assert 'strategy' in content
|
2020-06-28 07:04:19 +00:00
|
|
|
assert 'pairlist' in content
|
2020-06-27 17:48:45 +00:00
|
|
|
|
|
|
|
assert filename_last.is_file()
|
|
|
|
|
|
|
|
_clean_test_file(filename_last)
|
|
|
|
filename1.unlink()
|
|
|
|
|
2020-06-26 18:08:45 +00:00
|
|
|
|
2020-08-18 13:20:37 +00:00
|
|
|
def test_store_backtest_stats(testdatadir, mocker):
|
|
|
|
|
2023-06-25 15:42:58 +00:00
|
|
|
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.bt_storage.file_dump_json')
|
2020-08-18 13:20:37 +00:00
|
|
|
|
2023-07-30 17:49:20 +00:00
|
|
|
data = {'metadata': {}, 'strategy': {}, 'strategy_comparison': []}
|
|
|
|
store_backtest_stats(testdatadir, data, '2022_01_01_15_05_13')
|
2020-08-18 13:20:37 +00:00
|
|
|
|
2022-01-06 09:53:11 +00:00
|
|
|
assert dump_mock.call_count == 3
|
2020-08-18 13:20:37 +00:00
|
|
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
2022-04-11 16:02:02 +00:00
|
|
|
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'backtest-result'))
|
2020-08-18 13:20:37 +00:00
|
|
|
|
|
|
|
dump_mock.reset_mock()
|
|
|
|
filename = testdatadir / 'testresult.json'
|
2023-07-30 17:49:20 +00:00
|
|
|
store_backtest_stats(filename, data, '2022_01_01_15_05_13')
|
2022-01-06 09:53:11 +00:00
|
|
|
assert dump_mock.call_count == 3
|
2020-08-18 13:20:37 +00:00
|
|
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
|
|
|
# result will be testdatadir / testresult-<timestamp>.json
|
|
|
|
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult'))
|
|
|
|
|
|
|
|
|
2022-04-19 11:48:21 +00:00
|
|
|
def test_store_backtest_candles(testdatadir, mocker):
|
|
|
|
|
2023-06-25 14:59:53 +00:00
|
|
|
dump_mock = mocker.patch(
|
2023-06-25 15:42:58 +00:00
|
|
|
'freqtrade.optimize.optimize_reports.bt_storage.file_dump_joblib')
|
2022-04-19 11:48:21 +00:00
|
|
|
|
2022-04-20 12:38:52 +00:00
|
|
|
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
|
|
|
|
|
|
|
# mock directory exporting
|
2023-03-19 13:56:41 +00:00
|
|
|
store_backtest_analysis_results(testdatadir, candle_dict, {}, '2022_01_01_15_05_13')
|
2022-04-19 11:48:21 +00:00
|
|
|
|
2023-03-19 13:56:41 +00:00
|
|
|
assert dump_mock.call_count == 2
|
2022-04-19 11:48:21 +00:00
|
|
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
2023-03-19 16:50:08 +00:00
|
|
|
assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl')
|
2022-04-19 11:48:21 +00:00
|
|
|
|
|
|
|
dump_mock.reset_mock()
|
2022-04-20 12:38:52 +00:00
|
|
|
# mock file exporting
|
|
|
|
filename = Path(testdatadir / 'testresult')
|
2023-03-19 13:56:41 +00:00
|
|
|
store_backtest_analysis_results(filename, candle_dict, {}, '2022_01_01_15_05_13')
|
|
|
|
assert dump_mock.call_count == 2
|
2022-04-19 11:48:21 +00:00
|
|
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
|
|
|
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
2023-03-19 16:50:08 +00:00
|
|
|
assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl')
|
2022-04-20 12:38:52 +00:00
|
|
|
dump_mock.reset_mock()
|
|
|
|
|
|
|
|
|
2023-11-05 15:25:23 +00:00
|
|
|
def test_write_read_backtest_candles(tmp_path):
|
2022-04-20 12:38:52 +00:00
|
|
|
|
|
|
|
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
|
|
|
|
|
|
|
# test directory exporting
|
2023-03-19 13:56:41 +00:00
|
|
|
sample_date = '2022_01_01_15_05_13'
|
2023-11-05 15:25:23 +00:00
|
|
|
store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date)
|
|
|
|
stored_file = tmp_path / f'backtest-result-{sample_date}_signals.pkl'
|
2023-03-19 14:00:20 +00:00
|
|
|
with stored_file.open("rb") as scp:
|
|
|
|
pickled_signal_candles = joblib.load(scp)
|
2022-04-20 12:38:52 +00:00
|
|
|
|
|
|
|
assert pickled_signal_candles.keys() == candle_dict.keys()
|
|
|
|
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
|
|
|
assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \
|
|
|
|
.equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC'])
|
|
|
|
|
|
|
|
_clean_test_file(stored_file)
|
|
|
|
|
|
|
|
# test file exporting
|
2023-11-05 15:25:23 +00:00
|
|
|
filename = tmp_path / 'testresult'
|
2023-03-19 13:56:41 +00:00
|
|
|
store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
|
2023-11-05 15:25:23 +00:00
|
|
|
stored_file = tmp_path / f'testresult-{sample_date}_signals.pkl'
|
2023-03-19 14:00:20 +00:00
|
|
|
with stored_file.open("rb") as scp:
|
|
|
|
pickled_signal_candles = joblib.load(scp)
|
2022-04-20 12:38:52 +00:00
|
|
|
|
|
|
|
assert pickled_signal_candles.keys() == candle_dict.keys()
|
|
|
|
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
|
|
|
assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \
|
|
|
|
.equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC'])
|
|
|
|
|
|
|
|
_clean_test_file(stored_file)
|
2022-04-19 11:48:21 +00:00
|
|
|
|
|
|
|
|
2020-09-26 12:55:12 +00:00
|
|
|
def test_generate_pair_metrics():
|
2020-05-25 17:50:09 +00:00
|
|
|
|
|
|
|
results = pd.DataFrame(
|
|
|
|
{
|
|
|
|
'pair': ['ETH/BTC', 'ETH/BTC'],
|
2021-01-23 12:02:48 +00:00
|
|
|
'profit_ratio': [0.1, 0.2],
|
2020-05-25 17:50:09 +00:00
|
|
|
'profit_abs': [0.2, 0.4],
|
|
|
|
'trade_duration': [10, 30],
|
|
|
|
'wins': [2, 0],
|
|
|
|
'draws': [0, 0],
|
|
|
|
'losses': [0, 0]
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-01-07 07:44:11 +00:00
|
|
|
pair_results = generate_pair_metrics(['ETH/BTC'], stake_currency='BTC',
|
2021-02-13 08:01:05 +00:00
|
|
|
starting_balance=2, results=results)
|
2020-05-25 17:50:09 +00:00
|
|
|
assert isinstance(pair_results, list)
|
|
|
|
assert len(pair_results) == 2
|
|
|
|
assert pair_results[-1]['key'] == 'TOTAL'
|
|
|
|
assert (
|
|
|
|
pytest.approx(pair_results[-1]['profit_mean_pct']) == pair_results[-1]['profit_mean'] * 100)
|
|
|
|
assert (
|
|
|
|
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
|
|
|
|
|
|
|
|
|
2020-07-03 18:03:33 +00:00
|
|
|
def test_generate_daily_stats(testdatadir):
|
|
|
|
|
2022-12-26 14:30:39 +00:00
|
|
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
2020-07-03 18:03:33 +00:00
|
|
|
bt_data = load_backtest_data(filename)
|
|
|
|
res = generate_daily_stats(bt_data)
|
|
|
|
assert isinstance(res, dict)
|
|
|
|
assert round(res['backtest_best_day'], 4) == 0.1796
|
|
|
|
assert round(res['backtest_worst_day'], 4) == -0.1468
|
2022-01-05 19:17:04 +00:00
|
|
|
assert res['winning_days'] == 19
|
|
|
|
assert res['draw_days'] == 0
|
|
|
|
assert res['losing_days'] == 2
|
2020-07-03 18:03:33 +00:00
|
|
|
|
2020-08-20 17:56:41 +00:00
|
|
|
# Select empty dataframe!
|
|
|
|
res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :])
|
|
|
|
assert isinstance(res, dict)
|
|
|
|
assert round(res['backtest_best_day'], 4) == 0.0
|
|
|
|
assert res['winning_days'] == 0
|
|
|
|
assert res['draw_days'] == 0
|
|
|
|
assert res['losing_days'] == 0
|
|
|
|
|
2020-07-03 18:03:33 +00:00
|
|
|
|
2021-04-25 17:28:32 +00:00
|
|
|
def test_generate_trading_stats(testdatadir):
|
2022-12-26 14:30:39 +00:00
|
|
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
2021-04-25 17:28:32 +00:00
|
|
|
bt_data = load_backtest_data(filename)
|
|
|
|
res = generate_trading_stats(bt_data)
|
|
|
|
assert isinstance(res, dict)
|
|
|
|
assert res['winner_holding_avg'] == timedelta(seconds=1440)
|
|
|
|
assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420)
|
|
|
|
assert 'wins' in res
|
|
|
|
assert 'losses' in res
|
|
|
|
assert 'draws' in res
|
|
|
|
|
|
|
|
# Select empty dataframe!
|
|
|
|
res = generate_trading_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :])
|
|
|
|
assert res['wins'] == 0
|
|
|
|
assert res['losses'] == 0
|
|
|
|
|
|
|
|
|
2023-07-24 05:09:11 +00:00
|
|
|
def test_calc_streak(testdatadir):
|
2023-07-24 04:36:16 +00:00
|
|
|
df = pd.DataFrame({
|
|
|
|
'profit_ratio': [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03],
|
|
|
|
})
|
|
|
|
# 4 consecutive wins, 3 consecutive losses
|
2023-07-24 05:09:11 +00:00
|
|
|
res = calc_streak(df)
|
|
|
|
assert res == (4, 3)
|
|
|
|
assert isinstance(res[0], int)
|
|
|
|
assert isinstance(res[1], int)
|
2023-07-24 04:36:16 +00:00
|
|
|
|
|
|
|
# invert situation
|
|
|
|
df1 = df.copy()
|
|
|
|
df1['profit_ratio'] = df1['profit_ratio'] * -1
|
2023-07-24 05:09:11 +00:00
|
|
|
assert calc_streak(df1) == (3, 4)
|
2023-07-24 04:36:16 +00:00
|
|
|
|
|
|
|
df_empty = pd.DataFrame({
|
|
|
|
'profit_ratio': [],
|
|
|
|
})
|
|
|
|
assert df_empty.empty
|
2023-07-24 05:09:11 +00:00
|
|
|
assert calc_streak(df_empty) == (0, 0)
|
2023-07-24 04:36:16 +00:00
|
|
|
|
|
|
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
|
|
|
bt_data = load_backtest_data(filename)
|
2023-07-24 05:09:11 +00:00
|
|
|
assert calc_streak(bt_data) == (7, 18)
|
2023-07-24 04:36:16 +00:00
|
|
|
|
|
|
|
|
2022-03-24 19:33:47 +00:00
|
|
|
def test_text_table_exit_reason():
|
2020-01-02 06:33:55 +00:00
|
|
|
|
|
|
|
results = pd.DataFrame(
|
|
|
|
{
|
|
|
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
2021-01-23 12:02:48 +00:00
|
|
|
'profit_ratio': [0.1, 0.2, -0.1],
|
2020-01-09 05:46:39 +00:00
|
|
|
'profit_abs': [0.2, 0.4, -0.2],
|
2020-01-02 06:33:55 +00:00
|
|
|
'trade_duration': [10, 30, 10],
|
2020-02-07 02:51:50 +00:00
|
|
|
'wins': [2, 0, 0],
|
|
|
|
'draws': [0, 0, 0],
|
|
|
|
'losses': [0, 0, 1],
|
2022-03-24 19:33:47 +00:00
|
|
|
'exit_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
|
2020-01-02 06:33:55 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
result_str = (
|
2022-03-22 05:43:37 +00:00
|
|
|
'| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % | Cum Profit % |'
|
2021-05-21 09:31:16 +00:00
|
|
|
' Tot Profit BTC | Tot Profit % |\n'
|
|
|
|
'|---------------+---------+--------------------------+----------------+----------------+'
|
|
|
|
'------------------+----------------|\n'
|
|
|
|
'| roi | 2 | 2 0 0 100 | 15 | 30 |'
|
|
|
|
' 0.6 | 15 |\n'
|
|
|
|
'| stop_loss | 1 | 0 0 1 0 | -10 | -10 |'
|
|
|
|
' -0.2 | -5 |'
|
2020-01-02 06:33:55 +00:00
|
|
|
)
|
2020-05-25 05:08:15 +00:00
|
|
|
|
2022-03-24 19:33:47 +00:00
|
|
|
exit_reason_stats = generate_exit_reason_stats(max_open_trades=2,
|
2020-05-25 05:08:15 +00:00
|
|
|
results=results)
|
2022-03-24 19:33:47 +00:00
|
|
|
assert text_table_exit_reason(exit_reason_stats=exit_reason_stats,
|
2020-06-07 09:31:33 +00:00
|
|
|
stake_currency='BTC') == result_str
|
2020-01-02 06:33:55 +00:00
|
|
|
|
|
|
|
|
2020-09-26 12:55:12 +00:00
|
|
|
def test_generate_sell_reason_stats():
|
2020-05-25 05:14:21 +00:00
|
|
|
|
|
|
|
results = pd.DataFrame(
|
|
|
|
{
|
|
|
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
2021-01-23 12:02:48 +00:00
|
|
|
'profit_ratio': [0.1, 0.2, -0.1],
|
2020-05-25 05:14:21 +00:00
|
|
|
'profit_abs': [0.2, 0.4, -0.2],
|
|
|
|
'trade_duration': [10, 30, 10],
|
|
|
|
'wins': [2, 0, 0],
|
|
|
|
'draws': [0, 0, 0],
|
|
|
|
'losses': [0, 0, 1],
|
2022-03-24 19:33:47 +00:00
|
|
|
'exit_reason': [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value]
|
2020-05-25 05:14:21 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-03-24 19:33:47 +00:00
|
|
|
exit_reason_stats = generate_exit_reason_stats(max_open_trades=2,
|
2020-05-25 05:14:21 +00:00
|
|
|
results=results)
|
2022-03-24 19:33:47 +00:00
|
|
|
roi_result = exit_reason_stats[0]
|
|
|
|
assert roi_result['exit_reason'] == 'roi'
|
2020-05-25 05:14:21 +00:00
|
|
|
assert roi_result['trades'] == 2
|
|
|
|
assert pytest.approx(roi_result['profit_mean']) == 0.15
|
|
|
|
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
|
|
|
|
assert pytest.approx(roi_result['profit_mean']) == 0.15
|
|
|
|
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
|
|
|
|
|
2022-03-24 19:33:47 +00:00
|
|
|
stop_result = exit_reason_stats[1]
|
2020-05-25 05:14:21 +00:00
|
|
|
|
2022-03-24 19:33:47 +00:00
|
|
|
assert stop_result['exit_reason'] == 'stop_loss'
|
2020-05-25 05:14:21 +00:00
|
|
|
assert stop_result['trades'] == 1
|
|
|
|
assert pytest.approx(stop_result['profit_mean']) == -0.1
|
|
|
|
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
|
|
|
|
assert pytest.approx(stop_result['profit_mean']) == -0.1
|
|
|
|
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
|
|
|
|
|
|
|
|
|
2022-01-05 19:29:40 +00:00
|
|
|
def test_text_table_strategy(testdatadir):
|
2022-04-11 18:32:02 +00:00
|
|
|
filename = testdatadir / "backtest_results/backtest-result_multistrat.json"
|
2022-01-05 19:29:40 +00:00
|
|
|
bt_res_data = load_backtest_stats(filename)
|
|
|
|
|
|
|
|
bt_res_data_comparison = bt_res_data.pop('strategy_comparison')
|
2020-01-02 06:33:55 +00:00
|
|
|
|
|
|
|
result_str = (
|
2022-09-09 17:58:55 +00:00
|
|
|
'| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC |'
|
2021-05-21 09:31:16 +00:00
|
|
|
' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n'
|
2022-09-09 17:58:55 +00:00
|
|
|
'|----------------+-----------+----------------+----------------+------------------+'
|
2021-05-21 09:31:16 +00:00
|
|
|
'----------------+----------------+-------------------------+-----------------------|\n'
|
2022-09-09 17:58:55 +00:00
|
|
|
'| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |'
|
2022-01-05 19:29:40 +00:00
|
|
|
' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n'
|
2022-09-09 17:58:55 +00:00
|
|
|
'| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |'
|
2022-01-05 19:29:40 +00:00
|
|
|
' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |'
|
2020-01-02 06:33:55 +00:00
|
|
|
)
|
2020-05-25 17:55:02 +00:00
|
|
|
|
2022-01-05 19:29:40 +00:00
|
|
|
strategy_results = generate_strategy_comparison(bt_stats=bt_res_data['strategy'])
|
|
|
|
assert strategy_results == bt_res_data_comparison
|
2020-06-07 09:31:33 +00:00
|
|
|
assert text_table_strategy(strategy_results, 'BTC') == result_str
|
2020-01-09 05:52:34 +00:00
|
|
|
|
|
|
|
|
2020-09-26 12:55:12 +00:00
|
|
|
def test_generate_edge_table():
|
2020-01-09 05:52:34 +00:00
|
|
|
|
|
|
|
results = {}
|
|
|
|
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
|
2020-02-27 12:28:28 +00:00
|
|
|
assert generate_edge_table(results).count('+') == 7
|
2020-01-09 05:52:34 +00:00
|
|
|
assert generate_edge_table(results).count('| ETH/BTC |') == 1
|
|
|
|
assert generate_edge_table(results).count(
|
2020-02-07 02:51:50 +00:00
|
|
|
'| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1
|
2021-10-21 05:09:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
def test_generate_periodic_breakdown_stats(testdatadir):
|
2022-12-26 14:30:39 +00:00
|
|
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
2021-10-21 05:09:17 +00:00
|
|
|
bt_data = load_backtest_data(filename).to_dict(orient='records')
|
|
|
|
|
|
|
|
res = generate_periodic_breakdown_stats(bt_data, 'day')
|
|
|
|
assert isinstance(res, list)
|
|
|
|
assert len(res) == 21
|
|
|
|
day = res[0]
|
|
|
|
assert 'date' in day
|
|
|
|
assert 'draws' in day
|
|
|
|
assert 'loses' in day
|
|
|
|
assert 'wins' in day
|
|
|
|
assert 'profit_abs' in day
|
|
|
|
|
|
|
|
# Select empty dataframe!
|
|
|
|
res = generate_periodic_breakdown_stats([], 'day')
|
|
|
|
assert res == []
|
|
|
|
|
|
|
|
|
|
|
|
def test__get_resample_from_period():
|
|
|
|
|
|
|
|
assert _get_resample_from_period('day') == '1d'
|
2023-04-17 18:20:38 +00:00
|
|
|
assert _get_resample_from_period('week') == '1W-MON'
|
2021-10-21 05:09:17 +00:00
|
|
|
assert _get_resample_from_period('month') == '1M'
|
|
|
|
with pytest.raises(ValueError, match=r"Period noooo is not supported."):
|
|
|
|
_get_resample_from_period('noooo')
|
2021-10-30 14:53:48 +00:00
|
|
|
|
2023-04-24 07:41:36 +00:00
|
|
|
for period in BACKTEST_BREAKDOWNS:
|
|
|
|
assert isinstance(_get_resample_from_period(period), str)
|
|
|
|
|
2021-10-30 14:53:48 +00:00
|
|
|
|
2021-10-30 15:05:12 +00:00
|
|
|
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
|
2022-12-26 14:30:39 +00:00
|
|
|
filename = testdatadir / "backtest_results/backtest-result.json"
|
2021-10-30 14:53:48 +00:00
|
|
|
bt_data = load_backtest_stats(filename)
|
|
|
|
default_conf['backtest_show_pair_list'] = True
|
|
|
|
|
2021-10-30 15:05:12 +00:00
|
|
|
show_sorted_pairlist(default_conf, bt_data)
|
2021-10-30 14:53:48 +00:00
|
|
|
|
2024-01-24 19:31:38 +00:00
|
|
|
out, _err = capsys.readouterr()
|
2022-01-07 09:09:17 +00:00
|
|
|
assert 'Pairs for Strategy StrategyTestV3: \n[' in out
|
2021-10-30 14:53:48 +00:00
|
|
|
assert 'TOTAL' not in out
|
|
|
|
assert '"ETH/BTC", // ' in out
|