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; FOHLCCacheList: TList; FTimeframeSettingsArray: TArray; // Now includes registered generic builders FMemoLog: TStrings; FGenericBuilders: TDictionary; // 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; 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.Create; InitializeBaseTimeframeSettings; // This populates FTimeframeSettingsArray FOHLCCacheList := TList.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; ts: TTimeframeSetting; begin InitialSettings := TList.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; 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.