From ecee8b37bc2f9746af32aef81bbe0cbafe848923 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sun, 27 Jul 2025 16:41:54 +0200 Subject: [PATCH] Generic indicator factory --- AuraTrader/MainForm.pas | 22 +- AuraTrader/StrategyTest.pas | 6 +- AuraTrader/TestMethodCallFromRecordParams.pas | 7 +- Src/Myc.Data.Records.pas | 7 - Src/Myc.Trade.Indicators.Common.pas | 1044 +++++++++++++---- Src/Myc.Trade.Indicators.pas | 15 +- 6 files changed, 827 insertions(+), 274 deletions(-) diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index 03db26e..19874ed 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -312,13 +312,13 @@ begin var Closes := Ohlc.Field('Close'); - var Hull := Closes.Chain(TIndicators.CreateHMA(250)).MakeParallel; - var Sma := Closes.Chain(TIndicators.CreateSMA(200)).MakeParallel; + var Hull := Closes.Chain(THMA.CreateHMA(250)).MakeParallel; + var Sma := Closes.Chain(TSMA.CreateSMA(200)).MakeParallel; var Lowest: Double := Double.MaxValue; var Highest: Double := Double.MinValue; - var ATR := Ohlc.Chain(TIndicators.CreateATR(50)).MakeParallel; + var ATR := Ohlc.Chain(TATR.CreateATR(50)).MakeParallel; // next stage @@ -471,7 +471,7 @@ begin panel.AddDoubleSeries(Signal.Producer.Field('Entry'), TAlphaColors.Green, 1); panel.AddDoubleSeries(Signal.Producer.Field('SL'), TAlphaColors.Red, 2); - var mean := TConverter, Double>.CreateConverter(TIndicators.CreateMean()); + var mean := TConverter, Double>.CreateConverter(TMean.CreateMean()); TConverter.Join([Hull, Sma]).Chain(mean); @@ -616,13 +616,13 @@ begin var Ohlc := OhlcPoint.Field('Data'); var Closes := Ohlc.Field('Close'); - var Hull := Closes.Chain(TIndicators.CreateHMA(150)); - var Sma := Closes.Chain(TIndicators.CreateSMA(50)); - var Ema := Closes.Chain(TIndicators.CreateEMA(21)); - var Boli := Closes.MakeParallel.Chain(TIndicators.CreateBollingerBands(20, 2.0)); - var Rsi := Closes.Chain(TIndicators.CreateRSI(14)); - var Macd := Closes.MakeParallel.Chain(TIndicators.CreateMACD(12, 26, 9)); - var Stoch := Ohlc.Chain(TIndicators.CreateStochastic(14, 3)); + var Hull := Closes.Chain(THMA.CreateHMA(150)); + var Sma := Closes.Chain(TSMA.CreateSMA(50)); + var Ema := Closes.Chain(TEMA.CreateEMA(21)); + var Boli := Closes.MakeParallel.Chain(TBollingerBands.CreateBollingerBands(20, 2.0)); + var Rsi := Closes.Chain(TRSI.CreateRSI(14)); + var Macd := Closes.MakeParallel.Chain(TMACD.CreateMACD(12, 26, 9)); + var Stoch := Ohlc.Chain(TStochastic.CreateStochastic(14, 3)); chart.SetXAxisSeries(timeframe, Timestamps); diff --git a/AuraTrader/StrategyTest.pas b/AuraTrader/StrategyTest.pas index 9210629..1a81e15 100644 --- a/AuraTrader/StrategyTest.pas +++ b/AuraTrader/StrategyTest.pas @@ -35,9 +35,9 @@ begin var Closes := Ohlc.Field('Close'); - var Hull := Closes.Chain(TIndicators.CreateHMA(250)).MakeParallel; - var Sma := Closes.Chain(TIndicators.CreateSMA(200)).MakeParallel; - var ATR := Ohlc.Chain(TIndicators.CreateATR(50)).MakeParallel; + var Hull := Closes.Chain(THMA.CreateHMA(250)).MakeParallel; + var Sma := Closes.Chain(TSMA.CreateSMA(200)).MakeParallel; + var ATR := Ohlc.Chain(TATR.CreateATR(50)).MakeParallel; var conv := TConverter.Join([Ohlc.Field('Low'), Ohlc.Field('High'), Closes, ATR, Hull, Sma]); diff --git a/AuraTrader/TestMethodCallFromRecordParams.pas b/AuraTrader/TestMethodCallFromRecordParams.pas index 8eb81ac..f3b1fba 100644 --- a/AuraTrader/TestMethodCallFromRecordParams.pas +++ b/AuraTrader/TestMethodCallFromRecordParams.pas @@ -6,6 +6,7 @@ uses System.SysUtils, System.Classes, Myc.Data.Records, + Myc.Data.Pipeline, Myc.Trade.Indicators; type @@ -61,7 +62,7 @@ uses class function TMyWorker.CreateFactory: TIndicatorFactoryProc; begin Result := - function(const Params: TParams): TIndicatorProc + function(const Params: TParams): TConvertFunc begin var Log := TStrings(Params.Log); var text := Params.text; @@ -101,7 +102,7 @@ end; class function TSmaIndicator.CreateFactory: TIndicatorFactoryProc; begin Result := - function(const Params: TParams): TIndicatorProc + function(const Params: TParams): TConvertFunc var // State for the indicator closure period: Integer; @@ -150,7 +151,7 @@ procedure TestSma(const Log: TStrings); var fact: TGenericIndicatorFactory; params: TDataRecord; - indi: TIndicatorProc; + indi: TConvertFunc; args: TDataRecord; res: TDataRecord; smaValue: Double; diff --git a/Src/Myc.Data.Records.pas b/Src/Myc.Data.Records.pas index 51163f4..a3e28e0 100644 --- a/Src/Myc.Data.Records.pas +++ b/Src/Myc.Data.Records.pas @@ -136,15 +136,8 @@ end; procedure TDataRecord.CopyFrom(const Src: T); begin - var ctx := TRttiContext.Create; - var rttiType := ctx.GetType(TypeInfo(T)); - for var i := 0 to High(FLayout.FFields) do - begin - var P: PByte := @Src; - inc(P, FLayout.Fields[i].Offset); FLayout.Fields[i].FromType(FBuffer, Src); - end; end; procedure TDataRecord.CopyValue(const SrcRec: TDataRecord; SrcIdx, DstIdx: Integer); diff --git a/Src/Myc.Trade.Indicators.Common.pas b/Src/Myc.Trade.Indicators.Common.pas index 206f141..1e373f2 100644 --- a/Src/Myc.Trade.Indicators.Common.pas +++ b/Src/Myc.Trade.Indicators.Common.pas @@ -5,176 +5,431 @@ interface uses System.SysUtils, System.Math, - System.Generics.Collections, System.Rtti, Myc.Data.Records, Myc.Data.Pipeline, Myc.Data.Series, - Myc.Trade.Types; + Myc.Trade.Types, + Myc.Trade.Indicators; 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; + [IndicatorName('SMA', 'Simple Moving Average')] + [IndicatorHint('Calculates the average of a selected range of prices.')] + TSMA = class public - // Simple Moving Average + 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; - // Exponential Moving Average + end; + + [IndicatorName('EMA', 'Exponential Moving Average')] + [IndicatorHint('A moving average that places greater weight on the most recent data points.')] + TEMA = class + 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; - // Hull Moving Average + end; + + [IndicatorName('WMA', 'Weighted Moving Average')] + [IndicatorHint('A moving average that places greater weight on more recent data points.')] + TWMA = class + 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; + end; + + [IndicatorName('HMA', 'Hull Moving Average')] + [IndicatorHint('A fast, smooth moving average that minimizes lag.')] + THMA = class + 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; - // Relative Strength Index + end; + + [IndicatorName('RSI', 'Relative Strength Index')] + [IndicatorHint('A momentum indicator measuring the magnitude of recent price changes.')] + TRSI = class + 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; - // Moving Average Convergence Divergence - class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc; overload; static; + end; + + [IndicatorName('MACD', 'Moving Average Convergence Divergence')] + [IndicatorHint('A trend-following momentum indicator showing the relationship between two EMAs.')] + TMACD = class + 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; - // Stochastic Oscillator - class function CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc; overload; static; + ): TConvertFunc; overload; static; + end; + + [IndicatorName('Stoch', 'Stochastic Oscillator')] + [IndicatorHint('A momentum indicator comparing a closing price to a range of its prices.')] + TStochastic = class + 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; - // Bollinger Bands - class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc; static; - // Average True Range + ): TConvertFunc; overload; static; + end; + + [IndicatorName('StdDev', 'Standard Deviation')] + [IndicatorHint('Measures the amount of variation or dispersion of a set of values.')] + TStdDev = class + 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; + end; + + [IndicatorName('BB', 'Bollinger Bands')] + [IndicatorHint('Characterizes prices and volatility over time using standard deviation bands.')] + TBollingerBands = class + 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; + end; + + [IndicatorName('ATR', 'Average True Range')] + [IndicatorHint('Measures market volatility by decomposing the entire range of an asset price.')] + TATR = class + 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; - // Keltner Channels - class function CreateKeltnerChannels( - Period: Integer; - Multiplier: Double - ): TConvertFunc; overload; static; + end; + + [IndicatorName('KC', 'Keltner Channels')] + [IndicatorHint('A volatility-based indicator composed of an EMA and two ATR-based outer lines.')] + TKeltnerChannels = class + 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; + ): TConvertFunc; overload; static; + end; + [IndicatorName('Mean', 'Mean Value')] + [IndicatorHint('Calculates the arithmetic mean of an array of values.')] + TMean = class + 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; end; 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); +type + // A minimal, self-contained Deque (Double-Ended Queue) using a circular array. + TLightDeque = record + private + FItems: TArray; + FHead, FCount, FCapacity: Integer; + function GetLastIndex: Integer; + function GetFirstIndex: Integer; + public + constructor Create(ACapacity: Integer); + procedure AddLast(AValue: Integer); + procedure RemoveLast; + procedure RemoveFirst; + property Count: Integer read FCount; + property Last: Integer read GetLastIndex; + property First: Integer read GetFirstIndex; end; - Result := numerator / denominator; +constructor TLightDeque.Create(ACapacity: Integer); +begin + FCapacity := ACapacity; + SetLength(FItems, FCapacity); + FHead := 0; + FCount := 0; end; -class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc; +procedure TLightDeque.AddLast(AValue: Integer); 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 (FCount < FCapacity) then + begin + var tail := (FHead + FCount) mod FCapacity; + FItems[tail] := AValue; + inc(FCount); + end; +end; - 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; +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.GetLastIndex: Integer; +begin + var tail := (FHead + FCount - 1 + FCapacity) mod FCapacity; + Result := FItems[tail]; +end; + +function TLightDeque.GetFirstIndex: Integer; +begin + Result := FItems[FHead]; +end; + +{ TSMA } + +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 TIndicators.CreateEMA(Period: Integer): TConvertFunc; +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 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 := 2 / (Period + 1); + var multiplier: Double; + + if (Period > 0) then + multiplier := 2 / (Period + 1) + else + multiplier := 0; Result := function(const Value: Double): Double @@ -184,7 +439,7 @@ begin if (sourceData.Count < Period) then begin Result := Double.NaN; - Exit; + exit; end; if not IsNan(lastEma) then @@ -194,91 +449,149 @@ begin end else begin - // First EMA is a SMA of the initial period - lastEma := CalculateSMA(sourceData, Period); + // 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; -class function TIndicators.CreateHMA(Period: Integer): TConvertFunc; +{ TWMA } + +class function TWMA.CreateFactory: TIndicatorFactoryProc; begin - var periodHalf := Period div 2; - var periodSqrt := Round(Sqrt(Period)); - var sourceData: TSeries; - var diffSeries: TSeries; + 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 - price: Double; - wmaHalf, wmaFull, diff: Double; + oldestValue: Double; begin - price := Value; + inc(valueCount); - // Default HMA to NaN for the warm-up period. - Result := Double.NaN; + // Get the value that will be overwritten. Initially, this is 0.0. + oldestValue := buffer[currentIndex]; - // Add new price to the source data array, respecting the lookback period. - sourceData.Add(price, Period); + // --- Corrected Rolling Calculation --- + // IMPORTANT: Update weightedSum BEFORE simpleSum, using the old simpleSum. + weightedSum := weightedSum - simpleSum + (Period * Value); + simpleSum := simpleSum - oldestValue + Value; - // 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); + // Store the new value and advance the circular buffer index. + buffer[currentIndex] := Value; + currentIndex := (currentIndex + 1) mod Period; - // Calculate the difference and add to the intermediate series. - diff := 2 * wmaHalf - wmaFull; - diffSeries.Add(diff, periodSqrt); + // The indicator is not ready until the buffer is filled for the first time. + if (valueCount < Period) then + exit(Double.NaN); - // 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; + Result := weightedSum / denominator; 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; +{ THMA } -// Creates a MACD indicator from three provided moving average functions. -class function TIndicators.CreateMACD(const EmaFast, EmaSlow, EmaSignal: TConvertFunc): TConvertFunc; +class function THMA.CreateFactory: TIndicatorFactoryProc; begin Result := - function(const Value: Double): TMacdResult + function(const Params: TParams): TConvertFunc var - fastVal, slowVal: Double; + hmaFunc: TConvertFunc; 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; + hmaFunc := CreateHMA(Params.Period); + Result := function(const Value: TArgs): TResult begin Result.HMA := hmaFunc(Value.Value); end; end; end; -class function TIndicators.CreateRSI(Period: Integer): TConvertFunc; +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 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; @@ -337,76 +650,286 @@ begin end; end; -class function TIndicators.CreateSMA(Period: Integer): TConvertFunc; +{ TMACD } + +class function TMACD.CreateFactory: TIndicatorFactoryProc; begin - var sourceData: TSeries; Result := - function(const Value: Double): Double + function(const Params: TParams): TConvertFunc + var + macdFunc: TConvertFunc; begin - sourceData.Add(Value, Period); - if (sourceData.Count >= Period) then - Result := CalculateSMA(sourceData, Period) - else - Result := Double.NaN; + macdFunc := CreateMACD(Params.FastPeriod, Params.SlowPeriod, Params.SignalPeriod); + Result := function(const Value: TArgs): TResult begin Result := macdFunc(Value.Value); end; end; end; -// Standard Stochastic Oscillator using an SMA for the %D line. -class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc; +class function TMACD.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc; begin - Result := CreateStochastic(KPeriod, CreateSMA(DPeriod)); + Result := CreateMACD(TEMA.CreateEMA(FastPeriod), TEMA.CreateEMA(SlowPeriod), TEMA.CreateEMA(SignalPeriod)); end; -// Creates a Stochastic Oscillator using an injectable moving average for the %D line. -class function TIndicators.CreateStochastic( - KPeriod: Integer; - const SmaD: TConvertFunc -): TConvertFunc; +{ TMACD } + +// Creates a MACD indicator from three provided moving average functions. +class function TMACD.CreateMACD(const EmaFast, EmaSlow, EmaSignal: TConvertFunc): TConvertFunc; begin - var sourceData: TSeries; - Result := - function(const Value: TOhlcItem): TStochasticResult + function(const Value: Double): TMACD.TResult var - i: Integer; - highestHigh, lowestLow: Double; + fastVal, slowVal: Double; begin - sourceData.Add(Value, KPeriod); - Result.K := Double.NaN; - Result.D := Double.NaN; + fastVal := EmaFast(Value); + slowVal := EmaSlow(Value); - if (sourceData.Count >= KPeriod) then + if IsNan(slowVal) then // slowVal will be the last one to become non-NaN 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) + 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.K := 100; // Or 50, depends on convention - - Result.D := SmaD(Result.K); + Result.Histogram := Double.NaN; end; end; end; -// Standard ATR using an EMA for smoothing. -class function TIndicators.CreateATR(Period: Integer): TConvertFunc; +{ TStochastic } + +class function TStochastic.CreateFactory: TIndicatorFactoryProc; begin - Result := CreateATR(CreateEMA(Period)); + 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; + currentIndex: Integer; + valueCount: Integer; +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); + currentIndex := 0; + valueCount := 0; + + Result := + function(const Value: TOhlcItem): TStochastic.TResult + var + highestHigh, lowestLow: Double; + windowStart: Integer; + begin + inc(valueCount); + buffer[currentIndex] := Value; + + // Update deques for rolling min/max + while (highDeque.Count > 0) and (buffer[highDeque.Last].High <= Value.High) do + highDeque.RemoveLast; + highDeque.AddLast(currentIndex); + + while (lowDeque.Count > 0) and (buffer[lowDeque.Last].Low >= Value.Low) do + lowDeque.RemoveLast; + lowDeque.AddLast(currentIndex); + + // Remove indices that are now outside the window + windowStart := (currentIndex - KPeriod + 1 + KPeriod) mod KPeriod; + if (valueCount > KPeriod) then + begin + if (highDeque.First = windowStart - 1) or ((windowStart = 0) and (highDeque.First = KPeriod - 1)) then + highDeque.RemoveFirst; + if (lowDeque.First = windowStart - 1) or ((windowStart = 0) and (lowDeque.First = KPeriod - 1)) then + lowDeque.RemoveFirst; + end; + + if (valueCount < KPeriod) then + begin + Result.K := Double.NaN; + Result.D := SmaD(Result.K); + end + else + begin + highestHigh := buffer[highDeque.First].High; + lowestLow := buffer[lowDeque.First].Low; + + if (highestHigh > lowestLow) then + Result.K := 100 * (Value.Close - lowestLow) / (highestHigh - lowestLow) + else + Result.K := 100; + + Result.D := SmaD(Result.K); + end; + + currentIndex := (currentIndex + 1) mod KPeriod; + end; +end; + +{ TStdDev } + +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 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 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 TIndicators.CreateATR(const MovAvgTR: TConvertFunc): TConvertFunc; +class function TATR.CreateATR(const MovAvgTR: TConvertFunc): TConvertFunc; begin var sourceData: TSeries; @@ -433,21 +956,35 @@ begin 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; +{ TKeltnerChannels } + +class function TKeltnerChannels.CreateFactory: + TIndicatorFactoryProc; begin - Result := CreateKeltnerChannels(CreateEMA(Period), CreateATR(Period), Multiplier); + 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 TIndicators.CreateKeltnerChannels( +class function TKeltnerChannels.CreateKeltnerChannels( const MovAvgMiddle: TConvertFunc; const AtrFunc: TConvertFunc; Multiplier: Double -): TConvertFunc; +): TConvertFunc; begin Result := - function(const Value: TOhlcItem): TKeltnerChannelsResult + function(const Value: TOhlcItem): TKeltnerChannels.TResult var atrValue, middleValue, typicalPrice: Double; begin @@ -472,17 +1009,36 @@ begin end; end; -class function TIndicators.CreateMean: TConvertFunc, Double>; +{ TMean } + +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); - Result := Value[0]; - for var i := 1 to High(Value) do - Result := Result + Value[i]; - Result := Result / Length(Value); + + sum := 0.0; + for i := 0 to High(Value) do + sum := sum + Value[i]; + + Result := sum / Length(Value); end; end; diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas index ab6664d..0d50d87 100644 --- a/Src/Myc.Trade.Indicators.pas +++ b/Src/Myc.Trade.Indicators.pas @@ -3,13 +3,13 @@ unit Myc.Trade.Indicators; interface uses + Myc.Data.Pipeline, Myc.Data.Records; {$M+} type - TIndicatorProc = reference to function(const Value: TValue): TResult; - TIndicatorFactoryProc = reference to function(const Params: TParams): TIndicatorProc; + TIndicatorFactoryProc = reference to function(const Params: TParams): TConvertFunc; (* Sample definition of an indicator template: @@ -33,6 +33,9 @@ type [IndicatorFactory] class function CreateFactory: TIndicatorFactoryProc; static; + + // Hard coded version: + class function CreateHMA( Period: Integer ): TConvertFunc; static; end; *) @@ -66,7 +69,7 @@ type function GetResultLayout: TDataRecord.TLayout; {$endregion} - function CreateIndicator(const Params: TDataRecord): TIndicatorProc; + function CreateIndicator(const Params: TDataRecord): TConvertFunc; property ParameterLayout: TDataRecord.TLayout read GetParameterLayout; property ArgumentLayout: TDataRecord.TLayout read GetArgumentLayout; @@ -94,7 +97,7 @@ type class function CreateFromTemplate: TGenericIndicatorFactory; - function CreateIndicator(const Params: TDataRecord): TIndicatorProc; + function CreateIndicator(const Params: TDataRecord): TConvertFunc; property ParameterLayout: TDataRecord.TLayout read GetParameterLayout; property ArgumentLayout: TDataRecord.TLayout read GetArgumentLayout; @@ -297,7 +300,7 @@ begin // Create the main factory procedure. This is a double-nested anonymous method // that wraps the template's specific factory and worker functions. factoryProc := - function(const Params: TDataRecord): TIndicatorProc + function(const Params: TDataRecord): TConvertFunc begin // Outer anonymous method: This is the factory proc. // It gets called with a TDataRecord of parameters. @@ -364,7 +367,7 @@ begin Result := TGenericIndicatorFactory.Create(parameterLayout, argumentLayout, resultLayout, factoryProc, shortName, name, hint); end; -function TGenericIndicatorFactory.CreateIndicator(const Params: TDataRecord): TIndicatorProc; +function TGenericIndicatorFactory.CreateIndicator(const Params: TDataRecord): TConvertFunc; begin Assert( (not Assigned(FParameterLayout.Fields)) or (Params.Layout = FParameterLayout),