La generazione del setup, il versionamento e il caricamento su Gitea passano ora da un solo file MSBuild in build/Release.proj, com'e' gia' per AutoBidder, al posto dello script PowerShell make-installer.ps1. Il cambiamento di sostanza e' da dove viene la versione: dal tag git, non piu' da Directory.Build.props. Il numero arriva a dotnet publish come proprieta' da riga di comando, quindi tag, eseguibile, installatore e release lo portano uguale per costruzione invece che per disciplina. Il tag si crea in fondo, a installatore esistente, e non si crea affatto se l'albero e' sporco: un giro andato male non lascia dietro un tag per una versione mai costruita. Tre adattamenti rispetto ad AutoBidder, segnati sul posto: la versione sta in Directory.Build.props e non nel csproj; la pubblicazione produce una cartella e non un eseguibile unico, perche' l'app legge encelado.json accanto a se', quindi la copia portabile allegata alla release e' uno zip; il target Backtest rigioca serie storiche di prezzi invece dei dossier delle aste. Il setup si chiama Encelado_<versione>.exe e la pubblicazione ora fallisce se nella cartella finiscono sorgenti o simboli di debug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
646 lines
32 KiB
Markdown
646 lines
32 KiB
Markdown
# Encelado
|
||
|
||
Applicazione desktop Windows per il trading automatico su **Alpaca**, scritta in
|
||
**C# / .NET 10 + WPF**. Nessuna dipendenza esterna: né NuGet a runtime, né browser,
|
||
né servizi da tenere vivi.
|
||
|
||
Un eseguibile, una finestra, un bottone.
|
||
|
||

