diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index 5d16e48..d5fa573 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -12,7 +12,8 @@ uses DynamicFMXControl in 'DynamicFMXControl.pas', FirstStrategy in 'FirstStrategy.pas', Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', - Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas'; + Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas', + Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas'; {$R *.res} diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index eca116a..6670364 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -141,6 +141,7 @@ + Base diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas index 3ce4162..6b7e93a 100644 --- a/AuraTrader/FirstStrategy.pas +++ b/AuraTrader/FirstStrategy.pas @@ -3,6 +3,7 @@ unit FirstStrategy; interface uses + System.SysUtils, System.Generics.Collections, Myc.Signals, Myc.Lazy, @@ -42,29 +43,26 @@ type property Timeframe: TTimeframe read GetTimeframe; end; - // Implements the Hull Moving Average indicator. - THullMovingAverage = class(TMycConverter) - private - FPeriod: Integer; - FPeriodHalf: Integer; - FPeriodSqrt: Integer; - // Source data for HMA calculation - FSourceData: TMycDataArray; - // Intermediate data series for HMA calculation (2*WMA(n/2) - WMA(n)) - FDiffSeries: TMycDataArray; + TIndicator = class(TMycConverter) + strict private FQueue: TQueue; - // Calculates the Weighted Moving Average for the most recent data. - function CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; protected - function ProcessData(const Value: Double): TState; override; + function ProcessData(const Value: S): TState; override; final; + function Calculate(const Value: S): T; virtual; abstract; + end; + + TGenericIndicator = class(TIndicator) + private + FFunc: TFunc; + protected + function Calculate(const Value: S): T; override; final; public - constructor Create(const APeriod: Integer); + constructor Create(const AFunc: TFunc); end; implementation uses - System.SysUtils, System.DateUtils, System.Math; @@ -149,89 +147,6 @@ begin end; end; -{ THullMovingAverage } - -constructor THullMovingAverage.Create(const APeriod: Integer); -begin - inherited Create; - FPeriod := APeriod; - FPeriodHalf := APeriod div 2; - FPeriodSqrt := Round(Sqrt(APeriod)); - - // Initialize data arrays. - FSourceData := TMycDataArray.CreateEmpty; - FDiffSeries := TMycDataArray.CreateEmpty; -end; - -function THullMovingAverage.CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; -var - i: Integer; - numerator: Double; - denominator: Int64; -begin - // Ensure there is enough data to calculate the WMA - if (Series.Count < Period) or (Period <= 0) then - Exit(0.0); - - numerator := 0; - // The sum of weights (1 + 2 + ... + Period) - denominator := Period * (Period + 1) div 2; - - if (denominator = 0) then - Exit(0.0); - - for i := 0 to Period - 1 do - begin - // Newest data (index 0) gets the highest weight (Period) - numerator := numerator + Series[i] * (Period - i); - end; - - Result := numerator / denominator; -end; - -function THullMovingAverage.ProcessData(const Value: Double): TState; -begin - Result := - FQueue.Enqueue( - function: TState - var - price: Double; - wmaHalf, wmaFull, diff: Double; - hma: Double; - begin - price := Value; - - // Default HMA to NaN for the warm-up period. - hma := Double.NaN; - - // Add new price to the source data array, respecting the lookback period. - FSourceData := FSourceData.Add(price, FPeriod); - - // Check if there is enough data to start the first stage of calculation. - if (FSourceData.Count >= FPeriod) then - begin - // Calculate the two WMAs for the first step. - wmaHalf := CalculateWMA(FSourceData, FPeriodHalf); - wmaFull := CalculateWMA(FSourceData, FPeriod); - - // Calculate the difference and add to the intermediate series. - diff := 2 * wmaHalf - wmaFull; - FDiffSeries := FDiffSeries.Add(diff, FPeriodSqrt); - - // Check if there is enough intermediate data for the final calculation. - if (FDiffSeries.Count >= FPeriodSqrt) then - begin - // Calculate the final HMA value, overwriting the default 0.0. - hma := CalculateWMA(FDiffSeries, FPeriodSqrt); - end; - end; - - // Broadcast the result - Result := Broadcast(hma); - end - ); -end; - { TMycGenericConverter } constructor TMycGenericConverter.Create(const AFunc: TConvertFunc); @@ -245,4 +160,20 @@ begin Result := Broadcast(FFunc(Value)); end; +function TIndicator.ProcessData(const Value: S): TState; +begin + Result := FQueue.Enqueue(function: TState begin Result := Broadcast(Calculate(Value)); end); +end; + +constructor TGenericIndicator.Create(const AFunc: TFunc); +begin + inherited Create; + FFunc := AFunc; +end; + +function TGenericIndicator.Calculate(const Value: S): T; +begin + Result := FFunc(Value); +end; + end. diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index a1b4059..3f0a087 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -121,7 +121,8 @@ var implementation uses - TestModule; + TestModule, + Myc.Trade.Indicators; {$R *.fmx} @@ -180,24 +181,82 @@ begin Ohlc.Sender.Link(Closes); - for var i := 0 to 3 do - begin - var Hull: IMycConverter := THullMovingAverage.Create(50 + (500 * i)); - Closes.Sender.Link(Hull); - var col: TAlphaColorRec; - col.R := 25 * i; - col.G := 255 - 25 * i; - col.B := 100 + 5 * i; - col.A := 255; - chart.AddDoubleSeries(Hull.Sender, col.Color); - end; + var Hull: IMycConverter := TGenericIndicator.Create(TIndicators.CreateHMA(150)); + Closes.Sender.Link(Hull); + chart.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue); - // var Hull: IMycConverter := THullMovingAverage.Create(250); - // - // Closes.Sender.Link(Hull); - // - // chart.AddDoubleSeries(Hull.Sender); + // Add SMA (Simple Moving Average) + var Sma: IMycConverter := TGenericIndicator.Create(TIndicators.CreateSMA(50)); + Closes.Sender.Link(Sma); + chart.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow); + // Add EMA (Exponential Moving Average) + var Ema: IMycConverter := TGenericIndicator.Create(TIndicators.CreateEMA(21)); + Closes.Sender.Link(Ema); + chart.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua); + + // Add Bollinger Bands (20, 2.0) + var Boli: IMycConverter := + TGenericIndicator.Create(TIndicators.CreateBollingerBands(20, 2.0)); + Closes.Sender.Link(Boli); + + var BoliUpper: IMycConverter := + TMycGenericConverter + .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end); + Boli.Sender.Link(BoliUpper); + chart.AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray); + + var BoliMiddle: IMycConverter := + TMycGenericConverter + .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.MiddleBand; end); + Boli.Sender.Link(BoliMiddle); + chart.AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0); + + var BoliLower: IMycConverter := + TMycGenericConverter + .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.LowerBand; end); + Boli.Sender.Link(BoliLower); + chart.AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray); + + var rsiChart := TMycChart.Create(Self); + AlignControl(rsiChart); + rsiChart.Height := Layout.ChildrenRect.Width * 9 / 32; + rsiChart.Lookback.Value := 50000; + rsiChart.SetXAxisSeries(Timestamps.Sender); + + // Add RSI (Relative Strength Index) + var Rsi: IMycConverter := TGenericIndicator.Create(TIndicators.CreateRSI(14)); + Closes.Sender.Link(Rsi); + rsiChart.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia); + { + // Add MACD (12, 26, 9) + var Macd: IMycConverter := TGenericIndicator.Create(TIndicators.CreateMACD(12, 26, 9)); + Closes.Sender.Link(Macd); + + var MacdLine: IMycConverter := TMycGenericConverter.Create(function(const Item: TMacdResult): Double begin Result := Item.MacdLine; end); + Macd.Sender.Link(MacdLine); + chart.AddDoubleSeries(MacdLine.Sender, TAlphaColors.Orange); + + var MacdSignal: IMycConverter := TMycGenericConverter.Create(function(const Item: TMacdResult): Double begin Result := Item.SignalLine; end); + Macd.Sender.Link(MacdSignal); + chart.AddDoubleSeries(MacdSignal.Sender, TAlphaColors.Dodgerblue); + + var MacdHist: IMycConverter := TMycGenericConverter.Create(function(const Item: TMacdResult): Double begin Result := Item.Histogram; end); + Macd.Sender.Link(MacdHist); + chart.AddDoubleSeries(MacdHist.Sender, TAlphaColors.Lightgreen, 1.0); + + // Add Stochastic Oscillator (14, 3) - This needs OHLC data, not just Close prices. + var Stoch: IMycConverter := TGenericIndicator.Create(TIndicators.CreateStochastic(14, 3)); + Ohlc.Sender.Link(Stoch); + + var StochK: IMycConverter := TMycGenericConverter.Create(function(const Item: TStochasticResult): Double begin Result := Item.K; end); + Stoch.Sender.Link(StochK); + chart.AddDoubleSeries(StochK.Sender, TAlphaColors.Green); + + var StochD: IMycConverter := TMycGenericConverter.Create(function(const Item: TStochasticResult): Double begin Result := Item.D; end); + Stoch.Sender.Link(StochD); + chart.AddDoubleSeries(StochD.Sender, TAlphaColors.Red); +} OhlcPoint.Sender.Link(TimeStamps); chart.SetXAxisSeries(Timestamps.Sender); diff --git a/Src/Myc.Trade.DataArray.pas b/Src/Myc.Trade.DataArray.pas index b617cc9..78c2588 100644 --- a/Src/Myc.Trade.DataArray.pas +++ b/Src/Myc.Trade.DataArray.pas @@ -2,10 +2,10 @@ unit Myc.Trade.DataArray; interface -uses - Myc.Trade.DataPoint; - type + // A series is an array of values with the newest ite at index=0. Each series counts the total of added items since creation, but + // it actually may contain less items, because the array size is limited by the lookback parameter, when adding items. + // Series are immutable. TMycDataArray = record private const @@ -21,7 +21,9 @@ type function GetItems(Idx: Int64): T; inline; public constructor Create(const AChunks: TArray; ACount, ATotalCount: Int64); + // Add a singe item function Add(const Data: T; Lookback: Int64): TMycDataArray; overload; + // Add a ranmge of items function Add(const Data: array of T; First, Count, Lookback: Int64): TMycDataArray; overload; class function CreateEmpty: TMycDataArray; static; // Helper to create a data array from a raw TArray. diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas new file mode 100644 index 0000000..4cdc180 --- /dev/null +++ b/Src/Myc.Trade.Indicators.pas @@ -0,0 +1,355 @@ +unit Myc.Trade.Indicators; + +interface + +uses + System.SysUtils, + System.Math, + Myc.Trade.DataArray, + Myc.Trade.DataPoint; + +type + // Result for the Moving Average Convergence Divergence (MACD) indicator. + TMacdResult = record + MacdLine: Double; + SignalLine: Double; + Histogram: Double; + end; + + // Result for the Stochastic Oscillator indicator. + TStochasticResult = record + K: Double; // %K line + D: Double; // %D line (signal line) + end; + + // Result for the Bollinger Bands indicator. + TBollingerBandsResult = record + UpperBand: Double; + MiddleBand: Double; + LowerBand: Double; + end; + + TIndicators = record + private + class function CalculateSMA(const Series: TMycDataArray; const Period: Integer): Double; static; + class function CalculateStdDev(const Series: TMycDataArray; const Period: Integer): Double; static; + class function CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; static; + public + // Simple Moving Average + class function CreateSMA(Period: Integer): TFunc; static; + // Exponential Moving Average + class function CreateEMA(Period: Integer): TFunc; static; + // Hull Moving Average + class function CreateHMA(Period: Integer): TFunc; static; + // Relative Strength Index + class function CreateRSI(Period: Integer): TFunc; static; + // Moving Average Convergence Divergence + class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TFunc; static; + // Stochastic Oscillator + class function CreateStochastic(KPeriod, DPeriod: Integer): TFunc; static; + // Bollinger Bands + class function CreateBollingerBands(Period: Integer; Multiplier: Double): TFunc; static; + end; + +implementation + +{ TIndicators } + +class function TIndicators.CalculateSMA(const Series: TMycDataArray; const Period: Integer): Double; +var + i: Integer; + sum: Double; +begin + if (Series.Count < Period) or (Period <= 0) then + Exit(0.0); + + sum := 0; + for i := 0 to Period - 1 do + sum := sum + Series[i]; + + Result := sum / Period; +end; + +class function TIndicators.CalculateStdDev(const Series: TMycDataArray; const Period: Integer): Double; +var + i: Integer; + mean, sumOfSquares: Double; +begin + if (Series.Count < Period) or (Period <= 0) then + Exit(0.0); + + mean := CalculateSMA(Series, Period); + sumOfSquares := 0; + for i := 0 to Period - 1 do + sumOfSquares := sumOfSquares + Power(Series[i] - mean, 2); + + Result := Sqrt(sumOfSquares / Period); +end; + +class function TIndicators.CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; +var + i: Integer; + numerator: Double; + denominator: Int64; +begin + // Ensure there is enough data to calculate the WMA + if (Series.Count < Period) or (Period <= 0) then + Exit(0.0); + + numerator := 0; + // The sum of weights (1 + 2 + ... + Period) + denominator := Period * (Period + 1) div 2; + + if (denominator = 0) then + Exit(0.0); + + for i := 0 to Period - 1 do + begin + // Newest data (index 0) gets the highest weight (Period) + numerator := numerator + Series[i] * (Period - i); + end; + + Result := numerator / denominator; +end; + +class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TFunc; +begin + var sourceData := TMycDataArray.CreateEmpty; + Result := + function(Value: Double): TBollingerBandsResult + var + stdDev: Double; + begin + sourceData := sourceData.Add(Value, Period); + Result.MiddleBand := Double.NaN; + Result.UpperBand := Double.NaN; + Result.LowerBand := Double.NaN; + + if (sourceData.Count >= Period) then + begin + Result.MiddleBand := CalculateSMA(sourceData, Period); + stdDev := CalculateStdDev(sourceData, Period); + Result.UpperBand := Result.MiddleBand + (stdDev * Multiplier); + Result.LowerBand := Result.MiddleBand - (stdDev * Multiplier); + end; + end; +end; + +class function TIndicators.CreateEMA(Period: Integer): TFunc; +begin + var lastEma: Double := Double.NaN; + var sourceData := TMycDataArray.CreateEmpty; + var multiplier := 2 / (Period + 1); + + Result := + function(Value: Double): Double + begin + sourceData := sourceData.Add(Value, Period); + + if (sourceData.Count < Period) then + begin + Result := Double.NaN; + Exit; + end; + + if not IsNan(lastEma) then + begin + // Subsequent EMA calculation + lastEma := (Value - lastEma) * multiplier + lastEma; + end + else + begin + // First EMA is a SMA of the initial period + lastEma := CalculateSMA(sourceData, Period); + end; + Result := lastEma; + end; +end; + +class function TIndicators.CreateHMA(Period: Integer): TFunc; +begin + var periodHalf := Period div 2; + var periodSqrt := Round(Sqrt(Period)); + var sourceData := TMycDataArray.CreateEmpty; + var diffSeries := TMycDataArray.CreateEmpty; + + Result := + function(Value: Double): Double + var + price: Double; + wmaHalf, wmaFull, diff: Double; + begin + price := Value; + + // Default HMA to NaN for the warm-up period. + Result := Double.NaN; + + // Add new price to the source data array, respecting the lookback period. + sourceData := sourceData.Add(price, Period); + + // Check if there is enough data to start the first stage of calculation. + if (sourceData.Count >= Period) then + begin + // Calculate the two WMAs for the first step. + wmaHalf := CalculateWMA(sourceData, periodHalf); + wmaFull := CalculateWMA(sourceData, Period); + + // Calculate the difference and add to the intermediate series. + diff := 2 * wmaHalf - wmaFull; + diffSeries := diffSeries.Add(diff, periodSqrt); + + // Check if there is enough intermediate data for the final calculation. + if (diffSeries.Count >= periodSqrt) then + begin + // Calculate the final HMA value + Result := CalculateWMA(diffSeries, periodSqrt); + end; + end; + end; +end; + +class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TFunc; +begin + var emaFast := CreateEMA(FastPeriod); + var emaSlow := CreateEMA(SlowPeriod); + var emaSignal := CreateEMA(SignalPeriod); + + Result := + function(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): TFunc; +begin + var avgGain: Double := Double.NaN; + var avgLoss: Double := Double.NaN; + var sourceData := TMycDataArray.CreateEmpty; + + Result := + function(Value: Double): Double + var + change, gain, loss, rs: Double; + gainSum, lossSum: Double; + i: Integer; + begin + sourceData := sourceData.Add(Value, Period + 1); + Result := Double.NaN; + + if (sourceData.Count <= Period) then + Exit; + + // Initial calculation for the first full period + if IsNan(avgGain) then + begin + gainSum := 0; + lossSum := 0; + for i := 0 to Period - 1 do + begin + change := sourceData[i] - sourceData[i + 1]; + if (change > 0) then + gainSum := gainSum + change + else + lossSum := lossSum - change; + end; + avgGain := gainSum / Period; + avgLoss := lossSum / Period; + end + else // Smoothed calculation for subsequent values + begin + change := sourceData[0] - sourceData[1]; + gain := 0; + loss := 0; + if (change > 0) then + gain := change + else + loss := -change; + + avgGain := (avgGain * (Period - 1) + gain) / Period; + avgLoss := (avgLoss * (Period - 1) + loss) / Period; + end; + + if (avgLoss = 0) then + Result := 100 + else + begin + rs := avgGain / avgLoss; + Result := 100 - (100 / (1 + rs)); + end; + end; +end; + +class function TIndicators.CreateSMA(Period: Integer): TFunc; +begin + var sourceData := TMycDataArray.CreateEmpty; + Result := + function(Value: Double): Double + begin + sourceData := sourceData.Add(Value, Period); + if (sourceData.Count >= Period) then + Result := CalculateSMA(sourceData, Period) + else + Result := Double.NaN; + end; +end; + +class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TFunc; +begin + var sourceData := TMycDataArray.CreateEmpty; + var smaD := CreateSMA(DPeriod); + + Result := + function(Value: TOhlcItem): TStochasticResult + var + i: Integer; + highestHigh, lowestLow: Double; + begin + sourceData := sourceData.Add(Value, KPeriod); + Result.K := Double.NaN; + Result.D := Double.NaN; + + if (sourceData.Count >= KPeriod) then + begin + highestHigh := -MaxDouble; + lowestLow := MaxDouble; + for i := 0 to KPeriod - 1 do + begin + // Correctly use High and Low fields + if (sourceData[i].High > highestHigh) then + highestHigh := sourceData[i].High; + if (sourceData[i].Low < lowestLow) then + lowestLow := sourceData[i].Low; + end; + + if (highestHigh > lowestLow) then + // Correctly use the current Close + Result.K := 100 * (sourceData[0].Close - lowestLow) / (highestHigh - lowestLow) + else + Result.K := 100; // Or 50, depends on convention + + Result.D := smaD(Result.K); + end; + end; +end; + +end.