unit Myc.Trade.Indicators; interface uses System.SysUtils, System.Math, Myc.Trade.Types, Myc.Trade.DataArray; type // Result for the Moving Average Convergence Divergence (MACD) indicator. TMacdResult = record MacdLine: Double; SignalLine: Double; Histogram: Double; end; // Result for the Stochastic Oscillator indicator. TStochasticResult = record K: Double; // %K line D: Double; // %D line (signal line) end; // Result for the Bollinger Bands indicator. TBollingerBandsResult = record UpperBand: Double; MiddleBand: Double; LowerBand: Double; end; TIndicators = record private class function CalculateSMA(const Series: TMycDataArray; const Period: Integer): Double; static; class function CalculateStdDev(const Series: TMycDataArray; const Period: Integer): Double; static; class function CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; static; public // Simple Moving Average class function CreateSMA(Period: Integer): TIndicatorFunc; static; // Exponential Moving Average class function CreateEMA(Period: Integer): TIndicatorFunc; static; // Hull Moving Average class function CreateHMA(Period: Integer): TIndicatorFunc; static; // Relative Strength Index class function CreateRSI(Period: Integer): TIndicatorFunc; static; // Moving Average Convergence Divergence class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TIndicatorFunc; static; // Stochastic Oscillator class function CreateStochastic(KPeriod, DPeriod: Integer): TIndicatorFunc; static; // Bollinger Bands class function CreateBollingerBands(Period: Integer; Multiplier: Double): TIndicatorFunc; static; end; implementation { TIndicators } class function TIndicators.CalculateSMA(const Series: TMycDataArray; const Period: Integer): Double; var i: Integer; sum: Double; begin if (Series.Count < Period) or (Period <= 0) then Exit(0.0); sum := 0; for i := 0 to Period - 1 do sum := sum + Series[i]; Result := sum / Period; end; class function TIndicators.CalculateStdDev(const Series: TMycDataArray; const Period: Integer): Double; var i: Integer; mean, sumOfSquares: Double; begin if (Series.Count < Period) or (Period <= 0) then Exit(0.0); mean := CalculateSMA(Series, Period); sumOfSquares := 0; for i := 0 to Period - 1 do sumOfSquares := sumOfSquares + Power(Series[i] - mean, 2); Result := Sqrt(sumOfSquares / Period); end; class function TIndicators.CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; var i: Integer; numerator: Double; denominator: Int64; begin // Ensure there is enough data to calculate the WMA if (Series.Count < Period) or (Period <= 0) then Exit(0.0); numerator := 0; // The sum of weights (1 + 2 + ... + Period) denominator := Period * (Period + 1) div 2; if (denominator = 0) then Exit(0.0); for i := 0 to Period - 1 do begin // Newest data (index 0) gets the highest weight (Period) numerator := numerator + Series[i] * (Period - i); end; Result := numerator / denominator; end; class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TIndicatorFunc; begin var sourceData := TMycDataArray.CreateEmpty; Result := function(const Value: Double): TBollingerBandsResult var stdDev: Double; begin sourceData := sourceData.Add(Value, Period); Result.MiddleBand := Double.NaN; Result.UpperBand := Double.NaN; Result.LowerBand := Double.NaN; if (sourceData.Count >= Period) then begin Result.MiddleBand := CalculateSMA(sourceData, Period); stdDev := CalculateStdDev(sourceData, Period); Result.UpperBand := Result.MiddleBand + (stdDev * Multiplier); Result.LowerBand := Result.MiddleBand - (stdDev * Multiplier); end; end; end; class function TIndicators.CreateEMA(Period: Integer): TIndicatorFunc; begin var lastEma: Double := Double.NaN; var sourceData := TMycDataArray.CreateEmpty; var multiplier := 2 / (Period + 1); Result := function(const Value: Double): Double begin sourceData := sourceData.Add(Value, Period); if (sourceData.Count < Period) then begin Result := Double.NaN; Exit; end; if not IsNan(lastEma) then begin // Subsequent EMA calculation lastEma := (Value - lastEma) * multiplier + lastEma; end else begin // First EMA is a SMA of the initial period lastEma := CalculateSMA(sourceData, Period); end; Result := lastEma; end; end; class function TIndicators.CreateHMA(Period: Integer): TIndicatorFunc; begin var periodHalf := Period div 2; var periodSqrt := Round(Sqrt(Period)); var sourceData := TMycDataArray.CreateEmpty; var diffSeries := TMycDataArray.CreateEmpty; Result := function(const Value: Double): Double var price: Double; wmaHalf, wmaFull, diff: Double; begin price := Value; // Default HMA to NaN for the warm-up period. Result := Double.NaN; // Add new price to the source data array, respecting the lookback period. sourceData := sourceData.Add(price, Period); // Check if there is enough data to start the first stage of calculation. if (sourceData.Count >= Period) then begin // Calculate the two WMAs for the first step. wmaHalf := CalculateWMA(sourceData, periodHalf); wmaFull := CalculateWMA(sourceData, Period); // Calculate the difference and add to the intermediate series. diff := 2 * wmaHalf - wmaFull; diffSeries := diffSeries.Add(diff, periodSqrt); // Check if there is enough intermediate data for the final calculation. if (diffSeries.Count >= periodSqrt) then begin // Calculate the final HMA value Result := CalculateWMA(diffSeries, periodSqrt); end; end; end; end; class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TIndicatorFunc; begin var emaFast := CreateEMA(FastPeriod); var emaSlow := CreateEMA(SlowPeriod); var emaSignal := CreateEMA(SignalPeriod); Result := function(const Value: Double): TMacdResult var fastVal, slowVal: Double; begin fastVal := emaFast(Value); slowVal := emaSlow(Value); if IsNan(slowVal) then // slowVal will be the last one to become non-NaN begin Result.MacdLine := Double.NaN; Result.SignalLine := Double.NaN; Result.Histogram := Double.NaN; end else begin Result.MacdLine := fastVal - slowVal; Result.SignalLine := emaSignal(Result.MacdLine); if not IsNan(Result.SignalLine) then Result.Histogram := Result.MacdLine - Result.SignalLine else Result.Histogram := Double.NaN; end; end; end; class function TIndicators.CreateRSI(Period: Integer): TIndicatorFunc; begin var avgGain: Double := Double.NaN; var avgLoss: Double := Double.NaN; var sourceData := TMycDataArray.CreateEmpty; Result := function(const Value: Double): Double var change, gain, loss, rs: Double; gainSum, lossSum: Double; i: Integer; begin sourceData := sourceData.Add(Value, Period + 1); Result := Double.NaN; if (sourceData.Count <= Period) then Exit; // Initial calculation for the first full period if IsNan(avgGain) then begin gainSum := 0; lossSum := 0; for i := 0 to Period - 1 do begin change := sourceData[i] - sourceData[i + 1]; if (change > 0) then gainSum := gainSum + change else lossSum := lossSum - change; end; avgGain := gainSum / Period; avgLoss := lossSum / Period; end else // Smoothed calculation for subsequent values begin change := sourceData[0] - sourceData[1]; gain := 0; loss := 0; if (change > 0) then gain := change else loss := -change; avgGain := (avgGain * (Period - 1) + gain) / Period; avgLoss := (avgLoss * (Period - 1) + loss) / Period; end; if (avgLoss = 0) then Result := 100 else begin rs := avgGain / avgLoss; Result := 100 - (100 / (1 + rs)); end; end; end; class function TIndicators.CreateSMA(Period: Integer): TIndicatorFunc; begin var sourceData := TMycDataArray.CreateEmpty; Result := function(const Value: Double): Double begin sourceData := sourceData.Add(Value, Period); if (sourceData.Count >= Period) then Result := CalculateSMA(sourceData, Period) else Result := Double.NaN; end; end; class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TIndicatorFunc; begin var sourceData := TMycDataArray.CreateEmpty; var smaD := CreateSMA(DPeriod); Result := function(const Value: TOhlcItem): TStochasticResult var i: Integer; highestHigh, lowestLow: Double; begin sourceData := sourceData.Add(Value, KPeriod); Result.K := Double.NaN; Result.D := Double.NaN; if (sourceData.Count >= KPeriod) then begin highestHigh := -MaxDouble; lowestLow := MaxDouble; for i := 0 to KPeriod - 1 do begin // Correctly use High and Low fields if (sourceData[i].High > highestHigh) then highestHigh := sourceData[i].High; if (sourceData[i].Low < lowestLow) then lowestLow := sourceData[i].Low; end; if (highestHigh > lowestLow) then // Correctly use the current Close Result.K := 100 * (sourceData[0].Close - lowestLow) / (highestHigh - lowestLow) else Result.K := 100; // Or 50, depends on convention Result.D := smaD(Result.K); end; end; end; end.