|
||
|
||
```
|
||
Encelado.exe
|
||
│
|
||
├── Stato prezzi, posizioni, P&L, equity, log recente — e AVVIA / FERMA
|
||
├── Conto il conto come lo vede Alpaca
|
||
├── Posizioni dettaglio e chiusura manuale
|
||
├── Ordini storico dal broker
|
||
├── Grafici candele e prezzo in diretta, anche in finestra separata
|
||
├── Log tutto il log, filtrabile e ricercabile
|
||
└── Impostazioni credenziali, cartella dei log, dimensionamento
|
||
```
|
||
|
||
**Un solo asset e una sola strategia, preimpostati.** BTC/USD, long sopra la media a 100
|
||
giorni e flat sotto. Non c'è un menu per sceglierla né uno per fare backtest: la scelta
|
||
è già stata fatta misurando su due dataset indipendenti, e il risultato è dentro la
|
||
configurazione.
|
||
|
||
Sui tredici anni di storia disponibili rende **121% annuo contro il 115% del comprare e
|
||
tenere**, con un drawdown massimo del 74% invece dell'87%. I numeri, i limiti e il
|
||
periodo in cui *perde* contro il buy & hold sono più sotto.
|
||
|
||
---
|
||
|
||
## Avvio
|
||
|
||
```powershell
|
||
dotnet build Encelado.slnx -c Release
|
||
```
|
||
|
||
Poi apri `src\Encelado.Bot\bin\Release\net10.0-windows\Encelado.exe`. Da VS Code: **F5**.
|
||
|
||
Al primo avvio compare la finestra delle credenziali: incolli le due chiavi Alpaca,
|
||
l'app le verifica contro il conto e ti chiede se salvarle. Non serve preparare nulla
|
||
prima — niente variabili d'ambiente, niente file da editare.
|
||
|
||
---
|
||
|
||
## Verifica, pacchetto e rilascio
|
||
|
||
**È la stessa catena di [Mimante/AutoBidder](http://192.168.30.23:3000/Alby96/Mimante):
|
||
un solo file MSBuild in `build/Release.proj`, nessuno script.** I dettagli stanno in
|
||
[`build/README.md`](build/README.md).
|
||
|
||
Da VS Code: **Terminale ▸ Esegui attività…**
|
||
|
||
| Attività | Cosa fa |
|
||
|---|---|
|
||
| `verifica` | Compila e lancia i test. |
|
||
| `backtest` | Rigioca una serie storica di prezzi. |
|
||
| `crea installatore` | Verifica, pubblica, esegue Inno Setup, crea il tag. |
|
||
| `crea installatore (senza rieseguire i test)` | Solo pubblicazione e installatore. |
|
||
| `rilascia su Gitea` | Tutto quanto sopra, più tag e release con i file allegati. |
|
||
|
||
```powershell
|
||
dotnet msbuild build/Release.proj -t:Verifica
|
||
dotnet msbuild build/Release.proj -t:Pacchetto
|
||
dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=3.3.0
|
||
```
|
||
|
||
**La versione viene dal tag git, e da nient'altro.** Non viene mai riscritto un file:
|
||
il numero arriva a `dotnet publish` come proprietà da riga di comando, quindi tag,
|
||
eseguibile, installatore e release portano lo stesso numero per costruzione, non per
|
||
disciplina. Il `<Version>` in `Directory.Build.props` resta quello delle compilazioni
|
||
di sviluppo — è il numero che vedi nella finestra mentre lavori — e serve solo come
|
||
seme al primissimo rilascio, quando non esiste ancora nessun tag.
|
||
|
||
Il tag viene creato **in fondo**, a installatore esistente: un giro andato male non
|
||
lascia dietro un tag per una versione che non è mai stata costruita. E non viene creato
|
||
affatto se ci sono modifiche non committate, perché indicherebbe uno stato non
|
||
ricostruibile.
|
||
|
||
Il pacchetto esce in `bin/installer/`:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `Encelado-<versione>-setup.exe` | L'installatore, ~47 MB |
|
||
| `Encelado-<versione>-portabile.zip` | La cartella pubblicata, per chi non vuole installare niente |
|
||
| Destinazione | `%LOCALAPPDATA%\Programs\Encelado` |
|
||
| UAC | nessuna richiesta |
|
||
| Runtime .NET | incorporato, non serve installarlo |
|
||
|
||
**Perché per utente e non in `Program Files`.** Non è per evitare l'UAC. Encelado
|
||
scrive log, diario operazioni e CSV di analisi accanto al proprio eseguibile: dentro
|
||
`C:\Program Files` quelle scritture fallirebbero, e siccome il logger degrada in
|
||
silenzio piuttosto che bloccare il bot, te ne accorgeresti solo cercando i log per
|
||
capire cosa è successo. Per questo `PrivilegesRequired=lowest` non è sovrascrivibile.
|
||
|
||
**Aggiornamenti.** Reinstallare sopra una versione precedente **non** tocca il tuo
|
||
`encelado.json`: le modifiche fatte dalla scheda Impostazioni sopravvivono. Accanto
|
||
trovi sempre `encelado.default.json` aggiornato, per confrontare la tua configurazione
|
||
con i valori di fabbrica. Se l'applicazione è in esecuzione, l'installatore la chiude.
|
||
|
||
**Disinstallazione.** Rimuove programma e log. Le credenziali Alpaca stanno altrove
|
||
(`%LOCALAPPDATA%\Encelado`) e vengono cancellate solo se rispondi Sì alla domanda
|
||
esplicita — il default è No, così reinstallare non costringe a reinserire le chiavi.
|
||
In modalità silenziosa la domanda non viene posta e le credenziali restano.
|
||
|
||
---
|
||
|
||
## Cosa c'è nella finestra
|
||
|
||
Menu laterale a sinistra, una pagina alla volta a destra. Il bottone **AVVIA / FERMA**
|
||
sta in fondo al menu ed è quindi raggiungibile da qualunque pagina: non serve tornare
|
||
indietro per fermare il bot.
|
||
|
||
| Pagina | Cosa c'è |
|
||
|---|---|
|
||
| **Stato** | La pagina principale, quella su cui la finestra si apre. Riga KPI, prezzo in diretta, posizioni aperte, curva equity della sessione, i valori interni del modello (distanza dalla media, volatilità, order flow), contatori del motore, e una striscia con le ultime righe di log |
|
||
| **Conto** | Il conto come lo vede Alpaca: valore del portafoglio, variazione di oggi, liquidità, potere d'acquisto, leva, operazioni intraday, stato PDT, restrizioni. Sotto, i limiti che il bot si impone da sé — che sono un'altra cosa |
|
||
| **Posizioni** | Tabella completa con prezzo medio, valore, P&L in valuta e in percentuale, stop sorvegliato, barre in posizione, data di apertura, e **Chiudi** su ogni riga |
|
||
| **Ordini** | Lo storico come lo riporta il broker, non come lo ricorda il bot: dopo una riconnessione l'elenco di Alpaca è l'unico completo. Gli ordini ancora aperti sono evidenziati |
|
||
| **Grafici** | Candele delle barre su cui la strategia decide, oppure la linea del prezzo in diretta. Ogni asset si può staccare in una **finestra separata**, da mettere su un secondo monitor |
|
||
| **Log** | Tutto il log in memoria, colorato per livello, con filtro per severità e ricerca testuale. Da qui si apre anche il file su disco |
|
||
| **Impostazioni** | **Ogni valore su cui gira il bot, come campo modificabile.** Ognuno con una spiegazione dettagliata di cosa fa nella strategia e cosa succede se lo cambi. I valori fissati dalla strategia restano campi, con il lucchetto e il motivo. Salva scrivendo solo i valori cambiati in `encelado.json`, conservando commenti e formattazione |
|
||
|
||
Il prezzo in diretta è campionato dallo stream una volta al secondo. Sulle barre
|
||
giornaliere il modello decide una volta al giorno: il grafico in diretta serve a vedere
|
||
cosa succede *fra* una decisione e l'altra, non a suggerirne un'altra.
|
||
|
||
**La cornice della finestra è quella di Windows.** Nessun `WindowStyle="None"`, nessun
|
||
`WindowChrome`: barra del titolo, bordi, angoli, snap layout e i tre pulsanti sono di
|
||
sistema. L'unica cosa impostata è l'attributo DWM che chiede a Windows di disegnare *la
|
||
sua* barra in scuro invece che in chiaro, per non avere una striscia grigia sopra una
|
||
finestra quasi nera.
|
||
|
||
---
|
||
|
||
## La strategia, e cosa dice davvero il backtest
|
||
|
||
**Long sopra la media a 100 giorni, flat sotto.** Non c'è altro.
|
||
|
||
La semplicità non è pigrizia: è il risultato della misura. Il modello precedente
|
||
mescolava trend e mean-reversion, filtrava gli ingressi con un efficiency ratio e
|
||
trascinava uno stop a 5 ATR. Su tredici anni di BTC rendeva il **20,1% annuo** mentre
|
||
comprare e non fare niente rendeva il **114,8%**. Ogni singolo pezzo era difendibile; il
|
||
risultato era un sesto del mercato.
|
||
|
||
### L'aritmetica che decide la questione
|
||
|
||
Alpaca sulle crypto è solo spot: niente leva, il massimo è 1x. Una regola long/flat non
|
||
può quindi possedere più di quanto possieda il mercato, e ogni giorno passato fuori è un
|
||
giorno di capitalizzazione regalato. Contro un asset salito ventiduemila volte, una
|
||
regola del genere batte il comprare e tenere **solo se i giorni in cui sta fuori sono
|
||
sproporzionatamente quelli brutti**. Non può guadagnare più del mercato: può soltanto
|
||
evitarlo meglio.
|
||
|
||
Questo ribalta cosa sia un buon segnale. La variabile che conta non è la raffinatezza
|
||
del filtro, è il **tempo passato dentro**. Un modello che opera di rado e tiene poco è
|
||
strutturalmente incapace di stare al passo, per quanto buoni siano i suoi ingressi. La
|
||
furbizia che accorcia il periodo di detenzione non è gratis: su un asset così è la cosa
|
||
più cara del libro.
|
||
|
||
### La regola
|
||
|
||
```
|
||
distanza = (prezzo − SMA100) / SMA100
|
||
|
||
flat → long quando distanza ≥ +2%
|
||
long → flat quando distanza ≤ −2%
|
||
```
|
||
|
||
La banda del 2% è **isteresi, non un filtro**: si entra sopra e si esce sotto, così un
|
||
prezzo appoggiato alla media non genera un'operazione ogni due giorni. Dimezza gli
|
||
scambi lasciando il rendimento dov'era.
|
||
|
||
Nessun trailing stop. Nessun target. Nessun time stop. Ognuna di quelle cose chiude la
|
||
posizione durante i movimenti che la strategia esiste per catturare. Lo stop al 35% c'è
|
||
solo come rete per un gap: la vera uscita è la media che cede.
|
||
|
||
Quando è dentro, è dentro **per intero** (`risk.stakePct: 1.0`). Qualunque frazione
|
||
inferiore perde la gara con il comprare e tenere in partenza.
|
||
|
||
### Risultati
|
||
|
||
Due dataset indipendenti, commissione 25 bps e slippage 8 bps per eseguito.
|
||
|
||
**Bitstamp, 2012-2025** — 4.756 barre giornaliere ripiegate da 6,8 milioni di barre da
|
||
un minuto:
|
||
|
||
| | Encelado | Compra e tieni |
|
||
|---|---|---|
|
||
| CAGR | **121,02%** | 114,89% |
|
||
| Drawdown massimo | **73,8%** | 86,7% |
|
||
| Calmar | **1,64** | 1,33 |
|
||
| Operazioni | 34 in 13 anni | 1 |
|
||
|
||
**Binance, 2017-2026** — dataset indipendente, exchange diverso, nessun parametro scelto
|
||
guardandolo:
|
||
|
||
| | Encelado | Compra e tieni |
|
||
|---|---|---|
|
||
| CAGR | **47,13%** | 35,24% |
|
||
| Drawdown massimo | **67,7%** | 83,5% |
|
||
| Calmar | **0,70** | 0,42 |
|
||
| Operazioni | 22 in 9 anni | 1 |
|
||
|
||
Batte il comprare e tenere **su entrambi i dataset, sia sul rendimento sia sul
|
||
drawdown**. Rispetto al modello precedente il CAGR passa da 20,1% a 121,0%.
|
||
|
||
Per periodo, su Bitstamp:
|
||
|
||
| | 2012-2018 | 2019-2025 |
|
||
|---|---|---|
|
||
| CAGR | 165,81% | 69,25% |
|
||
| Drawdown | 73,8% | **43,1%** |
|
||
| Calmar | 2,25 | 1,61 |
|
||
|
||
> **Il caveat onesto.** Nel periodo 2019-2025 il comprare e tenere ha reso il 73,8%
|
||
> contro il 69,25% della strategia: sul rendimento, negli ultimi sei anni, perde di
|
||
> poco. Quello che compra con quella rinuncia è il drawdown, 43% contro 76%. Il
|
||
> vantaggio sul rendimento arriva dai tredici anni pieni e dal dataset Binance, non
|
||
> dall'ultimo tratto.
|
||
|
||
### Perché 100 giorni
|
||
|
||
Non perché è il migliore su un file, ma perché è **l'unico che vince su entrambi**.
|
||
|
||
| Media | Bitstamp 13 anni | Binance 9 anni |
|
||
|---|---|---|
|
||
| SMA80 | +2,3 punti | +1,0 punti |
|
||
| **SMA100** | **+7,8 punti** | **+12,5 punti** |
|
||
| SMA120 | +18,6 punti | **−2,2 punti** |
|
||
| SMA200 | −7,2 punti | −6,9 punti |
|
||
|
||
SMA120 rende molto di più su Bitstamp e **perde** su Binance: è taratura sul singolo
|
||
campione. La riga dei 100 giorni batte il comprare e tenere a **qualunque** banda su
|
||
entrambi i file, il che è la forma di un effetto reale invece che di un punto fortunato.
|
||
|
||
### Il filtro di order flow: misurato e scartato
|
||
|
||
Il documento insiste sul Cumulative Volume Delta. L'ho implementato e messo alla prova
|
||
sul file Binance, l'unico con il volume taker, sopra il filtro di prezzo:
|
||
|
||
| Soglia CVD | CAGR | Calmar |
|
||
|---|---|---|
|
||
| disattivato | 47,66% | **0,70** |
|
||
| ≥ 0,0 | 48,31% | 0,71 |
|
||
| ≥ 0,3 | 45,02% | 0,66 |
|
||
| ≥ 0,6 | 43,77% | 0,64 |
|
||
|
||
**Stringerlo peggiora, in modo monotono.** E il motivo è lo stesso di prima: il filtro
|
||
si era guadagnato il posto nel modello precedente, che operava di rado e poteva
|
||
permettersi di aspettare conferma. Qui ogni barra passata ad aspettare è una barra che
|
||
non compone.
|
||
|
||
Resta calcolato, registrato nei log e configurabile (`cvdThreshold`, default 0), perché
|
||
il valore è comunque informativo e un dataset futuro potrebbe dire altro. Ma non entra
|
||
in nessuna decisione.
|
||
|
||
Stessa sorte per il VWAP: aggiungerlo lascia il risultato identico a tre decimali,
|
||
perché un prezzo sopra la media a 100 giorni è quasi sempre anche sopra il VWAP a 20 —
|
||
il filtro non discrimina mai.
|
||
|
||
### Perché non è un bot ad alta frequenza, e perché cambiare exchange non aiuta
|
||
|
||
Stessi parametri, stesso file, solo barre più corte — e tre ipotesi di costo: Alpaca
|
||
(25 bps), Binance spot (10 bps) e **zero**, che nessun exchange al mondo può battere.
|
||
|
||
| Barre | Alpaca 25 bps | Binance 10 bps | Costo zero |
|
||
|---|---|---|---|
|
||
| **1 giorno** | **5,54** | **5,89** | **6,14** |
|
||
| 12 ore | 2,90 | 3,15 | 3,33 |
|
||
| 4 ore | 1,84 | 2,04 | 2,20 |
|
||
| 1 ora | 1,27 | 1,46 | 1,64 |
|
||
| 15 minuti | 1,03 | 1,13 | 1,25 |
|
||
| 5 minuti | **0,25** | **0,42** | **0,78** |
|
||
| 1 minuto | **0,28** | **0,28** | **0,30** |
|
||
|
||
*(profit factor: sopra 1 guadagna, sotto 1 perde)*
|
||
|
||
La colonna che risolve la questione è la terza. **Anche operando gratis, sotto i cinque
|
||
minuti si perde**: a 1 minuto il profit factor è 0,30 e il capitale scende del 99,7%.
|
||
|
||
Quindi il crollo **non è un problema di commissioni**, ed è per questo che cambiare
|
||
exchange non lo risolve. Commissioni più basse alzano ogni riga della tabella — Binance
|
||
migliorerebbe il giornaliero da 121,0% a 123,2% di CAGR — ma non spostano il punto in cui
|
||
il segno cambia. Sotto i quindici minuti il prezzo di BTC non contiene informazione
|
||
direzionale che questo modello sappia sfruttare: è rumore, e pagarlo meno non lo rende
|
||
segnale.
|
||
|
||
A questo si aggiunge un limite fisico. Da una connessione domestica in Italia il
|
||
round-trip verso i motori di matching di Binance (AWS Tokyo) è di **250-280 ms**: i
|
||
100 ms non sono raggiungibili senza un VPS in colocation in Asia. E a quella scala la
|
||
controparte è un market maker con hardware dedicato a pochi metri dal matching engine —
|
||
il lato sfavorito della negoziazione sarebbe il nostro, riempiti esattamente quando il
|
||
prezzo sta per muoversi contro.
|
||
|
||
**Il bot resta su Alpaca e su barre giornaliere**, che è dove i numeri sono migliori.
|
||
|
||
### Cosa del documento non è stato applicato, e perché
|
||
|
||
| Proposta | Esito |
|
||
|---|---|
|
||
| Scalping su Order Book Imbalance, micro-price, OFI | **Impossibile.** Servono i volumi per livello del book. Alpaca sulle crypto pubblica il top of book, non la profondità. Senza $V^{bid}$ e $V^{ask}$ per livello non c'è nulla da calcolare |
|
||
| Arbitraggio statistico BTC/ETH | **Impossibile.** La gamba corta richiede lo short, che Alpaca non consente sulle crypto. La versione "solo spot" del documento — ribilanciare i pesi invece di shortare — non è arbitraggio: è una scommessa direzionale sul rapporto, con il rischio di entrambe le gambe e la copertura di nessuna |
|
||
| Breakout su CVD/VWAP | **Implementato e misurato.** Vedi sopra: peggiora questa strategia |
|
||
| Rischio 1% per operazione, stop 0,7% | **Non applicabile.** Sono tarati su stop intraday dello 0,2-0,7%. Su barre giornaliere con uscite al 20-40% darebbero posizioni del 2-5% del conto e un CAGR sotto il 3% |
|
||
| Circuit breaker giornaliero | **Applicato**, al 25% invece che al 2%: su BTC un −20% in un giorno è successo più volte e non è una ragione per liquidare sul minimo |
|
||
| TPL Dataflow per la pipeline | **Equivalente già presente.** `System.Threading.Channels`, stessa semantica con meno allocazioni |
|
||
| SDK `Alpaca.Markets` | **Non adottato.** Il client REST e WebSocket è scritto a mano e non ha dipendenze NuGet a runtime |
|
||
|
||
---
|
||
|
||
## Perché 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 — i parametri di ETH erano copiati da quelli di BTC e non erano mai stati
|
||
validati su dati ETH.
|
||
|
||
Resta scelto per tre ragioni concrete:
|
||
|
||
- **Mercato aperto 24/7.** Nessun gap di apertura, nessun rischio overnight, nessuna
|
||
regola PDT.
|
||
- **Frazionabile.** Un conto da poche migliaia di euro può dimensionare con precisione.
|
||
- **Storia lunga e liquida.** Tredici anni di dati su più exchange indipendenti, che è
|
||
ciò che rende verificabile tutto quanto sopra.
|
||
|
||
---
|
||
|
||
## Credenziali
|
||
|
||
Risolte in quest'ordine, vince la prima che c'è:
|
||
|
||
1. variabili d'ambiente `APCA_API_KEY_ID` / `APCA_API_SECRET_KEY`
|
||
2. chiavi salvate dalla finestra di login
|
||
3. `alpaca.keyId` / `alpaca.secretKey` nel file di configurazione
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Percorso** | `%LOCALAPPDATA%\Encelado\credentials.dat` (override con `ENCELADO_HOME`) |
|
||
| **Protezione** | Cifrate con **DPAPI**, legate al tuo account Windows: nessuna passphrase a ogni riavvio, e un altro utente sulla stessa macchina non può leggerle |
|
||
| **Nei log** | Mai. Solo la chiave mascherata (`PKVU************`); il secret non viene mai stampato |
|
||
|
||
Paper e live hanno chiavi diverse e vengono salvate separatamente. Le chiavi non
|
||
finiscono mai nel repository.
|
||
|
||
---
|
||
|
||
## Configurazione
|
||
|
||
**Dalla scheda Impostazioni**, che è la via normale: ogni valore è un campo, ogni campo
|
||
ha una spiegazione di cosa fa nella strategia e di cosa succede se lo cambi, e i valori
|
||
che la strategia fissa sono mostrati con il lucchetto e il motivo — non nascosti.
|
||
|
||
Il salvataggio riscrive **solo i valori cambiati**, lasciando intatto il resto del file
|
||
compresi i commenti. Prima di scrivere applica le modifiche a una copia temporanea e ci
|
||
fa girare i validatori veri: una combinazione che il bot rifiuterebbe all'avvio viene
|
||
respinta subito, con il motivo, e sul disco non finisce niente.
|
||
|
||
Il file resta [`config/encelado.json`](config/encelado.json), copiato accanto
|
||
all'eseguibile durante la build, ed è modificabile a mano per tutto ciò che la pagina
|
||
non copre. Le chiavi che iniziano con `_` sono commenti: JSON non ne ha, e un file pieno
|
||
di assunzioni di trading ne ha bisogno.
|
||
|
||
I parametri principali, con i valori consegnati:
|
||
|
||
| Chiave | Valore | Perché |
|
||
|---|---|---|
|
||
| `engine.timeFrame` | `1Day` | Sotto i 15 minuti il segnale non esiste, nemmeno a costo zero |
|
||
| `engine.warmupBars` | `220` | La media è a 100 giorni, il resto è margine |
|
||
| `parameters.period` | `100` | L'unica media che batte il buy & hold su **entrambi** i dataset |
|
||
| `parameters.band` | `0.02` | Isteresi: dimezza gli scambi senza toccare il rendimento |
|
||
| `parameters.stopPct` | `0.35` | Rete per un gap. La vera uscita è la media |
|
||
| `risk.stakePct` | `1.0` | Dentro per intero: qualsiasi frazione perde la gara col buy & hold |
|
||
| `risk.maxDailyLossPct` | `0.25` | Kill switch. Largo perché su BTC un −20% in un giorno non è una ragione per liquidare sul minimo |
|
||
|
||
**Nessun limite di frequenza.** `maxOpenPositions`, `maxTradesPerDay` e
|
||
`maxTradesPerSymbolPerDay` sono a `0`, che significa *nessun tetto*: a fermare il bot è
|
||
la strategia, non un contatore. Erano una rete contro un bug — un ciclo che riapre la
|
||
stessa posizione mille volte costa mille commissioni — e con `0` quella rete non c'è
|
||
più. Il kill switch sulla perdita giornaliera resta attivo e non è toccato da questi.
|
||
|
||
Con un solo simbolo il numero di posizioni contemporanee resta comunque 1: 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. Quel limite torna a
|
||
contare quando si aggiungono simboli.
|
||
|
||
Le modifiche hanno effetto al riavvio dell'applicazione.
|
||
|
||
### `risk.stakePct` e `risk.stakeAmount` — decidere quanto puntare
|
||
|
||
Di default la dimensione la sceglie il bot: rischia una frazione fissa dell'equity
|
||
(`maxRiskPerTradePct`) fra ingresso e stop, e ricava la quantità da lì. Con questo
|
||
criterio ogni operazione perde più o meno lo stesso importo quando va male, ma la
|
||
dimensione cambia da un'operazione all'altra.
|
||
|
||
Per fissarla tu:
|
||
|
||
| `stakePct` | `stakeAmount` | Cosa succede |
|
||
|---|---|---|
|
||
| `0` | `0` | **Default.** Decide il bot, come sopra. È la configurazione testata |
|
||
| `0.20` | `0` | Ogni ingresso impegna il 20% dell'equity |
|
||
| `0` | `2500` | Ogni ingresso impegna 2.500, qualunque sia il saldo |
|
||
| `0.20` | `2500` | Il 20% dell'equity, ma **mai più di 2.500** |
|
||
|
||
La percentuale è del saldo del conto e non può superare l'importo: è la regola che hai
|
||
chiesto. Se metti solo l'importo, quello diventa fisso.
|
||
|
||
Due conseguenze che vale la pena sapere prima di impostarli:
|
||
|
||
- **La convinzione della strategia non riduce più la dimensione.** Hai chiesto un
|
||
importo, e quello viene usato anche su un segnale debole.
|
||
- **Lo stop protegge ma non dimensiona più.** Un ingresso con stop al 40% e uno con
|
||
stop al 10% avranno la stessa dimensione, e il primo può perdere quattro volte tanto.
|
||
È esattamente ciò che il dimensionamento a rischio evitava.
|
||
|
||
`stakePct` più alto di `maxPositionNotionalPct` **non parte**: l'applicazione si ferma
|
||
con un errore invece di tagliare in silenzio la dimensione a un valore che non hai
|
||
chiesto.
|
||
|
||
### `logging` — il materiale per migliorare il bot
|
||
|
||
| Chiave | Default | Significato |
|
||
|---|---|---|
|
||
| `level` | `debug` | `trace` / `debug` / `info` / `warn` / `error` / `none`. Con `debug` finiscono nel log anche i segnali scartati e i rifiuti del risk engine, cioè il *perché il bot non ha fatto* qualcosa |
|
||
| `directory` | `logs` | **Dove salvare tutti gli output.** Relativa all'eseguibile, oppure assoluta (`D:\encelado-logs`). Si cambia anche da **Impostazioni → Log**, che verifica di potervi scrivere davvero prima di salvare, e scrive la scelta in `encelado.json` conservando ogni altra chiave e ogni commento |
|
||
| `file` | `encelado.log` | Log applicativo |
|
||
| `maxFileSizeMb` / `maxFiles` | `32` / `10` | Rotazione: `encelado.log` → `encelado.1.log` → … |
|
||
| `decisionLog` | `decisions.csv` | **Una riga per ogni barra valutata** |
|
||
| `executionLog` | `executions.csv` | **Una riga per ogni segnale arrivato agli ordini** |
|
||
| `tradeJournal` | `trades.jsonl` | Una riga JSON per evento d'ordine |
|
||
| `logMarketData` | `false` | Con `level: trace`, registra ogni quotazione e ogni print. File enormi |
|
||
| `statusLines` | `200` | Righe tenute dalla striscia **Attività** nella pagina Stato |
|
||
| `bufferedLines` | `5000` | Righe tenute dalla scheda **Log**. È il tetto di memoria del log in-app: qualche megabyte. Il file su disco resta completo comunque, e la scheda lo può aprire |
|
||
|
||
`decisions.csv` contiene, per ogni barra e per ogni strumento: OHLCV completo,
|
||
**volume taker-buy e delta**, ogni indicatore che la strategia pubblica (score, regime,
|
||
trend, reversion, cvd, volatilità, scalare di volatilità), la posizione al momento
|
||
(quantità, ingresso, P&L, barre in trade, stop, target), il segnale prodotto con forza
|
||
e motivazione, bid/ask/spread, età della quotazione, equity, e se la sessione era
|
||
aperta o il kill switch attivo.
|
||
|
||
`executions.csv` aggiunge, per i segnali che sono arrivati agli ordini: la fase
|
||
(`suppressed` / `risk` / `order`), l'esito, il motivo del rifiuto, la quantità
|
||
approvata, il controvalore, e la latenza in millisecondi.
|
||
|
||
I due file si uniscono su `decisionId`:
|
||
|
||
```python
|
||
import pandas as pd
|
||
d = pd.read_csv("logs/decisions.csv", parse_dates=["barTimeUtc"])
|
||
e = pd.read_csv("logs/executions.csv")
|
||
df = d.merge(e, on="decisionId", how="left", suffixes=("", "_exec"))
|
||
|
||
# Quali segnali sono stati bloccati, e da cosa?
|
||
df[df.signal != "None"].groupby(["signal", "riskReason"]).size()
|
||
|
||
# Il filtro di order flow sta scartando operazioni che sarebbero state buone?
|
||
df[(df.score > 0.25) & (df.cvd < 0.30)][["barTimeUtc", "close", "score", "cvd"]]
|
||
```
|
||
|
||
CSV e non JSON di proposito: si apre in Excel, si carica in una riga di pandas, e resta
|
||
leggibile quando una sessione lunga produce decine di migliaia di righe.
|
||
|
||
---
|
||
|
||
## Sicurezza operativa
|
||
|
||
- **Paper è il default.** Con `alpaca.paper = false` l'app chiede conferma esplicita
|
||
prima di avviare il motore su denaro reale.
|
||
- **Kill switch giornaliero** su `maxDailyLossPct`: chiude tutto e non riapre fino alla
|
||
sessione successiva.
|
||
- **Idempotenza**: ogni ordine porta un `client_order_id` univoco, quindi un retry di
|
||
rete non può duplicare una posizione.
|
||
- **Riconciliazione REST ogni 30 s**: lo stream WebSocket è la via veloce, non la
|
||
verità. Il book locale viene riallineato a `/v2/positions`.
|
||
- **Latch per simbolo**: un solo ingresso in volo per strumento.
|
||
- **Alpaca non supporta i bracket order sulle crypto**, quindi stop e target sono
|
||
gestiti dall'engine e controllati a ogni quotazione. Se il processo muore con una
|
||
posizione aperta, **quella posizione resta senza stop sul broker**. È il compromesso
|
||
principale della scelta crypto.
|
||
|
||
### Quanto spesso decide, davvero
|
||
|
||
Su barre giornaliere il modello valuta **una volta al giorno**, alla chiusura di ogni
|
||
barra, e cambia stato circa cinque volte l'anno. Due giorni senza operazioni sono
|
||
normali: la detenzione media è di **81 giorni**.
|
||
|
||
Fino alla 3.1.0 però c'era anche un difetto vero, ed era peggio di "opera di rado".
|
||
Le barre della strategia venivano costruite accumulando le barre da un minuto dello
|
||
stream: con 1440 minuti per barra, l'aggregatore chiudeva un secchiello **solo quando
|
||
arrivava un minuto appartenente al successivo**, cioè a mezzanotte UTC, e solo se il
|
||
processo era vivo in quell'istante. Avviato alle 10 e chiuso alle 18, il bot riceveva
|
||
migliaia di quotazioni, contava decine di barre e **non valutava la strategia nemmeno una
|
||
volta**. Nel log si vedeva così:
|
||
|
||
```
|
||
bars=40 signals=0 decisions.csv: 0 righe
|
||
```
|
||
|
||
Ora, a ogni riconciliazione, il bot chiede al broker le ultime barre **già chiuse** al
|
||
timeframe della strategia e valuta quelle che non ha ancora visto. Non dipende più da
|
||
quando è acceso, e decide sulle chiusure giornaliere vere invece che su una giornata
|
||
parziale ricostruita dai minuti in cui era connesso — che è anche ciò che il backtest
|
||
misura.
|
||
|
||
### Come si vede che sta lavorando
|
||
|
||
Un bot che decide una volta al giorno, visto da fuori, è indistinguibile da un bot
|
||
bloccato. Tre cose lo rendono distinguibile:
|
||
|
||
**Ogni barra in arrivo finisce nel registro** (`logging.logEveryBar`), non solo quelle
|
||
che chiudono una barra della strategia:
|
||
|
||
```
|
||
[BTC/USD] barra 14:32 O 96.410,22 H 96.480,00 L 96.388,10 C 96.455,70 vol 3,1420 (+45,48 +0,05%)
|
||
```
|
||
|
||
**Un battito ogni cinque secondi** (`engine.explainSeconds`) dice cosa farebbe adesso e
|
||
cosa aspetta — scritto solo quando cambia, quindi non riempie il registro di ripetizioni:
|
||
|
||
```
|
||
[BTC/USD] FERMO — prezzo 96.456 è −1,17% dalla media 97.600. Per comprare serve che
|
||
superi 99.552, cioè +3,21% da qui.
|
||
```
|
||
|
||
**Ogni decisione è al livello info**, non nascosta nel debug, e dice il perché in
|
||
entrambi i casi:
|
||
|
||
```
|
||
[BTC/USD] barra 1Day CHIUSA 2026-08-04 00:00 C 99.812,40 — valuto la strategia
|
||
[BTC/USD] COMPRO — prezzo sopra la media di +2,27%
|
||
```
|
||
|
||
### Se lo stream dati viene rifiutato
|
||
|
||
Alpaca consente **una sola connessione ai dati di mercato per conto**. Se un'altra
|
||
istanza di Encelado è viva, o se una è stata chiusa male e il server non ha ancora
|
||
liberato lo slot, il nuovo tentativo riceve `406 connection limit exceeded`.
|
||
|
||
Il bot ora se ne accorge, lo dice in cima alla pagina Stato invece di lasciarlo nel file
|
||
di log, chiude subito il socket rifiutato e aspetta oltre i dieci secondi che il server
|
||
impiega a liberare uno slot non autenticato.
|
||
|
||
Quest'ultimo punto è il motivo per cui prima non si riprendeva da solo. Riconnettendosi
|
||
ogni tre secondi, il client apriva il socket successivo **mentre quello rifiutato teneva
|
||
ancora occupato l'unico slot disponibile**: si negava il servizio da sé, indefinitamente.
|
||
A peggiorare le cose, il contatore dei fallimenti veniva azzerato all'apertura del
|
||
socket anziché quando la sessione andava davvero a buon fine, così il backoff
|
||
esponenziale non cresceva mai oltre il primo gradino.
|
||
|
||
Se lo vedi: chiudi le altre istanze e aspetta. Si riprende da solo.
|
||
|
||
---
|
||
|
||
## Struttura
|
||
|
||
```
|
||
src/
|
||
Encelado.Core/ modelli, indicatori, strategie, risk engine, portfolio, backtest
|
||
Encelado.Alpaca/ client REST e stream WebSocket scritti su misura
|
||
Encelado.Bot/ l'applicazione WPF
|
||
Ui/ view model, convertitori, grafici, tema
|
||
Pages/ le sette pagine del menu laterale
|
||
Theme.xaml tavolozza, stili e template dei controlli
|
||
Assets/ icona multi-risoluzione (16→256 px)
|
||
tests/
|
||
Encelado.Tests/ 309 test
|
||
tools/
|
||
Encelado.Backtest/ banco di prova, non fa parte dell'applicazione
|
||
config/
|
||
encelado.json la configurazione consegnata
|
||
build/
|
||
Release.proj verifica, pacchetto e rilascio — un file MSBuild, nessuno script
|
||
Encelado.iss script Inno Setup, lanciato da Release.proj
|
||
gitea.example.json modello per gitea.json, che resta fuori dal repository
|
||
```
|
||
|
||
### Il banco di prova
|
||
|
||
`tools/Encelado.Backtest` non finisce nell'installer e non ha un menu nel bot: serve a
|
||
rispondere alla sola domanda che conta prima di toccare una taratura, cioè *questa
|
||
modifica regge su dati che non ho usato per farla?*
|
||
|
||
I parametri li legge da `config/encelado.json`, non li ridichiara: il banco e il bot non
|
||
possono divergere senza che qualcuno se ne accorga.
|
||
|
||
```powershell
|
||
dotnet build tools/Encelado.Backtest -c Release
|
||
$bt = "tools/Encelado.Backtest/bin/Release/net10.0/backtest.exe"
|
||
|
||
& $bt split --file dati.csv --tf 1d --boundary 2019-01-01 # taratura / verifica
|
||
& $bt walk --file dati.csv --tf 1d # un anno per riga
|
||
& $bt frequency --file dati.csv # 1d contro 15m
|
||
& $bt costs --file dati.csv --tf 1d # sensibilità ai costi
|
||
& $bt sweep --file dati.csv --tf 1d --boundary 2019-01-01 # griglia in parallelo
|
||
& $bt split --file dati.csv --set "minRegime=0.30,trailAtrMult=7.0"
|
||
```
|
||
|
||
Legge file da centinaia di megabyte perché ripiega le barre in streaming: un file da un
|
||
minuto di 342 MB sono 6,8 milioni di barre e mezzo gigabyte di oggetti vivi se caricato
|
||
com'è, meno di cinquemila barre se ripiegato in giorni mentre lo si legge.
|
||
|
||
```powershell
|
||
dotnet test Encelado.slnx
|
||
```
|
||
|
||
### Scelte tecniche
|
||
|
||
| Decisione | Motivo |
|
||
|---|---|
|
||
| **WPF, non un browser** | Applicazione desktop vera: una finestra, un processo, nessun server locale né porta aperta |
|
||
| **Zero dipendenze NuGet a runtime** | DPAPI è nel framework Windows Desktop; client REST e WebSocket sono scritti a mano |
|
||
| **Client Alpaca su misura** | Alpaca serializza i numeri come stringhe (`"qty": "10"`): il parsing manuale è più semplice *e* più veloce dell'SDK |
|
||
| **WebSocket, mai polling** | Barre e fill arrivano in push; il REST serve solo per ordini e riconciliazione |
|
||
| **Indicatori incrementali O(1)** | Aggiornano lo stato, non ricalcolano la finestra |
|
||
| **Backtest in `Encelado.Core`** | Non nella UI: è la stessa libreria che gira in produzione, quindi il replay misura il codice vero |
|
||
|
||
---
|
||
|
||
## Limiti noti
|
||
|
||
- **Su barre giornaliere il bot decide una volta al giorno.** È corretto — è ciò che
|
||
rende il sistema sostenibile rispetto ai costi — ma vederlo "lavorare" è poco
|
||
spettacolare: passano settimane fra un'operazione e l'altra.
|
||
- **Il backtest è su BTC.** I parametri di ETH sono gli stessi con un target di
|
||
volatilità più alto, ma non li ho validati su dati ETH: non ho il file.
|
||
- **Un backtest non è una previsione.** Nove anni sono 43 operazioni, di cui 7 fuori
|
||
campione: statisticamente pochissime. Il risultato fuori campione è coerente con
|
||
quello in campione, il che è incoraggiante, non probante.
|
||
- **Il filtro di order flow dipende dal campo `tks` del feed Alpaca.** Se in futuro
|
||
quel campo sparisse, il filtro si disattiverebbe silenziosamente e il modello
|
||
tornerebbe al comportamento precedente (peggiore, ma non rotto). Il log lo registra:
|
||
la colonna `cvd` in `decisions.csv` resterebbe a zero.
|
||
- Alpaca consente **una sola** connessione market data per account.
|
||
|
||
---
|
||
|
||
## Disclaimer
|
||
|
||
Software per trading automatico. Le strategie incluse sono state validate su dati
|
||
storici, il che non garantisce nulla sul futuro. Usa `paper` finché non capisci
|
||
esattamente cosa fa il bot con i tuoi parametri. Il rischio di perdita è reale e
|
||
interamente tuo.
|