Passa a Binance Futures con arbitraggio statistico su coppie cointegrate

Il bot smette di operare direzionalmente su un singolo asset e passa a coppie
delta-neutral: long una gamba, short l'altra nel rapporto che il test di
cointegrazione produce, scommettendo solo sul fatto che la distanza fra le due
si richiuda. È questo che gli permette di girare da una connessione domestica,
perché su barre da 15 minuti la latenza smette di contare.

Cosa cambia
- Encelado.Alpaca sostituito da Encelado.Binance: REST firmato in HMAC-SHA256
  con correzione dello scarto d'orologio, uno stream combinato per kline, book
  e mark price, e lo stream ordini autenticato con listen key rinnovata.
- Nuovo livello statistico in Core: OLS, test di Dickey-Fuller aumentato con
  scelta del ritardo per AIC, ed Engle-Granger con i valori critici di MacKinnon
  per la cointegrazione.
- Il rischio ragiona per coppia: divide il controvalore fra le gambe secondo β,
  così le due si annullano invece di lasciare un residuo direzionale, e corregge
  la dimensione con il funding netto atteso.
- Interfaccia da sette pagine a quattro. I grafici a candele sono spariti: su una
  coppia coperta la candela di una gamba non dice niente, lo z-score sì.
- Ripristino dei valori predefiniti da Impostazioni, con copia datata del file
  precedente. Ripristina il documento, commenti compresi, non solo i numeri.

L'ordine che non partiva
Il segnale diceva di entrare e non succedeva niente perché il router registrava
quasi tutti i rifiuti a livello debug: alla verbosità predefinita il bot
annunciava l'ingresso, rinunciava per un motivo che nessuno poteva vedere, e
sembrava aver ignorato la propria decisione. Adesso ogni intento produce una
riga a info o warn con il nome della coppia e il motivo esatto, la frase che
l'operatore legge e la decisione che il motore prende vengono dallo stesso
stato, e una barra che lo stream non consegna viene recuperata via REST.

Che cosa dice il backtest
Il banco di prova rigioca le coppie attraverso la STESSA classe che gira in
produzione, con la calibrazione che cammina in avanti. Su 6,6 anni di ETHUSDT,
BTCUSDT, SOLUSDT e AVAXUSDT: a 5 minuti nessuna combinazione di soglie supera
i filtri di taratura; a 15 minuti e a un'ora la griglia trova combinazioni che
rendono in taratura e in verifica, ma nessuna delle prime dieci resta positiva
sulla terza fetta. La finestra dello z-score va molte volte oltre l'emivita del
rientro — le 100 barre della guida sono le peggiori misurate — e il filtro di
cointegrazione è ciò che tiene in piedi tutto: senza, ogni combinazione passa
da leggermente positiva a −73%/−87%.

