1606 lines
57 KiB
ObjectPascal
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.
|