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:
@@ -1,10 +1,13 @@
|
||||
using IBKRTrader.Core.Configuration;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace IBKRTrader.Daemon;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnose: öffnet die Datenbank aus <c>settings.json</c> und gibt die Serverversion aus.
|
||||
/// Gebraucht für das EF-<c>ServerVersion</c>-Pinning; läuft ohne Host und ohne Oberfläche.
|
||||
/// Diagnose: öffnet die Datenbank, mit der die Anwendung wirklich arbeitet, und gibt die
|
||||
/// Serverversion aus. Gebraucht für das EF-<c>ServerVersion</c>-Pinning; läuft ohne Host und ohne
|
||||
/// Oberfläche – die Verbindung wird deshalb hier auf demselben Weg aufgelöst wie im Host.
|
||||
/// </summary>
|
||||
internal static class DbVersion
|
||||
{
|
||||
@@ -13,9 +16,20 @@ internal static class DbVersion
|
||||
var settings = new SettingsService();
|
||||
settings.Load();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddJsonFile("appsettings.Local.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var database = DatabaseEndpoint.Resolve(configuration["Database:MySqlConnectionString"],
|
||||
settings.Settings.Database);
|
||||
Console.WriteLine($"Datenbank: {database} (Quelle: {database.Source})");
|
||||
|
||||
try
|
||||
{
|
||||
using var c = new MySqlConnector.MySqlConnection(settings.Settings.Database.BuildConnectionString());
|
||||
using var c = new MySqlConnector.MySqlConnection(database.ConnectionString);
|
||||
c.Open();
|
||||
Console.WriteLine($"ServerVersion: {c.ServerVersion}");
|
||||
return 0;
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
CultureInfo.GetCultureInfo("de-DE") die invariante Kultur, aus "1.234,56" würde
|
||||
wieder "1,234.56" – in einem Dokument, das als prüfbare Aufstellung gilt. -->
|
||||
<InvariantGlobalization>false</InvariantGlobalization>
|
||||
<!-- Symbol der Windows-Programmdatei. Der Dienst zeigt kein Fenster; sichtbar wird es im
|
||||
Explorer, im Task-Manager und in der Dienstliste – also überall dort, wo mehrere unserer
|
||||
Anwendungen bisher dasselbe Platzhaltersymbol trugen. Unter Linux ohne Wirkung und ohne
|
||||
Kosten: ApplicationIcon landet in der PE-Ressource, der ELF-Launcher bleibt unberührt. -->
|
||||
<ApplicationIcon>..\..\icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Deploymentcenter.Client;
|
||||
using IBKRTrader.Core.Configuration;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Core.Trading;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
using IBKRTrader.Hosting;
|
||||
using IBKRTrader.Hosting.Deploymentcenter;
|
||||
@@ -34,23 +36,51 @@ internal static class Program
|
||||
if (setKeyIndex >= 0 && setKeyIndex + 1 < args.Length)
|
||||
return await SetLicenseKeyAsync(args[setKeyIndex + 1]).ConfigureAwait(false);
|
||||
|
||||
// Trockenlauf: Host bauen, Startprüfungen fahren, Dienste NICHT starten. Für Deployment
|
||||
// und CI – prüft Konfiguration, DB-Verbindungszeichenfolge, Master-Key und Ablageorte,
|
||||
// ohne eine einzige Verbindung zur Börse aufzubauen.
|
||||
// Trockenlauf: Host bauen, Startprüfungen fahren, Datenbank einmal wirklich öffnen,
|
||||
// Dienste NICHT starten. Für Deployment und CI – ohne eine einzige Verbindung zur Börse.
|
||||
var checkOnly = HasFlag(args, "--check");
|
||||
|
||||
// Verbindungsprüfung gegen TWS/Gateway: liest Konto, Positionen, einen Kurs und die
|
||||
// letzten Ausführungen. Platziert nichts. Optionales Argument: das Prüfsymbol.
|
||||
var ibkrCheck = HasFlag(args, "--ibkr-check");
|
||||
|
||||
// Der Nachweis fuer #293: eine echte Order gegen das Paper-Konto durch die vollstaendige
|
||||
// Kette. Ohne --confirm ein Trockenlauf. Siehe VerifyOrder fuer die Sicherungen.
|
||||
var verifyOrder = ArgAfter(args, "--verify-order");
|
||||
|
||||
var modules = AppHostBuilder.CreateModules();
|
||||
using var host = AppHostBuilder.Build(modules);
|
||||
|
||||
var logger = host.Services.GetRequiredService<LoggingService>();
|
||||
|
||||
// Beim Trockenlauf gehen Warnungen der Startprüfungen zusätzlich auf die Konsole. Standen
|
||||
// sie nur in der Logdatei, sah ein CI-Schritt bloß "OK" und Exitcode 0 – auch dann, wenn
|
||||
// dort "kein Master-Key" und "DB ohne SslMode" untereinander lagen.
|
||||
var warnings = 0;
|
||||
if (checkOnly || ibkrCheck || verifyOrder is not null)
|
||||
{
|
||||
logger.EntryWritten += e =>
|
||||
{
|
||||
// Bei der IBKR-Prüfung zählt auch, was der Adapter an Info meldet: Zeitkontext,
|
||||
// ServerVersion und Kontoliste stehen dort und sind der halbe Befund.
|
||||
var floor = ibkrCheck || verifyOrder is not null ? AppLogLevel.Info : AppLogLevel.Warn;
|
||||
if (e.Level < floor) return;
|
||||
if (e.Level >= AppLogLevel.Warn) Interlocked.Increment(ref warnings);
|
||||
Console.WriteLine($" [{e.Level}] {e.Module}: {e.Message}");
|
||||
};
|
||||
}
|
||||
|
||||
AppHostBuilder.RunStartupChecks(host.Services);
|
||||
|
||||
if (checkOnly)
|
||||
{
|
||||
logger.Info("Core", "Prüflauf (--check) erfolgreich: Host baubar, Startprüfungen bestanden.");
|
||||
Console.WriteLine("=== Prüflauf OK ===");
|
||||
return 0;
|
||||
}
|
||||
return await RunCheckAsync(host.Services, logger, warnings).ConfigureAwait(false);
|
||||
|
||||
if (ibkrCheck)
|
||||
return await RunIbkrCheckAsync(host.Services, SymbolArg(args) ?? "AAPL").ConfigureAwait(false);
|
||||
|
||||
if (verifyOrder is not null)
|
||||
return await VerifyOrder.RunAsync(host.Services, verifyOrder, HasFlag(args, "--confirm"))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
logger.Info("Core", "=== IBKRTrader (kopflos) startet ===");
|
||||
logger.Info("Core", $"Version: {IBKRTrader.Hosting.BuildInfo.Summary} | .NET {Environment.Version} | {RuntimeDescription()}");
|
||||
@@ -74,6 +104,110 @@ internal static class Program
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Der Trockenlauf hinter <c>--check</c>. Öffnet die Datenbank tatsächlich: eine Auslieferung,
|
||||
/// die den Schalter besteht, kann sonst vollständig ohne Datenbank dastehen – und genau dafür
|
||||
/// ist er gedacht.
|
||||
/// </summary>
|
||||
private static async Task<int> RunCheckAsync(IServiceProvider services, LoggingService logger, int warnings)
|
||||
{
|
||||
var database = services.GetRequiredService<DatabaseEndpoint>();
|
||||
|
||||
var error = await AppHostBuilder.TryConnectToDatabaseAsync(services, TimeSpan.FromSeconds(10))
|
||||
.ConfigureAwait(false);
|
||||
if (error != null)
|
||||
{
|
||||
logger.Error("Core", $"Prüflauf (--check) fehlgeschlagen: Datenbank {database} nicht erreichbar – {error}");
|
||||
Console.Error.WriteLine($"=== Prüflauf FEHLGESCHLAGEN ===");
|
||||
Console.Error.WriteLine($"Datenbank {database} (Quelle: {database.Source}) nicht erreichbar:");
|
||||
Console.Error.WriteLine($" {error}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
logger.Info("Core", $"Prüflauf (--check) erfolgreich: Startprüfungen bestanden, Datenbank {database} erreichbar.");
|
||||
Console.WriteLine($"Datenbank {database} erreichbar (Quelle: {database.Source}).");
|
||||
Console.WriteLine(warnings == 0
|
||||
? "=== Prüflauf OK ==="
|
||||
: $"=== Prüflauf OK – {warnings} Warnung(en), siehe oben ===");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verbindungsprüfung gegen TWS bzw. IB Gateway: Konto, Positionen, ein Kurs, die letzten
|
||||
/// Ausführungen. <b>Platziert keine Order</b> – der Adapter wird nur lesend benutzt.
|
||||
///
|
||||
/// <para>Der Adapter übersetzt jeden Fehler bewusst in ein leeres Ergebnis (Konto 0), weil ein
|
||||
/// nicht erreichbarer Broker den Betrieb nicht abbrechen darf. Für eine Prüfung ist genau das
|
||||
/// unbrauchbar – hier wird ein Kontowert von 0 deshalb als Fehlschlag gewertet und mit den
|
||||
/// beiden häufigsten Ursachen kommentiert.</para>
|
||||
/// </summary>
|
||||
private static async Task<int> RunIbkrCheckAsync(IServiceProvider services, string symbol)
|
||||
{
|
||||
var settings = services.GetRequiredService<SettingsService>().Settings;
|
||||
var ibkr = settings.IBKR;
|
||||
|
||||
Console.WriteLine($"IBKR-Prüflauf: {ibkr.Host}:{ibkr.Port}, Client-ID {ibkr.ClientId}, " +
|
||||
$"Modus {settings.Trading.Mode}, Marktdaten-Typ {ibkr.MarketDataType}.");
|
||||
|
||||
if (!ibkr.UseTwsApi)
|
||||
{
|
||||
Console.Error.WriteLine("IBKR.UseTwsApi ist false – registriert ist der NullBroker, der nie verbindet.");
|
||||
Console.Error.WriteLine("Zum Prüfen in settings.json IBKR.UseTwsApi auf true setzen.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(90));
|
||||
var broker = services.GetRequiredService<IBrokerClient>();
|
||||
var reader = services.GetRequiredService<IBrokerPortfolioReader>();
|
||||
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||||
|
||||
var account = await broker.GetAccountStateAsync(cts.Token).ConfigureAwait(false);
|
||||
if (account.NetLiquidation <= 0)
|
||||
{
|
||||
Console.Error.WriteLine("=== IBKR-Prüflauf FEHLGESCHLAGEN: keine Kontodaten ===");
|
||||
Console.Error.WriteLine("Die beiden häufigsten Ursachen (siehe docs/TWS-Setup-Checkliste.md):");
|
||||
Console.Error.WriteLine(" 1. Vertrauenswürdige IP 127.0.0.1 fehlt – TWS wartet auf die Bestätigung");
|
||||
Console.Error.WriteLine(" eines Verbindungs-Popups, auch wenn keines mehr sichtbar ist.");
|
||||
Console.Error.WriteLine(" 2. Client-ID doppelt vergeben – eine zweite Verbindung verdrängt die erste.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Konto: NetLiquidation {account.NetLiquidation.ToString("N2", inv)}, " +
|
||||
$"verfügbar {account.AvailableFunds.ToString("N2", inv)}");
|
||||
|
||||
var positions = await reader.GetPositionsAsync(cts.Token).ConfigureAwait(false);
|
||||
Console.WriteLine($"Positionen: {positions.Count}");
|
||||
foreach (var p in positions.Take(10))
|
||||
Console.WriteLine($" {p}");
|
||||
|
||||
var quote = await broker.GetQuoteAsync(symbol, cts.Token).ConfigureAwait(false);
|
||||
Console.WriteLine(quote is null
|
||||
? $"Kurs {symbol}: keiner geliefert (ohne Marktdaten-Abo bei geschlossener Börse normal)"
|
||||
: $"Kurs {symbol}: {quote}");
|
||||
|
||||
var executions = await reader.GetExecutionsAsync(DateTime.UtcNow.AddDays(-7), cts.Token).ConfigureAwait(false);
|
||||
Console.WriteLine($"Ausführungen der letzten 7 Tage: {executions.Count}");
|
||||
foreach (var e in executions.Take(10))
|
||||
Console.WriteLine($" {e}");
|
||||
|
||||
Console.WriteLine("=== IBKR-Prüflauf OK ===");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Der Wert hinter einem Schalter, falls einer mitgegeben wurde.</summary>
|
||||
private static string? ArgAfter(string[] args, string flag)
|
||||
{
|
||||
var i = Array.FindIndex(args, a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
|
||||
return i >= 0 && i + 1 < args.Length && !args[i + 1].StartsWith('-') ? args[i + 1] : null;
|
||||
}
|
||||
|
||||
/// <summary>Das Symbol hinter <c>--ibkr-check</c>, falls eines mitgegeben wurde.</summary>
|
||||
private static string? SymbolArg(string[] args)
|
||||
{
|
||||
var i = Array.FindIndex(args, a => string.Equals(a, "--ibkr-check", StringComparison.OrdinalIgnoreCase));
|
||||
return i >= 0 && i + 1 < args.Length && !args[i + 1].StartsWith('-') ? args[i + 1] : null;
|
||||
}
|
||||
|
||||
private static async Task StopModulesAsync(IReadOnlyList<IModule> modules, LoggingService logger)
|
||||
{
|
||||
foreach (var module in modules)
|
||||
|
||||
@@ -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