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;
}