refactor(protocol): swap S-expression grammar for strict JSON Schema
Sostituisce la grammatica S-expression con uno schema JSON stretto. La grammatica S-expression falliva il parsing nel 64% delle generazioni del modello Qwen3-235B sul run reale; JSON e' nativo per gli LLM moderni e si parsa con json.loads. Cambiamenti principali: - grammar.py: costanti rinominate LOGICAL_OPS / COMPARATOR_OPS / CROSSOVER_OPS / ACTION_VALUES / KIND_VALUES. - parser.py: nuovo AST a dataclass tipizzato (OpNode, IndicatorNode, FeatureNode, LiteralNode, Rule, Strategy); parse_strategy ora consuma JSON tramite json.loads. - validator.py: walk dispatchato per tipo (isinstance) invece di pattern-matching su 'kind'; arity check su operatori e indicator. - compiler.py: traversal del nuovo AST tipizzato, dispatch per isinstance; logica indicator/feature/literal invariata. - hypothesis.py: prompt SYSTEM riscritto con esempi JSON e vincoli espliciti su no-nesting; estrazione via fence ```json``` + fallback brace-balanced. - __init__.py: re-export pubblico delle entita' del protocollo. - Tutti i test (parser, validator, compiler, hypothesis_agent, falsification, adversarial, e2e, smoke_run) migrati a JSON. - Rimossa dipendenza sexpdata da pyproject.toml + uv.lock. Test: 135 passed (era 122; aggiunti casi parser/validator). ruff + mypy strict clean. Smoke run end-to-end OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,41 @@
|
||||
"""Semantic validation for the JSON-based strategy AST.
|
||||
|
||||
Il parser garantisce già shape sintattica (op vs kind, struttura args/params,
|
||||
tipi base). Qui si controllano vincoli semantici di Phase 1:
|
||||
|
||||
* Arity di operatori logici / comparatori / crossover.
|
||||
* Whitelist indicator + arity dei params.
|
||||
* Whitelist feature.
|
||||
* Niente nesting di indicator (params puramente numerici, garantito già dal
|
||||
parser ma ricontrollato esplicitamente per chiarezza).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .grammar import COMPARATOR_VERBS, LOGICAL_VERBS
|
||||
from .parser import Node, Strategy
|
||||
|
||||
KNOWN_INDICATORS: frozenset[str] = frozenset({"sma", "rsi", "atr", "macd", "realized_vol"})
|
||||
KNOWN_FEATURES: frozenset[str] = frozenset({"open", "high", "low", "close", "volume"})
|
||||
from .grammar import (
|
||||
COMPARATOR_OPS,
|
||||
CROSSOVER_OPS,
|
||||
KNOWN_FEATURES,
|
||||
KNOWN_INDICATORS,
|
||||
LOGICAL_OPS,
|
||||
)
|
||||
from .parser import (
|
||||
FeatureNode,
|
||||
IndicatorNode,
|
||||
LiteralNode,
|
||||
Node,
|
||||
OpNode,
|
||||
Strategy,
|
||||
)
|
||||
|
||||
# Numero di parametri numerici accettati dopo il nome dell'indicatore.
|
||||
# La tupla (min, max) include solo i numeri (gli argomenti di tipo Node sono
|
||||
# proibiti dal compiler - gli indicatori non sono annidabili in Phase 1).
|
||||
# (min, max) sui soli numeri. Indicatori non sono annidabili in Phase 1.
|
||||
INDICATOR_ARITY: dict[str, tuple[int, int]] = {
|
||||
"sma": (1, 1), # length
|
||||
"rsi": (1, 1), # length
|
||||
"atr": (1, 1), # length
|
||||
"realized_vol": (1, 1), # window
|
||||
"macd": (0, 3), # fast, slow, signal (tutti opzionali con default)
|
||||
"macd": (0, 3), # fast, slow, signal (tutti opzionali)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,77 +44,66 @@ class ValidationError(Exception):
|
||||
|
||||
|
||||
def validate_strategy(strategy: Strategy) -> None:
|
||||
"""Check semantic constraints on a parsed Strategy AST.
|
||||
|
||||
The parser already enforces verb-set membership; this pass adds:
|
||||
* arity checks for logical/comparator/data verbs,
|
||||
* known-indicator / known-feature whitelists.
|
||||
"""
|
||||
"""Walk every rule of the strategy and assert semantic constraints."""
|
||||
for rule in strategy.rules:
|
||||
_validate_node(rule.condition, _expect_bool=True)
|
||||
_validate_node(rule.condition)
|
||||
|
||||
|
||||
def _validate_node(node: Node, _expect_bool: bool) -> None:
|
||||
if node.kind in LOGICAL_VERBS:
|
||||
if node.kind == "not":
|
||||
if len(node.args) != 1:
|
||||
raise ValidationError(f"'not' needs 1 arg, got {len(node.args)}")
|
||||
arg = node.args[0]
|
||||
if isinstance(arg, Node):
|
||||
_validate_node(arg, _expect_bool=True)
|
||||
def _validate_node(node: Node) -> None:
|
||||
if isinstance(node, OpNode):
|
||||
_validate_op(node)
|
||||
return
|
||||
if isinstance(node, IndicatorNode):
|
||||
_validate_indicator(node)
|
||||
return
|
||||
if isinstance(node, FeatureNode):
|
||||
if node.name not in KNOWN_FEATURES:
|
||||
raise ValidationError(f"unknown feature: {node.name}")
|
||||
return
|
||||
if isinstance(node, LiteralNode):
|
||||
# parser ha già validato il tipo numerico
|
||||
return
|
||||
raise ValidationError(f"unexpected node type: {type(node).__name__}")
|
||||
|
||||
|
||||
def _validate_op(node: OpNode) -> None:
|
||||
op = node.op
|
||||
n = len(node.args)
|
||||
|
||||
if op in LOGICAL_OPS:
|
||||
if op == "not":
|
||||
if n != 1:
|
||||
raise ValidationError(f"'not' needs 1 arg, got {n}")
|
||||
else:
|
||||
if len(node.args) < 2:
|
||||
raise ValidationError(f"'{node.kind}' needs >=2 args")
|
||||
for a in node.args:
|
||||
if isinstance(a, Node):
|
||||
_validate_node(a, _expect_bool=True)
|
||||
return
|
||||
|
||||
if node.kind in COMPARATOR_VERBS:
|
||||
if len(node.args) != 2:
|
||||
raise ValidationError(f"'{node.kind}' needs 2 args, got {len(node.args)}")
|
||||
if n < 2:
|
||||
raise ValidationError(f"'{op}' needs >=2 args, got {n}")
|
||||
for a in node.args:
|
||||
if isinstance(a, Node):
|
||||
_validate_node(a, _expect_bool=False)
|
||||
_validate_node(a)
|
||||
return
|
||||
|
||||
if node.kind in {"crossover", "crossunder"}:
|
||||
if len(node.args) != 2:
|
||||
raise ValidationError(f"'{node.kind}' needs 2 args")
|
||||
if op in COMPARATOR_OPS:
|
||||
if n != 2:
|
||||
raise ValidationError(f"'{op}' needs 2 args, got {n}")
|
||||
for a in node.args:
|
||||
if isinstance(a, Node):
|
||||
_validate_node(a, _expect_bool=False)
|
||||
_validate_node(a)
|
||||
return
|
||||
|
||||
if node.kind == "indicator":
|
||||
if len(node.args) < 1:
|
||||
raise ValidationError("'indicator' needs >=1 args (name [, params...])")
|
||||
name_node = node.args[0]
|
||||
ind_name = name_node.kind if isinstance(name_node, Node) else str(name_node)
|
||||
if ind_name not in KNOWN_INDICATORS:
|
||||
raise ValidationError(f"unknown indicator: {ind_name}")
|
||||
# Gli indicatori non accettano Node come params (no-nesting in Phase 1).
|
||||
for a in node.args[1:]:
|
||||
if isinstance(a, Node):
|
||||
raise ValidationError(
|
||||
f"indicator '{ind_name}' does not accept nested expressions; "
|
||||
f"only numeric literals (got node {a.kind})"
|
||||
)
|
||||
n_params = len(node.args) - 1
|
||||
min_p, max_p = INDICATOR_ARITY[ind_name]
|
||||
if not (min_p <= n_params <= max_p):
|
||||
raise ValidationError(
|
||||
f"indicator '{ind_name}' arity {n_params} out of [{min_p},{max_p}]"
|
||||
)
|
||||
if op in CROSSOVER_OPS:
|
||||
if n != 2:
|
||||
raise ValidationError(f"'{op}' needs 2 args, got {n}")
|
||||
for a in node.args:
|
||||
_validate_node(a)
|
||||
return
|
||||
|
||||
if node.kind == "feature":
|
||||
if len(node.args) != 1:
|
||||
raise ValidationError("'feature' needs 1 arg")
|
||||
feat_node = node.args[0]
|
||||
feat_name = feat_node.kind if isinstance(feat_node, Node) else str(feat_node)
|
||||
if feat_name not in KNOWN_FEATURES:
|
||||
raise ValidationError(f"unknown feature: {feat_name}")
|
||||
return
|
||||
raise ValidationError(f"unexpected op in expression: {op}")
|
||||
|
||||
raise ValidationError(f"unexpected node kind in expression: {node.kind}")
|
||||
|
||||
def _validate_indicator(node: IndicatorNode) -> None:
|
||||
if node.name not in KNOWN_INDICATORS:
|
||||
raise ValidationError(f"unknown indicator: {node.name}")
|
||||
n_params = len(node.params)
|
||||
min_p, max_p = INDICATOR_ARITY[node.name]
|
||||
if not (min_p <= n_params <= max_p):
|
||||
raise ValidationError(
|
||||
f"indicator '{node.name}' arity {n_params} out of [{min_p},{max_p}]"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user