diff --git a/Encelado/.dockerignore b/Encelado/.dockerignore new file mode 100644 index 0000000..2334179 --- /dev/null +++ b/Encelado/.dockerignore @@ -0,0 +1,21 @@ +# Ciò che non serve dentro il contesto di build. .git resta fuori: il commit +# arriva dal build-arg GIT_COMMIT (vedi build/Release.proj e la Dockerfile). +.git +.gitignore +.vscode +.claude +**/bin +**/obj +**/TestResults +bin +obj +results +reports +docs/img +deploy/local +build/gitea.json +*.md +!docs/**/*.md +Modifiche.txt +*.local.json +.env diff --git a/Encelado/.gitignore b/Encelado/.gitignore index 314ef13..5028c28 100644 --- a/Encelado/.gitignore +++ b/Encelado/.gitignore @@ -19,3 +19,6 @@ build/gitea.json # Output dei test (coverlet) TestResults/ + +# Volumi di docker-compose per le prove in locale (configurazione e dati del container) +deploy/local/ diff --git a/Encelado/Dockerfile b/Encelado/Dockerfile new file mode 100644 index 0000000..0600be6 --- /dev/null +++ b/Encelado/Dockerfile @@ -0,0 +1,90 @@ +# syntax=docker/dockerfile:1.7 +# +# Encelado — immagine del bot (motore + interfaccia web su Kestrel). +# +# Tre stadi: build (restore + compilazione), test (la suite xunit gira dentro la +# build: un'immagine con i test rossi non esiste), publish (framework-dependent, +# non trimmed: il server ospita ASP.NET Core minimal e non è AOT). Il runtime è +# l'immagine ufficiale aspnet, con gosu per scendere ai PUID/PGID di Unraid. +# +# docker build -t 192.168.30.23:3000/alby96/encelado:5.0.0 --build-arg GIT_COMMIT=$(git rev-parse --short=12 HEAD) . +# docker run -p 8080:8080 -v ./config:/config -v ./data:/data -e TZ=Europe/Rome 192.168.30.23:3000/alby96/encelado:5.0.0 +# +# Vedi docs/DOCKER.md per volumi, variabili, healthcheck e aggiornamento. + +ARG DOTNET_VERSION=10.0 + +# ---------------------------------------------------------------- build +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build +ARG GIT_COMMIT=n/d +WORKDIR /src + +# Prima i file di progetto, così il restore resta in cache finché non cambiano. +COPY Directory.Build.props Encelado.slnx ./ +COPY src/Encelado.Core/Encelado.Core.csproj src/Encelado.Core/ +COPY src/Encelado.Etoro/Encelado.Etoro.csproj src/Encelado.Etoro/ +COPY src/Encelado.Engine/Encelado.Engine.csproj src/Encelado.Engine/ +COPY src/Encelado.Server/Encelado.Server.csproj src/Encelado.Server/ +COPY tools/Encelado.Backtest/Encelado.Backtest.csproj tools/Encelado.Backtest/ +COPY tests/Encelado.Tests/Encelado.Tests.csproj tests/Encelado.Tests/ +RUN dotnet restore Encelado.slnx + +COPY . . +RUN dotnet build Encelado.slnx -c Release --no-restore -p:GitCommit=${GIT_COMMIT} + +# ---------------------------------------------------------------- test +FROM build AS test +RUN dotnet test tests/Encelado.Tests -c Release --no-build --nologo -v q + +# ---------------------------------------------------------------- publish +FROM build AS publish +ARG GIT_COMMIT=n/d +# Dipende dallo stadio test solo per ordine: la publish parte se i test sono verdi. +COPY --from=test /src/tests/Encelado.Tests/bin/Release/net10.0/Encelado.Tests.dll /tmp/tests-ok +RUN dotnet publish src/Encelado.Server/Encelado.Server.csproj -c Release --no-build --no-restore -p:GitCommit=${GIT_COMMIT} -o /app + +# ---------------------------------------------------------------- runtime +FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS runtime +ARG VERSION=dev +ARG GIT_COMMIT=n/d +LABEL org.opencontainers.image.title="Encelado" \ + org.opencontainers.image.description="Correlation Baskets su eToro: motore, ledger e interfaccia web" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.revision="${GIT_COMMIT}" \ + org.opencontainers.image.authors="Alberto Balbo" \ + org.opencontainers.image.source="http://192.168.30.23:3000/Alby96/Encelado" + +# gosu per cambiare utente dopo aver sistemato i permessi; tzdata per TZ; curl +# non serve: il healthcheck è il server stesso con --health. +RUN apt-get update \ + && apt-get install -y --no-install-recommends gosu tzdata ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +ENV ENCELADO_IN_CONTAINER=1 \ + ENCELADO_WEB_PORT=8080 \ + ENCELADO_AUTOSTART=1 \ + DOTNET_gcServer=1 \ + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \ + ASPNETCORE_URLS= \ + ASPNETCORE_HTTP_PORTS= \ + TZ=UTC \ + PUID=99 \ + PGID=100 + +WORKDIR /app +COPY --from=publish /app ./ +COPY deploy/docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh && mkdir -p /config /data + +VOLUME ["/config", "/data"] +EXPOSE 8080 + +# Il server risponde da solo alla sonda: GET /api/health sulla porta configurata. +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD ["dotnet", "/app/Encelado.Server.dll", "--health"] + +# SIGTERM arriva al processo dotnet (exec nell'entrypoint): stop pulito, ledger +# chiuso, posizioni lasciate sul conto con gli stop nativi (closeOnShutdown). +STOPSIGNAL SIGTERM +ENTRYPOINT ["/entrypoint.sh"] +CMD ["dotnet", "/app/Encelado.Server.dll"] diff --git a/Encelado/assets/encelado.png b/Encelado/assets/encelado.png new file mode 100644 index 0000000..9bcf26a Binary files /dev/null and b/Encelado/assets/encelado.png differ diff --git a/Encelado/build/README.md b/Encelado/build/README.md index fcddc51..2aae0ca 100644 --- a/Encelado/build/README.md +++ b/Encelado/build/README.md @@ -1,23 +1,23 @@ # Catena di verifica, pacchetto e rilascio Tutto quello che serve a controllare, impacchettare e pubblicare Encelado sta in questa -cartella. La radice del progetto non contiene script. +cartella. La radice del progetto contiene solo la `Dockerfile` e la `docker-compose.yml`. È la stessa catena di [Mimante/AutoBidder](http://192.168.30.23:3000/Alby96/Mimante), -adattata a una soluzione con più progetti. Le differenze sono tre, tutte segnate sul -posto in `Release.proj`: +adattata a una soluzione con più progetti e a un pacchetto che è un'immagine Docker +(dalla 5.0, ADR-0008). Le differenze sono segnate sul posto in `Release.proj`: | | AutoBidder | Encelado | |---|---|---| | Dove sta la versione | `AutoBidder.csproj` | `Directory.Build.props`, ereditato da tutti i progetti | -| Cosa produce `dotnet publish` | un eseguibile unico | una cartella: l'app legge `encelado.json` accanto a sé | -| Copia portabile allegata | il solo `.exe` | uno zip della cartella | -| Cosa rigioca `Backtest` | i dossier delle aste | barre M15 bid/ask dei basket | +| Cosa produce `dotnet publish` | un eseguibile unico | una cartella framework-dependent: la stessa che il `Dockerfile` mette in `/app` | +| Il pacchetto | installatore Inno Setup | immagine Docker `192.168.30.23:3000/alby96/encelado:` (+ `:latest`) | +| Allegati della release | `.exe` e installatore | zip portabile e template Unraid | +| Cosa rigioca `Backtest` | i dossier delle aste | barre M15 bid/ask dei basket (+ `learn` sul ledger) | | File | Cos'è | |---|---| | `Release.proj` | La catena. Un solo file MSBuild, nessuno script. | -| `Encelado.iss` | Lo script di Inno Setup. Non si compila a mano: lo lancia `Release.proj`. | | `gitea.example.json` | Modello per `gitea.json` (che è escluso dal controllo di versione). | ## Da VS Code @@ -27,43 +27,49 @@ posto in `Release.proj`: | Attività | Cosa fa | |---|---| | `verifica` | Compila e lancia i test. | -| `backtest` | Ricerca sui basket: `ticks`, `baskets`, `falsify`. | -| `crea installatore` | Chiede la versione, verifica, pubblica, esegue Inno Setup. | -| `crea installatore (senza rieseguire i test)` | Solo pubblicazione e installatore. | -| `rilascia su Gitea` | Tutto quanto sopra, più tag e release con i file allegati. | +| `backtest` | Ricerca sui basket: `ticks`, `baskets`, `falsify`, `learn`. | +| `immagine docker` | Costruisce l'immagine (i test girano dentro la build). Non pubblica. | +| `pacchetto` | Verifica, cartella portabile, immagine, tag a pacchetto pronto. Non tocca Gitea. | +| `rilascia su Gitea` | Tutto quanto sopra, più push dell'immagine sul registro di Gitea e release con gli allegati. | +| `docker compose up` | Il container in locale con le cartelle di `deploy/local/`. | ## Da riga di comando ```powershell dotnet msbuild build/Release.proj -t:Verifica +dotnet msbuild build/Release.proj -t:Docker dotnet msbuild build/Release.proj -t:Pacchetto -dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=3.3.0 -p:Note="Cosa cambia" +dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=5.0.0 -p:Note="Cosa cambia" # La ricerca sui basket: barre M15 in data/market (da `ticks`), tabelle in results/ e reports/ dotnet msbuild build/Release.proj -t:Backtest -p:Dati="%USERPROFILE%\Documents\Encelado\data\market" dotnet msbuild build/Release.proj -t:Backtest -p:Dati="..." -p:Comando=falsify -p:Extra="--costs api" dotnet msbuild build/Release.proj -t:Backtest -p:Dati="A:\Download\Trading" -p:Comando=ticks +dotnet msbuild build/Release.proj -t:Backtest -p:Dati="%USERPROFILE%\Documents\Encelado\data" -p:Comando=learn ``` | Proprietà | Predefinito | A cosa serve | |---|---|---| -| `Versione` | vuoto | Vuoto = incrementa la minor (3.2.0 → 3.3.0). Altrimenti la scrive, se ha la forma `X.Y.Z`. | -| `Note` | vuoto | Note di rilascio. | -| `SaltaVerifica` | `false` | Non rieseguire i test. | +| `Versione` | vuoto | Vuoto = incrementa la minor (5.0.0 → 5.1.0). Altrimenti la scrive, se ha la forma `X.Y.Z`. | +| `Note` | vuoto | Note di rilascio (meglio via `ENCELADO_NOTE`, vedi sotto). | +| `Immagine` | `192.168.30.23:3000/alby96/encelado` | Nome completo dell'immagine. | +| `SaltaVerifica` | `false` | Non rieseguire i test (girano comunque dentro `docker build`). | +| `SaltaDocker` | `false` | Solo lo zip portabile, niente immagine né push. | | `Sovrascrivi` | `false` | Sostituisci una release Gitea con lo stesso tag. | | `Bozza` | `false` | Crea la release come bozza. | | `ConsentiModifiche` | `false` | Tagga anche con l'albero sporco. Serve saperlo. | -| `Dati` | — | La cartella dei dati (`candles__M15.csv` per `baskets`/`falsify`, tick MT5 per `ticks`). Obbligatoria per `Backtest`. | -| `Comando` | `baskets` | `ticks`, `baskets`, `falsify`. | +| `Dati` | — | La cartella dei dati. Obbligatoria per `Backtest`. | +| `Comando` | `baskets` | `ticks`, `baskets`, `falsify`, `learn`. | | `Extra` | vuoto | Altre opzioni passate allo strumento così come sono, es. `--costs api --quick`. | -## La ricerca e il meta-modello +## Cosa succede in `Rilascia` -Il meta-modello del bot (regressione logistica in ombra, MLP challenger, bandit) si -addestra da solo dal ledger nel ciclo settimanale e non passa da qui: vedi -`docs/ML_AND_LEARNING.md`. Lo strumento di ricerca produce solo le tabelle del -backtest (`results/trials.csv`, `results/riepilogo_baskets.csv`, -`reports/falsificazione.csv`), che `docs/STRATEGY.md` commenta. +1. `ConfigGitea`: legge url, owner, repo e token (ambiente o `gitea.json`); senza, si ferma prima di compilare. +2. `Verifica`: `dotnet test` nella cartella di verifica (`%TEMP%\Encelado.Verifica`). +3. `Pubblica`: `dotnet publish` framework-dependent di `Encelado.Server` in `bin/publish/portable`, con la versione da riga di comando; controlli su `Encelado.Server.dll`, `config/encelado.json` e sull'assenza di sorgenti. +4. `Docker`: `docker build --pull` con `VERSION` e `GIT_COMMIT` come build-arg; l'immagine ha i tag `:` e `:latest`. La `Dockerfile` esegue i test nel suo stadio `test`. +5. `Pacchetto`: zip portabile `bin/installer/Encelado__portabile.zip`, copia del template `deploy/unraid/encelado.xml` con la versione nel nome, **tag** `v` (solo ora, a pacchetto pronto, e solo con l'albero pulito). +6. `Rilascia`: `git push --tags` e verifica che il tag sia sul remoto; `docker login` sul registro di Gitea con il token (via stdin), `docker push` dei due tag, `docker logout`; release su Gitea con lo zip e il template allegati. ## Chi chiede la versione @@ -72,15 +78,15 @@ l'attività di VS Code (`inputs` in `.vscode/tasks.json`) e passa la risposta in `-p:Versione=`. Lasciando il campo vuoto si prende la minor successiva, che è il caso normale di fine sessione. -**La versione arriva dal tag e non viene scritta da nessuna parte.** `dotnet publish` la -riceve come proprietà da riga di comando, che è globale e vince su quella dichiarata in -`Directory.Build.props`. Tag, eseguibile, installatore e release portano quindi lo stesso -numero per costruzione. +**La versione arriva dal tag e non viene scritta da nessuna parte.** `dotnet publish` e +`docker build` la ricevono come proprietà da riga di comando, che è globale e vince su +quella dichiarata in `Directory.Build.props`. Tag, assembly, immagine e release portano +quindi lo stesso numero per costruzione. Il commit e la data di build finiscono in +`AssemblyMetadata` (Impostazioni ▸ Informazioni, `/api/info`). -Il tag si crea **in fondo**, quando l'installatore esiste davvero. Il contrario sembra più -naturale — decidi il numero, poi costruisci — ma lascia dietro un tag quando la verifica -fallisce, e il tentativo dopo riparte da lì: il numero sale senza che sia mai esistito un -pacchetto con quella versione. +Il tag si crea **in fondo**, quando lo zip e l'immagine esistono davvero. Il contrario +sembra più naturale — decidi il numero, poi costruisci — ma lascia dietro un tag quando la +verifica fallisce, e il tentativo dopo riparte da lì. ## Gitea @@ -90,34 +96,30 @@ macchina condivisa può rilasciare senza scrivere un token su disco: - `GITEA_URL`, `GITEA_OWNER`, `GITEA_REPO`, `GITEA_TOKEN` - oppure `build/gitea.json`, copiato da `gitea.example.json` -Il token si crea in Gitea da *Impostazioni ▸ Applicazioni ▸ Genera nuovo token*, con il -permesso `repository: read and write`. +Il token si crea in Gitea da *Impostazioni ▸ Applicazioni ▸ Genera nuovo token*, con i +permessi `repository: read and write` **e** `package: read and write` (serve per il +registro dei container). Il registro è lo stesso host di Gitea senza schema +(`192.168.30.23:3000`): in HTTP va dichiarato `insecure-registry` a Docker Desktop +(*Settings ▸ Docker Engine*). -Il token non passa mai dalla riga di comando: sta in un file di configurazione di curl, -cancellato subito dopo il rilascio. Gli `Exec` hanno `EchoOff` perché un registro di -compilazione è la classica cosa che si incolla in una chat. - -Nella release vengono caricati **sia l'installatore sia la copia portabile**: chi non -vuole installare niente deve continuare a poter scaricare l'applicazione e basta. +Il token non passa mai dalla riga di comando: sta in un file di configurazione di curl e +in un file letto da `docker login --password-stdin`, entrambi cancellati subito dopo. Gli +`Exec` hanno `EchoOff` perché un registro di compilazione è la classica cosa che si +incolla in una chat. ## Una trappola già pagata `dotnet test` e `dotnet publish` lanciati **da dentro** MSBuild ereditano l'ambiente del -processo padre. La compilazione WPF crea un progetto temporaneo (`_wpftmp.csproj`) e con -`MSBUILD_EXE_PATH` puntata al build in corso non genera più le classi parziali dello XAML: -si ottengono decine di errori su membri che esistono benissimo. +processo padre: `MSBUILD_EXE_PATH` e `MSBuildLoadMicrosoftTargetsReadOnly` puntate al +build in corso confondono il figlio. Gli `Exec` azzerano **solo quelle due**: la ricetta +che gira in rete azzera anche `MSBuildExtensionsPath` e `MSBuildSDKsPath`, e così il +figlio perde la posizione dell'SDK. -Per questo gli `Exec` azzerano `MSBUILD_EXE_PATH` e `MSBuildLoadMicrosoftTargetsReadOnly`. -**Solo quelle due**: la ricetta che gira in rete azzera anche `MSBuildExtensionsPath` e -`MSBuildSDKsPath`, e così il figlio perde la posizione dell'SDK — *«l'SDK Microsoft.NET.Sdk -specificato non è stato trovato»*. Serve isolare il motore, non nascondergli dove abita. - -I test girano in una cartella a parte (`%TEMP%\Encelado.Verifica`) perché l'applicazione -può essere aperta mentre si lavora e tiene bloccato `Encelado.exe`: senza, la compilazione -si ferma su MSB3027. +I test girano in una cartella a parte (`--artifacts-path`) perché un server lasciato +acceso da VS Code tiene bloccata `Encelado.Server.dll` nella `bin/` di lavoro. ## Prerequisiti - .NET SDK 10 -- [Inno Setup 6](https://jrsoftware.org/isinfo.php) — `winget install -e --id JRSoftware.InnoSetup` +- Docker Desktop (o un demone Docker raggiungibile) per `Docker`, `Pacchetto`, `Rilascia`; `-p:SaltaDocker=true` per farne a meno - `curl` e `git`, entrambi di serie in Windows 11 diff --git a/Encelado/build/Release.proj b/Encelado/build/Release.proj index 7039e51..55f3c7f 100644 --- a/Encelado/build/Release.proj +++ b/Encelado/build/Release.proj @@ -7,16 +7,18 @@ di VS Code (Terminale ▸ Esegui attività…) oppure a mano: dotnet msbuild build/Release.proj -t:Verifica - dotnet msbuild build/Release.proj -t:Backtest + dotnet msbuild build/Release.proj -t:Backtest -p:Dati=... + dotnet msbuild build/Release.proj -t:Docker dotnet msbuild build/Release.proj -t:Pacchetto - dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=3.3.0 + dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=5.0.0 È la stessa catena di Mimante/AutoBidder, adattata a una soluzione con più - progetti. Le differenze rispetto a quel file sono tre, tutte segnate sul + progetti. Le differenze rispetto a quel file sono quattro, tutte segnate sul posto: la versione vive in Directory.Build.props e non nel .csproj, la - pubblicazione produce una cartella e non un singolo eseguibile, e il target - Backtest rigioca coppie di serie storiche di prezzi invece dei dossier - delle aste. + pubblicazione produce una cartella e non un singolo eseguibile, il pacchetto + è un'immagine Docker e non un installatore Windows (dalla 5.0, ADR-0007 e + ADR-0008: il bot gira solo nel container), e il target Backtest rigioca + coppie di serie storiche di prezzi invece dei dossier delle aste. ── Perché MSBuild e non uno script ────────────────────────────────────── La catena vive accanto al codice che rilascia ed è versionata con lui: fra @@ -27,56 +29,52 @@ ── Da dove viene la versione ──────────────────────────────────────────── Dal tag git, e da nient'altro. Directory.Build.props non viene mai riscritto: - 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 numero arriva a `dotnet publish` e a `docker build` come proprietà da riga + di comando, quindi tag, assembly, immagine e release portano lo stesso numero + per costruzione, non per disciplina. Il modo previsto è taggare e poi rilasciare: - git tag v3.3.0 + git tag v5.0.0 dotnet msbuild build/Release.proj -t:Rilascia Se HEAD non ha un tag di versione la catena lo crea da sé — con il numero passato in `-p:Versione=`, oppure la minor successiva all'ultimo tag — e lo - fa in fondo, quando l'installatore esiste davvero. Un giro andato male non + fa in fondo, quando l'immagine esiste davvero. Un giro andato male non lascia dietro un tag per una versione che non è mai stata costruita. Il numero in Directory.Build.props resta quello delle compilazioni di - sviluppo. Continua ad avere senso alzarlo a ogni modifica — è quello che - compare nel titolo della finestra durante il lavoro — ma non decide più cosa - viene rilasciato: serve solo come seme al primissimo rilascio, quando non - esiste ancora nessun tag da cui ripartire. + sviluppo (Impostazioni ▸ Informazioni mentre si lavora) e serve solo come + seme al primissimo rilascio, quando non esiste ancora nessun tag. --> $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..')) - $(Radice)\src\Encelado.Bot\Encelado.Bot.csproj + $(Radice)\src\Encelado.Server\Encelado.Server.csproj $(Radice)\tests\Encelado.Tests\Encelado.Tests.csproj $(Radice)\tools\Encelado.Backtest\Encelado.Backtest.csproj - $(MSBuildThisFileDirectory)Encelado.iss + $(Radice)\Dockerfile + $(Radice)\deploy\unraid\encelado.xml $(Radice)\Directory.Build.props - $(Radice)\bin\publish\win-x64 + $(Radice)\bin\publish\portable $(Radice)\bin\installer + + 192.168.30.23:3000/alby96/encelado + + L'opzione artifacts-path sposta anche gli INTERMEDI, non solo il + risultato: la verifica non tocca le obj/ del lavoro in corso e non + viene disturbata da un server lasciato acceso da VS Code. --> $([System.IO.Path]::GetTempPath())Encelado.Verifica $(ENCELADO_NOTE) - + MSBUILD_EXE_PATH=;MSBuildLoadMicrosoftTargetsReadOnly= false + false false false @@ -243,33 +235,6 @@ - - - - - - - - - - - - - + @@ -325,13 +292,16 @@ " copia build/gitea.example.json in build/gitea.json e riempilo,\n" + " oppure imposta GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN.\n" + "Non e' stato costruito niente: si controlla prima di compilare, non dopo.\n" + - "Per il solo installatore, senza Gitea, usa il target Pacchetto."); + "Per il solo pacchetto, senza Gitea, usa il target Pacchetto."); // Senza questo il target prosegue lo stesso: git tag, poi curl con // l'indirizzo vuoto, e infine un "codice 3" che non dice niente a // nessuno. Un errore va fermato dove si capisce ancora cos'era. return false; } + + // Il registro dei container di Gitea e' lo stesso host, senza schema. + Registro = Regex.Replace(Url, "^https?://", ""); ]]> @@ -343,6 +313,7 @@ + @@ -358,6 +329,8 @@ .Replace("\r", "").Replace("\n", "\\n").Replace("\t", " "); var note = string.IsNullOrWhiteSpace(Note) ? "Versione " + Versione + "." : Note; + if (!string.IsNullOrWhiteSpace(Immagine)) + note += "\n\nImmagine: `" + Immagine + ":" + Versione + "` (anche `:latest`). Template Unraid allegato."; File.WriteAllText(Destinazione, "{\"tag_name\":\"" + esc(Tag) + "\"," + @@ -406,8 +379,6 @@ - @@ -420,12 +391,14 @@ @@ -434,7 +407,7 @@ + Text="Serve la cartella dei dati: -p:Dati="%USERPROFILE%\Documents\Encelado\data\market" (barre candles_SYMBOL_M15.csv) per baskets e falsify, la cartella dei tick MT5 per ticks, la cartella data del bot per learn.%0AComandi disponibili in -p:Comando= : ticks, baskets, falsify, learn (altre opzioni in -p:Extra=, es. --costs api --quick)." /> @@ -449,27 +422,24 @@ Command=""$(BacktestExe)" $(Comando) --data "$(Dati)" $(Extra)" /> - + - + - + + - @@ -509,6 +474,43 @@ Text="Nella cartella pubblicata ci sono file che non sono programma: @(SorgenteIntruso->'%(Filename)%(Extension)', ', ').%0ANon devono finire nella release." /> + + + + + + + + + + + + + + + + + + n/d + + + + + + + - - - - - - + + - - - $(CartellaPacchetti)\Encelado_$(V).exe $(CartellaPacchetti)\Encelado_$(V)_portabile.zip + $(CartellaPacchetti)\encelado-unraid-$(V).xml - - + + + - + + @@ -601,14 +595,15 @@ - + + @@ -619,9 +614,11 @@ $(GUrl)/api/v1/repos/$(GOwner)/$(GRepo) $([System.IO.Path]::GetTempPath())Encelado.Rilascio $(Tmp)\curl.cfg + $(Tmp)\registry.token $(Tmp)\release.json $(Tmp)\risposta.json - $(CartellaPacchetti)\Encelado_$(V).exe + $(CartellaPacchetti)\Encelado_$(V)_portabile.zip + $(CartellaPacchetti)\encelado-unraid-$(V).xml @@ -629,6 +626,7 @@ + + + + + + + @@ -676,7 +688,7 @@ + Immagine="$(Immagine)" Note="$(Note)" Bozza="$(Bozza)" /> @@ -692,17 +704,13 @@ - - + + - + + diff --git a/Encelado/deploy/docker/entrypoint.sh b/Encelado/deploy/docker/entrypoint.sh new file mode 100644 index 0000000..48cbf3a --- /dev/null +++ b/Encelado/deploy/docker/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Encelado — entrypoint del container. +# +# 1. Fuso orario da TZ (tzdata è nell'immagine). +# 2. Utente e gruppo da PUID/PGID (default 99/100, i «nobody:users» di Unraid): +# /config e /data vengono riassegnati a quell'utente, così i file scritti dal +# bot sono leggibili dalla share appdata senza giochi di permessi. +# 3. exec del processo finale con gosu: PID 1 è dotnet, SIGTERM arriva a lui. +# +# Con PUID=0 il bot resta root: sconsigliato, ma ammesso per il debug. +set -eu + +PUID="${PUID:-99}" +PGID="${PGID:-100}" + +if [ -n "${TZ:-}" ] && [ -f "/usr/share/zoneinfo/${TZ}" ]; then + ln -sf "/usr/share/zoneinfo/${TZ}" /etc/localtime + echo "${TZ}" > /etc/timezone +fi + +if [ "${PUID}" != "0" ]; then + if ! getent group "${PGID}" >/dev/null 2>&1; then + groupadd -g "${PGID}" encelado + fi + if ! getent passwd "${PUID}" >/dev/null 2>&1; then + useradd -r -o -u "${PUID}" -g "${PGID}" -M -d /data -s /usr/sbin/nologin encelado + fi + mkdir -p /config /data + chown -R "${PUID}:${PGID}" /config /data + echo "encelado: avvio come uid ${PUID} gid ${PGID}, fuso ${TZ:-UTC}" + exec gosu "${PUID}:${PGID}" "$@" +fi + +echo "encelado: avvio come root (PUID=0), fuso ${TZ:-UTC}" +exec "$@" diff --git a/Encelado/deploy/unraid/README.md b/Encelado/deploy/unraid/README.md new file mode 100644 index 0000000..6d66c23 --- /dev/null +++ b/Encelado/deploy/unraid/README.md @@ -0,0 +1,34 @@ +# Encelado su Unraid + +Il template `encelado.xml` installa il container dall'immagine sul registro di Gitea +(`192.168.30.23:3000/alby96/encelado:latest`). Istruzioni complete su volumi, variabili, +healthcheck e aggiornamento in `docs/DOCKER.md`. + +## Installazione + +1. **Registro privato**: se il registro di Gitea è in HTTP, Unraid deve fidarsi. + In *Impostazioni ▸ Docker* (modalità avanzata) aggiungi `192.168.30.23:3000` agli + `insecure-registries` del demone, oppure crea `/boot/config/docker.cfg` con + `DOCKER_OPTS="--insecure-registry 192.168.30.23:3000"`. Se il pacchetto è privato, + dal terminale di Unraid: `docker login 192.168.30.23:3000` con utente e token Gitea. +2. **Template**: *Docker ▸ Aggiungi container ▸ Template* e incolla l'URL raw + `http://192.168.30.23:3000/Alby96/Encelado/raw/branch/main/deploy/unraid/encelado.xml`, + oppure copia il file in `/boot/config/plugins/dockerMan/templates-user/`. +3. Compila **ENCELADO_WEB_TOKEN** (obbligatorio: senza, l'interfaccia resta chiusa in + localhost), le chiavi eToro (o lascia vuoto e inseriscile poi da *Impostazioni ▸ + Chiavi eToro* con `ENCELADO_KEY_PASSPHRASE` impostata), `TZ`, eventualmente Telegram. +4. Avvia. Al primo avvio il container semina `encelado.json` e `strategy.json` in + `/mnt/user/appdata/encelado/config`. L'interfaccia è su `http://IP:8080/`; il token + va inserito una volta e resta in un cookie per trenta giorni. + +## Aggiornamento + +*Docker ▸ Controlla aggiornamenti*: il tag `latest` segue l'ultima release. Il container si +ferma con SIGTERM (45 s di grazia): i basket aperti restano sul conto con gli stop +nativi e vengono ripresi dallo stato salvato e dalla riconciliazione al riavvio +(`docs/RUNBOOK.md`, «Recupero dopo inattività»). + +## Icona + +`assets/encelado.png` nel repository, servita da Gitea come file raw (`Icon` nel +template). Non serve pubblicarla altrove (D-29). diff --git a/Encelado/deploy/unraid/encelado.xml b/Encelado/deploy/unraid/encelado.xml new file mode 100644 index 0000000..cad9b98 --- /dev/null +++ b/Encelado/deploy/unraid/encelado.xml @@ -0,0 +1,43 @@ + + + Encelado + 192.168.30.23:3000/alby96/encelado:latest + http://192.168.30.23:3000/Alby96/-/packages/container/encelado + bridge + + sh + false + http://192.168.30.23:3000/Alby96/Encelado/issues + http://192.168.30.23:3000/Alby96/Encelado + Encelado: bot di trading "Correlation Baskets" su eToro (Public API). Motore, ledger e interfaccia web Material 3 in un solo container. Configurazione in /config (encelado.json, strategy.json, chiavi cifrate), dati in /data (ledger, stato, barre, conoscenza, rapporti, log). Le chiavi eToro si mettono nelle variabili ETORO_API_KEY e ETORO_USER_KEY oppure si salvano dall'interfaccia (cifrate con ENCELADO_KEY_PASSPHRASE). Modalita' Live solo con ENCELADO_EXECUTION_MODE=Live, ETORO_ENVIRONMENT=real e la frase CONFERMO LIVE in ENCELADO_CONFIRM_LIVE. + Tools: Productivity: + http://[IP]:[PORT:8080]/ + http://192.168.30.23:3000/Alby96/Encelado/raw/branch/main/deploy/unraid/encelado.xml + http://192.168.30.23:3000/Alby96/Encelado/raw/branch/main/assets/encelado.png + --restart=unless-stopped --stop-timeout 45 + + + + + + + + 8080 + /mnt/user/appdata/encelado/config + /mnt/user/appdata/encelado/data + + + + demo + Demo + + + 8080 + USD + + + + Europe/Rome + 99 + 100 + diff --git a/Encelado/docker-compose.yml b/Encelado/docker-compose.yml new file mode 100644 index 0000000..69101e9 --- /dev/null +++ b/Encelado/docker-compose.yml @@ -0,0 +1,48 @@ +# Encelado in locale con Docker Compose: immagine costruita dalla Dockerfile alla +# radice, cartelle di configurazione e dati sotto deploy/local (ignorate da git). +# +# docker compose up --build # costruisce e avvia, log a video +# docker compose up -d # in background +# docker compose logs -f encelado # segue il log +# docker compose down # ferma (SIGTERM: stop pulito del motore) +# +# Le chiavi eToro e il token Telegram NON stanno qui: mettili in un file .env +# accanto a questo (ignorato da git) con ETORO_API_KEY=…, ETORO_USER_KEY=…, +# TELEGRAM_BOT_TOKEN=…, TELEGRAM_CHAT_ID=…, ENCELADO_WEB_TOKEN=… +# Per Unraid usa invece il template in deploy/unraid/encelado.xml. + +services: + encelado: + build: + context: . + args: + GIT_COMMIT: ${GIT_COMMIT:-n/d} + VERSION: ${VERSION:-dev} + image: 192.168.30.23:3000/alby96/encelado:${VERSION:-dev} + container_name: encelado + restart: unless-stopped + ports: + - "${ENCELADO_WEB_PORT:-8080}:8080" + volumes: + - ./deploy/local/config:/config + - ./deploy/local/data:/data + environment: + TZ: ${TZ:-Europe/Rome} + PUID: ${PUID:-1000} + PGID: ${PGID:-1000} + # demo | real — l'ambiente eToro; real richiede anche ENCELADO_EXECUTION_MODE=Live + ETORO_ENVIRONMENT: ${ETORO_ENVIRONMENT:-demo} + # Paper | Demo | Live + ENCELADO_EXECUTION_MODE: ${ENCELADO_EXECUTION_MODE:-Demo} + # Solo per Live: la frase esatta «CONFERMO LIVE», altrimenti il bot non parte + ENCELADO_CONFIRM_LIVE: ${ENCELADO_CONFIRM_LIVE:-} + ENCELADO_DISPLAY_CURRENCY: ${ENCELADO_DISPLAY_CURRENCY:-USD} + # Senza token l'interfaccia è raggiungibile solo da localhost del container: + # per usarla dal browser il token serve. + ENCELADO_WEB_TOKEN: ${ENCELADO_WEB_TOKEN:-} + ENCELADO_KEY_PASSPHRASE: ${ENCELADO_KEY_PASSPHRASE:-} + ETORO_API_KEY: ${ETORO_API_KEY:-} + ETORO_USER_KEY: ${ETORO_USER_KEY:-} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-} + stop_grace_period: 45s diff --git a/Encelado/docs/DOCKER.md b/Encelado/docs/DOCKER.md new file mode 100644 index 0000000..e971849 --- /dev/null +++ b/Encelado/docs/DOCKER.md @@ -0,0 +1,72 @@ +# Encelado in Docker + +Aggiornato: 2026-09-23 (5.0). Dalla 5.0 il bot gira **solo** nel container (ADR-0007, ADR-0008): un processo `Encelado.Server` che ospita il motore e serve l'interfaccia web su Kestrel. Per Unraid c'è il template in `deploy/unraid/` (vedi il README lì). + +## Immagine + +`192.168.30.23:3000/alby96/encelado:` e `:latest`, sul registro dei container di Gitea. Si costruisce dalla `Dockerfile` alla radice: + +```powershell +dotnet msbuild build/Release.proj -t:Docker # : e :latest, con i test dentro la build +docker build -t encelado:dev --build-arg GIT_COMMIT=$(git rev-parse --short=12 HEAD) . # a mano +``` + +Stadi: `build` (restore + compilazione Release), `test` (la suite xunit: se è rossa l'immagine non esiste), `publish` (framework-dependent, non trimmed), `runtime` (`mcr.microsoft.com/dotnet/aspnet:10.0` + `gosu` + `tzdata`). L'immagine porta le etichette OCI `version` e `revision`; commit e data di build sono in Impostazioni ▸ Informazioni. + +## Volumi + +| Dentro | Contenuto | Su Unraid | +|---|---|---| +| `/config` | `encelado.json`, `strategy.json`, `instruments.json`, `etoro.keys.enc` (chiavi cifrate), file `STOP` per il kill-switch | `/mnt/user/appdata/encelado/config` | +| `/data` | `data/` (ledger, stato, barre, cache, modelli), `knowledge/`, `reports/`, `results/`, `logs/` | `/mnt/user/appdata/encelado/data` | + +Al primo avvio il container semina `encelado.json` e `strategy.json` dai valori di fabbrica. Nel container `run.dataPath`, `knowledgePath`, `reportsPath` e `logging.path` vengono rimappati sotto `/data` qualunque cosa dica il file: non c'è niente da cambiare a mano. I file appartengono a `PUID:PGID`. + +## Variabili d'ambiente + +| Variabile | Default | Effetto | +|---|---|---| +| `ENCELADO_WEB_TOKEN` | vuoto | Token dell'interfaccia. **Senza token il server ascolta solo su localhost del container**: dalla rete non si vede niente. Si inserisce una volta nel browser e resta in un cookie HttpOnly per 30 giorni; l'API accetta anche `Authorization: Bearer `. | +| `ENCELADO_WEB_PORT` | `8080` | Porta di Kestrel. | +| `ETORO_API_KEY`, `ETORO_USER_KEY` | vuoto | Chiavi eToro Public API; hanno la precedenza sul file cifrato. | +| `ENCELADO_KEY_PASSPHRASE` | vuoto | Passphrase del file `etoro.keys.enc` (AES-256-GCM, PBKDF2 200 000 iterazioni) scritto da Impostazioni ▸ Chiavi eToro. Senza passphrase il salvataggio dall'interfaccia è disattivato. | +| `ETORO_ENVIRONMENT` | `demo` | `demo` o `real`. | +| `ENCELADO_EXECUTION_MODE` | `Demo` | `Paper`, `Demo`, `Live`. | +| `ENCELADO_CONFIRM_LIVE` | vuoto | La frase `CONFERMO LIVE`, obbligatoria per `Live` insieme a `run.allowLive = true`. | +| `ENCELADO_AUTOSTART` | `1` | `0` = il motore non parte da solo: si preme AVVIA nell'interfaccia. | +| `ENCELADO_DISPLAY_CURRENCY` | `USD` | Valuta di visualizzazione (USD, EUR, GBP, CHF, JPY, AUD, CAD, NZD). Il ledger resta in USD. | +| `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` | vuoto | Notifiche e comandi Telegram. | +| `TZ` | `UTC` | Fuso degli orari a schermo e nel log (nome IANA, es. `Europe/Rome`); il ledger è UTC. Vale anche come `ui.timeZone` se il file dice `computer`. | +| `PUID`, `PGID` | `99`, `100` | Utente e gruppo del processo e dei volumi (`nobody:users` di Unraid). | +| `ENCELADO_LOG_LEVEL`, `ENCELADO_CONFIG_DIR`, `ENCELADO_DATA_DIR` | — | Livello di log; cartelle alternative (di norma non servono nel container). | + +## Avvio, arresto, aggiornamento + +```powershell +docker compose up -d # sviluppo locale: cartelle in deploy/local/, segreti in .env +docker compose logs -f encelado +docker compose down # SIGTERM → arresto pulito (45 s di grazia) +docker pull 192.168.30.23:3000/alby96/encelado:latest && docker compose up -d # aggiornamento +``` + +Su `SIGTERM` il server ferma il motore come da `run.closeOnShutdown` (default `false`: i basket restano sul conto con gli stop nativi), chiude il ledger, scrive l'heartbeat con la nota di arresto ed esce con 0. Al riavvio riprende lo stato salvato, riconcilia il conto e, se è passato più di `recovery.thresholdMinutes`, esegue il recupero dopo inattività (`docs/RUNBOOK.md`). + +## Healthcheck + +`HEALTHCHECK` ogni 30 s: `dotnet /app/Encelado.Server.dll --health` chiama `GET /api/health` sulla porta configurata ed esce 0 solo se il server risponde, il disco dei dati è scrivibile e, a motore acceso, l'heartbeat è più fresco di 105 s. `docker inspect --format '{{.State.Health.Status}}' encelado` per leggerlo. Il campo JSON di `/api/health` dice quale condizione fallisce. + +## Log + +Sulla console (stdout, `docker logs`) **e** nel file `/data/logs/encelado.log` (CSV `;`, rotazione per dimensione). L'ora nel log è nel fuso `TZ` con l'offset esplicito; il ledger resta UTC. Il livello si cambia con `ENCELADO_LOG_LEVEL` o da Impostazioni. + +## Rete + +Il server ascolta su tutte le interfacce **solo** quando `ENCELADO_WEB_TOKEN` è impostato. Non c'è TLS: l'interfaccia è pensata per la rete locale (Unraid) o dietro un reverse proxy che lo aggiunge. Le rotte `/api/health`, `/login`, `/icon.svg` e `/manifest.webmanifest` sono libere; tutto il resto chiede il token. + +## Registro privato in HTTP + +Il Gitea dell'utente è raggiunto in HTTP: chi fa `docker pull` deve dichiararlo `insecure-registry` (Docker Desktop: *Settings ▸ Docker Engine* → `"insecure-registries": ["192.168.30.23:3000"]`; Unraid: vedi `deploy/unraid/README.md`). Il push dalla catena di rilascio (`-t:Rilascia`) fa `docker login` con il token di `build/gitea.json` e `docker logout` subito dopo. + +## Esecuzione senza container + +Sconsigliata e non supportata in produzione, ma utile per lo sviluppo: F5 in VS Code (`Encelado (server)`) o `dotnet run --project src/Encelado.Server -- --no-autostart`. Configurazione e dati vanno in `Documenti\Encelado` (o `ENCELADO_CONFIG_DIR`). `Encelado (campione)` avvia il server con `--sample`: snapshot finto, nessuna chiave, per lavorare sulle pagine e fotografarle. diff --git a/Encelado/docs/STATE.md b/Encelado/docs/STATE.md index 4ebbb07..535039f 100644 --- a/Encelado/docs/STATE.md +++ b/Encelado/docs/STATE.md @@ -10,19 +10,22 @@ Aggiornato: 2026-09-23 (sessione 5.0, Fasi 6-9 concluse: il piano 5.0 è complet - **Fase 6 — Engine + Server**: `src/Encelado.Bot` → `src/Encelado.Engine` (libreria, senza UI); `src/Encelado.Server` (Sdk.Web, nessun NuGet) con `WebHost` (token in cookie HttpOnly o Bearer, senza token solo localhost), API JSON scritta a mano, SSE `/api/stream` a uno snapshot al secondo, `LogBuffer`, `HistoryService`; chiavi da variabili o file cifrato `etoro.keys.enc` (AES-256-GCM, `ENCELADO_KEY_PASSPHRASE`) al posto di DPAPI; `AppPaths` con `/config` e `/data`; `HeadlessRunner` ridotto a console (stato, comandi, bonifica); `--sample`, `--health`, `--no-autostart`, `ENCELADO_AUTOSTART`, `ENCELADO_CONFIRM_LIVE`. - **Fase 7 — interfaccia web Material 3** (`Web/wwwroot`, vanilla): navigation rail 80/256 px (drawer sotto 600 px), barra con orologi UTC e fuso, selettore della valuta (USD, EUR, GBP, CHF, JPY, AUD, CAD, NZD con tasso dalle quotazioni e tooltip in USD), AVVIA/FERMA, KILL-SWITCH con «chiudi anche le esterne»; dashboard con sei KPI (incluso il margine), tabella dei basket (cards da telefono), card evento/eToro/Telegram, attività, banner; **Storico ordini** (ordini, posizioni classificate, profitti per periodo con curva SVG, CSV); Log con filtri; Impostazioni con configurazione validata, chiavi eToro, ripristino in cinque passi, Ricerca, Diagnostica (con bonifica), Informazioni (versione, build, commit da `AssemblyMetadata`); temi scuro/chiaro. `HistoryBuilder` nel Core; `SnapshotJson`, `SampleSnapshot`, `SettingsService` nell'Engine; comando `learn` nello strumento (`Learn.cs`); `ui.displayCurrency/theme/navExpanded` e coppie di conversione nel polling. -- **Fase 8 — Docker e Unraid**: `Dockerfile` multi-stage (build, **test**, publish, runtime `aspnet:10.0` + gosu + tzdata), `deploy/docker/entrypoint.sh` (TZ, PUID/PGID), `docker-compose.yml`, `.dockerignore`, `HEALTHCHECK --health`; `deploy/unraid/encelado.xml` (Container v2, icona `assets/encelado.png` raw da Gitea, D-29) e README; `build/Release.proj` senza Inno Setup: `Pubblica` framework-dependent, `Docker`, `Pacchetto` (zip + template + tag), `Rilascia` (push dell'immagine sul registro di Gitea + release); VS Code con F5 sul server (`Encelado (server)`, `Encelado (campione)`, backtest) e attività Docker. Immagine `encelado:dev` costruita in locale con i test verdi nello stadio Linux. +- **Fase 8 — Docker e Unraid**: `Dockerfile` multi-stage (build, **test**, publish, runtime `aspnet:10.0` + gosu + tzdata), `deploy/docker/entrypoint.sh` (TZ, PUID/PGID), `docker-compose.yml`, `.dockerignore`, `HEALTHCHECK --health`; `deploy/unraid/encelado.xml` (Container v2, icona `assets/encelado.png` raw da Gitea, D-29) e README; `build/Release.proj` senza Inno Setup: `Pubblica` framework-dependent, `Docker`, `Pacchetto` (zip + template + tag), `Rilascia` (push dell'immagine sul registro di Gitea + release); VS Code con F5 sul server (`Encelado (server)`, `Encelado (campione)`, backtest) e attività Docker. Immagine `encelado:dev` costruita in locale con i test verdi nello stadio Linux e provata (avvio, semina di `/config`, permessi 99:100, `/api/health`, token, healthcheck). Il primo arresto con `docker stop` **non** era pulito (SIGKILL dopo la grazia): corretto con `PosixSignalRegistration` per SIGTERM in `Program.cs`; la correzione compila ed è committata ma **non è stata riprovata nel container** perché il disco C: del PC si è riempito (0 byte liberi) e il demone Docker ha iniziato a dare errori di I/O. - **Fase 9 — documenti e pulizia**: ADR-0007, ADR-0008, `docs/DOCKER.md`, `docs/UI_GUIDELINES.md`, `README.md` con gli screenshot (`docs/img/`), `CLAUDE.md`, ARCHITECTURE, RUNBOOK, KNOWN_ISSUES, GLOSSARY, DATA_SOURCES, ML_AND_LEARNING, QUESTIONS (D-28, D-29, D-36, D-37 risposte), POSTMORTEM (gli «accrediti» erano crediti demo per ogni ordine ridotto), skill aggiornate, `build/README.md`. **Codice morto rimosso**: progetto WPF, DPAPI, test WPF, `UiRenderTests`, ~45 membri mai usati (statistiche di ricerca, `Psi`, `EmptyContextProvider`, `EnvironmentKeyStore`, display helper dello snapshot, `INotifyPropertyChanged` in `SettingField`, …). Test nuovi: `EmbeddedUiTests`, `WebHostTests`, `HistoryBuilderTests`, `KeyStoreTests`; suite a **210 verdi**. ## Prossimi passi -1. **Rilascio 5.0.0** (utente): push del ramo, `-t:Rilascia -p:Versione=5.0.0` (richiede Docker acceso, `build/gitea.json` con permesso `package`, registro `192.168.30.23:3000` dichiarato `insecure-registry`), poi installazione su Unraid dal template. -2. Riaccendere il Demo nel container per 24 ore di verifica: contatore «orfane» a 0, `orders.jsonl` senza `Unknown` irrisolti, `sizing_bound = margin` sui primi ingressi; kill-switch a mano con verifica di piattezza; fermare il container un'ora con un basket aperto per vedere il recupero; messaggi Telegram reali. -3. Revisione visiva dell'interfaccia con l'utente (gli screenshot sono dal campione `--sample`); eventuali ritocchi ai testi e ai tooltip. -4. Avviso Telegram sui feed fermi da più di un'ora (resta in KNOWN_ISSUES). -5. Rifare il backtest (`backtest baskets`) con i limiti di margine e aggiornare §3 di `docs/STRATEGY.md`. +1. Liberare spazio su C:, riavviare Docker, ricostruire l'immagine e verificare l'arresto pulito con `docker stop` (vedi problemi aperti). +2. **Rilascio 5.0.0** (utente): push del ramo, `-t:Rilascia -p:Versione=5.0.0` (richiede Docker acceso, `build/gitea.json` con permesso `package`, registro `192.168.30.23:3000` dichiarato `insecure-registry`), poi installazione su Unraid dal template. +3. Riaccendere il Demo nel container per 24 ore di verifica: contatore «orfane» a 0, `orders.jsonl` senza `Unknown` irrisolti, `sizing_bound = margin` sui primi ingressi; kill-switch a mano con verifica di piattezza; fermare il container un'ora con un basket aperto per vedere il recupero; messaggi Telegram reali. +4. Revisione visiva dell'interfaccia con l'utente (gli screenshot sono dal campione `--sample`); eventuali ritocchi ai testi e ai tooltip. +5. Avviso Telegram sui feed fermi da più di un'ora (resta in KNOWN_ISSUES). +6. Rifare il backtest (`backtest baskets`) con i limiti di margine e aggiornare §3 di `docs/STRATEGY.md`. ## Problemi aperti +- **Il disco C: del PC di sviluppo è pieno** (0 byte liberi a fine sessione): Docker Desktop ha dato errori di I/O e l'ultima immagine non si è costruita. Liberare spazio, riavviare Docker Desktop, rifare `dotnet msbuild build/Release.proj -t:Docker` e riprovare `docker stop` (deve loggare «SIGTERM: arresto» e uscire con 0). + - Backtest negativo: la strategia non regge i costi (`docs/STRATEGY.md`). - Il conto reale vale 224,90 USD: il Live non è praticabile. - Google News blocca le ricerche RSS; RBA 403 a intermittenza; Fed 404 a tratti. diff --git a/Encelado/docs/adr/ADR-0008-engine-server-docker.md b/Encelado/docs/adr/ADR-0008-engine-server-docker.md new file mode 100644 index 0000000..679c4ec --- /dev/null +++ b/Encelado/docs/adr/ADR-0008-engine-server-docker.md @@ -0,0 +1,31 @@ +# ADR-0008 — Engine + Server, immagine Docker come pacchetto, Unraid come destinazione + +Data: 2026-09-23. Stato: accettata (D-28: «solo tramite container»; D-29: registro dei container di Gitea, icona nel repository). + +## Contesto + +Con la finestra WPF ritirata (ADR-0007) il bot ha bisogno di un processo che giri per mesi su una macchina sempre accesa, si riavvii da solo, non dipenda dal PC di sviluppo e sia aggiornabile con un comando. L'utente ha un server Unraid e un Gitea privato con il registro dei container. + +## Decisione + +1. **Tre progetti applicativi**: `Encelado.Core` (logica pura, zero I/O), `Encelado.Engine` (motore, ledger, feed, configurazione, impostazioni, storico, Telegram; ex `Encelado.Bot` senza UI), `Encelado.Server` (l'eseguibile: `Microsoft.NET.Sdk.Web`, Kestrel, API, UI incorporata, ciclo di vita del processo). `Encelado.Etoro` resta il client HTTP. Lo strumento `tools/Encelado.Backtest` referenzia anche Engine per il comando `learn`. +2. **Il pacchetto è l'immagine Docker** (`Dockerfile` alla radice, multi-stage: restore, build, test, publish framework-dependent, runtime `mcr.microsoft.com/dotnet/aspnet:10.0`). Un'immagine con i test rossi non esiste: `dotnet test` gira dentro lo stadio di build. Il commit arriva come `--build-arg GIT_COMMIT` (il contesto non contiene `.git`) e finisce in `AssemblyMetadata` con la data di build, visibili in Impostazioni ▸ Informazioni e in `/api/info`. +3. **Volumi**: `/config` (encelado.json, strategy.json, instruments.json, chiavi cifrate) e `/data` (ledger, stato, barre, cache, conoscenza, rapporti, log). Il rilevamento del container (`ENCELADO_IN_CONTAINER=1`, o `/config` esistente su Linux) rimappa `run.dataPath` e affini sotto `/data` senza toccare la configurazione dell'utente. +4. **Entrypoint** con `gosu`: `PUID`/`PGID` (default 99/100, `nobody:users` di Unraid) diventano proprietari di `/config` e `/data`; `TZ` regola gli orari a schermo; `exec` del processo dotnet come PID 1 per ricevere `SIGTERM` (arresto pulito: ledger chiuso, heartbeat con nota, posizioni lasciate sul conto con gli stop nativi). +5. **Healthcheck** `dotnet Encelado.Server.dll --health`: interroga `/api/health` (heartbeat fresco se il motore gira, disco scrivibile). Niente `curl` nell'immagine. +6. **Catena di rilascio** (`build/Release.proj`): `Verifica` → `Pubblica` (cartella portabile framework-dependent) → `Docker` (`/alby96/encelado:` e `:latest`) → tag → `Rilascia` (push dell'immagine sul registro di Gitea con lo stesso token della release, release con lo zip portabile e il template Unraid allegati). Inno Setup e `Encelado.iss` sono rimossi. +7. **Unraid**: template `deploy/unraid/encelado.xml` (Container v2) con porta 8080, i due volumi sotto `/mnt/user/appdata/encelado/`, tutte le variabili con descrizione in italiano e `Mask` sui segreti; icona `assets/encelado.png` servita come file raw da Gitea (D-29). + +## Alternative scartate + +- **Servizio Windows / VPS Windows**: obbliga a tenere un Windows acceso e non si aggiorna con un `docker pull`; l'utente ha già Unraid. +- **Pubblicazione self-contained o AOT nel container**: il server ospita ASP.NET Core minimal e non è AOT-compatibile; l'immagine `aspnet` porta già il runtime e resta più piccola di un self-contained. +- **Immagine su Docker Hub o GHCR**: il codice è privato su Gitea; il registro di Gitea usa lo stesso token della release e non richiede altri account. +- **Compose come unica via**: resta per lo sviluppo locale (`docker-compose.yml`), ma la destinazione è il template Unraid. + +## Conseguenze + +- `Documenti\Encelado` sopravvive solo per l'esecuzione fuori dal container (sviluppo con F5); nel container tutto sta in `/config` e `/data`. +- L'istanza unica per cartella dati (`instance.lock`) vale anche fra un container e un processo locale che montino la stessa cartella. +- Un aggiornamento dell'immagine è un riavvio: il recupero dopo inattività (Fase 4) copre il buco. +- La catena `build/` cambia struttura rispetto a Mimante/AutoBidder (niente installatore): la differenza è documentata in `build/README.md`. diff --git a/Encelado/src/Encelado.Server/Program.cs b/Encelado/src/Encelado.Server/Program.cs index 42dc2e5..44a1897 100644 --- a/Encelado/src/Encelado.Server/Program.cs +++ b/Encelado/src/Encelado.Server/Program.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Runtime.InteropServices; using System.Net.Http; using Encelado.Core.Baskets; using Encelado.Core.Notifications; @@ -120,14 +121,18 @@ public static class Program Log.Info("Ctrl+C: arresto"); stopping.Cancel(); }; - AppDomain.CurrentDomain.ProcessExit += (_, _) => + // docker stop sends SIGTERM to PID 1 (this process, via exec in the entrypoint). + // Cancelling the runtime's default handling keeps the process alive until the + // shutdown below has run: engine stopped, ledger closed, heartbeat with the note. + using PosixSignalRegistration sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx => { + ctx.Cancel = true; if (!stopping.IsCancellationRequested) { Log.Info("SIGTERM: arresto"); stopping.Cancel(); } - }; + }); if (sample) {