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; } }