From daeb26375b55a51a421f43e26c4a29eb8d0f2edc Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 14 Sep 2025 21:09:34 +0700 Subject: [PATCH] Add performance for price reminder --- .../CandleHelpersTests.cs | 73 ++++++++++++++++--- .../Grains/PriceFetcher15MinGrain.cs | 15 +++- src/Managing.Core/DateHelpers.cs | 2 +- src/Managing.Core/Managing.Core.csproj | 30 +++++--- src/Managing.Domain/Candles/CandleHelpers.cs | 68 ++++++++++++++++- 5 files changed, 160 insertions(+), 28 deletions(-) diff --git a/src/Managing.Application.Tests/CandleHelpersTests.cs b/src/Managing.Application.Tests/CandleHelpersTests.cs index 41510458..fe784de8 100644 --- a/src/Managing.Application.Tests/CandleHelpersTests.cs +++ b/src/Managing.Application.Tests/CandleHelpersTests.cs @@ -1,20 +1,75 @@ -using Xunit; +using Managing.Domain.Candles; +using Xunit; +using static Managing.Common.Enums; namespace Managing.Application.Tests { public class CandleHelpersTests { [Fact] - public void Shoud_Result_Correct_Range_Date() + public void GetDueTimeForTimeframe_FifteenMinutes_ShouldReturnCorrectTimeSpan() { - // Arrange - var expectedDate = DateTime.Now.AddMinutes(-15*5); - var currentCandleDate = DateTime.Now; - var previousCandleDate = DateTime.Now.AddMinutes(-15); - + // Arrange + var timeframe = Timeframe.FifteenMinutes; + var testTime = new DateTime(2024, 1, 1, 16, 42, 36, DateTimeKind.Utc); // 16:42:36 + // Act - //var result = CandleHelpers.GetRangeDateFromTimeframe + var dueTime = CandleHelpers.GetDueTimeForTimeframe(timeframe, testTime); + // Assert + Assert.True(dueTime.TotalMinutes > 0, "Due time should be positive"); + Assert.True(dueTime.TotalMinutes < 15, "Due time should be less than the timeframe interval"); + + // Next 15-minute boundary is 16:45:00, so execution should be at 16:45:01 + // Time difference should be approximately 2 minutes and 25 seconds + var expectedMinutes = 2; + var expectedSeconds = 25; + var expectedTotalSeconds = expectedMinutes * 60 + expectedSeconds; + + Assert.True(Math.Abs(dueTime.TotalSeconds - expectedTotalSeconds) <= 1, + $"Expected approximately {expectedMinutes}m {expectedSeconds}s, but got {dueTime.TotalMinutes:F2}m {dueTime.Seconds}s"); + + // Verify the next execution time + var nextExecution = testTime.Add(dueTime); + Assert.Equal(16, nextExecution.Hour); + Assert.Equal(45, nextExecution.Minute); + Assert.Equal(1, nextExecution.Second); + } + + [Fact] + public void GetDueTimeForTimeframe_FiveMinutes_ShouldAlignToFiveMinuteBoundaries() + { + // Arrange + var timeframe = Timeframe.FiveMinutes; + var testTime = new DateTime(2024, 1, 1, 10, 23, 45, DateTimeKind.Utc); // 10:23:45 + + // Act + var dueTime = CandleHelpers.GetDueTimeForTimeframe(timeframe, testTime); + + // Assert + var nextExecution = testTime.Add(dueTime); + // Should align to 10:25:01 (next 5-minute boundary + 1 second) + Assert.Equal(10, nextExecution.Hour); + Assert.Equal(25, nextExecution.Minute); + Assert.Equal(1, nextExecution.Second); + } + + [Fact] + public void GetDueTimeForTimeframe_OneHour_ShouldAlignToHourBoundaries() + { + // Arrange + var timeframe = Timeframe.OneHour; + var testTime = new DateTime(2024, 1, 1, 14, 30, 15, DateTimeKind.Utc); // 14:30:15 + + // Act + var dueTime = CandleHelpers.GetDueTimeForTimeframe(timeframe, testTime); + + // Assert + var nextExecution = testTime.Add(dueTime); + // Should align to 15:00:01 (next hour boundary + 1 second) + Assert.Equal(15, nextExecution.Hour); + Assert.Equal(0, nextExecution.Minute); + Assert.Equal(1, nextExecution.Second); } } -} +} \ No newline at end of file diff --git a/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs b/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs index aa94831a..3d77f7cf 100644 --- a/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs +++ b/src/Managing.Application/Grains/PriceFetcher15MinGrain.cs @@ -56,7 +56,7 @@ public class PriceFetcher15MinGrain : Grain, IPriceFetcher15MinGrain, IRemindabl await this.RegisterOrUpdateReminder( FetchPricesReminderName, TimeSpan.FromSeconds(5), - TimeSpan.FromMinutes(5)); + TimeSpan.FromMinutes(7.5)); await base.OnActivateAsync(cancellationToken); } @@ -184,12 +184,19 @@ public class PriceFetcher15MinGrain : Grain, IPriceFetcher15MinGrain, IRemindabl { 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 = TimeSpan.FromMinutes(15), - DueTime = TimeSpan.FromSeconds(1), + Period = period, + DueTime = dueTime, KeepAlive = true }); @@ -216,7 +223,7 @@ public class PriceFetcher15MinGrain : Grain, IPriceFetcher15MinGrain, IRemindabl RegisterAndStartTimer(); } } - + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Managing.Core/DateHelpers.cs b/src/Managing.Core/DateHelpers.cs index ac70177a..aa04d0a2 100644 --- a/src/Managing.Core/DateHelpers.cs +++ b/src/Managing.Core/DateHelpers.cs @@ -7,4 +7,4 @@ public class DateHelpers var dat_Time = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return dat_Time.AddSeconds(unixTimestamp); } -} +} \ No newline at end of file diff --git a/src/Managing.Core/Managing.Core.csproj b/src/Managing.Core/Managing.Core.csproj index 6926c459..feb4f5e4 100644 --- a/src/Managing.Core/Managing.Core.csproj +++ b/src/Managing.Core/Managing.Core.csproj @@ -1,17 +1,23 @@  - - net8.0 - enable - AnyCPU;x64 - + + net8.0 + enable + AnyCPU;x64 + - - - - - - - + + + + + + + + + + + ..\Managing.Api\bin\Debug\net8.0\Managing.Common.dll + + diff --git a/src/Managing.Domain/Candles/CandleHelpers.cs b/src/Managing.Domain/Candles/CandleHelpers.cs index b27e0de4..6aaa849f 100644 --- a/src/Managing.Domain/Candles/CandleHelpers.cs +++ b/src/Managing.Domain/Candles/CandleHelpers.cs @@ -96,9 +96,73 @@ public static class CandleHelpers return string.Join("-", exchange, ticker, timeframe); } - public static (TradingExchanges exchange, Ticker ticker, Timeframe timeframe) ParseCandleStoreGrainKey(string grainKey) + public static (TradingExchanges exchange, Ticker ticker, Timeframe timeframe) ParseCandleStoreGrainKey( + string grainKey) { var components = grainKey.Split('-'); - return (MiscExtensions.ParseEnum(components[0]), MiscExtensions.ParseEnum(components[1]), MiscExtensions.ParseEnum(components[2])); + return (MiscExtensions.ParseEnum(components[0]), + MiscExtensions.ParseEnum(components[1]), MiscExtensions.ParseEnum(components[2])); + } + + public static TimeSpan GetDueTimeForTimeframe(Timeframe timeframe, DateTime now) + { + var intervalMinutes = GetBaseIntervalInSeconds(timeframe) / 60; + + // Calculate the next candle boundary + var nextBoundary = GetNextCandleBoundary(now, intervalMinutes); + + // Add 1 second to ensure we're after the candle closes + var targetTime = nextBoundary.AddSeconds(1); + + // Return the time difference + return targetTime - now; + } + + private static DateTime GetNextCandleBoundary(DateTime now, double intervalMinutes) + { + // For different timeframes, we need to align to different boundaries + if (intervalMinutes == 1) // OneMinute + { + return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0).AddMinutes(1); + } + else if (intervalMinutes == 5) // FiveMinutes + { + var minute = (now.Minute / 5) * 5; + var boundary = new DateTime(now.Year, now.Month, now.Day, now.Hour, minute, 0); + return boundary.AddMinutes(5); + } + else if (intervalMinutes == 15) // FifteenMinutes + { + var minute = (now.Minute / 15) * 15; + var boundary = new DateTime(now.Year, now.Month, now.Day, now.Hour, minute, 0); + return boundary.AddMinutes(15); + } + else if (intervalMinutes == 30) // ThirtyMinutes + { + var minute = (now.Minute / 30) * 30; + var boundary = new DateTime(now.Year, now.Month, now.Day, now.Hour, minute, 0); + return boundary.AddMinutes(30); + } + else if (intervalMinutes == 60) // OneHour + { + var boundary = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0); + return boundary.AddHours(1); + } + else if (intervalMinutes == 240) // FourHour + { + var hour = (now.Hour / 4) * 4; + var boundary = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0); + return boundary.AddHours(4); + } + else if (intervalMinutes == 1440) // OneDay + { + var boundary = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0); + return boundary.AddDays(1); + } + + // Fallback to 5-minute intervals + var fallbackMinute = (now.Minute / 5) * 5; + var fallbackBoundary = new DateTime(now.Year, now.Month, now.Day, now.Hour, fallbackMinute, 0); + return fallbackBoundary.AddMinutes(5); } } \ No newline at end of file