diff --git a/tests/unit/test_protocol_compiler.py b/tests/unit/test_protocol_compiler.py index 04de7ba..8a92e4d 100644 --- a/tests/unit/test_protocol_compiler.py +++ b/tests/unit/test_protocol_compiler.py @@ -208,3 +208,48 @@ def test_compile_minute_of_hour_zero_on_1h_timeframe(ohlcv: pd.DataFrame) -> Non fn = compile_strategy(ast) signal = fn(ohlcv) assert (signal == Side.LONG).all() + + +def test_rule_with_temporal_gating_compiles_and_executes(ohlcv: pd.DataFrame) -> None: + # Rule: entry-long if hour > 14 AND close > sma(20). + # close in fixture is strictly increasing, so close > sma(20) holds after warmup. + # entry-long should appear only on rows with hour > 14. + src = json.dumps( + { + "rules": [ + { + "condition": { + "op": "and", + "args": [ + { + "op": "gt", + "args": [ + {"kind": "feature", "name": "hour"}, + {"kind": "literal", "value": 14.0}, + ], + }, + { + "op": "gt", + "args": [ + {"kind": "feature", "name": "close"}, + {"kind": "indicator", "name": "sma", "params": [20]}, + ], + }, + ], + }, + "action": "entry-long", + } + ] + } + ) + ast = parse_strategy(src) + fn = compile_strategy(ast) + signal = fn(ohlcv) + + # Bars with hour <= 14: never LONG (temporal gate blocks). + morning = signal[signal.index.hour <= 14] + assert (morning == Side.FLAT).all() + + # Bars with hour > 14 AND past SMA warmup (>=20 bars): LONG. + afternoon_warm = signal[(signal.index.hour > 14) & (np.arange(len(signal)) >= 20)] + assert (afternoon_warm == Side.LONG).all()