9 Commits

Author SHA1 Message Date
Adriano 68637d1102 feat(hypothesis): aggiungi feature temporali al prompt con 2 esempi few-shot 2026-05-11 17:04:07 +02:00
Adriano 36cbfadb40 test(protocol): integration test gating temporale + sma 2026-05-11 17:03:05 +02:00
Adriano 2014ed3815 test(protocol): compiler semantica minute_of_hour su 1h 2026-05-11 17:02:02 +02:00
Adriano 22a934a6cf test(protocol): compiler semantica dow + is_weekend 2026-05-11 17:01:02 +02:00
Adriano 9d1f97cff3 feat(protocol): dispatcher temporal features (hour) in compiler 2026-05-11 16:59:26 +02:00
Adriano 0e9489bf88 test(protocol): parametrize temporal feature validator tests + float literals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:58:08 +02:00
Adriano 3e9a4efcc2 feat(protocol): extend KNOWN_FEATURES with temporal feature names 2026-05-11 16:56:36 +02:00
Adriano 30dbba4d74 docs: piano implementativo feature temporali
7 task TDD-driven: estensione grammar, dispatcher compiler per 4
feature temporali (hour/dow/is_weekend/minute_of_hour), aggiornamento
prompt Hypothesis con few-shot, smoke run end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:54:55 +02:00
Adriano c6cb32325e 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>
2026-05-11 16:52:28 +02:00
7 changed files with 869 additions and 1 deletions
@@ -0,0 +1,482 @@
# Feature temporali nella grammatica Hypothesis — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Aggiungere quattro feature temporali (`hour`, `dow`, `is_weekend`, `minute_of_hour`) alla grammatica delle strategie Hypothesis come `FeatureNode`, universalmente accessibili a ogni genoma e usabili con i comparator esistenti.
**Architecture:** Estensione puramente additiva. La whitelist `KNOWN_FEATURES` in `protocol/grammar.py` cresce da 5 a 9 nomi. Il dispatcher di `FeatureNode` in `protocol/compiler.py` acquisisce un branch prioritario che mappa i nomi temporali a serie derivate da `df.index` (DatetimeIndex UTC). Il prompt template di `agents/hypothesis.py` riceve due esempi few-shot. Nessuna modifica a parser, mutation/crossover, genome dataclass.
**Tech Stack:** Python 3.13, pandas (DatetimeIndex), pytest. Esecuzione via `uv run`. Repository: `/home/adriano/Documenti/Git_XYZ/Multi_Swarm_Coevolutive`.
**Spec di riferimento:** `docs/superpowers/specs/2026-05-11-temporal-features-design.md`
---
## File map
| File | Tipo | Responsabilità |
|------|------|----------------|
| `src/multi_swarm/protocol/grammar.py` | Modify | Estendere `KNOWN_FEATURES` |
| `src/multi_swarm/protocol/compiler.py` | Modify | Aggiungere `_TIME_FEATURE_FNS` + branch in `_eval_node` |
| `src/multi_swarm/agents/hypothesis.py` | Modify | Estendere prompt template con sezione feature temporali + 2 esempi |
| `tests/unit/test_protocol_validator.py` | Modify | +2 test (accept/reject) |
| `tests/unit/test_protocol_compiler.py` | Modify | +5 test (4 feature + 1 integrazione) |
---
## Task 1: Grammar extension + validator tests
**Files:**
- Modify: `src/multi_swarm/protocol/grammar.py:21-23`
- Modify: `tests/unit/test_protocol_validator.py` (append)
- [ ] **Step 1.1: Write failing test — validator accepts temporal features**
Append to `tests/unit/test_protocol_validator.py`:
```python
def test_validator_accepts_temporal_features() -> None:
for name in ("hour", "dow", "is_weekend", "minute_of_hour"):
src = _wrap(
{
"op": "gt",
"args": [
{"kind": "feature", "name": name},
{"kind": "literal", "value": 0},
],
}
)
ast = parse_strategy(src)
validate_strategy(ast) # no exception
def test_validator_rejects_temporal_typo() -> None:
src = _wrap(
{
"op": "gt",
"args": [
{"kind": "feature", "name": "weekday"},
{"kind": "literal", "value": 0},
],
}
)
ast = parse_strategy(src)
with pytest.raises(ValidationError, match="unknown feature"):
validate_strategy(ast)
```
- [ ] **Step 1.2: Run tests to verify they fail**
Run: `uv run pytest tests/unit/test_protocol_validator.py::test_validator_accepts_temporal_features tests/unit/test_protocol_validator.py::test_validator_rejects_temporal_typo -v`
Expected: First test FAILs with `ValidationError: unknown feature: hour`. Second test PASSes already (weekday is unknown today too).
- [ ] **Step 1.3: Extend `KNOWN_FEATURES` whitelist**
Edit `src/multi_swarm/protocol/grammar.py`, lines 21-23:
```python
KNOWN_FEATURES: frozenset[str] = frozenset(
{"open", "high", "low", "close", "volume",
"hour", "dow", "is_weekend", "minute_of_hour"}
)
```
- [ ] **Step 1.4: Run tests to verify both pass**
Run: `uv run pytest tests/unit/test_protocol_validator.py -v`
Expected: All tests PASS (both new tests + all pre-existing ones).
- [ ] **Step 1.5: Commit**
```bash
git add src/multi_swarm/protocol/grammar.py tests/unit/test_protocol_validator.py
git commit -m "feat(protocol): extend KNOWN_FEATURES with temporal feature names"
```
---
## Task 2: Compiler — `hour` feature
**Files:**
- Modify: `src/multi_swarm/protocol/compiler.py:135-137`
- Modify: `tests/unit/test_protocol_compiler.py` (append)
- [ ] **Step 2.1: Write failing test for `hour`**
Append to `tests/unit/test_protocol_compiler.py`:
```python
def test_compile_hour_feature_returns_index_hour(ohlcv: pd.DataFrame) -> None:
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "gt",
"args": [
{"kind": "feature", "name": "hour"},
{"kind": "literal", "value": -1},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
# Tutte le righe hanno hour >= 0 > -1, quindi tutte entry-long
assert (signal == Side.LONG).all()
```
- [ ] **Step 2.2: Run test to verify it fails**
Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_hour_feature_returns_index_hour -v`
Expected: FAIL with `KeyError: 'hour'` (df has no `hour` column, dispatcher falls into `df[name]`).
- [ ] **Step 2.3: Add `_TIME_FEATURE_FNS` and dispatcher branch**
Edit `src/multi_swarm/protocol/compiler.py`. Insert after line 108 (end of `INDICATOR_FNS`):
```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"),
}
```
Then modify `_eval_node` at line 135-137. Replace:
```python
def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series:
if isinstance(node, FeatureNode):
return df[node.name]
```
With:
```python
def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series:
if isinstance(node, FeatureNode):
if node.name in _TIME_FEATURE_FNS:
return _TIME_FEATURE_FNS[node.name](df.index)
return df[node.name]
```
- [ ] **Step 2.4: Run test to verify it passes**
Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_hour_feature_returns_index_hour -v`
Expected: PASS.
- [ ] **Step 2.5: Commit**
```bash
git add src/multi_swarm/protocol/compiler.py tests/unit/test_protocol_compiler.py
git commit -m "feat(protocol): dispatcher temporal features (hour) in compiler"
```
---
## Task 3: Compiler — `dow` and `is_weekend` tests
**Files:**
- Modify: `tests/unit/test_protocol_compiler.py` (append)
Nessuna modifica al sorgente: `_TIME_FEATURE_FNS` definito in Task 2 contiene già le quattro funzioni. Questi test verificano semantica e copertura.
- [ ] **Step 3.1: Add `dow` test**
Append to `tests/unit/test_protocol_compiler.py`:
```python
def test_compile_dow_feature_monday_is_zero(ohlcv: pd.DataFrame) -> None:
# 2024-01-01 e' un lunedi -> dow=0; gating eq dow 0 deve dare LONG su monday only.
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "eq",
"args": [
{"kind": "feature", "name": "dow"},
{"kind": "literal", "value": 0},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
# ohlcv fixture: 200h da 2024-01-01 00:00 UTC -> primo lunedi e' bar 0..23
monday_hours = signal[(signal.index.dayofweek == 0)]
other_hours = signal[(signal.index.dayofweek != 0)]
assert (monday_hours == Side.LONG).all()
assert (other_hours == Side.FLAT).all()
```
- [ ] **Step 3.2: Add `is_weekend` test**
Append:
```python
def test_compile_is_weekend_returns_zero_one(ohlcv: pd.DataFrame) -> None:
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "eq",
"args": [
{"kind": "feature", "name": "is_weekend"},
{"kind": "literal", "value": 1},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
weekend = signal[signal.index.dayofweek >= 5]
weekdays = signal[signal.index.dayofweek < 5]
assert (weekend == Side.LONG).all()
assert (weekdays == Side.FLAT).all()
```
- [ ] **Step 3.3: Run both tests**
Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_dow_feature_monday_is_zero tests/unit/test_protocol_compiler.py::test_compile_is_weekend_returns_zero_one -v`
Expected: Both PASS.
- [ ] **Step 3.4: Commit**
```bash
git add tests/unit/test_protocol_compiler.py
git commit -m "test(protocol): compiler semantica dow + is_weekend"
```
---
## Task 4: Compiler — `minute_of_hour` test
**Files:**
- Modify: `tests/unit/test_protocol_compiler.py` (append)
- [ ] **Step 4.1: Add `minute_of_hour` test**
Append:
```python
def test_compile_minute_of_hour_zero_on_1h_timeframe(ohlcv: pd.DataFrame) -> None:
# Fixture ohlcv ha freq=1h, quindi tutti i minute_of_hour sono 0.
# gating eq minute_of_hour 0 -> LONG su TUTTE le righe.
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "eq",
"args": [
{"kind": "feature", "name": "minute_of_hour"},
{"kind": "literal", "value": 0},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
assert (signal == Side.LONG).all()
```
- [ ] **Step 4.2: Run test**
Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_compile_minute_of_hour_zero_on_1h_timeframe -v`
Expected: PASS.
- [ ] **Step 4.3: Commit**
```bash
git add tests/unit/test_protocol_compiler.py
git commit -m "test(protocol): compiler semantica minute_of_hour su 1h"
```
---
## Task 5: Compiler — integrazione con regola completa
**Files:**
- Modify: `tests/unit/test_protocol_compiler.py` (append)
- [ ] **Step 5.1: Add integration test**
Append:
```python
def test_rule_with_temporal_gating_compiles_and_executes(ohlcv: pd.DataFrame) -> None:
# Regola: entry-long se hour > 14 AND close > sma(20).
# close in fixture e' lineare crescente, quindi close > sma(20) e' True dopo warmup.
# entry-long deve apparire solo nelle bar con hour > 14.
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{"kind": "feature", "name": "hour"},
{"kind": "literal", "value": 14},
],
},
{
"op": "gt",
"args": [
{"kind": "feature", "name": "close"},
{"kind": "indicator", "name": "sma", "params": [20]},
],
},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
# Bar con hour <= 14: mai LONG (gating temporale blocca).
morning = signal[signal.index.hour <= 14]
assert (morning == Side.FLAT).all()
# Bar con hour > 14 e dopo warmup sma (>=20 bar dall'inizio): LONG.
afternoon_warm = signal[(signal.index.hour > 14) & (np.arange(len(signal)) >= 20)]
assert (afternoon_warm == Side.LONG).all()
```
- [ ] **Step 5.2: Run test**
Run: `uv run pytest tests/unit/test_protocol_compiler.py::test_rule_with_temporal_gating_compiles_and_executes -v`
Expected: PASS.
- [ ] **Step 5.3: Run full compiler + validator test suite to check regressions**
Run: `uv run pytest tests/unit/test_protocol_compiler.py tests/unit/test_protocol_validator.py -v`
Expected: All tests PASS (pre-existing + new). Nessun test rotto.
- [ ] **Step 5.4: Commit**
```bash
git add tests/unit/test_protocol_compiler.py
git commit -m "test(protocol): integration test gating temporale + sma"
```
---
## Task 6: Update Hypothesis prompt
**Files:**
- Modify: `src/multi_swarm/agents/hypothesis.py:84-85`
- [ ] **Step 6.1: Edit prompt template**
In `src/multi_swarm/agents/hypothesis.py`, alla riga 84-85 sostituire:
```python
Leaf - feature OHLCV:
{{"kind": "feature", "name": "open|high|low|close|volume"}}
```
con:
```python
Leaf - feature OHLCV:
{{"kind": "feature", "name": "open|high|low|close|volume"}}
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}}]}}
```
- [ ] **Step 6.2: Run existing hypothesis tests to verify prompt format still valid**
Run: `uv run pytest tests/unit/test_hypothesis_agent.py -v`
Expected: All tests PASS. Il template `{feature_access}` continua a funzionare perché non lo abbiamo toccato.
- [ ] **Step 6.3: Commit**
```bash
git add src/multi_swarm/agents/hypothesis.py
git commit -m "feat(hypothesis): aggiungi feature temporali al prompt con 2 esempi few-shot"
```
---
## Task 7: Smoke run end-to-end
**Files:**
- Nessuna modifica al codice.
Validazione che il loop intero giri con la grammatica estesa: carica OHLCV, genera 4 genomi, compila, backtesta, valuta DSR, applica Adversarial, persiste.
- [ ] **Step 7.1: Run smoke script**
Run: `uv run python -m scripts.smoke_run`
Expected: completamento senza eccezioni, output finale contenente `Smoke run completed`.
- [ ] **Step 7.2: Inspect at least one generated genome for temporal feature usage**
Run:
```bash
LATEST=$(sqlite3 runs.db "SELECT id FROM runs WHERE name LIKE 'smoke%' ORDER BY started_at DESC LIMIT 1;")
sqlite3 runs.db "SELECT genome_id, substr(raw_text, 1, 600) FROM evaluations WHERE run_id='$LATEST' LIMIT 4;"
```
Expected output: 4 righe raw_text JSON. Almeno 1 dovrebbe contenere `"name": "hour"`, `"name": "dow"`, `"name": "is_weekend"`, o `"name": "minute_of_hour"`. Se 0/4 usano feature temporali, il prompt non è abbastanza eloquente — apri un follow-up per iterare il prompt (non bloccante per questa PR).
- [ ] **Step 7.3: Push branch + open PR**
```bash
git log --oneline -8 # verifica 6 commit dei Task 1-6
git push origin HEAD
```
Aprire PR con titolo `feat: feature temporali nella grammatica Hypothesis` referenziando lo spec.
---
## Self-review notes (autore del piano)
- Tutti i 7 hard requirement dello spec (`grammar`, `compiler`, `prompt`, 4 feature, integration test, smoke, backward compat) sono coperti dai Task 1-7.
- Nessun placeholder `TBD`/`TODO`.
- Tipi consistenti: `_TIME_FEATURE_FNS` definito una volta in Task 2 e referenziato implicitamente dai tester nei Task 3-5 senza bisogno di re-definizione.
- Test pre-esistenti non vengono toccati; il Task 5 include `pytest` sull'intera suite del protocollo come regression check.
- Backward compat: `KNOWN_FEATURES` cresce, il branch OHLCV resta invariato → genomi vecchi restano validi senza migrazione DB.
@@ -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 | 023 | `df.index.hour` |
| `dow` | int64 | 06 (lun=0) | `df.index.dayofweek` |
| `is_weekend` | int64 | 0 o 1 | `(df.index.dayofweek >= 5).astype(int)` |
| `minute_of_hour` | int64 | 059 | `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).
+16
View File
@@ -84,6 +84,22 @@ Leaf - indicatori (calcolati su close):
Leaf - feature OHLCV:
{{"kind": "feature", "name": "open|high|low|close|volume"}}
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}}]}}
Leaf - letterale numerico:
{{"kind": "literal", "value": 70.0}}
+9
View File
@@ -107,6 +107,13 @@ INDICATOR_FNS: dict[str, Any] = {
"macd": _ind_macd,
}
_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"),
}
def _to_series(value: float, df: pd.DataFrame) -> pd.Series:
"""Broadcast a numeric literal across the DataFrame index."""
@@ -134,6 +141,8 @@ def _eval_bool_arg(node: Node, df: pd.DataFrame) -> pd.Series:
def _eval_node(node: Node, df: pd.DataFrame) -> pd.Series:
if isinstance(node, FeatureNode):
if node.name in _TIME_FEATURE_FNS:
return _TIME_FEATURE_FNS[node.name](df.index)
return df[node.name]
if isinstance(node, IndicatorNode):
+2 -1
View File
@@ -20,7 +20,8 @@ KNOWN_INDICATORS: frozenset[str] = frozenset(
{"sma", "rsi", "atr", "macd", "realized_vol"}
)
KNOWN_FEATURES: frozenset[str] = frozenset(
{"open", "high", "low", "close", "volume"}
{"open", "high", "low", "close", "volume",
"hour", "dow", "is_weekend", "minute_of_hour"}
)
# Convenience union (utile a validator / parser).
+147
View File
@@ -106,3 +106,150 @@ def test_compile_two_rules_priority(ohlcv: pd.DataFrame) -> None:
signals = fn(ohlcv)
last = signals.iloc[-1]
assert last == Side.LONG # close finale e' 120, regola 1 matcha
def test_compile_hour_feature_returns_index_hour(ohlcv: pd.DataFrame) -> None:
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "gt",
"args": [
{"kind": "feature", "name": "hour"},
{"kind": "literal", "value": -1.0},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
# All rows have hour >= 0 > -1, so all entry-long.
assert (signal == Side.LONG).all()
def test_compile_dow_feature_monday_is_zero(ohlcv: pd.DataFrame) -> None:
# 2024-01-01 is Monday -> dow=0; eq(dow, 0) gates LONG on Monday rows only.
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "eq",
"args": [
{"kind": "feature", "name": "dow"},
{"kind": "literal", "value": 0.0},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
monday_rows = signal[signal.index.dayofweek == 0]
other_rows = signal[signal.index.dayofweek != 0]
assert (monday_rows == Side.LONG).all()
assert (other_rows == Side.FLAT).all()
def test_compile_is_weekend_returns_zero_one(ohlcv: pd.DataFrame) -> None:
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "eq",
"args": [
{"kind": "feature", "name": "is_weekend"},
{"kind": "literal", "value": 1.0},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
weekend = signal[signal.index.dayofweek >= 5]
weekdays = signal[signal.index.dayofweek < 5]
assert (weekend == Side.LONG).all()
assert (weekdays == Side.FLAT).all()
def test_compile_minute_of_hour_zero_on_1h_timeframe(ohlcv: pd.DataFrame) -> None:
# Fixture has freq=1h, so minute_of_hour is 0 on every row.
# eq(minute_of_hour, 0.0) -> LONG on every row.
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "eq",
"args": [
{"kind": "feature", "name": "minute_of_hour"},
{"kind": "literal", "value": 0.0},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
assert (signal == Side.LONG).all()
def test_rule_with_temporal_gating_compiles_and_executes(ohlcv: pd.DataFrame) -> None:
# Rule: entry-long if hour > 14 AND close > sma(20).
# close in fixture is strictly increasing, so close > sma(20) holds after warmup.
# entry-long should appear only on rows with hour > 14.
src = json.dumps(
{
"rules": [
{
"condition": {
"op": "and",
"args": [
{
"op": "gt",
"args": [
{"kind": "feature", "name": "hour"},
{"kind": "literal", "value": 14.0},
],
},
{
"op": "gt",
"args": [
{"kind": "feature", "name": "close"},
{"kind": "indicator", "name": "sma", "params": [20]},
],
},
],
},
"action": "entry-long",
}
]
}
)
ast = parse_strategy(src)
fn = compile_strategy(ast)
signal = fn(ohlcv)
# Bars with hour <= 14: never LONG (temporal gate blocks).
morning = signal[signal.index.hour <= 14]
assert (morning == Side.FLAT).all()
# Bars with hour > 14 AND past SMA warmup (>=20 bars): LONG.
afternoon_warm = signal[(signal.index.hour > 14) & (np.arange(len(signal)) >= 20)]
assert (afternoon_warm == Side.LONG).all()
+30
View File
@@ -151,3 +151,33 @@ def test_feature_unknown_column_fails() -> None:
ast = parse_strategy(src)
with pytest.raises(ValidationError, match="unknown feature"):
validate_strategy(ast)
@pytest.mark.parametrize("name", ["hour", "dow", "is_weekend", "minute_of_hour"])
def test_validator_accepts_temporal_feature(name: str) -> None:
src = _wrap(
{
"op": "gt",
"args": [
{"kind": "feature", "name": name},
{"kind": "literal", "value": 0.0},
],
}
)
ast = parse_strategy(src)
validate_strategy(ast) # no exception
def test_validator_rejects_temporal_typo() -> None:
src = _wrap(
{
"op": "gt",
"args": [
{"kind": "feature", "name": "weekday"},
{"kind": "literal", "value": 0.0},
],
}
)
ast = parse_strategy(src)
with pytest.raises(ValidationError, match="unknown feature"):
validate_strategy(ast)