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:
@@ -35,42 +35,76 @@ Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm
|
||||
Il tuo stile cognitivo: {cognitive_style}
|
||||
Direttiva personale: {system_prompt}
|
||||
|
||||
Devi proporre una strategia di trading espressa nel linguaggio S-expression
|
||||
con i seguenti verbi disponibili:
|
||||
Devi proporre una strategia di trading espressa in JSON STRETTO.
|
||||
La risposta deve essere un singolo oggetto JSON dentro fence ```json...```
|
||||
con questa shape:
|
||||
|
||||
Azioni: entry-long, entry-short, exit, flat
|
||||
Logici: and, or, not
|
||||
Comparatori: gt, lt, eq
|
||||
Dati: feature, indicator, crossover, crossunder
|
||||
```json
|
||||
{{
|
||||
"rules": [
|
||||
{{"condition": <nodo>, "action": "entry-long|entry-short|exit|flat"}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
Indicatori disponibili (calcolati implicitamente sul prezzo close):
|
||||
sma <length>, rsi <length>, atr <length>, macd, realized_vol <window>.
|
||||
Feature disponibili: open, high, low, close, volume.
|
||||
NODI DISPONIBILI
|
||||
|
||||
REGOLE STRETTE DI SINTASSI:
|
||||
- (indicator <name> <args...>) restituisce una serie numerica. Es.
|
||||
(indicator rsi 14), (indicator sma 50), (indicator macd 12 26 9).
|
||||
- (feature <name>) restituisce la colonna OHLCV. Es. (feature close).
|
||||
- Gli indicatori NON sono annidabili: NON puoi scrivere
|
||||
(sma (indicator realized_vol 30) 150) o (indicator rsi (feature high) 14).
|
||||
Le funzioni sma/rsi/etc. ESISTONO SOLO come argomenti di indicator,
|
||||
non sono verbi indipendenti.
|
||||
- Costanti numeriche (es. 70.0, 30, 0.02) sono valide come 2° operando di gt/lt/eq.
|
||||
- crossover/crossunder accettano due espressioni-serie:
|
||||
(crossover (feature close) (indicator sma 20)) — corretto.
|
||||
(crossover (sma close 20) (sma close 50)) — ERRATO (sma non è verbo).
|
||||
Operatori logici:
|
||||
{{"op": "and", "args": [<nodo>, <nodo>, ...]}} // >=2 nodi
|
||||
{{"op": "or", "args": [<nodo>, <nodo>, ...]}} // >=2 nodi
|
||||
{{"op": "not", "args": [<nodo>]}} // 1 nodo
|
||||
|
||||
Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp.
|
||||
La default action se nessuna regola matcha è 'flat'.
|
||||
Comparatori (ritornano boolean series):
|
||||
{{"op": "gt", "args": [<a>, <b>]}} // a > b
|
||||
{{"op": "lt", "args": [<a>, <b>]}} // a < b
|
||||
{{"op": "eq", "args": [<a>, <b>]}} // a == b
|
||||
|
||||
Rispondi SOLO con la S-expression in un fence ```lisp ... ```, senza prosa,
|
||||
senza spiegazioni. Esempio formato:
|
||||
Crossover (eventi su 2 serie):
|
||||
{{"op": "crossover", "args": [<serie_a>, <serie_b>]}}
|
||||
{{"op": "crossunder", "args": [<serie_a>, <serie_b>]}}
|
||||
|
||||
```lisp
|
||||
(strategy
|
||||
(when (gt (indicator rsi 14) 70.0) (entry-short))
|
||||
(when (lt (indicator rsi 14) 30.0) (entry-long))
|
||||
(when (crossover (feature close) (indicator sma 50)) (entry-long)))
|
||||
Leaf - indicatori (calcolati su close):
|
||||
{{"kind": "indicator", "name": "sma", "params": [<length>]}}
|
||||
{{"kind": "indicator", "name": "rsi", "params": [<length>]}}
|
||||
{{"kind": "indicator", "name": "atr", "params": [<length>]}}
|
||||
{{"kind": "indicator", "name": "realized_vol", "params": [<window>]}}
|
||||
{{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}}
|
||||
// 0-3 numeri (tutti opzionali con default 12, 26, 9)
|
||||
|
||||
Leaf - feature OHLCV:
|
||||
{{"kind": "feature", "name": "open|high|low|close|volume"}}
|
||||
|
||||
Leaf - letterale numerico:
|
||||
{{"kind": "literal", "value": 70.0}}
|
||||
|
||||
VINCOLI
|
||||
- Gli indicator NON sono annidabili: 'params' accetta solo numeri, mai altri nodi.
|
||||
- Le regole sono valutate in ordine; la prima che matcha vince per ogni timestamp.
|
||||
- Default action se nessuna regola matcha = flat.
|
||||
- 'op' e 'kind' sono mutuamente esclusivi sullo stesso nodo.
|
||||
|
||||
Rispondi SOLO con il fence ```json...``` contenente l'oggetto strategy.
|
||||
Esempio:
|
||||
|
||||
```json
|
||||
{{
|
||||
"rules": [
|
||||
{{
|
||||
"condition": {{"op": "gt", "args": [
|
||||
{{"kind": "indicator", "name": "rsi", "params": [14]}},
|
||||
{{"kind": "literal", "value": 70.0}}
|
||||
]}},
|
||||
"action": "entry-short"
|
||||
}},
|
||||
{{
|
||||
"condition": {{"op": "lt", "args": [
|
||||
{{"kind": "indicator", "name": "rsi", "params": [14]}},
|
||||
{{"kind": "literal", "value": 30.0}}
|
||||
]}},
|
||||
"action": "entry-long"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -79,7 +113,7 @@ USER_TEMPLATE = """\
|
||||
Mercato: {symbol} timeframe {timeframe}, {n_bars} barre osservate.
|
||||
Statistiche return: mean={return_mean:.5f}, std={return_std:.5f}, \
|
||||
skew={skew:.3f}, kurt={kurtosis:.3f}.
|
||||
Regime volatilità: {volatility_regime}.
|
||||
Regime volatilità : {volatility_regime}.
|
||||
|
||||
Feature accessibili dal tuo genoma: {feature_access}.
|
||||
Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre.
|
||||
@@ -88,19 +122,57 @@ Genera una strategia che cerchi anomalie sfruttabili in questo regime.
|
||||
"""
|
||||
|
||||
|
||||
_SEXP_FENCE_RE = re.compile(
|
||||
r"```(?:lisp|scheme|sexp)?\s*(\(strategy[\s\S]*?\))\s*```",
|
||||
_JSON_FENCE_RE = re.compile(
|
||||
r"```(?:json)?\s*(\{[\s\S]*\})\s*```",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def _extract_sexp(text: str) -> str | None:
|
||||
m = _SEXP_FENCE_RE.search(text)
|
||||
def _balance_braces(s: str) -> str | None:
|
||||
"""Ritorna il prefix di ``s`` che chiude la prima ``{`` con bilanciamento.
|
||||
|
||||
Usato come fallback quando l'LLM ritorna JSON top-level senza fence ma
|
||||
seguito da prosa: troviamo dove finisce il primo oggetto e tagliamo.
|
||||
"""
|
||||
if not s.startswith("{"):
|
||||
return None
|
||||
depth = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
for i, ch in enumerate(s):
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif ch == "\\":
|
||||
escape = True
|
||||
elif ch == '"':
|
||||
in_string = False
|
||||
continue
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
elif ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return s[: i + 1]
|
||||
return None
|
||||
|
||||
|
||||
def _extract_json(text: str) -> str | None:
|
||||
"""Estrai un oggetto JSON dal testo del completion.
|
||||
|
||||
Strategie di estrazione, in ordine:
|
||||
1. Fence ```json...``` (greedy: cattura fino all'ultimo ``}`` prima della
|
||||
chiusura del fence).
|
||||
2. Testo che inizia direttamente con ``{`` (dopo strip), bilanciato a
|
||||
livello di parentesi graffe.
|
||||
"""
|
||||
m = _JSON_FENCE_RE.search(text)
|
||||
if m:
|
||||
return m.group(1)
|
||||
if text.strip().startswith("(strategy"):
|
||||
return text.strip()
|
||||
return None
|
||||
stripped = text.strip()
|
||||
return _balance_braces(stripped)
|
||||
|
||||
|
||||
class HypothesisAgent:
|
||||
@@ -131,16 +203,16 @@ class HypothesisAgent:
|
||||
|
||||
completion = self._llm.complete(genome, system=system, user=user)
|
||||
|
||||
sexp = _extract_sexp(completion.text)
|
||||
if sexp is None:
|
||||
payload = _extract_json(completion.text)
|
||||
if payload is None:
|
||||
return HypothesisProposal(
|
||||
strategy=None,
|
||||
raw_text=completion.text,
|
||||
completion=completion,
|
||||
parse_error="no s-expression found in output",
|
||||
parse_error="no JSON object found in output",
|
||||
)
|
||||
try:
|
||||
ast = parse_strategy(sexp)
|
||||
ast = parse_strategy(payload)
|
||||
validate_strategy(ast)
|
||||
return HypothesisProposal(
|
||||
strategy=ast,
|
||||
|
||||
Reference in New Issue
Block a user