From 8fd99ee6fa4f817d7220607a71a93f2e6fddfcf0 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 6 Sep 2026 12:41:06 +0200 Subject: [PATCH] Positionsverwaltung: Herkunft sichtbar, Zuordnung aenderbar, Sperren einstellbar Erster Teil von #485. Der PositionManager im Core beantwortet drei Fragen, die sich beim Debuggen sofort stellen: Wem gehoert eine Position, was sagt der Broker dazu, und was ist gesperrt. Drei Zugaenge auf denselben Dienst - das Fenster der Shell, --positions und --reassign im kopflosen Betrieb, GET /api/positions. Der Testbetrieb laeuft als systemd-Dienst; ein Werkzeug, das nur in der Oberflaeche existiert, fehlt genau dann, wenn man es braucht. Entscheidungen, die im Code begruendet stehen: - BrokerQuantity ist nullable und heisst "nicht bekannt", nicht "null Stueck". Ohne Verbindung sieht ein leergeraeumtes Konto genauso aus wie eine Stoerung; aus dem einen darf nicht auf das andere geschlossen werden. - Umbuchen aendert die Zuordnung, nie die Menge. Eine Umbuchung, die eine Stueckzahl bewegt, waere ein Handel ohne Order und stuende in keiner Historie. - Gegen eine entgegengesetzte Position wird abgelehnt: Das waere eine Glattstellung und gehoert ueber eine Order gemacht, mit Preis und Historie. - Das Quellbuch ist Pflicht - Schutz gegen die falsche Zeile -, ebenso eine Begruendung, sonst ist der Eingriff spaeter nicht mehr erklaerbar. - Das Cap des Zielbuchs sperrt NICHT. Die Position existiert bereits; sie zu benennen erzeugt kein neues Risiko. Wuerde die Grenze das verhindern, bliebe jede zu grosse Position fuer immer in UNASSIGNED. - Der REST-Endpunkt ist lesend. Der Listener kennt keine Anmeldung; ein schreibender Zugriff waere ein Eingriff in die Buecher, den niemand belegen kann. Beim ersten Testlauf gefunden: (Module, ContractKey) ist der Primaerschluessel von core_position, das Buch laesst sich in der Zeile nicht ueberschreiben. Umgebucht wird deshalb durch Loeschen und Neuanlegen. 471 Tests gruen. Co-Authored-By: Claude Opus 5 --- src/IBKRTrader.App/Shell/CoreViews.cs | 7 + src/IBKRTrader.App/ViewModels/Rows.cs | 13 + .../Views/PositionsWindow.axaml | 78 +++++ .../Views/PositionsWindow.axaml.cs | 217 ++++++++++++ .../Positions/PositionManager.cs | 309 ++++++++++++++++++ .../Workers/BuiltIn/WebApiService.cs | 63 +++- src/IBKRTrader.Daemon/PositionTool.cs | 164 ++++++++++ src/IBKRTrader.Daemon/Program.cs | 17 + src/IBKRTrader.Hosting/AppHostBuilder.cs | 1 + .../Positions/PositionManagerTests.cs | 251 ++++++++++++++ 10 files changed, 1116 insertions(+), 4 deletions(-) create mode 100644 src/IBKRTrader.App/Views/PositionsWindow.axaml create mode 100644 src/IBKRTrader.App/Views/PositionsWindow.axaml.cs create mode 100644 src/IBKRTrader.Core/Positions/PositionManager.cs create mode 100644 src/IBKRTrader.Daemon/PositionTool.cs create mode 100644 tests/IBKRTrader.Tests/Positions/PositionManagerTests.cs diff --git a/src/IBKRTrader.App/Shell/CoreViews.cs b/src/IBKRTrader.App/Shell/CoreViews.cs index 049ecbf..ef9ad9e 100644 --- a/src/IBKRTrader.App/Shell/CoreViews.cs +++ b/src/IBKRTrader.App/Shell/CoreViews.cs @@ -32,6 +32,13 @@ public static class CoreViews CreateView = () => new WorkersWindow(host, sp.GetRequiredService()) }); + host.RegisterView(new ModuleView + { + Id = "core.positions", Title = "Positionen", Group = "Core", Order = 15, + CreateView = () => new PositionsWindow( + host, sp.GetRequiredService()) + }); + host.RegisterView(new ModuleView { Id = "core.logs", Title = "Logs", Group = "Core", Order = 20, diff --git a/src/IBKRTrader.App/ViewModels/Rows.cs b/src/IBKRTrader.App/ViewModels/Rows.cs index 5035c9f..c141da2 100644 --- a/src/IBKRTrader.App/ViewModels/Rows.cs +++ b/src/IBKRTrader.App/ViewModels/Rows.cs @@ -22,6 +22,19 @@ public sealed record LogRow(string Text, IBrush Color); /// Eine offene Position im Modul-Fenster. public sealed record PositionRow(string Symbol, int Quantity, decimal AvgPrice, decimal Notional); +/// +/// Eine Zeile der Positionsverwaltung (#485). Zahlen kommen als fertiger Text: Die Formatierung +/// gehoert an eine Stelle, und „Broker“ muss zwischen „null Stück“ und „keine Auskunft“ +/// unterscheiden koennen - ein Zahlentyp kann das nicht. +/// +public sealed record ManagedPositionRow( + string Book, string Origin, string Instrument, string Quantity, string AvgPrice, + string Bound, string Broker, string Restrictions); + +/// Bestand, den der Broker fuehrt und den kein Buch beansprucht. +public sealed record UnknownPositionRow( + string Symbol, string SecType, string Currency, string Quantity, string AvgCost, string Note); + /// Eine Zeile der Supervisor-Berichtsliste. Tokens als fertiger Text (Prompt / Completion). public sealed record SupervisorReportRow( DateTime CreatedAt, string Profile, string Model, string Question, int ToolCallCount, string Tokens); diff --git a/src/IBKRTrader.App/Views/PositionsWindow.axaml b/src/IBKRTrader.App/Views/PositionsWindow.axaml new file mode 100644 index 0000000..2282859 --- /dev/null +++ b/src/IBKRTrader.App/Views/PositionsWindow.axaml @@ -0,0 +1,78 @@ + + + + + + + +