Das OptionsWheel setzt auf Dingen auf, die es im Core nicht gab. Diese Sitzung baut sie. #298 Optionen als Instrument. InstrumentKind/OptionSpec und ein druckbarer Instrumentenschluessel ("AAPL|20260918|C|150"); TradeSignal und OrderRequest tragen ein optionales OptionSpec, ohne das sich der Aktienpfad unveraendert verhaelt. Position.Notional rechnet ab jetzt mit der Kontraktgroesse - vorher war jede Option um Faktor 100 zu klein bewertet und damit jedes Risikolimit wirkungslos, lautlos und in die gefaehrliche Richtung. Daneben neu BoundCapital: ein Short Put bindet Strike x Kontraktgroesse, nicht die vereinnahmte Praemie. #300 sell-to-open. Jeder Auftrag zerfaellt in einen schliessenden und einen eroeffnenden Teil; der schliessende wird von keinem Limit verhindert - auch nicht vom Kaufstopp, denn ein Rueckkauf baut eine Verpflichtung ab. Deckungspruefung im Core, nicht im Modul: Short Call nur mit freien Aktien, Short Put nur mit reservierbarem Bargeld. #302-#305 Kapital- und Buchmodell. Buecher mit reservierten Namen und Eigentumsregeln, Pool mit Caps und Headroom, Margin-Sperre in vier Schichten, core_reservation mit Lebenszyklus, Frist, Idempotenz ueber (SignalId, ContractKey) und Quarantaene beim Start. #309/#310 Modul OptionsWheel: ow_-Schema mit fuenf Tabellen, Zustandsautomat, StrikeSelector, RollDecider, PremiumMath - vollstaendig ohne Broker testbar. Der Handelsbetrieb bleibt gesperrt, solange #299 und #294 fehlen; das Modul sagt das ueber GetActivationBlocker, statt still nichts zu tun. #293 --verify-order am Daemon: eine Order durch die vollstaendige Kette (Buch, Risiko, Bindung, Order, Fill, Buchung, Abschluss). Ohne --confirm ein Trockenlauf; Abbruch ausserhalb von Paper-Modus UND Paper-Port. Drei Widersprueche in den Konzepten sind dabei entschieden worden: Schicht B heisst ab jetzt "kein UNGEDECKTER Short" (der Grund der Regel war der Kredit, nicht der Short), die Hausreserve wird einmal abgezogen statt zweimal, und der Positionsschluessel ist eine Spalte statt eines Sechser-Verbunds (MySQL verlangt NOT NULL fuer Schluesselspalten). Begruendungen stehen in docs/ROADMAP.md. Enthaelt ausserdem die noch nicht committete Arbeit aus der vorigen Sitzung: die Fehlerbehebungen aus Epic X (WorkerBase-Intervall, BackupWorker, --check gegen die Datenbank, CapitolTrades-429, SIGTERM) samt DatabaseEndpoint, MysqlDumpTool und der Roadmap-Umstellung. Sie lag im Arbeitsbaum und liess sich nicht abtrennen, weil beide Staende dieselben Dateien beruehren. 213 -> 369 Tests, Build ohne Warnung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
6.0 KiB
C#
146 lines
6.0 KiB
C#
namespace IBKRTrader.Core.Capital;
|
||
|
||
/// <summary>Art eines Buchs. Sie entscheidet, ob es handeln darf und ob Limits für es gelten.</summary>
|
||
public enum BookKind
|
||
{
|
||
/// <summary>Ein Strategiemodul. Handelt, Limits gelten.</summary>
|
||
Strategy,
|
||
|
||
/// <summary>Von Hand in TWS angelegt. Handelt nicht über uns, Limits gelten nicht – zählt aber mit.</summary>
|
||
Manual,
|
||
|
||
/// <summary>Beim Broker gefunden, keinem Buch zuzuordnen. Handelt nicht, zählt mit.</summary>
|
||
Unassigned,
|
||
|
||
/// <summary>Nicht allokierte Reserve, FX, Ausgleichsposten.</summary>
|
||
House
|
||
}
|
||
|
||
/// <summary>
|
||
/// Kennung eines Kapitaltopfs mit zugeordneten Positionen.
|
||
///
|
||
/// <para><b>Ein Buch ist nicht dasselbe wie ein Modul</b>, auch wenn die Vorgabe
|
||
/// <c>BookId == Modulname</c> 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.</para>
|
||
/// </summary>
|
||
public readonly record struct BookId
|
||
{
|
||
/// <summary>Für Bestände, die der Betreiber selbst in TWS angelegt hat.</summary>
|
||
public static readonly BookId Manual = new("MANUAL", BookKind.Manual);
|
||
|
||
/// <summary>Für Bestände, die der Abgleich beim Broker fand und niemandem zuordnen konnte.</summary>
|
||
public static readonly BookId Unassigned = new("UNASSIGNED", BookKind.Unassigned);
|
||
|
||
/// <summary>Für die nicht allokierte Reserve.</summary>
|
||
public static readonly BookId House = new("HOUSE", BookKind.House);
|
||
|
||
/// <summary>
|
||
/// Buch der Diagnose-Aufträge: der Verifikationsorder aus <c>--verify-order</c> und später des
|
||
/// DebugTrading-Moduls.
|
||
///
|
||
/// <para>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.</para>
|
||
///
|
||
/// <para>Es handelt, also ist es ein Strategiebuch und unterliegt allen Limits.</para>
|
||
/// </summary>
|
||
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; }
|
||
|
||
/// <summary>Buch eines Strategiemoduls. Die reservierten Namen sind hier nicht zulässig.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>Liest einen gespeicherten Buchnamen zurück, einschließlich der reservierten.</summary>
|
||
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);
|
||
|
||
/// <summary>Darf dieses Buch Signale senden und empfangen? Nur Strategiebücher dürfen das.</summary>
|
||
public bool MayTrade => Kind == BookKind.Strategy;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public bool SubjectToLimits => Kind == BookKind.Strategy;
|
||
|
||
public override string ToString() => Value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Welche Bücher es gibt und welches Modul zu welchem gehört.
|
||
///
|
||
/// <para><b>Warum das nicht das Modul selbst entscheidet:</b> <c>TradeSignal.SourceModule</c> 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.</para>
|
||
/// </summary>
|
||
public sealed class BookRegistry
|
||
{
|
||
private readonly Dictionary<string, BookId> _byModule;
|
||
|
||
/// <param name="moduleNames">
|
||
/// Die Namen der beim Start registrierten Strategiemodule – dieselbe Liste, aus der der Host
|
||
/// seine Module baut.
|
||
/// </param>
|
||
public BookRegistry(IEnumerable<string> moduleNames)
|
||
{
|
||
_byModule = new Dictionary<string, BookId>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var name in moduleNames)
|
||
{
|
||
var trimmed = (name ?? "").Trim();
|
||
if (trimmed.Length == 0 || BookId.IsReserved(trimmed)) continue;
|
||
_byModule[trimmed] = BookId.ForModule(trimmed);
|
||
}
|
||
}
|
||
|
||
/// <summary>Alle handelnden Bücher.</summary>
|
||
public IReadOnlyCollection<BookId> StrategyBooks => _byModule.Values;
|
||
|
||
/// <summary>
|
||
/// Buch eines Modulnamens, oder <c>null</c>, 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.
|
||
/// </summary>
|
||
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;
|
||
}
|