5.0 Fase 8: immagine Docker come pacchetto, template Unraid, catena senza Inno Setup
Il container è la sola forma di esecuzione (ADR-0008): Dockerfile multi-stage con i test dentro la build, entrypoint con TZ e PUID/PGID, compose per le prove locali, healthcheck via --health. Il template Unraid installa dall'immagine sul registro di Gitea con l'icona servita dal repository (D-29). La catena di rilascio pubblica la cartella portabile, costruisce l'immagine, tagga a pacchetto pronto e spinge l'immagine sul registro con lo stesso token della release; l'installatore Windows non ha più senso e viene tolto. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
@@ -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/
|
||||
|
||||
@@ -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"]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
+53
-51
@@ -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:<versione>` (+ `: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_<SYMBOL>_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 `:<versione>` e `:latest`. La `Dockerfile` esegue i test nel suo stadio `test`.
|
||||
5. `Pacchetto`: zip portabile `bin/installer/Encelado_<v>_portabile.zip`, copia del template `deploy/unraid/encelado.xml` con la versione nel nome, **tag** `v<versione>` (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
|
||||
|
||||
+145
-136
@@ -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.
|
||||
-->
|
||||
<Project DefaultTargets="Pacchetto" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<PropertyGroup>
|
||||
<Radice>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..'))</Radice>
|
||||
<Csproj>$(Radice)\src\Encelado.Bot\Encelado.Bot.csproj</Csproj>
|
||||
<Csproj>$(Radice)\src\Encelado.Server\Encelado.Server.csproj</Csproj>
|
||||
<TestProj>$(Radice)\tests\Encelado.Tests\Encelado.Tests.csproj</TestProj>
|
||||
<BacktestProj>$(Radice)\tools\Encelado.Backtest\Encelado.Backtest.csproj</BacktestProj>
|
||||
<Iss>$(MSBuildThisFileDirectory)Encelado.iss</Iss>
|
||||
<Dockerfile>$(Radice)\Dockerfile</Dockerfile>
|
||||
<TemplateUnraid>$(Radice)\deploy\unraid\encelado.xml</TemplateUnraid>
|
||||
|
||||
<!-- La versione non sta nel .csproj come in AutoBidder: sta in
|
||||
Directory.Build.props, da cui la ereditano tutti e quattro i progetti.
|
||||
Directory.Build.props, da cui la ereditano tutti i progetti.
|
||||
È il file che VersioneDaTag legge quando non esiste ancora nessun tag. -->
|
||||
<Props>$(Radice)\Directory.Build.props</Props>
|
||||
|
||||
<CartellaPubblicazione>$(Radice)\bin\publish\win-x64</CartellaPubblicazione>
|
||||
<CartellaPubblicazione>$(Radice)\bin\publish\portable</CartellaPubblicazione>
|
||||
<CartellaPacchetti>$(Radice)\bin\installer</CartellaPacchetti>
|
||||
|
||||
<!-- ── L'immagine ─────────────────────────────────────────────────────
|
||||
Il registro è quello di Gitea (D-29): stesso host di build/gitea.json,
|
||||
owner in minuscolo perché i nomi delle immagini OCI lo richiedono. Si
|
||||
può cambiare con -p:Immagine=host/utente/nome. -->
|
||||
<Immagine Condition="'$(Immagine)' == ''">192.168.30.23:3000/alby96/encelado</Immagine>
|
||||
|
||||
<!-- ── Perché una cartella a parte per verifica e backtest ─────────────
|
||||
Due ragioni, e servono entrambe.
|
||||
|
||||
La prima: l'applicazione può essere aperta mentre si lavora, e tiene
|
||||
bloccato Encelado.exe — la compilazione si fermerebbe su MSB3027.
|
||||
|
||||
La seconda: l'opzione artifacts-path sposta anche gli INTERMEDI, non
|
||||
solo il risultato. La sola -o li lascia nella obj/ condivisa, e la
|
||||
compilazione WPF — che genera un progetto temporaneo `_wpftmp.csproj` a
|
||||
ogni giro — ogni tanto ci trovava stato altrui e smetteva di produrre
|
||||
le classi parziali dello XAML. Il sintomo era una raffica di
|
||||
"AuctionMonitorControl non contiene una definizione di ...", a giri
|
||||
alterni, senza che il codice fosse cambiato. -->
|
||||
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. -->
|
||||
<CartellaProve>$([System.IO.Path]::GetTempPath())Encelado.Verifica</CartellaProve>
|
||||
|
||||
<!-- Serve solo quando HEAD non è ancora taggato: è il numero del tag da
|
||||
@@ -98,21 +96,15 @@
|
||||
-->
|
||||
<Note Condition="'$(Note)' == ''">$(ENCELADO_NOTE)</Note>
|
||||
|
||||
<!-- ── Perché serve azzerare queste variabili ──────────────────────────
|
||||
`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 quelle variabili puntate al build in
|
||||
corso non genera più le classi parziali dello XAML: si ottengono decine
|
||||
di "AuctionMonitorControl non contiene una definizione di ..." che non
|
||||
hanno niente a che vedere col codice.
|
||||
|
||||
Si azzerano SOLO queste due. Togliere anche MSBuildExtensionsPath o
|
||||
MSBuildSDKsPath — la ricetta che gira in rete — fa perdere al figlio la
|
||||
posizione dell'SDK: "l'SDK Microsoft.NET.Sdk specificato non è stato
|
||||
trovato". Serve isolare il motore, non nascondergli dove abita. -->
|
||||
<!-- `dotnet test` e `dotnet publish` lanciati da dentro MSBuild ereditano
|
||||
l'ambiente del processo padre; queste due variabili puntate al build
|
||||
in corso confondono il figlio. Si azzerano SOLO queste due: togliere
|
||||
anche MSBuildExtensionsPath o MSBuildSDKsPath fa perdere al figlio la
|
||||
posizione dell'SDK. -->
|
||||
<AmbientePulito>MSBUILD_EXE_PATH=;MSBuildLoadMicrosoftTargetsReadOnly=</AmbientePulito>
|
||||
|
||||
<SaltaVerifica Condition="'$(SaltaVerifica)' == ''">false</SaltaVerifica>
|
||||
<SaltaDocker Condition="'$(SaltaDocker)' == ''">false</SaltaDocker>
|
||||
<Sovrascrivi Condition="'$(Sovrascrivi)' == ''">false</Sovrascrivi>
|
||||
<Bozza Condition="'$(Bozza)' == ''">false</Bozza>
|
||||
|
||||
@@ -243,33 +235,6 @@
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<UsingTask TaskName="TrovaInnoSetup" TaskFactory="RoslynCodeTaskFactory"
|
||||
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Percorso ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
var candidati = new[]
|
||||
{
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Inno Setup 6\ISCC.exe"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Inno Setup 6\ISCC.exe"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Inno Setup 6\ISCC.exe"),
|
||||
};
|
||||
|
||||
foreach (var c in candidati)
|
||||
if (File.Exists(c)) { Percorso = c; break; }
|
||||
|
||||
if (string.IsNullOrEmpty(Percorso))
|
||||
Log.LogError("Inno Setup 6 non trovato. Installalo con: winget install -e --id JRSoftware.InnoSetup");
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<!--
|
||||
Gitea si raggiunge con curl, di serie in Windows 10 e 11.
|
||||
|
||||
@@ -280,8 +245,9 @@
|
||||
al prossimo aggiornamento di .NET no. curl non ha questo problema.
|
||||
|
||||
Il token NON passa mai dalla riga di comando: sta in un file di configurazione
|
||||
di curl, che viene cancellato subito dopo. Gli Exec hanno EchoOff perché un
|
||||
registro di compilazione è la classica cosa che si incolla in una chat.
|
||||
di curl (e in un file letto da `docker login ‐‐password-stdin`), cancellati
|
||||
subito dopo. Gli Exec hanno EchoOff perché un registro di compilazione è la
|
||||
classica cosa che si incolla in una chat.
|
||||
-->
|
||||
|
||||
<UsingTask TaskName="LeggiConfigGitea" TaskFactory="RoslynCodeTaskFactory"
|
||||
@@ -292,6 +258,7 @@
|
||||
<Owner ParameterType="System.String" Output="true" />
|
||||
<Repo ParameterType="System.String" Output="true" />
|
||||
<Token ParameterType="System.String" Output="true" />
|
||||
<Registro ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
@@ -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?://", "");
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
@@ -343,6 +313,7 @@
|
||||
<Destinazione ParameterType="System.String" Required="true" />
|
||||
<Tag ParameterType="System.String" Required="true" />
|
||||
<Versione ParameterType="System.String" Required="true" />
|
||||
<Immagine ParameterType="System.String" />
|
||||
<Note ParameterType="System.String" />
|
||||
<Bozza ParameterType="System.Boolean" />
|
||||
</ParameterGroup>
|
||||
@@ -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 @@
|
||||
<Target Name="Verifica" Condition="'$(SaltaVerifica)' != 'true'">
|
||||
<Message Importance="High" Text="== Verifica (compilazione + test) ==" />
|
||||
|
||||
<!-- In una cartella a parte: l'applicazione puo' essere aperta e tenere
|
||||
bloccato Encelado.exe. -->
|
||||
<Exec Command="dotnet test "$(TestProj)" --nologo -v q --artifacts-path "$(CartellaProve)""
|
||||
WorkingDirectory="$(Radice)"
|
||||
EnvironmentVariables="$(AmbientePulito)" />
|
||||
@@ -420,12 +391,14 @@
|
||||
<!--
|
||||
Lo strumento in tools/Encelado.Backtest: `ticks` converte i tick MT5 in barre
|
||||
M15 bid/ask, `baskets` rigioca la strategia (baseline, griglia, PSR/DSR, PBO,
|
||||
walk-forward), `falsify` esegue i test di falsificazione. Ogni tabella è un CSV
|
||||
con ; e colonna motivazione. Vedi docs/STRATEGY.md per i risultati.
|
||||
walk-forward), `falsify` esegue i test di falsificazione, `learn` rigioca il
|
||||
ciclo di apprendimento in ombra su un ledger. Ogni tabella è un CSV con ; e
|
||||
colonna motivazione. Vedi docs/STRATEGY.md per i risultati.
|
||||
|
||||
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
|
||||
-->
|
||||
<Target Name="Backtest">
|
||||
<PropertyGroup>
|
||||
@@ -434,7 +407,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<Error Condition="'$(Dati)' == ''"
|
||||
Text="Serve la cartella dei dati: -p:Dati="%USERPROFILE%\Documents\Encelado\data\market" (barre candles_SYMBOL_M15.csv) per baskets e falsify, oppure la cartella dei tick MT5 per ticks.%0AComandi disponibili in -p:Comando= : ticks, baskets, falsify (altre opzioni in -p:Extra=, es. --costs api --quick)." />
|
||||
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)." />
|
||||
|
||||
<Error Condition="!Exists('$(Dati)')" Text="Cartella dati non trovata: $(Dati)" />
|
||||
|
||||
@@ -449,27 +422,24 @@
|
||||
Command=""$(BacktestExe)" $(Comando) --data "$(Dati)" $(Extra)" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Eseguibile ═════════════════════ -->
|
||||
<!-- ═════════════════════ Pubblicazione ═════════════════════ -->
|
||||
|
||||
<!--
|
||||
I quattro numeri di versione arrivano da qui, non da Directory.Build.props:
|
||||
le proprietà da riga di comando sono globali e vincono su quelle scritte nei
|
||||
progetti. Vengono passati tutti e quattro anche se il file ne dichiara uno
|
||||
solo — la finestra legge Assembly.GetName().Version, e vederne divergere uno
|
||||
significa un numero a schermo che mente.
|
||||
solo — Informazioni legge Assembly.GetName().Version, e vederne divergere
|
||||
uno significa un numero a schermo che mente.
|
||||
|
||||
── Perché una cartella e non un singolo eseguibile ────────────────────────
|
||||
AutoBidder pubblica con PublishSingleFile e allega quel file alla release.
|
||||
Qui non si può: Encelado legge `encelado.json` accanto al proprio eseguibile
|
||||
e ci scrive log, diario e CSV di analisi. Un singolo file estratto in una
|
||||
cartella temporanea a ogni avvio metterebbe la configurazione dell'utente e
|
||||
i suoi log in un percorso che cambia da un avvio all'altro.
|
||||
|
||||
Una cartella non si allega a una release, quindi al suo posto viene allegato
|
||||
uno zip: vedi il target Rilascia.
|
||||
── Cosa si pubblica ──────────────────────────────────────────────────────
|
||||
Una cartella framework-dependent, senza sistema operativo: è la stessa
|
||||
cosa che il Dockerfile mette in /app, e chi vuole girare senza container
|
||||
(sconsigliato: ADR-0007) la lancia con `dotnet Encelado.Server.dll` sopra
|
||||
il runtime ASP.NET Core 10. Una cartella non si allega a una release,
|
||||
quindi al suo posto viene allegato uno zip: vedi il target Pacchetto.
|
||||
-->
|
||||
<Target Name="Pubblica" DependsOnTargets="DeterminaVersione">
|
||||
<Message Importance="High" Text="== Pubblicazione dell'eseguibile ($(V)) ==" />
|
||||
<Message Importance="High" Text="== Pubblicazione della cartella portabile ($(V)) ==" />
|
||||
|
||||
<!-- Cartella pulita a ogni giro. Senza questo i resti di una pubblicazione
|
||||
precedente — una DLL rinominata, un runtime cambiato — finiscono nel
|
||||
@@ -479,29 +449,24 @@
|
||||
|
||||
<Exec WorkingDirectory="$(Radice)"
|
||||
EnvironmentVariables="$(AmbientePulito)"
|
||||
Command="dotnet publish "$(Csproj)" -c Release -r win-x64 --nologo -v q --self-contained true -p:PublishReadyToRun=true -p:PublishTrimmed=false -p:DebugType=none -p:Version=$(V) -p:AssemblyVersion=$(V).0 -p:FileVersion=$(V).0 -p:InformationalVersion=$(V) -o "$(CartellaPubblicazione)"" />
|
||||
Command="dotnet publish "$(Csproj)" -c Release --nologo -v q --self-contained false -p:DebugType=none -p:Version=$(V) -p:AssemblyVersion=$(V).0 -p:FileVersion=$(V).0 -p:InformationalVersion=$(V) -o "$(CartellaPubblicazione)"" />
|
||||
|
||||
<Error Condition="!Exists('$(CartellaPubblicazione)\Encelado.exe')"
|
||||
Text="Pubblicazione fallita: Encelado.exe non trovato." />
|
||||
<Error Condition="!Exists('$(CartellaPubblicazione)\Encelado.Server.dll')"
|
||||
Text="Pubblicazione fallita: Encelado.Server.dll non trovato." />
|
||||
|
||||
<!-- Senza configurazione l'applicazione non parte, e l'installatore la
|
||||
copierebbe senza accorgersi che manca. -->
|
||||
<Error Condition="!Exists('$(CartellaPubblicazione)\encelado.json')"
|
||||
Text="Pubblicazione incompleta: encelado.json non è finito accanto all'eseguibile." />
|
||||
<!-- Senza configurazione di fabbrica il primo avvio non può seminare
|
||||
encelado.json e strategy.json nella cartella di configurazione. -->
|
||||
<Error Condition="!Exists('$(CartellaPubblicazione)\config\encelado.json')"
|
||||
Text="Pubblicazione incompleta: config\encelado.json non è finito accanto all'eseguibile." />
|
||||
|
||||
<!--
|
||||
Nella release non devono finire i sorgenti: si pubblica il programma, non
|
||||
il progetto. La cartella pubblicata diventa lo zip portabile, quindi basta
|
||||
controllare qui.
|
||||
|
||||
Non è teorico: un <None CopyToOutputDirectory> aggiunto per comodità, o un
|
||||
pacchetto che porta i propri .cs, li farebbe scivolare dentro senza che
|
||||
nessuno se ne accorga fino a quando qualcuno non apre lo zip.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<SorgenteIntruso Include="$(CartellaPubblicazione)\**\*.cs" />
|
||||
<SorgenteIntruso Include="$(CartellaPubblicazione)\**\*.csproj" />
|
||||
<SorgenteIntruso Include="$(CartellaPubblicazione)\**\*.xaml" />
|
||||
<SorgenteIntruso Include="$(CartellaPubblicazione)\**\*.pdb" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -509,6 +474,43 @@
|
||||
Text="Nella cartella pubblicata ci sono file che non sono programma: @(SorgenteIntruso->'%(Filename)%(Extension)', ', ').%0ANon devono finire nella release." />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Immagine Docker ═════════════════════ -->
|
||||
|
||||
<!--
|
||||
L'immagine è il pacchetto vero (ADR-0008). La Dockerfile alla radice compila,
|
||||
esegue i test e pubblica dentro lo stadio di build: un'immagine con i test
|
||||
rossi non esiste. Il commit arriva come build-arg perché .git resta fuori
|
||||
dal contesto (.dockerignore).
|
||||
|
||||
dotnet msbuild build/Release.proj -t:Docker # :<versione> e :latest
|
||||
dotnet msbuild build/Release.proj -t:Docker -p:SaltaDocker=true # (no-op, per la catena senza Docker)
|
||||
-->
|
||||
<Target Name="Docker" DependsOnTargets="DeterminaVersione" Condition="'$(SaltaDocker)' != 'true'">
|
||||
<Message Importance="High" Text="== Immagine Docker $(Immagine):$(V) ==" />
|
||||
|
||||
<Error Condition="!Exists('$(Dockerfile)')" Text="Dockerfile non trovata: $(Dockerfile)" />
|
||||
|
||||
<Exec Command="docker version --format {{.Server.Version}}" ContinueOnError="true"
|
||||
ConsoleToMSBuild="true" StandardOutputImportance="low" StandardErrorImportance="low">
|
||||
<Output TaskParameter="ExitCode" PropertyName="DockerEsito" />
|
||||
</Exec>
|
||||
<Error Condition="'$(DockerEsito)' != '0'"
|
||||
Text="Docker non risponde: avvia Docker Desktop (o il demone) e rilancia. Per saltare l'immagine: -p:SaltaDocker=true." />
|
||||
|
||||
<Exec Command="git rev-parse --short=12 HEAD" WorkingDirectory="$(Radice)" ContinueOnError="true"
|
||||
ConsoleToMSBuild="true" StandardOutputImportance="low" StandardErrorImportance="low">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="Commit" />
|
||||
</Exec>
|
||||
<PropertyGroup>
|
||||
<Commit Condition="'$(Commit)' == ''">n/d</Commit>
|
||||
</PropertyGroup>
|
||||
|
||||
<Exec WorkingDirectory="$(Radice)"
|
||||
Command="docker build --pull -t "$(Immagine):$(V)" -t "$(Immagine):latest" --build-arg VERSION=$(V) --build-arg GIT_COMMIT=$(Commit) -f "$(Dockerfile)" "$(Radice)"" />
|
||||
|
||||
<Message Importance="High" Text=" immagine $(Immagine):$(V) (commit $(Commit))" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Pacchetto ═════════════════════ -->
|
||||
|
||||
<!--
|
||||
@@ -518,45 +520,37 @@
|
||||
lascia dietro un tag quando qualcosa va storto, e il tentativo successivo
|
||||
riparte da li'. Un giro andato male e il numero e' salito lo stesso, senza
|
||||
che sia mai esistito un pacchetto con quella versione. Taggando in fondo,
|
||||
ogni tag corrisponde a un installatore che esiste davvero.
|
||||
ogni tag corrisponde a un'immagine e a uno zip che esistono davvero.
|
||||
-->
|
||||
<Target Name="Pacchetto" DependsOnTargets="Verifica;DeterminaVersione;Pubblica">
|
||||
<Message Importance="High" Text="== Creazione dell'installatore ==" />
|
||||
|
||||
<TrovaInnoSetup>
|
||||
<Output TaskParameter="Percorso" PropertyName="Iscc" />
|
||||
</TrovaInnoSetup>
|
||||
<Target Name="Pacchetto" DependsOnTargets="Verifica;DeterminaVersione;Pubblica;Docker">
|
||||
<Message Importance="High" Text="== Creazione del pacchetto ==" />
|
||||
|
||||
<MakeDir Directories="$(CartellaPacchetti)" />
|
||||
|
||||
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)"
|
||||
Command=""$(Iscc)" /Qp "/DAppVersion=$(V)" "/DSourceDir=$(CartellaPubblicazione)" "/DOutputDir=$(CartellaPacchetti)" "$(Iss)"" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Setup>$(CartellaPacchetti)\Encelado_$(V).exe</Setup>
|
||||
<Portabile>$(CartellaPacchetti)\Encelado_$(V)_portabile.zip</Portabile>
|
||||
<TemplateAllegato>$(CartellaPacchetti)\encelado-unraid-$(V).xml</TemplateAllegato>
|
||||
</PropertyGroup>
|
||||
|
||||
<Error Condition="!Exists('$(Setup)')" Text="Installatore non trovato: $(Setup)" />
|
||||
|
||||
<!--
|
||||
La copia portabile per chi non vuole installare niente.
|
||||
|
||||
In AutoBidder è il solo .exe, perché lì la pubblicazione è un file unico.
|
||||
Qui l'applicazione ha bisogno di encelado.json accanto a sé, quindi è uno
|
||||
zip della cartella. Si crea qui e non nel rilascio: entrambi i pacchetti
|
||||
devono esistere anche costruendo senza pubblicare su Gitea.
|
||||
La copia portabile per chi non vuole il container (framework-dependent:
|
||||
serve il runtime ASP.NET Core 10). Si crea qui e non nel rilascio: deve
|
||||
esistere anche costruendo senza pubblicare su Gitea.
|
||||
-->
|
||||
<Delete Files="$(Portabile)" ContinueOnError="true" />
|
||||
<ZipDirectory SourceDirectory="$(CartellaPubblicazione)" DestinationFile="$(Portabile)" />
|
||||
|
||||
<!-- Il template Unraid con la versione nel nome, così la release lo porta con sé. -->
|
||||
<Copy SourceFiles="$(TemplateUnraid)" DestinationFiles="$(TemplateAllegato)" Condition="Exists('$(TemplateUnraid)')" />
|
||||
|
||||
<!-- Adesso: il pacchetto c'e', il tag puo' esistere. -->
|
||||
<CallTarget Targets="CreaTag" />
|
||||
|
||||
<Message Importance="High" Text=" " />
|
||||
<Message Importance="High" Text="Pacchetto pronto:" />
|
||||
<Message Importance="High" Text=" $(Setup)" />
|
||||
<Message Importance="High" Text=" $(Portabile)" />
|
||||
<Message Importance="High" Text=" $(TemplateAllegato)" />
|
||||
<Message Importance="High" Text=" $(Immagine):$(V)" Condition="'$(SaltaDocker)' != 'true'" />
|
||||
</Target>
|
||||
|
||||
<!-- ═════════════════════ Versione e tag ═════════════════════ -->
|
||||
@@ -601,14 +595,15 @@
|
||||
|
||||
<!-- ═════════════════════ Rilascio ═════════════════════ -->
|
||||
|
||||
<!-- Prima di tutto il resto: un token mancante non deve costare due minuti di
|
||||
compilazione per poi fermarsi all'ultimo passo. -->
|
||||
<!-- Prima di tutto il resto: un token mancante non deve costare i minuti
|
||||
dell'immagine per poi fermarsi all'ultimo passo. -->
|
||||
<Target Name="ConfigGitea">
|
||||
<LeggiConfigGitea Percorso="$(MSBuildThisFileDirectory)gitea.json">
|
||||
<Output TaskParameter="Url" PropertyName="GUrl" />
|
||||
<Output TaskParameter="Owner" PropertyName="GOwner" />
|
||||
<Output TaskParameter="Repo" PropertyName="GRepo" />
|
||||
<Output TaskParameter="Token" PropertyName="GToken" />
|
||||
<Output TaskParameter="Registro" PropertyName="GRegistro" />
|
||||
</LeggiConfigGitea>
|
||||
</Target>
|
||||
|
||||
@@ -619,9 +614,11 @@
|
||||
<Api>$(GUrl)/api/v1/repos/$(GOwner)/$(GRepo)</Api>
|
||||
<Tmp>$([System.IO.Path]::GetTempPath())Encelado.Rilascio</Tmp>
|
||||
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
||||
<TokenFile>$(Tmp)\registry.token</TokenFile>
|
||||
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
||||
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
||||
<Setup>$(CartellaPacchetti)\Encelado_$(V).exe</Setup>
|
||||
<Portabile>$(CartellaPacchetti)\Encelado_$(V)_portabile.zip</Portabile>
|
||||
<TemplateAllegato>$(CartellaPacchetti)\encelado-unraid-$(V).xml</TemplateAllegato>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(Tmp)" />
|
||||
@@ -629,6 +626,7 @@
|
||||
<!-- Il token vive qui e solo qui, per il tempo del rilascio. -->
|
||||
<WriteLinesToFile File="$(CurlCfg)" Overwrite="true"
|
||||
Lines="header = "Authorization: token $(GToken)"" />
|
||||
<WriteLinesToFile File="$(TokenFile)" Overwrite="true" Lines="$(GToken)" />
|
||||
|
||||
<!--
|
||||
Il tag esiste gia' in locale: l'ha creato Pacchetto, o c'era prima. Qui va
|
||||
@@ -658,6 +656,20 @@
|
||||
|
||||
<Message Importance="High" Text=" tag $(Tag) sul remoto" />
|
||||
|
||||
<!--
|
||||
L'immagine sul registro dei container di Gitea (stesso host, stesso
|
||||
token: il registro accetta utente + token come password). Il push viene
|
||||
prima della release: se fallisce, non esiste una release che indica
|
||||
un'immagine che nessuno può scaricare.
|
||||
-->
|
||||
<Exec Condition="'$(SaltaDocker)' != 'true'" EchoOff="true" StandardOutputImportance="low"
|
||||
Command="docker login "$(GRegistro)" -u "$(GOwner)" --password-stdin < "$(TokenFile)"" />
|
||||
<Exec Condition="'$(SaltaDocker)' != 'true'" Command="docker push "$(Immagine):$(V)"" />
|
||||
<Exec Condition="'$(SaltaDocker)' != 'true'" Command="docker push "$(Immagine):latest"" />
|
||||
<Exec Condition="'$(SaltaDocker)' != 'true'" ContinueOnError="true" StandardOutputImportance="low"
|
||||
Command="docker logout "$(GRegistro)"" />
|
||||
<Message Condition="'$(SaltaDocker)' != 'true'" Importance="High" Text=" immagine $(Immagine):$(V) e :latest sul registro" />
|
||||
|
||||
<!-- Release gia' presente? -->
|
||||
<Exec EchoOff="true" ContinueOnError="true" StandardOutputImportance="low"
|
||||
Command="curl -s -K "$(CurlCfg)" -o "$(RispostaJson)" "$(Api)/releases/tags/$(Tag)"" />
|
||||
@@ -676,7 +688,7 @@
|
||||
|
||||
<!-- Creazione -->
|
||||
<PreparaCorpoRelease Destinazione="$(CorpoJson)" Tag="$(Tag)" Versione="$(V)"
|
||||
Note="$(Note)" Bozza="$(Bozza)" />
|
||||
Immagine="$(Immagine)" Note="$(Note)" Bozza="$(Bozza)" />
|
||||
|
||||
<Exec EchoOff="true" StandardOutputImportance="low"
|
||||
Command="curl -s -K "$(CurlCfg)" -X POST -H "Content-Type: application/json" --data-binary "@$(CorpoJson)" -o "$(RispostaJson)" "$(Api)/releases"" />
|
||||
@@ -692,17 +704,13 @@
|
||||
<Message Importance="High" Text=" release creata" />
|
||||
|
||||
<!--
|
||||
Allegati: l'installatore e la copia portabile, entrambi già costruiti da
|
||||
Allegati: la copia portabile e il template Unraid, entrambi già pronti da
|
||||
Pacchetto. Nient'altro — in particolare nessun sorgente: si pubblica il
|
||||
programma, non il progetto.
|
||||
|
||||
Gli archivi "Source code" che Gitea mostra da sé sulla pagina della release
|
||||
non arrivano da qui: li genera il server dal tag, e si tolgono solo dalla
|
||||
sua configurazione (DISABLE_DOWNLOAD_SOURCE_ARCHIVES in app.ini).
|
||||
programma, non il progetto. L'immagine non si allega: sta nel registro.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<Allegato Include="$(Setup)" />
|
||||
<Allegato Include="$(CartellaPacchetti)\Encelado_$(V)_portabile.zip" />
|
||||
<Allegato Include="$(Portabile)" />
|
||||
<Allegato Include="$(TemplateAllegato)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Exec Condition="Exists('%(Allegato.FullPath)')" EchoOff="true" StandardOutputImportance="low"
|
||||
@@ -712,11 +720,12 @@
|
||||
Condition="Exists('%(Allegato.FullPath)')" />
|
||||
|
||||
<!-- Il token non deve sopravvivere al rilascio. -->
|
||||
<Delete Files="$(CurlCfg);$(CorpoJson);$(RispostaJson)" ContinueOnError="true" />
|
||||
<Delete Files="$(CurlCfg);$(TokenFile);$(CorpoJson);$(RispostaJson)" ContinueOnError="true" />
|
||||
|
||||
<Message Importance="High" Text=" " />
|
||||
<Message Importance="High" Text="Rilascio completato:" />
|
||||
<Message Importance="High" Text=" $(GUrl)/$(GOwner)/$(GRepo)/releases/tag/$(Tag)" />
|
||||
<Message Importance="High" Text=" docker pull $(Immagine):$(V)" Condition="'$(SaltaDocker)' != 'true'" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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).
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0"?>
|
||||
<Container version="2">
|
||||
<Name>Encelado</Name>
|
||||
<Repository>192.168.30.23:3000/alby96/encelado:latest</Repository>
|
||||
<Registry>http://192.168.30.23:3000/Alby96/-/packages/container/encelado</Registry>
|
||||
<Network>bridge</Network>
|
||||
<MyIP/>
|
||||
<Shell>sh</Shell>
|
||||
<Privileged>false</Privileged>
|
||||
<Support>http://192.168.30.23:3000/Alby96/Encelado/issues</Support>
|
||||
<Project>http://192.168.30.23:3000/Alby96/Encelado</Project>
|
||||
<Overview>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.</Overview>
|
||||
<Category>Tools: Productivity:</Category>
|
||||
<WebUI>http://[IP]:[PORT:8080]/</WebUI>
|
||||
<TemplateURL>http://192.168.30.23:3000/Alby96/Encelado/raw/branch/main/deploy/unraid/encelado.xml</TemplateURL>
|
||||
<Icon>http://192.168.30.23:3000/Alby96/Encelado/raw/branch/main/assets/encelado.png</Icon>
|
||||
<ExtraParams>--restart=unless-stopped --stop-timeout 45</ExtraParams>
|
||||
<PostArgs/>
|
||||
<CPUset/>
|
||||
<DateInstalled/>
|
||||
<DonateText/>
|
||||
<DonateLink/>
|
||||
<Requires/>
|
||||
|
||||
<Config Name="Porta web" Target="8080" Default="8080" Mode="tcp" Description="Porta dell'interfaccia web e dell'API (http://IP:porta/)." Type="Port" Display="always" Required="true" Mask="false">8080</Config>
|
||||
<Config Name="Configurazione (/config)" Target="/config" Default="/mnt/user/appdata/encelado/config" Mode="rw" Description="encelado.json, strategy.json, instruments.json e il file cifrato delle chiavi. Seminati al primo avvio dai valori di fabbrica." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/encelado/config</Config>
|
||||
<Config Name="Dati (/data)" Target="/data" Default="/mnt/user/appdata/encelado/data" Mode="rw" Description="Ledger (data/ledger), stato (data/state), barre, cache dei feed, conoscenza, rapporti e log. Non cancellare: il ledger e' la memoria del bot." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/encelado/data</Config>
|
||||
|
||||
<Config Name="ETORO_API_KEY" Target="ETORO_API_KEY" Default="" Mode="" Description="Chiave x-api-key di eToro Public API (portale sviluppatori). In alternativa si salvano dall'interfaccia, cifrate con ENCELADO_KEY_PASSPHRASE." Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<Config Name="ETORO_USER_KEY" Target="ETORO_USER_KEY" Default="" Mode="" Description="Chiave x-user-key di eToro Public API. Demo e reale hanno chiavi diverse." Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<Config Name="ETORO_ENVIRONMENT" Target="ETORO_ENVIRONMENT" Default="demo" Mode="" Description="Ambiente eToro: demo (denaro virtuale) oppure real (conto reale; serve anche ENCELADO_EXECUTION_MODE=Live e la frase di conferma)." Type="Variable" Display="always" Required="true" Mask="false">demo</Config>
|
||||
<Config Name="ENCELADO_EXECUTION_MODE" Target="ENCELADO_EXECUTION_MODE" Default="Demo" Mode="" Description="Paper (simulatore locale sopra le quotazioni reali), Demo (conto demo eToro, predefinito) o Live (conto reale: richiede run.allowLive=true in encelado.json e la frase in ENCELADO_CONFIRM_LIVE)." Type="Variable" Display="always" Required="true" Mask="false">Demo</Config>
|
||||
<Config Name="ENCELADO_CONFIRM_LIVE" Target="ENCELADO_CONFIRM_LIVE" Default="" Mode="" Description="Solo per la modalita' Live: la frase esatta CONFERMO LIVE. Con qualunque altro valore il bot non parte sul conto reale." Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
|
||||
<Config Name="ENCELADO_WEB_TOKEN" Target="ENCELADO_WEB_TOKEN" Default="" Mode="" Description="Token di accesso all'interfaccia web (una stringa lunga a piacere). SENZA token il server ascolta solo su localhost del container e l'interfaccia non e' raggiungibile dalla rete: mettilo." Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
||||
<Config Name="ENCELADO_WEB_PORT" Target="ENCELADO_WEB_PORT" Default="8080" Mode="" Description="Porta interna di Kestrel. Cambiala solo se cambi anche la mappatura della porta qui sopra." Type="Variable" Display="advanced" Required="false" Mask="false">8080</Config>
|
||||
<Config Name="ENCELADO_DISPLAY_CURRENCY" Target="ENCELADO_DISPLAY_CURRENCY" Default="USD" Mode="" Description="Valuta con cui l'interfaccia mostra gli importi (USD, EUR, GBP, CHF, JPY, AUD, CAD, NZD). Il ledger resta in USD; il tasso viene dalle quotazioni eToro." Type="Variable" Display="always" Required="false" Mask="false">USD</Config>
|
||||
<Config Name="ENCELADO_KEY_PASSPHRASE" Target="ENCELADO_KEY_PASSPHRASE" Default="" Mode="" Description="Passphrase con cui l'interfaccia cifra le chiavi eToro salvate in /config/etoro.keys.enc (AES-256-GCM). Non serve se le chiavi arrivano dalle variabili qui sopra." Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
|
||||
<Config Name="TELEGRAM_BOT_TOKEN" Target="TELEGRAM_BOT_TOKEN" Default="" Mode="" Description="Token del bot Telegram (da @BotFather) per notifiche e comandi. Vuoto = Telegram spento." Type="Variable" Display="always" Required="false" Mask="true"></Config>
|
||||
<Config Name="TELEGRAM_CHAT_ID" Target="TELEGRAM_CHAT_ID" Default="" Mode="" Description="Chat id autorizzata a ricevere le notifiche e a mandare i comandi (/stato, /posizioni, /pausa, /kill CONFERMO...). Ogni altro mittente viene ignorato." Type="Variable" Display="always" Required="false" Mask="false"></Config>
|
||||
<Config Name="TZ" Target="TZ" Default="Europe/Rome" Mode="" Description="Fuso orario degli orari a schermo e nel log (nome IANA). Il ledger resta in UTC." Type="Variable" Display="always" Required="false" Mask="false">Europe/Rome</Config>
|
||||
<Config Name="PUID" Target="PUID" Default="99" Mode="" Description="Utente con cui gira il bot e a cui vengono assegnati /config e /data (99 = nobody su Unraid)." Type="Variable" Display="advanced" Required="false" Mask="false">99</Config>
|
||||
<Config Name="PGID" Target="PGID" Default="100" Mode="" Description="Gruppo del bot (100 = users su Unraid)." Type="Variable" Display="advanced" Required="false" Mask="false">100</Config>
|
||||
</Container>
|
||||
@@ -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
|
||||
@@ -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:<versione>` e `:latest`, sul registro dei container di Gitea. Si costruisce dalla `Dockerfile` alla radice:
|
||||
|
||||
```powershell
|
||||
dotnet msbuild build/Release.proj -t:Docker # :<versione dal tag> 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 <token>`. |
|
||||
| `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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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` (`<registro>/alby96/encelado:<versione>` 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`.
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user