Abweichung wird je Instrument bestimmt, Sperren auch kopflos setzbar

Der erste Lauf gegen die Testmaschine zeigte den Fehler sofort: 2 Stueck SAP in
DIAG, 2 in UNASSIGNED, der Broker fuehrt 4 - alles richtig, aber zeilenweise
verglichen meldete die Ansicht zwei Fehlbestaende. Der Broker kennt unsere
Buecher nicht; vergleichbar ist nur die Summe ueber alle. ManagedPosition traegt
jetzt OwnTotalQuantity, und Diverges misst daran. Zwei Tests halten beide
Richtungen fest: eine aufgeteilte Position ist keine Abweichung, eine echte
verschwindet nicht in der Summe.

Dazu --lock und --instrument-rule fuer den kopflosen Betrieb. Beide mit
Trockenlauf; die Stufe "Locked" schreibt vor dem Setzen aus, dass sie auch das
SCHLIESSEN durch Module verhindert - das gehoert dorthin, wo entschieden wird,
nicht in den Stoerungsfall.

Auf der Testmaschine durchgespielt: GOOG von UNASSIGNED nach MANUAL umgebucht
(Menge und Einstand unveraendert), dann auf CloseOnly gesetzt - beides steht in
der Uebersicht.

473 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-06 12:45:46 +02:00
co-authored by Claude Opus 5
parent 8fd99ee6fa
commit 834f035ff6
4 changed files with 211 additions and 9 deletions
@@ -20,12 +20,21 @@ namespace IBKRTrader.Core.Positions;
/// <param name="Lock">Sperrstufe des Betreibers für dieses Buch und Instrument.</param>
/// <param name="Rule">Regel des Instruments (Handelsverbot, Stückgrenze) – gilt über alle Bücher.</param>
/// <param name="BrokerQuantity">
/// Was der Broker führt. <c>null</c> heißt <b>nicht bekannt</b> und ausdrücklich nicht „null Stück":
/// Ohne Verbindung ist jede Aussage über Abweichungen wertlos.
/// Was der Broker für dieses Instrument führt – <b>kontoweit</b>, denn der Broker kennt keine
/// Bücher. <c>null</c> heißt <b>nicht bekannt</b> und ausdrücklich nicht „null Stück": Ohne
/// Verbindung ist jede Aussage über Abweichungen wertlos.
///
/// <para>Dezimal und nicht ganzzahlig, weil der Broker es so meldet – Bruchstücke gibt es. Sie hier
/// zu runden hieße, eine Abweichung wegzurechnen, die es gibt.</para>
/// </param>
/// <param name="OwnTotalQuantity">
/// Unser Bestand desselben Instruments <b>über alle Bücher</b> – die einzige Zahl, die sich mit
/// <paramref name="BrokerQuantity"/> vergleichen lässt.
///
/// <para>Ohne sie meldete jede Aufteilung eine Abweichung: Liegen 2 Stück SAP in DIAG und 2 in
/// UNASSIGNED, während der Broker 4 führt, stimmt alles – zeilenweise verglichen sähe es nach zwei
/// Fehlbeständen aus. Am 2026-09-06 auf der Test-VM genau so beobachtet.</para>
/// </param>
public sealed record ManagedPosition(
BookId Book,
InstrumentRef Instrument,
@@ -34,7 +43,8 @@ public sealed record ManagedPosition(
decimal BoundCapital,
PositionLock Lock,
InstrumentRule Rule,
decimal? BrokerQuantity)
decimal? BrokerQuantity,
int OwnTotalQuantity)
{
/// <summary>Herkunft in einem Wort – das, wonach beim Aufräumen zuerst gefragt wird.</summary>
public string Origin => Book.Kind switch
@@ -46,10 +56,16 @@ public sealed record ManagedPosition(
};
/// <summary>
/// Weicht unser Bestand von dem des Brokers ab? <c>false</c>, solange der Broker nichts gesagt
/// hat – eine Abweichung, die nur aus fehlenden Daten besteht, ist keine.
/// Weicht unser Bestand von dem des Brokers ab? Verglichen wird der Bestand <b>aller</b> Bücher,
/// nicht der dieser Zeile – der Broker kennt unsere Aufteilung nicht.
///
/// <para><c>false</c>, solange der Broker nichts gesagt hat: Eine Abweichung, die nur aus
/// fehlenden Daten besteht, ist keine.</para>
/// </summary>
public bool Diverges => BrokerQuantity is { } broker && broker != Quantity;
public bool Diverges => BrokerQuantity is { } broker && broker != OwnTotalQuantity;
/// <summary>Teilen sich mehrere Bücher dieses Instrument?</summary>
public bool IsSharedAcrossBooks => OwnTotalQuantity != Quantity;
public bool IsRestricted => Lock != PositionLock.Free || Rule.IsRestrictive;
}
@@ -142,6 +158,13 @@ public sealed class PositionManager
.GroupBy(p => p.Instrument!.Value.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Sum(p => p.Quantity), StringComparer.OrdinalIgnoreCase);
// Unser Bestand je Instrument ueber alle Buecher - die einzige Groesse, die sich mit dem
// Broker vergleichen laesst.
var ownTotals = own
.Where(p => p.Quantity != 0)
.GroupBy(p => p.Instrument.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Sum(p => p.Quantity), StringComparer.OrdinalIgnoreCase);
var managed = new List<ManagedPosition>(own.Count);
foreach (var position in own.Where(p => p.Quantity != 0))
{
@@ -162,7 +185,10 @@ public sealed class PositionManager
BoundCapital: position.BoundCapital,
Lock: await _rules.GetLockAsync(book, position.Instrument, ct).ConfigureAwait(false),
Rule: await _rules.GetRuleAsync(position.Instrument, ct).ConfigureAwait(false),
BrokerQuantity: brokerQuantity));
BrokerQuantity: brokerQuantity,
OwnTotalQuantity: ownTotals.TryGetValue(position.Instrument.Key, out var total)
? total
: position.Quantity));
}
// Bestand, den der Broker fuehrt und den wir in keinem Buch haben. Der Abgleich (#306)