Work in Progress

This commit is contained in:
Michael Schimmel
2025-07-15 20:29:19 +02:00
parent 8ebcd81561
commit bc75f08477
13 changed files with 597 additions and 320 deletions
+198 -10
View File
@@ -19,7 +19,7 @@ type
end;
// Concrete data provider that manages a list of processors (listeners).
TMycDataProvider<T> = class abstract(TContainedObject, TDataProvider<T>.IDataProvider)
TMycContainedDataProvider<T> = class abstract(TContainedObject, TDataProvider<T>.IDataProvider)
private
FListeners: TMycNotifyList<IMycProcessor<T>>;
public
@@ -33,6 +33,19 @@ type
procedure Unlink(Tag: TDataProvider<T>.TTag);
end;
TMycSequence<T> = class(TMycProcessor<T>, IMycDataSequence<T>)
private
FDataProviders: TArray<TMycContainedDataProvider<T>>;
function GetCount: Integer;
function GetDataProvider(Idx: Integer): TDataProvider<T>;
protected
function ProcessData(const Value: T): TState; override;
function ProcessDataProvider(Idx: Integer; const Value: T): TState;
public
constructor Create(ACount: Integer);
destructor Destroy; override;
end;
// Null object implementation for IDataProvider.
TNullDataProvider<T> = class(TInterfacedObject, TDataProvider<T>.IDataProvider)
public
@@ -43,7 +56,7 @@ type
// Abstract base class for components that process data of type S and provide data of type T.
TMycConverter<S, T> = class abstract(TMycProcessor<S>, TConverter<S, T>.IConverter)
private
FSender: TMycDataProvider<T>;
FSender: TMycContainedDataProvider<T>;
function GetSender: TDataProvider<T>.IDataProvider;
protected
function ProcessData(const Value: S): TState; override; abstract;
@@ -73,6 +86,12 @@ type
constructor Create(const AFunc: TConstFunc<S, T>);
end;
// A converter specialized for calculating indicators.
TMycIdentityConverter<T> = class(TMycConverter<T, T>)
protected
function ProcessData(const Value: T): TState; override; final;
end;
// A converter specialized for calculating indicators.
TMycIndicator<S, T> = class(TMycConverter<S, T>)
protected
@@ -145,26 +164,43 @@ type
destructor Destroy; override;
end;
TTickAggregation = class(TMycConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>)
private
FTimeframe: TTimeframe;
FCurrentBar: TDataPoint<TOhlcItem>;
function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetTimeframe: TTimeframe;
public
constructor Create(const ATimeframe: TTimeframe);
function ProcessData(const Value: TDataPoint<Double>): TState; override;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property Timeframe: TTimeframe read GetTimeframe;
end;
implementation
uses
System.TypInfo,
System.RTTI;
System.RTTI,
System.DateUtils,
System.Math,
Myc.TaskManager;
{ TMycDataProvider<T> }
{ TMycContainedDataProvider<T> }
constructor TMycDataProvider<T>.Create(const Controller: IInterface);
constructor TMycContainedDataProvider<T>.Create(const Controller: IInterface);
begin
inherited Create(Controller);
end;
destructor TMycDataProvider<T>.Destroy;
destructor TMycContainedDataProvider<T>.Destroy;
begin
FListeners.Finalize;
inherited Destroy;
end;
function TMycDataProvider<T>.Broadcast(const Value: T): TState;
function TMycContainedDataProvider<T>.Broadcast(const Value: T): TState;
begin
FListeners.Lock;
try
@@ -192,7 +228,7 @@ begin
end;
end;
function TMycDataProvider<T>.Link(const Processor: IMycProcessor<T>): TDataProvider<T>.TTag;
function TMycContainedDataProvider<T>.Link(const Processor: IMycProcessor<T>): TDataProvider<T>.TTag;
begin
// Add the Processor to the notification list
FListeners.Lock;
@@ -203,7 +239,7 @@ begin
end;
end;
procedure TMycDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
procedure TMycContainedDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
begin
FListeners.Lock;
try
@@ -230,7 +266,7 @@ end;
constructor TMycConverter<S, T>.Create;
begin
inherited Create;
FSender := TMycDataProvider<T>.Create(Self);
FSender := TMycContainedDataProvider<T>.Create(Self);
end;
destructor TMycConverter<S, T>.Destroy;
@@ -411,4 +447,156 @@ begin
FChanged.Notify;
end;
{ TTickAggregation }
constructor TTickAggregation.Create(const ATimeframe: TTimeframe);
begin
inherited Create;
FTimeframe := ATimeframe;
end;
function TTickAggregation.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
var
baseTime: TDateTime;
begin
// Align the time grid to UTC 0:00 using functions from System.DateUtils
baseTime := RecodeMilliSecond(TimeStamp, 0);
case Timeframe of
S: Result := baseTime;
S5: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 5));
S15: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 15));
S30: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 30));
M: Result := RecodeSecond(baseTime, 0);
M2: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 2));
M3: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 3));
M5: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 5));
M10: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 10));
M15: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 15));
M30: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 30));
H: Result := RecodeMinute(RecodeSecond(baseTime, 0), 0);
H2: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 2));
H3: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 3));
H4: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 4));
H8: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 8));
H12: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 12));
D: Result := StartOfTheDay(TimeStamp);
// D2, D3 are uncommon; this is a simple modulo-based approach relative to TDateTime's epoch.
D2: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 2);
D3: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 3);
W: Result := TimeStamp.StartOfTheWeek;
MN: Result := TimeStamp.StartOfTheMonth;
// Quarter alignment
MN3: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 3 * 3 + 1);
// Half-year alignment
MN6: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 6 * 6 + 1);
Y: Result := TimeStamp.StartOfTheYear;
else
// Fallback for any undefined timeframe
Result := 0;
end;
end;
function TTickAggregation.GetCurrentBar: TDataPoint<TOhlcItem>;
begin
Result := FCurrentBar;
end;
function TTickAggregation.GetTimeframe: TTimeframe;
begin
Result := FTimeframe;
end;
function TTickAggregation.ProcessData(const Value: TDataPoint<Double>): TState;
var
barStartTime: TDateTime;
lastBarTime: TDateTime;
begin
// Update bar for the strategy's timeframe
barStartTime := GetBarStartTime(Value.Time, FTimeframe);
lastBarTime := FCurrentBar.Time;
if (barStartTime > lastBarTime) then
begin
// A new bar starts, so the previous one is now complete.
if (lastBarTime > 0) then
begin
Result := Broadcast(FCurrentBar);
end;
// Start a new bar, Volume is 1 because this is the first tick.
FCurrentBar.Data.Open := Value.Data;
FCurrentBar.Data.High := Value.Data;
FCurrentBar.Data.Low := Value.Data;
FCurrentBar.Data.Close := Value.Data;
FCurrentBar.Data.Volume := 1;
FCurrentBar.Time := barStartTime;
end
else
begin
// Update the currently aggregating bar
if Value.Data > FCurrentBar.Data.High then
FCurrentBar.Data.High := Value.Data;
if Value.Data < FCurrentBar.Data.Low then
FCurrentBar.Data.Low := Value.Data;
FCurrentBar.Data.Close := Value.Data;
// Volume is the number of ticks needed to build the complete bar.
FCurrentBar.Data.Volume := FCurrentBar.Data.Volume + 1;
end;
end;
{ TMycSequence<T> }
constructor TMycSequence<T>.Create(ACount: Integer);
begin
inherited Create;
SetLength(FDataProviders, ACount);
for var i := 0 to High(FDataProviders) do
FDataProviders[i] := TMycContainedDataProvider<T>.Create(Self);
end;
destructor TMycSequence<T>.Destroy;
begin
for var i := High(FDataProviders) downto 0 do
FDataProviders[i].Free;
inherited;
end;
function TMycSequence<T>.GetCount: Integer;
begin
Result := Length(FDataProviders);
end;
function TMycSequence<T>.GetDataProvider(Idx: Integer): TDataProvider<T>;
begin
Result := FDataProviders[Idx];
end;
function TMycSequence<T>.ProcessData(const Value: T): TState;
begin
Result := ProcessDataProvider(0, Value);
end;
function TMycSequence<T>.ProcessDataProvider(Idx: Integer; const Value: T): TState;
begin
if Idx >= Length(FDataProviders) then
exit;
Result :=
TaskManager
.RunTask(FDataProviders[idx].Broadcast(Value), function: TState begin Result := ProcessDataProvider(1 + idx, Value); end);
end;
function TMycIdentityConverter<T>.ProcessData(const Value: T): TState;
begin
Result := Broadcast(Value);
end;
end.