460 lines
14 KiB
ObjectPascal
460 lines
14 KiB
ObjectPascal
unit FirstStrategy;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Generics.Collections,
|
|
Myc.Signals,
|
|
Myc.Lazy,
|
|
Myc.Trade.DataPoint,
|
|
Myc.Trade.DataArray,
|
|
Myc.Core.Notifier;
|
|
|
|
type
|
|
TTimeframe = (M1, M5, H1, D);
|
|
|
|
TTag = Pointer;
|
|
|
|
IMycBroadcast<T> = interface
|
|
function Link(const Strategy: IMycProcessor<T>): TTag;
|
|
procedure Unlink(Tag: TTag);
|
|
end;
|
|
|
|
// A contained object that broadcasts Values to linked strategies
|
|
TMycBroadcast<T> = class(TContainedObject, IMycBroadcast<T>)
|
|
private
|
|
FLinkedStrategies: TMycNotifyList<IMycProcessor<T>>;
|
|
protected
|
|
// Link a strategy
|
|
function Link(const Strategy: IMycProcessor<T>): TTag;
|
|
// Unlink a linked strategy
|
|
procedure Unlink(Tag: TTag);
|
|
// Broadcasts the given data points to all linked strategies.
|
|
procedure Broadcast(const Value: T);
|
|
public
|
|
constructor Create(const Controller: IInterface);
|
|
destructor Destroy; override;
|
|
end;
|
|
|
|
IMycConverter<S, T> = interface(IMycProcessor<S>)
|
|
function GetObservers: IMycBroadcast<T>;
|
|
property Observers: IMycBroadcast<T> read GetObservers;
|
|
end;
|
|
|
|
TMycConverter<S, T> = class abstract(TMycProcessor<S>, IMycConverter<S, T>)
|
|
private
|
|
FObservers: TMycBroadcast<T>;
|
|
protected
|
|
procedure Broadcast(const Value: T);
|
|
function GetObservers: IMycBroadcast<T>;
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
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;
|
|
|
|
// Series
|
|
|
|
IMycSeriesConverter<S, T> = interface(IMycConverter<TArray<S>, T>)
|
|
function GetLookback: Integer;
|
|
property Lookback: Integer read GetLookback;
|
|
end;
|
|
|
|
// Indicator
|
|
|
|
IMycIndicator<S, T> = interface(IMycSeriesConverter<S, TArray<T>>)
|
|
end;
|
|
|
|
TMycIndicator<S, T> = class abstract(TMycConverter<TArray<S>, TArray<T>>, IMycIndicator<S, T>)
|
|
protected
|
|
function GetLookback: Integer; virtual; abstract;
|
|
function ProcessData(const Value: TArray<S>): Boolean; override; abstract;
|
|
public
|
|
property Lookback: Integer read GetLookback;
|
|
end;
|
|
|
|
TMycGenericIndicator<S, T> = class(TMycIndicator<S, T>)
|
|
type
|
|
TConvertFunc = reference to function(const Value: S): T;
|
|
private
|
|
FLookback: Integer;
|
|
FFunc: TConvertFunc;
|
|
protected
|
|
function GetLookback: Integer; override;
|
|
function ProcessData(const Value: TArray<S>): Boolean; override;
|
|
public
|
|
constructor Create(ALookback: Integer; const AFunc: TConvertFunc);
|
|
end;
|
|
|
|
ITicksToTimeframe = interface(IMycConverter<TArray<TDataPoint<TAskBidItem>>, TArray<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>>, TArray<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(TMycIndicator<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 Values: TArray<Double>): Boolean; override;
|
|
function GetLookback: Integer; override;
|
|
public
|
|
constructor Create(const APeriod: Integer);
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
System.SysUtils,
|
|
System.DateUtils,
|
|
System.Math;
|
|
|
|
{ TMycBroadcast<T> }
|
|
|
|
constructor TMycBroadcast<T>.Create(const Controller: IInterface);
|
|
begin
|
|
inherited Create(Controller);
|
|
end;
|
|
|
|
destructor TMycBroadcast<T>.Destroy;
|
|
begin
|
|
FLinkedStrategies.Finalize;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TMycBroadcast<T>.Broadcast(const Value: T);
|
|
begin
|
|
var cValue := Value;
|
|
|
|
FLinkedStrategies.Lock;
|
|
try
|
|
FLinkedStrategies.Notify(
|
|
function(const Processor: IMycProcessor<T>): Boolean
|
|
begin
|
|
Result := Processor.ProcessData(cValue);
|
|
end
|
|
);
|
|
finally
|
|
FLinkedStrategies.Release;
|
|
end;
|
|
end;
|
|
|
|
function TMycBroadcast<T>.Link(const Strategy: IMycProcessor<T>): TTag;
|
|
begin
|
|
// Add the strategy to the notification list
|
|
FLinkedStrategies.Lock;
|
|
try
|
|
Result := FLinkedStrategies.Advise(Strategy);
|
|
finally
|
|
FLinkedStrategies.Release;
|
|
end;
|
|
end;
|
|
|
|
procedure TMycBroadcast<T>.Unlink(Tag: TTag);
|
|
begin
|
|
FLinkedStrategies.Lock;
|
|
try
|
|
FLinkedStrategies.Unadvise(Tag);
|
|
finally
|
|
FLinkedStrategies.Release;
|
|
end;
|
|
end;
|
|
|
|
{ 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;
|
|
producedBars: TList<TDataPoint<TOhlcItem>>;
|
|
begin
|
|
Result := true;
|
|
producedBars := TList<TDataPoint<TOhlcItem>>.Create;
|
|
try
|
|
// 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
|
|
producedBars.Add(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]);
|
|
|
|
Broadcast(producedBars.ToArray);
|
|
finally
|
|
producedBars.Free;
|
|
end;
|
|
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.GetLookback: Integer;
|
|
begin
|
|
Result := FPeriod + FPeriodSqrt - 1;
|
|
end;
|
|
|
|
function THullMovingAverage.ProcessData(const Values: TArray<Double>): Boolean;
|
|
var
|
|
i: Integer;
|
|
price: Double;
|
|
wmaHalf, wmaFull, diff: Double;
|
|
hma: Double;
|
|
resultArray: TArray<Double>;
|
|
begin
|
|
Result := true;
|
|
|
|
// Pre-allocate the result array since its size is known in advance.
|
|
SetLength(resultArray, Length(Values));
|
|
|
|
for i := 0 to High(Values) do
|
|
begin
|
|
price := Values[i];
|
|
|
|
// 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;
|
|
|
|
// Assign the result (either the calculated HMA or 0.0) directly into the array.
|
|
resultArray[i] := hma;
|
|
end;
|
|
|
|
// Broadcast the result array if the input was not empty.
|
|
if (Length(resultArray) > 0) then
|
|
begin
|
|
Broadcast(resultArray);
|
|
end;
|
|
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;
|
|
|
|
constructor TMycGenericIndicator<S, T>.Create(ALookback: Integer; const AFunc: TConvertFunc);
|
|
begin
|
|
inherited Create;
|
|
FFunc := AFunc;
|
|
FLookback := ALookback;
|
|
end;
|
|
|
|
function TMycGenericIndicator<S, T>.GetLookback: Integer;
|
|
begin
|
|
Result := FLookback;
|
|
end;
|
|
|
|
function TMycGenericIndicator<S, T>.ProcessData(const Value: TArray<S>): Boolean;
|
|
var
|
|
Arr: TArray<T>;
|
|
begin
|
|
Result := true;
|
|
SetLength(Arr, Length(Value));
|
|
for var i := 0 to High(Arr) do
|
|
Arr[i] := FFunc(Value[i]);
|
|
Broadcast(Arr);
|
|
end;
|
|
|
|
{ TMycConverter<S, T> }
|
|
|
|
constructor TMycConverter<S, T>.Create;
|
|
begin
|
|
inherited Create;
|
|
FObservers := TMycBroadcast<T>.Create(Self);
|
|
end;
|
|
|
|
destructor TMycConverter<S, T>.Destroy;
|
|
begin
|
|
FObservers.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TMycConverter<S, T>.Broadcast(const Value: T);
|
|
begin
|
|
FObservers.Broadcast(Value);
|
|
end;
|
|
|
|
function TMycConverter<S, T>.GetObservers: IMycBroadcast<T>;
|
|
begin
|
|
Result := FObservers;
|
|
end;
|
|
|
|
end.
|