using System.Globalization;
using IBKRTrader.Core.Capital;
using IBKRTrader.Core.Settings;
using IBKRTrader.Core.Trading;
using Microsoft.Extensions.DependencyInjection;
namespace IBKRTrader.Daemon;
///
/// Der Nachweis für #293: eine echte Order gegen das Paper-Konto, durch die vollständige
/// Kette – Buch, Risikoprüfung, Kapitalbindung, Order, Fill, Buchung, Abschluss der Bindung.
///
/// --ibkr-check daneben liest nur. Genau das ist der Grund, warum es diesen Schalter
/// zusätzlich gibt: PlaceOrderAsync ist der einzige Teil des Adapters, der nie unter realen
/// Bedingungen lief, und alles Weitere – Fill-Buchung, Kapitalmodell, OptionsWheel – hängt an
/// diesem Nachweis.
///
/// Die Sicherungen, in dieser Reihenfolge:
///
/// - Ohne --confirm ist es ein Trockenlauf: alles bis unmittelbar vor die Order.
/// - Nur im Paper-Modus, und nur auf einem Paper-Port. Beides wird geprüft, weil eines allein
/// nichts aussagt – ein Paper-Modus auf einem Live-Port bewegt echtes Geld.
/// - Die Menge ist klein und wird ausgeschrieben, bevor irgendetwas passiert.
///
///
internal static class VerifyOrder
{
/// Nominalwert der Prüforder. Klein genug, dass ein Fehlschlag nichts kostet.
private const decimal Notional = 500m;
public static async Task RunAsync(
IServiceProvider services, string symbol, string currency, bool confirm)
{
var inv = CultureInfo.InvariantCulture;
var settings = services.GetRequiredService().Settings;
var trading = settings.Trading;
var ibkr = settings.IBKR;
var instrument = InstrumentRef.ForStock(symbol, currency);
Console.WriteLine($"Order-Nachweis (#293) für {instrument}: {ibkr.Host}:{ibkr.Port}, " +
$"Modus {trading.Mode}, Client-ID {ibkr.ClientId}.");
// ── Sicherungen ──────────────────────────────────────────────────────────
if (trading.ParsedMode != TradingMode.Paper)
{
Console.Error.WriteLine("ABBRUCH: Trading.Mode ist nicht Paper. Dieser Schalter platziert " +
"eine echte Order und ist ausschließlich für das Papierkonto gedacht.");
return 1;
}
if (!BrokerPorts.IsPaper(ibkr.Port))
{
Console.Error.WriteLine($"ABBRUCH: Port {ibkr.Port} ist kein Paper-Port " +
$"(erwartet {BrokerPorts.GatewayPaper} oder {BrokerPorts.TwsPaper}). " +
$"Der Modus allein sagt nichts – entscheidend ist, womit tatsächlich verbunden wird.");
return 1;
}
if (!ibkr.UseTwsApi)
{
Console.Error.WriteLine("ABBRUCH: IBKR.UseTwsApi ist false – registriert ist der NullBroker, " +
"der nie eine Order platziert. Es gäbe nichts zu beweisen.");
return 1;
}
if (!trading.TradingEnabled)
{
Console.Error.WriteLine("ABBRUCH: Trading.TradingEnabled ist false. Der Hauptschalter wird von " +
"diesem Schalter bewusst NICHT übergangen – sonst wäre er keiner.");
return 1;
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(180));
var broker = services.GetRequiredService();
var capital = services.GetRequiredService();
var execution = services.GetRequiredService();
// ── Lage vor der Order ───────────────────────────────────────────────────
var account = await broker.GetAccountStateAsync(cts.Token).ConfigureAwait(false);
if (account.NetLiquidation <= 0)
{
Console.Error.WriteLine("ABBRUCH: keine Kontodaten – siehe --ibkr-check für die Ursachen.");
return 1;
}
var quote = await broker.GetQuoteAsync(instrument, cts.Token).ConfigureAwait(false);
if (quote is null || quote.Last <= 0)
{
Console.Error.WriteLine($"ABBRUCH: kein Kurs für {instrument}. Entweder ist der Kontrakt " +
$"nicht auflösbar (Symbol/Währung prüfen – die Meldung des Adapters " +
$"steht oben), oder die Börse ist zu und es liegt kein Marktdaten-Abo vor.");
return 1;
}
var quantity = (int)Math.Floor(Notional / quote.Last);
if (quantity < 1)
{
Console.Error.WriteLine($"ABBRUCH: bei Kurs {quote.Last.ToString("N2", inv)} ergäbe " +
$"{Notional.ToString("N0", inv)} keine ganze Aktie. " +
$"Ein günstigeres Symbol wählen.");
return 1;
}
var book = capital.ResolveBook(BookId.Diagnostics.Value);
if (book is null)
{
Console.Error.WriteLine($"ABBRUCH: Das Diagnose-Buch {BookId.Diagnostics} ist nicht registriert.");
return 1;
}
var headroom = await capital.GetHeadroomAsync(book.Value, account, cts.Token).ConfigureAwait(false);
// Der haeufigste Grund fuer eine Ablehnung durch Schicht C ist nicht "kein Geld", sondern
// "Geld in der falschen Waehrung". Deshalb steht die Aufschluesselung hier, bevor
// irgendetwas passiert.
var cashHere = account.CashIn(instrument.Currency);
Console.WriteLine($"Bargeld: {instrument.Currency} " +
(cashHere is { } c ? c.ToString("N2", inv) : "keine Auskunft"));
Console.WriteLine($"Konto: NetLiquidation {account.NetLiquidation.ToString("N2", inv)}, " +
$"Bargeld {account.TotalCashValue.ToString("N2", inv)}");
Console.WriteLine($"Kurs: {quote}");
Console.WriteLine($"Buch: {book} – Cap {headroom.Cap.ToString("N0", inv)}, " +
$"gebunden {headroom.Bound.ToString("N0", inv)}, " +
$"frei {headroom.Available.ToString("N0", inv)}");
Console.WriteLine($"Vorhaben: KAUF {quantity} x {instrument} zum Marktpreis " +
$"(~{(quantity * quote.Last).ToString("N2", inv)} {instrument.Currency}).");
if (!confirm)
{
Console.WriteLine();
Console.WriteLine("=== TROCKENLAUF – es wurde nichts platziert ===");
Console.WriteLine("Zum tatsächlichen Absenden denselben Aufruf mit --confirm wiederholen.");
return 0;
}
// ── Die Order ────────────────────────────────────────────────────────────
Console.WriteLine();
Console.WriteLine("Sende Order …");
var signal = new TradeSignal
{
Symbol = symbol,
Side = TradeSide.Buy,
SourceModule = BookId.Diagnostics.Value,
Reason = "Verifikation der Orderstrecke (#293)",
SuggestedNotional = Notional,
Currency = instrument.Currency
};
var result = await execution.ExecuteAsync(signal, cts.Token).ConfigureAwait(false);
Console.WriteLine($"Ergebnis: {result.Action} – {result.Reason}");
if (result.Order is { } order)
Console.WriteLine($"Order: {order.OrderId}, gefüllt {order.FilledQuantity} " +
$"@ {order.AvgFillPrice.ToString("N2", inv)}");
if (result.IsPending)
{
// Kein Fehlschlag. Bei geschlossener Börse ist das der richtige Ausgang: Die Order liegt
// beim Broker, das Kapital bleibt gebunden, und die Bindung trägt die Order-ID.
Console.WriteLine();
Console.WriteLine("=== Order liegt beim Broker – Ausgang offen ===");
Console.WriteLine("Das ist bei geschlossener Börse der erwartete Ausgang und ausdrücklich KEIN");
Console.WriteLine("Fehlschlag: Die Kapitalbindung bleibt bestehen, bis die Order ausgeführt oder");
Console.WriteLine("zurückgenommen ist.");
Console.WriteLine();
Console.WriteLine("Gegenprobe: --open-orders");
Console.WriteLine("Zurücknehmen: --cancel-order --confirm");
return 0;
}
if (!result.Executed)
{
Console.Error.WriteLine("=== Order-Nachweis FEHLGESCHLAGEN ===");
Console.Error.WriteLine("Die Order ist beweisbar nicht zustande gekommen – der Grund steht oben.");
return 1;
}
// ── Lage nach der Order ──────────────────────────────────────────────────
var afterAccount = await broker.GetAccountStateAsync(cts.Token).ConfigureAwait(false);
var afterHeadroom = await capital.GetHeadroomAsync(book.Value, afterAccount, cts.Token).ConfigureAwait(false);
Console.WriteLine($"Buch nach: gebunden {afterHeadroom.Bound.ToString("N0", inv)}, " +
$"frei {afterHeadroom.Available.ToString("N0", inv)}");
Console.WriteLine();
Console.WriteLine("=== Order-Nachweis OK ===");
Console.WriteLine($"Zu prüfen bleibt von Hand: core_trade_history, core_position (Buch {book}, " +
$"Instrument {instrument}), core_reservation (Zustand Filled) und " +
$"core_order_events – Signal-ID {signal.SignalId}.");
return 0;
}
}