feat(protocol): aggiungi sma_pct + macd_pct per completare la famiglia *_pct
Estende il fix atr_pct (commitf875df3/9c3b5ad) coprendo anche SMA e MACD: protocol/compiler.py: - _ind_sma_pct(length) = (close - sma) / sma Deviazione frazionale del close dalla SMA. Range tipico ±0.1. NB: NON e' sma/close (sempre ~1.0, inutile per literal). Uso ideale: sma_pct(50) > 0.05 -> "close 5% sopra media a 50 barre" (mean reversion) - _ind_macd_pct(fast, slow, signal) = macd / close MACD come frazione del prezzo. Range tipico ±0.02. Uso ideale: macd_pct(12,26,9) > 0.005 -> "momentum > 0.5% del prezzo" protocol/grammar.py: KNOWN_INDICATORS estesa con sma_pct + macd_pct protocol/validator.py: arity (1,1) per sma_pct, (0,3) per macd_pct (come macd) agents/hypothesis.py (system prompt LLM): - Lista indicatori include sma_pct e macd_pct con annotazioni unita' - Esempi corretti/errati estesi: sma_pct > 0.05, macd_pct > 0.005 - Pattern guidance: "Mean reversion: sma_pct(long) > 0.05" e "Momentum positivo conferma: macd_pct(12,26,9) > 0.005" genome/mutation_prompt_llm.py: keyword whitelist estesa con sma_pct + macd_pct Tests (+3): - test_sma_pct_is_close_deviation_from_sma: identita' algebrica + sign - test_macd_pct_is_macd_divided_by_close: identita' + scala (rapporto ~close) - test_sma_pct_and_macd_pct_in_validator: regression validator Verifica: 191 pass (era 188). Closes [[protocol_unit_bug]] in full. Family *_pct ora completa per atr/sma/macd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Decisione: indicatore `atr_pct` per fix bug protocollo unità
|
||||
# Decisione: indicatori `*_pct` per fix bug protocollo unità
|
||||
|
||||
**Data:** 2026-05-15
|
||||
**Status:** Implementato
|
||||
**Scope:** `multi_swarm_core.protocol.{compiler,grammar,validator}` + `strategy_crypto/strategies/eth_*.json`
|
||||
**Status:** Implementato (atr_pct + sma_pct + macd_pct + prompt LLM aggiornato)
|
||||
**Scope:** `multi_swarm_core.protocol.{compiler,grammar,validator}` + `agents/hypothesis.py` + `strategy_crypto/strategies/eth_*.json`
|
||||
|
||||
## Contesto
|
||||
|
||||
@@ -73,12 +73,25 @@ _ind_atr_pct(df, 14).mean() # ~0.011 (1.1% del prezzo)
|
||||
|
||||
`atr > 0.02` → 500/500 (broken). `atr_pct > 0.02` → 0/500 (real signal).
|
||||
|
||||
## Estensione: sma_pct + macd_pct (2026-05-15)
|
||||
|
||||
Aggiunti anche `sma_pct` e `macd_pct` per coerenza famiglia `*_pct`:
|
||||
|
||||
- **`sma_pct(length) = (close - sma) / sma`** — deviazione frazionale del
|
||||
close dalla SMA. Range tipico ±0.1. NB: NON è `sma/close` (che sarebbe
|
||||
sempre ~1.0, inutile per literal). Uso: `sma_pct(50) > 0.05` significa
|
||||
"close 5% sopra la media a 50 barre" (mean reversion).
|
||||
- **`macd_pct(fast, slow, signal) = macd / close`** — MACD come frazione
|
||||
del prezzo. Range tipico ±0.02. Uso: `macd_pct > 0.005` significa
|
||||
"momentum > 0.5% del prezzo".
|
||||
|
||||
Prompt LLM aggiornato di conseguenza (`agents/hypothesis.py`):
|
||||
- Lista indicatori include `sma_pct` e `macd_pct` con annotazioni unità
|
||||
- Pattern guidance: "Mean reversion strutturale: sma_pct(long) > 0.05",
|
||||
"Momentum positivo conferma: macd_pct(12,26,9) > 0.005"
|
||||
- 3 nuovi test (sma_pct identity, macd_pct identity, validator integration)
|
||||
|
||||
## Open items
|
||||
|
||||
- ~~Aggiornare il system prompt LLM~~ ✅ **chiuso 2026-05-15**:
|
||||
`agents/hypothesis.py` ora elenca `atr_pct` con annotazione unità e
|
||||
include una sezione "UNITÀ — REGOLA CRITICA" che spiega esplicitamente
|
||||
al modello quando usare `atr_pct` (literal frazionali) vs `atr`
|
||||
(confronti relativi). `mutation_prompt_llm._VALID_KEYWORDS` esteso.
|
||||
- Considerare l'aggiunta di `sma_pct`, `macd_pct` se emergono usi
|
||||
analoghi in future strategie (still open).
|
||||
- ~~Aggiornare il system prompt LLM~~ ✅ chiuso 2026-05-15
|
||||
- ~~`sma_pct`, `macd_pct` se emergono usi analoghi~~ ✅ chiuso 2026-05-15
|
||||
|
||||
@@ -77,20 +77,29 @@ Crossover (eventi su 2 serie):
|
||||
|
||||
Leaf - indicatori (calcolati su close):
|
||||
{{"kind": "indicator", "name": "sma", "params": [<length>]}} // media mobile, UNITÀ PREZZO
|
||||
{{"kind": "indicator", "name": "sma_pct", "params": [<length>]}} // (close-sma)/sma, FRAZIONE ±0.1
|
||||
{{"kind": "indicator", "name": "rsi", "params": [<length>]}} // 0-100, adimensionale
|
||||
{{"kind": "indicator", "name": "atr", "params": [<length>]}} // true range, UNITÀ PREZZO
|
||||
{{"kind": "indicator", "name": "atr_pct", "params": [<length>]}} // atr/close, FRAZIONE 0.0-0.1
|
||||
{{"kind": "indicator", "name": "realized_vol", "params": [<window>]}} // std dei returns, FRAZIONE
|
||||
{{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}}
|
||||
// 0-3 numeri (tutti opzionali con default 12, 26, 9)
|
||||
{{"kind": "indicator", "name": "macd", "params": [<fast>, <slow>, <signal>]}} // UNITÀ PREZZO
|
||||
{{"kind": "indicator", "name": "macd_pct", "params": [<fast>, <slow>, <signal>]}} // macd/close, FRAZIONE ±0.02
|
||||
// params: 0-3 numeri (tutti opzionali, default 12, 26, 9)
|
||||
|
||||
UNITÀ — REGOLA CRITICA per i confronti con literal numerici:
|
||||
* Confronti con literal FRAZIONALI (0.01, 0.02, 0.05): usa `atr_pct`, `realized_vol`
|
||||
Esempio CORRETTO: `atr_pct(14) > 0.02` significa "ATR > 2% del prezzo"
|
||||
Esempio ERRATO: `atr(14) > 0.02` è sempre TRUE su asset $>1 (atr in dollari)
|
||||
* Confronti RELATIVI fra indicatori in stessa unità: usa `atr`, `sma`, `macd`
|
||||
Esempio: `atr(14) > sma(14)` (entrambi in dollari, confronto valido)
|
||||
* RSI usa literal 0-100 (mai frazione): `rsi(14) > 70`
|
||||
* Confronti con literal FRAZIONALI (0.01, 0.02, 0.05): usa le varianti _pct
|
||||
Esempi CORRETTI:
|
||||
`atr_pct(14) > 0.02` "ATR > 2% del prezzo" (volatilità alta)
|
||||
`sma_pct(50) > 0.05` "close 5% sopra SMA(50)" (deviazione media)
|
||||
`macd_pct(12,26,9) > 0.005` "momentum > 0.5% del prezzo"
|
||||
Esempi ERRATI (sempre TRUE/FALSE su crypto, dead branch):
|
||||
`atr(14) > 0.02` atr in dollari (~30 su ETH) >> 0.02
|
||||
`sma(50) > 0.02` sma in dollari (~3000) >> 0.02
|
||||
`macd > 0.02` macd in dollari, ordine ±10
|
||||
* Confronti RELATIVI fra indicatori in stessa unità: usa nomi senza _pct
|
||||
Esempi: `atr(14) > sma(14)` (entrambi $), `sma(50) > sma(200)` (golden cross)
|
||||
`close > sma(50)` (entrambi $) — preferito su `sma_pct(50) > 0` (equivalente)
|
||||
* RSI usa literal 0-100 (mai frazione): `rsi(14) > 70`, `rsi(14) < 30`
|
||||
|
||||
Leaf - feature OHLCV:
|
||||
{{"kind": "feature", "name": "open|high|low|close|volume"}}
|
||||
@@ -121,7 +130,8 @@ PATTERN GUIDANCE (oltre agli indicatori, considera forma delle curve e ripetibil
|
||||
- Trend discendente: SMA(short) < SMA(long) E close < SMA(short)
|
||||
- Compressione di volatilità (pre-breakout): atr_pct(N) < 0.01 (sotto 1% del prezzo)
|
||||
- Espansione di volatilità: atr_pct(N) > 0.03 (sopra 3%) OPPURE ATR(N) > ATR(N*2) confronto relativo
|
||||
- Mean reversion strutturale: |close - SMA(long)| eccessivo → reversal atteso
|
||||
- Mean reversion strutturale: sma_pct(long) > 0.05 (close 5% sopra media) OR sma_pct(long) < -0.05
|
||||
- Momentum positivo conferma: macd_pct(12,26,9) > 0.005 (> 0.5% del prezzo)
|
||||
|
||||
Ripetibilità dell'andamento:
|
||||
- Eventi crossover/crossunder ricorrenti (golden/death cross, RSI cross zone)
|
||||
|
||||
@@ -51,9 +51,9 @@ MUTATION_INSTRUCTIONS: dict[str, str] = {
|
||||
|
||||
# Keyword tecniche minime per validare che il prompt sia ancora "una strategia".
|
||||
_VALID_KEYWORDS = (
|
||||
"rsi", "sma", "ema", "atr", "atr_pct", "realized_vol",
|
||||
"rsi", "sma", "sma_pct", "ema", "atr", "atr_pct", "realized_vol",
|
||||
"momentum", "breakout", "mean", "reversion",
|
||||
"macd", "vwap", "bb", "bollinger", "stoch", "trend", "signal", "buy",
|
||||
"macd", "macd_pct", "vwap", "bb", "bollinger", "stoch", "trend", "signal", "buy",
|
||||
"sell", "long", "short", "entry", "exit", "stop", "rule", "condition",
|
||||
"if", "when", "and", "or", "gt", "lt", ">", "<", "ge", "le",
|
||||
"hour", "dow", "weekend", "indicator", "feature",
|
||||
|
||||
@@ -88,6 +88,29 @@ def _ind_atr_pct(df: pd.DataFrame, length: float) -> pd.Series:
|
||||
return _atr(df, int(length)) / df["close"]
|
||||
|
||||
|
||||
def _ind_sma_pct(df: pd.DataFrame, length: float) -> pd.Series:
|
||||
# Deviazione frazionale del close dalla SMA: (close - sma) / sma.
|
||||
# Range tipico +/- 0.1 (close +/- 10% dalla media). Uso ideale:
|
||||
# sma_pct(50) > 0.05 -> "close 5% sopra la media a 50 barre"
|
||||
# NB: non e' "sma/close" perche' quel valore (sempre ~1.0) e' inutile
|
||||
# per confronti con literal frazionali.
|
||||
sma = _sma(df["close"], int(length))
|
||||
return (df["close"] - sma) / sma
|
||||
|
||||
|
||||
def _ind_macd_pct(
|
||||
df: pd.DataFrame,
|
||||
fast: float = 12,
|
||||
slow: float = 26,
|
||||
signal: float = 9,
|
||||
) -> pd.Series:
|
||||
# MACD normalizzato come frazione del prezzo close: macd_value / close.
|
||||
# Range tipico +/- 0.02. Uso: `macd_pct > 0` (momentum positivo) o
|
||||
# `macd_pct > 0.005` (momentum positivo >= 0.5% del prezzo).
|
||||
macd = _ind_macd(df, fast, slow, signal)
|
||||
return macd / df["close"]
|
||||
|
||||
|
||||
def _ind_realized_vol(df: pd.DataFrame, window: float) -> pd.Series:
|
||||
return _realized_vol(df["close"], int(window))
|
||||
|
||||
@@ -109,11 +132,13 @@ def _ind_macd(
|
||||
# against this map.
|
||||
INDICATOR_FNS: dict[str, Any] = {
|
||||
"sma": _ind_sma,
|
||||
"sma_pct": _ind_sma_pct,
|
||||
"rsi": _ind_rsi,
|
||||
"atr": _ind_atr,
|
||||
"atr_pct": _ind_atr_pct,
|
||||
"realized_vol": _ind_realized_vol,
|
||||
"macd": _ind_macd,
|
||||
"macd_pct": _ind_macd_pct,
|
||||
}
|
||||
|
||||
_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = {
|
||||
|
||||
@@ -17,7 +17,7 @@ ACTION_VALUES: frozenset[str] = frozenset(
|
||||
KIND_VALUES: frozenset[str] = frozenset({"indicator", "feature", "literal"})
|
||||
|
||||
KNOWN_INDICATORS: frozenset[str] = frozenset(
|
||||
{"sma", "rsi", "atr", "atr_pct", "macd", "realized_vol"}
|
||||
{"sma", "sma_pct", "rsi", "atr", "atr_pct", "macd", "macd_pct", "realized_vol"}
|
||||
)
|
||||
KNOWN_FEATURES: frozenset[str] = frozenset(
|
||||
{"open", "high", "low", "close", "volume",
|
||||
|
||||
@@ -31,12 +31,14 @@ from .parser import (
|
||||
# Numero di parametri numerici accettati dopo il nome dell'indicatore.
|
||||
# (min, max) sui soli numeri. Indicatori non sono annidabili in Phase 1.
|
||||
INDICATOR_ARITY: dict[str, tuple[int, int]] = {
|
||||
"sma": (1, 1), # length
|
||||
"sma": (1, 1), # length (assoluto, unita' prezzo)
|
||||
"sma_pct": (1, 1), # length: (close - sma)/sma, deviazione frazionale
|
||||
"rsi": (1, 1), # length
|
||||
"atr": (1, 1), # length (assoluto, unita' prezzo)
|
||||
"atr_pct": (1, 1), # length (frazione del close, per confronti con literal)
|
||||
"realized_vol": (1, 1), # window
|
||||
"macd": (0, 3), # fast, slow, signal (tutti opzionali)
|
||||
"macd_pct": (0, 3), # macd/close, frazionale (per confronti con literal)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -309,3 +309,84 @@ def test_atr_pct_in_strategy_eval(ohlcv: pd.DataFrame) -> None:
|
||||
# senza dead-branch (qualche match e' possibile in warmup).
|
||||
non_warmup = signal.iloc[30:]
|
||||
assert (non_warmup == Side.FLAT).all() or (non_warmup == Side.LONG).any()
|
||||
|
||||
|
||||
def test_sma_pct_is_close_deviation_from_sma() -> None:
|
||||
"""sma_pct = (close - sma) / sma: deviazione frazionale del close dalla SMA."""
|
||||
from multi_swarm_core.protocol.compiler import _ind_sma, _ind_sma_pct
|
||||
|
||||
idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC")
|
||||
# Prezzo che cresce poi torna: sma_pct passa da +qualcosa a -qualcosa
|
||||
close = np.concatenate([np.linspace(100, 120, 50), np.linspace(120, 95, 50)])
|
||||
df = pd.DataFrame(
|
||||
{"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0},
|
||||
index=idx,
|
||||
)
|
||||
|
||||
sma = _ind_sma(df, 20)
|
||||
sma_pct = _ind_sma_pct(df, 20)
|
||||
expected = (df["close"] - sma) / sma
|
||||
assert np.allclose(sma_pct.dropna(), expected.dropna())
|
||||
# In salita: close > sma -> sma_pct positivo
|
||||
assert (sma_pct.iloc[30:50] > 0).any()
|
||||
# In discesa estesa: close < sma -> sma_pct negativo
|
||||
assert (sma_pct.iloc[80:] < 0).any()
|
||||
|
||||
|
||||
def test_macd_pct_is_macd_divided_by_close() -> None:
|
||||
"""macd_pct = macd / close: momentum normalizzato al prezzo."""
|
||||
from multi_swarm_core.protocol.compiler import _ind_macd, _ind_macd_pct
|
||||
|
||||
idx = pd.date_range("2024-01-01", periods=300, freq="1h", tz="UTC")
|
||||
# Random walk realistico su crypto (~3000 USDT, vol ~30/bar): MACD ha
|
||||
# ampiezza non trascurabile vs trend lineare puro.
|
||||
rng = np.random.default_rng(42)
|
||||
close = 3000.0 + np.cumsum(rng.standard_normal(300) * 30)
|
||||
df = pd.DataFrame(
|
||||
{"open": close, "high": close + 5, "low": close - 5, "close": close, "volume": 1.0},
|
||||
index=idx,
|
||||
)
|
||||
|
||||
macd = _ind_macd(df, 12, 26, 9)
|
||||
macd_pct = _ind_macd_pct(df, 12, 26, 9)
|
||||
# Identita' algebrica: macd_pct == macd / close
|
||||
assert np.allclose(macd_pct.dropna(), (macd / df["close"]).dropna())
|
||||
# macd_pct ha scala << 1 (frazione del prezzo, ordine 1e-3)
|
||||
assert macd_pct.abs().mean() < 0.05
|
||||
# macd assoluto e' >> macd_pct (rapporto = close ~3000)
|
||||
ratio = macd.abs().mean() / max(macd_pct.abs().mean(), 1e-12)
|
||||
assert 1000 < ratio < 5000
|
||||
|
||||
|
||||
def test_sma_pct_and_macd_pct_in_validator() -> None:
|
||||
"""Regression: i nuovi indicatori sono accettati dal validator."""
|
||||
from multi_swarm_core.protocol.validator import validate_strategy
|
||||
|
||||
spec = {
|
||||
"rules": [
|
||||
{
|
||||
"condition": {
|
||||
"op": "and",
|
||||
"args": [
|
||||
{
|
||||
"op": "gt",
|
||||
"args": [
|
||||
{"kind": "indicator", "name": "sma_pct", "params": [50]},
|
||||
{"kind": "literal", "value": 0.05},
|
||||
],
|
||||
},
|
||||
{
|
||||
"op": "gt",
|
||||
"args": [
|
||||
{"kind": "indicator", "name": "macd_pct", "params": [12, 26, 9]},
|
||||
{"kind": "literal", "value": 0.005},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"action": "entry-long",
|
||||
}
|
||||
]
|
||||
}
|
||||
strat = parse_strategy(json.dumps(spec))
|
||||
validate_strategy(strat) # no exception
|
||||
|
||||
Reference in New Issue
Block a user