Aggiunge hour/dow/is_weekend/minute_of_hour come FeatureNode nella grammatica esistente. Universal access (non passa da feature_access), riuso di FeatureNode (no nuovo tipo AST), few-shot examples nel prompt Hypothesis. Cinque file toccati, ~120 LOC, backward-compatible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Feature temporali nella grammatica Hypothesis — Design
Data: 11 maggio 2026
Status: design approvato dall'operatore, pronto per writing-plans
Scope target: Phase 2
Riferimenti: docs/decisions/2026-05-11-phase1-5-nemotron-run.md (memo che ha originato la discussione)
1. Motivazione
Le strategie LLM-generate da Phase 1 operano in modo time-blind: la grammatica espone solo OHLCV (open, high, low, close, volume) e indicatori tecnici (sma, rsi, atr, macd, realized_vol) calcolati sopra. Non esiste alcuna feature che permetta al genoma di condizionare il comportamento sull'orario o sul giorno della settimana.
Questo è un limite strutturale rispetto a BTC-PERPETUAL su Cerbero, dove esistono effetti temporali sistematici:
- apertura USA (14:30 UTC) e Europa (08:00 UTC) generano volatilità sistematica;
- apertura/chiusura settimanale crypto (Sabato/Domenica vs. resto della settimana) ha liquidità diversa e basis funding diverso;
- la sessione asiatica overnight presenta pattern di trend reversal noti.
Il design seguente aggiunge alla grammatica quattro feature temporali — hour, dow, is_weekend, minute_of_hour — universalmente accessibili a ogni genoma, lasciando inalterati i meccanismi di mutation/crossover esistenti.
2. Decisioni di design
Le seguenti scelte sono state ratificate in fase di brainstorming.
Quattro feature, non una. hour da sola coprirebbe l'80% dei casi, ma dow cattura un asse ortogonale (weekend effect) e is_weekend è una scorciatoia espressiva utile al LLM. minute_of_hour è incluso per disponibilità futura (timeframe 5m/15m in Phase 2+), inerte sui dati 1h attuali.
Accesso universale, non soggetto a feature_access. Le feature temporali sono sempre disponibili a ogni genoma, indipendentemente dal subset OHLCV randomizzato in ga/initial.py e mutato da mutate_feature_access. Motivo: vogliamo che ogni genoma possa testarle; passarle attraverso FEATURE_POOL rischia di lasciarle inutilizzate in metà della popolazione e vanificare l'esperimento. Il prompt indica esplicitamente che sono "sempre accessibili", separate dalla sezione {feature_access} del template.
Riuso di FeatureNode, niente nuovo tipo AST. Le feature temporali entrano nella stessa whitelist KNOWN_FEATURES di OHLCV e usano la stessa shape JSON {"kind": "feature", "name": "..."}. Il dispatcher in compiler.py discrimina per nome. Alternativa scartata: introdurre TimeFeatureNode separato. Avrebbe dato type-safety formale ma richiesto modifiche a parser, validator, JSON shape, prompt — costo eccessivo per beneficio puramente strutturale, dato che semanticamente "ora del giorno" e "prezzo close" sono entrambi attributi della riga.
Few-shot examples nel prompt. L'istruzione minimale (solo nomi) lascia troppo spazio a interpretazioni errate (es. dow=7 per domenica all'italiana, hour in fuso locale invece che UTC). Due esempi concreti — un gating intraday gt hour 14 AND lt hour 22, un gating settimanale eq is_weekend 1 — fissano la semantica al costo di ~200 token addizionali per call.
Out-of-range non è errore di validazione. Il LLM potrebbe emettere gt hour 25 o eq dow 7. Il validator non li intercetta: tecnicamente sono LiteralNode(value=...) numerici legali. La condizione sarà semplicemente sempre falsa e l'Adversarial layer (flat_too_long, no_trades) sanzionerà i genomi che ne sono dipendenti. Aggiungere un check range esplicito sarebbe over-engineering per un caso che il sistema già gestisce.
3. Architettura — modifiche file-by-file
Cinque file toccati. Nessun nuovo modulo.
src/multi_swarm/protocol/grammar.py
Estendere KNOWN_FEATURES da 5 a 9 nomi:
KNOWN_FEATURES: frozenset[str] = frozenset(
{"open", "high", "low", "close", "volume",
"hour", "dow", "is_weekend", "minute_of_hour"}
)
Nessun'altra modifica al file. Il validator legge da qui automaticamente.
src/multi_swarm/protocol/compiler.py
Aggiungere un dizionario di derivazioni temporali ed estendere il dispatcher di FeatureNode con un branch prioritario:
_TIME_FEATURE_FNS: dict[str, Callable[[pd.DatetimeIndex], pd.Series]] = {
"hour": lambda idx: pd.Series(idx.hour, index=idx, dtype="int64"),
"dow": lambda idx: pd.Series(idx.dayofweek, index=idx, dtype="int64"),
"is_weekend": lambda idx: pd.Series((idx.dayofweek >= 5).astype("int64"), index=idx),
"minute_of_hour": lambda idx: pd.Series(idx.minute, index=idx, dtype="int64"),
}
# nel branch FeatureNode di _eval_node:
if isinstance(node, FeatureNode):
if node.name in _TIME_FEATURE_FNS:
return _TIME_FEATURE_FNS[node.name](df.index)
return df[node.name]
Il branch OHLCV preesistente (return df[node.name]) resta invariato come fallback per i nomi non temporali. Si assume df.index di tipo DatetimeIndex UTC, già garantito da CerberoOHLCVLoader.
src/multi_swarm/agents/hypothesis.py
Aggiungere nel prompt template, dopo la sezione "Leaf - feature OHLCV" (intorno a riga 84), una sezione "Leaf - feature TEMPORALI" con i quattro nomi, i loro range, e due esempi few-shot completi (gating sessione US, gating weekend). Mantenere la sezione separata da {feature_access} e dichiarare esplicitamente che le feature temporali sono "sempre accessibili". Contenuto preciso definito nella sezione 5 di questo spec.
tests/protocol/test_compiler.py
Cinque test nuovi:
test_compile_hour_feature_returns_index_hour— DataFrame 24-bar con index orario,FeatureNode("hour")restituisce serie[0,1,...,23].test_compile_dow_feature_lunedi_is_zero— verifica convenzione pandas (lunedì → 0, domenica → 6).test_compile_is_weekend_returns_zero_one— sabato e domenica → 1, altri → 0.test_compile_minute_of_hour_zero_on_1h_timeframe— su index 1h tutti gli output sono 0 (test di regressione del comportamento atteso).test_rule_with_temporal_gating_compiles_and_executes— integrazione: regolaentry-long if hour > 14 AND close > sma(20), verifica cheSide.LONGappaia solo nelle bar conhour > 14.
tests/protocol/test_validator.py
Due test nuovi:
test_validator_accepts_temporal_features— i quattro nuovi nomi non sollevanoValidationError.test_validator_rejects_temporal_typo—FeatureNode("weekday")sollevaValidationError.
Test esistenti non devono cambiare. L'aggiunta è puramente additiva.
4. Contratto delle feature
| Feature | Tipo | Range | Derivazione pandas |
|---|---|---|---|
hour |
int64 | 0–23 | df.index.hour |
dow |
int64 | 0–6 (lun=0) | df.index.dayofweek |
is_weekend |
int64 | 0 o 1 | (df.index.dayofweek >= 5).astype(int) |
minute_of_hour |
int64 | 0–59 | df.index.minute |
L'indice del DataFrame è UTC tz-aware per costruzione (CerberoOHLCVLoader). I valori temporali sono quindi in UTC, non in fuso locale italiano. Questa scelta è coerente con la convenzione di prezzi e timestamp del progetto e con la natura globale del mercato crypto.
I confronti tipici emessi dal LLM saranno della forma {"op": "gt", "args": [{"kind": "feature", "name": "hour"}, {"kind": "literal", "value": 14}]}. Funzionano via broadcasting numpy senza modifiche a comparator o operator nodes.
5. Frammento di prompt aggiunto
Da inserire in hypothesis.py dopo l'attuale sezione "Leaf - feature OHLCV":
Leaf - feature TEMPORALI (sempre accessibili, UTC):
{{"kind": "feature", "name": "hour"}} range 0-23
{{"kind": "feature", "name": "dow"}} range 0-6 (lun=0, dom=6)
{{"kind": "feature", "name": "is_weekend"}} 0 o 1
{{"kind": "feature", "name": "minute_of_hour"}} range 0-59
Esempi di gating temporale:
// Solo durante la sessione US (14:00-22:00 UTC)
{{"op": "and", "args": [
{{"op": "gt", "args": [{{"kind": "feature", "name": "hour"}}, {{"kind": "literal", "value": 14}}]}},
{{"op": "lt", "args": [{{"kind": "feature", "name": "hour"}}, {{"kind": "literal", "value": 22}}]}}
]}}
// Solo nel weekend (sab+dom)
{{"op": "eq", "args": [{{"kind": "feature", "name": "is_weekend"}}, {{"kind": "literal", "value": 1}}]}}
Il blocco va inserito prima della frase corrente "Feature accessibili dal tuo genoma: {feature_access}", per chiarire che {feature_access} riguarda solo OHLCV mentre le temporali sono universali.
6. Backward compatibility e impatto sui run esistenti
Tutti i genomi esistenti nei runs.db storici (Phase 1, Phase 1.5 nemotron, Phase 1.5 grok in corso) usano solo feature OHLCV. Con la grammatica estesa restano validi: il validator continua ad accettarli, il compiler li gestisce nel branch OHLCV invariato.
Non c'è quindi alcuna migrazione di dati. I run vecchi possono essere ri-letti dalla dashboard senza modifiche. La distinzione "run pre/post feature temporali" sarà tracciata implicitamente dalla data del commit di merge.
7. Validazione end-to-end
Dopo il merge dei cinque file, la procedura di validazione è:
- Esecuzione test suite completa (
uv run pytest) — i 7 nuovi test devono passare, nessun test esistente deve rompersi. scripts/smoke_run.pyconpopulation_size=4, n_generations=1per verificare che il loop end-to-end completi (caricamento OHLCV → generazione genome → compile → backtest → DSR → adversarial → persistenza). Tempo atteso ~2 minuti.- Ispezione manuale di almeno 1 genoma generato post-merge: verificare che il LLM abbia effettivamente usato almeno una feature temporale tra le sue regole. Se in 4 genomi nessuno usa feature temporali, ri-esaminare il prompt.
Non è previsto un confronto ablation formale (con/senza feature temporali) in questo spec — è un'attività di Phase 2 separata che andrà pianificata in un proprio spec quando si avvierà il run di valutazione.
8. Out of scope
I seguenti elementi sono esplicitamente fuori dallo scope di questo spec e dovranno essere oggetto di design dedicato se desiderati:
- Feature temporali con segno periodico (es.
sin_hour,cos_dow): utili per regressioni continue, non per regole booleane GA-based. Skip. - Feature di sessione discreta (es.
session=us|europe|asia): derivabili componendohourcon comparator, non necessario aggiungere come feature primitiva. - Time-zone configurabile: rimane fissa UTC. Cambiare implica refactor del loader OHLCV.
- Validator range-check (es. rifiutare
gt(dow, 6)): sanzionato già dal loop GA via fitness e Adversarial. - Modifica del meccanismo
mutate_feature_access: invariato. Le feature temporali non entrano nel pool mutabile. - Indicatori temporali (es.
time_since_last_high): richiede stato persistente, fuori dal modello stateless attuale.
9. Stima di sforzo
Implementazione: ~120 LOC (60 di codice + 60 di test) in 5 file. Complessità bassa.
TDD-driven: scrivere prima i 7 test, verificare che falliscano, poi aggiungere whitelist + dispatcher + prompt. Tempo stimato: 2-3 ore di lavoro continuo, validation smoke run inclusa.
Costo prompt addizionale per call: ~200 token. Su un run da 200 call, ~40k token aggiuntivi → impatto economico trascurabile (<$0.05 con qualsiasi tier).