1st commit
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
(* ------------------------------------------------------------------------------
|
||||
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.TickLoader;
|
||||
|
||||
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
|
||||
['{B4F6A9C1-0A7E-42D3-847B-170705A8CC8D}'] // Added GUID
|
||||
function Init(const ADataSet: TArray<TTickData>): Boolean; // Changed DataSet to ADataSet for clarity
|
||||
function IsNewBar(AIndex: Integer; const ATick: TTickData): 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<TTickData>;
|
||||
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<TTickData>;
|
||||
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: TTickData;
|
||||
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].Ask;
|
||||
FGlobalMaxY := ATickData[0].Ask;
|
||||
for I := 1 to High(ATickData) do
|
||||
begin
|
||||
if ATickData[I].Ask < FGlobalMinY then
|
||||
FGlobalMinY := ATickData[I].Ask;
|
||||
if ATickData[I].Ask > FGlobalMaxY then
|
||||
FGlobalMaxY := ATickData[I].Ask;
|
||||
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].Ask;
|
||||
HighPrice := ATickData[0].Ask;
|
||||
LowPrice := ATickData[0].Ask;
|
||||
ClosePrice := ATickData[0].Ask;
|
||||
|
||||
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].Ask; // Close of previous candle is the tick before the new bar starts
|
||||
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.Ask;
|
||||
HighPrice := CurrentTick.Ask;
|
||||
LowPrice := CurrentTick.Ask;
|
||||
ClosePrice := CurrentTick.Ask;
|
||||
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.Ask; // Open is set
|
||||
HighPrice := CurrentTick.Ask; // Initial High
|
||||
LowPrice := CurrentTick.Ask; // Initial Low
|
||||
end
|
||||
else // Update High/Low for ongoing candle
|
||||
begin
|
||||
if CurrentTick.Ask > HighPrice then
|
||||
HighPrice := CurrentTick.Ask;
|
||||
if CurrentTick.Ask < LowPrice then
|
||||
LowPrice := CurrentTick.Ask;
|
||||
end;
|
||||
ClosePrice := CurrentTick.Ask; // Always update close to current tick
|
||||
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].OADateTime;
|
||||
var LastTickTimeVal_A := ATickData[High(ATickData)].OADateTime;
|
||||
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].Ask;
|
||||
ClosePrice := ATickData[DataIndexEnd].Ask;
|
||||
HighPrice := OpenPrice; // Initialize with Open
|
||||
LowPrice := OpenPrice; // Initialize with Open
|
||||
|
||||
for J := DataIndexStart to DataIndexEnd do
|
||||
begin
|
||||
if ATickData[J].Ask > HighPrice then HighPrice := ATickData[J].Ask;
|
||||
if ATickData[J].Ask < LowPrice then LowPrice := ATickData[J].Ask;
|
||||
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].Ask;
|
||||
FCachedCandles[I].HighPrice := ATickData[I].Ask;
|
||||
FCachedCandles[I].LowPrice := ATickData[I].Ask;
|
||||
FCachedCandles[I].ClosePrice := ATickData[I].Ask;
|
||||
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].OADateTime / TimeframeIntervalOADays) * TimeframeIntervalOADays;
|
||||
BucketEndTimeOADate := BucketStartTimeOADate + TimeframeIntervalOADays;
|
||||
|
||||
DataIndexStart := CurrentTickIndex;
|
||||
OpenPrice := ATickData[CurrentTickIndex].Ask;
|
||||
HighPrice := ATickData[CurrentTickIndex].Ask;
|
||||
LowPrice := ATickData[CurrentTickIndex].Ask;
|
||||
ClosePrice := ATickData[CurrentTickIndex].Ask; // Initialize close with open
|
||||
DataIndexEnd := CurrentTickIndex; // Initialize end with start
|
||||
|
||||
// Iterate through ticks that fall into the current bucket
|
||||
while (CurrentTickIndex <= High(ATickData)) and
|
||||
(ATickData[CurrentTickIndex].OADateTime >= BucketStartTimeOADate) and
|
||||
(ATickData[CurrentTickIndex].OADateTime < BucketEndTimeOADate) do
|
||||
begin
|
||||
if ATickData[CurrentTickIndex].Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Ask;
|
||||
if ATickData[CurrentTickIndex].Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Ask;
|
||||
ClosePrice := ATickData[CurrentTickIndex].Ask; // Continuously update close price
|
||||
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].OADateTime;
|
||||
var LastTickTimeVal := ATickData[High(ATickData)].OADateTime;
|
||||
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.
|
||||
Reference in New Issue
Block a user