Nach jedem Verbindungsaufbau lief ein weiterer IBKR-Lesethread mit: Die Schleifenbedingung "while (_socket.IsConnected())" ist nach einem Reconnect wieder wahr, weil dasselbe Socket-Objekt neu verbunden wird. Auf der Test-VM waren es nach zwei TWS-Neustarts drei Threads gleichzeitig. Alle warten auf demselben EReaderMonitorSignal, und ein Signal weckt genau einen. Weckt es den falschen, bleibt die Nachricht in der Warteschlange eines anderen Readers liegen. Bei laufend wiederholten Push-Daten faellt das nicht auf, die einmalige Antwort auf reqPositions laeuft in die Frist - der Dienst sieht gesund aus und ist blind. Zeitueberschreitungen je Tag: 1, 3, 215, 96. Jeder Lesethread bekommt jetzt eine Generationsnummer. Veraltete beenden sich beim naechsten Aufwachen und reichen das verbrauchte Signal weiter, damit die Nachricht des Nachfolgers nicht liegen bleibt. Dazu ein Zwischenspeicher fuer die Handelszeiten (#505): dieselbe Auskunft wurde viermal je Stunde und Instrument neu angefragt, obwohl sie sich hoechstens taeglich aendert. Gueltig bis zum Ende des gemeldeten Zeitraums; Fehlschlaege werden bewusst nicht gemerkt, sonst bliebe ein Modul nach einer einzigen Stoerung bis zum Neustart stumm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
@@ -26,6 +27,10 @@ public sealed class IbkrBrokerClient
|
||||
private readonly TimeSpan _requestTimeout;
|
||||
private readonly TimeSpan _orderTimeout;
|
||||
|
||||
// Handelszeiten je Instrument, bis zum Ende des gemeldeten Zeitraums gueltig (#505).
|
||||
private readonly ConcurrentDictionary<string, MarketSchedule> _schedules =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IbkrBrokerClient(SettingsService settings, LoggingService logger)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -113,11 +118,38 @@ public sealed class IbkrBrokerClient
|
||||
return await _connection!.RequestExecutionsAsync(since, _requestTimeout, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handelszeiten des Instruments, zwischengespeichert bis zum Ende des gemeldeten Zeitraums.
|
||||
///
|
||||
/// <para><b>Warum ueberhaupt ein Zwischenspeicher.</b> Ein Modul im 15-Minuten-Takt fragte sonst
|
||||
/// dieselbe Auskunft viermal je Stunde und Instrument neu ab, obwohl sie sich hoechstens
|
||||
/// taeglich aendert. Jede dieser Anfragen belegt einen Antwortplatz beim Broker und kann in die
|
||||
/// Zeitueberschreitung laufen – Aufwand ohne Ertrag.</para>
|
||||
///
|
||||
/// <para><b>Fehlschlaege werden nicht gemerkt.</b> Ein <c>null</c> heisst „keine Auskunft" und
|
||||
/// gilt beim Aufrufer als geschlossen. Wuerde das haengenbleiben, bliebe ein Modul nach einer
|
||||
/// einzigen Stoerung bis zum Neustart stumm.</para>
|
||||
/// </summary>
|
||||
public async Task<MarketSchedule?> GetScheduleAsync(InstrumentRef instrument, CancellationToken ct = default)
|
||||
{
|
||||
var key = instrument.Key;
|
||||
|
||||
// Gueltig, solange der gemeldete Zeitraum noch in der Zukunft endet. Danach wuerde die
|
||||
// Auskunft still veralten - und "gestern war offen" ist die gefaehrliche Richtung.
|
||||
if (_schedules.TryGetValue(key, out var cached) &&
|
||||
cached.Sessions.Count > 0 &&
|
||||
cached.Sessions[^1].CloseUtc > DateTime.UtcNow)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return null;
|
||||
|
||||
return await _connection!.RequestMarketScheduleAsync(instrument, _requestTimeout, ct).ConfigureAwait(false);
|
||||
var schedule = await _connection!.RequestMarketScheduleAsync(instrument, _requestTimeout, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (schedule is not null) _schedules[key] = schedule;
|
||||
return schedule;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BrokerOrder>> GetOpenOrdersAsync(CancellationToken ct = default)
|
||||
|
||||
Reference in New Issue
Block a user