Files
Finlytic/FinlyticTechnicals/Indicators/TechnicalIndicatorsEngine.cs
T

318 lines
10 KiB
C#

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
);
/// <summary>
/// High-performance mathematical indicators engine for time-series analysis.
/// </summary>
public static class TechnicalIndicatorsEngine
{
public static decimal CalculateSma(IReadOnlyList<CandleDto> 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<CandleDto> candles, int period)
{
if (candles == null || candles.Count == 0 || period <= 0) return 0m;
if (candles.Count < period) return CalculateSma(candles, candles.Count);
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<CandleDto> 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<CandleDto> 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<CandleDto> 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<CandleDto>();
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<CandleDto> 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<CandleDto> 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<CandleDto> 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<CandleDto> 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<CandleDto> 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<CandleDto> 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;
}
}