Bestandsabgleich gegen den Broker: Zuteilung, Verfall, Fremdbestand
Zuteilung und Verfall aendern Positionen ohne eine Order von uns - es gibt kein Ereignis, an das sich ein Abgleich haengen koennte. Nach einem Verfallstag fuehren wir Kontrakte, die es nicht mehr gibt; nach einer Zuteilung liegen Aktien im Konto, die in keinem Buch stehen. Beides verschiebt still die Deckungslage: Ein Covered Call, dessen Aktien in Wahrheit abgerufen wurden, ist ab dann ein nackter Call. PositionReconciler ist reine Rechnung und deutet die Differenz, statt nur Mengen anzugleichen. Verschwindet ein geschriebener Put und tauchen zugleich Aktien des Basiswerts auf, gehoeren diese Aktien dem Buch, das den Put geschrieben hat - landeten sie in UNASSIGNED, verloere ein Wheel-Modul genau den Bestand, aus dem es den naechsten Call schreiben muss. Der Einstand ist dabei der Strike, denn zu genau dem Preis wurde geliefert. Passt die Aktienbewegung nicht zur Groesse der Option, gilt sie NICHT als Zuteilung. Verfall wird nur angenommen, wenn der Verfallstag erreicht ist. Ein Kontrakt, der vorher verschwindet, ist eine Abweichung und wird als solche gemeldet - von Hand geschlossen ist kein Verfall. Die gefaehrlichste Stelle ist die leere Antwort: Die Leseschnittstelle liefert bei gestoerter Verbindung eine leere Liste, die von einem leeren Konto nicht zu unterscheiden ist. Solange wir selbst Positionen fuehren, gilt sie deshalb als Stoerung. Ohne diese Sperre setzte ein Verbindungsabbruch jede Position auf null. Gehandelt wird nie. Der Abgleich bucht ausschliesslich nach. BrokerPosition traegt jetzt das Instrument (IbkrMapping.ToInstrument); ein Kontrakt, den unser Modell nicht kennt - Future, Devise, Fonds -, liefert null und wird uebersprungen statt als Aktie gedeutet. Gemessen auf dem Testserver gegen das Paper-Konto: vier Altlasten erkannt (GOOG, MIGA:EUR, die SAP-ADR aus dem Fehllauf und zwei ungebuchte SAP:EUR), alle an UNASSIGNED gebucht, das Buch DIAG unangetastet. Gegenprobe danach: "Bestand stimmt ueberein (5 Positionen)". 402 Tests gruen. Erledigt den offenen Teil von #300, Kern von #306. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Deploymentcenter.Client;
|
||||
using IBKRTrader.Core.Configuration;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Reconciliation;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Core.Trading;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
@@ -48,6 +49,9 @@ internal static class Program
|
||||
// Kette. Ohne --confirm ein Trockenlauf. Siehe VerifyOrder fuer die Sicherungen.
|
||||
var verifyOrder = ArgAfter(args, "--verify-order");
|
||||
|
||||
// Bestandsabgleich von Hand. Mit --dry-run wird nur festgestellt, nicht gebucht.
|
||||
var reconcile = HasFlag(args, "--reconcile");
|
||||
|
||||
var modules = AppHostBuilder.CreateModules();
|
||||
using var host = AppHostBuilder.Build(modules);
|
||||
|
||||
@@ -57,13 +61,14 @@ internal static class Program
|
||||
// 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)
|
||||
if (checkOnly || ibkrCheck || verifyOrder is not null || reconcile)
|
||||
{
|
||||
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;
|
||||
var floor = ibkrCheck || verifyOrder is not null || reconcile
|
||||
? 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}");
|
||||
@@ -75,6 +80,9 @@ internal static class Program
|
||||
if (checkOnly)
|
||||
return await RunCheckAsync(host.Services, logger, warnings).ConfigureAwait(false);
|
||||
|
||||
if (reconcile)
|
||||
return await RunReconcileAsync(host.Services, !HasFlag(args, "--dry-run")).ConfigureAwait(false);
|
||||
|
||||
if (ibkrCheck)
|
||||
return await RunIbkrCheckAsync(host.Services, SymbolArg(args) ?? "AAPL").ConfigureAwait(false);
|
||||
|
||||
@@ -111,6 +119,29 @@ internal static class Program
|
||||
/// die den Schalter besteht, kann sonst vollständig ohne Datenbank dastehen – und genau dafür
|
||||
/// ist er gedacht.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Bestandsabgleich von Hand. Ohne <c>--dry-run</c> werden die Korrekturen gebucht;
|
||||
/// gehandelt wird in keinem Fall.
|
||||
/// </summary>
|
||||
private static async Task<int> RunReconcileAsync(IServiceProvider services, bool apply)
|
||||
{
|
||||
var service = services.GetRequiredService<ReconciliationService>();
|
||||
var result = await service.RunAsync(apply).ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine(apply
|
||||
? "Bestandsabgleich (Korrekturen werden gebucht)"
|
||||
: "Bestandsabgleich (Trockenlauf – es wird nichts gebucht)");
|
||||
Console.WriteLine($"Eigene Positionen: {result.OwnPositions}, beim Broker: {result.BrokerPositions}");
|
||||
|
||||
foreach (var action in result.Actions)
|
||||
Console.WriteLine($" [{action.Kind}] {action.Book}: {action.Explanation}");
|
||||
|
||||
Console.WriteLine(result.Summary);
|
||||
|
||||
// Ausgesetzt heißt: nicht geprüft. Das darf ein aufrufendes Skript nicht als Erfolg lesen.
|
||||
return result.Skipped ? 1 : 0;
|
||||
}
|
||||
|
||||
private static async Task<int> RunCheckAsync(IServiceProvider services, LoggingService logger, int warnings)
|
||||
{
|
||||
var database = services.GetRequiredService<DatabaseEndpoint>();
|
||||
|
||||
Reference in New Issue
Block a user