docs: design spec feature temporali Phase 2
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>
This commit is contained in:
@@ -0,0 +1,183 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_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:
|
||||||
|
|
||||||
|
1. `test_compile_hour_feature_returns_index_hour` — DataFrame 24-bar con index orario, `FeatureNode("hour")` restituisce serie `[0,1,...,23]`.
|
||||||
|
2. `test_compile_dow_feature_lunedi_is_zero` — verifica convenzione pandas (lunedì → 0, domenica → 6).
|
||||||
|
3. `test_compile_is_weekend_returns_zero_one` — sabato e domenica → 1, altri → 0.
|
||||||
|
4. `test_compile_minute_of_hour_zero_on_1h_timeframe` — su index 1h tutti gli output sono 0 (test di regressione del comportamento atteso).
|
||||||
|
5. `test_rule_with_temporal_gating_compiles_and_executes` — integrazione: regola `entry-long if hour > 14 AND close > sma(20)`, verifica che `Side.LONG` appaia solo nelle bar con `hour > 14`.
|
||||||
|
|
||||||
|
### `tests/protocol/test_validator.py`
|
||||||
|
|
||||||
|
Due test nuovi:
|
||||||
|
|
||||||
|
1. `test_validator_accepts_temporal_features` — i quattro nuovi nomi non sollevano `ValidationError`.
|
||||||
|
2. `test_validator_rejects_temporal_typo` — `FeatureNode("weekday")` solleva `ValidationError`.
|
||||||
|
|
||||||
|
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":
|
||||||
|
|
||||||
|
```text
|
||||||
|
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 è:
|
||||||
|
|
||||||
|
1. Esecuzione test suite completa (`uv run pytest`) — i 7 nuovi test devono passare, nessun test esistente deve rompersi.
|
||||||
|
2. `scripts/smoke_run.py` con `population_size=4, n_generations=1` per verificare che il loop end-to-end completi (caricamento OHLCV → generazione genome → compile → backtest → DSR → adversarial → persistenza). Tempo atteso ~2 minuti.
|
||||||
|
3. 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 componendo `hour` con 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).
|
||||||
Reference in New Issue
Block a user