Files
MycLib/Src/Myc.Trade.Indicators.Common.pas
T
2025-07-27 14:05:54 +02:00

490 lines
17 KiB
ObjectPascal

unit Myc.Trade.Indicators.Common;
interface
uses
System.SysUtils,
System.Math,
System.Generics.Collections,
System.Rtti,
Myc.Data.Records,
Myc.Data.Pipeline,
Myc.Data.Series,
Myc.Trade.Types;
type
// Result for the Moving Average Convergence Divergence (MACD) indicator.
TMacdResult = record
MacdLine: Double;
SignalLine: Double;
Histogram: Double;
end;
// Result for the Stochastic Oscillator indicator.
TStochasticResult = record
K: Double; // %K line
D: Double; // %D line (signal line)
end;
// Result for the Bollinger Bands indicator.
TBollingerBandsResult = record
UpperBand: Double;
MiddleBand: Double;
LowerBand: Double;
end;
// Result for the Keltner Channels indicator.
TKeltnerChannelsResult = record
UpperBand: Double;
MiddleBand: Double;
LowerBand: Double;
end;
TIndicators = record
private
class function CalculateSMA(const Series: TSeries<Double>; const Period: Integer): Double; static;
class function CalculateStdDev(const Series: TSeries<Double>; const Period: Integer): Double; static;
class function CalculateWMA(const Series: TSeries<Double>; const Period: Integer): Double; static;
public
// Simple Moving Average
class function CreateSMA(Period: Integer): TConvertFunc<Double, Double>; static;
// Exponential Moving Average
class function CreateEMA(Period: Integer): TConvertFunc<Double, Double>; static;
// Hull Moving Average
class function CreateHMA(Period: Integer): TConvertFunc<Double, Double>; static;
// Relative Strength Index
class function CreateRSI(Period: Integer): TConvertFunc<Double, Double>; static;
// Moving Average Convergence Divergence
class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc<Double, TMacdResult>; overload; static;
class function CreateMACD(
const EmaFast,
EmaSlow,
EmaSignal: TConvertFunc<Double, Double>
): TConvertFunc<Double, TMacdResult>; overload; static;
// Stochastic Oscillator
class function CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc<TOhlcItem, TStochasticResult>; overload; static;
class function CreateStochastic(
KPeriod: Integer;
const SmaD: TConvertFunc<Double, Double>
): TConvertFunc<TOhlcItem, TStochasticResult>; overload; static;
// Bollinger Bands
class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc<Double, TBollingerBandsResult>; static;
// Average True Range
class function CreateATR(Period: Integer): TConvertFunc<TOhlcItem, Double>; overload; static;
class function CreateATR(const MovAvgTR: TConvertFunc<Double, Double>): TConvertFunc<TOhlcItem, Double>; overload; static;
// Keltner Channels
class function CreateKeltnerChannels(
Period: Integer;
Multiplier: Double
): TConvertFunc<TOhlcItem, TKeltnerChannelsResult>; overload; static;
class function CreateKeltnerChannels(
const MovAvgMiddle: TConvertFunc<Double, Double>;
const AtrFunc: TConvertFunc<TOhlcItem, Double>;
Multiplier: Double
): TConvertFunc<TOhlcItem, TKeltnerChannelsResult>; overload; static;
class function CreateMean: TConvertFunc<TArray<Double>, Double>; static;
end;
implementation
{ TIndicators }
class function TIndicators.CalculateSMA(const Series: TSeries<Double>; 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<Double>; 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<Double>; const Period: Integer): Double;
var
i: Integer;
numerator: Double;
denominator: Int64;
begin
// Ensure there is enough data to calculate the WMA
if (Series.Count < Period) or (Period <= 0) then
Exit(0.0);
numerator := 0;
// The sum of weights (1 + 2 + ... + Period)
denominator := Period * (Period + 1) div 2;
if (denominator = 0) then
Exit(0.0);
for i := 0 to Period - 1 do
begin
// Newest data (index 0) gets the highest weight (Period)
numerator := numerator + Series[i] * (Period - i);
end;
Result := numerator / denominator;
end;
class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc<Double, TBollingerBandsResult>;
begin
var sourceData: TSeries<Double>;
Result :=
function(const Value: Double): TBollingerBandsResult
var
stdDev: Double;
begin
sourceData.Add(Value, Period);
Result.MiddleBand := Double.NaN;
Result.UpperBand := Double.NaN;
Result.LowerBand := Double.NaN;
if (sourceData.Count >= Period) then
begin
Result.MiddleBand := CalculateSMA(sourceData, Period);
stdDev := CalculateStdDev(sourceData, Period);
Result.UpperBand := Result.MiddleBand + (stdDev * Multiplier);
Result.LowerBand := Result.MiddleBand - (stdDev * Multiplier);
end;
end;
end;
class function TIndicators.CreateEMA(Period: Integer): TConvertFunc<Double, Double>;
begin
var lastEma: Double := Double.NaN;
var sourceData: TSeries<Double>;
var multiplier := 2 / (Period + 1);
Result :=
function(const Value: Double): Double
begin
sourceData.Add(Value, Period);
if (sourceData.Count < Period) then
begin
Result := Double.NaN;
Exit;
end;
if not IsNan(lastEma) then
begin
// Subsequent EMA calculation
lastEma := (Value - lastEma) * multiplier + lastEma;
end
else
begin
// First EMA is a SMA of the initial period
lastEma := CalculateSMA(sourceData, Period);
end;
Result := lastEma;
end;
end;
class function TIndicators.CreateHMA(Period: Integer): TConvertFunc<Double, Double>;
begin
var periodHalf := Period div 2;
var periodSqrt := Round(Sqrt(Period));
var sourceData: TSeries<Double>;
var diffSeries: TSeries<Double>;
Result :=
function(const Value: Double): Double
var
price: Double;
wmaHalf, wmaFull, diff: Double;
begin
price := Value;
// Default HMA to NaN for the warm-up period.
Result := Double.NaN;
// Add new price to the source data array, respecting the lookback period.
sourceData.Add(price, Period);
// Check if there is enough data to start the first stage of calculation.
if (sourceData.Count >= Period) then
begin
// Calculate the two WMAs for the first step.
wmaHalf := CalculateWMA(sourceData, periodHalf);
wmaFull := CalculateWMA(sourceData, Period);
// Calculate the difference and add to the intermediate series.
diff := 2 * wmaHalf - wmaFull;
diffSeries.Add(diff, periodSqrt);
// Check if there is enough intermediate data for the final calculation.
if (diffSeries.Count >= periodSqrt) then
begin
// Calculate the final HMA value
Result := CalculateWMA(diffSeries, periodSqrt);
end;
end;
end;
end;
// Standard MACD using EMAs.
class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc<Double, TMacdResult>;
begin
Result := CreateMACD(CreateEMA(FastPeriod), CreateEMA(SlowPeriod), CreateEMA(SignalPeriod));
end;
// Creates a MACD indicator from three provided moving average functions.
class function TIndicators.CreateMACD(const EmaFast, EmaSlow, EmaSignal: TConvertFunc<Double, Double>): TConvertFunc<Double, TMacdResult>;
begin
Result :=
function(const Value: Double): TMacdResult
var
fastVal, slowVal: Double;
begin
fastVal := EmaFast(Value);
slowVal := EmaSlow(Value);
if IsNan(slowVal) then // slowVal will be the last one to become non-NaN
begin
Result.MacdLine := Double.NaN;
Result.SignalLine := Double.NaN;
Result.Histogram := Double.NaN;
end
else
begin
Result.MacdLine := fastVal - slowVal;
Result.SignalLine := EmaSignal(Result.MacdLine);
if not IsNan(Result.SignalLine) then
Result.Histogram := Result.MacdLine - Result.SignalLine
else
Result.Histogram := Double.NaN;
end;
end;
end;
class function TIndicators.CreateRSI(Period: Integer): TConvertFunc<Double, Double>;
begin
var avgGain: Double := Double.NaN;
var avgLoss: Double := Double.NaN;
var sourceData: TSeries<Double>;
Result :=
function(const Value: Double): Double
var
change, gain, loss, rs: Double;
gainSum, lossSum: Double;
i: Integer;
begin
sourceData.Add(Value, Period + 1);
Result := Double.NaN;
if (sourceData.Count <= Period) then
Exit;
// Initial calculation for the first full period
if IsNan(avgGain) then
begin
gainSum := 0;
lossSum := 0;
for i := 0 to Period - 1 do
begin
change := sourceData[i] - sourceData[i + 1];
if (change > 0) then
gainSum := gainSum + change
else
lossSum := lossSum - change;
end;
avgGain := gainSum / Period;
avgLoss := lossSum / Period;
end
else // Smoothed calculation for subsequent values
begin
change := sourceData[0] - sourceData[1];
gain := 0;
loss := 0;
if (change > 0) then
gain := change
else
loss := -change;
avgGain := (avgGain * (Period - 1) + gain) / Period;
avgLoss := (avgLoss * (Period - 1) + loss) / Period;
end;
if (avgLoss = 0) then
Result := 100
else
begin
rs := avgGain / avgLoss;
Result := 100 - (100 / (1 + rs));
end;
end;
end;
class function TIndicators.CreateSMA(Period: Integer): TConvertFunc<Double, Double>;
begin
var sourceData: TSeries<Double>;
Result :=
function(const Value: Double): Double
begin
sourceData.Add(Value, Period);
if (sourceData.Count >= Period) then
Result := CalculateSMA(sourceData, Period)
else
Result := Double.NaN;
end;
end;
// Standard Stochastic Oscillator using an SMA for the %D line.
class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc<TOhlcItem, TStochasticResult>;
begin
Result := CreateStochastic(KPeriod, CreateSMA(DPeriod));
end;
// Creates a Stochastic Oscillator using an injectable moving average for the %D line.
class function TIndicators.CreateStochastic(
KPeriod: Integer;
const SmaD: TConvertFunc<Double, Double>
): TConvertFunc<TOhlcItem, TStochasticResult>;
begin
var sourceData: TSeries<TOhlcItem>;
Result :=
function(const Value: TOhlcItem): TStochasticResult
var
i: Integer;
highestHigh, lowestLow: Double;
begin
sourceData.Add(Value, KPeriod);
Result.K := Double.NaN;
Result.D := Double.NaN;
if (sourceData.Count >= KPeriod) then
begin
highestHigh := -MaxDouble;
lowestLow := MaxDouble;
for i := 0 to KPeriod - 1 do
begin
// Correctly use High and Low fields
if (sourceData[i].High > highestHigh) then
highestHigh := sourceData[i].High;
if (sourceData[i].Low < lowestLow) then
lowestLow := sourceData[i].Low;
end;
if (highestHigh > lowestLow) then
// Correctly use the current Close
Result.K := 100 * (sourceData[0].Close - lowestLow) / (highestHigh - lowestLow)
else
Result.K := 100; // Or 50, depends on convention
Result.D := SmaD(Result.K);
end;
end;
end;
// Standard ATR using an EMA for smoothing.
class function TIndicators.CreateATR(Period: Integer): TConvertFunc<TOhlcItem, Double>;
begin
Result := CreateATR(CreateEMA(Period));
end;
// Calculates the Average True Range (ATR) using an injectable moving average.
class function TIndicators.CreateATR(const MovAvgTR: TConvertFunc<Double, Double>): TConvertFunc<TOhlcItem, Double>;
begin
var sourceData: TSeries<TOhlcItem>;
Result :=
function(const Value: TOhlcItem): Double
var
tr: Double;
begin
// We only need the previous bar to calculate true range.
sourceData.Add(Value, 2);
if (sourceData.Count < 2) then
begin
// Feed a dummy value to keep the moving average count in sync. It will correctly return NaN.
Result := MovAvgTR(0);
Exit;
end;
// Calculate current True Range.
tr := Max(Value.High - Value.Low, Max(Abs(Value.High - sourceData[1].Close), Abs(Value.Low - sourceData[1].Close)));
// Feed the calculated TR into the provided moving average function.
Result := MovAvgTR(tr);
end;
end;
// Standard Keltner Channels using an EMA for the middle line and an EMA-based ATR.
class function TIndicators.CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConvertFunc<TOhlcItem, TKeltnerChannelsResult>;
begin
Result := CreateKeltnerChannels(CreateEMA(Period), CreateATR(Period), Multiplier);
end;
// Calculates Keltner Channels using an injectable ATR and middle band moving average.
class function TIndicators.CreateKeltnerChannels(
const MovAvgMiddle: TConvertFunc<Double, Double>;
const AtrFunc: TConvertFunc<TOhlcItem, Double>;
Multiplier: Double
): TConvertFunc<TOhlcItem, TKeltnerChannelsResult>;
begin
Result :=
function(const Value: TOhlcItem): TKeltnerChannelsResult
var
atrValue, middleValue, typicalPrice: Double;
begin
// Calculate Typical Price for the middle band.
typicalPrice := (Value.High + Value.Low + Value.Close) / 3.0;
// Get values from the provided indicator functions.
middleValue := MovAvgMiddle(typicalPrice);
atrValue := AtrFunc(Value);
// Set default NaN values for the warm-up period.
Result.MiddleBand := middleValue;
Result.UpperBand := Double.NaN;
Result.LowerBand := Double.NaN;
// Once both middle band and ATR have valid (non-NaN) values, calculate the channels.
if not IsNan(middleValue) and not IsNan(atrValue) then
begin
Result.UpperBand := middleValue + (atrValue * Multiplier);
Result.LowerBand := middleValue - (atrValue * Multiplier);
end;
end;
end;
class function TIndicators.CreateMean: TConvertFunc<TArray<Double>, Double>;
begin
Result :=
function(const Value: TArray<Double>): Double
begin
if Length(Value) = 0 then
exit(NaN);
Result := Value[0];
for var i := 1 to High(Value) do
Result := Result + Value[i];
Result := Result / Length(Value);
end;
end;
end.