diff --git a/src/IBKRTrader.Core/Trading/ExecutionService.cs b/src/IBKRTrader.Core/Trading/ExecutionService.cs index 2022151..05a9f55 100644 --- a/src/IBKRTrader.Core/Trading/ExecutionService.cs +++ b/src/IBKRTrader.Core/Trading/ExecutionService.cs @@ -60,7 +60,7 @@ public sealed class ExecutionService : IExecutionService } // 2. Kurs - var quote = await _broker.GetQuoteAsync(signal.Symbol, ct); + var quote = await _broker.GetQuoteAsync(signal.Instrument, ct); if (quote is null || quote.Last <= 0) { Journal(signal, TradeDecision.Skipped, DecisionReason.NoQuote, $"Kein Kurs für {signal.Symbol} verfügbar."); diff --git a/src/IBKRTrader.Core/Trading/IBrokerClient.cs b/src/IBKRTrader.Core/Trading/IBrokerClient.cs index 982a5c6..8e784db 100644 --- a/src/IBKRTrader.Core/Trading/IBrokerClient.cs +++ b/src/IBKRTrader.Core/Trading/IBrokerClient.cs @@ -6,8 +6,14 @@ namespace IBKRTrader.Core.Trading; /// public interface IBrokerClient { - /// Aktuelle Kurs-Momentaufnahme oder null, wenn nicht verfügbar. - Task GetQuoteAsync(string symbol, CancellationToken ct = default); + /// + /// Aktuelle Kurs-Momentaufnahme oder null, wenn nicht verfügbar. + /// + /// Nimmt das Instrument und nicht nur das Symbol: ohne Währung wird jeder Kontrakt + /// als USD gesucht, und ein Wert, der nur in EUR notiert, ist dann schlicht "nicht auflösbar". + /// Ohne Kontraktangaben gälte dasselbe für jede Option. + /// + Task GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default); /// Kontostand-Momentaufnahme. Task GetAccountStateAsync(CancellationToken ct = default); diff --git a/src/IBKRTrader.Core/Trading/Ibkr/IbkrBrokerClient.cs b/src/IBKRTrader.Core/Trading/Ibkr/IbkrBrokerClient.cs index c6f231d..ba385e2 100644 --- a/src/IBKRTrader.Core/Trading/Ibkr/IbkrBrokerClient.cs +++ b/src/IBKRTrader.Core/Trading/Ibkr/IbkrBrokerClient.cs @@ -53,14 +53,14 @@ public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, ID $"TWS-Broker aktiv: {ibkr.Host}:{ibkr.Port} (Client {ibkr.ClientId}), Modus {mode}."); } - public async Task GetQuoteAsync(string symbol, CancellationToken ct = default) + public async Task GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default) { if (!await IsReadyAsync(ct).ConfigureAwait(false)) return null; - var contract = await _connection!.ResolveContractAsync(InstrumentRef.ForStock(symbol), _requestTimeout, ct).ConfigureAwait(false); + var contract = await _connection!.ResolveContractAsync(instrument, _requestTimeout, ct).ConfigureAwait(false); if (contract is null) return null; - return await _connection.RequestQuoteAsync(contract, symbol, _requestTimeout, ct).ConfigureAwait(false); + return await _connection.RequestQuoteAsync(contract, instrument.Symbol, _requestTimeout, ct).ConfigureAwait(false); } public async Task GetAccountStateAsync(CancellationToken ct = default) diff --git a/src/IBKRTrader.Core/Trading/NullBrokerClient.cs b/src/IBKRTrader.Core/Trading/NullBrokerClient.cs index 8709084..b7d4e5b 100644 --- a/src/IBKRTrader.Core/Trading/NullBrokerClient.cs +++ b/src/IBKRTrader.Core/Trading/NullBrokerClient.cs @@ -19,7 +19,7 @@ public sealed class NullBrokerClient : IBrokerClient, IBrokerPortfolioReader public Task> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default) => Task.FromResult>(Array.Empty()); - public Task GetQuoteAsync(string symbol, CancellationToken ct = default) + public Task GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default) => Task.FromResult(null); public Task GetAccountStateAsync(CancellationToken ct = default) diff --git a/src/IBKRTrader.Daemon/Program.cs b/src/IBKRTrader.Daemon/Program.cs index af308e1..4aeb0d1 100644 --- a/src/IBKRTrader.Daemon/Program.cs +++ b/src/IBKRTrader.Daemon/Program.cs @@ -182,7 +182,7 @@ internal static class Program foreach (var p in positions.Take(10)) Console.WriteLine($" {p}"); - var quote = await broker.GetQuoteAsync(symbol, cts.Token).ConfigureAwait(false); + var quote = await broker.GetQuoteAsync(InstrumentRef.ForStock(symbol), cts.Token).ConfigureAwait(false); Console.WriteLine(quote is null ? $"Kurs {symbol}: keiner geliefert (ohne Marktdaten-Abo bei geschlossener Börse normal)" : $"Kurs {symbol}: {quote}"); diff --git a/src/IBKRTrader.Daemon/VerifyOrder.cs b/src/IBKRTrader.Daemon/VerifyOrder.cs index 9eac446..324503f 100644 --- a/src/IBKRTrader.Daemon/VerifyOrder.cs +++ b/src/IBKRTrader.Daemon/VerifyOrder.cs @@ -84,13 +84,12 @@ internal static class VerifyOrder return 1; } - // Die Kursabfrage laeuft weiterhin ueber das Symbol; der Adapter loest den Kontrakt - // anhand des Instruments samt Waehrung auf. - var quote = await broker.GetQuoteAsync(symbol, cts.Token).ConfigureAwait(false); + var quote = await broker.GetQuoteAsync(instrument, cts.Token).ConfigureAwait(false); if (quote is null || quote.Last <= 0) { - Console.Error.WriteLine($"ABBRUCH: kein Kurs für {symbol}. Bei geschlossener Börse und ohne " + - $"Marktdaten-Abo ist das normal – dann später erneut versuchen."); + Console.Error.WriteLine($"ABBRUCH: kein Kurs für {instrument}. Entweder ist der Kontrakt " + + $"nicht auflösbar (Symbol/Währung prüfen – die Meldung des Adapters " + + $"steht oben), oder die Börse ist zu und es liegt kein Marktdaten-Abo vor."); return 1; } diff --git a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs index 1f089d6..e5fb5da 100644 --- a/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs +++ b/tests/IBKRTrader.Tests/Trading/ExecutionServiceTests.cs @@ -58,7 +58,7 @@ public class ExecutionServiceTests private void ArrangeHappyPath() { EnableTrading(); - _broker.GetQuoteAsync(Arg.Any(), Arg.Any()) + _broker.GetQuoteAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new Quote("AAPL", 100m, 99m, 101m))); _broker.GetAccountStateAsync(Arg.Any()) .Returns(Task.FromResult(new AccountState(10_000m, 10_000m))); @@ -87,7 +87,7 @@ public class ExecutionServiceTests public async Task NoQuote_Skips() { EnableTrading(); - _broker.GetQuoteAsync(Arg.Any(), Arg.Any()) + _broker.GetQuoteAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(null)); var result = await CreateSut().ExecuteAsync(BuySignal); @@ -100,7 +100,7 @@ public class ExecutionServiceTests public async Task RiskRejects_Skips_WithReason() { EnableTrading(); - _broker.GetQuoteAsync(Arg.Any(), Arg.Any()) + _broker.GetQuoteAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new Quote("AAPL", 100m, 99m, 101m))); _broker.GetAccountStateAsync(Arg.Any()) .Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));