79 lines
3.2 KiB
C#
79 lines
3.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
|
|
|
namespace FinlyticTechnicals.Indicators;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// (<c>MultiTimeframeCandleAggregator</c>, which previously duplicated this exact bucketing logic per
|
|
/// timeframe) and backtest replay (<c>FinlyticSimulation.Engine.HistoricalReplayRunner</c>, 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.
|
|
/// </summary>
|
|
public static class CandleResampler
|
|
{
|
|
/// <summary>Bucket size in minutes for every timeframe name known across FinlyticTechnicals/FinlyticSimulation.</summary>
|
|
public static readonly IReadOnlyDictionary<string, int> KnownTimeframeMinutes =
|
|
new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["1m"] = 1,
|
|
["5m"] = 5,
|
|
["15m"] = 15,
|
|
["1h"] = 60,
|
|
["1d"] = 1440
|
|
};
|
|
|
|
/// <summary>Every known timeframe strictly coarser than <paramref name="baseMinutes"/>, ascending.</summary>
|
|
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));
|
|
|
|
/// <summary>
|
|
/// Aggregates <paramref name="source"/> into <paramref name="bucketMinutes"/>-wide bars. Returns an empty
|
|
/// list (never fabricates a partial/synthetic bar) if <paramref name="source"/> is empty.
|
|
/// </summary>
|
|
public static List<CandleDto> Resample(IReadOnlyList<CandleDto> 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<CandleDto>();
|
|
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);
|
|
}
|
|
}
|