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