Options-Fundament und Kapitalmodell im Core; Modul OptionsWheel
Das OptionsWheel setzt auf Dingen auf, die es im Core nicht gab. Diese Sitzung baut sie. #298 Optionen als Instrument. InstrumentKind/OptionSpec und ein druckbarer Instrumentenschluessel ("AAPL|20260918|C|150"); TradeSignal und OrderRequest tragen ein optionales OptionSpec, ohne das sich der Aktienpfad unveraendert verhaelt. Position.Notional rechnet ab jetzt mit der Kontraktgroesse - vorher war jede Option um Faktor 100 zu klein bewertet und damit jedes Risikolimit wirkungslos, lautlos und in die gefaehrliche Richtung. Daneben neu BoundCapital: ein Short Put bindet Strike x Kontraktgroesse, nicht die vereinnahmte Praemie. #300 sell-to-open. Jeder Auftrag zerfaellt in einen schliessenden und einen eroeffnenden Teil; der schliessende wird von keinem Limit verhindert - auch nicht vom Kaufstopp, denn ein Rueckkauf baut eine Verpflichtung ab. Deckungspruefung im Core, nicht im Modul: Short Call nur mit freien Aktien, Short Put nur mit reservierbarem Bargeld. #302-#305 Kapital- und Buchmodell. Buecher mit reservierten Namen und Eigentumsregeln, Pool mit Caps und Headroom, Margin-Sperre in vier Schichten, core_reservation mit Lebenszyklus, Frist, Idempotenz ueber (SignalId, ContractKey) und Quarantaene beim Start. #309/#310 Modul OptionsWheel: ow_-Schema mit fuenf Tabellen, Zustandsautomat, StrikeSelector, RollDecider, PremiumMath - vollstaendig ohne Broker testbar. Der Handelsbetrieb bleibt gesperrt, solange #299 und #294 fehlen; das Modul sagt das ueber GetActivationBlocker, statt still nichts zu tun. #293 --verify-order am Daemon: eine Order durch die vollstaendige Kette (Buch, Risiko, Bindung, Order, Fill, Buchung, Abschluss). Ohne --confirm ein Trockenlauf; Abbruch ausserhalb von Paper-Modus UND Paper-Port. Drei Widersprueche in den Konzepten sind dabei entschieden worden: Schicht B heisst ab jetzt "kein UNGEDECKTER Short" (der Grund der Regel war der Kredit, nicht der Short), die Hausreserve wird einmal abgezogen statt zweimal, und der Positionsschluessel ist eine Spalte statt eines Sechser-Verbunds (MySQL verlangt NOT NULL fuer Schluesselspalten). Begruendungen stehen in docs/ROADMAP.md. Enthaelt ausserdem die noch nicht committete Arbeit aus der vorigen Sitzung: die Fehlerbehebungen aus Epic X (WorkerBase-Intervall, BackupWorker, --check gegen die Datenbank, CapitolTrades-429, SIGTERM) samt DatabaseEndpoint, MysqlDumpTool und der Roadmap-Umstellung. Sie lag im Arbeitsbaum und liess sich nicht abtrennen, weil beide Staende dieselben Dateien beruehren. 213 -> 369 Tests, Build ohne Warnung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
using System.Globalization;
|
||||
using IBKRTrader.Core.Capital;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Core.Trading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IBKRTrader.Daemon;
|
||||
|
||||
/// <summary>
|
||||
/// Der Nachweis für #293: eine echte Order gegen das Paper-Konto, durch die <b>vollständige</b>
|
||||
/// Kette – Buch, Risikoprüfung, Kapitalbindung, Order, Fill, Buchung, Abschluss der Bindung.
|
||||
///
|
||||
/// <para><c>--ibkr-check</c> daneben liest nur. Genau das ist der Grund, warum es diesen Schalter
|
||||
/// zusätzlich gibt: <c>PlaceOrderAsync</c> ist der einzige Teil des Adapters, der nie unter realen
|
||||
/// Bedingungen lief, und alles Weitere – Fill-Buchung, Kapitalmodell, OptionsWheel – hängt an
|
||||
/// diesem Nachweis.</para>
|
||||
///
|
||||
/// <para><b>Die Sicherungen, in dieser Reihenfolge:</b></para>
|
||||
/// <list type="number">
|
||||
/// <item>Ohne <c>--confirm</c> ist es ein Trockenlauf: alles bis unmittelbar vor die Order.</item>
|
||||
/// <item>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.</item>
|
||||
/// <item>Die Menge ist klein und wird ausgeschrieben, bevor irgendetwas passiert.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal static class VerifyOrder
|
||||
{
|
||||
/// <summary>Nominalwert der Prüforder. Klein genug, dass ein Fehlschlag nichts kostet.</summary>
|
||||
private const decimal Notional = 500m;
|
||||
|
||||
public static async Task<int> RunAsync(IServiceProvider services, string symbol, bool confirm)
|
||||
{
|
||||
var inv = CultureInfo.InvariantCulture;
|
||||
var settings = services.GetRequiredService<SettingsService>().Settings;
|
||||
var trading = settings.Trading;
|
||||
var ibkr = settings.IBKR;
|
||||
|
||||
Console.WriteLine($"Order-Nachweis (#293) für {symbol}: {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<IBrokerClient>();
|
||||
var capital = services.GetRequiredService<ICapitalService>();
|
||||
var execution = services.GetRequiredService<IExecutionService>();
|
||||
|
||||
// ── 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(symbol, cts.Token).ConfigureAwait(false);
|
||||
if (quote is null || quote.Last <= 0)
|
||||
{
|
||||
Console.Error.WriteLine($"ABBRUCH: kein Kurs für {symbol}. Bei geschlossener Börse und ohne " +
|
||||
$"Marktdaten-Abo ist das normal – dann später erneut versuchen.");
|
||||
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);
|
||||
|
||||
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 {symbol} zum Marktpreis " +
|
||||
$"(~{(quantity * quote.Last).ToString("N2", inv)} USD).");
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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.Executed)
|
||||
{
|
||||
Console.Error.WriteLine("=== Order-Nachweis FEHLGESCHLAGEN ===");
|
||||
Console.Error.WriteLine("Die Kette ist bis zur obigen Stelle gekommen. Bei einer Limit-Order, " +
|
||||
"die im Buch liegt, ist das der bekannte Fall aus #294: sie gilt uns " +
|
||||
"als Fehlschlag, ist bei IBKR aber weiterhin aktiv.");
|
||||
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}), " +
|
||||
$"core_reservation (Zustand Filled) und core_order_events – " +
|
||||
$"Signal-ID {signal.SignalId}.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user