unit Myc.Trade.Indicators.Common; interface uses System.SysUtils, System.Math, System.Rtti, Myc.Data.Pipeline, Myc.Data.Series, Myc.Data.Types, Myc.Data.Value, Myc.Trade.Types, Myc.Trade.Indicators; type // --- Indicator Templates --- [IndicatorName('SMA', 'Simple Moving Average')] [IndicatorHint('Calculates the average of a selected range of prices.')] TSMA = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: Double; end; TResult = record SMA: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateSMA(Period: Integer): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('EMA', 'Exponential Moving Average')] [IndicatorHint('A moving average that places greater weight on the most recent data points.')] TEMA = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: Double; end; TResult = record EMA: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateEMA(Period: Integer): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('WMA', 'Weighted Moving Average')] [IndicatorHint('A moving average that places greater weight on more recent data points.')] TWMA = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: Double; end; TResult = record WMA: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateWMA(Period: Integer): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('HMA', 'Hull Moving Average')] [IndicatorHint('A fast, smooth moving average that minimizes lag.')] THMA = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: Double; end; TResult = record HMA: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateHMA(Period: Integer): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('RSI', 'Relative Strength Index')] [IndicatorHint('A momentum indicator measuring the magnitude of recent price changes.')] TRSI = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: Double; end; TResult = record RSI: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateRSI(Period: Integer): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('MACD', 'Moving Average Convergence Divergence')] [IndicatorHint('A trend-following momentum indicator showing the relationship between two EMAs.')] TMACD = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record FastPeriod: Integer; SlowPeriod: Integer; SignalPeriod: Integer; end; TArgs = record Value: Double; end; TResult = record MacdLine: Double; SignalLine: Double; Histogram: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc; overload; static; class function CreateMACD( const EmaFast, EmaSlow, EmaSignal: TConvertFunc ): TConvertFunc; overload; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('Stoch', 'Stochastic Oscillator')] [IndicatorHint('A momentum indicator comparing a closing price to a range of its prices.')] TStochastic = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record KPeriod: Integer; DPeriod: Integer; end; TArgs = record Value: TOhlcItem; end; TResult = record K: Double; // %K line D: Double; // %D line (signal line) end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc; overload; static; // Creates a Stochastic Oscillator using an injectable moving average for the %D line. class function CreateStochastic( KPeriod: Integer; const SmaD: TConvertFunc ): TConvertFunc; overload; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('StdDev', 'Standard Deviation')] [IndicatorHint('Measures the amount of variation or dispersion of a set of values.')] TStdDev = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: Double; end; TResult = record StdDev: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateStdDev(Period: Integer): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('BB', 'Bollinger Bands')] [IndicatorHint('Characterizes prices and volatility over time using standard deviation bands.')] TBollingerBands = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; Multiplier: Double; end; TArgs = record Value: Double; end; TResult = record UpperBand: Double; MiddleBand: Double; LowerBand: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('ATR', 'Average True Range')] [IndicatorHint('Measures market volatility by decomposing the entire range of an asset price.')] TATR = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; end; TArgs = record Value: TOhlcItem; end; TResult = record ATR: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateATR(Period: Integer): TConvertFunc; overload; static; class function CreateATR(const MovAvgTR: TConvertFunc): TConvertFunc; overload; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('KC', 'Keltner Channels')] [IndicatorHint('A volatility-based indicator composed of an EMA and two ATR-based outer lines.')] TKeltnerChannels = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record Period: Integer; Multiplier: Double; end; TArgs = record Value: TOhlcItem; end; TResult = record UpperBand: Double; MiddleBand: Double; LowerBand: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConvertFunc; overload; static; class function CreateKeltnerChannels( const MovAvgMiddle: TConvertFunc; const AtrFunc: TConvertFunc; Multiplier: Double ): TConvertFunc; overload; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; [IndicatorName('Mean', 'Mean Value')] [IndicatorHint('Calculates the arithmetic mean of an array of values.')] TMean = class strict private class var FFactory: IDataMethodValue; class constructor CreateClass; public type TParams = record end; TArgs = record Values: TArray; end; TResult = record Mean: Double; end; [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; class function CreateMean: TConvertFunc, Double>; static; // Provides the factory method for this indicator as a data value. class property Factory: IDataMethodValue read FFactory; end; implementation { TSMA } class constructor TSMA.CreateClass; begin var paramsType := TDataType.Ordinal; var argsType := TDataType.Float; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin // Local variable is necessary here to capture the created function. var SMA := CreateSMA(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin Result := TDataType.Float.CreateValue(SMA(Args.AsFloat.Value)); end ); end ); end; class function TSMA.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var smaFunc: TConvertFunc; begin smaFunc := CreateSMA(Params.Period); Result := function(const Value: TArgs): TResult begin Result.SMA := smaFunc(Value.Value); end; end; end; class function TSMA.CreateSMA(Period: Integer): TConvertFunc; begin // Implemented using a rolling sum and a circular array for O(1) performance. var sum: Double; var buffer: TArray; var currentIndex: Integer; var isReady: Boolean; if (Period <= 0) then begin Result := function(const Value: Double): Double begin Result := Double.NaN; end; exit; end; sum := 0.0; SetLength(buffer, Period); currentIndex := 0; isReady := false; Result := function(const Value: Double): Double begin if not isReady then begin // --- Warm-up phase --- // Fill the buffer until it has 'Period' elements. sum := sum + Value; buffer[currentIndex] := Value; inc(currentIndex); if (currentIndex < Period) then begin Result := Double.NaN; exit; end else begin // The buffer is now full, the first SMA can be calculated. isReady := true; currentIndex := 0; // Wrap index for the next write. Result := sum / Period; exit; end; end; // --- Rolling phase --- // Subtract the oldest value (which is being overwritten). sum := sum - buffer[currentIndex]; // Add the new value. sum := sum + Value; // Store the new value in the circular buffer. buffer[currentIndex] := Value; // Advance the index for the next write. currentIndex := (currentIndex + 1) mod Period; Result := sum / Period; end; end; { TEMA } class constructor TEMA.CreateClass; begin var paramsType := TDataType.Ordinal; var argsType := TDataType.Float; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var EMA := CreateEMA(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin Result := TDataType.Float.CreateValue(EMA(Args.AsFloat.Value)); end ); end ); end; class function TEMA.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var emaFunc: TConvertFunc; begin emaFunc := CreateEMA(Params.Period); Result := function(const Value: TArgs): TResult begin Result.EMA := emaFunc(Value.Value); end; end; end; class function TEMA.CreateEMA(Period: Integer): TConvertFunc; begin var lastEma: Double := Double.NaN; var sourceData: TSeries; var multiplier: Double; if (Period > 0) then multiplier := 2 / (Period + 1) else multiplier := 0; 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. Calculate it directly. var sum: Double := 0.0; var i: Integer; for i := 0 to Period - 1 do sum := sum + sourceData[i]; if (Period > 0) then lastEma := sum / Period else lastEma := 0.0; end; Result := lastEma; end; end; { TWMA } class constructor TWMA.CreateClass; begin var paramsType := TDataType.Ordinal; var argsType := TDataType.Float; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var WMA := CreateWMA(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin Result := TDataType.Float.CreateValue(WMA(Args.AsFloat.Value)); end ); end ); end; class function TWMA.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var wmaFunc: TConvertFunc; begin wmaFunc := CreateWMA(Params.Period); Result := function(const Value: TArgs): TResult begin Result.WMA := wmaFunc(Value.Value); end; end; end; class function TWMA.CreateWMA(Period: Integer): TConvertFunc; begin // Corrected O(1) implementation using a rolling window. var weightedSum: Double; var simpleSum: Double; var buffer: TArray; var currentIndex: Integer; var valueCount: Integer; var denominator: Int64; if (Period <= 0) then begin Result := function(const Value: Double): Double begin Result := Double.NaN; end; exit; end; weightedSum := 0.0; simpleSum := 0.0; SetLength(buffer, Period); currentIndex := 0; valueCount := 0; denominator := Period * (Period + 1) div 2; if (denominator = 0) then begin Result := function(const Value: Double): Double begin Result := Double.NaN; end; exit; end; Result := function(const Value: Double): Double var oldestValue: Double; begin inc(valueCount); // Get the value that will be overwritten. Initially, this is 0.0. oldestValue := buffer[currentIndex]; // --- Corrected Rolling Calculation --- // IMPORTANT: Update weightedSum BEFORE simpleSum, using the old simpleSum. weightedSum := weightedSum - simpleSum + (Period * Value); simpleSum := simpleSum - oldestValue + Value; // Store the new value and advance the circular buffer index. buffer[currentIndex] := Value; currentIndex := (currentIndex + 1) mod Period; // The indicator is not ready until the buffer is filled for the first time. if (valueCount < Period) then exit(Double.NaN); Result := weightedSum / denominator; end; end; { THMA } class constructor THMA.CreateClass; begin var paramsType := TDataType.Ordinal; var argsType := TDataType.Float; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var HMA := CreateHMA(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin Result := TDataType.Float.CreateValue(HMA(Args.AsFloat.Value)); end ); end ); end; class function THMA.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var hmaFunc: TConvertFunc; begin hmaFunc := CreateHMA(Params.Period); Result := function(const Value: TArgs): TResult begin Result.HMA := hmaFunc(Value.Value); end; end; end; class function THMA.CreateHMA(Period: Integer): TConvertFunc; begin // Implemented as a pipeline of three efficient WMA indicators. var wmaFuncHalf := TWMA.CreateWMA(Period div 2); var wmaFuncFull := TWMA.CreateWMA(Period); var wmaFuncFinal := TWMA.CreateWMA(Round(Sqrt(Period))); Result := function(const Value: Double): Double var wmaHalf, wmaFull, diff: Double; begin // Step 1: Calculate the two WMAs on the source data. wmaHalf := wmaFuncHalf(Value); wmaFull := wmaFuncFull(Value); // Wait until the longest WMA has a valid value. if IsNan(wmaFull) then exit(Double.NaN); // Step 2: Calculate the intermediate difference value. diff := 2 * wmaHalf - wmaFull; // Step 3: The final WMA is calculated on the difference series. // This will correctly return NaN during its own warm-up phase. Result := wmaFuncFinal(diff); end; end; { TRSI } class constructor TRSI.CreateClass; begin var paramsType := TDataType.Ordinal; var argsType := TDataType.Float; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var RSI := CreateRSI(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin Result := TDataType.Float.CreateValue(RSI(Args.AsFloat.Value)); end ); end ); end; class function TRSI.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var rsiFunc: TConvertFunc; begin rsiFunc := CreateRSI(Params.Period); Result := function(const Value: TArgs): TResult begin Result.RSI := rsiFunc(Value.Value); end; end; end; class function TRSI.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; { TMACD } class constructor TMACD.CreateClass; begin var paramsType := TDataType.RecordOf( [ TDataRecordField.Create('FastPeriod', TDataType.Ordinal), TDataRecordField.Create('SlowPeriod', TDataType.Ordinal), TDataRecordField.Create('SignalPeriod', TDataType.Ordinal) ] ); var argsType := TDataType.Float; var resultType := TDataType.RecordOf( [ TDataRecordField.Create('MacdLine', TDataType.Float), TDataRecordField.Create('SignalLine', TDataType.Float), TDataRecordField.Create('Histogram', TDataType.Float) ] ); var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue var paramsRec: IDataRecordValue; begin paramsRec := Params.AsRecord; var MACD := CreateMACD( TDataType.TValue(paramsRec.Items[0]).AsOrdinal.Value, TDataType.TValue(paramsRec.Items[1]).AsOrdinal.Value, TDataType.TValue(paramsRec.Items[2]).AsOrdinal.Value ); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin var res := MACD(Args.AsFloat.Value); Result := resultType.CreateValue( [ TDataType.Float.CreateValue(res.MacdLine), TDataType.Float.CreateValue(res.SignalLine), TDataType.Float.CreateValue(res.Histogram) ] ); end ); end ); end; class function TMACD.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var macdFunc: TConvertFunc; begin macdFunc := CreateMACD(Params.FastPeriod, Params.SlowPeriod, Params.SignalPeriod); Result := function(const Value: TArgs): TResult begin Result := macdFunc(Value.Value); end; end; end; class function TMACD.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc; begin Result := CreateMACD(TEMA.CreateEMA(FastPeriod), TEMA.CreateEMA(SlowPeriod), TEMA.CreateEMA(SignalPeriod)); end; // Creates a MACD indicator from three provided moving average functions. class function TMACD.CreateMACD(const EmaFast, EmaSlow, EmaSignal: TConvertFunc): TConvertFunc; begin Result := function(const Value: Double): TMACD.TResult 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; { TStochastic } type // A minimal, self-contained Deque (Double-Ended Queue) using a circular array. // This local type is used to implement the O(1) sliding window for Stochastic. TLightDeque = record private FItems: TArray; // Stores absolute value counts, not indices FHead, FCount, FCapacity: Integer; function GetLastValue: Int64; function GetFirstValue: Int64; public constructor Create(ACapacity: Integer); procedure AddLast(AValue: Int64); procedure RemoveLast; procedure RemoveFirst; property Count: Integer read FCount; property Last: Int64 read GetLastValue; property First: Int64 read GetFirstValue; end; constructor TLightDeque.Create(ACapacity: Integer); begin FCapacity := ACapacity; SetLength(FItems, FCapacity); FHead := 0; FCount := 0; end; procedure TLightDeque.AddLast(AValue: Int64); begin if (FCount < FCapacity) then begin var tail := (FHead + FCount) mod FCapacity; FItems[tail] := AValue; inc(FCount); end; end; procedure TLightDeque.RemoveLast; begin if (FCount > 0) then dec(FCount); end; procedure TLightDeque.RemoveFirst; begin if (FCount > 0) then begin FHead := (FHead + 1) mod FCapacity; dec(FCount); end; end; function TLightDeque.GetLastValue: Int64; begin var tail := (FHead + FCount - 1 + FCapacity) mod FCapacity; Result := FItems[tail]; end; function TLightDeque.GetFirstValue: Int64; begin Result := FItems[FHead]; end; class constructor TStochastic.CreateClass; begin var paramsType := TDataType.RecordOf([TDataRecordField.Create('KPeriod', TDataType.Ordinal), TDataRecordField.Create('DPeriod', TDataType.Ordinal)]); var ohlcType := TDataType.RecordOf( [ TDataRecordField.Create('Open', TDataType.Float), TDataRecordField.Create('High', TDataType.Float), TDataRecordField.Create('Low', TDataType.Float), TDataRecordField.Create('Close', TDataType.Float), TDataRecordField.Create('Volume', TDataType.Float) ] ); var argsType := ohlcType; var resultType := TDataType.RecordOf([TDataRecordField.Create('K', TDataType.Float), TDataRecordField.Create('D', TDataType.Float)]); var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue var paramsRec: IDataRecordValue; begin paramsRec := Params.AsRecord; var Stochastic := CreateStochastic( TDataType.TValue(paramsRec.Items[0]).AsOrdinal.Value, TDataType.TValue(paramsRec.Items[1]).AsOrdinal.Value ); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue var ohlcRec: IDataRecordValue; ohlcVal: TOhlcItem; begin ohlcRec := Args.AsRecord; ohlcVal.Open := TDataType.TValue(ohlcRec.Items[0]).AsFloat.Value; ohlcVal.High := TDataType.TValue(ohlcRec.Items[1]).AsFloat.Value; ohlcVal.Low := TDataType.TValue(ohlcRec.Items[2]).AsFloat.Value; ohlcVal.Close := TDataType.TValue(ohlcRec.Items[3]).AsFloat.Value; ohlcVal.Volume := TDataType.TValue(ohlcRec.Items[4]).AsFloat.Value; var res := Stochastic(ohlcVal); Result := resultType.CreateValue([TDataType.Float.CreateValue(res.K), TDataType.Float.CreateValue(res.D)]); end ); end ); end; class function TStochastic.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var stochFunc: TConvertFunc; begin stochFunc := CreateStochastic(Params.KPeriod, Params.DPeriod); Result := function(const Value: TArgs): TResult begin Result := stochFunc(Value.Value); end; end; end; class function TStochastic.CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc; begin Result := CreateStochastic(KPeriod, TSMA.CreateSMA(DPeriod)); end; // Creates a Stochastic Oscillator using an injectable moving average for the %D line. class function TStochastic.CreateStochastic( KPeriod: Integer; const SmaD: TConvertFunc ): TConvertFunc; var buffer: TArray; highDeque: TLightDeque; lowDeque: TLightDeque; valueCount: Int64; begin if (KPeriod <= 0) then begin Result := function(const Value: TOhlcItem): TResult begin Result.K := Double.NaN; Result.D := Double.NaN; end; exit; end; SetLength(buffer, KPeriod); highDeque := TLightDeque.Create(KPeriod); lowDeque := TLightDeque.Create(KPeriod); valueCount := 0; Result := function(const Value: TOhlcItem): TStochastic.TResult var currentIndex, firstIndex, lastIndex: Integer; highestHigh, lowestLow: Double; begin inc(valueCount); currentIndex := (valueCount - 1) mod KPeriod; buffer[currentIndex] := Value; // Update deques using absolute valueCount as item identifier while (highDeque.Count > 0) do begin lastIndex := (highDeque.Last - 1) mod KPeriod; if (buffer[lastIndex].High <= Value.High) then highDeque.RemoveLast else break; end; highDeque.AddLast(valueCount); while (lowDeque.Count > 0) do begin lastIndex := (lowDeque.Last - 1) mod KPeriod; if (buffer[lastIndex].Low >= Value.Low) then lowDeque.RemoveLast else break; end; lowDeque.AddLast(valueCount); // Remove indices that are now outside the window while (highDeque.Count > 0) and (highDeque.First <= valueCount - KPeriod) do highDeque.RemoveFirst; while (lowDeque.Count > 0) and (lowDeque.First <= valueCount - KPeriod) do lowDeque.RemoveFirst; // Calculate Indicator if (valueCount < KPeriod) then begin Result.K := Double.NaN; Result.D := SmaD(Result.K); // Feed NaN to keep SMA in sync end else begin firstIndex := (highDeque.First - 1) mod KPeriod; highestHigh := buffer[firstIndex].High; firstIndex := (lowDeque.First - 1) mod KPeriod; lowestLow := buffer[firstIndex].Low; if (highestHigh > lowestLow) then Result.K := 100 * (Value.Close - lowestLow) / (highestHigh - lowestLow) else Result.K := 100; Result.D := SmaD(Result.K); end; end; end; { TStdDev } class constructor TStdDev.CreateClass; begin var paramsType := TDataType.Ordinal; var argsType := TDataType.Float; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var StdDev := CreateStdDev(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin Result := TDataType.Float.CreateValue(StdDev(Args.AsFloat.Value)); end ); end ); end; class function TStdDev.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var stdDevFunc: TConvertFunc; begin stdDevFunc := CreateStdDev(Params.Period); Result := function(const Value: TArgs): TResult begin Result.StdDev := stdDevFunc(Value.Value); end; end; end; class function TStdDev.CreateStdDev(Period: Integer): TConvertFunc; begin // O(1) implementation using rolling sums of X and X^2 to calculate variance. var sumX, sumX2: Double; var buffer: TArray; var currentIndex: Integer; var valueCount: Integer; if (Period <= 1) then // StdDev requires at least 2 data points begin Result := function(const Value: Double): Double begin Result := Double.NaN; end; exit; end; sumX := 0.0; sumX2 := 0.0; SetLength(buffer, Period); currentIndex := 0; valueCount := 0; Result := function(const Value: Double): Double var oldestValue, variance, mean: Double; begin inc(valueCount); oldestValue := buffer[currentIndex]; buffer[currentIndex] := Value; // Update sums incrementally sumX := sumX - oldestValue + Value; sumX2 := sumX2 - (oldestValue * oldestValue) + (Value * Value); // Advance index currentIndex := (currentIndex + 1) mod Period; if (valueCount < Period) then exit(Double.NaN); // Variance = E[X^2] - (E[X])^2 mean := sumX / Period; variance := (sumX2 / Period) - (mean * mean); // Prevent negative variance from floating point inaccuracies if (variance < 0) then variance := 0; Result := Sqrt(variance); end; end; { TBollingerBands } class constructor TBollingerBands.CreateClass; begin var paramsType := TDataType.RecordOf([TDataRecordField.Create('Period', TDataType.Ordinal), TDataRecordField.Create('Multiplier', TDataType.Float)]); var argsType := TDataType.Float; var resultType := TDataType.RecordOf( [ TDataRecordField.Create('UpperBand', TDataType.Float), TDataRecordField.Create('MiddleBand', TDataType.Float), TDataRecordField.Create('LowerBand', TDataType.Float) ] ); var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue var paramsRec: IDataRecordValue; begin paramsRec := Params.AsRecord; var BollingerBands := CreateBollingerBands( TDataType.TValue(paramsRec.Items[0]).AsOrdinal.Value, TDataType.TValue(paramsRec.Items[1]).AsFloat.Value ); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue begin var res := BollingerBands(Args.AsFloat.Value); Result := resultType.CreateValue( [ TDataType.Float.CreateValue(res.UpperBand), TDataType.Float.CreateValue(res.MiddleBand), TDataType.Float.CreateValue(res.LowerBand) ] ); end ); end ); end; class function TBollingerBands.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var bbFunc: TConvertFunc; begin bbFunc := CreateBollingerBands(Params.Period, Params.Multiplier); Result := function(const Value: TArgs): TResult begin Result := bbFunc(Value.Value); end; end; end; class function TBollingerBands.CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc; begin // Implemented as a pipeline of efficient SMA and StdDev indicators. var smaFunc := TSMA.CreateSMA(Period); var stdDevFunc := TStdDev.CreateStdDev(Period); Result := function(const Value: Double): TResult var middleBand, stdDev: Double; begin // Calculate middle band (SMA) and standard deviation in parallel. middleBand := smaFunc(Value); stdDev := stdDevFunc(Value); // Wait until both indicators are ready (they have the same period). if IsNan(middleBand) then begin Result.MiddleBand := Double.NaN; Result.UpperBand := Double.NaN; Result.LowerBand := Double.NaN; end else begin Result.MiddleBand := middleBand; Result.UpperBand := middleBand + (stdDev * Multiplier); Result.LowerBand := middleBand - (stdDev * Multiplier); end; end; end; { TATR } class constructor TATR.CreateClass; begin var paramsType := TDataType.Ordinal; var ohlcType := TDataType.RecordOf( [ TDataRecordField.Create('Open', TDataType.Float), TDataRecordField.Create('High', TDataType.Float), TDataRecordField.Create('Low', TDataType.Float), TDataRecordField.Create('Close', TDataType.Float), TDataRecordField.Create('Volume', TDataType.Float) ] ); var argsType := ohlcType; var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var ATR := CreateATR(Params.AsOrdinal.Value); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue var ohlcRec: IDataRecordValue; ohlcVal: TOhlcItem; begin ohlcRec := Args.AsRecord; ohlcVal.Open := TDataType.TValue(ohlcRec.Items[0]).AsFloat.Value; ohlcVal.High := TDataType.TValue(ohlcRec.Items[1]).AsFloat.Value; ohlcVal.Low := TDataType.TValue(ohlcRec.Items[2]).AsFloat.Value; ohlcVal.Close := TDataType.TValue(ohlcRec.Items[3]).AsFloat.Value; ohlcVal.Volume := TDataType.TValue(ohlcRec.Items[4]).AsFloat.Value; Result := TDataType.Float.CreateValue(ATR(ohlcVal)); end ); end ); end; class function TATR.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var atrFunc: TConvertFunc; begin atrFunc := CreateATR(Params.Period); Result := function(const Value: TArgs): TResult begin Result.ATR := atrFunc(Value.Value); end; end; end; class function TATR.CreateATR(Period: Integer): TConvertFunc; begin Result := CreateATR(TEMA.CreateEMA(Period)); end; // Calculates the Average True Range (ATR) using an injectable moving average. class function TATR.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; { TKeltnerChannels } class constructor TKeltnerChannels.CreateClass; begin var paramsType := TDataType.RecordOf([TDataRecordField.Create('Period', TDataType.Ordinal), TDataRecordField.Create('Multiplier', TDataType.Float)]); var ohlcType := TDataType.RecordOf( [ TDataRecordField.Create('Open', TDataType.Float), TDataRecordField.Create('High', TDataType.Float), TDataRecordField.Create('Low', TDataType.Float), TDataRecordField.Create('Close', TDataType.Float), TDataRecordField.Create('Volume', TDataType.Float) ] ); var argsType := ohlcType; var resultType := TDataType.RecordOf( [ TDataRecordField.Create('UpperBand', TDataType.Float), TDataRecordField.Create('MiddleBand', TDataType.Float), TDataRecordField.Create('LowerBand', TDataType.Float) ] ); var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(paramsType, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue var paramsRec: IDataRecordValue; begin paramsRec := Params.AsRecord; var KeltnerChannels := CreateKeltnerChannels( TDataType.TValue(paramsRec.Items[0]).AsOrdinal.Value, TDataType.TValue(paramsRec.Items[1]).AsFloat.Value ); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue var ohlcRec: IDataRecordValue; ohlcVal: TOhlcItem; begin ohlcRec := Args.AsRecord; ohlcVal.Open := TDataType.TValue(ohlcRec.Items[0]).AsFloat.Value; ohlcVal.High := TDataType.TValue(ohlcRec.Items[1]).AsFloat.Value; ohlcVal.Low := TDataType.TValue(ohlcRec.Items[2]).AsFloat.Value; ohlcVal.Close := TDataType.TValue(ohlcRec.Items[3]).AsFloat.Value; ohlcVal.Volume := TDataType.TValue(ohlcRec.Items[4]).AsFloat.Value; var res := KeltnerChannels(ohlcVal); Result := resultType.CreateValue( [ TDataType.Float.CreateValue(res.UpperBand), TDataType.Float.CreateValue(res.MiddleBand), TDataType.Float.CreateValue(res.LowerBand) ] ); end ); end ); end; class function TKeltnerChannels.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var kcFunc: TConvertFunc; begin kcFunc := CreateKeltnerChannels(Params.Period, Params.Multiplier); Result := function(const Value: TArgs): TResult begin Result := kcFunc(Value.Value); end; end; end; class function TKeltnerChannels.CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConvertFunc; begin Result := CreateKeltnerChannels(TEMA.CreateEMA(Period), TATR.CreateATR(Period), Multiplier); end; // Calculates Keltner Channels using an injectable ATR and middle band moving average. class function TKeltnerChannels.CreateKeltnerChannels( const MovAvgMiddle: TConvertFunc; const AtrFunc: TConvertFunc; Multiplier: Double ): TConvertFunc; begin Result := function(const Value: TOhlcItem): TKeltnerChannels.TResult 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; { TMean } class constructor TMean.CreateClass; begin var argsType := TDataType.ArrayOf(TDataType.Float); var resultType := TDataType.Float; var indicatorMethodType := TDataType.MethodOf(argsType, resultType); var factoryMethodType := TDataType.MethodOf(TDataType.Void, indicatorMethodType); FFactory := factoryMethodType.CreateValue( function(const Params: TDataType.TValue): TDataType.TValue begin var Mean := CreateMean(); Result := indicatorMethodType.CreateValue( function(const Args: TDataType.TValue): TDataType.TValue var i: Integer; valuesArray: IDataArrayValue; values: TArray; begin valuesArray := Args.AsArray; SetLength(values, valuesArray.ElementCount); for i := 0 to valuesArray.ElementCount - 1 do values[i] := TDataType.TValue(valuesArray.Items[i]).AsFloat.Value; Result := TDataType.Float.CreateValue(Mean(values)); end ); end ); end; class function TMean.CreateFactory: TIndicatorFactoryProc; begin Result := function(const Params: TParams): TConvertFunc var meanFunc: TConvertFunc, Double>; begin meanFunc := CreateMean(); Result := function(const Value: TArgs): TResult begin Result.Mean := meanFunc(Value.Values); end; end; end; class function TMean.CreateMean: TConvertFunc, Double>; begin Result := function(const Value: TArray): Double var i: Integer; sum: Double; begin if Length(Value) = 0 then exit(NaN); sum := 0.0; for i := 0 to High(Value) do sum := sum + Value[i]; Result := sum / Length(Value); end; end; initialization IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; IndicatorRegistry.RegisterTemplate; end.