From 9d1f97cff39c4c9e5f3206d2534fb79472ac937f Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 11 May 2026 16:59:26 +0200 Subject: [PATCH] feat(protocol): dispatcher temporal features (hour) in compiler --- src/multi_swarm/protocol/compiler.py | 9 +++++++++ tests/unit/test_protocol_compiler.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/multi_swarm/protocol/compiler.py b/src/multi_swarm/protocol/compiler.py index 7486fe0..2b98467 100644 --- a/src/multi_swarm/protocol/compiler.py +++ b/src/multi_swarm/protocol/compiler.py @@ -107,6 +107,13 @@ INDICATOR_FNS: dict[str, Any] = { "macd": _ind_macd, } +_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = { + "hour": lambda idx: pd.Series(idx.hour, index=idx, dtype="int64"), + "dow": lambda idx: pd.Series(idx.dayofweek, index=idx, dtype="int64"), + "is_weekend": lambda idx: pd.Series((idx.dayofweek >= 5).astype("int64"), index=idx), + "minute_of_hour": lambda idx: pd.Series(idx.minute, index=idx, dtype="int64"), +} + def _to_series(value: float, df: pd.DataFrame) -> pd.Series: """Broadcast a numeric literal across the DataFrame index.""" @@ -134,6 +141,8 @@ def _eval_bool_arg(node: Node, df: pd.DataFrame) -> pd.Series: def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series: if isinstance(node, FeatureNode): + if node.name in _TIME_FEATURE_FNS: + return _TIME_FEATURE_FNS[node.name](df.index) return df[node.name] if isinstance(node, IndicatorNode): diff --git a/tests/unit/test_protocol_compiler.py b/tests/unit/test_protocol_compiler.py index 80726f3..16ea00e 100644 --- a/tests/unit/test_protocol_compiler.py +++ b/tests/unit/test_protocol_compiler.py @@ -106,3 +106,27 @@ def test_compile_two_rules_priority(ohlcv: pd.DataFrame) -> None: signals = fn(ohlcv) last = signals.iloc[-1] assert last == Side.LONG # close finale e' 120, regola 1 matcha + + +def test_compile_hour_feature_returns_index_hour(ohlcv: pd.DataFrame) -> None: + src = json.dumps( + { + "rules": [ + { + "condition": { + "op": "gt", + "args": [ + {"kind": "feature", "name": "hour"}, + {"kind": "literal", "value": -1.0}, + ], + }, + "action": "entry-long", + } + ] + } + ) + ast = parse_strategy(src) + fn = compile_strategy(ast) + signal = fn(ohlcv) + # All rows have hour >= 0 > -1, so all entry-long. + assert (signal == Side.LONG).all()