Files
MycLib/Src/Myc.Trade.Indicators.Common.pas
T
Michael Schimmel 8e8f139785 New type system
2025-08-26 14:42:14 +02:00

1606 lines
57 KiB
ObjectPascal

unit Myc.Trade.Indicators.Common;
interface
uses
System.SysUtils,
System.Math,
System.Rtti,
Myc.Data.Records,
Myc.Data.Pipeline,
Myc.Data.Series,
Myc.Data.Types,
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<TParams, TArgs, TResult>; static;
class function CreateSMA(Period: Integer): TConvertFunc<Double, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateEMA(Period: Integer): TConvertFunc<Double, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateWMA(Period: Integer): TConvertFunc<Double, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateHMA(Period: Integer): TConvertFunc<Double, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateRSI(Period: Integer): TConvertFunc<Double, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConvertFunc<Double, TResult>; overload; static;
class function CreateMACD(
const EmaFast,
EmaSlow,
EmaSignal: TConvertFunc<Double, Double>
): TConvertFunc<Double, TMACD.TResult>; 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<TParams, TArgs, TResult>; static;
class function CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc<TOhlcItem, TResult>; overload; static;
// Creates a Stochastic Oscillator using an injectable moving average for the %D line.
class function CreateStochastic(
KPeriod: Integer;
const SmaD: TConvertFunc<Double, Double>
): TConvertFunc<TOhlcItem, TStochastic.TResult>; 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<TParams, TArgs, TResult>; static;
class function CreateStdDev(Period: Integer): TConvertFunc<Double, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc<Double, TResult>; 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<TParams, TArgs, TResult>; static;
class function CreateATR(Period: Integer): TConvertFunc<TOhlcItem, Double>; overload; static;
class function CreateATR(const MovAvgTR: TConvertFunc<Double, Double>): TConvertFunc<TOhlcItem, Double>; 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<TParams, TArgs, TResult>; static;
class function CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConvertFunc<TOhlcItem, TResult>; overload; static;
class function CreateKeltnerChannels(
const MovAvgMiddle: TConvertFunc<Double, Double>;
const AtrFunc: TConvertFunc<TOhlcItem, Double>;
Multiplier: Double
): TConvertFunc<TOhlcItem, TKeltnerChannels.TResult>; 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<Double>;
end;
TResult = record
Mean: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateMean: TConvertFunc<TArray<Double>, 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<TSMA.TParams, TSMA.TArgs, TSMA.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
smaFunc: TConvertFunc<Double, Double>;
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<Double, Double>;
begin
// Implemented using a rolling sum and a circular array for O(1) performance.
var sum: Double;
var buffer: TArray<Double>;
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<TEMA.TParams, TEMA.TArgs, TEMA.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
emaFunc: TConvertFunc<Double, Double>;
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<Double, Double>;
begin
var lastEma: Double := Double.NaN;
var sourceData: TSeries<Double>;
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<TWMA.TParams, TWMA.TArgs, TWMA.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
wmaFunc: TConvertFunc<Double, Double>;
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<Double, Double>;
begin
// Corrected O(1) implementation using a rolling window.
var weightedSum: Double;
var simpleSum: Double;
var buffer: TArray<Double>;
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<THMA.TParams, THMA.TArgs, THMA.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
hmaFunc: TConvertFunc<Double, Double>;
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<Double, Double>;
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<TRSI.TParams, TRSI.TArgs, TRSI.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
rsiFunc: TConvertFunc<Double, Double>;
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<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;
{ 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<TMACD.TParams, TMACD.TArgs, TMACD.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
macdFunc: TConvertFunc<Double, TResult>;
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<Double, TResult>;
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<Double, Double>): TConvertFunc<Double, TMACD.TResult>;
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<Int64>; // 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<TStochastic.TParams, TStochastic.TArgs, TStochastic.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
stochFunc: TConvertFunc<TOhlcItem, TResult>;
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<TOhlcItem, TStochastic.TResult>;
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<Double, Double>
): TConvertFunc<TOhlcItem, TStochastic.TResult>;
var
buffer: TArray<TOhlcItem>;
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<TStdDev.TParams, TStdDev.TArgs, TStdDev.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
stdDevFunc: TConvertFunc<Double, Double>;
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<Double, Double>;
begin
// O(1) implementation using rolling sums of X and X^2 to calculate variance.
var sumX, sumX2: Double;
var buffer: TArray<Double>;
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<TBollingerBands.TParams, TBollingerBands.TArgs, TBollingerBands.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
bbFunc: TConvertFunc<Double, TResult>;
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<Double, TBollingerBands.TResult>;
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<TATR.TParams, TATR.TArgs, TATR.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
atrFunc: TConvertFunc<TOhlcItem, Double>;
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<TOhlcItem, Double>;
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<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;
{ 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<TKeltnerChannels.TParams, TKeltnerChannels.TArgs, TKeltnerChannels.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
kcFunc: TConvertFunc<TOhlcItem, TResult>;
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<TOhlcItem, TResult>;
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<Double, Double>;
const AtrFunc: TConvertFunc<TOhlcItem, Double>;
Multiplier: Double
): TConvertFunc<TOhlcItem, TKeltnerChannels.TResult>;
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<Double>;
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<TMean.TParams, TMean.TArgs, TMean.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
meanFunc: TConvertFunc<TArray<Double>, Double>;
begin
meanFunc := CreateMean();
Result := function(const Value: TArgs): TResult begin Result.Mean := meanFunc(Value.Values); end;
end;
end;
class function TMean.CreateMean: TConvertFunc<TArray<Double>, Double>;
begin
Result :=
function(const Value: TArray<Double>): 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<TSMA>;
IndicatorRegistry.RegisterTemplate<TEMA>;
IndicatorRegistry.RegisterTemplate<TWMA>;
IndicatorRegistry.RegisterTemplate<THMA>;
IndicatorRegistry.RegisterTemplate<TRSI>;
IndicatorRegistry.RegisterTemplate<TMACD>;
IndicatorRegistry.RegisterTemplate<TStochastic>;
IndicatorRegistry.RegisterTemplate<TStdDev>;
IndicatorRegistry.RegisterTemplate<TBollingerBands>;
IndicatorRegistry.RegisterTemplate<TATR>;
IndicatorRegistry.RegisterTemplate<TKeltnerChannels>;
IndicatorRegistry.RegisterTemplate<TMean>;
end.