feat(technicals): add technical analysis microservice with indicator engines, pattern detectors, and strategies
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user