Files
cTrader-DataExport/common/Myc.ChartDataController.pas
T
Michael Schimmel f571965762 integrated TFuture
2025-06-04 01:55:24 +02:00

435 lines
20 KiB
ObjectPascal

unit Myc.ChartDataController;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
Myc.Futures,
Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants
type
TTimeframeSetting = record
Caption: string; // Unique key identifier (e.g., "M1", "Auto", "Every 20 Ticks")
Description: string; // User-friendly description for UI
Seconds: Int64; // TF_AUTO_AGGREGATION, TF_TICK_BY_TICK, TF_GENERIC_CALLBACK, or >0
end;
TChartDataController = class
private
FTickDataArray: TArray<TTickData>;
FOHLCCacheList: TList<TOHLC>;
FTimeframeSettingsArray: TArray<TTimeframeSetting>; // Now includes registered generic builders
FMemoLog: TStrings;
FGenericBuilders: TDictionary<string, IGenericCandleBuilder>; // Keyed by TTimeframeSetting.Caption
procedure InitializeBaseTimeframeSettings; // Renamed for clarity
function GetGenericBuilder(const ABuilderCaption: string): IGenericCandleBuilder;
public
constructor Create(AMemoTarget: TStrings);
destructor Destroy; override;
procedure LoadTickDataSeries(const AFileName: string);
// GetOrBuildCache now uses a single ASelectedTimeframeCaption for all types of timeframes.
function GetOrBuildCache(const ASelectedTimeframeCaption: string; // e.g., "M1", "Auto", "Every 20 Ticks"
AAutoAggCandleWidth: Integer;
AAutoAggCandleSpacing: Integer;
AAutoAggPaintBoxClientWidth: Integer;
AForceRebuild: Boolean): TOHLC;
procedure ClearAllDataAndCaches;
function GetTickDataCount: Integer;
function GetTickDataArrayPtr: PTTickData;
function GetTickDataLength: Integer;
function GetTickDataItem(AIndex: Integer): TTickData;
function HasData: Boolean;
// ABuilderCaption is the unique key used for FTimeframeSettingsArray.Caption and FGenericBuilders key.
procedure RegisterGenericBuilder(const ABuilderCaption: string;
const ABuilderDescription: string;
ABuilder: IGenericCandleBuilder);
// GetGenericBuilderCaptions is removed. Use GetAvailableTimeframes.
function GetAvailableTimeframes: TArray<TTimeframeSetting>;
end;
implementation
uses
System.DateUtils; // For FormatDateTime in logging
const
dcSecsPerDayConst = 24 * 60 * 60;
{ TChartDataController }
constructor TChartDataController.Create(AMemoTarget: TStrings);
var
I: Integer; // Standard loop variable
begin
inherited Create;
FMemoLog := AMemoTarget;
FGenericBuilders := TDictionary<string, IGenericCandleBuilder>.Create;
InitializeBaseTimeframeSettings; // This populates FTimeframeSettingsArray
FOHLCCacheList := TList<TOHLC>.Create; // Create the TList object
// Add nil placeholders for each timeframe setting defined in FTimeframeSettingsArray
// This ensures FOHLCCacheList is parallel to FTimeframeSettingsArray from the start.
for I := 0 to High(FTimeframeSettingsArray) do
begin
FOHLCCacheList.Add(nil); // Add a nil item to the list
end;
if FMemoLog <> nil then
FMemoLog.Add('TChartDataController instance created. Base timeframes initialized.');
end;
destructor TChartDataController.Destroy;
begin
if FMemoLog <> nil then
FMemoLog.Add('TChartDataController instance destroying...');
ClearAllDataAndCaches(); // This will free TOHLC objects
FreeAndNil(FOHLCCacheList);
FreeAndNil(FGenericBuilders); // Frees dictionary, ARC handles interfaces
if FMemoLog <> nil then
FMemoLog.Add('TChartDataController instance destroyed.');
inherited Destroy;
end;
procedure TChartDataController.InitializeBaseTimeframeSettings;
var
InitialSettings: TList<TTimeframeSetting>;
ts: TTimeframeSetting;
begin
InitialSettings := TList<TTimeframeSetting>.Create;
try
// Define standard, non-generic timeframes
ts.Caption := 'Auto'; ts.Description := 'Auto (Screen Fit)'; ts.Seconds := TF_AUTO_AGGREGATION; InitialSettings.Add(ts);
ts.Caption := 'Tick'; ts.Description := 'Tick-by-Tick'; ts.Seconds := TF_TICK_BY_TICK; InitialSettings.Add(ts);
// The old "Generic Rule" placeholder is removed. Generic rules are added via RegisterGenericBuilder.
ts.Caption := 'M1'; ts.Description := '1 Minute (M1)'; ts.Seconds := 60; InitialSettings.Add(ts);
ts.Caption := 'M5'; ts.Description := '5 Minutes (M5)'; ts.Seconds := 5 * 60; InitialSettings.Add(ts);
ts.Caption := 'M10'; ts.Description := '10 Minutes (M10)'; ts.Seconds := 10 * 60; InitialSettings.Add(ts);
ts.Caption := 'M15'; ts.Description := '15 Minutes (M15)'; ts.Seconds := 15 * 60; InitialSettings.Add(ts);
ts.Caption := 'M30'; ts.Description := '30 Minutes (M30)'; ts.Seconds := 30 * 60; InitialSettings.Add(ts);
ts.Caption := 'H1'; ts.Description := '1 Hour (H1)'; ts.Seconds := 60 * 60; InitialSettings.Add(ts);
ts.Caption := 'H4'; ts.Description := '4 Hours (H4)'; ts.Seconds := 4 * 60 * 60; InitialSettings.Add(ts);
ts.Caption := 'D1'; ts.Description := '1 Day (D1)'; ts.Seconds := dcSecsPerDayConst; InitialSettings.Add(ts);
ts.Caption := 'W1'; ts.Description := '1 Week (W1)'; ts.Seconds := 7 * dcSecsPerDayConst; InitialSettings.Add(ts);
ts.Caption := 'MN1'; ts.Description := '1 Month (MN1)'; ts.Seconds := 30 * dcSecsPerDayConst; InitialSettings.Add(ts);
ts.Caption := 'Y1'; ts.Description := '1 Year (Y1)'; ts.Seconds := 365 * dcSecsPerDayConst; InitialSettings.Add(ts);
FTimeframeSettingsArray := InitialSettings.ToArray;
finally
InitialSettings.Free;
end;
end;
function TChartDataController.GetAvailableTimeframes: TArray<TTimeframeSetting>;
begin
Result := Copy(FTimeframeSettingsArray); // Returns a copy of the current (potentially dynamic) list
end;
procedure TChartDataController.RegisterGenericBuilder(const ABuilderCaption: string;
const ABuilderDescription: string;
ABuilder: IGenericCandleBuilder);
var
NewTimeframeSetting: TTimeframeSetting;
I: Integer;
begin
if not Assigned(ABuilder) then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Attempt to register a nil generic builder for caption "%s".', [ABuilderCaption]));
Exit;
end;
// Check for duplicate caption in existing timeframe settings (both base and previously registered builders)
for I := 0 to High(FTimeframeSettingsArray) do
begin
if CompareText(FTimeframeSettingsArray[I].Caption, ABuilderCaption) = 0 then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Cannot register generic builder. A timeframe (base or generic) with caption "%s" already exists.', [ABuilderCaption]));
Exit;
end;
end;
// Although the loop above should catch caption clashes for FTimeframeSettingsArray,
// FGenericBuilders is a separate dictionary. A builder might be re-registered
// with the same caption but perhaps a different description or instance.
// The current logic is to prevent adding a new TimeframeSetting if the caption exists.
// If a builder with the same caption is already in FGenericBuilders (but somehow not in FTimeframeSettingsArray,
// which would be an inconsistent state), the Add below would fail.
// For robustness, we ensure it's not in FGenericBuilders OR handle replacement.
// Given the check against FTimeframeSettingsArray already prevents duplicate *timeframe captions*,
// if ABuilderCaption is new for timeframes, it must be new for FGenericBuilders too.
if FGenericBuilders.ContainsKey(ABuilderCaption) then
begin
// This case should ideally not be reached if the previous loop correctly exits
// for existing captions in FTimeframeSettingsArray.
// If it is reached, it implies an inconsistent state or a desire to replace only the builder object
// for an existing generic timeframe caption that was somehow registered differently.
// For safety, let's log and replace if this happens.
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Warning - Generic builder key "%s" already in dictionary, but not found as a TimeframeSetting caption. Replacing builder interface.', [ABuilderCaption]));
FGenericBuilders.Remove(ABuilderCaption);
FGenericBuilders.Add(ABuilderCaption, ABuilder);
end
else
begin
// Add to builder dictionary
FGenericBuilders.Add(ABuilderCaption, ABuilder);
// Add to FTimeframeSettingsArray
NewTimeframeSetting.Caption := ABuilderCaption;
NewTimeframeSetting.Description := ABuilderDescription;
NewTimeframeSetting.Seconds := TF_GENERIC_CALLBACK;
SetLength(FTimeframeSettingsArray, Length(FTimeframeSettingsArray) + 1);
FTimeframeSettingsArray[High(FTimeframeSettingsArray)] := NewTimeframeSetting; // This is line 181 from your error reference.
// Extend FOHLCCacheList with a nil placeholder - CORRECTED PART
FOHLCCacheList.Add(nil); // Use TList.Add method
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Registered new generic builder timeframe: Caption="%s", Description="%s".', [ABuilderCaption, ABuilderDescription]));
end;
end;
function TChartDataController.GetGenericBuilder(const ABuilderCaption: string): IGenericCandleBuilder;
begin
Result := nil;
if not FGenericBuilders.TryGetValue(ABuilderCaption, Result) then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Generic builder with caption/key "%s" not found in dictionary.', [ABuilderCaption]));
end;
end;
procedure TChartDataController.LoadTickDataSeries(const AFileName: string);
begin
ClearAllDataAndCaches();
try
FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName).WaitFor;
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Loading tick records from series: %s',
[AFileName]));
except
on E: Exception do
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Error loading tick data from %s - %s', [AFileName, E.Message]));
end;
end;
end;
// ASelectedTimeframeCaption is the UNIQUE key (e.g. "M1", "Auto", or "Every 20 Ticks")
function TChartDataController.GetOrBuildCache(const ASelectedTimeframeCaption: string;
AAutoAggCandleWidth, AAutoAggCandleSpacing, AAutoAggPaintBoxClientWidth: Integer;
AForceRebuild: Boolean): TOHLC;
var
Index: Integer;
CacheNeedsRebuild: Boolean;
FoundTimeframe: Boolean;
CacheAtIndex: TOHLC;
LGenericBuilder: IGenericCandleBuilder;
CurrentTimeframeSetting: TTimeframeSetting;
begin
Result := nil;
FoundTimeframe := False;
Index := -1;
LGenericBuilder := nil;
for var I := 0 to High(FTimeframeSettingsArray) do
begin
if CompareText(FTimeframeSettingsArray[I].Caption, ASelectedTimeframeCaption) = 0 then
begin
Index := I;
CurrentTimeframeSetting := FTimeframeSettingsArray[I];
FoundTimeframe := True;
Break;
end;
end;
if not FoundTimeframe then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Timeframe setting for caption "%s" not found.', [ASelectedTimeframeCaption]));
Exit;
end;
CacheAtIndex := FOHLCCacheList[Index];
CacheNeedsRebuild := AForceRebuild;
if CacheAtIndex = nil then
begin
CacheAtIndex := TOHLC.Create;
FOHLCCacheList[Index] := CacheAtIndex;
CacheNeedsRebuild := True;
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Cache for [%s] (Desc: "%s") is nil. Creating & Building...',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
end
else if not CacheAtIndex.IsValid then
begin
CacheNeedsRebuild := True;
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Cache for [%s] (Desc: "%s") exists but is invalid. Rebuilding...',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
end
else if CacheAtIndex.AggregationTimeframeSeconds <> CurrentTimeframeSetting.Seconds then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Aggregation timeframe (seconds) mismatch for [%s] (Desc: "%s"). Cache has %ds, requested %ds. Forcing rebuild.',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description, CacheAtIndex.AggregationTimeframeSeconds, CurrentTimeframeSetting.Seconds]));
CacheNeedsRebuild := True;
end;
// If it's a generic callback timeframe, an additional check for builder matching might be needed if not forced.
// The TOHLC's ApproxTimePerCandleText stores "(BuilderCaption)" for generic builds.
// ASelectedTimeframeCaption *is* the builder caption for generic rules.
if (CurrentTimeframeSetting.Seconds = TF_GENERIC_CALLBACK) and (not CacheNeedsRebuild) and Assigned(CacheAtIndex) and CacheAtIndex.IsValid then
begin
// For generic, CurrentTimeframeSetting.Caption is the Builder's caption.
// We check if the existing cache was built with this same builder.
// TOHLC.ApproxTimePerCandleText stores "(BuilderCaption)".
var ExpectedCacheBuilderInfo: string;
ExpectedCacheBuilderInfo := Format('(%s)', [CurrentTimeframeSetting.Caption]); // Caption is the builder's key
if CacheAtIndex.ApproxTimePerCandleText <> ExpectedCacheBuilderInfo then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Generic builder changed for timeframe key [%s] (Desc: "%s"). Cache has info "%s", current builder key implies info "%s". Forcing rebuild.',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description, CacheAtIndex.ApproxTimePerCandleText, ExpectedCacheBuilderInfo]));
CacheNeedsRebuild := True;
end;
end;
if CacheNeedsRebuild then
begin
if HasData then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Building cache for [%s] (Desc: "%s")...',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
if CurrentTimeframeSetting.Seconds = TF_GENERIC_CALLBACK then
begin
// ASelectedTimeframeCaption IS the key for the generic builder in this new design.
LGenericBuilder := GetGenericBuilder(ASelectedTimeframeCaption);
if not Assigned(LGenericBuilder) then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Cannot build cache for generic timeframe [%s] (Desc: "%s"). Builder with this caption not found in dictionary.',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
CacheAtIndex.ClearCache();
Result := CacheAtIndex;
Exit;
end;
end;
CacheAtIndex.Build(
FTickDataArray,
CurrentTimeframeSetting.Seconds,
AAutoAggCandleWidth,
AAutoAggCandleSpacing,
AAutoAggPaintBoxClientWidth,
FMemoLog,
LGenericBuilder // This will be nil if not TF_GENERIC_CALLBACK, or the found builder
);
end
else
begin
if FMemoLog <> nil then
FMemoLog.Add('DataController: No TickData to build cache for ' + CurrentTimeframeSetting.Description);
CacheAtIndex.ClearCache();
end;
end
else
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Using existing valid cache for [%s] (Desc: "%s")',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
end;
Result := CacheAtIndex;
end;
procedure TChartDataController.ClearAllDataAndCaches;
var
I: Integer;
begin
if FMemoLog <> nil then
FMemoLog.Add('DataController: Clearing all data and caches from instance...');
SetLength(FTickDataArray, 0); // Clear the raw tick data
if Assigned(FOHLCCacheList) then
begin
// First, free all TOHLC objects currently in the list
for I := 0 to FOHLCCacheList.Count - 1 do
begin
FreeAndNil(FOHLCCacheList[I]); // Free the TOHLC object
end;
FOHLCCacheList.Clear; // Clears the list, setting its Count to 0.
// Re-populate FOHLCCacheList with nil placeholders,
// one for each setting in FTimeframeSettingsArray.
// This ensures FOHLCCacheList remains parallel to FTimeframeSettingsArray.
if Assigned(FTimeframeSettingsArray) then // FTimeframeSettingsArray should be assigned
begin
for I := 0 to High(FTimeframeSettingsArray) do // Iterate based on the actual array
begin
FOHLCCacheList.Add(nil); // Add a nil placeholder
end;
end;
end;
// Note on FGenericBuilders and FTimeframeSettingsArray:
// This procedure currently does NOT clear registered generic builders from FGenericBuilders
// nor does it remove their corresponding entries from FTimeframeSettingsArray.
// If a full reset including unregistering builders is desired, FGenericBuilders.Clear and
// re-calling InitializeBaseTimeframeSettings (to reset FTimeframeSettingsArray) would be needed here,
// followed by re-initializing FOHLCCacheList.
// The current behavior is that builder registrations and the full list of timeframe definitions persist.
if FMemoLog <> nil then // This is the line (approx. 376) the user reported
FMemoLog.Add('DataController: Instance data and OHLC caches cleared. Registered builders and timeframe definitions (including custom ones) remain.');
end;
function TChartDataController.GetTickDataCount: Integer;
begin
Result := Length(FTickDataArray);
end;
function TChartDataController.GetTickDataArrayPtr: PTTickData;
begin
if Length(FTickDataArray) > 0 then
Result := @FTickDataArray[0]
else
Result := nil;
end;
function TChartDataController.GetTickDataLength: Integer;
begin
Result := Length(FTickDataArray);
end;
function TChartDataController.GetTickDataItem(AIndex: Integer): TTickData;
begin
if (AIndex >= 0) and (AIndex < Length(FTickDataArray)) then
Result := FTickDataArray[AIndex]
else
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Attempted to access invalid tick data index %d. Count is %d.', [AIndex, Length(FTickDataArray)]));
Result := Default(TTickData);
end;
end;
function TChartDataController.HasData: Boolean;
begin
Result := Length(FTickDataArray) > 0;
end;
end.