436 lines
20 KiB
ObjectPascal
436 lines
20 KiB
ObjectPascal
unit Myc.ChartDataController;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
|
|
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;
|
|
|
|
SetLength(FTickDataArray, 0);
|
|
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);
|
|
if FMemoLog <> nil then
|
|
FMemoLog.Add(Format('DataController: Loaded %d tick records from series: %s',
|
|
[Length(FTickDataArray), 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]));
|
|
SetLength(FTickDataArray, 0);
|
|
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.
|