using System; using System.Collections.Generic; using System.Linq; using FinlyticCore.Dtos.TechnicalAnalysis; namespace FinlyticTechnicals.Indicators; /// /// Standard OHLCV rollup of a finer-grained, chronologically ordered candle series into coarser buckets /// (first Open, max High, min Low, last Close, summed Volume). Shared by the live ring-buffer aggregator /// (MultiTimeframeCandleAggregator, which previously duplicated this exact bucketing logic per /// timeframe) and backtest replay (FinlyticSimulation.Engine.HistoricalReplayRunner, which previously /// had no way to derive a higher timeframe at all - see its own doc comment) so both paths compute higher /// timeframes identically instead of maintaining two separate implementations. /// public static class CandleResampler { /// Bucket size in minutes for every timeframe name known across FinlyticTechnicals/FinlyticSimulation. public static readonly IReadOnlyDictionary KnownTimeframeMinutes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["1m"] = 1, ["5m"] = 5, ["15m"] = 15, ["1h"] = 60, ["1d"] = 1440 }; /// Every known timeframe strictly coarser than , ascending. public static IEnumerable<(string Timeframe, int Minutes)> CoarserTimeframes(int baseMinutes) => KnownTimeframeMinutes .Where(kv => kv.Value > baseMinutes) .OrderBy(kv => kv.Value) .Select(kv => (kv.Key, kv.Value)); /// /// Aggregates into -wide bars. Returns an empty /// list (never fabricates a partial/synthetic bar) if is empty. /// public static List Resample(IReadOnlyList source, int bucketMinutes) { if (source.Count == 0 || bucketMinutes <= 0) return []; var groups = source .GroupBy(c => BucketStart(c.Timestamp, bucketMinutes)) .OrderBy(g => g.Key); var result = new List(); foreach (var group in groups) { var bars = group.OrderBy(b => b.Timestamp).ToList(); if (bars.Count == 0) continue; result.Add(new CandleDto( Timestamp: group.Key, Open: bars[0].Open, High: bars.Max(b => b.High), Low: bars.Min(b => b.Low), Close: bars[^1].Close, Volume: bars.Sum(b => b.Volume), Bid: bars[^1].Bid, Ask: bars[^1].Ask )); } return result; } private static DateTime BucketStart(DateTime timestamp, int bucketMinutes) { var dayStart = new DateTime(timestamp.Year, timestamp.Month, timestamp.Day, 0, 0, 0, DateTimeKind.Utc); if (bucketMinutes >= 1440) return dayStart; int totalMinutes = timestamp.Hour * 60 + timestamp.Minute; int bucketed = (totalMinutes / bucketMinutes) * bucketMinutes; return dayStart.AddMinutes(bucketed); } }