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