Kursabfrage nimmt das Instrument statt nur das Symbol
Ohne Waehrung wird jeder Kontrakt als USD gesucht: ein Wert, der nur in EUR notiert, ist dann "nicht aufloesbar", und die Ursache steht nirgends. Beim Order-Nachweis gegen MIGA:EUR genau so aufgetreten - die Kontraktaufloesung fuer die ORDER war bereits waehrungsfaehig, die fuer den KURS nicht, und damit brach die Kette eine Stufe vorher ab. Dieselbe Aenderung ist die Voraussetzung fuer Optionskurse (#299): ohne Kontraktangaben laesst sich eine Option ueberhaupt nicht abfragen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ public sealed class ExecutionService : IExecutionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Kurs
|
// 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)
|
if (quote is null || quote.Last <= 0)
|
||||||
{
|
{
|
||||||
Journal(signal, TradeDecision.Skipped, DecisionReason.NoQuote, $"Kein Kurs für {signal.Symbol} verfügbar.");
|
Journal(signal, TradeDecision.Skipped, DecisionReason.NoQuote, $"Kein Kurs für {signal.Symbol} verfügbar.");
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ namespace IBKRTrader.Core.Trading;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IBrokerClient
|
public interface IBrokerClient
|
||||||
{
|
{
|
||||||
/// <summary>Aktuelle Kurs-Momentaufnahme oder null, wenn nicht verfügbar.</summary>
|
/// <summary>
|
||||||
Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default);
|
/// Aktuelle Kurs-Momentaufnahme oder <c>null</c>, wenn nicht verfügbar.
|
||||||
|
///
|
||||||
|
/// <para>Nimmt das <b>Instrument</b> 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.</para>
|
||||||
|
/// </summary>
|
||||||
|
Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>Kontostand-Momentaufnahme.</summary>
|
/// <summary>Kontostand-Momentaufnahme.</summary>
|
||||||
Task<AccountState> GetAccountStateAsync(CancellationToken ct = default);
|
Task<AccountState> GetAccountStateAsync(CancellationToken ct = default);
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, ID
|
|||||||
$"TWS-Broker aktiv: {ibkr.Host}:{ibkr.Port} (Client {ibkr.ClientId}), Modus {mode}.");
|
$"TWS-Broker aktiv: {ibkr.Host}:{ibkr.Port} (Client {ibkr.ClientId}), Modus {mode}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
|
public async Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return null;
|
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;
|
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<AccountState> GetAccountStateAsync(CancellationToken ct = default)
|
public async Task<AccountState> GetAccountStateAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public sealed class NullBrokerClient : IBrokerClient, IBrokerPortfolioReader
|
|||||||
public Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default)
|
public Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default)
|
||||||
=> Task.FromResult<IReadOnlyList<BrokerExecution>>(Array.Empty<BrokerExecution>());
|
=> Task.FromResult<IReadOnlyList<BrokerExecution>>(Array.Empty<BrokerExecution>());
|
||||||
|
|
||||||
public Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
|
public Task<Quote?> GetQuoteAsync(InstrumentRef instrument, CancellationToken ct = default)
|
||||||
=> Task.FromResult<Quote?>(null);
|
=> Task.FromResult<Quote?>(null);
|
||||||
|
|
||||||
public Task<AccountState> GetAccountStateAsync(CancellationToken ct = default)
|
public Task<AccountState> GetAccountStateAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ internal static class Program
|
|||||||
foreach (var p in positions.Take(10))
|
foreach (var p in positions.Take(10))
|
||||||
Console.WriteLine($" {p}");
|
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
|
Console.WriteLine(quote is null
|
||||||
? $"Kurs {symbol}: keiner geliefert (ohne Marktdaten-Abo bei geschlossener Börse normal)"
|
? $"Kurs {symbol}: keiner geliefert (ohne Marktdaten-Abo bei geschlossener Börse normal)"
|
||||||
: $"Kurs {symbol}: {quote}");
|
: $"Kurs {symbol}: {quote}");
|
||||||
|
|||||||
@@ -84,13 +84,12 @@ internal static class VerifyOrder
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Die Kursabfrage laeuft weiterhin ueber das Symbol; der Adapter loest den Kontrakt
|
var quote = await broker.GetQuoteAsync(instrument, cts.Token).ConfigureAwait(false);
|
||||||
// anhand des Instruments samt Waehrung auf.
|
|
||||||
var quote = await broker.GetQuoteAsync(symbol, cts.Token).ConfigureAwait(false);
|
|
||||||
if (quote is null || quote.Last <= 0)
|
if (quote is null || quote.Last <= 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"ABBRUCH: kein Kurs für {symbol}. Bei geschlossener Börse und ohne " +
|
Console.Error.WriteLine($"ABBRUCH: kein Kurs für {instrument}. Entweder ist der Kontrakt " +
|
||||||
$"Marktdaten-Abo ist das normal – dann später erneut versuchen.");
|
$"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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class ExecutionServiceTests
|
|||||||
private void ArrangeHappyPath()
|
private void ArrangeHappyPath()
|
||||||
{
|
{
|
||||||
EnableTrading();
|
EnableTrading();
|
||||||
_broker.GetQuoteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
_broker.GetQuoteAsync(Arg.Any<InstrumentRef>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult<Quote?>(new Quote("AAPL", 100m, 99m, 101m)));
|
.Returns(Task.FromResult<Quote?>(new Quote("AAPL", 100m, 99m, 101m)));
|
||||||
_broker.GetAccountStateAsync(Arg.Any<CancellationToken>())
|
_broker.GetAccountStateAsync(Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));
|
.Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));
|
||||||
@@ -87,7 +87,7 @@ public class ExecutionServiceTests
|
|||||||
public async Task NoQuote_Skips()
|
public async Task NoQuote_Skips()
|
||||||
{
|
{
|
||||||
EnableTrading();
|
EnableTrading();
|
||||||
_broker.GetQuoteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
_broker.GetQuoteAsync(Arg.Any<InstrumentRef>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult<Quote?>(null));
|
.Returns(Task.FromResult<Quote?>(null));
|
||||||
|
|
||||||
var result = await CreateSut().ExecuteAsync(BuySignal);
|
var result = await CreateSut().ExecuteAsync(BuySignal);
|
||||||
@@ -100,7 +100,7 @@ public class ExecutionServiceTests
|
|||||||
public async Task RiskRejects_Skips_WithReason()
|
public async Task RiskRejects_Skips_WithReason()
|
||||||
{
|
{
|
||||||
EnableTrading();
|
EnableTrading();
|
||||||
_broker.GetQuoteAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
_broker.GetQuoteAsync(Arg.Any<InstrumentRef>(), Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult<Quote?>(new Quote("AAPL", 100m, 99m, 101m)));
|
.Returns(Task.FromResult<Quote?>(new Quote("AAPL", 100m, 99m, 101m)));
|
||||||
_broker.GetAccountStateAsync(Arg.Any<CancellationToken>())
|
_broker.GetAccountStateAsync(Arg.Any<CancellationToken>())
|
||||||
.Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));
|
.Returns(Task.FromResult(new AccountState(10_000m, 10_000m)));
|
||||||
|
|||||||
Reference in New Issue
Block a user