using System; using System.Collections.Generic; using System.Linq; using FinlyticCore.Dtos.TechnicalAnalysis; namespace FinlyticTechnicals.Indicators; public record MacdResult( decimal MacdLine, decimal SignalLine, decimal Histogram ); public record BollingerBandsResult( decimal UpperBand, decimal MiddleBand, decimal LowerBand, decimal Bandwidth, decimal PercentB ); public record KeltnerChannelResult( decimal UpperBand, decimal MiddleBand, decimal LowerBand ); public record SuperTrendResult( decimal Value, SignalDirection Direction, bool IsFlipped ); public record SqueezeResult( bool IsInSqueeze, decimal MomentumHistogram, string SqueezeState // "ON", "FIRED_BULLISH", "FIRED_BEARISH", "NONE" ); public record AdxResult( decimal Adx, decimal PlusDi, decimal MinusDi, bool IsTrending ); /// /// High-performance mathematical indicators engine for time-series analysis. /// public static class TechnicalIndicatorsEngine { public static decimal CalculateSma(IReadOnlyList candles, int period) { if (candles == null || candles.Count < period || period <= 0) return 0m; decimal sum = 0m; for (int i = candles.Count - period; i < candles.Count; i++) { sum += candles[i].Close; } return sum / period; } public static decimal CalculateEma(IReadOnlyList candles, int period) { if (candles == null || candles.Count < period || period <= 0) return 0m; decimal k = 2m / (period + 1); // Seed with SMA decimal ema = 0m; for (int i = 0; i < period; i++) { ema += candles[i].Close; } ema /= period; for (int i = period; i < candles.Count; i++) { ema = (candles[i].Close * k) + (ema * (1m - k)); } return ema; } public static decimal CalculateRsi(IReadOnlyList candles, int period = 14) { if (candles == null || candles.Count <= period || period <= 0) return 50m; decimal gains = 0m; decimal losses = 0m; for (int i = 1; i <= period; i++) { decimal diff = candles[i].Close - candles[i - 1].Close; if (diff >= 0) gains += diff; else losses += Math.Abs(diff); } decimal avgGain = gains / period; decimal avgLoss = losses / period; for (int i = period + 1; i < candles.Count; i++) { decimal diff = candles[i].Close - candles[i - 1].Close; if (diff >= 0) { avgGain = ((avgGain * (period - 1)) + diff) / period; avgLoss = (avgLoss * (period - 1)) / period; } else { avgGain = (avgGain * (period - 1)) / period; avgLoss = ((avgLoss * (period - 1)) + Math.Abs(diff)) / period; } } if (avgLoss == 0m) return 100m; decimal rs = avgGain / avgLoss; return 100m - (100m / (1m + rs)); } public static decimal CalculateAtr(IReadOnlyList candles, int period = 14) { if (candles == null || candles.Count < 2 || period <= 0) return 0m; int count = candles.Count; int effectivePeriod = Math.Min(period, count - 1); decimal trSum = 0m; for (int i = count - effectivePeriod; i < count; i++) { decimal high = candles[i].High; decimal low = candles[i].Low; decimal prevClose = candles[i - 1].Close; decimal tr = Math.Max(high - low, Math.Max(Math.Abs(high - prevClose), Math.Abs(low - prevClose))); trSum += tr; } return trSum / effectivePeriod; } public static MacdResult CalculateMacd(IReadOnlyList candles, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) { if (candles == null || candles.Count < slowPeriod) return new MacdResult(0m, 0m, 0m); decimal fastEma = CalculateEma(candles, fastPeriod); decimal slowEma = CalculateEma(candles, slowPeriod); decimal macdLine = fastEma - slowEma; // Calculate series of MACD lines for signal line calculation var macdHistory = new List(); int start = Math.Max(0, candles.Count - (signalPeriod + 5)); for (int i = start; i < candles.Count; i++) { var subCandles = candles.Take(i + 1).ToList(); if (subCandles.Count >= slowPeriod) { var f = CalculateEma(subCandles, fastPeriod); var s = CalculateEma(subCandles, slowPeriod); var val = f - s; macdHistory.Add(new CandleDto(candles[i].Timestamp, val, val, val, val, 0)); } } decimal signalLine = macdHistory.Count >= signalPeriod ? CalculateEma(macdHistory, signalPeriod) : macdLine; decimal histogram = macdLine - signalLine; return new MacdResult(macdLine, signalLine, histogram); } public static BollingerBandsResult CalculateBollingerBands(IReadOnlyList candles, int period = 20, decimal multiplier = 2.0m) { if (candles == null || candles.Count < period || period <= 0) return new BollingerBandsResult(0m, 0m, 0m, 0m, 0m); decimal sma = CalculateSma(candles, period); decimal sumSquares = 0m; for (int i = candles.Count - period; i < candles.Count; i++) { decimal diff = candles[i].Close - sma; sumSquares += diff * diff; } decimal stdDev = (decimal)Math.Sqrt((double)(sumSquares / period)); decimal upper = sma + (multiplier * stdDev); decimal lower = sma - (multiplier * stdDev); decimal bandwidth = sma > 0 ? ((upper - lower) / sma) * 100m : 0m; decimal currentClose = candles.Last().Close; decimal percentB = (upper - lower) > 0 ? (currentClose - lower) / (upper - lower) : 0.5m; return new BollingerBandsResult(upper, sma, lower, bandwidth, percentB); } public static KeltnerChannelResult CalculateKeltnerChannels(IReadOnlyList candles, int period = 20, decimal atrMultiplier = 1.5m) { if (candles == null || candles.Count < period) return new KeltnerChannelResult(0m, 0m, 0m); decimal ema = CalculateEma(candles, period); decimal atr = CalculateAtr(candles, period); decimal upper = ema + (atrMultiplier * atr); decimal lower = ema - (atrMultiplier * atr); return new KeltnerChannelResult(upper, ema, lower); } public static SqueezeResult CalculateVolatilitySqueeze(IReadOnlyList candles) { var bb = CalculateBollingerBands(candles, 20, 2.0m); var kc = CalculateKeltnerChannels(candles, 20, 1.5m); bool inSqueeze = bb.LowerBand > kc.LowerBand && bb.UpperBand < kc.UpperBand; var macd = CalculateMacd(candles, 12, 26, 9); decimal momentum = macd.Histogram; string state = "NONE"; if (inSqueeze) { state = "ON"; } else if (momentum > 0) { state = "FIRED_BULLISH"; } else if (momentum < 0) { state = "FIRED_BEARISH"; } return new SqueezeResult(inSqueeze, momentum, state); } public static SuperTrendResult CalculateSuperTrend(IReadOnlyList candles, int period = 10, decimal multiplier = 3.0m) { if (candles == null || candles.Count < period) return new SuperTrendResult(0m, SignalDirection.Neutral, false); decimal atr = CalculateAtr(candles, period); var last = candles.Last(); decimal hl2 = (last.High + last.Low) / 2m; decimal basicUpperBand = hl2 + (multiplier * atr); decimal basicLowerBand = hl2 - (multiplier * atr); // Determine trend relative to previous candle decimal prevClose = candles.Count > 1 ? candles[^2].Close : last.Close; SignalDirection dir = last.Close > basicUpperBand ? SignalDirection.Buy : last.Close < basicLowerBand ? SignalDirection.Sell : (last.Close >= prevClose ? SignalDirection.Buy : SignalDirection.Sell); decimal superTrendValue = dir == SignalDirection.Buy ? basicLowerBand : basicUpperBand; bool isFlipped = (prevClose < basicUpperBand && last.Close > basicUpperBand) || (prevClose > basicLowerBand && last.Close < basicLowerBand); return new SuperTrendResult(superTrendValue, dir, isFlipped); } public static AdxResult CalculateAdx(IReadOnlyList candles, int period = 14) { if (candles == null || candles.Count <= period * 2) return new AdxResult(15m, 15m, 15m, false); decimal trSum = 0m; decimal plusDmSum = 0m; decimal minusDmSum = 0m; for (int i = candles.Count - period; i < candles.Count; i++) { var curr = candles[i]; var prev = candles[i - 1]; decimal upMove = curr.High - prev.High; decimal downMove = prev.Low - curr.Low; decimal plusDm = (upMove > downMove && upMove > 0) ? upMove : 0m; decimal minusDm = (downMove > upMove && downMove > 0) ? downMove : 0m; decimal tr = Math.Max(curr.High - curr.Low, Math.Max(Math.Abs(curr.High - prev.Close), Math.Abs(curr.Low - prev.Close))); trSum += tr; plusDmSum += plusDm; minusDmSum += minusDm; } if (trSum == 0m) return new AdxResult(0m, 0m, 0m, false); decimal plusDi = (plusDmSum / trSum) * 100m; decimal minusDi = (minusDmSum / trSum) * 100m; decimal diSum = plusDi + minusDi; decimal dx = diSum > 0 ? (Math.Abs(plusDi - minusDi) / diSum) * 100m : 0m; bool isTrending = dx >= 25m; return new AdxResult(dx, plusDi, minusDi, isTrending); } public static decimal CalculateVwap(IReadOnlyList candles) { if (candles == null || candles.Count == 0) return 0m; decimal totalTypicalPriceVolume = 0m; long totalVolume = 0; foreach (var c in candles) { decimal typicalPrice = (c.High + c.Low + c.Close) / 3m; totalTypicalPriceVolume += typicalPrice * c.Volume; totalVolume += c.Volume; } return totalVolume > 0 ? totalTypicalPriceVolume / totalVolume : candles.Last().Close; } }