Files
MycLib/AuraTrader/FirstStrategy.pas
T
Michael Schimmel 6f0b927a05 TTicker
2025-07-14 16:44:29 +02:00

221 lines
7.6 KiB
ObjectPascal

unit FirstStrategy;
interface
uses
System.SysUtils,
System.Generics.Collections,
Myc.Signals,
Myc.Mutable,
Myc.TaskManager,
Myc.Trade.Types,
Myc.Trade.DataPoint,
Myc.Trade.DataArray;
type
TMycGenericConverter<S, T> = class(TMycConverter<S, T>)
type
TConvertFunc = reference to function(const Value: S): T;
private
FFunc: TConvertFunc;
protected
function ProcessData(const Value: S): TState; override;
public
constructor Create(const AFunc: TConvertFunc);
end;
TIndicator<S, T> = class(TMycConverter<S, T>)
protected
function ProcessData(const Value: S): TState; override; final;
function Calculate(const Value: S): T; virtual; abstract;
end;
TGenericIndicator<S, T> = class(TIndicator<S, T>)
private
FFunc: TIndicatorFunc<S, T>;
protected
function Calculate(const Value: S): T; override; final;
public
constructor Create(const AFunc: TIndicatorFunc<S, T>);
end;
TTicksToBars = class(TMycConverter<TDataPoint<TAskBidItem>, TDataPoint<TOhlcItem>>)
private
FTimeframe: TTimeframe;
// Stores the currently aggregating OHLC data.
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);
// Process new data. This is called concurrently and must not have side effects out of the scope of this class!
function ProcessData(const Value: TDataPoint<TAskBidItem>): TState; override;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property Timeframe: TTimeframe read GetTimeframe;
end;
TTicker<T> = class(TMycConverter<TArray<T>, T>)
public
function ProcessData(const Values: TArray<T>): TState; override;
end;
implementation
uses
System.DateUtils,
System.Math;
{ TMycGenericConverter<S, T> }
constructor TMycGenericConverter<S, T>.Create(const AFunc: TConvertFunc);
begin
inherited Create;
FFunc := AFunc;
end;
function TMycGenericConverter<S, T>.ProcessData(const Value: S): TState;
begin
Result := Broadcast(FFunc(Value));
end;
{ TIndicator<S,T> }
function TIndicator<S, T>.ProcessData(const Value: S): TState;
begin
Result := Broadcast(Calculate(Value));
end;
{ TGenericIndicator<S,T> }
constructor TGenericIndicator<S, T>.Create(const AFunc: TIndicatorFunc<S, T>);
begin
inherited Create;
FFunc := AFunc;
end;
function TGenericIndicator<S, T>.Calculate(const Value: S): T;
begin
Result := FFunc(Value);
end;
{ TTicksToBars }
constructor TTicksToBars.Create(const ATimeframe: TTimeframe);
begin
inherited Create;
FTimeframe := ATimeframe;
end;
function TTicksToBars.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 TTicksToBars.GetCurrentBar: TDataPoint<TOhlcItem>;
begin
Result := FCurrentBar;
end;
function TTicksToBars.GetTimeframe: TTimeframe;
begin
Result := FTimeframe;
end;
function TTicksToBars.ProcessData(const Value: TDataPoint<TAskBidItem>): TState;
var
midPrice: Single;
barStartTime: TDateTime;
lastBarTime: TDateTime;
currentBar: TOhlcItem;
begin
midPrice := (Value.Data.Ask + Value.Data.Bid) / 2;
// 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.
currentBar := TOhlcItem.Create(midPrice, midPrice, midPrice, midPrice, 1);
FCurrentBar.Data := currentBar;
FCurrentBar.Time := barStartTime;
end
else
begin
// Update the currently aggregating bar
currentBar := FCurrentBar.Data;
currentBar.High := Max(currentBar.High, midPrice);
currentBar.Low := Min(currentBar.Low, midPrice);
currentBar.Close := midPrice;
// Volume is the number of ticks needed to build the complete bar.
currentBar.Volume := currentBar.Volume + 1;
FCurrentBar.Data := currentBar;
end;
end;
function TTicker<T>.ProcessData(const Values: TArray<T>): TState;
begin
var done := TLatch.CreateLatch( Length(Values) );
// Process each incoming data point
for var i:=0 to High(Values) do
Broadcast(Values[i]).Signal.Subscribe(done);
Result := done.State;
end;
end.