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)
|
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||||
return 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_max_drawdown,
|
||||||
calculate_sharpe,
|
calculate_sharpe,
|
||||||
calculate_sortino,
|
calculate_sortino,
|
||||||
|
calculate_sqn,
|
||||||
calculate_underwater,
|
calculate_underwater,
|
||||||
combine_dataframes_with_mean,
|
combine_dataframes_with_mean,
|
||||||
combined_dataframes_with_rel_mean,
|
combined_dataframes_with_rel_mean,
|
||||||
@@ -457,6 +458,42 @@ def test_calculate_calmar(testdatadir):
|
|||||||
assert pytest.approx(calmar) == 559.040508
|
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(
|
@pytest.mark.parametrize(
|
||||||
"start,end,days, expected",
|
"start,end,days, expected",
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user