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