unit Myc.Trade.Core.DataPoint; interface uses System.Generics.Collections, System.TimeSpan, Myc.Trade.DataPoint, Myc.Trade.DataArray; type // The implementation class for IDataSeries. TMycDataSeries = class(TInterfacedObject, IDataSeries) private FData: TMycDataArray>; FLookback: Int64; FTotalCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; public constructor Create(ALookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64); destructor Destroy; override; function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class function CreateDataSeries( Lookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64 ): IDataSeries; static; end; // Null object implementation for IDataSeries TNullDataSeries = class(TInterfacedObject, IDataSeries) strict private class var FNull: IDataSeries; private FTotalCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; function GetLookback: Int64; class constructor CreateClass; public constructor Create(ATotalCount: Int64); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class property Null: IDataSeries read FNull; end; // A virtual series that combines a base series and an array of new data without copying. TCompositeDataSeries = class(TInterfacedObject, IDataSeries) private FBaseSeries: IDataSeries; FAddedData: TMycDataArray>; FLookback: Int64; FCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; public constructor Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray>); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class function CreateComposite( const BaseSeries: IDataSeries; const Data: TArray>; First, Count: Integer ): IDataSeries; static; end; TConvertSeries = class(TInterfacedObject, IDataSeries) private FSource: IDataSeries; FConvertFunc: TDataSeries.TConvertFunc; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; public constructor Create(const ASource: IDataSeries; const AConvertFunc: TDataSeries.TConvertFunc); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; end; TAggregateDataSeries = class(TInterfacedObject, IDataSeries) public // Defines the function signature for aggregating a set of source data points into a single target value. type TAggregateFunc = reference to function(const ASourcePoints: TArray>): S; private FSource: IDataSeries; FTimeFrame: TDateTime; FAggregateFunc: TAggregateFunc; FCachedItems: TDictionary>; FBaseTime: TDateTime; FAggregatedCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; procedure CalculateAggregatedCount; public constructor Create(const ASource: IDataSeries; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc); destructor Destroy; override; function Add(const Data: TArray>; First, Count: Integer): IDataSeries; end; implementation uses System.SysUtils, System.Math; { TMycDataSeries } constructor TMycDataSeries.Create(ALookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64); begin inherited Create; FLookback := ALookback; FData := AData; FTotalCount := ATotalCount; end; destructor TMycDataSeries.Destroy; begin inherited; end; class function TMycDataSeries.CreateDataSeries( Lookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64 ): IDataSeries; begin if Lookback > 0 then Result := TMycDataSeries.Create(Lookback, AData, ATotalCount) else Result := TNullDataSeries.Null; end; function TMycDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; var newData: TMycDataArray>; newTotalCount: Int64; begin if Count < 0 then Count := Length(Data) - First; newData := FData.Add(Data, First, Count, FLookback); newTotalCount := FTotalCount + Count; Result := TMycDataSeries.Create(FLookback, newData, newTotalCount); end; function TMycDataSeries.GetCount: Int64; begin Result := FData.Count; end; function TMycDataSeries.GetItems(Idx: Int64): TDataPoint; begin Assert((Idx >= 0) and (Idx < FData.Count), 'Index is out of bounds.'); Result := FData.Items[Idx]; end; function TMycDataSeries.GetLookback: Int64; begin Result := FLookback; end; function TMycDataSeries.GetTotalCount: Int64; begin Result := FTotalCount; end; { TNullDataSeries } class constructor TNullDataSeries.CreateClass; begin FNull := TNullDataSeries.Create(0); end; constructor TNullDataSeries.Create(ATotalCount: Int64); begin inherited Create; FTotalCount := ATotalCount; end; function TNullDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; begin if Count < 0 then Count := Length(Data) - First; if Count > 0 then Result := TNullDataSeries.Create(FTotalCount + Count) else Result := Self; end; function TNullDataSeries.GetCount: Int64; begin Result := 0; end; function TNullDataSeries.GetItems(Idx: Int64): TDataPoint; begin Assert(false, 'Data series is empty.'); Result := Default(TDataPoint); end; function TNullDataSeries.GetLookback: Int64; begin Result := 0; end; function TNullDataSeries.GetTotalCount: Int64; begin Result := FTotalCount; end; { TCompositeDataSeries } constructor TCompositeDataSeries.Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray>); begin inherited Create; FBaseSeries := ABaseSeries; FAddedData := AAddedData; FLookback := FBaseSeries.Lookback; Assert(FLookback > 0); FCount := FBaseSeries.Count + FAddedData.Count; if FCount > FLookback then FCount := FLookback; end; function TCompositeDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; var newAddedData: TMycDataArray>; itemsInBase: Int64; lookbackForAdd: Int64; begin if Count < 0 then Count := Length(Data) - First; if Count = 0 then exit(Self); itemsInBase := FBaseSeries.Count; lookbackForAdd := FLookback - itemsInBase; if lookbackForAdd < 0 then lookbackForAdd := 0; newAddedData := FAddedData.Add(Data, First, Count, lookbackForAdd); // Optimization: If the added data fills the entire lookback window, // the base series is no longer relevant. We can return a simpler TMycDataSeries. if newAddedData.Count >= FLookback then begin Result := TMycDataSeries.Create(FLookback, newAddedData, GetTotalCount + Count); end else begin Result := TCompositeDataSeries.Create(FBaseSeries, newAddedData); end; end; class function TCompositeDataSeries.CreateComposite( const BaseSeries: IDataSeries; const Data: TArray>; First, Count: Integer ): IDataSeries; begin if Count < 0 then Count := Length(Data) - First; if Count = 0 then exit(BaseSeries); Result := TCompositeDataSeries.Create(BaseSeries, TMycDataArray>.CreateFromArray(Data, First, Count)); end; function TCompositeDataSeries.GetCount: Int64; begin Result := FCount; end; function TCompositeDataSeries.GetItems(Idx: Int64): TDataPoint; var addedCount: Int64; begin Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.'); addedCount := FAddedData.Count; if Idx < addedCount then begin Result := FAddedData.Items[Idx]; end else begin Result := FBaseSeries.Items[Idx - addedCount]; end; end; function TCompositeDataSeries.GetLookback: Int64; begin Result := FLookback; end; function TCompositeDataSeries.GetTotalCount: Int64; begin Result := FBaseSeries.TotalCount + FAddedData.Count; end; { TConvertSeries } constructor TConvertSeries.Create(const ASource: IDataSeries; const AConvertFunc: TDataSeries.TConvertFunc); begin inherited Create; FSource := ASource; FConvertFunc := AConvertFunc; end; function TConvertSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; begin Result := TCompositeDataSeries.CreateComposite(Self, Data, First, Count); end; function TConvertSeries.GetCount: Int64; begin Result := FSource.Count; end; function TConvertSeries.GetItems(Idx: Int64): TDataPoint; var P: TDataPoint; begin P := FSource[Idx]; Result.Create(P.Time, FConvertFunc(P)); end; function TConvertSeries.GetLookback: Int64; begin Result := FSource.Lookback; end; function TConvertSeries.GetTotalCount: Int64; begin Result := FSource.TotalCount; end; { TAggregateDataSeries } constructor TAggregateDataSeries.Create(const ASource: IDataSeries; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc); begin inherited Create; FSource := ASource; FTimeFrame := ATimeFrame; FAggregateFunc := AAggregateFunc; FCachedItems := TDictionary>.Create; FAggregatedCount := -1; // -1 indicates that it has not been calculated yet end; destructor TAggregateDataSeries.Destroy; begin FCachedItems.Free; inherited; end; procedure TAggregateDataSeries.CalculateAggregatedCount; var totalTimeSpan: Double; begin if FSource.Count = 0 then begin FBaseTime := 0; FAggregatedCount := 0; end else begin // The timestamp of the oldest element serves as the anchor for our time grid. FBaseTime := FSource.Items[FSource.Count - 1].Time; // Total duration covered by the source series. totalTimeSpan := FSource.Items[0].Time - FBaseTime; if totalTimeSpan >= 0 then FAggregatedCount := Trunc(totalTimeSpan / FTimeFrame) + 1 else FAggregatedCount := 0; end; end; function TAggregateDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; begin // Adding to an aggregated series is complex. // The most straightforward approach is to create a composite series. Result := TCompositeDataSeries.CreateComposite(Self, Data, First, Count); end; function TAggregateDataSeries.GetCount: Int64; begin if FAggregatedCount = -1 then CalculateAggregatedCount; Result := FAggregatedCount; end; function TAggregateDataSeries.GetItems(Idx: Int64): TDataPoint; var startTime, endTime: TDateTime; sourcePoints: TList>; sourceIdx: Int64; begin Assert((Idx >= 0) and (Idx < GetCount), 'Index is out of bounds.'); if FCachedItems.TryGetValue(Idx, Result) then exit; // Calculate the time window for the requested aggregated data point. // Index 0 is the newest, so we calculate backwards from the total count. endTime := FBaseTime + (FAggregatedCount - Idx) * FTimeFrame; startTime := endTime - FTimeFrame; sourcePoints := TList>.Create; try // Find all source data points that fall into this time window. // We can optimize the start of the search using the IndexOf function. sourceIdx := TDataSeries(FSource).IndexOf(endTime); if sourceIdx = -1 then sourceIdx := 0; // If endTime is after the last element, start at the newest. while (sourceIdx < FSource.Count) do begin var P := FSource.Items[sourceIdx]; if P.Time < startTime then break; // We have moved past our time window if P.Time < endTime then // Time is within [startTime, endTime) begin sourcePoints.Add(P); end; Inc(sourceIdx); end; // The aggregation function expects the data in chronological order (oldest first), // but we collected it in reverse. So we reverse the list. sourcePoints.Reverse; Result.Create(endTime, FAggregateFunc(sourcePoints.ToArray)); finally sourcePoints.Free; end; // Cache the result for future calls FCachedItems.Add(Idx, Result); end; function TAggregateDataSeries.GetLookback: Int64; begin Result := FSource.Lookback; // The lookback is defined by the source series. end; function TAggregateDataSeries.GetTotalCount: Int64; begin // The total count of aggregated items is simply its current count, as it's a view. Result := GetCount; end; end.