unit Myc.Trade.DataPoint; interface uses System.TimeSpan, Myc.Core.Notifier; type // A data record for an Ask/Bid price pair. TAskBidItem = packed record Ask: Double; Bid: Double; constructor Create(AAsk, ABid: Double); end; TOhlcItem = record Open: Double; High: Double; Low: Double; Close: Double; Volume: Double; constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double); end; // Represents a time-stamped data point in a series. TDataPoint = record Time: TDateTime; Data: T; constructor Create(ATime: TDateTime; const AData: T); end; IMycProcessor = interface function ProcessData(const Value: T): Boolean; procedure Update; end; TMycProcessor = class abstract(TInterfacedObject, IMycProcessor) protected function ProcessData(const Value: T): Boolean; virtual; abstract; procedure Update; virtual; end; TTag = Pointer; IMycDataProvider = interface function Link(const Receiver: IMycProcessor): TTag; procedure Unlink(Tag: TTag); end; TMycDataProvider = class abstract(TContainedObject, IMycDataProvider) private FListeners: TMycNotifyList>; public constructor Create(const Controller: IInterface); destructor Destroy; override; // Notifies all linked processors. procedure Notify(const Func: TMycNotifyList>.TNotifyProc); // Link a Processor function Link(const Processor: IMycProcessor): TTag; // Unlink a linked strategy procedure Unlink(Tag: TTag); end; TMycGenericProcessor = class(TMycProcessor) type TProc = reference to function(const Value: T): Boolean; private FProc: TProc; protected function ProcessData(const Value: T): Boolean; override; public constructor Create(const AProc: TProc); end; TMycContainedProcessor = class(TContainedObject, IMycProcessor) type TProc = function(const Value: T): Boolean of object; private FProc: TProc; function ProcessData(const Value: T): Boolean; procedure Update; public constructor Create(const Controller: IInterface; const AProc: TProc); end; // An immutable time-ordered series of data points. // The most recent element has the logical index 0. IDataSeries = interface function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; function GetLookback: Int64; // Add data and result the new series. 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; TDataSeriesOhlcHelper = record helper for TDataSeries function ToOpen: TDataSeries; function ToClose: TDataSeries; function ToHigh: TDataSeries; function ToLow: TDataSeries; function ToVolume: TDataSeries; end; implementation uses System.SysUtils, System.Math, System.Generics.Collections, Myc.Trade.DataArray, 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)); Result := TDataSeries.CreateDataSeries(ohlcPoints.Count, ohlcPoints.ToArray); finally ohlcPoints.Free; end; end; function TDataSeriesOhlcHelper.ToClose: TDataSeries; begin Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Close; end); end; function TDataSeriesOhlcHelper.ToHigh: TDataSeries; begin Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.High; end); end; function TDataSeriesOhlcHelper.ToLow: TDataSeries; begin Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Low; end); end; function TDataSeriesOhlcHelper.ToOpen: TDataSeries; begin Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Open; end); end; function TDataSeriesOhlcHelper.ToVolume: TDataSeries; begin Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Volume; end); end; { TAskBidItem } constructor TAskBidItem.Create(AAsk, ABid: Double); begin Ask := AAsk; Bid := ABid; end; { TOhlcItem } constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Double); 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 {$ifdef DEBUG} for var i := First + 1 to First + Count - 1 do Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted'); if FDataSeries.Count > 0 then Assert(Data[First].Time >= FDataSeries[0].Time, 'First new item is older than last existing item'); {$endif} Result := FDataSeries.Add(Data, First, Count); end; function TDataSeries.Add(const Data: TArray>): TDataSeries; begin Result := 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; constructor TMycGenericProcessor.Create(const AProc: TProc); begin inherited Create; FProc := AProc; end; function TMycGenericProcessor.ProcessData(const Value: T): Boolean; begin Result := FProc(Value); end; constructor TMycContainedProcessor.Create(const Controller: IInterface; const AProc: TProc); begin inherited Create(Controller); FProc := AProc; end; function TMycContainedProcessor.ProcessData(const Value: T): Boolean; begin Result := FProc(Value); end; procedure TMycContainedProcessor.Update; begin end; { TMycDataProvider } constructor TMycDataProvider.Create(const Controller: IInterface); begin inherited Create(Controller); end; destructor TMycDataProvider.Destroy; begin FListeners.Finalize; inherited Destroy; end; procedure TMycDataProvider.Notify(const Func: TMycNotifyList>.TNotifyProc); begin FListeners.Lock; try FListeners.Notify(Func); finally FListeners.Release; end; end; function TMycDataProvider.Link(const Processor: IMycProcessor): TTag; begin // Add the Processor to the notification list FListeners.Lock; try Result := FListeners.Advise(Processor); finally FListeners.Release; end; end; procedure TMycDataProvider.Unlink(Tag: TTag); begin FListeners.Lock; try FListeners.Unadvise(Tag); finally FListeners.Release; end; end; procedure TMycProcessor.Update; begin end; end.