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.