Add trailing stops and parameter sweeps
CI/CD / backend (pull_request) Successful in 6m6s
CI/CD / frontend (pull_request) Successful in 5m17s
CI/CD / build (pull_request) Skipped
CI/CD / deploy (pull_request) Skipped
CI/CD / cleanup (pull_request) Skipped

Trailing stops: strategies can pass trailing_stop_pct to enter_long/enter_short.
advance_trailing_stop in app/engine/portfolio.py ratchets the stop with the price
(up for longs, down for shorts) and never loosens; the engine persists the
ratcheted level and the backtester applies the same logic. Position gains a
trailing_stop_pct column (additive migration). momentum exposes it as a param.

Parameter sweeps: app/backtest/sweep.py runs a cartesian grid through the same
backtester and ranks by return minus max drawdown. POST /api/sweep plus a
dashboard Parameter sweep panel.
This commit is contained in:
matitos committed 2026-09-19 23:04:20 +00:00
1 parent d80e7b155e
commit 9dc00eba95
22 files changed
+575 -7

No files matched your search

+28
View File
@@ -4,6 +4,7 @@ import pytest
from app.engine.portfolio import (
account_equity,
advance_trailing_stop,
apply_close,
apply_open,
order_qty,
@@ -57,3 +58,30 @@ def test_risk_exit_short() -> None:
assert risk_exit(PositionSide.SHORT, 110, 90, high=111, low=100) == ("stop loss", 110)
assert risk_exit(PositionSide.SHORT, 110, 90, high=100, low=89) == ("take profit", 90)
assert risk_exit(PositionSide.SHORT, None, None, high=100, low=95) is None
def test_trailing_stop_long_ratchets_up_only() -> None:
# First update seeds the stop 2% below the high.
stop = advance_trailing_stop(PositionSide.LONG, None, 0.02, high=100, low=95)
assert stop == pytest.approx(98.0)
# A higher high raises it.
stop = advance_trailing_stop(PositionSide.LONG, stop, 0.02, high=110, low=100)
assert stop == pytest.approx(107.8)
# A pullback never loosens it.
stop = advance_trailing_stop(PositionSide.LONG, stop, 0.02, high=105, low=100)
assert stop == pytest.approx(107.8)
def test_trailing_stop_short_ratchets_down_only() -> None:
stop = advance_trailing_stop(PositionSide.SHORT, None, 0.02, high=100, low=95)
assert stop == pytest.approx(96.9)
stop = advance_trailing_stop(PositionSide.SHORT, stop, 0.02, high=95, low=90)
assert stop == pytest.approx(91.8)
# A bounce never loosens the short's stop.
stop = advance_trailing_stop(PositionSide.SHORT, stop, 0.02, high=95, low=92)
assert stop == pytest.approx(91.8)
def test_trailing_stop_disabled_is_noop() -> None:
assert advance_trailing_stop(PositionSide.LONG, 90.0, 0.0, high=100, low=80) == 90.0
assert advance_trailing_stop(PositionSide.LONG, None, 0.0, high=100, low=80) is None