@ -231,17 +231,50 @@ class TestRunEntryGates:
class TestMacroRegimeGate :
class TestMacroRegimeGate :
""" Macro regime filter gate tests. """
""" Macro regime filter gate tests. """
def test_blocks_when_spy_below_sma ( self ) :
def test_blocks_when_spy_below_sma _and_scaler_gte_1 ( self ) :
from libs . backtest . allocator import run_entry_gates
from libs . backtest . allocator import run_entry_gates
c = _make_candidate ( )
c = _make_candidate ( )
ps = _make_portfolio_state ( )
ps = _make_portfolio_state ( )
cfg = _make_config ( )
cfg = _make_config ( )
cfg . risk . macro_regime_enabled = True
cfg . risk . macro_regime_enabled = True
cfg . risk . macro_regime_size_scaler = 1.0 # default — hard block
macro = { " spy_close " : 490.0 , " spy_sma_20 " : 500.0 } # SPY below SMA
macro = { " spy_close " : 490.0 , " spy_sma_20 " : 500.0 } # SPY below SMA
result = run_entry_gates ( c , ps , [ ] , cfg , macro_data = macro )
result = run_entry_gates ( c , ps , [ ] , cfg , macro_data = macro )
assert result == " macro_regime_unfavorable "
assert result == " macro_regime_unfavorable "
def test_passes_when_spy_below_sma_and_scaler_lt_1 ( self ) :
from libs . backtest . allocator import run_entry_gates
c = _make_candidate ( )
ps = _make_portfolio_state ( )
cfg = _make_config ( )
cfg . risk . macro_regime_enabled = True
cfg . risk . macro_regime_size_scaler = 0.5 # size scaler — don't hard block
macro = { " spy_close " : 490.0 , " spy_sma_20 " : 500.0 }
result = run_entry_gates ( c , ps , [ ] , cfg , macro_data = macro )
assert result is None # passes gate, size scaler applied in build_planned_order
def test_macro_size_scaler_reduces_shares ( self ) :
from libs . backtest . allocator import build_planned_order
c = _make_candidate ( entry_price_est = 100.0 , atr_14 = 2.0 )
ps = _make_portfolio_state ( )
cfg = _make_config ( )
cfg . risk . macro_regime_enabled = True
cfg . risk . macro_regime_size_scaler = 0.5
macro = { " spy_close " : 490.0 , " spy_sma_20 " : 500.0 }
# Without macro scaler
order_normal = build_planned_order ( c , ps , [ ] , cfg , macro_data = None )
# With macro scaler
order_scaled = build_planned_order ( c , ps , [ ] , cfg , macro_data = macro )
assert order_normal . skip_reason is None
assert order_scaled . skip_reason is None
assert order_scaled . shares < order_normal . shares
assert order_scaled . shares > = 1
def test_passes_when_spy_above_sma ( self ) :
def test_passes_when_spy_above_sma ( self ) :
from libs . backtest . allocator import run_entry_gates
from libs . backtest . allocator import run_entry_gates
@ -296,13 +329,14 @@ class TestMacroRegimeGate:
result = run_entry_gates ( c , ps , [ ] , cfg , macro_data = macro )
result = run_entry_gates ( c , ps , [ ] , cfg , macro_data = macro )
assert result is None # Can't evaluate, don't block
assert result is None # Can't evaluate, don't block
def test_build_planned_order_with_macro ( self ) :
def test_build_planned_order_with_macro _hard_block ( self ) :
from libs . backtest . allocator import build_planned_order
from libs . backtest . allocator import build_planned_order
c = _make_candidate ( )
c = _make_candidate ( )
ps = _make_portfolio_state ( )
ps = _make_portfolio_state ( )
cfg = _make_config ( )
cfg = _make_config ( )
cfg . risk . macro_regime_enabled = True
cfg . risk . macro_regime_enabled = True
cfg . risk . macro_regime_size_scaler = 1.0 # hard block mode
macro = { " spy_close " : 490.0 , " spy_sma_20 " : 500.0 }
macro = { " spy_close " : 490.0 , " spy_sma_20 " : 500.0 }
order = build_planned_order ( c , ps , [ ] , cfg , macro_data = macro )
order = build_planned_order ( c , ps , [ ] , cfg , macro_data = macro )
assert order . skip_reason == " macro_regime_unfavorable "
assert order . skip_reason == " macro_regime_unfavorable "
@ -337,41 +371,6 @@ class TestComputeTargetPrice:
assert target == pytest . approx ( 110.0 ) # falls back to fixed_r
assert target == pytest . approx ( 110.0 ) # falls back to fixed_r
class TestSUEEntryGate :
def test_negative_eps_growth_blocked ( self ) :
from libs . backtest . allocator import run_entry_gates
c = _make_candidate (
event_type = " earnings_release " ,
features = { " eps_growth_qoq " : - 0.05 } ,
)
ps = _make_portfolio_state ( )
result = run_entry_gates ( c , ps , [ ] , _make_config ( ) )
assert result == " negative_earnings_surprise "
def test_positive_eps_growth_passes ( self ) :
from libs . backtest . allocator import run_entry_gates
c = _make_candidate (
event_type = " earnings_release " ,
features = { " eps_growth_qoq " : 0.10 } ,
)
ps = _make_portfolio_state ( )
result = run_entry_gates ( c , ps , [ ] , _make_config ( ) )
assert result is None
def test_non_earnings_not_checked ( self ) :
from libs . backtest . allocator import run_entry_gates
c = _make_candidate (
event_type = " guidance_update " ,
features = { " eps_growth_qoq " : - 0.50 } ,
)
ps = _make_portfolio_state ( )
result = run_entry_gates ( c , ps , [ ] , _make_config ( ) )
assert result is None
class TestDirectionFilter :
class TestDirectionFilter :
def test_bullish_only_blocks_bearish ( self ) :
def test_bullish_only_blocks_bearish ( self ) :
from libs . backtest . allocator import run_entry_gates
from libs . backtest . allocator import run_entry_gates