unit Myc.Trade.DataPoint; interface uses System.TimeSpan; type // A data record for an Ask/Bid price pair. TAskBidItem = packed record Ask: Single; Bid: Single; constructor Create(AAsk, ABid: Single); end; TOhlcItem = record Open: Single; High: Single; Low: Single; Close: Single; Volume: Single; constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Single); end; // Represents a time-stamped data point in a series. TDataPoint = record Time: TDateTime; Data: T; constructor Create(ATime: TDateTime; const AData: T); end; // A time-ordered series of data points, optimized for chronological additions. // The most recently added element has the logical index 0. IDataSeries = interface function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; function GetLookback: Int64; function Add(const Data: TArray>; First, Count: Integer): IDataSeries; property Count: Int64 read GetCount; // Accesses data points by their logical index. // Index 0 is the newest element, Index (Count - 1) is the oldest. property Items[Idx: Int64]: TDataPoint read GetItems; default; // The maximum number of adressable items. Count will never be bigger than the Lookback. property Lookback: Int64 read GetLookback; // The total number of items ever added to the series. property TotalCount: Int64 read GetTotalCount; end; // Interface Helper for IDataSeries. // Provides a safe, value-type-like wrapper around the interface. TDataSeries = record type TConvertFunc = reference to function(const Val: TDataPoint): S; private FDataSeries: IDataSeries; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetData(Idx: Int64): T; function GetTime(Idx: Int64): TDateTime; function GetTotalCount: Int64; function GetLookback: Int64; class function GetNull: IDataSeries; static; public constructor Create(ADataSeries: IDataSeries); class operator Initialize(out Dest: TDataSeries); class operator Finalize(var Dest: TDataSeries); class operator Implicit(const A: TDataSeries): IDataSeries; class operator Implicit(const A: IDataSeries): TDataSeries; class function CreateDataSeries(Lookback: Int64; const Data: TArray> = nil): TDataSeries; static; function Add(const Data: TArray>): TDataSeries; overload; function Add(const Data: TArray>; First, Count: Integer): TDataSeries; overload; function Convert(const Func: TConvertFunc): TDataSeries; // Searches for a data point by its timestamp. // Returns the logical index of the matching item. // If no exact match, returns the index of the item immediately preceding the timestamp. // Returns -1 if the timestamp is before the oldest item in the series. function IndexOf(TimeStamp: TDateTime): Int64; function ToArray: TArray>; function ToDataArray: TArray; class property Null: IDataSeries read GetNull; property Count: Int64 read GetCount; property TotalCount: Int64 read GetTotalCount; property Lookback: Int64 read GetLookback; property Items[Idx: Int64]: TDataPoint read GetItems; default; property Data[Idx: Int64]: T read GetData; property Time[Idx: Int64]: TDateTime read GetTime; end; TDataSeriesDoubleHelper = record helper for TDataSeries function ToOhlc(TimeFrame: TTimeSpan): TDataSeries; end; implementation uses System.SysUtils, System.Math, System.Generics.Collections, Myc.Trade.Core.DataPoint; // Optimized helper function using direct TTimeSpan features and integer arithmetic. function CeilToTimeSpan(const ATime: TDateTime; const ATimeSpan: TTimeSpan): TDateTime; var timeSinceMidnight: TTimeSpan; timeSpanTicks: Int64; numIntervals, ceiledTicks: Int64; begin timeSpanTicks := ATimeSpan.Ticks; Assert(timeSpanTicks > 0, 'TimeSpan must be positive.'); // Get the time portion of ATime directly as a TTimeSpan. timeSinceMidnight := TTimeSpan.Subtract(ATime, Trunc(ATime)); // Using integer arithmetic to find the ceiling is robust. // This is a standard formula for integer ceiling division: (numerator + denominator - 1) / denominator numIntervals := (timeSinceMidnight.Ticks + timeSpanTicks - 1) div timeSpanTicks; ceiledTicks := numIntervals * timeSpanTicks; // Construct the final DateTime from the date part and the new, aligned time part. Result := Trunc(ATime) + TTimeSpan.FromTicks(ceiledTicks); end; function TDataSeriesDoubleHelper.ToOhlc(TimeFrame: TTimeSpan): TDataSeries; var ohlcPoints: TList>; currentBar: TOhlcItem; windowEndTime: TDateTime; sourceIdx: Int64; firstPointInBar: Boolean; begin if Self.Count = 0 then exit(TDataSeries.Create(TNullDataSeries.Null)); ohlcPoints := TList>.Create; try if TimeFrame.Ticks <= 0 then raise EArgumentException.Create('Invalid TimeFrame for OHLC aggregation.'); sourceIdx := Self.Count - 1; // Start with the oldest data point firstPointInBar := True; windowEndTime := 0; // Iterate through all source points chronologically (oldest to newest) while sourceIdx >= 0 do begin var P := Self.Items[sourceIdx]; if firstPointInBar then begin windowEndTime := CeilToTimeSpan(P.Time, TimeFrame); currentBar.Create(P.Data, P.Data, P.Data, P.Data, 0); firstPointInBar := False; end; if P.Time >= windowEndTime then begin ohlcPoints.Add(TDataPoint.Create(windowEndTime, currentBar)); firstPointInBar := True; Continue; // Re-evaluate the same point for the next bar end; currentBar.High := Max(currentBar.High, P.Data); currentBar.Low := Min(currentBar.Low, P.Data); currentBar.Close := P.Data; currentBar.Volume := currentBar.Volume + 1; Dec(sourceIdx); end; if not firstPointInBar then ohlcPoints.Add(TDataPoint.Create(windowEndTime, currentBar)); var dataArray := TMycDataArray.CreateFromArray(ohlcPoints.ToArray, 0, ohlcPoints.Count); var seriesImpl := TMycDataSeries.Create(ohlcPoints.Count, dataArray, ohlcPoints.Count); Result := TDataSeries.Create(seriesImpl); finally ohlcPoints.Free; end; end; { TAskBidItem } constructor TAskBidItem.Create(AAsk, ABid: Single); begin Ask := AAsk; Bid := ABid; end; { TOhlcItem } constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Single); begin Open := AOpen; High := AHigh; Low := ALow; Close := AClose; Volume := AVolume; end; { TDataPoint } constructor TDataPoint.Create(ATime: TDateTime; const AData: T); begin Time := ATime; Data := AData; end; constructor TDataSeries.Create(ADataSeries: IDataSeries); begin if Assigned(ADataSeries) then FDataSeries := ADataSeries else FDataSeries := Null; end; function TDataSeries.Add(const Data: TArray>; First, Count: Integer): TDataSeries; begin Result := FDataSeries.Add(Data, First, Count); end; function TDataSeries.Add(const Data: TArray>): TDataSeries; begin Result := FDataSeries.Add(Data, 0, Length(Data)); end; function TDataSeries.Convert(const Func: TConvertFunc): TDataSeries; begin Result := TConvertSeries.Create(FDataSeries, Func); end; class function TDataSeries.CreateDataSeries(Lookback: Int64; const Data: TArray> = nil): TDataSeries; begin Result := TMycDataSeries.CreateDataSeries(Lookback, TMycDataArray.CreateFromArray(Data, 0, Length(Data)), Length(Data)); end; class operator TDataSeries.Finalize(var Dest: TDataSeries); begin Dest.FDataSeries := nil; end; class operator TDataSeries.Initialize(out Dest: TDataSeries); begin Dest.FDataSeries := Null; end; class operator TDataSeries.Implicit(const A: TDataSeries): IDataSeries; begin Result := A.FDataSeries; end; class operator TDataSeries.Implicit(const A: IDataSeries): TDataSeries; begin Result.Create(A); end; function TDataSeries.GetCount: Int64; begin Result := FDataSeries.GetCount; end; function TDataSeries.GetItems(Idx: Int64): TDataPoint; begin Result := FDataSeries[Idx]; end; function TDataSeries.GetData(Idx: Int64): T; begin Result := FDataSeries[Idx].Data; end; function TDataSeries.GetLookback: Int64; begin Result := FDataSeries.Lookback; end; function TDataSeries.GetTime(Idx: Int64): TDateTime; begin Result := FDataSeries[Idx].Time; end; class function TDataSeries.GetNull: IDataSeries; begin Result := TNullDataSeries.Null; end; function TDataSeries.GetTotalCount: Int64; begin Result := FDataSeries.GetTotalCount; end; function TDataSeries.IndexOf(TimeStamp: TDateTime): Int64; var low, high, mid: Int64; dataPointTime: TDateTime; begin Result := -1; if Count = 0 then Exit; low := 0; high := Count - 1; while (low <= high) do begin mid := low + (high - low) div 2; dataPointTime := FDataSeries[mid].Time; if (dataPointTime = TimeStamp) then begin Result := mid; break; end else if (dataPointTime < TimeStamp) then begin Result := mid; high := mid - 1; end else begin low := mid + 1; end; end; end; function TDataSeries.ToArray: TArray>; begin var n := FDataSeries.Count; SetLength(Result, n); dec(n); for var i := 0 to n do Result[i] := FDataSeries[n - i]; end; function TDataSeries.ToDataArray: TArray; begin var n := FDataSeries.Count; SetLength(Result, n); dec(n); for var i := 0 to n do Result[i] := FDataSeries[n - i].Data; end; end.