Optionskette und Bewertungsgroessen im Core

Der Core kennt jetzt die handelbaren Verfallstage und Strikes eines Basiswerts
(reqSecDefOptParams) und die Bewertungsgroessen eines Kontrakts
(tickOptionComputation). Damit steht der letzte Baustein, den eine
Wheel-Strategie braucht, um ueberhaupt einen Kontrakt auswaehlen zu koennen.

OptionChain ist bewusst nur eine Liste und trifft keine Marktentscheidung. Die
Auswahlhilfen daran sind reine Rechnung - "der Verfall bei etwa 35 Tagen" ist
eine Kalenderfrage, "welches Delta will ich" nicht. Letzteres gehoert ins Modul.

Zwei Eigenheiten der TWS-API sind beim Messen aufgefallen und haben je einen
Fehler gekostet:

1. Greeks kommen NICHT im Snapshot. reqMktData mit snapshot=true liefert Geld-,
   Brief- und letzten Kurs, aber keine tickOptionComputation. Es braucht ein Abo,
   das beim ersten Modell-Tick wieder abbestellt wird.

2. TWS meldet je Basiswert MEHRERE Serien. Fuer AAPL kamen "AAPL" und "2AAPL" -
   letztere eine Sonderserie mit eigenen Ausuebungspreisen. Der erstbeste Satz
   war die falsche: Strike 320 in "2AAPL" gibt es nicht, TWS antwortete mit 200
   ("No security definition has been found"). Die Auswahl nimmt jetzt SMART vor
   allen anderen Boersen und darin die Handelsklasse, die dem Symbol entspricht.

Massgeblich ist ausserdem allein das Modell-Feld (13 bzw. 83 bei verzoegerten
Daten). Die Varianten zu Geld-, Brief- und letztem Kurs fehlen bei illiquiden
Serien und lieferten sonst ein Delta aus einem Kurs, den niemand stellt. Fehlende
Werte meldet TWS als double.MaxValue oder -1; sie werden zu null, nicht zu 0 -
ein Delta von 0 hiesse "reagiert nicht auf den Basiswert".

Gemessen gegen das Paper-Gateway:
  AAPL (USD, SMART/AAPL, x100): 22 Verfallstage, 123 Strikes
  Auswahl wie beim Wheel: AAPL|20261009|P|320 (Kurs 320.01, 34 Tage Laufzeit)
  Deckungsbedarf je Kontrakt: 32.000,00 USD
  IV 24.5 %, Delta -0.471, Gamma 0.0169, Vega 0.392, Theta -0.123 (verzoegert)

Der Schalter --option-chain SYMBOL [--currency X] faehrt genau diesen Ablauf und
bleibt als Pruefwerkzeug. 414 Tests gruen.

