using Managing.Core; using static Managing.Common.Enums; namespace Managing.Domain.Candles; public static class CandleHelpers { public static DateTime GetBotPreloadSinceFromTimeframe(Timeframe timeframe) { return timeframe switch { Timeframe.FiveMinutes => DateTime.UtcNow.AddDays(-1), Timeframe.FifteenMinutes => DateTime.UtcNow.AddDays(-5), Timeframe.ThirtyMinutes => DateTime.UtcNow.AddDays(-10), Timeframe.OneHour => DateTime.UtcNow.AddDays(-30), Timeframe.FourHour => DateTime.UtcNow.AddDays(-60), Timeframe.OneDay => DateTime.UtcNow.AddDays(-360), _ => DateTime.Now.AddHours(-15), }; } public static DateTime GetPreloadSinceFromTimeframe(Timeframe timeframe) { return DateTime.UtcNow.AddDays(GetMinimalDays(timeframe)); } public static int GetIntervalFromTimeframe(Timeframe timeframe) { var seconds = GetBaseIntervalInSeconds(timeframe); // Run every 1/5th of the candle duration return seconds / 5 * 1000; // milliseconds } /// /// Gets the interval in minutes for the given timeframe. /// This is useful for cooldown period calculations. /// /// The timeframe to get the interval for /// The interval in minutes public static double GetIntervalInMinutes(Timeframe timeframe) { var seconds = GetBaseIntervalInSeconds(timeframe); // Run every 1/5th of the candle duration return (seconds / 5.0) / 60.0; // minutes } /// /// Gets the base interval in seconds for the given timeframe. /// This is the core interval logic that can be used for various calculations. /// /// The timeframe to get the base interval for /// The base interval in seconds public static int GetBaseIntervalInSeconds(Timeframe timeframe) { return timeframe switch { Timeframe.OneDay => 86400, Timeframe.FourHour => 14400, Timeframe.OneHour => 3600, Timeframe.ThirtyMinutes => 1800, Timeframe.FifteenMinutes => 900, Timeframe.FiveMinutes => 300, _ => 300, }; } public static int GetUnixInterval(this Timeframe timeframe) { return timeframe switch { Timeframe.OneDay => 86400, Timeframe.FiveMinutes => 300, Timeframe.FifteenMinutes => 900, Timeframe.FourHour => 14400, Timeframe.OneHour => 3600, _ => throw new NotImplementedException() }; } public static double GetMinimalDays(Timeframe timeframe) { return timeframe switch { Timeframe.FiveMinutes => -1, Timeframe.FifteenMinutes => -5, Timeframe.ThirtyMinutes => -10, Timeframe.OneHour => -30, Timeframe.FourHour => -60, Timeframe.OneDay => -360, _ => throw new NotImplementedException() }; } public static string GetCandleStoreGrainKey(TradingExchanges exchange, Ticker ticker, Timeframe timeframe) { return string.Join("-", exchange, ticker, timeframe); } 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])); } 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; } /// /// Gets the next expected candle time for the given timeframe. /// This is useful to determine if a new candle should be available yet. /// /// The timeframe to calculate for /// The current time (defaults to DateTime.UtcNow) /// The next expected candle time public static DateTime GetNextExpectedCandleTime(Timeframe timeframe, DateTime? now = null) { var currentTime = now ?? DateTime.UtcNow; var intervalMinutes = GetBaseIntervalInSeconds(timeframe) / 60; // Calculate the next candle boundary return GetNextCandleBoundary(currentTime, intervalMinutes).AddSeconds(-1); } 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); } }