refactor(layout): docs+tests core sotto modulo, cleanup superflui, README strategy
Ownership per modulo:
- Move docs/ root → src/multi_swarm_core/docs/{design,decisions,reports}/
* 00_documento_zero.md + coevolutive_swarm_system.md → docs/design/
* decisions/* → docs/decisions/
* reports/2026-05-14-stato-progetto-e-roadmap.md → docs/reports/
- Move tests/ root → src/multi_swarm_core/tests/
Cleanup superflui consumati (audit trail preservato in docs/decisions):
- poc_trading_swarm.md (POC superato — Phase 3 attiva in prod)
- docs/reports/2026-05-10-phase1-technical-report.md (superato dal 14-mag)
- docs/superpowers/plans/*.md (3 file, plan consumati)
- docs/superpowers/specs/*.md (2 file, spec consumate)
- tests/unit/paper_trading/ (vuota, paper_trading e' migrato in strategy_crypto)
- Directory docs/ root cancellata
NEW: src/strategy_crypto/README.md — overview strategia (scope, layout,
run, DB schema, pattern N strategie future)
Root resta minima: README.md, pyproject.toml, docker-compose.yml,
Dockerfile, .env*, uv.lock + data/series/state/scripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,318 +0,0 @@
|
||||
# `mutate_prompt_llm` — Phase 2.5 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 (`- [x]`) syntax for tracking.
|
||||
|
||||
**Status:** **TUTTI I 6 TASK COMPLETATI** (task 1-5 il 2026-05-11, task 6 il 2026-05-12). Mergiati su main. Validato empiricamente: run `phase2-5-qwen25-prompt-mut-004` ha raggiunto max fitness **0.1012** (+225% vs baseline `phase2-qwen25-control-001` 0.0311). Sweet spot weight=0.30 (curva U: weight=0.50 → regressione plateau 0.0311; weight=0.00 → baseline piatto).
|
||||
|
||||
**Trigger Phase 2.5 verificati con esito Phase 2 + run controllo:**
|
||||
- ✅ Plateau max fitness ≥ 4 gen consecutive (Phase 2 qwen3-235b stuck 8 gen a 0.0238; run controllo qwen-2.5-72b stuck 9 gen a 0.0311).
|
||||
- ✅ Diversità prompt collapsed: top genomi del run controllo hanno fitness/Sharpe/DD identici (mutazioni scalari non producono varianti significative).
|
||||
- ✗ Top quasi-fit ≥ 0.10 non raggiunto, ma 2/3 trigger sufficienti.
|
||||
|
||||
**Decisione collaterale:** rollback tier C a `qwen/qwen-2.5-72b-instruct` (run controllo l'ha dimostrato superiore a qwen3-235b: +30% fitness, 4× entropy, metà costo e tempo).
|
||||
|
||||
**Goal:** Introdurre un quinto operatore di mutazione che usa un LLM tier B come "mutator" per riscrivere il `system_prompt` di un genoma, generando diversità reale dove oggi `random_mutate` tocca solo quattro scalari. La pipeline GA esistente resta intatta: `mutate_prompt_llm` è solo un nuovo membro di `MUTATION_OPS` con peso configurabile.
|
||||
|
||||
**Architecture:** Operatore puro come gli altri quattro (`mutate_temperature`, `mutate_lookback`, `mutate_feature_access`, `mutate_cognitive_style`). Riceve `parent_genome`, `llm_client`, `rng` e restituisce un child genome con `system_prompt` modificato. Il mutator LLM (tier B = `deepseek/deepseek-v4-flash`) riceve una mutation-instruction casuale tra sei tipi predefiniti (`tighten_threshold`, `swap_comparator`, `add_condition`, `remove_condition`, `change_timeframe`, `add_temporal_gate`) e produce un nuovo prompt vincolato a una mutazione "atomica". Il child viene validato (parser + adversarial dry-run); su fallimento si effettua fallback a `random_mutate`. Selezione probabilistica nel `random_mutate` dispatcher con peso configurabile (default 0.30) — i quattro operator scalari mantengono il 70% complessivo.
|
||||
|
||||
**Tech Stack:** Python 3.13, `LLMClient` esistente (OpenAI SDK via OpenRouter), pytest + `pytest-mock`. Niente nuove dipendenze.
|
||||
|
||||
**Spec di riferimento:** sezione "Meccanismo di mutazione" della conversazione `2026-05-11`, valutazione `mutate_prompt_llm` (questa pagina contiene la sintesi).
|
||||
|
||||
---
|
||||
|
||||
## Trigger condition (quando attivare)
|
||||
|
||||
Implementare e mergiare **solo se** uno dei seguenti è vero al termine di Phase 2:
|
||||
|
||||
1. **Plateau evolutivo**: max fitness stagnante (Δ < 0.01) per ≥ 4 generazioni consecutive su `phase2-qwen3-001` o successori.
|
||||
2. **Diversità prompt collassa**: media Levenshtein normalizzata fra i prompt della popolazione finale ≤ 0.15 (= popolazione clonata).
|
||||
3. **Top genome problematico ma quasi-fit**: max fitness ≥ 0.10 ma adversarial finding HIGH ≥ 2 per il top, suggerendo che una mutazione mirata del prompt potrebbe "ripararlo".
|
||||
|
||||
Se Phase 2 raggiunge max fitness ≥ 0.30 senza plateau, **non attivare** (la diversità random basta).
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Tipo | Responsabilità |
|
||||
|------|------|----------------|
|
||||
| `src/multi_swarm/genome/mutation_prompt_llm.py` | New | Operatore `mutate_prompt_llm` + helper `MUTATION_INSTRUCTIONS` + retry/fallback wrapper |
|
||||
| `src/multi_swarm/genome/mutation.py` | Modify | Estendere `MUTATION_OPS` + introdurre dispatcher pesato `weighted_random_mutate` |
|
||||
| `src/multi_swarm/ga/loop.py` | Modify | Sostituire `random_mutate(parent, rng)` con `weighted_random_mutate(parent, rng, llm_client, weights)` |
|
||||
| `src/multi_swarm/orchestrator/run.py` | Modify | Aggiungere `mutator_tier: ModelTier = ModelTier.B` e `prompt_mutation_weight: float = 0.30` a `RunConfig`, passare `LLMClient` al loop GA |
|
||||
| `src/multi_swarm/llm/cost_tracker.py` | Modify (minimo) | Loggare `mutation_call` separatamente da `hypothesis_call` per attribuzione costo |
|
||||
| `src/multi_swarm/metrics/diversity.py` | New | Funzione `population_prompt_diversity` (Levenshtein normalizzata) — usata in trigger check + telemetry |
|
||||
| `tests/unit/test_mutation_prompt_llm.py` | New | Test operator con mock `LLMClient` (success + validation fail + retry/fallback) |
|
||||
| `tests/unit/test_mutation_dispatcher.py` | New | Test `weighted_random_mutate` rispetta i pesi |
|
||||
| `tests/unit/test_diversity.py` | New | Test `population_prompt_diversity` su prompt identici/diversi |
|
||||
| `tests/integration/test_ga_loop_with_prompt_mutator.py` | New | Loop end-to-end di 2 gen × 5 genomi con mock LLM, verifica diversità prompt cresce |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Mutator instructions + operator stub
|
||||
|
||||
**Files:**
|
||||
- New: `src/multi_swarm/genome/mutation_prompt_llm.py`
|
||||
- New: `tests/unit/test_mutation_prompt_llm.py`
|
||||
|
||||
- [x] **Step 1.1: Write failing test — operator returns child con system_prompt diverso**
|
||||
|
||||
Append a `tests/unit/test_mutation_prompt_llm.py`:
|
||||
|
||||
```python
|
||||
def test_mutate_prompt_llm_produces_different_prompt(mock_llm: LLMClient) -> None:
|
||||
parent = make_genome(system_prompt="Strategia: compra quando RSI < 30")
|
||||
mock_llm.respond_with("Strategia: compra quando RSI < 25 e ora >= 14")
|
||||
child = mutate_prompt_llm(parent, mock_llm, rng=random.Random(0))
|
||||
assert child.system_prompt != parent.system_prompt
|
||||
assert child.parent_ids == [*parent.parent_ids, parent.id]
|
||||
assert child.generation == parent.generation + 1
|
||||
```
|
||||
|
||||
- [x] **Step 1.2: Implement `MUTATION_INSTRUCTIONS` constant**
|
||||
|
||||
`mutation_prompt_llm.py`:
|
||||
|
||||
```python
|
||||
MUTATION_INSTRUCTIONS: dict[str, str] = {
|
||||
"tighten_threshold": "Rendi una soglia numerica più restrittiva del 10–20%...",
|
||||
"swap_comparator": "Inverti un comparator (gt ↔ lt, gte ↔ lte) mantenendo intent...",
|
||||
"add_condition": "Aggiungi una condizione AND/OR alla rule più specifica...",
|
||||
"remove_condition": "Rimuovi una condizione ridondante o debole...",
|
||||
"change_timeframe": "Modifica una finestra rolling (lookback) di ±30%...",
|
||||
"add_temporal_gate": "Aggiungi un gate temporale (hour, dow, is_weekend)...",
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 1.3: Implement `mutate_prompt_llm`**
|
||||
|
||||
Firma:
|
||||
```python
|
||||
def mutate_prompt_llm(
|
||||
g: HypothesisAgentGenome,
|
||||
llm: LLMClient,
|
||||
rng: random.Random,
|
||||
mutator_tier: ModelTier = ModelTier.B,
|
||||
) -> HypothesisAgentGenome:
|
||||
```
|
||||
|
||||
Logica:
|
||||
1. Scegli `instruction_key = rng.choice(list(MUTATION_INSTRUCTIONS))`.
|
||||
2. Costruisci messaggio system + user con `MUTATION_INSTRUCTIONS[instruction_key]` + `g.system_prompt`.
|
||||
3. Crea genoma temporaneo `mutator_genome` con `model_tier=mutator_tier`.
|
||||
4. Chiama `llm.complete(mutator_genome, system, user, max_tokens=2000)`.
|
||||
5. Estrai nuovo prompt da risposta (cerca blocco `<prompt>...</prompt>` o intero output).
|
||||
6. Ritorna `_clone_with(g, system_prompt=new_prompt)` (riusa helper di `mutation.py`).
|
||||
|
||||
- [x] **Step 1.4: Run test → green**
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit/test_mutation_prompt_llm.py::test_mutate_prompt_llm_produces_different_prompt -xvs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Validation + fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/multi_swarm/genome/mutation_prompt_llm.py`
|
||||
- Append: `tests/unit/test_mutation_prompt_llm.py`
|
||||
|
||||
- [x] **Step 2.1: Write failing test — fallback a random_mutate su prompt invalid**
|
||||
|
||||
```python
|
||||
def test_mutate_prompt_llm_falls_back_on_invalid_prompt(mock_llm: LLMClient) -> None:
|
||||
parent = make_genome()
|
||||
mock_llm.respond_with("garbage that does not parse")
|
||||
child = mutate_prompt_llm(parent, mock_llm, rng=random.Random(0))
|
||||
# Garbage prompt deve fallback: child è prodotto da random_mutate, quindi
|
||||
# system_prompt == parent.system_prompt (random_mutate tocca solo scalari)
|
||||
assert child.system_prompt == parent.system_prompt
|
||||
assert child.parent_ids == [*parent.parent_ids, parent.id]
|
||||
```
|
||||
|
||||
- [x] **Step 2.2: Implement validation step**
|
||||
|
||||
Dopo aver estratto `new_prompt`, esegui `validate_prompt(new_prompt)`:
|
||||
- Lunghezza minima 50 caratteri.
|
||||
- Contiene almeno una keyword fra `{rsi, sma, ema, atr, momentum, breakout, mean reversion, gt, lt, ...}`.
|
||||
- Non identico a `parent.system_prompt` (Levenshtein > 0.05 normalizzata).
|
||||
|
||||
Su fail → log warning + ritorna `random_mutate(g, rng)`.
|
||||
|
||||
- [x] **Step 2.3: Write failing test — diversity guard**
|
||||
|
||||
Mock LLM ritorna prompt identico al parent → `validate_prompt` rifiuta → fallback.
|
||||
|
||||
- [x] **Step 2.4: Run test suite parziale**
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit/test_mutation_prompt_llm.py -xvs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Weighted dispatcher
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/multi_swarm/genome/mutation.py`
|
||||
- New: `tests/unit/test_mutation_dispatcher.py`
|
||||
|
||||
- [x] **Step 3.1: Write failing test — weighted_random_mutate rispetta pesi**
|
||||
|
||||
```python
|
||||
def test_weighted_random_mutate_picks_prompt_op_at_configured_rate() -> None:
|
||||
rng = random.Random(0)
|
||||
weights = {"prompt": 1.0, "scalar": 0.0} # 100% prompt
|
||||
counter = Counter()
|
||||
for _ in range(100):
|
||||
op_name = _pick_op_name(weights, rng)
|
||||
counter[op_name] += 1
|
||||
assert counter["prompt"] == 100
|
||||
```
|
||||
|
||||
- [x] **Step 3.2: Implement `weighted_random_mutate`**
|
||||
|
||||
```python
|
||||
def weighted_random_mutate(
|
||||
g: HypothesisAgentGenome,
|
||||
rng: random.Random,
|
||||
llm: LLMClient | None = None,
|
||||
prompt_mutation_weight: float = 0.30,
|
||||
) -> HypothesisAgentGenome:
|
||||
if llm is not None and rng.random() < prompt_mutation_weight:
|
||||
return mutate_prompt_llm(g, llm, rng)
|
||||
return random_mutate(g, rng)
|
||||
```
|
||||
|
||||
- [x] **Step 3.3: Test edge cases**
|
||||
|
||||
- `llm=None` → sempre scalar mutation (backward compat).
|
||||
- `prompt_mutation_weight=0.0` → sempre scalar.
|
||||
- `prompt_mutation_weight=1.0` → sempre prompt (se llm presente).
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Integrazione GA loop
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/multi_swarm/ga/loop.py`
|
||||
- Modify: `src/multi_swarm/orchestrator/run.py`
|
||||
- New: `tests/integration/test_ga_loop_with_prompt_mutator.py`
|
||||
|
||||
- [x] **Step 4.1: Estendere `GAConfig`**
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class GAConfig:
|
||||
population_size: int
|
||||
elite_k: int
|
||||
tournament_k: int
|
||||
p_crossover: float
|
||||
prompt_mutation_weight: float = 0.0 # default off → opt-in
|
||||
```
|
||||
|
||||
- [x] **Step 4.2: Pass `LLMClient` in `next_generation`**
|
||||
|
||||
```python
|
||||
def next_generation(
|
||||
population: list[HypothesisAgentGenome],
|
||||
fitnesses: dict[str, float],
|
||||
cfg: GAConfig,
|
||||
rng: random.Random,
|
||||
llm: LLMClient | None = None,
|
||||
) -> list[HypothesisAgentGenome]:
|
||||
...
|
||||
child = weighted_random_mutate(parent, rng, llm, cfg.prompt_mutation_weight)
|
||||
```
|
||||
|
||||
- [x] **Step 4.3: Wire in orchestrator**
|
||||
|
||||
`RunConfig.prompt_mutation_weight: float = 0.0` (default off). Quando attivo via CLI `--prompt-mutation-weight 0.30`, passare a `next_generation`.
|
||||
|
||||
- [x] **Step 4.4: Integration test**
|
||||
|
||||
Loop 2 gen × 5 genomi, mock LLM che ritorna prompt sempre diversi. Verifica che la popolazione finale abbia più diversità prompt della iniziale.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Diversity metric
|
||||
|
||||
**Files:**
|
||||
- New: `src/multi_swarm/metrics/diversity.py`
|
||||
- New: `tests/unit/test_diversity.py`
|
||||
|
||||
- [x] **Step 5.1: Implement `population_prompt_diversity`**
|
||||
|
||||
```python
|
||||
def population_prompt_diversity(prompts: list[str]) -> float:
|
||||
"""Levenshtein normalizzata media su tutte le coppie. 0.0 = identici, 1.0 = totalmente diversi."""
|
||||
```
|
||||
|
||||
- [x] **Step 5.2: Test**
|
||||
|
||||
Tre prompt identici → 0.0. Tre prompt totalmente diversi → ~1.0.
|
||||
|
||||
- [x] **Step 5.3: Logging**
|
||||
|
||||
Aggiungere `diversity_prompt` come campo per-generazione in `repository.save_generation` (richiede migration leggera).
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Cost attribution
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/multi_swarm/llm/cost_tracker.py`
|
||||
- Modify: tests esistenti
|
||||
|
||||
- [x] **Step 6.1: Aggiungere `call_kind` a `CostRecord`**
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CostRecord:
|
||||
...
|
||||
call_kind: str = "hypothesis" # "hypothesis" | "mutation"
|
||||
```
|
||||
|
||||
- [x] **Step 6.2: Loggare separatamente in summary**
|
||||
|
||||
`summary()["by_call_kind"]` con breakdown.
|
||||
|
||||
- [x] **Step 6.3: Test compatibilità con record esistenti**
|
||||
|
||||
Backward compat: record senza `call_kind` interpretati come `"hypothesis"`.
|
||||
|
||||
---
|
||||
|
||||
## Verification end-to-end
|
||||
|
||||
- [x] `uv run pytest -q` → 100% passa (157 + nuovi test).
|
||||
- [x] `uv run python scripts/smoke_run.py` → completa con mock LLM.
|
||||
- [x] **Run baseline B**: ripetere `phase2-qwen3-001` con `--prompt-mutation-weight 0.0` per controllo.
|
||||
- [x] **Run trattamento T**: `phase2-qwen3-prompt-mut-001` con `--prompt-mutation-weight 0.30`.
|
||||
- [x] Confronto: max fitness T > B + 20%, diversity_prompt(T) > diversity_prompt(B) + 30%.
|
||||
- [x] Costo aggiuntivo run T ≤ $0.10 (sanity check).
|
||||
|
||||
---
|
||||
|
||||
## Risks & mitigations
|
||||
|
||||
| Rischio | Mitigazione |
|
||||
|---------|-------------|
|
||||
| Mode collapse mutator LLM | `mutation_instruction` scelta random + diversity guard Levenshtein |
|
||||
| Prompt LLM-output non parsabile dal compiler | Validation step + fallback `random_mutate` |
|
||||
| Costo runaway (loop infinito retry) | `max_tokens=2000`, no retry su validation fail |
|
||||
| Bias condiviso con generator tier C | Mutator tier B = `deepseek-v4-flash`, famiglia diversa da Qwen3 |
|
||||
| Variabili confuse con Phase 2 | Attivare **solo** dopo Phase 2 baseline; A/B isolato |
|
||||
|
||||
---
|
||||
|
||||
## Cost estimate
|
||||
|
||||
Pop = 20, gen = 10, mutation rate ~75% (5 elite + 15 children), prompt_mutation_weight = 0.30:
|
||||
- ~45 chiamate LLM tier B aggiuntive per run.
|
||||
- ~500 tok input + 200 tok output per call → 22.5k in + 9k out totali.
|
||||
- 22.5k × $0.14/1M + 9k × $0.28/1M ≈ **$0.0057/run**.
|
||||
|
||||
Trascurabile rispetto al budget run base (~$0.10).
|
||||
@@ -1,482 +0,0 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user