From 3a48a7fa421b32aa38af2e650da4ccd4bd4b3206 Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 11 Sep 2025 13:18:28 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #183 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Bot/issues/183 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5aae2a3a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Bot/issues/183 +Your prepared branch: issue-183-f7d0021d +Your prepared working directory: /tmp/gh-issue-solver-1757585904449 + +Proceed. \ No newline at end of file From aa1323f4a0a842b0e198ac50175103f2a2b0828a Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 11 Sep 2025 13:18:46 +0300 Subject: [PATCH 2/3] Remove CLAUDE.md - PR created successfully --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 5aae2a3a..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/linksplatform/Bot/issues/183 -Your prepared branch: issue-183-f7d0021d -Your prepared working directory: /tmp/gh-issue-solver-1757585904449 - -Proceed. \ No newline at end of file From b52de5464fbf11194dcd0bbd907d81d478907bec Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 11 Sep 2025 13:23:40 +0300 Subject: [PATCH 3/3] Add timer-based subscription restart mechanism for trader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SubscriptionTimeoutInterval (5 minutes) to detect silent subscription failures - Track LastTradesDataTicks and LastMarketDataTicks for both subscription streams - Implement CheckTradesTimeout and CheckMarketDataTimeout methods to monitor data flow - Update ReceiveTradesLoop and SendOrdersLoop to restart subscriptions when no data received - Fix existing Task.Delay calls to properly handle cancellation tokens Fixes issue where trades subscription was broken after 17 days of uptime. The bot now proactively restarts subscriptions if no data is received within the timeout period. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- csharp/TraderBot/TradingService.cs | 102 +++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/csharp/TraderBot/TradingService.cs b/csharp/TraderBot/TradingService.cs index 0302809b..26a957a6 100644 --- a/csharp/TraderBot/TradingService.cs +++ b/csharp/TraderBot/TradingService.cs @@ -19,6 +19,7 @@ public class TradingService : BackgroundService protected static readonly TimeSpan RefreshInterval = TimeSpan.FromSeconds(10); protected static readonly TimeSpan SyncInterval = TimeSpan.FromSeconds(20); protected static readonly TimeSpan WaitOutputInterval = TimeSpan.FromSeconds(20); + protected static readonly TimeSpan SubscriptionTimeoutInterval = TimeSpan.FromMinutes(5); protected readonly InvestApiClient InvestApi; protected readonly ILogger Logger; protected readonly IHostApplicationLifetime Lifetime; @@ -33,6 +34,8 @@ public class TradingService : BackgroundService protected long LastRefreshTicks; protected long LastSyncTicks; protected long LastWaitOutputTicks; + protected long LastTradesDataTicks; + protected long LastMarketDataTicks; protected TimeSpan MinimumTimeToBuy; protected TimeSpan MaximumTimeToBuy; protected readonly ConcurrentDictionary ActiveBuyOrders; @@ -112,6 +115,9 @@ public TradingService(ILogger logger, InvestApiClient investApi, LotsSets = new ConcurrentDictionary(); ActiveSellOrderSourcePrice = new ConcurrentDictionary(); LastOperationsCheckpoint = settings.LoadOperationsFrom; + var nowTicks = DateTime.UtcNow.Ticks; + LastTradesDataTicks = nowTicks; + LastMarketDataTicks = nowTicks; } protected async Task ReceiveTrades(CancellationToken cancellationToken) @@ -122,6 +128,7 @@ protected async Task ReceiveTrades(CancellationToken cancellationToken) }); await foreach (var data in tradesStream.ResponseStream.ReadAllAsync(cancellationToken)) { + Interlocked.Exchange(ref LastTradesDataTicks, DateTime.UtcNow.Ticks); Logger.LogInformation($"Trade: {data}"); if (data.PayloadCase == TradesStreamResponse.PayloadOneofCase.OrderTrades) { @@ -349,14 +356,40 @@ protected async Task SendOrdersLoop(CancellationToken cancellationToken) try { await Refresh(forceReset: true); - await SendOrders(cancellationToken); + + using var timeoutCancellationTokenSource = new CancellationTokenSource(); + using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); + + var sendOrdersTask = SendOrders(combinedCancellationTokenSource.Token); + var timeoutTask = CheckMarketDataTimeout(timeoutCancellationTokenSource, cancellationToken); + + await Task.WhenAny(sendOrdersTask, timeoutTask); + + if (timeoutTask.IsCompleted && !timeoutTask.IsCanceled) + { + Logger.LogWarning("Market data subscription timeout detected, restarting subscription."); + timeoutCancellationTokenSource.Cancel(); + } + + try + { + await sendOrdersTask; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (OperationCanceledException) + { + Logger.LogInformation("Market data subscription cancelled due to timeout, will restart."); + } } catch (Exception ex) { if (!cancellationToken.IsCancellationRequested) { Logger.LogError(ex, "SendOrders exception."); - await Task.Delay(RecoveryInterval); + await Task.Delay(RecoveryInterval, cancellationToken); } } } @@ -369,19 +402,79 @@ protected async Task ReceiveTradesLoop(CancellationToken cancellationToken) try { await Refresh(forceReset: true); - await ReceiveTrades(cancellationToken); + + using var timeoutCancellationTokenSource = new CancellationTokenSource(); + using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); + + var receiveTradesTask = ReceiveTrades(combinedCancellationTokenSource.Token); + var timeoutTask = CheckTradesTimeout(timeoutCancellationTokenSource, cancellationToken); + + await Task.WhenAny(receiveTradesTask, timeoutTask); + + if (timeoutTask.IsCompleted && !timeoutTask.IsCanceled) + { + Logger.LogWarning("Trades subscription timeout detected, restarting subscription."); + timeoutCancellationTokenSource.Cancel(); + } + + try + { + await receiveTradesTask; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (OperationCanceledException) + { + Logger.LogInformation("Trades subscription cancelled due to timeout, will restart."); + } } catch (Exception ex) { if (!cancellationToken.IsCancellationRequested) { Logger.LogError(ex, "ReceiveTrades exception."); - await Task.Delay(RecoveryInterval); + await Task.Delay(RecoveryInterval, cancellationToken); } } } } + protected async Task CheckTradesTimeout(CancellationTokenSource timeoutCancellationTokenSource, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && !timeoutCancellationTokenSource.Token.IsCancellationRequested) + { + var nowTicks = DateTime.UtcNow.Ticks; + var lastDataTicks = Interlocked.Read(ref LastTradesDataTicks); + + if (nowTicks - lastDataTicks > SubscriptionTimeoutInterval.Ticks) + { + Logger.LogWarning($"No trades data received for {SubscriptionTimeoutInterval.TotalMinutes} minutes, triggering restart."); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + } + } + + protected async Task CheckMarketDataTimeout(CancellationTokenSource timeoutCancellationTokenSource, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && !timeoutCancellationTokenSource.Token.IsCancellationRequested) + { + var nowTicks = DateTime.UtcNow.Ticks; + var lastDataTicks = Interlocked.Read(ref LastMarketDataTicks); + + if (nowTicks - lastDataTicks > SubscriptionTimeoutInterval.Ticks) + { + Logger.LogWarning($"No market data received for {SubscriptionTimeoutInterval.TotalMinutes} minutes, triggering restart."); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + } + } + protected async Task SendOrders(CancellationToken cancellationToken) { var marketDataStream = InvestApi.MarketDataStream.MarketDataStream(); @@ -402,6 +495,7 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest }, cancellationToken); await foreach (var data in marketDataStream.ResponseStream.ReadAllAsync(cancellationToken)) { + Interlocked.Exchange(ref LastMarketDataTicks, DateTime.UtcNow.Ticks); // Logger.LogInformation($"data.PayloadCase: {data.PayloadCase}"); if (data.PayloadCase == MarketDataResponse.PayloadOneofCase.SubscribeOrderBookResponse) {