512 lines
25 KiB
ObjectPascal
512 lines
25 KiB
ObjectPascal
(* ------------------------------------------------------------------------------
|
|
Unit Name: Myc.OHLCCache
|
|
Author: Delphi Algo Trading Assistant (Generated by AI)
|
|
Date: May 21, 2025
|
|
Purpose: This unit provides a class, TOHLC, designed to aggregate raw
|
|
tick data into OHLC (Open, High, Low, Close) candle data and
|
|
manage this cached candle data. It supports various aggregation
|
|
timeframes, including a generic callback mode using an interface,
|
|
and provides helper functions for data interpretation.
|
|
|
|
Main Features:
|
|
- OHLC Candle Structure: Defines TCachedCandle record.
|
|
- TOHLC Class: Encapsulates OHLC candle building and storage.
|
|
* Aggregation Modes:
|
|
- Fixed Timeframes: Aggregates ticks into candles of a specified duration.
|
|
- Tick-by-Tick: Each incoming tick forms a separate OHLC candle.
|
|
- Auto Aggregation: Calculates aggregation to fit display width.
|
|
- Generic Builder: Uses a user-defined class implementing IGenericCandleBuilder
|
|
to determine new candle breaks.
|
|
* Data Caching, Global Price Range, Validity Status, Metadata, Informational Properties.
|
|
- Time Formatting: Helper function FormatTimeSpanFromSeconds.
|
|
|
|
Key Constants:
|
|
- TF_AUTO_AGGREGATION, TF_TICK_BY_TICK, TF_GENERIC_CALLBACK.
|
|
Key Types:
|
|
- TCachedCandle, IGenericCandleBuilder, TOHLC.
|
|
Key Methods in TOHLC:
|
|
- Create, ClearCache, Build.
|
|
Dependencies: (As before)
|
|
------------------------------------------------------------------------------ *)
|
|
unit Myc.OHLCCache;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Classes, System.SysUtils, System.Math,
|
|
System.Generics.Collections, System.DateUtils,
|
|
Myc.Trade.DataStream, Myc.Trade.DataPoint;
|
|
|
|
const
|
|
TF_AUTO_AGGREGATION = 0;
|
|
TF_TICK_BY_TICK = -1;
|
|
TF_GENERIC_CALLBACK = -2; // Constant remains, signifies generic mode
|
|
SecsPerDay_Double = 24.0 * 60.0 * 60.0;
|
|
|
|
type
|
|
TCachedCandle = record
|
|
OpenPrice: Single;
|
|
HighPrice: Single;
|
|
LowPrice: Single;
|
|
ClosePrice: Single;
|
|
OriginalDataIndexStart: Integer;
|
|
OriginalDataIndexEnd: Integer;
|
|
TickVolume: Integer;
|
|
end;
|
|
|
|
// Interface for generic candle building logic
|
|
IGenericCandleBuilder = interface
|
|
function Init(const ADataSet: TArray<TDataRecord<TAskBidItem>>): Boolean; // Changed DataSet to ADataSet for clarity
|
|
function IsNewBar(AIndex: Integer; const ATick: TDataRecord<TAskBidItem>): Boolean; // Changed Idx to AIndex, Tick to ATick
|
|
function GetCaption: string; // Method for the Caption property
|
|
property Caption: string read GetCaption;
|
|
end;
|
|
|
|
TOHLC = class
|
|
private
|
|
FCachedCandles: TArray<TCachedCandle>;
|
|
FGlobalMinY: Single;
|
|
FGlobalMaxY: Single;
|
|
FIsValid: Boolean;
|
|
FAggregationTimeframeSeconds: Int64;
|
|
FAutoAggCandleDisplayWidth: Integer;
|
|
FAutoAggPaintBoxClientWidth: Integer;
|
|
FAutoAggNumCandlesInCache: Integer;
|
|
FApproxTimePerCandleText: string;
|
|
FTotalDataTimeSpanText: string;
|
|
|
|
function GetCount: Integer;
|
|
function GetCandle(Index: Integer): TCachedCandle;
|
|
public
|
|
constructor Create;
|
|
procedure ClearCache;
|
|
procedure Build(const ATickData: TArray<TDataRecord<TAskBidItem>>;
|
|
ASelectedTimeframeSeconds: Int64;
|
|
AAutoAggCandleWidth: Integer;
|
|
AAutoAggCandleSpacing: Integer;
|
|
AAutoAggPaintBoxClientWidth: Integer;
|
|
AMemoForLogging: TStrings;
|
|
// Parameter changed from TGenericCandleProc to IGenericCandleBuilder
|
|
AGenericCandleBuilder: IGenericCandleBuilder = nil);
|
|
|
|
property Candles[Index: Integer]: TCachedCandle read GetCandle; default;
|
|
property Count: Integer read GetCount;
|
|
property IsValid: Boolean read FIsValid;
|
|
property GlobalMinY: Single read FGlobalMinY;
|
|
property GlobalMaxY: Single read FGlobalMaxY;
|
|
property AggregationTimeframeSeconds: Int64 read FAggregationTimeframeSeconds;
|
|
property ApproxTimePerCandleText: string read FApproxTimePerCandleText;
|
|
property TotalDataTimeSpanText: string read FTotalDataTimeSpanText;
|
|
property AutoAggNumCandlesInCache: Integer read FAutoAggNumCandlesInCache;
|
|
end;
|
|
|
|
implementation
|
|
|
|
function FormatTimeSpanFromSeconds(const TotalSecondsInput: Double): string;
|
|
const
|
|
SecsPerMin = 60;
|
|
SecsPerHour = SecsPerMin * 60;
|
|
SecsPerDayConst = SecsPerHour * 24;
|
|
var
|
|
Days, Hours, Minutes, SecComponent: Int64;
|
|
sTotalSeconds, FracSec: Double;
|
|
ResultBuilder: TStringBuilder;
|
|
begin // Standard implementation as previously provided
|
|
sTotalSeconds := Abs(TotalSecondsInput);
|
|
if sTotalSeconds < 0.001 then
|
|
Result := 'less than 1 ms'
|
|
else if sTotalSeconds < 1.0 then
|
|
Result := Format('%.0f ms', [sTotalSeconds * 1000.0])
|
|
else if sTotalSeconds < SecsPerMin then
|
|
Result := Format('%.2f seconds', [sTotalSeconds])
|
|
else if sTotalSeconds < SecsPerHour then
|
|
Result := Format('%.1f minutes (approx. %d secs)', [sTotalSeconds / SecsPerMin, Floor(sTotalSeconds)])
|
|
else if sTotalSeconds < SecsPerDayConst then
|
|
Result := Format('%.1f hours (approx. %d mins)', [sTotalSeconds / SecsPerHour, Floor(sTotalSeconds / SecsPerMin)])
|
|
else
|
|
begin
|
|
ResultBuilder := TStringBuilder.Create;
|
|
try
|
|
Days := Trunc(sTotalSeconds / SecsPerDayConst);
|
|
sTotalSeconds := sTotalSeconds - (Days * SecsPerDayConst);
|
|
Hours := Trunc(sTotalSeconds / SecsPerHour);
|
|
sTotalSeconds := sTotalSeconds - (Hours * SecsPerHour);
|
|
Minutes := Trunc(sTotalSeconds / SecsPerMin);
|
|
sTotalSeconds := sTotalSeconds - (Minutes * SecsPerMin);
|
|
SecComponent := Trunc(sTotalSeconds);
|
|
FracSec := Frac(sTotalSeconds);
|
|
if Days > 0 then
|
|
ResultBuilder.Append(Format('%d days, ', [Days]));
|
|
if Hours > 0 then
|
|
ResultBuilder.Append(Format('%d hours, ', [Hours]));
|
|
if Minutes > 0 then
|
|
ResultBuilder.Append(Format('%d minutes, ', [Minutes]));
|
|
ResultBuilder.Append(Format('%d', [SecComponent]));
|
|
if FracSec > 0.001 then
|
|
ResultBuilder.Append(Format('.%02d', [Round(FracSec * 100)]));
|
|
ResultBuilder.Append(' seconds');
|
|
Result := ResultBuilder.ToString;
|
|
finally
|
|
ResultBuilder.Free;
|
|
end;
|
|
end;
|
|
if TotalSecondsInput < 0.0 then
|
|
Result := '-' + Result;
|
|
end;
|
|
|
|
{ TOHLC }
|
|
|
|
constructor TOHLC.Create;
|
|
begin
|
|
ClearCache;
|
|
end;
|
|
|
|
procedure TOHLC.ClearCache;
|
|
begin
|
|
FIsValid := False;
|
|
SetLength(FCachedCandles, 0);
|
|
FGlobalMinY := 0;
|
|
FGlobalMaxY := 0;
|
|
FAggregationTimeframeSeconds := 0;
|
|
FAutoAggCandleDisplayWidth := 0;
|
|
FAutoAggPaintBoxClientWidth := 0;
|
|
FAutoAggNumCandlesInCache := 0;
|
|
FApproxTimePerCandleText := 'N/A';
|
|
FTotalDataTimeSpanText := 'N/A';
|
|
end;
|
|
|
|
function TOHLC.GetCandle(Index: Integer): TCachedCandle;
|
|
begin
|
|
if (Index >= 0) and (Index < Length(FCachedCandles)) then
|
|
Result := FCachedCandles[Index]
|
|
else
|
|
Result := Default(TCachedCandle);
|
|
end;
|
|
|
|
function TOHLC.GetCount: Integer;
|
|
begin
|
|
Result := Length(FCachedCandles);
|
|
end;
|
|
|
|
procedure TOHLC.Build(const ATickData: TArray<TDataRecord<TAskBidItem>>;
|
|
ASelectedTimeframeSeconds: Int64;
|
|
AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; AAutoAggPaintBoxClientWidth: Integer;
|
|
AMemoForLogging: TStrings; AGenericCandleBuilder: IGenericCandleBuilder = nil); // Parameter changed
|
|
var
|
|
I, CurrentTickIndex, DataIndexStart, DataIndexEnd, J, NumDataPoints, ChartPixelWidthForAutoAgg, NumCandlesToCacheAuto,
|
|
SlotWidthForAutoAgg: Integer;
|
|
OpenPrice, HighPrice, LowPrice, ClosePrice: Single;
|
|
BucketStartTimeOADate, BucketEndTimeOADate, TimeframeIntervalOADays: Double;
|
|
CandleList: TList<TCachedCandle>;
|
|
TempCandle: TCachedCandle;
|
|
LogTimePerCandleStr: string;
|
|
CurrentTick: TDataRecord<TAskBidItem>;
|
|
begin
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - TOHLC.Build: TF Secs: %d...',
|
|
[ASelectedTimeframeSeconds]));
|
|
ClearCache();
|
|
FAggregationTimeframeSeconds := ASelectedTimeframeSeconds;
|
|
NumDataPoints := Length(ATickData);
|
|
|
|
if NumDataPoints < 1 then
|
|
begin
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add('TOHLC.Build: No tick data.');
|
|
Exit;
|
|
end;
|
|
|
|
FGlobalMinY := ATickData[0].Data.Ask; // Changed
|
|
FGlobalMaxY := ATickData[0].Data.Ask; // Changed
|
|
for I := 1 to High(ATickData) do
|
|
begin
|
|
if ATickData[I].Data.Ask < FGlobalMinY then // Changed
|
|
FGlobalMinY := ATickData[I].Data.Ask; // Changed
|
|
if ATickData[I].Data.Ask > FGlobalMaxY then // Changed
|
|
FGlobalMaxY := ATickData[I].Data.Ask; // Changed
|
|
end;
|
|
if FGlobalMaxY = FGlobalMinY then
|
|
begin
|
|
FGlobalMaxY := FGlobalMinY + 0.0001; // Ensure a minimal range
|
|
FGlobalMinY := FGlobalMinY - 0.0001;
|
|
end;
|
|
if FGlobalMaxY = FGlobalMinY then // Still equal (e.g. if original was 0)
|
|
FGlobalMaxY := FGlobalMinY + 1; // Ensure a non-zero range
|
|
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build: Global Y-Range: %.5f to %.5f', [FGlobalMinY, FGlobalMaxY]));
|
|
LogTimePerCandleStr := 'N/A';
|
|
|
|
// Generic Candle Building using IGenericCandleBuilder
|
|
if (ASelectedTimeframeSeconds = TF_GENERIC_CALLBACK) and Assigned(AGenericCandleBuilder) then
|
|
begin
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Generic Builder: %s): Aggregating %d ticks.', [AGenericCandleBuilder.Caption, NumDataPoints]));
|
|
|
|
if not AGenericCandleBuilder.Init(ATickData) then
|
|
begin
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Generic Builder: %s): Init method failed.', [AGenericCandleBuilder.Caption]));
|
|
FIsValid := False; // Ensure cache is marked as invalid
|
|
Exit; // Exit if Init fails
|
|
end;
|
|
|
|
CandleList := TList<TCachedCandle>.Create;
|
|
try
|
|
if NumDataPoints > 0 then
|
|
begin
|
|
DataIndexStart := 0;
|
|
OpenPrice := ATickData[0].Data.Ask; // Changed
|
|
HighPrice := ATickData[0].Data.Ask; // Changed
|
|
LowPrice := ATickData[0].Data.Ask; // Changed
|
|
|
|
for I := 0 to NumDataPoints - 1 do
|
|
begin
|
|
CurrentTick := ATickData[I];
|
|
// Use IGenericCandleBuilder.IsNewBar
|
|
if (I > DataIndexStart) and AGenericCandleBuilder.IsNewBar(I, CurrentTick) then // Current tick starts a NEW candle
|
|
begin
|
|
// Finalize the PREVIOUS candle (ending at I-1)
|
|
TempCandle.OpenPrice := OpenPrice;
|
|
TempCandle.HighPrice := HighPrice;
|
|
TempCandle.LowPrice := LowPrice;
|
|
TempCandle.ClosePrice := ATickData[I - 1].Data.Ask; // Close of previous candle is the tick before the new bar starts // Changed
|
|
TempCandle.OriginalDataIndexStart := DataIndexStart;
|
|
TempCandle.OriginalDataIndexEnd := I - 1;
|
|
TempCandle.TickVolume := (I - 1) - DataIndexStart + 1;
|
|
CandleList.Add(TempCandle);
|
|
|
|
// Current tick 'I' starts a new candle
|
|
DataIndexStart := I;
|
|
OpenPrice := CurrentTick.Data.Ask; // Changed
|
|
HighPrice := CurrentTick.Data.Ask; // Changed
|
|
LowPrice := CurrentTick.Data.Ask; // Changed
|
|
ClosePrice := CurrentTick.Data.Ask; // Changed
|
|
end
|
|
else // Current tick continues the existing candle OR is the first tick of the very first candle
|
|
begin
|
|
if I = DataIndexStart then // This is the first tick of the current (or overall first) candle
|
|
begin
|
|
OpenPrice := CurrentTick.Data.Ask; // Open is set // Changed
|
|
HighPrice := CurrentTick.Data.Ask; // Initial High // Changed
|
|
LowPrice := CurrentTick.Data.Ask; // Initial Low // Changed
|
|
end
|
|
else // Update High/Low for ongoing candle
|
|
begin
|
|
if CurrentTick.Data.Ask > HighPrice then // Changed
|
|
HighPrice := CurrentTick.Data.Ask; // Changed
|
|
if CurrentTick.Data.Ask < LowPrice then // Changed
|
|
LowPrice := CurrentTick.Data.Ask; // Changed
|
|
end;
|
|
ClosePrice := CurrentTick.Data.Ask; // Always update close to current tick // Changed
|
|
end;
|
|
|
|
// If this is the last tick in the dataset, finalize the current candle
|
|
if I = NumDataPoints - 1 then
|
|
begin
|
|
TempCandle.OpenPrice := OpenPrice;
|
|
TempCandle.HighPrice := HighPrice;
|
|
TempCandle.LowPrice := LowPrice;
|
|
TempCandle.ClosePrice := ClosePrice; // Close price is this tick's price
|
|
TempCandle.OriginalDataIndexStart := DataIndexStart;
|
|
TempCandle.OriginalDataIndexEnd := I;
|
|
TempCandle.TickVolume := I - DataIndexStart + 1;
|
|
CandleList.Add(TempCandle);
|
|
end;
|
|
end;
|
|
end;
|
|
FCachedCandles := CandleList.ToArray;
|
|
LogTimePerCandleStr := Format('(%s)', [AGenericCandleBuilder.Caption]); // Use Builder's Caption
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Generic Builder: %s): Created %d candles.', [AGenericCandleBuilder.Caption, Length(FCachedCandles)]));
|
|
finally
|
|
CandleList.Free;
|
|
end;
|
|
end
|
|
else if FAggregationTimeframeSeconds = TF_AUTO_AGGREGATION then
|
|
begin // Auto Aggregation logic
|
|
FAutoAggCandleDisplayWidth := AAutoAggCandleWidth;
|
|
FAutoAggPaintBoxClientWidth := AAutoAggPaintBoxClientWidth;
|
|
ChartPixelWidthForAutoAgg := FAutoAggPaintBoxClientWidth;
|
|
if ChartPixelWidthForAutoAgg <= 0 then
|
|
begin
|
|
if AMemoForLogging <> nil then AMemoForLogging.Add('TOHLC.Build (Auto): PaintBox width too small.');
|
|
Exit;
|
|
end;
|
|
SlotWidthForAutoAgg := FAutoAggCandleDisplayWidth + AAutoAggCandleSpacing;
|
|
if SlotWidthForAutoAgg <= 0 then
|
|
begin
|
|
if AMemoForLogging <> nil then AMemoForLogging.Add('TOHLC.Build (Auto): SlotWidth zero or negative.');
|
|
Exit;
|
|
end;
|
|
NumCandlesToCacheAuto := ChartPixelWidthForAutoAgg div SlotWidthForAutoAgg;
|
|
FAutoAggNumCandlesInCache := NumCandlesToCacheAuto;
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Auto): Aggregating with CW %dpx, Sp %dpx. SlotW %dpx. NumCandles: %d.',
|
|
[FAutoAggCandleDisplayWidth, AAutoAggCandleSpacing, SlotWidthForAutoAgg, NumCandlesToCacheAuto]));
|
|
|
|
if NumCandlesToCacheAuto <= 0 then
|
|
begin
|
|
if AMemoForLogging <> nil then AMemoForLogging.Add('TOHLC.Build (Auto): Not enough space for candles.');
|
|
Exit;
|
|
end;
|
|
SetLength(FCachedCandles, NumCandlesToCacheAuto);
|
|
if (NumDataPoints > 1) and (NumCandlesToCacheAuto > 0) then
|
|
begin
|
|
var FirstTickTimeVal_A := ATickData[0].TimeStamp;
|
|
var LastTickTimeVal_A := ATickData[High(ATickData)].TimeStamp;
|
|
var TotalDataDurationDaysVal_A := LastTickTimeVal_A - FirstTickTimeVal_A;
|
|
if NumCandlesToCacheAuto > 0 then // Avoid division by zero if NumCandlesToCacheAuto is somehow 0 here
|
|
begin
|
|
var AvgCandleDurationDaysVal_A := TotalDataDurationDaysVal_A / NumCandlesToCacheAuto;
|
|
LogTimePerCandleStr := FormatTimeSpanFromSeconds(AvgCandleDurationDaysVal_A * SecsPerDay_Double);
|
|
end
|
|
else
|
|
LogTimePerCandleStr := 'N/A (Auto)';
|
|
end;
|
|
for I := 0 to NumCandlesToCacheAuto - 1 do
|
|
begin
|
|
DataIndexStart := Floor((I * SlotWidthForAutoAgg) * (NumDataPoints / ChartPixelWidthForAutoAgg));
|
|
DataIndexEnd := Floor(((I + 1) * SlotWidthForAutoAgg) * (NumDataPoints / ChartPixelWidthForAutoAgg)) - 1;
|
|
DataIndexStart := Max(0, DataIndexStart);
|
|
DataIndexEnd := Min(High(ATickData), Max(DataIndexStart, DataIndexEnd));
|
|
|
|
if (DataIndexStart > High(ATickData)) or (DataIndexStart > DataIndexEnd) then
|
|
begin
|
|
FCachedCandles[I] := Default(TCachedCandle); // Initialize with default values
|
|
Continue;
|
|
end;
|
|
|
|
OpenPrice := ATickData[DataIndexStart].Data.Ask; // Changed
|
|
ClosePrice := ATickData[DataIndexEnd].Data.Ask; // Changed
|
|
HighPrice := OpenPrice; // Initialize with Open
|
|
LowPrice := OpenPrice; // Initialize with Open
|
|
|
|
for J := DataIndexStart to DataIndexEnd do
|
|
begin
|
|
if ATickData[J].Data.Ask > HighPrice then HighPrice := ATickData[J].Data.Ask; // Changed
|
|
if ATickData[J].Data.Ask < LowPrice then LowPrice := ATickData[J].Data.Ask; // Changed
|
|
end;
|
|
// Ensure High/Low encompass Open/Close after loop
|
|
HighPrice := Max(HighPrice, Max(OpenPrice, ClosePrice));
|
|
LowPrice := Min(LowPrice, Min(OpenPrice, ClosePrice));
|
|
|
|
FCachedCandles[I].OpenPrice := OpenPrice;
|
|
FCachedCandles[I].HighPrice := HighPrice;
|
|
FCachedCandles[I].LowPrice := LowPrice;
|
|
FCachedCandles[I].ClosePrice := ClosePrice;
|
|
FCachedCandles[I].OriginalDataIndexStart := DataIndexStart;
|
|
FCachedCandles[I].OriginalDataIndexEnd := DataIndexEnd;
|
|
FCachedCandles[I].TickVolume := DataIndexEnd - DataIndexStart + 1;
|
|
end;
|
|
end
|
|
else if FAggregationTimeframeSeconds = TF_TICK_BY_TICK then
|
|
begin // Tick-by-Tick logic
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Tick-by-Tick): Creating %d candles.', [NumDataPoints]));
|
|
SetLength(FCachedCandles, NumDataPoints);
|
|
for I := 0 to NumDataPoints - 1 do
|
|
begin
|
|
FCachedCandles[I].OpenPrice := ATickData[I].Data.Ask; // Changed
|
|
FCachedCandles[I].HighPrice := ATickData[I].Data.Ask; // Changed
|
|
FCachedCandles[I].LowPrice := ATickData[I].Data.Ask; // Changed
|
|
FCachedCandles[I].ClosePrice := ATickData[I].Data.Ask; // Changed
|
|
FCachedCandles[I].OriginalDataIndexStart := I;
|
|
FCachedCandles[I].OriginalDataIndexEnd := I;
|
|
FCachedCandles[I].TickVolume := 1;
|
|
end;
|
|
LogTimePerCandleStr := '(duration of one tick)';
|
|
end
|
|
else // Fixed Timeframe (FAggregationTimeframeSeconds > 0)
|
|
begin // Fixed Timeframe logic
|
|
CandleList := TList<TCachedCandle>.Create;
|
|
try
|
|
TimeframeIntervalOADays := FAggregationTimeframeSeconds / SecsPerDay_Double;
|
|
if TimeframeIntervalOADays <= 1E-9 then // Avoid division by zero or too small interval
|
|
begin
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Fixed TF): Error - Interval too small (%.10f).', [TimeframeIntervalOADays]));
|
|
Exit;
|
|
end;
|
|
|
|
CurrentTickIndex := 0;
|
|
while CurrentTickIndex <= High(ATickData) do
|
|
begin
|
|
BucketStartTimeOADate := Floor(ATickData[CurrentTickIndex].TimeStamp / TimeframeIntervalOADays) * TimeframeIntervalOADays;
|
|
BucketEndTimeOADate := BucketStartTimeOADate + TimeframeIntervalOADays;
|
|
|
|
DataIndexStart := CurrentTickIndex;
|
|
OpenPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
|
HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
|
LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
|
ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Initialize close with open // Changed
|
|
DataIndexEnd := CurrentTickIndex; // Initialize end with start
|
|
|
|
// Iterate through ticks that fall into the current bucket
|
|
while (CurrentTickIndex <= High(ATickData)) and
|
|
(ATickData[CurrentTickIndex].TimeStamp >= BucketStartTimeOADate) and
|
|
(ATickData[CurrentTickIndex].TimeStamp < BucketEndTimeOADate) do
|
|
begin
|
|
if ATickData[CurrentTickIndex].Data.Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
|
if ATickData[CurrentTickIndex].Data.Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
|
ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Continuously update close price // Changed
|
|
DataIndexEnd := CurrentTickIndex; // Mark the last tick included in this candle
|
|
Inc(CurrentTickIndex);
|
|
end;
|
|
|
|
// If no ticks were processed in the inner loop (e.g., if the first tick was already >= BucketEndTimeOADate)
|
|
// ensure we advance CurrentTickIndex to avoid an infinite loop if DataIndexStart remains unchanged.
|
|
// This situation should ideally be rare if data is sorted and dense enough for the timeframe.
|
|
// However, if DataIndexStart equals CurrentTickIndex here, it means the inner loop didn't run.
|
|
// This could happen if ATickData[CurrentTickIndex].OADateTime was already >= BucketEndTimeOADate.
|
|
// In such case, we still form a candle with the single tick at DataIndexStart, and then CurrentTickIndex must advance.
|
|
// If the loop DID run, CurrentTickIndex is already pointing to the next tick outside the bucket.
|
|
if (DataIndexStart = DataIndexEnd) And (CurrentTickIndex = DataIndexStart) then // Only one tick considered, and it might not have advanced CurrentTickIndex
|
|
begin
|
|
if CurrentTickIndex <= High(ATickData) then // If it wasn't the very last tick.
|
|
Inc(CurrentTickIndex); // Ensure progress if loop condition makes it stick.
|
|
end;
|
|
// If the inner while loop exited because CurrentTickIndex > High(ATickData), DataIndexStart might still be a valid index
|
|
// for the last candle.
|
|
// Or, if the loop exited due to time boundary, CurrentTickIndex is correctly positioned for the next bucket.
|
|
|
|
TempCandle.OpenPrice := OpenPrice;
|
|
TempCandle.HighPrice := HighPrice;
|
|
TempCandle.LowPrice := LowPrice;
|
|
TempCandle.ClosePrice := ClosePrice;
|
|
TempCandle.OriginalDataIndexStart := DataIndexStart;
|
|
TempCandle.OriginalDataIndexEnd := DataIndexEnd;
|
|
TempCandle.TickVolume := (DataIndexEnd - DataIndexStart) + 1;
|
|
CandleList.Add(TempCandle);
|
|
|
|
if CurrentTickIndex > High(ATickData) then Break; // All ticks processed
|
|
end;
|
|
FCachedCandles := CandleList.ToArray;
|
|
LogTimePerCandleStr := FormatTimeSpanFromSeconds(FAggregationTimeframeSeconds);
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build (Fixed TF): Created %d candles.', [Length(FCachedCandles)]));
|
|
finally
|
|
CandleList.Free;
|
|
end;
|
|
end;
|
|
|
|
if Length(ATickData) > 0 then
|
|
begin
|
|
var FirstTickTimeVal := ATickData[0].TimeStamp;
|
|
var LastTickTimeVal := ATickData[High(ATickData)].TimeStamp;
|
|
FTotalDataTimeSpanText := FormatTimeSpanFromSeconds((LastTickTimeVal - FirstTickTimeVal) * SecsPerDay_Double);
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build: Total data time span: %s.', [FTotalDataTimeSpanText]));
|
|
end;
|
|
|
|
FApproxTimePerCandleText := LogTimePerCandleStr;
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(Format('TOHLC.Build: Time per cached candle: %s.', [FApproxTimePerCandleText]));
|
|
|
|
FIsValid := True;
|
|
if AMemoForLogging <> nil then
|
|
AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - TOHLC.Build: OHLC cache built.');
|
|
end;
|
|
|
|
end.
|