Files
MycLib/Src/Myc.Trade.Indicators.Common.pas
T
2025-07-27 23:12:48 +02:00

1076 lines
36 KiB
ObjectPascal

unit Myc.Trade.Indicators.Common;
interface
uses
System.SysUtils,
System.Math,
System.Rtti,
Myc.Data.Pipeline,
Myc.Data.Series,
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
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;
end;
[IndicatorName('EMA', 'Exponential Moving Average')]
[IndicatorHint('A moving average that places greater weight on the most recent data points.')]
TEMA = class
public
type
TParams = record
Period: Integer;
end;
TArgs = record
Value: Double;
end;
TResult = record
EMA: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateEMA(Period: Integer): TConvertFunc<Double, Double>; static;
end;
[IndicatorName('WMA', 'Weighted Moving Average')]
[IndicatorHint('A moving average that places greater weight on more recent data points.')]
TWMA = class
public
type
TParams = record
Period: Integer;
end;
TArgs = record
Value: Double;
end;
TResult = record
WMA: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateWMA(Period: Integer): TConvertFunc<Double, Double>; static;
end;
[IndicatorName('HMA', 'Hull Moving Average')]
[IndicatorHint('A fast, smooth moving average that minimizes lag.')]
THMA = class
public
type
TParams = record
Period: Integer;
end;
TArgs = record
Value: Double;
end;
TResult = record
HMA: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateHMA(Period: Integer): TConvertFunc<Double, Double>; static;
end;
[IndicatorName('RSI', 'Relative Strength Index')]
[IndicatorHint('A momentum indicator measuring the magnitude of recent price changes.')]
TRSI = class
public
type
TParams = record
Period: Integer;
end;
TArgs = record
Value: Double;
end;
TResult = record
RSI: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateRSI(Period: Integer): TConvertFunc<Double, Double>; static;
end;
[IndicatorName('MACD', 'Moving Average Convergence Divergence')]
[IndicatorHint('A trend-following momentum indicator showing the relationship between two EMAs.')]
TMACD = class
public
type
TParams = record
FastPeriod: Integer;
SlowPeriod: Integer;
SignalPeriod: Integer;
end;
TArgs = record
Value: Double;
end;
TResult = record
MacdLine: Double;
SignalLine: Double;
Histogram: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<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;
end;
[IndicatorName('Stoch', 'Stochastic Oscillator')]
[IndicatorHint('A momentum indicator comparing a closing price to a range of its prices.')]
TStochastic = class
public
type
TParams = record
KPeriod: Integer;
DPeriod: Integer;
end;
TArgs = record
High: Double;
Low: Double;
Close: Double;
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<TArgs, TResult>; overload; static;
class function CreateStochastic(
KPeriod: Integer;
const SmaD: TConvertFunc<Double, Double>
): TConvertFunc<TArgs, TStochastic.TResult>; overload; static;
end;
[IndicatorName('StdDev', 'Standard Deviation')]
[IndicatorHint('Measures the amount of variation or dispersion of a set of values.')]
TStdDev = class
public
type
TParams = record
Period: Integer;
end;
TArgs = record
Value: Double;
end;
TResult = record
StdDev: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateStdDev(Period: Integer): TConvertFunc<Double, Double>; static;
end;
[IndicatorName('BB', 'Bollinger Bands')]
[IndicatorHint('Characterizes prices and volatility over time using standard deviation bands.')]
TBollingerBands = class
public
type
TParams = record
Period: Integer;
Multiplier: Double;
end;
TArgs = record
Value: Double;
end;
TResult = record
UpperBand: Double;
MiddleBand: Double;
LowerBand: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConvertFunc<Double, TResult>; static;
end;
[IndicatorName('ATR', 'Average True Range')]
[IndicatorHint('Measures market volatility by decomposing the entire range of an asset price.')]
TATR = class
public
type
TParams = record
Period: Integer;
end;
TArgs = record
High: Double;
Low: Double;
Close: Double;
end;
TResult = record
ATR: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateATR(Period: Integer): TConvertFunc<TArgs, Double>; overload; static;
class function CreateATR(const MovAvgTR: TConvertFunc<Double, Double>): TConvertFunc<TArgs, Double>; overload; static;
end;
[IndicatorName('KC', 'Keltner Channels')]
[IndicatorHint('A volatility-based indicator composed of an EMA and two ATR-based outer lines.')]
TKeltnerChannels = class
public
type
TParams = record
Period: Integer;
Multiplier: Double;
end;
TArgs = record
High: Double;
Low: Double;
Close: Double;
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<TArgs, TResult>; overload; static;
class function CreateKeltnerChannels(
const MovAvgMiddle: TConvertFunc<Double, Double>;
const AtrFunc: TConvertFunc<TATR.TArgs, Double>;
Multiplier: Double
): TConvertFunc<TArgs, TKeltnerChannels.TResult>; overload; static;
end;
[IndicatorName('Mean', 'Mean Value')]
[IndicatorHint('Calculates the arithmetic mean of an array of values.')]
TMean = class
public
type
TParams = record
end;
TArgs = record
Values: TArray<Double>;
end;
TResult = record
Mean: Double;
end;
[IndicatorFactory]
class function CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>; static;
class function CreateMean: TConvertFunc<TArray<Double>, Double>; static;
end;
implementation
{ TSMA }
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 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 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 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 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 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 function TStochastic.CreateFactory: TIndicatorFactoryProc<TStochastic.TParams, TStochastic.TArgs, TStochastic.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
stochFunc: TConvertFunc<TArgs, TResult>;
begin
stochFunc := CreateStochastic(Params.KPeriod, Params.DPeriod);
Result := function(const Value: TArgs): TResult begin Result := stochFunc(Value); end;
end;
end;
class function TStochastic.CreateStochastic(KPeriod, DPeriod: Integer): TConvertFunc<TArgs, 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<TArgs, TResult>;
var
buffer: TArray<TArgs>;
highDeque: TLightDeque;
lowDeque: TLightDeque;
valueCount: Int64;
begin
if (KPeriod <= 0) then
begin
Result :=
function(const Value: TArgs): 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: TArgs): 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 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 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 function TATR.CreateFactory: TIndicatorFactoryProc<TATR.TParams, TATR.TArgs, TATR.TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
atrFunc: TConvertFunc<TArgs, Double>;
begin
atrFunc := CreateATR(Params.Period);
Result := function(const Value: TArgs): TResult begin Result.ATR := atrFunc(Value); end;
end;
end;
class function TATR.CreateATR(Period: Integer): TConvertFunc<TArgs, 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<TArgs, Double>;
begin
var sourceData: TSeries<TArgs>;
Result :=
function(const Value: TArgs): 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 function TKeltnerChannels.CreateFactory: TIndicatorFactoryProc<TParams, TArgs, TResult>;
begin
Result :=
function(const Params: TParams): TConvertFunc<TArgs, TResult>
var
kcFunc: TConvertFunc<TArgs, TResult>;
begin
kcFunc := CreateKeltnerChannels(Params.Period, Params.Multiplier);
Result := function(const Value: TArgs): TResult begin Result := kcFunc(Value); end;
end;
end;
class function TKeltnerChannels.CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConvertFunc<TArgs, 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<TATR.TArgs, Double>;
Multiplier: Double
): TConvertFunc<TArgs, TKeltnerChannels.TResult>;
begin
Result :=
function(const Value: TArgs): 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);
var atrArgs: TATR.TArgs;
atrArgs.High := Value.High;
atrArgs.Low := Value.Low;
atrArgs.Close := Value.Close;
atrValue := AtrFunc(atrArgs);
// 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 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.