unit Myc.Trade.Indicators; interface uses System.SysUtils, System.Math, System.Generics.Collections, System.Rtti, Myc.Data.Records, Myc.Data.Pipeline, Myc.Data.Series, Myc.Trade.Types; 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; // Result for the Keltner Channels indicator. TKeltnerChannelsResult = record UpperBand: Double; MiddleBand: Double; LowerBand: Double; end; TIndicators = record private class function CalculateSMA(const Series: TSeries; const Period: Integer): Double; static; class function CalculateStdDev(const Series: TSeries; const Period: Integer): Double; static; class function CalculateWMA(const Series: TSeries; const Period: Integer): Double; static; public // Simple Moving Average class function CreateSMA(Period: Integer): TConvertFunc; static; // Exponential Moving Average class function CreateEMA(Period: Integer): TConvertFunc; static; // Hull Moving Average class function CreateHMA(Period: Integer): TConvertFunc; static; // Relative Strength Index class function CreateRSI(Period: Integer): TConvertFunc; static; // Moving Average Convergence Divergence class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc; overload; static; class function CreateMACD( const EmaFast, EmaSlow, EmaSignal: TConvertFunc ): TConvertFunc; overload; static; // Stochastic Oscillator class function CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc; overload; static; class function CreateStochastic( KPeriod: Integer; const SmaD: TConvertFunc ): TConvertFunc; overload; static; // Bollinger Bands class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc; static; // Average True Range class function CreateATR(Period: Integer): TConvertFunc; overload; static; class function CreateATR(const MovAvgTR: TConvertFunc): TConvertFunc; overload; static; // Keltner Channels class function CreateKeltnerChannels( Period: Integer; Multiplier: Double ): TConvertFunc; overload; static; class function CreateKeltnerChannels( const MovAvgMiddle: TConvertFunc; const AtrFunc: TConvertFunc; Multiplier: Double ): TConvertFunc; overload; static; class function CreateMean: TConvertFunc, Double>; static; end; TIndicatorFactory = class type TFunc = TConvertFunc; private FParams: TDataRecord.TLayout; FInput: TDataRecord.TLayout; FOutput: TDataRecord.TLayout; public constructor Create(const AParams, AInput, AOutput: TDataRecord.TLayout); function CreateIndicator(const Params: TDataRecord): TFunc; virtual; abstract; property Params: TDataRecord.TLayout read FParams; property Input: TDataRecord.TLayout read FInput; property Output: TDataRecord.TLayout read FOutput; end; TEMA = class(TIndicatorFactory) type TParam = record Period: Integer; end; TInput = record Price: Double; end; TResult = record MA: Double; end; public constructor Create; function CreateIndicator(const Params: TDataRecord): TIndicatorFactory.TFunc; override; end; TMACD = class type TParam = record Fast: TConvertFunc; Slow: TConvertFunc; Signal: TConvertFunc; end; TInput = record Price: Double; end; TResult = record MacdLine: Double; SignalLine: Double; Histogram: Double; end; public class function CreateMACD(const Param: TParam): TConvertFunc; static; end; var Registry: TList; implementation { TIndicators } class function TIndicators.CalculateSMA(const Series: TSeries; 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: TSeries; 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: TSeries; 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): TConvertFunc; begin var sourceData: TSeries; Result := function(const Value: Double): TBollingerBandsResult var stdDev: Double; begin 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): TConvertFunc; begin var lastEma: Double := Double.NaN; var sourceData: TSeries; var multiplier := 2 / (Period + 1); Result := function(const Value: Double): Double begin 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): TConvertFunc; begin var periodHalf := Period div 2; var periodSqrt := Round(Sqrt(Period)); var sourceData: TSeries; var diffSeries: TSeries; 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.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.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; // Standard MACD using EMAs. class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc; begin Result := CreateMACD(CreateEMA(FastPeriod), CreateEMA(SlowPeriod), CreateEMA(SignalPeriod)); end; // Creates a MACD indicator from three provided moving average functions. class function TIndicators.CreateMACD(const EmaFast, EmaSlow, EmaSignal: TConvertFunc): TConvertFunc; begin 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): TConvertFunc; begin var avgGain: Double := Double.NaN; var avgLoss: Double := Double.NaN; var sourceData: TSeries; Result := function(const Value: Double): Double var change, gain, loss, rs: Double; gainSum, lossSum: Double; i: Integer; begin 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): TConvertFunc; begin var sourceData: TSeries; Result := function(const Value: Double): Double begin sourceData.Add(Value, Period); if (sourceData.Count >= Period) then Result := CalculateSMA(sourceData, Period) else Result := Double.NaN; end; end; // Standard Stochastic Oscillator using an SMA for the %D line. class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc; begin Result := CreateStochastic(KPeriod, CreateSMA(DPeriod)); end; // Creates a Stochastic Oscillator using an injectable moving average for the %D line. class function TIndicators.CreateStochastic( KPeriod: Integer; const SmaD: TConvertFunc ): TConvertFunc; begin var sourceData: TSeries; Result := function(const Value: TOhlcItem): TStochasticResult var i: Integer; highestHigh, lowestLow: Double; begin 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; // Standard ATR using an EMA for smoothing. class function TIndicators.CreateATR(Period: Integer): TConvertFunc; begin Result := CreateATR(CreateEMA(Period)); end; // Calculates the Average True Range (ATR) using an injectable moving average. class function TIndicators.CreateATR(const MovAvgTR: TConvertFunc): TConvertFunc; begin var sourceData: TSeries; Result := function(const Value: TOhlcItem): Double var tr: Double; begin // We only need the previous bar to calculate true range. sourceData.Add(Value, 2); if (sourceData.Count < 2) then begin // Feed a dummy value to keep the moving average count in sync. It will correctly return NaN. Result := MovAvgTR(0); Exit; end; // Calculate current True Range. tr := Max(Value.High - Value.Low, Max(Abs(Value.High - sourceData[1].Close), Abs(Value.Low - sourceData[1].Close))); // Feed the calculated TR into the provided moving average function. Result := MovAvgTR(tr); end; end; // Standard Keltner Channels using an EMA for the middle line and an EMA-based ATR. class function TIndicators.CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConvertFunc; begin Result := CreateKeltnerChannels(CreateEMA(Period), CreateATR(Period), Multiplier); end; // Calculates Keltner Channels using an injectable ATR and middle band moving average. class function TIndicators.CreateKeltnerChannels( const MovAvgMiddle: TConvertFunc; const AtrFunc: TConvertFunc; Multiplier: Double ): TConvertFunc; begin Result := function(const Value: TOhlcItem): TKeltnerChannelsResult var atrValue, middleValue, typicalPrice: Double; begin // Calculate Typical Price for the middle band. typicalPrice := (Value.High + Value.Low + Value.Close) / 3.0; // Get values from the provided indicator functions. middleValue := MovAvgMiddle(typicalPrice); atrValue := AtrFunc(Value); // Set default NaN values for the warm-up period. Result.MiddleBand := middleValue; Result.UpperBand := Double.NaN; Result.LowerBand := Double.NaN; // Once both middle band and ATR have valid (non-NaN) values, calculate the channels. if not IsNan(middleValue) and not IsNan(atrValue) then begin Result.UpperBand := middleValue + (atrValue * Multiplier); Result.LowerBand := middleValue - (atrValue * Multiplier); end; end; end; class function TIndicators.CreateMean: TConvertFunc, Double>; begin Result := function(const Value: TArray): Double begin if Length(Value) = 0 then exit(NaN); Result := Value[0]; for var i := 1 to High(Value) do Result := Result + Value[i]; Result := Result / Length(Value); end; end; // Creates a MACD indicator from three provided moving average functions. class function TMACD.CreateMACD(const Param: TParam): TConvertFunc; begin Result := function(const Input: TInput): TResult var fastVal, slowVal: Double; begin fastVal := Param.Fast(Input.Price); slowVal := Param.Slow(Input.Price); 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 := Param.Signal(Result.MacdLine); if not IsNan(Result.SignalLine) then Result.Histogram := Result.MacdLine - Result.SignalLine else Result.Histogram := Double.NaN; end; end; end; constructor TEMA.Create; begin inherited Create(TDataRecord.TLayout.FromRecord, TDataRecord.TLayout.FromRecord, TDataRecord.TLayout.FromRecord) end; function TEMA.CreateIndicator(const Params: TDataRecord): TIndicatorFactory.TFunc; begin var Period := Params.GetValue('Period'); var Input := TDataRecord.TLayout.FromRecord; var inPrice := Input.IndexOf('Price'); var Output := TDataRecord.TLayout.FromRecord; var outMA := Output.IndexOf('MA'); var CalcEMA := TIndicators.CreateEMA(Period); Result := function(const Input: TDataRecord): TDataRecord begin var Price: Double; Input.GetValue(inPrice, Price); var MA := CalcEMA(Price); Result := TDataRecord.FromRecord; Result.SetValue(outMA, MA); end; end; constructor TIndicatorFactory.Create(const AParams, AInput, AOutput: TDataRecord.TLayout); begin inherited Create; FParams := AParams; FInput := AInput; FOutput := AOutput; end; initialization Registry := TList.Create; Registry.Add(TEMA.Create); finalization Registry.Free; end.