Files
MycLib/AuraTrader/FirstStrategy.pas
T
2025-07-12 11:38:23 +02:00

317 lines
9.9 KiB
ObjectPascal

unit FirstStrategy;
interface
uses
System.Generics.Collections,
Myc.Signals,
Myc.Lazy,
Myc.Trade.DataPoint,
Myc.Trade.DataArray;
type
TTimeframe = (M1, M5, H1, D);
IMycConverter<S, T> = interface(IMycProcessor<S>)
function GetSender: IMycDataProvider<T>;
property Sender: IMycDataProvider<T> read GetSender;
end;
TMycConverter<S, T> = class abstract(TMycProcessor<S>, IMycConverter<S, T>)
private
FSender: TMycDataProvider<T>;
function GetSender: IMycDataProvider<T>;
protected
// Broadcasts the given data to all linked processors.
procedure Broadcast(const Value: T);
procedure Update; override;
public
constructor Create;
destructor Destroy; override;
property Sender: IMycDataProvider<T> read GetSender;
end;
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): Boolean; override;
public
constructor Create(const AFunc: TConvertFunc);
end;
ITicksToTimeframe = interface(IMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>>)
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetStateText: TWriteable<String>;
function GetTimeframe: TTimeframe;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property StateText: TWriteable<String> read GetStateText;
property Timeframe: TTimeframe read GetTimeframe;
end;
TTicksToTimeframe = class(TMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>>, ITicksToTimeframe)
private
FTimeframe: TTimeframe;
FStateText: TWriteable<String>;
// Stores the currently aggregating OHLC data.
FCurrentBar: TDataPoint<TOhlcItem>;
function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetStateText: TWriteable<String>;
function GetTimeframe: TTimeframe;
public
constructor Create(const ATimeframe: TTimeframe; const AStateText: TWriteable<String>);
// Process new data. This is called concurrently and must not have side effects out of the scope of this class!
function ProcessData(const Values: TArray<TDataPoint<TAskBidItem>>): Boolean; override;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property StateText: TWriteable<String> read GetStateText;
property Timeframe: TTimeframe read GetTimeframe;
end;
// Implements the Hull Moving Average indicator.
THullMovingAverage = class(TMycConverter<Double, Double>)
private
FPeriod: Integer;
FPeriodHalf: Integer;
FPeriodSqrt: Integer;
// Source data for HMA calculation
FSourceData: TMycDataArray<Double>;
// Intermediate data series for HMA calculation (2*WMA(n/2) - WMA(n))
FDiffSeries: TMycDataArray<Double>;
// Calculates the Weighted Moving Average for the most recent data.
function CalculateWMA(const Series: TMycDataArray<Double>; const Period: Integer): Double;
protected
function ProcessData(const Value: Double): Boolean; override;
public
constructor Create(const APeriod: Integer);
end;
implementation
uses
System.SysUtils,
System.DateUtils,
System.Math;
{ TTicksToTimeframe }
constructor TTicksToTimeframe.Create(const ATimeframe: TTimeframe; const AStateText: TWriteable<String>);
begin
inherited Create;
FStateText := AStateText;
FTimeframe := ATimeframe;
end;
function TTicksToTimeframe.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
begin
// Align the time grid to UTC 0:00 using functions from System.DateUtils
case Timeframe of
M1: Result := RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0);
M5: Result := RecodeMinute(RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0), MinuteOf(TimeStamp) - MinuteOf(TimeStamp) mod 5);
H1: Result := RecodeMinute(RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0), 0);
D: Result := StartOfTheDay(TimeStamp);
else
Result := 0;
end;
end;
function TTicksToTimeframe.GetCurrentBar: TDataPoint<TOhlcItem>;
begin
Result := FCurrentBar;
end;
function TTicksToTimeframe.GetStateText: TWriteable<String>;
begin
Result := FStateText;
end;
function TTicksToTimeframe.GetTimeframe: TTimeframe;
begin
Result := FTimeframe;
end;
function TTicksToTimeframe.ProcessData(const Values: TArray<TDataPoint<TAskBidItem>>): Boolean;
var
point: TDataPoint<TAskBidItem>;
midPrice: Single;
barStartTime: TDateTime;
lastBarTime: TDateTime;
currentBar: TOhlcItem;
begin
Result := true;
// Process each incoming data point
for point in Values do
begin
midPrice := (point.Data.Ask + point.Data.Bid) / 2;
// Update bar for the strategy's timeframe
barStartTime := GetBarStartTime(point.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
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;
with FCurrentBar do
FStateText.Value :=
Format('Cuur Bar: O:%.5f H:%.5f L:%.5f C:%.5f V:%.0f', [Data.Open, Data.High, Data.Low, Data.Close, Data.Volume]);
end;
{ THullMovingAverage }
constructor THullMovingAverage.Create(const APeriod: Integer);
begin
inherited Create;
FPeriod := APeriod;
FPeriodHalf := APeriod div 2;
FPeriodSqrt := Round(Sqrt(APeriod));
// Initialize data arrays.
FSourceData := TMycDataArray<Double>.CreateEmpty;
FDiffSeries := TMycDataArray<Double>.CreateEmpty;
end;
function THullMovingAverage.CalculateWMA(const Series: TMycDataArray<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;
function THullMovingAverage.ProcessData(const Value: Double): Boolean;
var
price: Double;
wmaHalf, wmaFull, diff: Double;
hma: Double;
begin
Result := true;
price := Value;
// Default HMA to NaN for the warm-up period.
hma := Double.NaN;
// Add new price to the source data array, respecting the lookback period.
FSourceData := FSourceData.Add(price, FPeriod);
// Check if there is enough data to start the first stage of calculation.
if (FSourceData.Count >= FPeriod) then
begin
// Calculate the two WMAs for the first step.
wmaHalf := CalculateWMA(FSourceData, FPeriodHalf);
wmaFull := CalculateWMA(FSourceData, FPeriod);
// Calculate the difference and add to the intermediate series.
diff := 2 * wmaHalf - wmaFull;
FDiffSeries := FDiffSeries.Add(diff, FPeriodSqrt);
// Check if there is enough intermediate data for the final calculation.
if (FDiffSeries.Count >= FPeriodSqrt) then
begin
// Calculate the final HMA value, overwriting the default 0.0.
hma := CalculateWMA(FDiffSeries, FPeriodSqrt);
end;
end;
// Broadcast the result
Broadcast(hma);
end;
{ 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): Boolean;
begin
Result := true;
Broadcast(FFunc(Value));
end;
{ TMycConverter<S, T> }
constructor TMycConverter<S, T>.Create;
begin
inherited Create;
FSender := TMycDataProvider<T>.Create(Self);
end;
destructor TMycConverter<S, T>.Destroy;
begin
FSender.Free;
inherited Destroy;
end;
procedure TMycConverter<S, T>.Broadcast(const Value: T);
begin
var cValue := Value;
FSender.Notify(function(const Processor: IMycProcessor<T>): Boolean begin Result := Processor.ProcessData(cValue) end);
end;
function TMycConverter<S, T>.GetSender: IMycDataProvider<T>;
begin
Result := FSender;
end;
procedure TMycConverter<S, T>.Update;
begin
FSender.Notify(
function(const Processor: IMycProcessor<T>): Boolean
begin
Processor.Update;
Result := true
end
);
end;
end.