1st commit
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user