Un ordine dall'esito ignoto non viene più abbandonato: entra in data/state/pending_orders.json prima della chiamata HTTP, l'esito si legge per orderId (il server non registra il referenceId degli ordini v2) e in mancanza si ricostruisce dalla posizione comparsa sul conto. Una gamba senza esito porta il basket in PendingA/PendingB invece di rifiutarlo; alla risoluzione parte la gamba B, ridimensionata sulle unità eseguite, oppure la gamba A viene richiusa. Ogni posizione del conto è classificata basket / orfana-bot / esterna: le orfane del bot vengono adottate e chiuse, le esterne contate e mai toccate. Il picco di equity ignora i movimenti di cassa. Bonifica da headless, orders.jsonl, contatori in dashboard, bandit che propone e non applica. ADR-0009, test (m)-(q). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
3.9 KiB
C#
103 lines
3.9 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
|
|
namespace Encelado.Core.Baskets;
|
|
|
|
/// <summary>
|
|
/// An entry the decider approved whose execution is not finished: leg A is on its way
|
|
/// (state <c>PendingA</c>) or filled while leg B is on its way (<c>PendingB</c>). Holds
|
|
/// everything needed to finish the basket later — or to undo leg A — without the
|
|
/// original decision object, which does not survive a restart.
|
|
/// </summary>
|
|
public sealed class PendingEntry
|
|
{
|
|
public required string BasketId { get; init; }
|
|
|
|
public required bool BuyCross { get; init; }
|
|
|
|
public required double EntryZ { get; init; }
|
|
|
|
public required double UnitsA { get; init; }
|
|
|
|
public required double UnitsB { get; init; }
|
|
|
|
public required double TpPips { get; init; }
|
|
|
|
public required double MaxLossUsd { get; init; }
|
|
|
|
public double EntryCostPips { get; init; } = double.NaN;
|
|
|
|
public required double EquityAtEntry { get; init; }
|
|
|
|
public string Motivazione { get; init; } = string.Empty;
|
|
|
|
public required DateTime DecidedUtc { get; init; }
|
|
|
|
public string ClientRefA { get; set; } = string.Empty;
|
|
|
|
public string ClientRefB { get; set; } = string.Empty;
|
|
|
|
/// <summary>The quotes seen at decision time, for the slippage of each leg.</summary>
|
|
public double QuoteA { get; init; }
|
|
|
|
public double QuoteB { get; init; }
|
|
|
|
/// <summary>Leg A as filled, once it is.</summary>
|
|
public BasketLeg? LegA { get; set; }
|
|
|
|
public void Write(Utf8JsonWriter w)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(w);
|
|
w.WriteStartObject("pending");
|
|
w.WriteString("basketId", BasketId);
|
|
w.WriteBoolean("buyCross", BuyCross);
|
|
w.WriteNumber("entryZ", EntryZ);
|
|
w.WriteNumber("unitsA", UnitsA);
|
|
w.WriteNumber("unitsB", UnitsB);
|
|
w.WriteNumber("tpPips", TpPips);
|
|
w.WriteNumber("maxLossUsd", MaxLossUsd);
|
|
w.WriteNumber("entryCostPips", double.IsFinite(EntryCostPips) ? EntryCostPips : 0);
|
|
w.WriteNumber("equityAtEntry", EquityAtEntry);
|
|
w.WriteString("motivazione", Motivazione);
|
|
w.WriteString("decidedUtc", DecidedUtc.ToString("O", CultureInfo.InvariantCulture));
|
|
w.WriteString("clientRefA", ClientRefA);
|
|
w.WriteString("clientRefB", ClientRefB);
|
|
w.WriteNumber("quoteA", QuoteA);
|
|
w.WriteNumber("quoteB", QuoteB);
|
|
if (LegA is { } leg)
|
|
{
|
|
BasketLeg.Write(w, "legA", leg);
|
|
}
|
|
|
|
w.WriteEndObject();
|
|
}
|
|
|
|
public static PendingEntry Read(JsonElement e)
|
|
{
|
|
PendingEntry p = new()
|
|
{
|
|
BasketId = e.GetProperty("basketId").GetString() ?? string.Empty,
|
|
BuyCross = e.GetProperty("buyCross").GetBoolean(),
|
|
EntryZ = e.GetProperty("entryZ").GetDouble(),
|
|
UnitsA = e.GetProperty("unitsA").GetDouble(),
|
|
UnitsB = e.GetProperty("unitsB").GetDouble(),
|
|
TpPips = e.GetProperty("tpPips").GetDouble(),
|
|
MaxLossUsd = e.GetProperty("maxLossUsd").GetDouble(),
|
|
EntryCostPips = e.GetProperty("entryCostPips").GetDouble(),
|
|
EquityAtEntry = e.GetProperty("equityAtEntry").GetDouble(),
|
|
Motivazione = e.GetProperty("motivazione").GetString() ?? string.Empty,
|
|
DecidedUtc = DateTime.Parse(e.GetProperty("decidedUtc").GetString()!, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal),
|
|
QuoteA = e.TryGetProperty("quoteA", out JsonElement qa) ? qa.GetDouble() : 0,
|
|
QuoteB = e.TryGetProperty("quoteB", out JsonElement qb) ? qb.GetDouble() : 0,
|
|
};
|
|
p.ClientRefA = e.TryGetProperty("clientRefA", out JsonElement ca) ? ca.GetString() ?? string.Empty : string.Empty;
|
|
p.ClientRefB = e.TryGetProperty("clientRefB", out JsonElement cb) ? cb.GetString() ?? string.Empty : string.Empty;
|
|
if (e.TryGetProperty("legA", out JsonElement la))
|
|
{
|
|
p.LegA = BasketLeg.Read(la);
|
|
}
|
|
|
|
return p;
|
|
}
|
|
}
|