using IBKRTrader.Core.Trading; namespace IBKRTrader.Core.Capital; /// /// Wie weit ein Modul eine bestehende Position noch anfassen darf. Vom Betreiber gesetzt, nicht /// von einer Strategie. /// public enum PositionLock { /// Keine Einschränkung – die Vorgabe. Free, /// Nur abbauen. Ein Modul darf verkaufen bzw. glattstellen, aber nicht aufstocken. CloseOnly, /// Nur aufbauen. Das Gegenstück, etwa während eines geplanten Einstiegs. OpenOnly, /// /// Kein Modul fasst die Position an – weder öffnen noch schließen. /// /// Diese Stufe verhindert auch den Ausgang. Eine Position, die aus Risikogründen /// geschlossen werden müsste, bleibt liegen, bis jemand die Sperre löst. Das ist eine zulässige /// Entscheidung des Betreibers und muss beim Setzen sichtbar sein – nicht erst im /// Störungsfall. /// Locked } /// /// Was für ein Instrument gilt, unabhängig von Buch und Bestand: ein Handelsverbot und/oder eine /// harte Obergrenze in Stück. /// /// /// Kein Modul eröffnet in diesem Instrument. Schließen bleibt erlaubt – ein nachträgliches Verbot /// darf keine Position einsperren, die man gerade loswerden will. Wer auch das Schließen /// unterbinden will, nimmt . /// /// /// Höchstbestand über alle Bücher, einschließlich MANUAL; null = keine Grenze. /// Bewusst etwas anderes als das prozentuale Konzentrationslimit: Der Prozentsatz wächst mit dem /// Konto und ist Risikostreuung, die Stückzahl ist eine Aussage über ein bestimmtes Papier. Beide /// gelten nebeneinander, es zieht die engere. /// public sealed record InstrumentRule(bool Blocked = false, int? MaxQuantity = null) { /// Die Regel, die nichts einschränkt – für Instrumente ohne Eintrag. public static readonly InstrumentRule None = new(); public bool IsRestrictive => Blocked || MaxQuantity is not null; } /// /// Die Sperren des Betreibers, als reine Rechnung – kein Zustand, keine Datenbank. /// /// Warum im Core und nicht im Modul. Eine Sperre, die ein Modul selbst prüfen müsste, /// ist keine Sperre, sondern eine Bitte. Sie steht deshalb im Risikopfad neben der /// Deckungsprüfung, wo ein Modulfehler sie nicht umgehen kann. /// /// Was sie nicht tut: Der manuelle Handel des Betreibers in TWS bleibt unberührt. /// Gesperrt werden Module, nicht der Mensch. /// public static class TradingRules { /// /// Prüft eine geplante Order gegen die Positionssperre. null = erlaubt. /// /// Die gesetzte Stufe. /// Betroffenes Instrument – nur für die Meldung. /// Der eröffnende Teil des Auftrags (0 bei reinem Schließen). /// Der schließende Teil. public static LimitBreach? CheckPositionLock( PositionLock lockLevel, InstrumentRef instrument, int openingQuantity, int closingQuantity) { switch (lockLevel) { case PositionLock.Free: return null; case PositionLock.CloseOnly when openingQuantity <= 0: case PositionLock.OpenOnly when closingQuantity <= 0: return null; case PositionLock.CloseOnly: // Der schließende Teil bleibt erlaubt; abgelehnt wird nur, was darüber hinausgeht. return Breach(instrument, LimitKind.PositionLocked, maxQuantity: closingQuantity); case PositionLock.OpenOnly: return Breach(instrument, LimitKind.PositionLocked, maxQuantity: openingQuantity); case PositionLock.Locked: return Breach(instrument, LimitKind.PositionLocked, maxQuantity: 0); default: return null; } } /// /// Prüft eine geplante Order gegen die Regeln des Instruments. null = erlaubt. /// /// Regel des Instruments; für „keine". /// Betroffenes Instrument. /// Der eröffnende Teil – nur er wird begrenzt. /// /// Bestand über alle Bücher, als Betrag. Eine geschriebene Option zählt dabei mit ihrer Menge, /// nicht mit ihrem Vorzeichen: „höchstens 100 Stück" meint die Größe der Verpflichtung. /// public static LimitBreach? CheckInstrumentRule( InstrumentRule rule, InstrumentRef instrument, int openingQuantity, int heldTotal) { if (openingQuantity <= 0 || !rule.IsRestrictive) return null; // Ein Verbot sperrt das Eröffnen, nicht das Schließen: Der schließende Teil kommt hier gar // nicht an, deshalb genügt der Blick auf openingQuantity. if (rule.Blocked) return Breach(instrument, LimitKind.InstrumentBlocked, maxQuantity: 0); if (rule.MaxQuantity is not { } max) return null; var held = Math.Abs(heldTotal); if (held + openingQuantity <= max) return null; return new LimitBreach( LimitKind.InstrumentQuantity, instrument.Key, Current: held + openingQuantity, Limit: max, MaxQuantity: Math.Max(0, max - held), MaxNotional: 0m); } private static LimitBreach Breach(InstrumentRef instrument, LimitKind kind, int maxQuantity) => new(kind, instrument.Key, Current: 0m, Limit: 0m, MaxQuantity: maxQuantity, MaxNotional: 0m); }