Generic indicator factory

This commit is contained in:
Michael Schimmel
2025-07-27 20:31:25 +02:00
parent ecee8b37bc
commit 7842c3bd87
6 changed files with 173 additions and 116 deletions
+1 -1
View File
@@ -682,7 +682,7 @@ begin
exit;
var timeframe := TTimeframe.M15;
ExecuteStrategy(Symbol, timeframe, CreateStrategy2(timeframe));
// ExecuteStrategy(Symbol, timeframe, CreateStrategy2(timeframe));
var tstStrat := StrategyTest.CreateStrategy1(timeframe);
ExecuteStrategy(Symbol, timeframe, tstStrat.Consumer);
+1
View File
@@ -242,6 +242,7 @@ type
implementation
uses
Winapi.Windows,
System.RTTI,
Myc.TaskManager;
+2
View File
@@ -2,6 +2,8 @@ unit Myc.Data.Pipeline;
interface
{$M+}
uses
Myc.Signals,
Myc.Mutable,
+52 -22
View File
@@ -18,6 +18,7 @@ type
FTypeInfo: PtypeInfo;
FName: string;
FOffset: Integer;
FSize: Integer;
procedure FromType(const [ref] Buffer: TBytes; const Src);
procedure ToType(const [ref] Buffer: TBytes; var Dst);
@@ -30,7 +31,7 @@ type
property Offset: Integer read FOffset;
constructor Create(const AName: string; AFieldType: TFieldType; ATypeInfo: PTypeInfo; AOffset: Integer);
constructor Create(const AName: string; AFieldType: TFieldType; ATypeInfo: PTypeInfo; AOffset, ASize: Integer);
procedure Copy(Src, Dst: Pointer);
@@ -115,10 +116,6 @@ implementation
uses
System.Rtti;
const
DataSize: array[TDataRecord.TFieldType] of Integer =
(sizeof(Double), sizeof(Int64), sizeof(String), sizeof(TDateTime), sizeof(TDataRecord));
{ TDataRecord }
constructor TDataRecord.Create(const ALayout: TLayout; const ABuffer: TBytes);
@@ -246,12 +243,13 @@ begin
Dest.Layout.Fields[i].FinalizeField(Dest.FBuffer);
end;
constructor TDataRecord.TField.Create(const AName: string; AFieldType: TFieldType; ATypeInfo: PTypeInfo; AOffset: Integer);
constructor TDataRecord.TField.Create(const AName: string; AFieldType: TFieldType; ATypeInfo: PTypeInfo; AOffset, ASize: Integer);
begin
FName := AName;
FFieldType := AFieldType;
FOffset := AOffset;
FTypeInfo := ATypeInfo;
FSize := ASize;
end;
procedure TDataRecord.TField.InitField(const [ref] Buffer: TBytes);
@@ -289,6 +287,9 @@ begin
end;
function TDataRecord.TField.GetSize: Integer;
const
DataSize: array[TDataRecord.TFieldType] of Integer =
(sizeof(Double), sizeof(Int64), sizeof(String), sizeof(TDateTime), sizeof(TDataRecord));
begin
Result := DataSize[FFieldType];
end;
@@ -425,6 +426,9 @@ begin
end;
class function TDataRecord.TLayout.Construct(const Def: TArray<TFieldDef>): TLayout;
const
DataSize: array[TDataRecord.TFieldType] of Integer =
(sizeof(Double), sizeof(Int64), sizeof(String), sizeof(TDateTime), sizeof(TDataRecord));
var
Fields: TArray<TField>;
begin
@@ -432,7 +436,7 @@ begin
var ofs := 0;
for var i := 0 to High(Fields) do
begin
var ti: PTypeInfo;
var ti: PTypeInfo := nil;
case Def[i].FieldType of
dfFloat: ti := TypeInfo(Double);
dfInteger: ti := TypeInfo(Int64);
@@ -441,7 +445,7 @@ begin
dfRecord: ti := TypeInfo(TDataRecord);
end;
Fields[i] := TField.Create(Def[i].Name, Def[i].FieldType, ti, ofs);
Fields[i] := TField.Create(Def[i].Name, Def[i].FieldType, ti, ofs, DataSize[Def[i].FieldType]);
inc(ofs, Fields[i].AlignedSize);
end;
@@ -477,33 +481,59 @@ begin
begin
var rf := rttiFields[i];
var ft: TFieldType := Default(TFieldType);
var supported := true;
var size: Integer := 0;
case rf.FieldType.TypeKind of
tkInt64: ft := dfInteger;
tkInteger:
begin
ft := dfInteger;
size := sizeof(Integer);
end;
tkInt64:
begin
ft := dfInteger;
size := sizeof(Int64);
end;
tkFloat:
if rf.FieldType.HasName(GetTypeName(TypeInfo(TDateTime))) then
ft := dfTimestamp
else if GetTypeData(rf.FieldType.Handle).FloatType = ftDouble then
ft := dfFloat
begin
ft := dfTimestamp;
size := sizeof(TDateTime);
end
else
supported := false;
tkString, tkWString, tkLString, tkUString: ft := dfString;
begin
case GetTypeData(rf.FieldType.Handle).FloatType of
ftSingle:
begin
ft := dfFloat;
size := sizeof(Single);
end;
ftDouble:
begin
ft := dfFloat;
size := sizeof(Double);
end;
end;
end;
tkString, tkWString, tkLString, tkUString:
begin
ft := dfString;
size := sizeof(String);
end;
tkMRecord:
begin
if rf.FieldType.HasName(GetTypeName(TypeInfo(TDataRecord))) then
ft := dfRecord
else
supported := false;
begin
ft := dfRecord;
size := sizeof(TDataRecord);
end
end;
else
supported := false;
end;
if not supported then
if size = 0 then
raise Exception.Create('Type ' + rf.FieldType.Name + ' not supported in data records');
fields[i] := TField.Create(rf.Name, ft, rf.FieldType.Handle, rf.Offset);
fields[i] := TField.Create(rf.Name, ft, rf.FieldType.Handle, rf.Offset, size);
end;
Result.Create(fields);
+112 -88
View File
@@ -13,6 +13,8 @@ uses
Myc.Trade.Indicators;
type
// --- Indicator Templates ---
[IndicatorName('SMA', 'Simple Moving Average')]
[IndicatorHint('Calculates the average of a selected range of prices.')]
TSMA = class
@@ -270,68 +272,6 @@ type
implementation
type
// A minimal, self-contained Deque (Double-Ended Queue) using a circular array.
TLightDeque = record
private
FItems: TArray<Integer>;
FHead, FCount, FCapacity: Integer;
function GetLastIndex: Integer;
function GetFirstIndex: Integer;
public
constructor Create(ACapacity: Integer);
procedure AddLast(AValue: Integer);
procedure RemoveLast;
procedure RemoveFirst;
property Count: Integer read FCount;
property Last: Integer read GetLastIndex;
property First: Integer read GetFirstIndex;
end;
constructor TLightDeque.Create(ACapacity: Integer);
begin
FCapacity := ACapacity;
SetLength(FItems, FCapacity);
FHead := 0;
FCount := 0;
end;
procedure TLightDeque.AddLast(AValue: Integer);
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.GetLastIndex: Integer;
begin
var tail := (FHead + FCount - 1 + FCapacity) mod FCapacity;
Result := FItems[tail];
end;
function TLightDeque.GetFirstIndex: Integer;
begin
Result := FItems[FHead];
end;
{ TSMA }
class function TSMA.CreateFactory: TIndicatorFactoryProc<TSMA.TParams, TSMA.TArgs, TSMA.TResult>;
@@ -669,8 +609,6 @@ begin
Result := CreateMACD(TEMA.CreateEMA(FastPeriod), TEMA.CreateEMA(SlowPeriod), TEMA.CreateEMA(SignalPeriod));
end;
{ TMACD }
// 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
@@ -702,6 +640,69 @@ 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 :=
@@ -728,8 +729,7 @@ var
buffer: TArray<TOhlcItem>;
highDeque: TLightDeque;
lowDeque: TLightDeque;
currentIndex: Integer;
valueCount: Integer;
valueCount: Int64;
begin
if (KPeriod <= 0) then
begin
@@ -745,46 +745,58 @@ begin
SetLength(buffer, KPeriod);
highDeque := TLightDeque.Create(KPeriod);
lowDeque := TLightDeque.Create(KPeriod);
currentIndex := 0;
valueCount := 0;
Result :=
function(const Value: TOhlcItem): TStochastic.TResult
var
currentIndex, firstIndex, lastIndex: Integer;
highestHigh, lowestLow: Double;
windowStart: Integer;
begin
inc(valueCount);
currentIndex := (valueCount - 1) mod KPeriod;
buffer[currentIndex] := Value;
// Update deques for rolling min/max
while (highDeque.Count > 0) and (buffer[highDeque.Last].High <= Value.High) do
highDeque.RemoveLast;
highDeque.AddLast(currentIndex);
// 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) and (buffer[lowDeque.Last].Low >= Value.Low) do
lowDeque.RemoveLast;
lowDeque.AddLast(currentIndex);
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
windowStart := (currentIndex - KPeriod + 1 + KPeriod) mod KPeriod;
if (valueCount > KPeriod) then
begin
if (highDeque.First = windowStart - 1) or ((windowStart = 0) and (highDeque.First = KPeriod - 1)) then
highDeque.RemoveFirst;
if (lowDeque.First = windowStart - 1) or ((windowStart = 0) and (lowDeque.First = KPeriod - 1)) then
lowDeque.RemoveFirst;
end;
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);
Result.D := SmaD(Result.K); // Feed NaN to keep SMA in sync
end
else
begin
highestHigh := buffer[highDeque.First].High;
lowestLow := buffer[lowDeque.First].Low;
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)
@@ -793,8 +805,6 @@ begin
Result.D := SmaD(Result.K);
end;
currentIndex := (currentIndex + 1) mod KPeriod;
end;
end;
@@ -1042,4 +1052,18 @@ begin
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.
+5 -5
View File
@@ -2,12 +2,12 @@ unit Myc.Trade.Indicators;
interface
{$M+}
uses
Myc.Data.Pipeline,
Myc.Data.Records;
{$M+}
type
TIndicatorFactoryProc<TParams, TValue, TResult> = reference to function(const Params: TParams): TConvertFunc<TValue, TResult>;
@@ -145,7 +145,7 @@ type
end;
var
Registry: TIndicatorRegistry;
IndicatorRegistry: TIndicatorRegistry;
implementation
@@ -459,9 +459,9 @@ begin
end;
initialization
Registry := TIndicatorRegistry.Create;
IndicatorRegistry := TIndicatorRegistry.Create;
finalization
Registry.Free;
IndicatorRegistry.Free;
end.