From 2847778c7ca1b3718b95f73bc42d85583f8a5e41 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 14 Sep 2025 22:27:54 +0700 Subject: [PATCH] Update pricing timing --- .../Grains/IPriceFetcher15MinGrain.cs | 18 -- .../Grains/IPriceFetcher1DayGrain.cs | 19 -- .../Grains/IPriceFetcher1HourGrain.cs | 19 -- .../Grains/IPriceFetcher4HourGrain.cs | 19 -- .../GrainHelpersTests.cs | 26 ++ .../Bots/Grains/AgentGrain.cs | 7 +- .../Bots/Grains/LiveTradingBotGrain.cs | 88 ++++--- .../Grains/PriceFetcher15MinGrain.cs | 229 ------------------ .../Grains/PriceFetcherInitializer.cs | 25 +- .../Shared/GrainHelpers.cs | 46 ++++ 10 files changed, 145 insertions(+), 351 deletions(-) delete mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher15MinGrain.cs delete mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs delete mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs delete mode 100644 src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs create mode 100644 src/Managing.Application.Tests/GrainHelpersTests.cs delete mode 100644 src/Managing.Application/Grains/PriceFetcher15MinGrain.cs create mode 100644 src/Managing.Application/Shared/GrainHelpers.cs diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher15MinGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher15MinGrain.cs deleted file mode 100644 index d0aa6438..00000000 --- a/src/Managing.Application.Abstractions/Grains/IPriceFetcher15MinGrain.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Orleans; - -namespace Managing.Application.Abstractions.Grains; - -/// -/// Orleans grain interface for 5-minute price fetching operations. -/// This stateless worker grain handles fetching 5-minute price data from external APIs -/// and publishing to Orleans streams. -/// -public partial interface IPriceFetcher15MinGrain : IGrainWithIntegerKey -{ - /// - /// Fetches 5-minute price data for all supported exchange/ticker combinations - /// and publishes new candles to their respective streams. - /// - /// True if the operation completed successfully, false otherwise - Task FetchAndPublishPricesAsync(); -} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs deleted file mode 100644 index 2cf71826..00000000 --- a/src/Managing.Application.Abstractions/Grains/IPriceFetcher1DayGrain.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Orleans; - -namespace Managing.Application.Abstractions.Grains; - -/// -/// Orleans grain interface for daily price fetching operations. -/// This stateless worker grain handles fetching daily price data from external APIs -/// and publishing to Orleans streams. -/// -public interface IPriceFetcher1DayGrain : IGrainWithIntegerKey -{ - /// - /// Fetches daily price data for all supported exchange/ticker combinations - /// and publishes new candles to their respective streams. - /// - /// True if the operation completed successfully, false otherwise - Task FetchAndPublishPricesAsync(); -} - diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs deleted file mode 100644 index 06396133..00000000 --- a/src/Managing.Application.Abstractions/Grains/IPriceFetcher1HourGrain.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Orleans; - -namespace Managing.Application.Abstractions.Grains; - -/// -/// Orleans grain interface for 1-hour price fetching operations. -/// This stateless worker grain handles fetching 1-hour price data from external APIs -/// and publishing to Orleans streams. -/// -public interface IPriceFetcher1HourGrain : IGrainWithIntegerKey -{ - /// - /// Fetches 1-hour price data for all supported exchange/ticker combinations - /// and publishes new candles to their respective streams. - /// - /// True if the operation completed successfully, false otherwise - Task FetchAndPublishPricesAsync(); -} - diff --git a/src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs b/src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs deleted file mode 100644 index ecbd9bf8..00000000 --- a/src/Managing.Application.Abstractions/Grains/IPriceFetcher4HourGrain.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Orleans; - -namespace Managing.Application.Abstractions.Grains; - -/// -/// Orleans grain interface for 4-hour price fetching operations. -/// This stateless worker grain handles fetching 4-hour price data from external APIs -/// and publishing to Orleans streams. -/// -public interface IPriceFetcher4HourGrain : IGrainWithIntegerKey -{ - /// - /// Fetches 4-hour price data for all supported exchange/ticker combinations - /// and publishes new candles to their respective streams. - /// - /// True if the operation completed successfully, false otherwise - Task FetchAndPublishPricesAsync(); -} - diff --git a/src/Managing.Application.Tests/GrainHelpersTests.cs b/src/Managing.Application.Tests/GrainHelpersTests.cs new file mode 100644 index 00000000..bfc8a955 --- /dev/null +++ b/src/Managing.Application.Tests/GrainHelpersTests.cs @@ -0,0 +1,26 @@ +using Managing.Application.Shared; +using Xunit; +using static Managing.Common.Enums; + +namespace Managing.Application.Tests; + +public class GrainHelpersTests +{ + [Fact] + public void GetIntervalMinutes_FifteenMinutes_ShouldReturn75() + { + var result = GrainHelpers.GetIntervalMinutes(Timeframe.FifteenMinutes); + Assert.Equal(7.5, result); + } + + [Fact] + public void GetRandomizedTimerOptions_OneHour_ShouldReturn30() + { + var result = GrainHelpers.GetDynamicRandomizedTimerOptions(TimeSpan.FromMinutes(1), 200); + Assert.True(result.period.TotalSeconds <= 60.0); + Assert.True(result.dueTime.TotalSeconds <= 60.0); + + Assert.True(result.period.TotalSeconds > 0.0); + Assert.True(result.dueTime.TotalSeconds > 0.0); + } +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs index f90f0696..b80d6ecb 100644 --- a/src/Managing.Application/Bots/Grains/AgentGrain.cs +++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs @@ -2,6 +2,7 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Bots.Models; +using Managing.Application.Shared; using Managing.Domain.Statistics; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -60,9 +61,11 @@ public class AgentGrain : Grain, IAgentGrain, IRemindable { try { + var options = GrainHelpers.GetDynamicRandomizedTimerOptions(TimeSpan.FromMinutes(2), 200); + // Register a reminder that fires every 5 minutes - await this.RegisterOrUpdateReminder(_updateSummaryReminderName, TimeSpan.FromMinutes(5), - TimeSpan.FromMinutes(1)); + await this.RegisterOrUpdateReminder(_updateSummaryReminderName, options.dueTime, + options.period); _logger.LogInformation("Reminder registered for agent {UserId} to update summary every 5 minutes", this.GetPrimaryKeyLong()); } diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index ca6aa8ad..3f7a8ee1 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -1,6 +1,7 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; +using Managing.Application.Shared; using Managing.Common; using Managing.Core; using Managing.Domain.Accounts; @@ -120,15 +121,15 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable // Set startup time when bot actually starts running _state.State.StartupTime = DateTime.UtcNow; await _state.WriteStateAsync(); - + // Start the in-memory timer and persistent reminder RegisterAndStartTimer(); await RegisterReminder(); - + // Update both database and registry status await SaveBotAsync(BotStatus.Running); await UpdateBotRegistryStatus(BotStatus.Running); - + _logger.LogInformation("LiveTradingBotGrain {GrainId} resumed successfully", this.GetPrimaryKey()); } catch (Exception ex) @@ -184,12 +185,14 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable if (_timer != null) return; + var botOptions = GrainHelpers.GetDynamicRandomizedTimerOptions(TimeSpan.FromMinutes(1), 20); + _timer = this.RegisterGrainTimer( async _ => await ExecuteBotCycle(), new GrainTimerCreationOptions { - Period = TimeSpan.FromMinutes(1), - DueTime = TimeSpan.FromMinutes(1), + Period = botOptions.period, + DueTime = botOptions.dueTime, KeepAlive = true }); } @@ -294,21 +297,18 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable } // Check broker balance before running - var balances = await ServiceScopeHelpers.WithScopedService>(_scopeFactory, async exchangeService => - { - return await exchangeService.GetBalances(_tradingBot.Account, false); - - }); + var balances = await ServiceScopeHelpers.WithScopedService>(_scopeFactory, + async exchangeService => { return await exchangeService.GetBalances(_tradingBot.Account, false); }); var usdcBalance = balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString()); var ethBalance = balances.FirstOrDefault(b => b.TokenName == Ticker.ETH.ToString()); - + // Check USDC balance first if (usdcBalance?.Value < Constants.GMX.Config.MinimumPositionAmount) { await _tradingBot.LogWarning( $"USDC balance is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {usdcBalance?.Value:F2}). Stopping bot {_tradingBot.Identifier}."); - + await StopAsync(); return; } @@ -319,16 +319,19 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { await _tradingBot.LogWarning( $"ETH balance is below 2 USD (actual: {ethValueInUsd:F2}). Attempting to swap USDC to ETH."); - + // Check if we have enough USDC for the swap if (usdcBalance?.Value >= 5) // Need at least 5 USD for swap { try { - var swapInfo = await ServiceScopeHelpers.WithScopedService(_scopeFactory, async accountService => - { - return await accountService.SwapGmxTokensAsync(_state.State.User, _tradingBot.Account.Name, Ticker.USDC, Ticker.ETH, 5); - }); + var swapInfo = await ServiceScopeHelpers.WithScopedService( + _scopeFactory, + async accountService => + { + return await accountService.SwapGmxTokensAsync(_state.State.User, + _tradingBot.Account.Name, Ticker.USDC, Ticker.ETH, 5); + }); if (swapInfo.Success) { @@ -351,7 +354,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable // Both USDC and ETH are low - stop the strategy await _tradingBot.LogWarning( $"Both USDC ({usdcBalance?.Value:F2}) and ETH ({ethValueInUsd:F2}) balances are low. Stopping bot {_tradingBot.Identifier}."); - + await StopAsync(); return; } @@ -395,7 +398,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { _logger.LogInformation("LastCandle is null, loading latest candle data for manual position opening"); await _tradingBot.LoadLastCandle(); - + // Sync the loaded candle to grain state SyncStateFromBase(); await _state.WriteStateAsync(); @@ -480,7 +483,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable // For non-running bots, just update the configuration _state.State.Config = newConfig; await _state.WriteStateAsync(); - + var botRegistry = GrainFactory.GetGrain(0); var status = await botRegistry.GetBotStatus(this.GetPrimaryKey()); await SaveBotAsync(status); @@ -507,6 +510,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { throw new InvalidOperationException("Bot is not running - cannot get account information"); } + return Task.FromResult(_tradingBot.Account); } @@ -543,21 +547,21 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable public async Task RestartAsync() { _logger.LogInformation("Restarting LiveTradingBotGrain {GrainId}", this.GetPrimaryKey()); - + try { await StopAsync(); - + // Add a small delay to ensure stop operations complete await Task.Delay(100); - + await StartAsync(); - + // Verify the restart was successful var botRegistry = GrainFactory.GetGrain(0); var finalStatus = await botRegistry.GetBotStatus(this.GetPrimaryKey()); - - _logger.LogInformation("LiveTradingBotGrain {GrainId} restart completed with final status: {Status}", + + _logger.LogInformation("LiveTradingBotGrain {GrainId} restart completed with final status: {Status}", this.GetPrimaryKey(), finalStatus); } catch (Exception ex) @@ -605,39 +609,42 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { const int maxRetries = 3; var botId = this.GetPrimaryKey(); - + for (int attempt = 1; attempt <= maxRetries; attempt++) { try { var botRegistry = GrainFactory.GetGrain(0); await botRegistry.UpdateBotStatus(botId, status); - + // Verify the update was successful var actualStatus = await botRegistry.GetBotStatus(botId); if (actualStatus == status) { - _logger.LogDebug("Bot {BotId} status successfully updated to {Status} in BotRegistry (attempt {Attempt})", + _logger.LogDebug( + "Bot {BotId} status successfully updated to {Status} in BotRegistry (attempt {Attempt})", botId, status, attempt); return; } else { - _logger.LogWarning("Bot {BotId} status update verification failed. Expected: {Expected}, Actual: {Actual} (attempt {Attempt})", + _logger.LogWarning( + "Bot {BotId} status update verification failed. Expected: {Expected}, Actual: {Actual} (attempt {Attempt})", botId, status, actualStatus, attempt); } } catch (Exception ex) { - _logger.LogError(ex, "Failed to update bot {BotId} status to {Status} in BotRegistry (attempt {Attempt})", + _logger.LogError(ex, + "Failed to update bot {BotId} status to {Status} in BotRegistry (attempt {Attempt})", botId, status, attempt); - + if (attempt == maxRetries) { throw; } } - + // Wait before retry if (attempt < maxRetries) { @@ -734,7 +741,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable { _logger.LogDebug( "Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}, Long={LongPositions}, Short={ShortPositions}", - _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees, bot.LongPositionCount, bot.ShortPositionCount); + _state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees, + bot.LongPositionCount, bot.ShortPositionCount); } else { @@ -750,7 +758,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable /// /// Notifies the user about swap operations via webhook/telegram /// - private async Task NotifyUserAboutSwap(bool isSuccess, decimal amount, string? transactionHash, string? errorMessage = null) + private async Task NotifyUserAboutSwap(bool isSuccess, decimal amount, string? transactionHash, + string? errorMessage = null) { try { @@ -769,10 +778,11 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable $"⏰ **Time:** {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"; // Send notification via webhook service - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async webhookService => - { - await webhookService.SendMessage(message, _state.State.User?.TelegramChannel); - }); + await ServiceScopeHelpers.WithScopedService(_scopeFactory, + async webhookService => + { + await webhookService.SendMessage(message, _state.State.User?.TelegramChannel); + }); } catch (Exception ex) { diff --git a/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs b/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs deleted file mode 100644 index 3d77f7cf..00000000 --- a/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs +++ /dev/null @@ -1,229 +0,0 @@ -using Managing.Application.Abstractions.Grains; -using Managing.Application.Abstractions.Repositories; -using Managing.Application.Abstractions.Services; -using Managing.Common; -using Managing.Domain.Accounts; -using Managing.Domain.Candles; -using Microsoft.Extensions.Logging; -using Orleans.Concurrency; -using Orleans.Streams; -using static Managing.Common.Enums; - -namespace Managing.Application.Grains; - -/// -/// StatelessWorker grain for fetching 5-minute price data from external APIs and publishing to Orleans streams. -/// This grain runs every 5 minutes and processes all exchange/ticker combinations for the 5-minute timeframe. -/// -[StatelessWorker] -public class PriceFetcher15MinGrain : Grain, IPriceFetcher15MinGrain, IRemindable -{ - private readonly ILogger _logger; - private readonly IExchangeService _exchangeService; - private readonly ICandleRepository _candleRepository; - private readonly IGrainFactory _grainFactory; - - private const string FetchPricesReminderName = "Fetch15minPricesReminder"; - private IDisposable _timer; - - // Predefined lists of trading parameters to fetch - private static readonly TradingExchanges[] SupportedExchanges = - { - TradingExchanges.Evm - }; - - private static readonly Ticker[] SupportedTickers = Constants.GMX.Config.SupportedTickers; - - private static readonly Timeframe TargetTimeframe = Timeframe.FifteenMinutes; - - public PriceFetcher15MinGrain( - ILogger logger, - IExchangeService exchangeService, - ICandleRepository candleRepository, - IGrainFactory grainFactory) - { - _logger = logger; - _exchangeService = exchangeService; - _candleRepository = candleRepository; - _grainFactory = grainFactory; - } - - public override async Task OnActivateAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("{0} activated", nameof(PriceFetcher15MinGrain)); - - // Register a reminder to enable timer if not existing - await this.RegisterOrUpdateReminder( - FetchPricesReminderName, - TimeSpan.FromSeconds(5), - TimeSpan.FromMinutes(7.5)); - - await base.OnActivateAsync(cancellationToken); - } - - public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken) - { - _logger.LogInformation("{0} deactivating. Reason: {Reason}", - nameof(PriceFetcher15MinGrain), reason.Description); - - StopAndDisposeTimer(); - await base.OnDeactivateAsync(reason, cancellationToken); - } - - public async Task FetchAndPublishPricesAsync() - { - try - { - _logger.LogInformation("Starting {timeframe} price fetch cycle", TargetTimeframe); - - var fetchTasks = new List(); - - // Create fetch tasks for all exchange/ticker combinations for 15-minute timeframe - foreach (var exchange in SupportedExchanges) - { - foreach (var ticker in SupportedTickers) - { - fetchTasks.Add(FetchAndPublish(exchange, ticker, TargetTimeframe)); - } - } - - // Execute all fetch operations in parallel - await Task.WhenAll(fetchTasks); - - _logger.LogInformation("{0} - Completed {1} price fetch cycle for {2} combinations", - nameof(PriceFetcher15MinGrain), TargetTimeframe, fetchTasks.Count); - - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during price fetch cycle"); - return false; - } - } - - private async Task FetchAndPublish(TradingExchanges exchange, Ticker ticker, Timeframe timeframe) - { - try - { - // Create a dummy account for API calls (this may need to be adjusted based on your implementation) - var account = new Account - { - Name = "PriceFetcher", - Exchange = exchange, - Type = AccountType.Watch - }; - - // Get the last candle date from database - var existingCandles = await _candleRepository.GetCandles(exchange, ticker, timeframe, - DateTime.UtcNow.AddDays(-30), DateTime.UtcNow.AddDays(1), 20); - - var isFirstCall = !existingCandles.Any(); - - var startDate = !isFirstCall - ? existingCandles.Last().Date - : new DateTime(2017, 1, 1); - - // Fetch new candles from external API - var newCandles = await _exchangeService.GetCandles(account, ticker, startDate, timeframe, isFirstCall); - - if (newCandles?.Any() == true) - { - var streamKey = CandleHelpers.GetCandleStoreGrainKey(exchange, ticker, timeframe); - - _logger.LogDebug("Fetched {CandleCount} new candles for {StreamKey}", - newCandles.Count, streamKey); - - // Process all new candles - var processedCandles = newCandles.OrderBy(c => c.Date) - .Where(c => c.Date <= DateTime.UtcNow) // Avoid duplicates - .ToList(); - - foreach (var candle in processedCandles) - { - // Ensure candle has correct metadata - candle.Exchange = exchange; - candle.Ticker = ticker; - candle.Timeframe = timeframe; - } - - // Save all candles to database in a single batch - if (processedCandles.Any()) - { - await _candleRepository.InsertCandles(processedCandles).ConfigureAwait(false); - - _logger.LogDebug( - "[{Ticker}][{Exchange}][{Timeframe}] Inserted {CandleCount} candles for {StreamKey}", - ticker, exchange, timeframe, - processedCandles.Count, streamKey); - } - - var streamProvider = this.GetStreamProvider("DefaultStreamProvider"); - var stream = streamProvider.GetStream(streamKey); - - // Publish to stream (if needed) - foreach (var candle in processedCandles) - { - await stream.OnNextAsync(candle); - _logger.LogTrace("Published candle for {StreamKey} at {Date}", - streamKey, candle.Date); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching prices for {Exchange}-{Ticker}-{Timeframe}", - exchange, ticker, timeframe); - } - } - - /// - /// Starts the Orleans timer for periodic price fetching - /// - private void RegisterAndStartTimer() - { - if (_timer != null) return; - - // Calculate the next execution time aligned to X-minute boundaries - var now = DateTime.UtcNow; - var dueTime = CandleHelpers.GetDueTimeForTimeframe(TargetTimeframe, now); - var period = TimeSpan.FromSeconds(CandleHelpers.GetBaseIntervalInSeconds(TargetTimeframe)); - _logger.LogInformation("{0} next execution scheduled in {1} seconds and at {2:} UTC every {3} seconds", - nameof(PriceFetcher15MinGrain), dueTime, now.Add(dueTime), period); - - _timer = this.RegisterGrainTimer( - async _ => await FetchAndPublishPricesAsync(), - new GrainTimerCreationOptions - { - Period = period, - DueTime = dueTime, - KeepAlive = true - }); - - _logger.LogInformation("{0} timer registered and started", nameof(PriceFetcher15MinGrain)); - } - - private void StopAndDisposeTimer() - { - if (_timer != null) - { - _timer?.Dispose(); - _timer = null; - _logger.LogInformation("{0} timer stopped and disposed", nameof(PriceFetcher15MinGrain)); - } - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - if (reminderName == FetchPricesReminderName) - { - // Only enable timer if not existing anymore - if (_timer == null) - { - RegisterAndStartTimer(); - } - } - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Managing.Application/Grains/PriceFetcherInitializer.cs b/src/Managing.Application/Grains/PriceFetcherInitializer.cs index 36ec43af..ad8a13ef 100644 --- a/src/Managing.Application/Grains/PriceFetcherInitializer.cs +++ b/src/Managing.Application/Grains/PriceFetcherInitializer.cs @@ -1,5 +1,6 @@ using Managing.Application.Abstractions.Grains; using Microsoft.Extensions.Hosting; +using static Managing.Common.Enums; namespace Managing.Application.Grains; @@ -14,14 +15,26 @@ public class PriceFetcherInitializer : IHostedService public async Task StartAsync(CancellationToken cancellationToken) { - var fiveMinute = _grainFactory.GetGrain(0); - Console.WriteLine("GrainId : {0}", fiveMinute.GetGrainId()); + // Initialize grains for different timeframes + var timeframes = new[] + { + Timeframe.FifteenMinutes, + Timeframe.OneHour, + Timeframe.FourHour, + Timeframe.OneDay + }; - // Actually call a method on the grain to activate it - // This will trigger OnActivateAsync and register the reminder - await fiveMinute.FetchAndPublishPricesAsync(); + foreach (var timeframe in timeframes) + { + var grain = _grainFactory.GetGrain(timeframe.ToString()); + Console.WriteLine("GrainId for {0}: {1}", timeframe, grain.GetGrainId()); - Console.WriteLine("PriceFetcher5MinGrain activated and initial fetch completed"); + // Actually call a method on the grain to activate it + // This will trigger OnActivateAsync and register the reminder + await grain.FetchAndPublishPricesAsync(); + + Console.WriteLine("PriceFetcherGrain for {0} activated and initial fetch completed", timeframe); + } } public Task StopAsync(CancellationToken cancellationToken) diff --git a/src/Managing.Application/Shared/GrainHelpers.cs b/src/Managing.Application/Shared/GrainHelpers.cs new file mode 100644 index 00000000..dfe3d364 --- /dev/null +++ b/src/Managing.Application/Shared/GrainHelpers.cs @@ -0,0 +1,46 @@ +using Managing.Common; + +namespace Managing.Application.Shared; + +public static class GrainHelpers +{ + /// + /// Gets the interval in minutes for the reminder based on the timeframe + /// + public static double GetIntervalMinutes(Enums.Timeframe timeframe) + { + return timeframe switch + { + Enums.Timeframe.OneMinute => 1.0, + Enums.Timeframe.FiveMinutes => 5.0, + Enums.Timeframe.FifteenMinutes => 7.5, // Half of the timeframe for more frequent checks + Enums.Timeframe.OneHour => 30.0, // Half of the timeframe for more frequent checks + Enums.Timeframe.FourHour => 120.0, // Half of the timeframe for more frequent checks + Enums.Timeframe.OneDay => 720.0, // Half of the timeframe for more frequent checks + _ => 5.0 // Default to 5 minutes + }; + } + + // Helper method to get a randomized due time and period + public static (TimeSpan dueTime, TimeSpan period) GetDynamicRandomizedTimerOptions(TimeSpan basePeriod, + int numberOfBots) + { + var random = new Random(); + + // Determine the jitter percentage based on bot count + // Example: 2% jitter for 100 bots, 10% for 1000 bots, etc. + // This formula can be fine-tuned based on your system's performance metrics. + double jitterPercentage = Math.Min(0.20, (double)numberOfBots / 10000); // Caps at 20% for 10,000 bots + + // Calculate a random due time within the period + var dueTimeSeconds = random.Next(1, (int)basePeriod.TotalSeconds); + var dueTime = TimeSpan.FromSeconds(dueTimeSeconds); + + // Add a random jitter to the period + var jitterFactor = (random.NextDouble() * jitterPercentage) - (jitterPercentage / 2); + var periodSeconds = (int)(basePeriod.TotalSeconds * (1 + jitterFactor)); + var period = TimeSpan.FromSeconds(periodSeconds); + + return (dueTime, period); + } +} \ No newline at end of file