Behebt #299.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-05 13:52:27 +02:00
co-authored by Claude Opus 5
parent b618efa9ce
commit 0feeeba815
8 changed files with 580 additions and 4 deletions
+68 -2
View File
@@ -3,6 +3,7 @@ using IBKRTrader.Core.Configuration;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Reconciliation;
using IBKRTrader.Core.Settings;
using IBKRTrader.Core.Time;
using IBKRTrader.Core.Trading;
using IBKRTrader.Core.Modularity;
using IBKRTrader.Hosting;
@@ -52,6 +53,9 @@ internal static class Program
// Bestandsabgleich von Hand. Mit --dry-run wird nur festgestellt, nicht gebucht.
var reconcile = HasFlag(args, "--reconcile");
// Optionskette und Bewertungsgrößen eines Basiswerts ansehen. Liest nur.
var chainSymbol = ArgAfter(args, "--option-chain");
var modules = AppHostBuilder.CreateModules();
using var host = AppHostBuilder.Build(modules);
@@ -61,13 +65,13 @@ internal static class Program
// sie nur in der Logdatei, sah ein CI-Schritt bloß "OK" und Exitcode 0 – auch dann, wenn
// dort "kein Master-Key" und "DB ohne SslMode" untereinander lagen.
var warnings = 0;
if (checkOnly || ibkrCheck || verifyOrder is not null || reconcile)
if (checkOnly || ibkrCheck || verifyOrder is not null || reconcile || chainSymbol is not null)
{
logger.EntryWritten += e =>
{
// Bei der IBKR-Prüfung zählt auch, was der Adapter an Info meldet: Zeitkontext,
// ServerVersion und Kontoliste stehen dort und sind der halbe Befund.
var floor = ibkrCheck || verifyOrder is not null || reconcile
var floor = ibkrCheck || verifyOrder is not null || reconcile || chainSymbol is not null
? AppLogLevel.Info : AppLogLevel.Warn;
if (e.Level < floor) return;
if (e.Level >= AppLogLevel.Warn) Interlocked.Increment(ref warnings);
@@ -83,6 +87,10 @@ internal static class Program
if (reconcile)
return await RunReconcileAsync(host.Services, !HasFlag(args, "--dry-run")).ConfigureAwait(false);
if (chainSymbol is not null)
return await RunOptionChainAsync(host.Services, chainSymbol,
ArgAfter(args, "--currency") ?? Currencies.Default).ConfigureAwait(false);
if (ibkrCheck)
return await RunIbkrCheckAsync(host.Services, SymbolArg(args) ?? "AAPL").ConfigureAwait(false);
@@ -119,6 +127,64 @@ internal static class Program
/// die den Schalter besteht, kann sonst vollständig ohne Datenbank dastehen – und genau dafür
/// ist er gedacht.
/// </summary>
/// <summary>
/// Zeigt die Optionskette eines Basiswerts und die Bewertungsgrößen des Kontrakts, den eine
/// Wheel-Strategie als Erstes ansehen würde: der Verfall bei etwa 35 Tagen, der höchste Strike
/// unter dem Kurs, ein Put. <b>Liest nur</b> – es wird nichts platziert.
/// </summary>
private static async Task<int> RunOptionChainAsync(
IServiceProvider services, string symbol, string currency)
{
var market = services.GetRequiredService<IOptionMarketData>();
var broker = services.GetRequiredService<IBrokerClient>();
var underlying = InstrumentRef.ForStock(symbol, currency);
var inv = System.Globalization.CultureInfo.InvariantCulture;
Console.WriteLine($"Optionskette für {underlying.Key}");
var chain = await market.GetChainAsync(underlying).ConfigureAwait(false);
if (chain is null || chain.IsEmpty)
{
Console.Error.WriteLine("Keine Kette erhalten. Werden auf diesen Wert Optionen gehandelt, " +
"und stimmt die Währung?");
return 1;
}
Console.WriteLine($" {chain}");
Console.WriteLine($" Verfallstage (erste 6): {string.Join(", ", chain.Expiries.Take(6))}");
Console.WriteLine($" Strikes: {chain.Strikes[0].ToString(inv)} … {chain.Strikes[^1].ToString(inv)}");
var quote = await broker.GetQuoteAsync(underlying).ConfigureAwait(false);
if (quote is null)
{
Console.WriteLine(" Kein Kurs verfügbar – ohne ihn keine Strike-Auswahl.");
return 0;
}
var today = DateOnly.FromDateTime(AppTimeZone.Now);
var expiry = chain.ExpiryClosestTo(today, targetDaysOut: 35);
var strike = chain.HighestStrikeBelow(quote.Last);
if (expiry is null || strike is null)
{
Console.WriteLine($" Kurs {quote.Last.ToString(inv)}, aber keine passende Auswahl " +
"(kein Verfall in der Nähe oder kein Strike darunter).");
return 0;
}
var spec = chain.Spec(expiry.Value, strike.Value, OptionRight.Put);
Console.WriteLine($" Auswahl wie beim Wheel: {spec} " +
$"(Kurs {quote.Last.ToString(inv)}, {(expiry.Value.DayNumber - today.DayNumber)} Tage Laufzeit)");
Console.WriteLine($" Deckungsbedarf je Kontrakt: {spec.CashCoveragePerContract.ToString("N2", inv)} {spec.Currency}");
var greeks = await market.GetGreeksAsync(spec).ConfigureAwait(false);
Console.WriteLine(greeks is null
? " Keine Bewertungsgrößen geliefert (bei geschlossener Börse und illiquiden Serien normal)."
: $" {greeks}");
return 0;
}
/// <summary>
/// Bestandsabgleich von Hand. Ohne <c>--dry-run</c> werden die Korrekturen gebucht;
/// gehandelt wird in keinem Fall.