Per questo dryRun parte attivo. I valori consegnati sono i meglio supportati
fra quelli provati, non una strategia dimostrata, e il file lo dice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 13:01:47 +02:00
co-authored by Claude Opus 5
parent 9453e20cfc
commit a4e297f77a
109 changed files with 11991 additions and 12296 deletions
+141 -86
View File
@@ -1,149 +1,204 @@
{
"_comment": "Encelado — configurazione unica. Ogni numero qui sotto è stato verificato su due dataset indipendenti: 4.756 barre giornaliere Bitstamp (2012-2025, ripiegate da 6,8 milioni di barre da un minuto) e 3.260 barre Binance (2017-2026). Vedi il README.",
"_comment": "Encelado — arbitraggio statistico su coppie cointegrate, Binance Futures USDⓈ-M. Il bot non prende posizione sulla direzione del mercato: è long una gamba e short l'altra nel rapporto che il test di cointegrazione produce, e scommette solo sul fatto che la distanza fra le due si richiuda.",
"alpaca": {
"paper": true,
"dataFeed": "iex",
"requestsPerMinute": 180,
"httpTimeoutSeconds": 15,
"maxRetries": 4
"_misura": "IMPORTANTE — che cosa dice il backtest, e perché dryRun parte ATTIVO. Misurato su 6,6 anni di barre da un minuto (2020-2026) di ETHUSDT, BTCUSDT, SOLUSDT e AVAXUSDT, ripiegate su 5m, 15m, 30m, 1h e 4h, con commissione taker 4 bps più 1 bp di slippage per lato. Il risultato: su 5m nessuna combinazione di soglie supera nemmeno i filtri di taratura; su 15m e 1h la ricerca a griglia trova combinazioni che rendono in taratura e in verifica, ma NESSUNA delle prime dieci resta positiva sulla terza fetta, quella che non entra mai nella scelta. Con i valori qui sotto, sulle quattro coppie misurabili, la conferma dà tre risultati negativi e uno positivo del +0,40% su una sola operazione. La ragione di fondo è che il test di cointegrazione passa solo l'8-13% del tempo, quindi in sei anni una coppia produce qualche decina di operazioni: troppo poche per distinguere un margine reale da una serie fortunata. Questi valori sono i meglio supportati fra quelli provati, non una strategia dimostrata. Falla girare in dry-run finché non hai visto coi tuoi occhi come si comporta sul tuo conto. Lo strumento è in tools/Encelado.Backtest: 'backtest basket --data <cartella>' rifa la scelta da capo sui tuoi dati.",
"binance": {
"_testnet": "true = testnet (denaro finto, stesse API). Metterlo a false opera con denaro reale. Le chiavi dei due ambienti sono diverse e vengono salvate separatamente.",
"testnet": true,
"_leverage": "Leva applicata a ogni simbolo all'avvio. La guida indica 2x-3x: un libro delta-neutral si liquida comunque se una sola gamba fa un salto isolato abbastanza grande. Alzarla non aumenta il margine della strategia, moltiplica soltanto guadagno e perdita.",
"leverage": 2,
"_marginType": "CROSSED mette le due gambe nello stesso pool di margine, così si compensano invece di avere ciascuna il proprio prezzo di liquidazione.",
"marginType": "CROSSED",
"recvWindowMs": 5000,
"requestsPerMinute": 1200,
"httpTimeoutSeconds": 10,
"maxRetries": 3
},
"engine": {
"assetClass": "crypto",
"_timeFrame": "15m. La guida indica 5m o 15m; il backtest ha bocciato 5m senza appello — nessuna combinazione di soglie supera nemmeno i filtri di taratura, perché a cinque minuti la commissione vale circa una deviazione standard dello spread. A 15m il numero di operazioni è almeno misurabile. A 1h e 4h le operazioni scendono a poche decine in sei anni: troppo poche per dire alcunché. '1m' è rifiutato dal validatore.",
"timeFrame": "15m",
"_timeFrame": "Giornaliero. Le stesse regole su barre orarie perdono il 99% del capitale: con ~50 bps di costo per giro completo la frequenza uccide prima della direzione.",
"timeFrame": "1Day",
"_warmupBars": "Barre storiche scaricate all'avvio per riempire la finestra dello z-score prima della prima decisione. Deve coprire zWindow.",
"warmupBars": 1500,
"_warmup": "La media è a 100 giorni. 220 barre danno margine.",
"warmupBars": 220,
"_calibrationBars": "Su quante barre gira la regressione di cointegrazione che produce beta e p-value. 500 è il valore della guida ed è anche il migliore misurato: a 1000 lo stesso paniere peggiora nettamente su tutte e tre le fette. Questa finestra è indipendente da zWindow, che è molto più lunga: la prima stabilisce il rapporto di copertura, la seconda dice quanto è insolito lo scostamento di adesso.",
"calibrationBars": 500,
"tradeOnlyRegularHours": false,
"flattenBeforeCloseMinutes": 0,
"_crypto": "Alpaca sulle crypto vuole quantità frazionarie e non supporta i bracket order: lo stop lo tiene l'engine e lo verifica a ogni quotazione.",
"allowFractionalShares": true,
"useBracketOrders": false,
"_recalibrateHours": "Ogni quanto l'intero paniere viene rifittato e ritestato. La guida dice 24 ore; provate anche una settimana, senza differenze sostanziali.",
"recalibrateHours": 24,
"_entryOrderType": "'limit' invia un limite marcabile, prezzato oltre il touch di limitOffsetBps: si comporta come un ordine a mercato ma non può eseguire a un prezzo assurdo. 'market' attraversa e basta.",
"entryOrderType": "limit",
"limitOffsetBps": 8,
"limitOffsetBps": 2,
"dryRun": false,
"_postOnly": "Solo maker (GTX). Fa risparmiare la commissione ma un ordine che non esegue lascia una gamba scoperta, che costa molto di più. Lasciare false se non hai misurato il tuo tasso di riempimento.",
"postOnlyEntries": false,
"_reconcile": "Ogni quanto il bot ricontrolla conto, posizioni e ordini contro il broker, e chiede le barre già chiuse. È anche il momento in cui si accorge che una barra nuova è disponibile da valutare, quindi abbassarlo lo rende più reattivo all'apertura di una barra.",
"reconcileSeconds": 30,
"_dryRun": "ATTIVO DI FABBRICA, e la ragione è nella nota _misura qui sopra. Calcola e registra tutto, non invia nessun ordine. Toglilo solo dopo aver visto in Stato che le tue coppie passano davvero il test di cointegrazione.",
"dryRun": true,
"_status": "Riepilogo periodico nel log: contatori, latenze, stato delle connessioni.",
"_reconcile": "Ogni quanto il bot ricontrolla conto, posizioni e ordini contro Binance. È anche quando recupera le barre che lo stream non ha consegnato e chiude le coppie rimaste con una gamba sola.",
"reconcileSeconds": 20,
"statusSeconds": 60,
"_explain": "Ogni quanto il bot rilegge cosa farebbe al prezzo attuale e lo scrive nel log, se è cambiato rispetto a prima. Su barre giornaliere il bot è legittimamente silenzioso per settimane, e da fuori il silenzio è indistinguibile da un blocco: questa riga lo trasforma in una frase.",
"_explain": "Ogni quanto il bot rilegge cosa farebbe adesso e lo scrive nel log, se è cambiato. È la riga che distingue un bot che aspetta da uno bloccato.",
"explainSeconds": 5,
"maxQuoteAgeSeconds": 120,
"closeOnShutdown": false
"_maxQuoteAge": "Rifiuta un ingresso se il book di una gamba è più vecchio di così. 0 disattiva il controllo.",
"maxQuoteAgeSeconds": 15,
"closeOnShutdown": false,
"logEveryBar": true
},
"risk": {
"_sizing": "Questa strategia compete con il comprare e tenere, quindi quando è dentro deve esserci per intero: qualunque frazione inferiore perde la gara in partenza. stakePct 1.0 impegna tutto il saldo disponibile; il risk engine si ferma comunque al 98% per lasciare spazio alle commissioni.",
"stakePct": 1.0,
"_stake": "Frazione dell'equity impegnata come MARGINE su una coppia. Con leva 2 il controvalore effettivamente al lavoro è il doppio, diviso fra le due gambe secondo β. Con 4 coppie al 15% il margine totale impegnato arriva al 60% dell'equity.",
"stakePct": 0.15,
"stakeAmount": 0,
"_risk": "Non usato finché stakePct è impostato, ma deve restare valido: è il criterio di riserva se un giorno azzeri stakePct.",
"maxRiskPerTradePct": 0.05,
"_funding": "Una coppia delta-neutral incassa il funding su una gamba e lo paga sull'altra. Quando il netto è a favore la posizione rende per il solo fatto di esistere. 0.12 è il centro dell'intervallo 10-15% indicato dalla guida. 0 disattiva.",
"fundingTiltPct": 0.12,
"fundingTiltThreshold": 0.0001,
"_caps": "A 1.0 perché con un solo asset e stake pieno la posizione È il portafoglio. Abbassarli qui significa restare parzialmente liquidi e perdere rendimento senza guadagnare protezione: la protezione la dà l'uscita sotto la media.",
"maxPositionNotionalPct": 1.0,
"maxGrossExposurePct": 1.0,
"_exposure": "Controvalore lordo totale come multiplo dell'equity. Con leva 2 e 4 coppie al 15% si arriva a 1.2×: 2.0 lascia margine senza permettere il raddoppio.",
"maxGrossExposurePct": 2.0,
"maxOpenPairs": 4,
"_openPositions": "0 = nessun limite. Nota però che con un solo simbolo il numero di posizioni contemporanee resta 1 comunque: il risk engine rifiuta un secondo ingresso sullo stesso strumento con 'already in position'. E con stakePct 1.0 la prima posizione impegna tutto il saldo, quindi una seconda non avrebbe con cosa aprirsi. Questo limite torna a contare quando aggiungi simboli.",
"maxOpenPositions": 0,
"_frequency": "0 = nessun limite. Sono reti contro un difetto, non contro la strategia: un ciclo che riapre la stessa coppia cento volte costa cento volte le commissioni.",
"maxTradesPerDay": 40,
"maxTradesPerPairPerDay": 8,
"minSecondsBetweenEntries": 60,
"_frequency": "0 = nessun limite. Il bot può aprire quante posizioni vuole e fare quante operazioni vuole: a fermarlo è la strategia, non un contatore. Attenzione: erano una rete contro un bug (un ciclo che riapre la stessa posizione mille volte costa mille commissioni). Con 0 quella rete non c'è più.",
"maxTradesPerDay": 0,
"maxTradesPerSymbolPerDay": 0,
"minSecondsBetweenEntries": 0,
"_dailyLoss": "Kill switch giornaliero. Al 25% perché su BTC un -20% in un giorno è successo più volte e non è una ragione per smettere: la strategia esce quando cede la media, non quando fa male. Troppo stretto qui significa liquidare sul minimo.",
"maxDailyLossPct": 0.25,
"_dailyLoss": "Kill switch giornaliero, non disattivabile. Su futures con leva è l'ultima fermata prima di una liquidazione.",
"maxDailyLossPct": 0.06,
"maxDailyProfitPct": 0,
"maxRelativeSpread": 0.0015,
"minPrice": 0.01,
"maxPrice": 10000000,
"_spread": "Il book più largo che una gamba può mostrare ed essere comunque entrata. Conta molto più che su un modello direzionale: un giro completo attraversa lo spread QUATTRO volte, quindi un book da 10 bps costa 40 bps contro un rientro che spesso vale meno.",
"maxRelativeSpread": 0.0006,
"_notional": "Controvalore minimo e massimo per gamba, in USDT. Binance ha anche i suoi minimi per simbolo, più stringenti su BTCUSDT.",
"minOrderNotional": 25,
"maxOrderNotional": 0,
"_shorting": "Alpaca non consente lo short sulle crypto. La strategia è long/flat.",
"allowShorting": false,
"_margin": "Rapporto massimo fra margine di mantenimento ed equity oltre il quale non si aprono nuove coppie.",
"maxMarginRatio": 0.5,
"_stop": "Rete di sicurezza per un gap, non il controllo del rischio. Quello vero è l'uscita sotto la media: uno stop stretto venderebbe e poi aspetterebbe un nuovo incrocio per rientrare, che è esattamente come il modello precedente trasformava le oscillazioni in perdite realizzate.",
"defaultStopPct": 0.35,
"maxStopDistancePct": 0.60
"_hedge": "β fuori da [1/5, 5] viene rifiutato: non è una copertura, è una scommessa sulla seconda gamba travestita da copertura.",
"maxHedgeRatio": 5.0
},
"logging": {
"_level": "trace | debug | info | warn | error | none. 'debug' registra anche ogni segnale scartato e ogni rifiuto del risk engine: utile per capire perché il bot NON ha fatto qualcosa.",
"level": "debug",
"_level": "trace | debug | info | warn | error | none. Ogni rifiuto che ferma un ordine viene scritto a 'info' o sopra, quindi 'debug' serve per il flusso dati, non per capire perché il bot non ha operato.",
"level": "info",
"_directory": "Dove salvare tutti gli output. Relativa all'eseguibile, oppure un percorso assoluto tipo D:\\encelado-logs. Si cambia anche da Impostazioni → Log, che verifica di potervi scrivere prima di salvare.",
"_directory": "Dove salvare tutti gli output. Relativa all'eseguibile, oppure un percorso assoluto. Si cambia anche da Impostazioni.",
"directory": "logs",
"console": false,
"file": "encelado.log",
"_rotation": "Ruota encelado.log in encelado.1.log e così via, tenendo gli ultimi 10.",
"maxFileSizeMb": 32,
"maxFiles": 10,
"_analysis": "decisions.csv ha una riga per ogni barra valutata con tutti gli indicatori; executions.csv ha una riga per ogni segnale arrivato agli ordini, con il verdetto del risk engine. Si uniscono su decisionId. Sono il materiale per migliorare il modello.",
"_analysis": "decisions.csv ha una riga per ogni barra valutata con spread, z-score e calibrazione; executions.csv una riga per ogni segnale arrivato agli ordini, con il verdetto del risk engine. Si uniscono su decisionId.",
"tradeJournal": "trades.jsonl",
"decisionLog": "decisions.csv",
"executionLog": "executions.csv",
"_verbose": "Con logMarketData attivo e level=trace registra ogni singola quotazione e ogni print. File enormi: serve solo per diagnosticare il flusso dati.",
"logMarketData": false,
"_everyBar": "Scrive una riga per ogni barra da un minuto che arriva dallo stream, non solo per quelle che chiudono una barra della strategia. Su barre giornaliere 1439 minuti su 1440 vengono assorbiti in silenzio: senza questo il log non mostra nulla per ventiquattr'ore e il bot sembra fermo.",
"logEveryBar": true,
"_inApp": "Quante righe tiene la striscia ATTIVITÀ nella pagina Stato e quante ne tiene la scheda Log. La seconda è il tetto di memoria del log in-app. Il file su disco resta completo comunque.",
"statusLines": 200,
"bufferedLines": 5000
},
"ui": {
"url": "http://localhost:5088",
"autoStartBot": false,
"openBrowser": false
},
"_pairs": "Il paniere della guida. Ogni coppia si legge come ln(A) β·ln(B): A è la gamba su cui si misura lo spread, B quella di copertura. Il test di cointegrazione decide da solo, a ogni ricalibrazione, se una coppia è operabile: quelle che non passano restano in elenco e non vengono aperte. Sui dati disponibili passano il test solo l'8-17% del tempo, quindi il bot resta fermo a lungo — è il comportamento previsto.",
"_symbols": "Solo BTC/USD. ETH è stato tolto: la strategia è tarata e verificata su BTC, e con stakePct 1.0 un secondo asset dimezzerebbe l'esposizione al primo senza che nessun backtest lo giustifichi.",
"symbols": [
"_parametri": "I valori qui sotto vengono dalla ricerca a griglia (tools/Encelado.Backtest, comando 'basket'), non dalla guida. Le differenze rispetto alla guida sono tre e sono tutte misurate: zWindow 1500 invece di 100, entryZ 2.5 invece di 2.0, stopZ 6.0 invece di 3.5. La prima è la più importante: con una finestra vicina all'emivita del rientro (30-40 barre) la media mobile insegue lo scostamento e lo assorbe, e il margine sparisce prima ancora delle commissioni.",
"pairs": [
{
"symbol": "BTC/USD",
"strategy": "trend-filter",
"_note": "Benchmark. È l'unica coppia del paniere risultata positiva sull'intero periodo, ma con sole 35-50 operazioni in 6,6 anni: troppe poche perché il risultato significhi qualcosa.",
"symbolA": "ETHUSDT",
"symbolB": "BTCUSDT",
"enabled": true,
"parameters": {
"_period": "Media a 100 giorni. È l'unico valore che batte il comprare e tenere su ENTRAMBI i dataset: 120 rende di più su Bitstamp ma perde su Binance, 200 perde su tutti e due. La riga dei 100 giorni vince su entrambi a qualunque banda.",
"period": 100,
"_zWindow": "Finestra mobile su cui si normalizza lo spread. 1500 barre da 15 minuti sono circa 15 giorni. Deve essere molte volte l'emivita del rientro (qui 30-40 barre), altrimenti la media insegue lo scostamento e lo cancella.",
"zWindow": 1500,
"_band": "Isteresi, non un filtro: si entra il 2% sopra la media e si esce il 2% sotto, così un prezzo appoggiato alla media non genera un'operazione ogni due giorni. Dimezza gli scambi lasciando il rendimento dov'era.",
"band": 0.02,
"_entryZ": "Quante deviazioni standard di scostamento servono per aprire. 2.5-3.0 ha battuto costantemente 2.0.",
"entryZ": 2.5,
"_stop": "Rete per un gap. La vera uscita è la media.",
"stopPct": 0.35,
"_exitZ": "Sotto questo valore lo spread è rientrato: si chiude in guadagno.",
"exitZ": 0.5,
"_cvd": "Gate di order flow, disattivato. Misurato su Binance con il volume taker: alzandolo il Calmar scende da 0,71 a 0,66 a 0,64. Serviva al modello precedente, che operava di rado e poteva permettersi di aspettare conferma; qui ogni barra passata ad aspettare è una barra che non compone. Il valore resta calcolato e registrato nei log.",
"cvdThreshold": 0,
"cvdPeriod": 10,
"cvdNormPeriod": 60,
"_stopZ": "Stop statistico: non è uno stop di prezzo, è l'affermazione che la relazione ha smesso di valere. 6.0 ha battuto 3.5 e 4.0 — uno stop stretto su uno spread che rientra lentamente realizza perdite che sarebbero rientrate.",
"stopZ": 6.0,
"_diagnostics": "Solo per il pannello e i log, non entrano in nessuna decisione.",
"volPeriod": 30,
"barsPerYear": 365,
"atrPeriod": 14,
"_maxPValue": "Soglia di cointegrazione. È il filtro che tiene in piedi tutto: senza, ogni combinazione provata passa da leggermente positiva a 73%/87%.",
"maxPValue": 0.05,
"allowShort": 0
"_halfLife": "Emivita del rientro, in barre. Sotto il minimo lo spread rientra troppo in fretta per ripagare le commissioni; sopra il massimo il capitale resta impegnato più a lungo di quanto duri la relazione.",
"minHalfLife": 3,
"maxHalfLife": 200,
"_maxBarsInTrade": "Stop temporale in barre. 0 disattiva: nei test non ha migliorato nulla, ma è la rete contro il caso peggiore — uno spread che si ferma a metà strada e ci resta per giorni.",
"maxBarsInTrade": 0,
"requireCointegration": 1
}
},
{
"_note": "Layer-1. Verificata sul backtest: negativa sull'intero periodo con questi parametri.",
"symbolA": "SOLUSDT",
"symbolB": "AVAXUSDT",
"enabled": true,
"parameters": {
"zWindow": 1500,
"entryZ": 2.5,
"exitZ": 0.5,
"stopZ": 6.0,
"maxPValue": 0.05,
"minHalfLife": 3,
"maxHalfLife": 200,
"maxBarsInTrade": 0,
"requireCointegration": 1
}
},
{
"_note": "Layer-2 su Ethereum. NON verificata: non avevo dati storici di OP e ARB. È la coppia con le premesse migliori — stesso ecosistema, stessa età, stessi flussi — ma finché non la misuri resta un'ipotesi.",
"symbolA": "OPUSDT",
"symbolB": "ARBUSDT",
"enabled": true,
"parameters": {
"zWindow": 1500,
"entryZ": 2.5,
"exitZ": 0.5,
"stopZ": 6.0,
"maxPValue": 0.05,
"minHalfLife": 3,
"maxHalfLife": 200,
"maxBarsInTrade": 0,
"requireCointegration": 1
}
},
{
"_note": "DeFi blue-chip. NON verificata: non avevo dati storici di LINK e UNI.",
"symbolA": "LINKUSDT",
"symbolB": "UNIUSDT",
"enabled": true,
"parameters": {
"zWindow": 1500,
"entryZ": 2.5,
"exitZ": 0.5,
"stopZ": 6.0,
"maxPValue": 0.05,
"minHalfLife": 3,
"maxHalfLife": 200,
"maxBarsInTrade": 0,
"requireCointegration": 1
}
}
]
@@ -1,12 +0,0 @@
{
"_comment": "Copy this file to encelado.local.json (gitignored) next to encelado.json. It is merged on top of the main config, so it only needs the keys you want to override. Environment variables still win over both.",
"alpaca": {
"keyId": "PK...........",
"secretKey": "................................"
},
"engine": {
"dryRun": true
}
}