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);
}
}