From 0fd31d52ecb70306ecc5ea68f7d68c10c9e1b63b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 15 May 2026 20:43:58 +0000 Subject: [PATCH] feat(agents): estendi input LLM con 4 statistiche regime-aware + directive v2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunge al USER_TEMPLATE dell'HypothesisAgent 4 metriche calcolate su rolling window 500 barre (no backward bias del full sample): - autocorr_lag1_recent vs autocorr_lag1_baseline (AR(1) delta vs storico) - hurst_recent (R/S analysis, persistenza di scala) - vol_percentile (0-100, posizione vol corrente nella distribuzione storica) - seasonality_hour, seasonality_dow (0-1, varianza spiegata da feature temporali) Razionale: skew/kurt da soli sono ambigui — un AR(1) discrimina momentum vs mean-reversion meglio di tutta la guidance sui momenti. NEW funzioni in metrics/basic.py: - autocorr_lag1(returns) - hurst_exponent(returns) via R/S a scale multiple - vol_percentile_historical(returns, current_window=24, ref_window=2000) - seasonality_strength(returns, by={"hour"|"dow"}) MarketSummary esteso con 6 nuovi campi (con default); build_market_summary calcola rolling-500 per "recent", full sample per "baseline". prompts.json v2.1: tutte le 7 directive estese con frase di interpretazione style-specific dei 4 nuovi input (no style collapse). Es: - physicist: "AR(1) sopra baseline = sistema con memoria fuori equilibrio" - engineer: "AR(1) > 0.05 con std contenuta = SNR favorevole" - psychologist: "AR(1) recente >> baseline = euforia coordinata" Tests: +16 unit per le metriche, +1 smoke per MarketSummary populated. Verifica: 207 test pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../multi_swarm_core/agents/hypothesis.py | 18 +++ .../multi_swarm_core/agents/market_summary.py | 22 +++ .../multi_swarm_core/metrics/basic.py | 116 ++++++++++++++++ .../tests/unit/test_market_summary.py | 32 +++++ .../tests/unit/test_metrics_basic.py | 130 +++++++++++++++++- .../strategy_crypto/prompts.json | 29 ++++ 6 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/strategy_crypto/strategy_crypto/prompts.json diff --git a/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py index ba9f809..83f4b40 100644 --- a/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py +++ b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py @@ -21,6 +21,12 @@ class MarketSummary: skew: float kurtosis: float volatility_regime: str + autocorr_lag1_recent: float = 0.0 # AR(1) ultimi 500 bar + autocorr_lag1_baseline: float = 0.0 # AR(1) full sample (riferimento) + hurst_recent: float = 0.5 # Hurst ultimi 500 bar + vol_percentile: float = 50.0 # 0-100 percentile della vol corrente + seasonality_hour: float = 0.0 # 0-1 varianza spiegata da hour + seasonality_dow: float = 0.0 # 0-1 varianza spiegata da dow @dataclass(frozen=True) @@ -181,6 +187,12 @@ Statistiche return: mean={return_mean:.5f}, std={return_std:.5f}, \ skew={skew:.3f}, kurt={kurtosis:.3f}. Regime volatilità: {volatility_regime}. +Regime recente (ultime 500 barre): + autocorr_lag1: {autocorr_lag1_recent:.3f} (baseline storica: {autocorr_lag1_baseline:.3f}) + hurst: {hurst_recent:.3f} (0.5 = random walk, >0.55 trending, <0.45 mean rev) + vol_pct: {vol_percentile:.0f}° percentile storico + stagionalita: hour={seasonality_hour:.2f}, dow={seasonality_dow:.2f} (0-1, varianza spiegata) + Feature accessibili dal tuo genoma: {feature_access}. Lookback massimo che puoi usare nel ragionamento: {lookback_window} barre. @@ -294,6 +306,12 @@ class HypothesisAgent: skew=market.skew, kurtosis=market.kurtosis, volatility_regime=market.volatility_regime, + autocorr_lag1_recent=market.autocorr_lag1_recent, + autocorr_lag1_baseline=market.autocorr_lag1_baseline, + hurst_recent=market.hurst_recent, + vol_percentile=market.vol_percentile, + seasonality_hour=market.seasonality_hour, + seasonality_dow=market.seasonality_dow, feature_access=", ".join(genome.feature_access), lookback_window=genome.lookback_window, ) diff --git a/src/multi_swarm_core/multi_swarm_core/agents/market_summary.py b/src/multi_swarm_core/multi_swarm_core/agents/market_summary.py index 4c42117..f8a8b33 100644 --- a/src/multi_swarm_core/multi_swarm_core/agents/market_summary.py +++ b/src/multi_swarm_core/multi_swarm_core/agents/market_summary.py @@ -3,6 +3,12 @@ from __future__ import annotations import pandas as pd # type: ignore[import-untyped] from scipy import stats # type: ignore[import-untyped] +from ..metrics.basic import ( + autocorr_lag1, + hurst_exponent, + seasonality_strength, + vol_percentile_historical, +) from .hypothesis import MarketSummary @@ -24,6 +30,16 @@ def build_market_summary( else: regime = "high" + recent_window = min(500, len(returns)) + recent_returns = returns.iloc[-recent_window:] + + autocorr_recent = autocorr_lag1(recent_returns) + autocorr_baseline = autocorr_lag1(returns) + hurst_r = hurst_exponent(recent_returns) + vol_pct = vol_percentile_historical(returns, current_window=24, ref_window=2000) + season_h = seasonality_strength(returns, by="hour") + season_d = seasonality_strength(returns, by="dow") + return MarketSummary( symbol=symbol, timeframe=timeframe, @@ -33,4 +49,10 @@ def build_market_summary( skew=skew, kurtosis=kurt, volatility_regime=regime, + autocorr_lag1_recent=autocorr_recent, + autocorr_lag1_baseline=autocorr_baseline, + hurst_recent=hurst_r, + vol_percentile=vol_pct, + seasonality_hour=season_h, + seasonality_dow=season_d, ) diff --git a/src/multi_swarm_core/multi_swarm_core/metrics/basic.py b/src/multi_swarm_core/multi_swarm_core/metrics/basic.py index eea934c..eb10afc 100644 --- a/src/multi_swarm_core/multi_swarm_core/metrics/basic.py +++ b/src/multi_swarm_core/multi_swarm_core/metrics/basic.py @@ -4,6 +4,122 @@ import numpy as np import pandas as pd # type: ignore[import-untyped] +def autocorr_lag1(returns: pd.Series) -> float: + """Autocorrelazione dei ritorni con lag 1 (correlazione di Pearson).""" + if len(returns) < 2: + return 0.0 + val = returns.autocorr(lag=1) + return float(val) if pd.notna(val) else 0.0 + + +def hurst_exponent(returns: pd.Series) -> float: + """Hurst exponent via R/S analysis (rescaled range). + + Range 0-1: 0.5 random walk, >0.55 trending persistente, <0.45 mean reverting. + Implementazione classica con scale multiple (2^k bins). + """ + n = len(returns) + if n < 100: + return 0.5 # insufficiente dati, ritorna random-walk default + + series = returns.dropna().values + n = len(series) + if n < 100: + return 0.5 + + # Scale: 2^k che dividono n in segmenti + scales = [2**k for k in range(4, int(np.log2(n // 2)) + 1)] + if not scales: + return 0.5 + + rs_values: list[float] = [] + for scale in scales: + n_chunks = n // scale + if n_chunks < 1: + continue + rs_chunk: list[float] = [] + for i in range(n_chunks): + chunk = series[i * scale : (i + 1) * scale] + mean = chunk.mean() + cumdev = (chunk - mean).cumsum() + r = cumdev.max() - cumdev.min() + s = chunk.std() + if s > 0: + rs_chunk.append(r / s) + if rs_chunk: + rs_values.append(float(np.mean(rs_chunk))) + + if len(rs_values) < 2: + return 0.5 + + log_scales = np.log(scales[: len(rs_values)]) + log_rs = np.log(rs_values) + # Hurst = slope della regressione log-log + h, _ = np.polyfit(log_scales, log_rs, 1) + return float(np.clip(h, 0.0, 1.0)) + + +def vol_percentile_historical( + returns: pd.Series, + current_window: int = 24, + ref_window: int = 2000, +) -> float: + """Percentile (0-100) della vol corrente nella distribuzione storica. + + Vol = std rolling su current_window barre. Confronta l'ultimo valore contro + la distribuzione dei valori della stessa std rolling sugli ultimi ref_window. + Output: 0 (vol attuale tra le piu basse), 100 (tra le piu alte). + """ + if len(returns) < max(current_window, 100): + return 50.0 + + rolling_vol = returns.rolling(current_window, min_periods=current_window).std() + rolling_vol = rolling_vol.dropna() + if len(rolling_vol) < 10: + return 50.0 + + # Limita ref_window all'effettiva disponibilita + ref = rolling_vol.iloc[-ref_window:] if len(rolling_vol) > ref_window else rolling_vol + current = float(rolling_vol.iloc[-1]) + pct = float((ref < current).sum()) / len(ref) * 100.0 + return pct + + +def seasonality_strength( + returns: pd.Series, + by: str, +) -> float: + """Frazione di varianza dei ritorni spiegata dalla feature temporale `by`. + + `by` in {"hour", "dow"}. Output 0-1: 0 = no seasonality, 1 = tutta la varianza + e dovuta al ciclo. Calcolato come 1 - (var residua / var totale) usando i + gruppi indotti dalla feature. + """ + if not isinstance(returns.index, pd.DatetimeIndex): + return 0.0 + if len(returns) < 50: + return 0.0 + + if by == "hour": + groups = returns.index.hour + elif by == "dow": + groups = returns.index.dayofweek + else: + raise ValueError(f"by deve essere 'hour' o 'dow', non {by!r}") + + total_var = float(returns.var()) + if total_var <= 0: + return 0.0 + + grouped = returns.groupby(groups) + group_means = grouped.transform("mean") + residuals = returns - group_means + residual_var = float(residuals.var()) + + explained = 1.0 - (residual_var / total_var) + return float(np.clip(explained, 0.0, 1.0)) + + def sharpe_ratio(returns: pd.Series, periods_per_year: int = 8760, rf: float = 0.0) -> float: """Sharpe annualizzato. periods_per_year=8760 per dati orari.""" excess = returns - rf / periods_per_year diff --git a/src/multi_swarm_core/tests/unit/test_market_summary.py b/src/multi_swarm_core/tests/unit/test_market_summary.py index a3b245e..2ad0123 100644 --- a/src/multi_swarm_core/tests/unit/test_market_summary.py +++ b/src/multi_swarm_core/tests/unit/test_market_summary.py @@ -31,3 +31,35 @@ def test_volatility_regime_high_for_volatile() -> None: ) s = build_market_summary(df, symbol="BTC/USDT", timeframe="1h") assert s.volatility_regime in {"medium", "high"} + + +def test_build_summary_new_fields_populated() -> None: + """I 6 nuovi campi devono essere float nei range attesi.""" + idx = pd.date_range("2024-01-01", periods=600, freq="1h", tz="UTC") + np.random.seed(42) + close = 100 + np.cumsum(np.random.normal(0, 1, 600)) + df = pd.DataFrame( + {"open": close, "high": close + 0.5, "low": close - 0.5, "close": close, "volume": 1.0}, + index=idx, + ) + s = build_market_summary(df, symbol="ETH/USDT", timeframe="1h") + + # autocorr fields: float in [-1, 1] + assert isinstance(s.autocorr_lag1_recent, float) + assert isinstance(s.autocorr_lag1_baseline, float) + assert -1.0 <= s.autocorr_lag1_recent <= 1.0 + assert -1.0 <= s.autocorr_lag1_baseline <= 1.0 + + # hurst: float in [0, 1] + assert isinstance(s.hurst_recent, float) + assert 0.0 <= s.hurst_recent <= 1.0 + + # vol_percentile: float in [0, 100] + assert isinstance(s.vol_percentile, float) + assert 0.0 <= s.vol_percentile <= 100.0 + + # seasonality fields: float in [0, 1] + assert isinstance(s.seasonality_hour, float) + assert isinstance(s.seasonality_dow, float) + assert 0.0 <= s.seasonality_hour <= 1.0 + assert 0.0 <= s.seasonality_dow <= 1.0 diff --git a/src/multi_swarm_core/tests/unit/test_metrics_basic.py b/src/multi_swarm_core/tests/unit/test_metrics_basic.py index 7c67bae..6f8af2e 100644 --- a/src/multi_swarm_core/tests/unit/test_metrics_basic.py +++ b/src/multi_swarm_core/tests/unit/test_metrics_basic.py @@ -2,7 +2,15 @@ import numpy as np import pandas as pd import pytest -from multi_swarm_core.metrics.basic import max_drawdown, sharpe_ratio, total_return +from multi_swarm_core.metrics.basic import ( + autocorr_lag1, + hurst_exponent, + max_drawdown, + seasonality_strength, + sharpe_ratio, + total_return, + vol_percentile_historical, +) def test_sharpe_zero_returns(): @@ -38,3 +46,123 @@ def test_max_drawdown_known_curve(): def test_total_return(): eq = pd.Series([100.0, 110.0, 105.0, 120.0]) assert total_return(eq) == pytest.approx(0.20) + + +# --- New metrics tests --- + + +def test_autocorr_lag1_short_series(): + """Serie corta ritorna 0.0 senza errori.""" + r = pd.Series([0.01]) + assert autocorr_lag1(r) == 0.0 + + +def test_autocorr_lag1_all_nan(): + """Serie di NaN ritorna 0.0.""" + r = pd.Series([float("nan")] * 10) + assert autocorr_lag1(r) == 0.0 + + +def test_autocorr_lag1_range(): + """Autocorrelazione sempre in [-1, 1].""" + np.random.seed(7) + r = pd.Series(np.random.normal(0, 0.01, 500)) + val = autocorr_lag1(r) + assert -1.0 <= val <= 1.0 + + +def test_autocorr_lag1_trending_positive(): + """Serie con autocorrelazione positiva artificiale deve dare val > 0.""" + # costruiamo una serie con forte AR(1): x_t = 0.8*x_{t-1} + noise + np.random.seed(1) + n = 300 + x = np.zeros(n) + for i in range(1, n): + x[i] = 0.8 * x[i - 1] + np.random.normal(0, 0.1) + r = pd.Series(x) + assert autocorr_lag1(r) > 0.5 + + +def test_hurst_short_series(): + """Serie < 100 elementi ritorna 0.5 (random-walk default).""" + r = pd.Series(np.random.normal(0, 0.01, 50)) + assert hurst_exponent(r) == pytest.approx(0.5) + + +def test_hurst_range(): + """Hurst sempre in [0, 1].""" + np.random.seed(3) + r = pd.Series(np.random.normal(0, 0.01, 500)) + h = hurst_exponent(r) + assert 0.0 <= h <= 1.0 + + +def test_hurst_random_walk_approx_half(): + """Random walk iid deve avere Hurst vicino a 0.5.""" + np.random.seed(42) + r = pd.Series(np.random.normal(0, 0.01, 2000)) + h = hurst_exponent(r) + assert 0.3 <= h <= 0.7 + + +def test_vol_percentile_short_series(): + """Serie troppo corta ritorna 50.0.""" + r = pd.Series(np.random.normal(0, 0.01, 10)) + assert vol_percentile_historical(r) == pytest.approx(50.0) + + +def test_vol_percentile_range(): + """Percentile sempre in [0, 100].""" + np.random.seed(5) + r = pd.Series(np.random.normal(0, 0.01, 500)) + p = vol_percentile_historical(r, current_window=24, ref_window=200) + assert 0.0 <= p <= 100.0 + + +def test_vol_percentile_high_vol_at_end(): + """Vol molto alta alla fine deve dare percentile elevato.""" + np.random.seed(9) + low_vol = np.random.normal(0, 0.001, 400) + high_vol = np.random.normal(0, 0.05, 100) + r = pd.Series(np.concatenate([low_vol, high_vol])) + p = vol_percentile_historical(r, current_window=24, ref_window=400) + assert p > 70.0 + + +def test_seasonality_strength_no_datetimeindex(): + """Senza DatetimeIndex ritorna 0.0.""" + r = pd.Series(np.random.normal(0, 0.01, 200)) + assert seasonality_strength(r, by="hour") == 0.0 + + +def test_seasonality_strength_short_series(): + """Serie < 50 elementi ritorna 0.0.""" + idx = pd.date_range("2024-01-01", periods=30, freq="1h", tz="UTC") + r = pd.Series(np.random.normal(0, 0.01, 30), index=idx) + assert seasonality_strength(r, by="hour") == 0.0 + + +def test_seasonality_strength_range(): + """Risultato sempre in [0, 1].""" + np.random.seed(11) + idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC") + r = pd.Series(np.random.normal(0, 0.01, 500), index=idx) + val = seasonality_strength(r, by="hour") + assert 0.0 <= val <= 1.0 + + +def test_seasonality_strength_dow_range(): + """Stagionalita dow sempre in [0, 1].""" + np.random.seed(13) + idx = pd.date_range("2024-01-01", periods=500, freq="1h", tz="UTC") + r = pd.Series(np.random.normal(0, 0.01, 500), index=idx) + val = seasonality_strength(r, by="dow") + assert 0.0 <= val <= 1.0 + + +def test_seasonality_strength_invalid_by(): + """by non valido solleva ValueError.""" + idx = pd.date_range("2024-01-01", periods=100, freq="1h", tz="UTC") + r = pd.Series(np.random.normal(0, 0.01, 100), index=idx) + with pytest.raises(ValueError, match="'minute'"): + seasonality_strength(r, by="minute") diff --git a/src/strategy_crypto/strategy_crypto/prompts.json b/src/strategy_crypto/strategy_crypto/prompts.json new file mode 100644 index 0000000..e5c9c22 --- /dev/null +++ b/src/strategy_crypto/strategy_crypto/prompts.json @@ -0,0 +1,29 @@ +{ + "_comment": "Stili cognitivi e direttive del system_prompt per il GA di strategy_crypto. Modifica liberamente: cambia 'directive' di uno stile esistente o aggiungi nuove voci a 'styles'. Il nome dello stile (key) viene usato come 'cognitive_style' del genoma; la 'directive' diventa il system_prompt iniziale.", + "_schema": "2.1", + "_changelog": "v2.1 - directive estese con interpretazione style-specific dei 4 nuovi input statistici (autocorr_lag1, hurst, vol_pct, seasonality). v2.0 - Riprogettato per blind-generator GA. Directive medio-compatte (~700 char) che orientano l'esplorazione cognitiva senza prescrivere indicatori specifici (lascia evolvere il GA). Mappate sulle 4 statistiche disponibili: mean, std, skew, kurtosis + volatility_regime. Rimosse ecologist (richiede multi-asset), game_theorist/epidemiologist (richiedono info esterne non visibili all'agente). Tenute 7 lenti che mappano bene sulle statistiche aggregate.", + "_design_notes": "Le directive sono BIAS DI ESPLORAZIONE, non template. Suggeriscono cosa cercare nei 4 momenti e quali archetipi di strategia preferire, lasciando al GA la scoperta della combinazione esatta di indicatori e soglie. Sono pensate per essere riscritte dall'operatore mutate_prompt_llm mantenendo coerenza con la lente.", + "styles": { + "physicist": { + "directive": "Pensa come un fisico: il mercato e un sistema con leggi di conservazione e regimi di scala. Leggi std come dispersione energetica e kurtosis come densita di eventi estremi (kurt alta = fat tails, sistema fuori equilibrio). Cerca simmetrie nei ritorni (skew ≈0 = sistema simmetrico) e rotture (skew marcato = forzante asimmetrica). AR(1) positivo significativamente sopra baseline = sistema con memoria fuori equilibrio, momentum legittimo; Hurst > 0.55 conferma persistenza di scala; vol percentile alto + kurt bassa = energia immagazzinata non ancora rilasciata. Preferisci ritorno all'equilibrio in regime simmetrico/basso vol, propagazione (momentum/breakout) in regime asimmetrico/alto vol. Pattern coerenti su piu lookback sono robusti, pattern singoli sono rumore." + }, + "biologist": { + "directive": "Pensa come un biologo evoluzionista: il mercato e un ecosistema di strategie in competizione. Skew negativo = predazione asimmetrica (vol-selling crowded che subisce shock). Skew positivo = predatori che cacciano breakout. Kurt alta = eventi di estinzione/fioritura. AR(1) positivo persistente = una specie sta colonizzando la nicchia (overcrowding imminente, fade); Hurst > 0.55 con vol percentile basso = nicchia stabile (occupa); seasonality forte = ritmo biologico ricorrente, sfruttabile. Preferisci contrarian in regime di skew estremo (fade la specie dominante) e coordinamento in regime simmetrico. Pattern asimmetrici: cattura la coda opposta al consensus." + }, + "historian": { + "directive": "Pensa come uno storico: i regimi si ripetono ma non identici. Usa mean come drift strutturale e std come ampiezza ciclo. Kurt alta + vol regime medium/high = fase tardiva (eventi estremi addensati, pre-transizione); kurt bassa + skew ≈0 = accumulazione/stabilita. AR(1) recente >> baseline storica = regime sta accelerando rispetto al normale; Hurst > 0.55 + vol percentile alto = fase markup matura, mean reversion attesa; seasonality forte = abitudini collettive consolidate, replicabili. Preferisci mean reversion strutturali: deviazioni significative tendono a ritornare su orizzonti multipli. Identifica analogie tra regime corrente e fasi tipiche (compressione vol, espansione, esaurimento trend)." + }, + "meteorologist": { + "directive": "Pensa come un meteorologo: la volatilita ha regimi persistenti con transizioni brusche. Vol regime e input primario. Std + kurt = microclima: std bassa + kurt bassa = calma stabile (vendi vol), std alta + kurt alta = tempesta (compra convexity), std bassa + kurt alta = calma ingannevole (riduci esposizione). AR(1) recente > baseline = fronte persistente in arrivo; Hurst > 0.55 = sistema su scala lunga (ciclone), Hurst < 0.45 = turbolenza locale (no trend persistente); vol percentile estremo = posizione nel ciclo seasonal; seasonality alta = pattern cyclonico ricorrente. Strategie regime-aware: gate espliciti su vol che attivano logiche diverse. Pattern multi-regime sono robusti." + }, + "engineer": { + "directive": "Pensa come un ingegnere di controllo: ogni segnale deve avere SNR favorevole e robustezza. Std e il rumore di fondo: il segnale deve essere significativamente piu grande della varianza intraday. Kurt alta riduce affidabilita dei segnali medi. AR(1) > 0.05 con std contenuta = SNR favorevole, segnale azionabile; AR(1) ≈0 = random walk, non costruirci sopra; Hurst < 0.45 = filtro mean-reversion causale efficace; vol percentile > 80 = saturazione (sensori instabili, riduci leverage); seasonality < 0.05 = feature temporali sono rumore, NON usarle. Pattern semplici e tarabili: poche condizioni in AND, soglie con margine, isteresi entry/exit. Robustezza > ottimalita su singolo regime." + }, + "military_strategist": { + "directive": "Pensa come uno stratega: distingui regime offensivo da difensivo. Vol regime medium/low + skew positivo + kurt moderata = terreno favorevole all'attacco (entry direzionali su breakout/momentum). Vol regime high + kurt elevata = terreno ostile (difesa: posizioni limitate, exit rapide). AR(1) > 0 = vento alle spalle, carica con momentum; AR(1) < 0 = imboscata possibile, contrarian; Hurst > 0.55 = posizione difendibile (hold trade); vol percentile alta = artiglieria nemica attiva (ritirata); seasonality forte = via predicibile da percorrere, sfruttala. Concentrazione: poche condizioni forti. Economia: flat quando il segnale non e dominante. Sorpresa: contrarian su skew estremo (consensus). Sicurezza: ogni entry con exit chiara." + }, + "psychologist": { + "directive": "Pensa come uno psicologo del comportamento collettivo: skew e kurt catturano emozioni. Skew neg + kurt alta = paura ricorrente (capitulation spikes, fade gli estremi al ribasso). Skew pos + kurt alta = euforia (FOMO spikes, fade gli estremi al rialzo). Skew ≈0 + kurt bassa = apatia/range (gioca i bordi). AR(1) recente >> baseline = euforia coordinata in corso, posizionati contro l'ultimo arrivato; Hurst > 0.55 = trance collettiva (trend trance, dura piu del razionale); vol percentile estremo + kurt alta = momentum emozionale puro; seasonality intra-day forte = bias circadiani sfruttabili (FOMO apertura US, panico chiusura asiatica). Estremi di oscillatori in regimi emotivi (kurt alta), crossover in regimi razionali (kurt ≈3). Contrarian sugli estremi, continuazione sulle medie." + } + } +}