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>
140 lines
5.0 KiB
C#
140 lines
5.0 KiB
C#
using Encelado.Bot.Configuration;
|
||
|
||
namespace Encelado.Tests;
|
||
|
||
/// <summary>
|
||
/// Every test redirects the store to a scratch directory via ENCELADO_HOME, so the
|
||
/// developer's real saved keys are never read, written or deleted.
|
||
/// </summary>
|
||
public sealed class EtoroKeyStoreTests : IDisposable
|
||
{
|
||
private readonly string _home;
|
||
private readonly string? _previousHome;
|
||
|
||
public EtoroKeyStoreTests()
|
||
{
|
||
_previousHome = Environment.GetEnvironmentVariable("ENCELADO_HOME");
|
||
_home = Path.Combine(Path.GetTempPath(), $"encelado-store-{Guid.NewGuid():N}");
|
||
Environment.SetEnvironmentVariable("ENCELADO_HOME", _home);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
Environment.SetEnvironmentVariable("ENCELADO_HOME", _previousHome);
|
||
if (Directory.Exists(_home))
|
||
{
|
||
Directory.Delete(_home, recursive: true);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void HonoursTheHomeOverride() =>
|
||
Assert.Equal(Path.Combine(_home, "etoro.dat"), EtoroKeyStore.FilePath);
|
||
|
||
[Fact]
|
||
public void KeysRoundTripPerEnvironment()
|
||
{
|
||
Assert.False(EtoroKeyStore.Exists);
|
||
Assert.Null(EtoroKeyStore.Load(demo: true));
|
||
|
||
EtoroKeyStore.Save(demo: true, new EtoroKeys("api-demo", "user-demo", DateTime.UtcNow));
|
||
EtoroKeyStore.Save(demo: false, new EtoroKeys("api-real", "user-real", DateTime.UtcNow));
|
||
|
||
Assert.True(EtoroKeyStore.Exists);
|
||
Assert.Equal("api-demo", EtoroKeyStore.Load(demo: true)!.ApiKey);
|
||
Assert.Equal("user-real", EtoroKeyStore.Load(demo: false)!.UserKey);
|
||
|
||
Assert.True(EtoroKeyStore.Clear(demo: true));
|
||
Assert.Null(EtoroKeyStore.Load(demo: true));
|
||
Assert.NotNull(EtoroKeyStore.Load(demo: false));
|
||
Assert.True(EtoroKeyStore.Clear(demo: false));
|
||
Assert.False(EtoroKeyStore.Exists);
|
||
Assert.False(EtoroKeyStore.Clear(demo: false));
|
||
}
|
||
|
||
[Fact]
|
||
public void KeysAreNotReadableAsPlainTextOnWindows()
|
||
{
|
||
EtoroKeyStore.Save(demo: true, new EtoroKeys("api-key-value", "user-key-value", DateTime.UtcNow));
|
||
string onDisk = File.ReadAllText(EtoroKeyStore.FilePath);
|
||
|
||
if (EtoroKeyStore.IsEncrypted)
|
||
{
|
||
Assert.DoesNotContain("user-key-value", onDisk, StringComparison.Ordinal);
|
||
}
|
||
else
|
||
{
|
||
Assert.Contains("user-key-value", onDisk, StringComparison.Ordinal);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void ACorruptFileIsTreatedAsAbsentRatherThanThrowing()
|
||
{
|
||
Directory.CreateDirectory(_home);
|
||
File.WriteAllBytes(EtoroKeyStore.FilePath, [0x00, 0x01, 0x02, 0x03, 0x04]);
|
||
|
||
Assert.Null(EtoroKeyStore.Load(demo: true));
|
||
|
||
EtoroKeyStore.Save(demo: true, new EtoroKeys("new-api", "new-user", DateTime.UtcNow));
|
||
Assert.Equal("new-api", EtoroKeyStore.Load(demo: true)!.ApiKey);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolvePrefersTheEnvironmentThenTheStore()
|
||
{
|
||
BotConfig config = new();
|
||
Assert.False(EtoroKeyStore.Resolve(config, out string origin));
|
||
Assert.Contains("nessuna", origin, StringComparison.OrdinalIgnoreCase);
|
||
|
||
EtoroKeyStore.Save(demo: true, new EtoroKeys("api-stored", "user-stored", DateTime.UtcNow));
|
||
Assert.True(EtoroKeyStore.Resolve(config, out origin));
|
||
Assert.Equal("api-stored", config.Etoro.ApiKey);
|
||
Assert.Contains("salvate", origin, StringComparison.OrdinalIgnoreCase);
|
||
|
||
BotConfig fromEnv = new();
|
||
fromEnv.Etoro.ApiKey = "api-env";
|
||
fromEnv.Etoro.UserKey = "user-env";
|
||
Assert.True(EtoroKeyStore.Resolve(fromEnv, out origin));
|
||
Assert.Equal("api-env", fromEnv.Etoro.ApiKey);
|
||
Assert.Contains("ambiente", origin, StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("PKABCDEFGH1234", "PKABCD********")]
|
||
[InlineData("PKAB", "****")]
|
||
[InlineData("ab", "**")]
|
||
[InlineData("", "(vuota)")]
|
||
[InlineData(null, "(vuota)")]
|
||
public void MaskKeepsOnlyThePrefix(string? input, string expected) =>
|
||
Assert.Equal(expected, EtoroKeyStore.Mask(input));
|
||
|
||
[Theory]
|
||
[InlineData(" PKKEY123 ", "PKKEY123")]
|
||
[InlineData("PKKEY123", "PKKEY123")]
|
||
[InlineData("PKKEY123", "PKKEY123")]
|
||
[InlineData("P\0K\0K\0E\0Y\0", "PKKEY")]
|
||
[InlineData("PKKEY123\r\n", "PKKEY123")]
|
||
public void CleanStripsInvisibleCharactersFromPastedKeys(string raw, string expected) =>
|
||
Assert.Equal(expected, EtoroKeyStore.Clean(raw));
|
||
|
||
[Theory]
|
||
[InlineData(null)]
|
||
[InlineData("")]
|
||
[InlineData(" ")]
|
||
[InlineData("\r\n")]
|
||
public void CleanReturnsNullWhenNothingUsableRemains(string? raw) =>
|
||
Assert.Null(EtoroKeyStore.Clean(raw));
|
||
|
||
[Fact]
|
||
public void MaskNeverEchoesAWholeSecret()
|
||
{
|
||
const string secret = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||
string masked = EtoroKeyStore.Mask(secret);
|
||
|
||
Assert.DoesNotContain(secret, masked, StringComparison.Ordinal);
|
||
Assert.DoesNotContain(secret[4..], masked, StringComparison.Ordinal);
|
||
Assert.StartsWith("abcd", masked, StringComparison.Ordinal);
|
||
}
|
||
}
|