Perché: l'utente ha chiesto un bot che operi cinque basket di coppie forex correlate su eToro, autonomo, con ledger, feed gratuiti e apprendimento costruito da zero, e ha deciso di eliminare tutto ciò che restava delle gestioni precedenti (Binance, cTrader/proba, ricerca con SQLite, GBDT, RL, TA-Lib) e di non avere approvazioni manuali sui singoli ordini. Cosa cambia: - nuovo Core dei basket (cross sintetici, decisore, cost gate, sizing, esecutore leg-risk, backtest con PSR/DSR/PBO, livelli 0-3 di apprendimento), adattatore eToro Public API, motore autonomo con equity stop, kill-switch, riconciliazione, ledger append-only, calendario e notizie con sentiment; - modalità Paper / Demo / Live (Live con flag e frase CONFERMO LIVE); - interfaccia rifatta: barra in alto con tre schede, dashboard con i soli numeri principali, fuso orario selezionabile, test di rendering in PNG; - corretto il parser dei costi eToro (campo "value"): markup e overnight non venivano letti; - strumento di ricerca ridotto a ticks / baskets / falsify con due scenari di costo; risultati in results/ e reports/: nessuna configurazione è profittevole al netto dei costi (docs/STRATEGY.md lo dice con i numeri); - documentazione completa (STRATEGY, ML_AND_LEARNING, RUNBOOK, GLOSSARY, KNOWN_ISSUES, ADR-0004, ADR-0005) e catena di rilascio aggiornata. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
723 lines
36 KiB
XML
723 lines
36 KiB
XML
<!--
|
|
═══════════════════════════════════════════════════════════════════════════
|
|
Encelado — catena di verifica, pacchetto e rilascio
|
|
═══════════════════════════════════════════════════════════════════════════
|
|
|
|
Un solo file, nessuno script. Si richiama con `dotnet msbuild`, dalle attività
|
|
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:Pacchetto
|
|
dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=3.3.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
|
|
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.
|
|
|
|
── Perché MSBuild e non uno script ──────────────────────────────────────
|
|
La catena vive accanto al codice che rilascia ed è versionata con lui: fra
|
|
sei mesi, ripescato un tag, questo file ricostruisce quel pacchetto e non
|
|
quello di oggi. La logica non banale (leggere e riscrivere la versione,
|
|
parlare con Gitea) sta in attività C# in linea: si legge come codice, non
|
|
come una successione di comandi.
|
|
|
|
── 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 modo previsto è taggare e poi rilasciare:
|
|
|
|
git tag v3.3.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
|
|
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.
|
|
-->
|
|
<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>
|
|
<TestProj>$(Radice)\tests\Encelado.Tests\Encelado.Tests.csproj</TestProj>
|
|
<BacktestProj>$(Radice)\tools\Encelado.Backtest\Encelado.Backtest.csproj</BacktestProj>
|
|
<Iss>$(MSBuildThisFileDirectory)Encelado.iss</Iss>
|
|
|
|
<!-- La versione non sta nel .csproj come in AutoBidder: sta in
|
|
Directory.Build.props, da cui la ereditano tutti e quattro 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>
|
|
<CartellaPacchetti>$(Radice)\bin\installer</CartellaPacchetti>
|
|
|
|
<!-- ── 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. -->
|
|
<CartellaProve>$([System.IO.Path]::GetTempPath())Encelado.Verifica</CartellaProve>
|
|
|
|
<!-- Serve solo quando HEAD non è ancora taggato: è il numero del tag da
|
|
creare. Vuoto = la minor successiva all'ultimo tag. Vedi VersioneDaTag. -->
|
|
<Versione Condition="'$(Versione)' == ''"></Versione>
|
|
|
|
<!--
|
|
Note di rilascio. Vuote = solo il numero di versione.
|
|
|
|
Arrivano da una variabile d'ambiente, non da -p:. MSBuild spezza il valore
|
|
di una proprietà sulle virgole: `-p:Note=uno, due` diventa la proprietà
|
|
Note=uno più l'opzione " due", e si finisce su MSB1006 "proprietà non
|
|
valida". Una nota di rilascio in italiano contiene quasi sempre una
|
|
virgola, quindi il passaggio per riga di comando è inutilizzabile.
|
|
L'ambiente non ha questo problema, e MSBuild legge le variabili
|
|
d'ambiente come proprietà.
|
|
|
|
-p:Note= resta accettato per chi lo passa a mano senza virgole.
|
|
-->
|
|
<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. -->
|
|
<AmbientePulito>MSBUILD_EXE_PATH=;MSBuildLoadMicrosoftTargetsReadOnly=</AmbientePulito>
|
|
|
|
<SaltaVerifica Condition="'$(SaltaVerifica)' == ''">false</SaltaVerifica>
|
|
<Sovrascrivi Condition="'$(Sovrascrivi)' == ''">false</Sovrascrivi>
|
|
<Bozza Condition="'$(Bozza)' == ''">false</Bozza>
|
|
|
|
<!-- Un rilascio da albero sporco produce un tag che non corrisponde a
|
|
nessuno stato ricostruibile: è esattamente la garanzia che il tag come
|
|
fonte unica dovrebbe dare. Si può forzare, ma va detto. -->
|
|
<ConsentiModifiche Condition="'$(ConsentiModifiche)' == ''">false</ConsentiModifiche>
|
|
</PropertyGroup>
|
|
|
|
<!-- ═════════════════════ Attività in linea ═════════════════════ -->
|
|
|
|
<!--
|
|
Decide quale versione si sta costruendo, senza toccare niente.
|
|
|
|
Il tag su HEAD, se c'è, comanda: è la fonte. Se non c'è se ne propone uno —
|
|
il numero chiesto, o la minor successiva al tag più alto esistente — che
|
|
verrà creato solo a pacchetto pronto, da CreaTag.
|
|
-->
|
|
<UsingTask TaskName="VersioneDaTag" TaskFactory="RoslynCodeTaskFactory"
|
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
|
<ParameterGroup>
|
|
<TagsHead ParameterType="System.String" />
|
|
<TagsTutti ParameterType="System.String" />
|
|
<Richiesta ParameterType="System.String" />
|
|
<FileVersione ParameterType="System.String" Required="true" />
|
|
<Versione ParameterType="System.String" Output="true" />
|
|
<Tag ParameterType="System.String" Output="true" />
|
|
<DaCreare ParameterType="System.String" Output="true" />
|
|
</ParameterGroup>
|
|
<Task>
|
|
<Using Namespace="System" />
|
|
<Using Namespace="System.IO" />
|
|
<Using Namespace="System.Collections.Generic" />
|
|
<Using Namespace="System.Text.RegularExpressions" />
|
|
<Code Type="Fragment" Language="cs">
|
|
<![CDATA[
|
|
// ConsoleToMSBuild unisce le righe con il punto e virgola; git le separa
|
|
// con a-capo. Si accettano entrambi e non si fanno domande.
|
|
Func<string, string[]> spezza = s =>
|
|
(s ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var forma = new Regex(@"^v(\d+)\.(\d+)\.(\d+)$");
|
|
|
|
var suHead = new List<string>();
|
|
foreach (var t in spezza(TagsHead))
|
|
if (forma.IsMatch(t.Trim())) suHead.Add(t.Trim());
|
|
|
|
if (suHead.Count > 1)
|
|
{
|
|
Log.LogError(
|
|
"HEAD ha piu' di un tag di versione (" + string.Join(", ", suHead) + ").\n" +
|
|
"Non si puo' sapere quale sia il rilascio: tienine uno solo con git tag -d <tag>");
|
|
return false;
|
|
}
|
|
|
|
var chiesta = (Richiesta ?? "").Trim();
|
|
if (chiesta.Length > 0 && !Regex.IsMatch(chiesta, @"^\d+\.\d+\.\d+$"))
|
|
{
|
|
// Un refuso qui produrrebbe un tag e un pacchetto sbagliati.
|
|
Log.LogError("Versione '" + chiesta + "' non valida: serve la forma X.Y.Z");
|
|
return false;
|
|
}
|
|
|
|
if (suHead.Count == 1)
|
|
{
|
|
var dalTag = suHead[0].Substring(1);
|
|
|
|
if (chiesta.Length > 0 && chiesta != dalTag)
|
|
{
|
|
Log.LogError(
|
|
"HEAD e' gia' taggato " + suHead[0] + ", ma e' stata chiesta la versione " + chiesta + ".\n" +
|
|
"Il tag e' la fonte: o rilasci " + dalTag + " lasciando vuota la versione,\n" +
|
|
"oppure togli il tag con git tag -d " + suHead[0] + " e rilancia.");
|
|
return false;
|
|
}
|
|
|
|
Versione = dalTag;
|
|
Tag = suHead[0];
|
|
DaCreare = "false";
|
|
Log.LogMessage(MessageImportance.High, " versione " + Versione + " — dal tag " + Tag + " su HEAD");
|
|
return true;
|
|
}
|
|
|
|
if (chiesta.Length > 0)
|
|
{
|
|
Versione = chiesta;
|
|
}
|
|
else
|
|
{
|
|
// La minor successiva al tag piu' alto: cosi' il numero proposto e'
|
|
// sempre libero, anche se il tag piu' alto sta su un altro ramo.
|
|
int maggiore = -1, minore = -1;
|
|
foreach (var t in spezza(TagsTutti))
|
|
{
|
|
var m = forma.Match(t.Trim());
|
|
if (!m.Success) continue;
|
|
|
|
int ma = int.Parse(m.Groups[1].Value), mi = int.Parse(m.Groups[2].Value);
|
|
if (ma > maggiore || (ma == maggiore && mi > minore)) { maggiore = ma; minore = mi; }
|
|
}
|
|
|
|
if (maggiore < 0)
|
|
{
|
|
// Primo rilascio: non c'e' nessun tag da cui ripartire, e l'unico
|
|
// numero che esiste e' quello delle compilazioni di sviluppo.
|
|
var testo = File.Exists(FileVersione) ? File.ReadAllText(FileVersione) : "";
|
|
var m = Regex.Match(testo, @"<Version>(\d+)\.(\d+)\.\d+</Version>");
|
|
if (!m.Success)
|
|
{
|
|
Log.LogError(
|
|
"Nessun tag di versione nel repository e <Version> illeggibile in " + FileVersione + ".\n" +
|
|
"Indica la versione con -p:Versione=X.Y.Z");
|
|
return false;
|
|
}
|
|
|
|
maggiore = int.Parse(m.Groups[1].Value);
|
|
minore = int.Parse(m.Groups[2].Value);
|
|
}
|
|
|
|
Versione = maggiore + "." + (minore + 1) + ".0";
|
|
}
|
|
|
|
Tag = "v" + Versione;
|
|
DaCreare = "true";
|
|
Log.LogMessage(MessageImportance.High, " versione " + Versione + " — il tag " + Tag + " sara' creato a pacchetto pronto");
|
|
]]>
|
|
</Code>
|
|
</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.
|
|
|
|
L'alternativa naturale sarebbe HttpClient in un'attività C# in linea, ma
|
|
RoslynCodeTaskFactory referenzia solo gli assembly di base: System.Net.Http e
|
|
System.Text.Json andrebbero indicati per percorso assoluto, dentro il runtime
|
|
condiviso, con il numero di versione nel mezzo. Un percorso che oggi funziona e
|
|
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.
|
|
-->
|
|
|
|
<UsingTask TaskName="LeggiConfigGitea" TaskFactory="RoslynCodeTaskFactory"
|
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
|
<ParameterGroup>
|
|
<Percorso ParameterType="System.String" Required="true" />
|
|
<Url ParameterType="System.String" Output="true" />
|
|
<Owner ParameterType="System.String" Output="true" />
|
|
<Repo ParameterType="System.String" Output="true" />
|
|
<Token ParameterType="System.String" Output="true" />
|
|
</ParameterGroup>
|
|
<Task>
|
|
<Using Namespace="System" />
|
|
<Using Namespace="System.IO" />
|
|
<Using Namespace="System.Text.RegularExpressions" />
|
|
<Code Type="Fragment" Language="cs">
|
|
<![CDATA[
|
|
var testo = File.Exists(Percorso) ? File.ReadAllText(Percorso) : "";
|
|
|
|
// Le variabili d'ambiente hanno la precedenza sul file: una macchina
|
|
// condivisa deve poter rilasciare senza scrivere un token su disco, e un
|
|
// file dimenticato non deve vincere su una scelta esplicita.
|
|
Func<string,string,string> leggi = (env, campo) =>
|
|
{
|
|
var v = Environment.GetEnvironmentVariable(env);
|
|
if (!string.IsNullOrWhiteSpace(v)) return v.Trim();
|
|
|
|
var m = Regex.Match(testo, "\"" + campo + "\"\\s*:\\s*\"([^\"]*)\"");
|
|
return m.Success ? m.Groups[1].Value.Trim() : "";
|
|
};
|
|
|
|
Url = leggi("GITEA_URL", "url").TrimEnd('/');
|
|
Owner = leggi("GITEA_OWNER", "owner");
|
|
Repo = leggi("GITEA_REPO", "repo");
|
|
Token = leggi("GITEA_TOKEN", "token");
|
|
|
|
if (Url.Length == 0 || Owner.Length == 0 || Repo.Length == 0 || Token.Length == 0)
|
|
{
|
|
Log.LogError(
|
|
"Configurazione di Gitea incompleta. Servono url, owner, repo, token:\n" +
|
|
" 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.");
|
|
|
|
// 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;
|
|
}
|
|
]]>
|
|
</Code>
|
|
</Task>
|
|
</UsingTask>
|
|
|
|
<UsingTask TaskName="PreparaCorpoRelease" TaskFactory="RoslynCodeTaskFactory"
|
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
|
<ParameterGroup>
|
|
<Destinazione ParameterType="System.String" Required="true" />
|
|
<Tag ParameterType="System.String" Required="true" />
|
|
<Versione ParameterType="System.String" Required="true" />
|
|
<Note ParameterType="System.String" />
|
|
<Bozza ParameterType="System.Boolean" />
|
|
</ParameterGroup>
|
|
<Task>
|
|
<Using Namespace="System" />
|
|
<Using Namespace="System.IO" />
|
|
<Code Type="Fragment" Language="cs">
|
|
<![CDATA[
|
|
// Le note arrivano da un prompt: possono contenere virgolette, barre e
|
|
// a-capo. Scritte grezze romperebbero il JSON, o peggio lo cambierebbero.
|
|
Func<string,string> esc = t => (t ?? "")
|
|
.Replace("\\", "\\\\").Replace("\"", "\\\"")
|
|
.Replace("\r", "").Replace("\n", "\\n").Replace("\t", " ");
|
|
|
|
var note = string.IsNullOrWhiteSpace(Note) ? "Versione " + Versione + "." : Note;
|
|
|
|
File.WriteAllText(Destinazione,
|
|
"{\"tag_name\":\"" + esc(Tag) + "\"," +
|
|
"\"name\":\"Encelado " + esc(Versione) + "\"," +
|
|
"\"body\":\"" + esc(note) + "\"," +
|
|
"\"draft\":" + (Bozza ? "true" : "false") + "," +
|
|
"\"prerelease\":false}");
|
|
]]>
|
|
</Code>
|
|
</Task>
|
|
</UsingTask>
|
|
|
|
<UsingTask TaskName="LeggiIdRelease" TaskFactory="RoslynCodeTaskFactory"
|
|
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
|
<ParameterGroup>
|
|
<Risposta ParameterType="System.String" Required="true" />
|
|
<Id ParameterType="System.String" Output="true" />
|
|
<Errore ParameterType="System.String" Output="true" />
|
|
</ParameterGroup>
|
|
<Task>
|
|
<Using Namespace="System" />
|
|
<Using Namespace="System.IO" />
|
|
<Using Namespace="System.Text.RegularExpressions" />
|
|
<Code Type="Fragment" Language="cs">
|
|
<![CDATA[
|
|
var testo = File.Exists(Risposta) ? File.ReadAllText(Risposta) : "";
|
|
|
|
// L'id della release e' il primo campo "id" della risposta; quelli annidati
|
|
// (autore, allegati) vengono dopo. Si prende il primo e basta.
|
|
var m = Regex.Match(testo, "\"id\"\\s*:\\s*(\\d+)");
|
|
Id = m.Success ? m.Groups[1].Value : "";
|
|
|
|
if (Id.Length == 0)
|
|
{
|
|
var msg = Regex.Match(testo, "\"message\"\\s*:\\s*\"([^\"]*)\"");
|
|
Errore = msg.Success ? msg.Groups[1].Value
|
|
: (testo.Length > 200 ? testo.Substring(0, 200) : testo);
|
|
}
|
|
]]>
|
|
</Code>
|
|
</Task>
|
|
</UsingTask>
|
|
|
|
<!-- ═════════════════════ Verifica ═════════════════════ -->
|
|
|
|
<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)" />
|
|
|
|
<Message Importance="High" Text=" tutto a posto" />
|
|
</Target>
|
|
|
|
<!-- ═════════════════════ Ricerca sui basket ═════════════════════ -->
|
|
|
|
<!--
|
|
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.
|
|
|
|
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
|
|
-->
|
|
<Target Name="Backtest">
|
|
<PropertyGroup>
|
|
<Comando Condition="'$(Comando)' == ''">baskets</Comando>
|
|
<BacktestExe>$(Radice)\tools\Encelado.Backtest\bin\Release\net10.0\backtest.exe</BacktestExe>
|
|
</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)." />
|
|
|
|
<Error Condition="!Exists('$(Dati)')" Text="Cartella dati non trovata: $(Dati)" />
|
|
|
|
<Message Importance="High" Text="== Ricerca sui basket ==" />
|
|
<Message Importance="High" Text=" dati : $(Dati)" />
|
|
<Message Importance="High" Text=" comando : $(Comando) $(Extra)" />
|
|
|
|
<Exec WorkingDirectory="$(Radice)" EnvironmentVariables="$(AmbientePulito)"
|
|
Command="dotnet build "$(BacktestProj)" -c Release --nologo -v q" />
|
|
|
|
<Exec WorkingDirectory="$(Radice)"
|
|
Command=""$(BacktestExe)" $(Comando) --data "$(Dati)" $(Extra)" />
|
|
</Target>
|
|
|
|
<!-- ═════════════════════ Eseguibile ═════════════════════ -->
|
|
|
|
<!--
|
|
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.
|
|
|
|
── 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.
|
|
-->
|
|
<Target Name="Pubblica" DependsOnTargets="DeterminaVersione">
|
|
<Message Importance="High" Text="== Pubblicazione dell'eseguibile ($(V)) ==" />
|
|
|
|
<!-- Cartella pulita a ogni giro. Senza questo i resti di una pubblicazione
|
|
precedente — una DLL rinominata, un runtime cambiato — finiscono nel
|
|
pacchetto, ed è il tipo di problema che si manifesta solo sulla
|
|
macchina di qualcun altro. -->
|
|
<RemoveDir Directories="$(CartellaPubblicazione)" ContinueOnError="true" />
|
|
|
|
<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)"" />
|
|
|
|
<Error Condition="!Exists('$(CartellaPubblicazione)\Encelado.exe')"
|
|
Text="Pubblicazione fallita: Encelado.exe 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." />
|
|
|
|
<!--
|
|
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>
|
|
|
|
<Error Condition="'@(SorgenteIntruso)' != ''"
|
|
Text="Nella cartella pubblicata ci sono file che non sono programma: @(SorgenteIntruso->'%(Filename)%(Extension)', ', ').%0ANon devono finire nella release." />
|
|
</Target>
|
|
|
|
<!-- ═════════════════════ Pacchetto ═════════════════════ -->
|
|
|
|
<!--
|
|
L'ordine conta: si verifica, si costruisce, e il tag si crea per ultimo.
|
|
|
|
Il contrario sembra piu' naturale — decidi il numero, poi costruisci — ma
|
|
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.
|
|
-->
|
|
<Target Name="Pacchetto" DependsOnTargets="Verifica;DeterminaVersione;Pubblica">
|
|
<Message Importance="High" Text="== Creazione dell'installatore ==" />
|
|
|
|
<TrovaInnoSetup>
|
|
<Output TaskParameter="Percorso" PropertyName="Iscc" />
|
|
</TrovaInnoSetup>
|
|
|
|
<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>
|
|
</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.
|
|
-->
|
|
<Delete Files="$(Portabile)" ContinueOnError="true" />
|
|
<ZipDirectory SourceDirectory="$(CartellaPubblicazione)" DestinationFile="$(Portabile)" />
|
|
|
|
<!-- 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)" />
|
|
</Target>
|
|
|
|
<!-- ═════════════════════ Versione e tag ═════════════════════ -->
|
|
|
|
<Target Name="DeterminaVersione">
|
|
<!-- Nessuno dei due comandi fallisce mai: senza tag l'uscita e' vuota, e
|
|
basta. `git describe` invece esce in errore, e qui non serve. -->
|
|
<Exec Command="git tag --points-at HEAD" WorkingDirectory="$(Radice)"
|
|
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
|
<Output TaskParameter="ConsoleOutput" PropertyName="TagSuHead" />
|
|
</Exec>
|
|
|
|
<Exec Command="git tag --list v*" WorkingDirectory="$(Radice)"
|
|
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
|
<Output TaskParameter="ConsoleOutput" PropertyName="TagEsistenti" />
|
|
</Exec>
|
|
|
|
<VersioneDaTag TagsHead="$(TagSuHead)" TagsTutti="$(TagEsistenti)"
|
|
Richiesta="$(Versione)" FileVersione="$(Props)">
|
|
<Output TaskParameter="Versione" PropertyName="V" />
|
|
<Output TaskParameter="Tag" PropertyName="Tag" />
|
|
<Output TaskParameter="DaCreare" PropertyName="TagDaCreare" />
|
|
</VersioneDaTag>
|
|
</Target>
|
|
|
|
<Target Name="CreaTag" Condition="'$(TagDaCreare)' == 'true'">
|
|
<!-- Solo le modifiche ai file gia' tracciati: un appunto non versionato
|
|
accanto al progetto non cambia cosa viene compilato. -->
|
|
<Exec Command="git status --porcelain --untracked-files=no" WorkingDirectory="$(Radice)"
|
|
ConsoleToMSBuild="true" StandardOutputImportance="low">
|
|
<Output TaskParameter="ConsoleOutput" PropertyName="AlberoSporco" />
|
|
</Exec>
|
|
|
|
<Error Condition="'$(AlberoSporco)' != '' AND '$(ConsentiModifiche)' != 'true'"
|
|
Text="Ci sono modifiche non committate: il tag $(Tag) indicherebbe uno stato che non e' ricostruibile.%0ACommitta prima di rilasciare, oppure rilancia con -p:ConsentiModifiche=true se sai cosa stai facendo.%0A%0A$(AlberoSporco)" />
|
|
|
|
<Exec Command="git tag -a $(Tag) -m "Encelado $(V)"" WorkingDirectory="$(Radice)"
|
|
StandardOutputImportance="low" StandardErrorImportance="low" />
|
|
|
|
<Message Importance="High" Text=" tag $(Tag) creato" />
|
|
</Target>
|
|
|
|
<!-- ═════════════════════ Rilascio ═════════════════════ -->
|
|
|
|
<!-- Prima di tutto il resto: un token mancante non deve costare due minuti di
|
|
compilazione 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" />
|
|
</LeggiConfigGitea>
|
|
</Target>
|
|
|
|
<Target Name="Rilascia" DependsOnTargets="ConfigGitea;Pacchetto">
|
|
<Message Importance="High" Text="== Pubblicazione su Gitea ==" />
|
|
|
|
<PropertyGroup>
|
|
<Api>$(GUrl)/api/v1/repos/$(GOwner)/$(GRepo)</Api>
|
|
<Tmp>$([System.IO.Path]::GetTempPath())Encelado.Rilascio</Tmp>
|
|
<CurlCfg>$(Tmp)\curl.cfg</CurlCfg>
|
|
<CorpoJson>$(Tmp)\release.json</CorpoJson>
|
|
<RispostaJson>$(Tmp)\risposta.json</RispostaJson>
|
|
<Setup>$(CartellaPacchetti)\Encelado_$(V).exe</Setup>
|
|
</PropertyGroup>
|
|
|
|
<MakeDir Directories="$(Tmp)" />
|
|
|
|
<!-- Il token vive qui e solo qui, per il tempo del rilascio. -->
|
|
<WriteLinesToFile File="$(CurlCfg)" Overwrite="true"
|
|
Lines="header = "Authorization: token $(GToken)"" />
|
|
|
|
<!--
|
|
Il tag esiste gia' in locale: l'ha creato Pacchetto, o c'era prima. Qui va
|
|
spinto, e va verificato che ci sia arrivato.
|
|
|
|
Non e' una formalita'. Il push qui sotto spinge i tag, non i commit: se il
|
|
ramo e' indietro, il tag punta a un oggetto che il remoto non conosce e il
|
|
push viene rifiutato. A quel punto la creazione della release non
|
|
fallisce — Gitea, non trovando il tag, lo crea da se' sulla testa del ramo
|
|
predefinito. Verrebbe pubblicata una release che dichiara di essere il
|
|
commit X mentre il codice allegato e' il commit Y, e nessuno se ne
|
|
accorgerebbe. Meglio fermarsi e dire cosa manca.
|
|
|
|
I commit non si spingono da qui: quando spingere il ramo lo decide chi
|
|
lavora, non la catena di rilascio.
|
|
-->
|
|
<Exec Command="git push --tags" WorkingDirectory="$(Radice)" ContinueOnError="true"
|
|
StandardOutputImportance="low" StandardErrorImportance="low" />
|
|
|
|
<Exec Command="git ls-remote --tags origin refs/tags/$(Tag)" WorkingDirectory="$(Radice)"
|
|
ConsoleToMSBuild="true" ContinueOnError="true" StandardOutputImportance="low">
|
|
<Output TaskParameter="ConsoleOutput" PropertyName="TagSulRemoto" />
|
|
</Exec>
|
|
|
|
<Error Condition="'$(TagSulRemoto)' == ''"
|
|
Text="Il tag $(Tag) non e' arrivato sul remoto, quasi sempre perche' il ramo e' indietro.%0AGitea creerebbe la release sulla testa del ramo predefinito: il codice allegato non corrisponderebbe al commit dichiarato.%0A%0ASpingi il ramo e rilancia: git push" />
|
|
|
|
<Message Importance="High" Text=" tag $(Tag) sul remoto" />
|
|
|
|
<!-- Release gia' presente? -->
|
|
<Exec EchoOff="true" ContinueOnError="true" StandardOutputImportance="low"
|
|
Command="curl -s -K "$(CurlCfg)" -o "$(RispostaJson)" "$(Api)/releases/tags/$(Tag)"" />
|
|
|
|
<LeggiIdRelease Risposta="$(RispostaJson)">
|
|
<Output TaskParameter="Id" PropertyName="IdEsistente" />
|
|
</LeggiIdRelease>
|
|
|
|
<Error Condition="'$(IdEsistente)' != '' AND '$(Sovrascrivi)' != 'true'"
|
|
Text="La release $(Tag) esiste gia'. Alza la versione, oppure rilancia con -p:Sovrascrivi=true." />
|
|
|
|
<Exec Condition="'$(IdEsistente)' != ''" EchoOff="true" StandardOutputImportance="low"
|
|
Command="curl -s -K "$(CurlCfg)" -X DELETE "$(Api)/releases/$(IdEsistente)"" />
|
|
<Message Condition="'$(IdEsistente)' != ''" Importance="High"
|
|
Text=" release $(Tag) esistente: sostituita" />
|
|
|
|
<!-- Creazione -->
|
|
<PreparaCorpoRelease Destinazione="$(CorpoJson)" Tag="$(Tag)" Versione="$(V)"
|
|
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"" />
|
|
|
|
<LeggiIdRelease Risposta="$(RispostaJson)">
|
|
<Output TaskParameter="Id" PropertyName="IdRelease" />
|
|
<Output TaskParameter="Errore" PropertyName="ErroreRelease" />
|
|
</LeggiIdRelease>
|
|
|
|
<Error Condition="'$(IdRelease)' == ''"
|
|
Text="Creazione della release non riuscita: $(ErroreRelease)" />
|
|
|
|
<Message Importance="High" Text=" release creata" />
|
|
|
|
<!--
|
|
Allegati: l'installatore e la copia portabile, entrambi già costruiti 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).
|
|
-->
|
|
<ItemGroup>
|
|
<Allegato Include="$(Setup)" />
|
|
<Allegato Include="$(CartellaPacchetti)\Encelado_$(V)_portabile.zip" />
|
|
</ItemGroup>
|
|
|
|
<Exec Condition="Exists('%(Allegato.FullPath)')" EchoOff="true" StandardOutputImportance="low"
|
|
Command="curl -s -K "$(CurlCfg)" -X POST -F "attachment=@%(Allegato.FullPath)" "$(Api)/releases/$(IdRelease)/assets?name=%(Allegato.Filename)%(Allegato.Extension)"" />
|
|
|
|
<Message Importance="High" Text=" caricato %(Allegato.Filename)%(Allegato.Extension)"
|
|
Condition="Exists('%(Allegato.FullPath)')" />
|
|
|
|
<!-- Il token non deve sopravvivere al rilascio. -->
|
|
<Delete Files="$(CurlCfg);$(CorpoJson);$(RispostaJson)" ContinueOnError="true" />
|
|
|
|
<Message Importance="High" Text=" " />
|
|
<Message Importance="High" Text="Rilascio completato:" />
|
|
<Message Importance="High" Text=" $(GUrl)/$(GOwner)/$(GRepo)/releases/tag/$(Tag)" />
|
|
</Target>
|
|
|
|
</Project>
|