unit FirstStrategy; interface uses System.Generics.Collections, Myc.Signals, Myc.Lazy, Myc.Trade.DataPoint, Myc.Trade.DataArray; type TTimeframe = (M1, M5, H1, D); IMycConverter = interface(IMycProcessor) function GetSender: IMycDataProvider; property Sender: IMycDataProvider read GetSender; end; TMycConverter = class abstract(TMycProcessor, IMycConverter) private FSender: TMycDataProvider; function GetSender: IMycDataProvider; 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 read GetSender; end; TMycGenericConverter = class(TMycConverter) 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>, TDataPoint>) function GetCurrentBar: TDataPoint; function GetStateText: TWriteable; function GetTimeframe: TTimeframe; property CurrentBar: TDataPoint read GetCurrentBar; property StateText: TWriteable read GetStateText; property Timeframe: TTimeframe read GetTimeframe; end; TTicksToTimeframe = class(TMycConverter>, TDataPoint>, ITicksToTimeframe) private FTimeframe: TTimeframe; FStateText: TWriteable; // Stores the currently aggregating OHLC data. FCurrentBar: TDataPoint; function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; function GetCurrentBar: TDataPoint; function GetStateText: TWriteable; function GetTimeframe: TTimeframe; public constructor Create(const ATimeframe: TTimeframe; const AStateText: TWriteable); // 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>): Boolean; override; property CurrentBar: TDataPoint read GetCurrentBar; property StateText: TWriteable read GetStateText; property Timeframe: TTimeframe read GetTimeframe; end; // Implements the Hull Moving Average indicator. THullMovingAverage = class(TMycConverter) private FPeriod: Integer; FPeriodHalf: Integer; FPeriodSqrt: Integer; // Source data for HMA calculation FSourceData: TMycDataArray; // Intermediate data series for HMA calculation (2*WMA(n/2) - WMA(n)) FDiffSeries: TMycDataArray; // Calculates the Weighted Moving Average for the most recent data. function CalculateWMA(const Series: TMycDataArray; 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); 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; begin Result := FCurrentBar; end; function TTicksToTimeframe.GetStateText: TWriteable; begin Result := FStateText; end; function TTicksToTimeframe.GetTimeframe: TTimeframe; begin Result := FTimeframe; end; function TTicksToTimeframe.ProcessData(const Values: TArray>): Boolean; var point: TDataPoint; 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.CreateEmpty; FDiffSeries := TMycDataArray.CreateEmpty; end; function THullMovingAverage.CalculateWMA(const Series: TMycDataArray; 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 } constructor TMycGenericConverter.Create(const AFunc: TConvertFunc); begin inherited Create; FFunc := AFunc; end; function TMycGenericConverter.ProcessData(const Value: S): Boolean; begin Result := true; Broadcast(FFunc(Value)); end; { TMycConverter } constructor TMycConverter.Create; begin inherited Create; FSender := TMycDataProvider.Create(Self); end; destructor TMycConverter.Destroy; begin FSender.Free; inherited Destroy; end; procedure TMycConverter.Broadcast(const Value: T); begin var cValue := Value; FSender.Notify(function(const Processor: IMycProcessor): Boolean begin Result := Processor.ProcessData(cValue) end); end; function TMycConverter.GetSender: IMycDataProvider; begin Result := FSender; end; procedure TMycConverter.Update; begin FSender.Notify( function(const Processor: IMycProcessor): Boolean begin Processor.Update; Result := true end ); end; end.