From 2aa5646aeb7265330bbbb96fdb3621f0c711092a Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Tue, 19 May 2026 13:29:16 +0000 Subject: [PATCH] feat(protocol): validate arity + semantics of 3 Pythagoras indicators --- .../multi_swarm_core/protocol/validator.py | 38 ++++++++++ .../tests/test_validator_indicators.py | 71 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/strategy_pythagoras/tests/test_validator_indicators.py diff --git a/src/multi_swarm_core/multi_swarm_core/protocol/validator.py b/src/multi_swarm_core/multi_swarm_core/protocol/validator.py index 6c979f8..e85a009 100644 --- a/src/multi_swarm_core/multi_swarm_core/protocol/validator.py +++ b/src/multi_swarm_core/multi_swarm_core/protocol/validator.py @@ -39,6 +39,10 @@ INDICATOR_ARITY: dict[str, tuple[int, int]] = { "realized_vol": (1, 1), # window "macd": (0, 3), # fast, slow, signal (tutti opzionali) "macd_pct": (0, 3), # macd/close, frazionale (per confronti con literal) + # Pythagoras indicators (params encoded as floats) + "candle_pattern": (4, 13), # [length, sym0, ..., sym_{length-1}] + "pythagorean_ratio": (1, 1), # lookback in [12,200] + "fractal_mirror": (2, 2), # k in [3,12], axis_int in {0=h,1=v} } @@ -110,3 +114,37 @@ def _validate_indicator(node: IndicatorNode) -> None: raise ValidationError( f"indicator '{node.name}' arity {n_params} out of [{min_p},{max_p}]" ) + + # Pythagoras-specific param semantics + name = node.name + if name == "candle_pattern": + length = int(node.params[0]) + if not (3 <= length <= 12): + raise ValidationError( + f"candle_pattern length must be in [3,12], got {length}" + ) + if n_params != 1 + length: + raise ValidationError( + f"candle_pattern: expected 1+length={1 + length} params, got {n_params}" + ) + for i, sym in enumerate(node.params[1:], start=1): + sym_int = int(sym) + if sym_int not in (0, 1, 2): + raise ValidationError( + f"candle_pattern sym[{i - 1}] must be 0(U)/1(D)/2(doji), got {sym}" + ) + elif name == "pythagorean_ratio": + lookback = int(node.params[0]) + if not (12 <= lookback <= 200): + raise ValidationError( + f"pythagorean_ratio lookback in [12,200], got {lookback}" + ) + elif name == "fractal_mirror": + k = int(node.params[0]) + if not (3 <= k <= 12): + raise ValidationError(f"fractal_mirror k must be in [3,12], got {k}") + axis_int = int(node.params[1]) + if axis_int not in (0, 1): + raise ValidationError( + f"fractal_mirror axis must be 0(h)/1(v), got {axis_int}" + ) diff --git a/src/strategy_pythagoras/tests/test_validator_indicators.py b/src/strategy_pythagoras/tests/test_validator_indicators.py new file mode 100644 index 0000000..d9bfb7b --- /dev/null +++ b/src/strategy_pythagoras/tests/test_validator_indicators.py @@ -0,0 +1,71 @@ +"""Validator accetta i 3 nuovi indicatori candle con arity corrette.""" +from __future__ import annotations + +import json + +import pytest + +from multi_swarm_core.protocol.parser import parse_strategy +from multi_swarm_core.protocol.validator import ValidationError, validate_strategy + + +def _strategy_with_indicator(name: str, params: list[float]) -> str: + return json.dumps( + { + "rules": [ + { + "condition": { + "op": "gt", + "args": [ + {"kind": "indicator", "name": name, "params": params}, + {"kind": "literal", "value": 0.5}, + ], + }, + "action": "entry-long", + } + ] + } + ) + + +def test_candle_pattern_valid_min_arity() -> None: + s = parse_strategy(_strategy_with_indicator("candle_pattern", [3, 0, 1, 0])) + validate_strategy(s) + + +def test_candle_pattern_valid_max_arity() -> None: + s = parse_strategy(_strategy_with_indicator("candle_pattern", [12] + [0] * 12)) + validate_strategy(s) + + +def test_candle_pattern_rejects_too_few_params() -> None: + s = parse_strategy(_strategy_with_indicator("candle_pattern", [3, 0, 1])) + with pytest.raises(ValidationError): + validate_strategy(s) + + +def test_pythagorean_ratio_valid() -> None: + s = parse_strategy(_strategy_with_indicator("pythagorean_ratio", [89])) + validate_strategy(s) + + +def test_pythagorean_ratio_rejects_zero_params() -> None: + s = parse_strategy(_strategy_with_indicator("pythagorean_ratio", [])) + with pytest.raises(ValidationError): + validate_strategy(s) + + +def test_fractal_mirror_valid_h() -> None: + s = parse_strategy(_strategy_with_indicator("fractal_mirror", [12, 0])) + validate_strategy(s) + + +def test_fractal_mirror_valid_v() -> None: + s = parse_strategy(_strategy_with_indicator("fractal_mirror", [12, 1])) + validate_strategy(s) + + +def test_fractal_mirror_rejects_one_param() -> None: + s = parse_strategy(_strategy_with_indicator("fractal_mirror", [12])) + with pytest.raises(ValidationError): + validate_strategy(s)