mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 05:50:36 +00:00
feat: add SQN calculation as backtest metric
This commit is contained in:
@@ -375,3 +375,32 @@ def calculate_calmar(
|
||||
|
||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||
return calmar_ratio
|
||||
|
||||
|
||||
def calculate_sqn(trades: pd.DataFrame, starting_balance: float) -> float:
|
||||
"""
|
||||
Calculate System Quality Number (SQN) - Van K. Tharp.
|
||||
SQN measures systematic trading quality and takes into account both
|
||||
the number of trades and their standard deviation.
|
||||
|
||||
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||
:param starting_balance: Starting balance of the trading system
|
||||
:return: SQN value
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
return 0.0
|
||||
|
||||
total_profit = trades["profit_abs"] / starting_balance
|
||||
number_of_trades = len(trades)
|
||||
|
||||
# Calculate average trade and standard deviation
|
||||
average_profits = total_profit.mean()
|
||||
profits_std = total_profit.std()
|
||||
|
||||
if profits_std != 0 and not np.isnan(profits_std):
|
||||
sqn = math.sqrt(number_of_trades) * (average_profits / profits_std)
|
||||
else:
|
||||
# Define negative SQN to indicate this is NOT optimal
|
||||
sqn = -100.0
|
||||
|
||||
return round(sqn, 4)
|
||||
|
||||
@@ -30,6 +30,7 @@ from freqtrade.data.metrics import (
|
||||
calculate_max_drawdown,
|
||||
calculate_sharpe,
|
||||
calculate_sortino,
|
||||
calculate_sqn,
|
||||
calculate_underwater,
|
||||
combine_dataframes_with_mean,
|
||||
combined_dataframes_with_rel_mean,
|
||||
@@ -457,6 +458,42 @@ def test_calculate_calmar(testdatadir):
|
||||
assert pytest.approx(calmar) == 559.040508
|
||||
|
||||
|
||||
def test_calculate_sqn(testdatadir):
|
||||
filename = testdatadir / "backtest_results/backtest-result.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
|
||||
sqn = calculate_sqn(DataFrame(), 0)
|
||||
assert sqn == 0.0
|
||||
|
||||
sqn = calculate_sqn(
|
||||
bt_data,
|
||||
0.01,
|
||||
)
|
||||
assert isinstance(sqn, float)
|
||||
assert pytest.approx(sqn) == 3.2991
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"profits,starting_balance,expected_sqn,description",
|
||||
[
|
||||
([1.0, -0.5, 2.0, -1.0, 0.5, 1.5, -0.5, 1.0], 100, 1.3229, "Mixed profits/losses"),
|
||||
([], 100, 0.0, "Empty dataframe"),
|
||||
([1.0, 0.5, 2.0, 1.5, 0.8], 100, 4.3657, "All winning trades"),
|
||||
([-1.0, -0.5, -2.0, -1.5, -0.8], 100, -4.3657, "All losing trades"),
|
||||
([1.0], 100, -100, "Single trade"),
|
||||
],
|
||||
)
|
||||
def test_calculate_sqn_cases(profits, starting_balance, expected_sqn, description):
|
||||
"""
|
||||
Test SQN calculation with various scenarios:
|
||||
"""
|
||||
trades = DataFrame({"profit_abs": profits})
|
||||
sqn = calculate_sqn(trades, starting_balance=starting_balance)
|
||||
|
||||
assert isinstance(sqn, float)
|
||||
assert pytest.approx(sqn, rel=1e-4) == expected_sqn
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"start,end,days, expected",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user