unit FirstStrategy; interface uses System.SysUtils, System.Generics.Collections, Myc.Signals, Myc.Mutable, Myc.TaskManager, Myc.Trade.Types, Myc.Trade.DataPoint, Myc.Trade.DataArray, Myc.Trade.DataConverter, Myc.Trade.Core.DataConverter; type TTickAggregation = class(TMycConverter, TDataPoint>) private FTimeframe: TTimeframe; FCurrentBar: TDataPoint; function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; function GetCurrentBar: TDataPoint; function GetTimeframe: TTimeframe; public constructor Create(const ATimeframe: TTimeframe); function ProcessData(const Value: TDataPoint): TState; override; property CurrentBar: TDataPoint read GetCurrentBar; property Timeframe: TTimeframe read GetTimeframe; end; implementation uses System.DateUtils, System.Math; { 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; begin Result := FCurrentBar; end; function TTickAggregation.GetTimeframe: TTimeframe; begin Result := FTimeframe; end; function TTickAggregation.ProcessData(const Value: TDataPoint): 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; end.