Sperren des Betreibers: Positionen und Instrumente, im Core durchgesetzt
Vier Stufen je Position (frei, nur schliessen, nur aufbauen, gesperrt) und je Instrument ein Handelsverbot samt harter Bestandsobergrenze. Beides gilt fuer Module; der manuelle Handel in TWS bleibt unberuehrt. Die Pruefung sitzt im RiskService, nicht im Modul: Eine Sperre, die ein Modul selbst pruefen muesste, ist keine Sperre, sondern eine Bitte. Sie laeuft NACH der fachlichen Bewertung, weil erst dort feststeht, welcher Teil eines Auftrags eroeffnet und welcher schliesst - genau daran haengen die Stufen. Drei Entscheidungen, die im Betrieb zaehlen: "Gesperrt" verhindert auch den Ausgang. Eine Position, die aus Risikogruenden geschlossen werden muesste, bleibt liegen, bis jemand die Sperre loest. Das ist gewollt und steht als eigener Test da, damit es niemand fuer einen Fehler haelt. Ein Handelsverbot sperrt das Eroeffnen, nicht das Schliessen. Ein nachtraeglich gesetztes Verbot darf keine Position einsperren, die man gerade loswerden will; wer auch das will, nimmt die Positionssperre. Die harte Stueckzahl zaehlt den Bestand ueber ALLE Buecher, MANUAL eingeschlossen, und Short-Positionen mit ihrer Groesse statt ihrem Vorzeichen: "hoechstens 100" meint die Groesse der Verpflichtung. Sie ersetzt das prozentuale Konzentrationslimit nicht - der Prozentsatz waechst mit dem Konto, die Stueckzahl ist eine Aussage ueber ein bestimmtes Papier. Es zieht die engere Grenze. TradingRuleService haelt die Regeln im Speicher, weil sie bei jeder Risikopruefung gebraucht werden, sich aber selten aendern. Laesst sich die Liste NICHT laden, gilt das nicht als "keine Sperren": der Dienst meldet es und sperrt, bis sie wieder lesbar ist. Eine Sperre, die bei einem Datenbankfehler verschwindet, waere keine. Ein unlesbarer Stufenwert wird ebenso als schaerfste Stufe gelesen. Auf dem Testserver angewendet (Tabellen core_position_lock und core_instrument_rule) und der Dienst damit neu gestartet - keine Ladefehler, der Handel bleibt frei. Zur Probe stehen dort die beiden Beispiele des Betreibers: WIRECARD gesperrt, NVDA hoechstens 100 Stueck. 425 Tests gruen, 11 davon neu. Arbeit an #485 und #486; die Oberflaeche fehlt noch, die Durchsetzung steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using IBKRTrader.Core.Trading;
|
||||
|
||||
namespace IBKRTrader.Core.Capital;
|
||||
|
||||
/// <summary>
|
||||
/// Wie weit ein Modul eine bestehende Position noch anfassen darf. Vom Betreiber gesetzt, nicht
|
||||
/// von einer Strategie.
|
||||
/// </summary>
|
||||
public enum PositionLock
|
||||
{
|
||||
/// <summary>Keine Einschränkung – die Vorgabe.</summary>
|
||||
Free,
|
||||
|
||||
/// <summary>Nur abbauen. Ein Modul darf verkaufen bzw. glattstellen, aber nicht aufstocken.</summary>
|
||||
CloseOnly,
|
||||
|
||||
/// <summary>Nur aufbauen. Das Gegenstück, etwa während eines geplanten Einstiegs.</summary>
|
||||
OpenOnly,
|
||||
|
||||
/// <summary>
|
||||
/// Kein Modul fasst die Position an – weder öffnen noch schließen.
|
||||
///
|
||||
/// <para><b>Diese Stufe verhindert auch den Ausgang.</b> 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.</para>
|
||||
/// </summary>
|
||||
Locked
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Was für ein Instrument gilt, unabhängig von Buch und Bestand: ein Handelsverbot und/oder eine
|
||||
/// harte Obergrenze in Stück.
|
||||
/// </summary>
|
||||
/// <param name="Blocked">
|
||||
/// 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 <see cref="PositionLock.Locked"/>.
|
||||
/// </param>
|
||||
/// <param name="MaxQuantity">
|
||||
/// Höchstbestand über <b>alle</b> Bücher, einschließlich MANUAL; <c>null</c> = 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.
|
||||
/// </param>
|
||||
public sealed record InstrumentRule(bool Blocked = false, int? MaxQuantity = null)
|
||||
{
|
||||
/// <summary>Die Regel, die nichts einschränkt – für Instrumente ohne Eintrag.</summary>
|
||||
public static readonly InstrumentRule None = new();
|
||||
|
||||
public bool IsRestrictive => Blocked || MaxQuantity is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Die Sperren des Betreibers, als reine Rechnung – kein Zustand, keine Datenbank.
|
||||
///
|
||||
/// <para><b>Warum im Core und nicht im Modul.</b> 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.</para>
|
||||
///
|
||||
/// <para><b>Was sie nicht tut:</b> Der manuelle Handel des Betreibers in TWS bleibt unberührt.
|
||||
/// Gesperrt werden Module, nicht der Mensch.</para>
|
||||
/// </summary>
|
||||
public static class TradingRules
|
||||
{
|
||||
/// <summary>
|
||||
/// Prüft eine geplante Order gegen die Positionssperre. <c>null</c> = erlaubt.
|
||||
/// </summary>
|
||||
/// <param name="lockLevel">Die gesetzte Stufe.</param>
|
||||
/// <param name="instrument">Betroffenes Instrument – nur für die Meldung.</param>
|
||||
/// <param name="openingQuantity">Der eröffnende Teil des Auftrags (0 bei reinem Schließen).</param>
|
||||
/// <param name="closingQuantity">Der schließende Teil.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prüft eine geplante Order gegen die Regeln des Instruments. <c>null</c> = erlaubt.
|
||||
/// </summary>
|
||||
/// <param name="rule">Regel des Instruments; <see cref="InstrumentRule.None"/> für „keine".</param>
|
||||
/// <param name="instrument">Betroffenes Instrument.</param>
|
||||
/// <param name="openingQuantity">Der eröffnende Teil – nur er wird begrenzt.</param>
|
||||
/// <param name="heldTotal">
|
||||
/// 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.
|
||||
/// </param>
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user