namespace IBKRTrader.Core.Capital; /// Art eines Buchs. Sie entscheidet, ob es handeln darf und ob Limits für es gelten. public enum BookKind { /// Ein Strategiemodul. Handelt, Limits gelten. Strategy, /// Von Hand in TWS angelegt. Handelt nicht über uns, Limits gelten nicht – zählt aber mit. Manual, /// Beim Broker gefunden, keinem Buch zuzuordnen. Handelt nicht, zählt mit. Unassigned, /// Nicht allokierte Reserve, FX, Ausgleichsposten. House } /// /// Kennung eines Kapitaltopfs mit zugeordneten Positionen. /// /// Ein Buch ist nicht dasselbe wie ein Modul, auch wenn die Vorgabe /// BookId == Modulname lautet. Die Trennung erlaubt später mehrere Bücher desselben Moduls /// und macht MANUAL, UNASSIGNED und HOUSE überhaupt erst modellierbar. Jetzt kostenlos, später /// teuer nachzurüsten. /// public readonly record struct BookId { /// Für Bestände, die der Betreiber selbst in TWS angelegt hat. public static readonly BookId Manual = new("MANUAL", BookKind.Manual); /// Für Bestände, die der Abgleich beim Broker fand und niemandem zuordnen konnte. public static readonly BookId Unassigned = new("UNASSIGNED", BookKind.Unassigned); /// Für die nicht allokierte Reserve. public static readonly BookId House = new("HOUSE", BookKind.House); /// /// Buch der Diagnose-Aufträge: der Verifikationsorder aus --verify-order und später des /// DebugTrading-Moduls. /// /// Ein eigenes Buch, damit eine Prüforder nicht in der Buchführung eines Strategiemoduls /// landet. Sonst verfälschte sie dessen Auslastung, dessen GuV und dessen Konzentration – und /// zwar dauerhaft, weil die Position echt ist. /// /// Es handelt, also ist es ein Strategiebuch und unterliegt allen Limits. /// public static readonly BookId Diagnostics = new("DIAG", BookKind.Strategy); private BookId(string value, BookKind kind) { Value = value; Kind = kind; } public string Value { get; } public BookKind Kind { get; } /// Buch eines Strategiemoduls. Die reservierten Namen sind hier nicht zulässig. public static BookId ForModule(string module) { var name = (module ?? "").Trim().ToUpperInvariant(); if (name.Length == 0) throw new ArgumentException("Buchname darf nicht leer sein.", nameof(module)); if (IsReserved(name)) throw new ArgumentException( $"'{name}' ist ein reservierter Buchname und keinem Modul zuweisbar.", nameof(module)); return new BookId(name, BookKind.Strategy); } /// Liest einen gespeicherten Buchnamen zurück, einschließlich der reservierten. public static BookId Parse(string value) { var name = (value ?? "").Trim().ToUpperInvariant(); return name switch { "MANUAL" => Manual, "UNASSIGNED" => Unassigned, "HOUSE" => House, _ => ForModule(name) }; } public static bool IsReserved(string name) => name.Equals("MANUAL", StringComparison.OrdinalIgnoreCase) || name.Equals("UNASSIGNED", StringComparison.OrdinalIgnoreCase) || name.Equals("HOUSE", StringComparison.OrdinalIgnoreCase); /// Darf dieses Buch Signale senden und empfangen? Nur Strategiebücher dürfen das. public bool MayTrade => Kind == BookKind.Strategy; /// /// Gelten die Kapital- und Konzentrationslimits für dieses Buch? MANUAL ist ausgenommen – /// zählt aber im Zähler jeder Konzentrationsprüfung voll mit. Hält der Betreiber 50 % AAPL von /// Hand, sind die Module bei AAPL gesperrt. So gewollt. /// public bool SubjectToLimits => Kind == BookKind.Strategy; public override string ToString() => Value; } /// /// Welche Bücher es gibt und welches Modul zu welchem gehört. /// /// Warum das nicht das Modul selbst entscheidet: TradeSignal.SourceModule kommt /// aus dem Modul und ist damit kein Ausweis. Ohne diese Prüfung könnte ein fehlerhaftes Modul sich /// als MANUAL ausgeben und damit jede Limitprüfung umgehen, oder mit einem Tippfehler ein /// Phantombuch erzeugen, für das nie ein Cap konfiguriert wurde. Die zulässigen Namen stammen /// deshalb aus der Modul-Registrierung des Hosts, nicht aus dem Signal. /// public sealed class BookRegistry { private readonly Dictionary _byModule; /// /// Die Namen der beim Start registrierten Strategiemodule – dieselbe Liste, aus der der Host /// seine Module baut. /// public BookRegistry(IEnumerable moduleNames) { _byModule = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var name in moduleNames) { var trimmed = (name ?? "").Trim(); if (trimmed.Length == 0 || BookId.IsReserved(trimmed)) continue; _byModule[trimmed] = BookId.ForModule(trimmed); } } /// Alle handelnden Bücher. public IReadOnlyCollection StrategyBooks => _byModule.Values; /// /// Buch eines Modulnamens, oder null, wenn der Name nicht registriert ist. Ein /// unbekannter Name wird abgelehnt und nicht angelegt – ein Buch, das erst durch ein Signal /// entsteht, hat kein Cap und wäre damit unbegrenzt. /// public BookId? Resolve(string? moduleName) => moduleName is not null && _byModule.TryGetValue(moduleName.Trim(), out var book) ? book : null; public bool IsRegistered(string? moduleName) => Resolve(moduleName) is not null; }