feat(prompt_library): v3.2 custom_indicators_spec — fix LLM parse rate on Pythagoras indicators

This commit is contained in:
Adriano Dal Pastro
2026-05-20 10:00:31 +00:00
parent 68e0b009e9
commit c6bf0f31cc
5 changed files with 89 additions and 1 deletions
@@ -181,6 +181,12 @@ def _build_system_prompt(lib: PromptLibrary, genome: HypothesisAgentGenome) -> s
parts.append("") parts.append("")
# 3. Grammar spec (core scaffold) # 3. Grammar spec (core scaffold)
parts.append(_SYSTEM_GRAMMAR_SPEC) parts.append(_SYSTEM_GRAMMAR_SPEC)
# 3b. Custom indicators spec (da prompts.json, opzionale) - estende la lista
# "Leaf - indicatori" con firme strategy-specific non note al core.
if lib.custom_indicators_spec:
parts.append("\nINDICATORI CUSTOM (firme aggiuntive, applica le stesse regole grammaticali):\n")
parts.append(lib.custom_indicators_spec)
parts.append("")
# 4. Pattern guidance (da prompts.json, opzionale) # 4. Pattern guidance (da prompts.json, opzionale)
if lib.pattern_guidance: if lib.pattern_guidance:
parts.append( parts.append(
@@ -101,6 +101,10 @@ class PromptLibrary:
domain_warnings: str = field(default="") # opzionale: warning di dominio (es. crypto 24/7) domain_warnings: str = field(default="") # opzionale: warning di dominio (es. crypto 24/7)
anti_patterns: str = field(default="") # NEW v3.1: lista esplicita di pattern da evitare anti_patterns: str = field(default="") # NEW v3.1: lista esplicita di pattern da evitare
output_priorities: str = field(default="") # NEW v3.1: trade-off espliciti di output output_priorities: str = field(default="") # NEW v3.1: trade-off espliciti di output
# NEW v3.2: firme formali (arity + range) di indicatori strategy-specific
# non noti al core. Iniettato in _SYSTEM_GRAMMAR_SPEC come estensione della
# lista "Leaf - indicatori" per consentire alla LLM di chiamarli correttamente.
custom_indicators_spec: str = field(default="")
def __post_init__(self) -> None: def __post_init__(self) -> None:
if not self.styles: if not self.styles:
@@ -120,6 +124,7 @@ class PromptLibrary:
("domain_warnings", self.domain_warnings), ("domain_warnings", self.domain_warnings),
("anti_patterns", self.anti_patterns), ("anti_patterns", self.anti_patterns),
("output_priorities", self.output_priorities), ("output_priorities", self.output_priorities),
("custom_indicators_spec", self.custom_indicators_spec),
): ):
if not isinstance(value, str): if not isinstance(value, str):
raise PromptLibraryError( raise PromptLibraryError(
@@ -142,6 +147,7 @@ class PromptLibrary:
domain_warnings="", domain_warnings="",
anti_patterns="", anti_patterns="",
output_priorities="", output_priorities="",
custom_indicators_spec="",
) )
@classmethod @classmethod
@@ -204,6 +210,12 @@ class PromptLibrary:
raise PromptLibraryError(f"anti_patterns deve essere stringa, non {type(anti_patterns_raw)}") raise PromptLibraryError(f"anti_patterns deve essere stringa, non {type(anti_patterns_raw)}")
if not isinstance(output_priorities_raw, str): if not isinstance(output_priorities_raw, str):
raise PromptLibraryError(f"output_priorities deve essere stringa, non {type(output_priorities_raw)}") raise PromptLibraryError(f"output_priorities deve essere stringa, non {type(output_priorities_raw)}")
# Parse new optional top-level field (v3.2)
custom_indicators_spec_raw = data.get("custom_indicators_spec", "")
if not isinstance(custom_indicators_spec_raw, str):
raise PromptLibraryError(
f"custom_indicators_spec deve essere stringa, non {type(custom_indicators_spec_raw)}"
)
return cls( return cls(
styles=styles, styles=styles,
@@ -214,6 +226,7 @@ class PromptLibrary:
domain_warnings=domain_warnings, domain_warnings=domain_warnings,
anti_patterns=anti_patterns_raw, anti_patterns=anti_patterns_raw,
output_priorities=output_priorities_raw, output_priorities=output_priorities_raw,
custom_indicators_spec=custom_indicators_spec_raw,
) )
@property @property
@@ -420,3 +420,48 @@ def test_build_system_prompt_skips_anti_patterns_and_priorities_when_empty(mocke
system_msg = call_kwargs["system"] system_msg = call_kwargs["system"]
assert "ANTI-PATTERN" not in system_msg assert "ANTI-PATTERN" not in system_msg
assert "PRIORITA' DI OUTPUT" not in system_msg assert "PRIORITA' DI OUTPUT" not in system_msg
def test_build_system_prompt_includes_custom_indicators_spec(mocker): # type: ignore[no-untyped-def]
"""custom_indicators_spec da PromptLibrary appare nel SYSTEM prompt con header."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
custom_indicators_spec="CUSTOM_IND_X",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
system_msg = call_kwargs["system"]
assert "INDICATORI CUSTOM" in system_msg
assert "CUSTOM_IND_X" in system_msg
def test_build_system_prompt_skips_custom_indicators_spec_when_empty(mocker): # type: ignore[no-untyped-def]
"""custom_indicators_spec='' -> sezione assente nel SYSTEM prompt."""
fake_llm = mocker.MagicMock()
fake_llm.complete.return_value = CompletionResult(
text=VALID_STRATEGY_JSON,
input_tokens=200,
output_tokens=80,
tier=ModelTier.C,
model="qwen",
)
lib = PromptLibrary(
styles={"physicist": "Pensa come un fisico."},
focus={},
custom_indicators_spec="",
)
agent = HypothesisAgent(llm=fake_llm, prompt_library=lib)
agent.propose(make_genome(), make_summary())
call_kwargs = fake_llm.complete.call_args.kwargs
system_msg = call_kwargs["system"]
assert "INDICATORI CUSTOM" not in system_msg
@@ -90,6 +90,28 @@ def test_from_json_loads_anti_patterns_and_output_priorities(tmp_path: Path) ->
assert lib.output_priorities == "Robustezza > ottimalita." assert lib.output_priorities == "Robustezza > ottimalita."
def test_from_json_loads_custom_indicators_spec(tmp_path: Path) -> None:
"""from_json() legge custom_indicators_spec (v3.2): firme di indicatori strategy-specific."""
data = {
"styles": {"physicist": {"directive": "Cerca leggi conservative."}},
"custom_indicators_spec": (
"candle_pattern: params=[length, sym0, sym1, ...], length in [3,12], "
"sym in {0=U,1=D,2=doji}\n"
"pythagorean_ratio: params=[lookback], lookback in [12,200]"
),
}
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
assert "candle_pattern" in lib.custom_indicators_spec
assert "lookback in [12,200]" in lib.custom_indicators_spec
def test_from_json_defaults_custom_indicators_spec_when_absent(tmp_path: Path) -> None:
"""custom_indicators_spec assente -> default stringa vuota."""
data = {"styles": {"physicist": {"directive": "Cerca leggi conservative."}}}
lib = PromptLibrary.from_json(_write_json(data, tmp_path))
assert lib.custom_indicators_spec == ""
def test_strategy_crypto_directives_ascii_safe() -> None: def test_strategy_crypto_directives_ascii_safe() -> None:
"""REGRESSION GUARD: nessuna directive contiene caratteri > U+007F. """REGRESSION GUARD: nessuna directive contiene caratteri > U+007F.
@@ -4,7 +4,9 @@
"_changelog": "v1.0 - Initial release. Schema clonato da strategy_crypto v3.2 con contenuto Pythagoras-aligned.", "_changelog": "v1.0 - Initial release. Schema clonato da strategy_crypto v3.2 con contenuto Pythagoras-aligned.",
"_focus_metrics_design": "Le focus_metrics sono ENFASI per la lente, non filtri. 4 per stile. Devono includere almeno 1 dei 3 indicatori Pythagoras (candle_pattern, pythagorean_ratio, fractal_mirror).", "_focus_metrics_design": "Le focus_metrics sono ENFASI per la lente, non filtri. 4 per stile. Devono includere almeno 1 dei 3 indicatori Pythagoras (candle_pattern, pythagorean_ratio, fractal_mirror).",
"_design_invariants": "(1) ASCII-safe; (2) Archetipo dominante: <metafora> come ancora; (3) Lookback range esplicito; (4) Prima frase 'Il mercato e ...'; (5) Lunghezza directive 800-950 char.", "_design_invariants": "(1) ASCII-safe; (2) Archetipo dominante: <metafora> come ancora; (3) Lookback range esplicito; (4) Prima frase 'Il mercato e ...'; (5) Lunghezza directive 800-950 char.",
"_param_encoding_note": "Vincolo grammar: tutti i params sono float. candle_pattern: [length, sym0, sym1, ...] (sym: 0=U up close>open, 1=D down close<open, 2=doji). pythagorean_ratio: [lookback]. fractal_mirror: [k, axis_int] (0=h temporale, 1=v prezzo).", "_param_encoding_note": "Promosso a campo top-level 'custom_indicators_spec' (v3.2). Vedi sotto.",
"custom_indicators_spec": "Indicatori Pythagoras (oltre a sma/sma_pct/rsi/atr/atr_pct/realized_vol/macd/macd_pct gia documentati sopra). REGOLA CRITICA: params accetta SOLO numeri float, MAI stringhe e MAI altri nodi.\n\n {\"kind\": \"indicator\", \"name\": \"candle_pattern\", \"params\": [length, sym0, sym1, ..., sym_{length-1}]}\n length: int in [3,12] (numero di candele consecutive che devono matchare)\n sym_i: int in {0, 1, 2} (0=U up close>open, 1=D down close<open, 2=doji)\n Esempio: pattern U-D-U di 3 candele = params=[3, 0, 1, 0]\n Esempio: pattern D-D-D-U (reversal classico) = params=[4, 1, 1, 1, 0]\n Output: 1.0 se le ultime length candele matchano, 0.0 altrimenti. Confronta solo con literal 0.0 o 1.0.\n\n {\"kind\": \"indicator\", \"name\": \"pythagorean_ratio\", \"params\": [lookback]}\n lookback: int in [12,200] (finestra rolling per max/min close)\n Esempio: ratio su finestra 89 (Fibonacci) = params=[89]\n Output: max(close[-lookback:]) / min(close[-lookback:]), adimensionale >= 1.0.\n Confronta con literal vicini a phi=1.618, 1/phi=0.618 (in realta usa 1.618 perche ratio >= 1), sqrt2=1.414, pi/2=1.571, e=2.718.\n\n {\"kind\": \"indicator\", \"name\": \"fractal_mirror\", \"params\": [k, axis_int]}\n k: int in [3,12] (lunghezza finestra di confronto)\n axis_int: int in {0, 1} (0=h mirror temporale, 1=v mirror prezzo)\n Esempio: mirror temporale su 8 candele = params=[8, 0]\n Esempio: mirror prezzo su 6 candele = params=[6, 1]\n Output: correlation di Pearson in [-1.0, 1.0] tra finestra e suo mirror. Confronta con literal in (-1, 1), tipicamente |0.5|-|0.8|.\n\nESEMPI di confronti CORRETTI:\n candle_pattern di 3 candele U-D-U attiva: {\"op\": \"eq\", \"args\": [{\"kind\":\"indicator\",\"name\":\"candle_pattern\",\"params\":[3,0,1,0]}, {\"kind\":\"literal\",\"value\":1.0}]}\n ratio phi entro 0.5%: {\"op\": \"and\", \"args\": [{\"op\":\"gt\",\"args\":[{\"kind\":\"indicator\",\"name\":\"pythagorean_ratio\",\"params\":[89]},{\"kind\":\"literal\",\"value\":1.610}]}, {\"op\":\"lt\",\"args\":[{\"kind\":\"indicator\",\"name\":\"pythagorean_ratio\",\"params\":[89]},{\"kind\":\"literal\",\"value\":1.626}]}]}\n mirror temporale forte: {\"op\": \"gt\", \"args\": [{\"kind\":\"indicator\",\"name\":\"fractal_mirror\",\"params\":[8,0]}, {\"kind\":\"literal\",\"value\":0.7}]}\n\nESEMPI ERRATI (dead branch o validation error):\n candle_pattern con stringa: params=[\"UDU\"] (errato: params devono essere numeri)\n candle_pattern: params=[3, \"U\", \"D\", \"U\"] (errato: usa codici 0/1/2)\n pythagorean_ratio: params=[\"close\"] (errato: params devono essere numeri, non nomi feature)\n pythagorean_ratio: params=[5] (errato: lookback < 12)\n pythagorean_ratio: params=[89, 1.618] (errato: arity 1, non 2)\n fractal_mirror: params=[0, 1] (errato: k < 3)\n fractal_mirror: params=[100, 0] (errato: k > 12)\n fractal_mirror: params=[8] (errato: arity 2, manca axis_int)",
"agent_role": "Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm coevolutivo che cerca pattern frattali ricorrenti sui mercati crypto secondo il framework Pythagoras-Malanga (frattali H-C, trasformata di Fourier, geometria Evideon). Sei parte di una popolazione che esplora collettivamente lo spazio dei pattern: la diversita delle ipotesi e un asset critico. Preferisci esplorare territori meno ovvi rispetto a quelli che la tua lente cognitiva renderebbe predicibili. La strategia che produci deve essere riconoscibile come emanata dal tuo stile.", "agent_role": "Sei un agente generatore di ipotesi di trading quantitativo per un sistema swarm coevolutivo che cerca pattern frattali ricorrenti sui mercati crypto secondo il framework Pythagoras-Malanga (frattali H-C, trasformata di Fourier, geometria Evideon). Sei parte di una popolazione che esplora collettivamente lo spazio dei pattern: la diversita delle ipotesi e un asset critico. Preferisci esplorare territori meno ovvi rispetto a quelli che la tua lente cognitiva renderebbe predicibili. La strategia che produci deve essere riconoscibile come emanata dal tuo stile.",