|
18 | 18 | calculate_market_change, |
19 | 19 | calculate_max_drawdown, |
20 | 20 | calculate_max_drawdown_from_balance, |
| 21 | + calculate_p_value, |
21 | 22 | calculate_sharpe, |
22 | 23 | calculate_sharpe_from_balance, |
23 | 24 | calculate_sortino, |
@@ -442,6 +443,56 @@ def test_calculate_sqn_cases(profits, starting_balance, expected_sqn, descriptio |
442 | 443 | assert pytest.approx(sqn, rel=1e-4) == expected_sqn |
443 | 444 |
|
444 | 445 |
|
| 446 | +def test_calculate_p_value_edge_cases(): |
| 447 | + # Fewer than two trades -> not computable, returns "no evidence" default. |
| 448 | + assert calculate_p_value(DataFrame({"profit_abs": []}), 100) == 1.0 |
| 449 | + assert calculate_p_value(DataFrame({"profit_abs": [1.0]}), 100) == 1.0 |
| 450 | + |
| 451 | + # Zero variance (all identical returns) -> not computable. |
| 452 | + assert calculate_p_value(DataFrame({"profit_abs": [1.0, 1.0, 1.0]}), 100) == 1.0 |
| 453 | + |
| 454 | + # p-value is always within [0, 1]. |
| 455 | + p_value = calculate_p_value(DataFrame({"profit_abs": [1.0, -0.5, 2.0, -1.0]}), 100) |
| 456 | + assert 0.0 <= p_value <= 1.0 |
| 457 | + |
| 458 | + |
| 459 | +def test_calculate_p_value_scale_invariance(): |
| 460 | + # The t-statistic, and hence the p-value, is invariant to the stake scale. |
| 461 | + profits = [1.0, -0.5, 2.0, -1.0, 0.5, 1.5, -0.5, 1.0] |
| 462 | + trades = DataFrame({"profit_abs": profits}) |
| 463 | + p_small = calculate_p_value(trades, starting_balance=10) |
| 464 | + p_large = calculate_p_value(trades, starting_balance=100_000) |
| 465 | + assert pytest.approx(p_small, rel=1e-9) == p_large |
| 466 | + |
| 467 | + |
| 468 | +def test_calculate_p_value_matches_reference(): |
| 469 | + """ |
| 470 | + calculate_p_value must match scipy.stats.ttest_1samp, the canonical |
| 471 | + reference, computed live for each case. |
| 472 | + """ |
| 473 | + from scipy import stats |
| 474 | + |
| 475 | + cases = [ |
| 476 | + [0.01, -0.005, 0.02, 0.015, -0.01], |
| 477 | + [0.05, 0.04, 0.06, 0.045, 0.055], |
| 478 | + [-0.01, -0.02, -0.015, -0.005, -0.025], |
| 479 | + [0.001, -0.001, 0.001, -0.001], |
| 480 | + ] |
| 481 | + starting_balance = 1000.0 |
| 482 | + for returns in cases: |
| 483 | + trades = DataFrame({"profit_abs": [r * starting_balance for r in returns]}) |
| 484 | + result = calculate_p_value(trades, starting_balance) |
| 485 | + _, expected = stats.ttest_1samp(returns, popmean=0) |
| 486 | + assert abs(result - float(expected)) < 1e-10 |
| 487 | + |
| 488 | + |
| 489 | +def test_calculate_p_value_zero_mean(): |
| 490 | + # A strategy whose average trade is exactly break-even has a t-statistic of |
| 491 | + # zero -> p-value of exactly 1.0 (entirely indistinguishable from noise). |
| 492 | + trades = DataFrame({"profit_abs": [1.0, -1.0, 2.0, -2.0]}) |
| 493 | + assert calculate_p_value(trades, starting_balance=100) == 1.0 |
| 494 | + |
| 495 | + |
445 | 496 | @pytest.mark.parametrize( |
446 | 497 | "start,end,days, expected", |
447 | 498 | [ |
|
0 commit comments