From d9c9aacff3063acf44f98d423bb9e7131b3298e6 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Thu, 5 Jun 2025 14:37:00 +0200 Subject: [PATCH] Refactoring to new TDataSeries --- ChartDisplay/ChartDisplayMain.dfm | 26 +- ChartDisplay/ChartDisplayMain.pas | 1290 +++++++++++++++++----------- common/Myc.ChartDataController.pas | 23 +- common/Myc.OHLCCache.pas | 99 ++- common/Myc.TickLoader.pas | 454 ---------- 5 files changed, 862 insertions(+), 1030 deletions(-) delete mode 100644 common/Myc.TickLoader.pas diff --git a/ChartDisplay/ChartDisplayMain.dfm b/ChartDisplay/ChartDisplayMain.dfm index 5f75148..2c7c38f 100644 --- a/ChartDisplay/ChartDisplayMain.dfm +++ b/ChartDisplay/ChartDisplayMain.dfm @@ -16,14 +16,14 @@ object ChartForm: TChartForm 900 835) TextHeight = 15 - object FileSelectButton: TSpeedButton - Left = 647 - Top = 17 + object PathSelectButton: TSpeedButton + Left = 423 + Top = 18 Width = 23 - Height = 22 + Height = 23 Anchors = [akTop, akRight] Caption = '...' - OnClick = FileSelectButtonClick + OnClick = PathSelectButtonClick end object MouseHoverDataLabel: TLabel Left = 32 @@ -34,14 +34,14 @@ object ChartForm: TChartForm AutoSize = False ExplicitTop = 359 end - object FileNameEdit: TEdit + object DataPathEdit: TEdit Left = 32 Top = 17 - Width = 609 + Width = 385 Height = 23 Anchors = [akLeft, akTop, akRight] TabOrder = 0 - Text = '\\COFFEE\TickData\Pepperstone\EURUSD_2017_02.tab_zip' + Text = '\\COFFEE\TickData\Pepperstone' end object LoadButton: TButton Left = 686 @@ -116,4 +116,14 @@ object ChartForm: TChartForm ExplicitHeight = 97 end end + object SymbolComboBox: TComboBox + Left = 452 + Top = 18 + Width = 228 + Height = 23 + Anchors = [akTop, akRight] + TabOrder = 7 + Text = 'GER40' + OnChange = SymbolComboBoxChange + end end diff --git a/ChartDisplay/ChartDisplayMain.pas b/ChartDisplay/ChartDisplayMain.pas index a926e44..7389e84 100644 --- a/ChartDisplay/ChartDisplayMain.pas +++ b/ChartDisplay/ChartDisplayMain.pas @@ -3,21 +3,36 @@ unit ChartDisplayMain; // ------------------------------------------------------------------------------ // Unit Name: ChartDisplayMain // Author: Delphi Algo Trading Assistant -// Date: May 22, 2025 +// Date: June 05, 2025 // Updated Date // Purpose: Main form for chart display. Uses TChartDataController for data -// management and OHLC aggregation, including IGenericCandleBuilder. -// Timeframes are selected via unique captions (keys), while user-friendly -// descriptions are shown in the UI. +// management and OHLC aggregation, including IGenericCandleBuilder. +// Data is loaded based on a selected directory and symbol. +// Timeframes are selected via unique captions (keys), while user-friendly +// descriptions are shown in the UI. // ------------------------------------------------------------------------------ interface uses - Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, - Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, - System.Generics.Collections, System.DateUtils, System.Math, System.IOUtils, - System.UITypes, System.Types, - Myc.TickLoader, // For TTickData + Winapi.Windows, + Winapi.Messages, + System.SysUtils, + System.Variants, + System.Classes, + Vcl.Graphics, + Vcl.Controls, + Vcl.Forms, + Vcl.Dialogs, + Vcl.StdCtrls, + Vcl.ExtCtrls, + System.Generics.Collections, + System.DateUtils, + System.Math, + System.IOUtils, + System.UITypes, + System.Types, + Vcl.FileCtrl, // Added for SelectDirectory + Myc.Trade.DataSeries, // For TTickData, FindOldestFilesPerSymbol, TryParseFileName [cite: 231, 237, 238] Myc.OHLCCache, // For TOHLC, IGenericCandleBuilder, TF_ constants Myc.ChartDataController, // Uses the updated DataController Vcl.Buttons; @@ -26,34 +41,32 @@ type // TTimeframeSetting is defined in Myc.ChartDataController.pas // IGenericCandleBuilder is defined in Myc.OHLCCache.pas - TChartForm = class( TForm ) - FileNameEdit: TEdit; + TChartForm = class(TForm) + DataPathEdit: TEdit; // Used to store and display the selected data directory LoadButton: TButton; Memo: TMemo; RefreshButton: TButton; TimeframeComboBox: TComboBox; GoToCurrentButton: TButton; - FileSelectButton: TSpeedButton; + PathSelectButton: TSpeedButton; MouseHoverDataLabel: TLabel; ChartScrollBox: TScrollBox; PaintBox: TPaintBox; - procedure LoadButtonClick( Sender: TObject ); - procedure PaintBoxPaint( Sender: TObject ); - procedure RefreshButtonClick( Sender: TObject ); - procedure FormCreate( Sender: TObject ); - procedure FormDestroy( Sender: TObject ); - procedure FormMouseWheel( Sender: TObject; Shift: TShiftState; - WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean ); - procedure PaintBoxMouseDown( Sender: TObject; Button: TMouseButton; - Shift: TShiftState; X, Y: Integer ); - procedure PaintBoxMouseMove( Sender: TObject; Shift: TShiftState; X, - Y: Integer ); - procedure PaintBoxMouseUp( Sender: TObject; Button: TMouseButton; - Shift: TShiftState; X, Y: Integer ); - procedure TimeframeComboBoxChange( Sender: TObject ); - procedure GoToCurrentButtonClick( Sender: TObject ); - procedure FileSelectButtonClick( Sender: TObject ); - procedure PaintBoxMouseLeave( Sender: TObject ); + SymbolComboBox: TComboBox; // ComboBox for selecting the symbol + procedure LoadButtonClick(Sender: TObject); + procedure PaintBoxPaint(Sender: TObject); + procedure RefreshButtonClick(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); + procedure PaintBoxMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); + procedure PaintBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); + procedure PaintBoxMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); + procedure TimeframeComboBoxChange(Sender: TObject); + procedure GoToCurrentButtonClick(Sender: TObject); + procedure PathSelectButtonClick(Sender: TObject); + procedure PaintBoxMouseLeave(Sender: TObject); + procedure SymbolComboBoxChange(Sender: TObject); // Event handler for symbol selection change private FActiveOHLCCache: TOHLC; FCandleWidthInPixels: Integer; @@ -70,11 +83,13 @@ type FMouseCurrentlyInChartArea: Boolean; FBufferBitmap: TBitmap; FDataController: TChartDataController; + FSymbolFiles: TArray; // Stores full paths from FindOldestFilesPerSymbol procedure PopulateTimeframeComboBox; - procedure EnsureCacheForCurrentTimeframe( ARebuildIfPresent: Boolean = False ); + procedure PopulateSymbolComboBox; + procedure EnsureCacheForCurrentTimeframe(ARebuildIfPresent: Boolean = False); procedure ClearAllCachesInForm; - procedure DrawChart( ACanvas: TCanvas; ARect: TRect ); + procedure DrawChart(ACanvas: TCanvas; ARect: TRect); public end; @@ -85,7 +100,6 @@ implementation {$R *.dfm} - const DefaultChartMargin = 20; DojiPenWidth = 2; @@ -94,52 +108,52 @@ const DefaultEveryNTicksCount = 20; DefaultVolumeAccumulationThreshold = 10; - // --- Generic Candle Builder Implementations --- +// --- Generic Candle Builder Implementations --- type - TEveryNTicksBuilder = class( TInterfacedObject, IGenericCandleBuilder ) + TEveryNTicksBuilder = class(TInterfacedObject, IGenericCandleBuilder) private FTickCountForNewBar: Integer; FDescriptionText: string; // User-friendly description public - constructor Create( ATickCount: Integer ); - function Init( const ADataSet: TArray ): Boolean; - function IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean; + constructor Create(ATickCount: Integer); + function Init(const ADataSet: TArray): Boolean; + function IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; function GetCaption: string; // Returns the Description for UI property Caption: string read GetCaption; // Interface property uses GetCaption end; - TVolumeAccumulationBuilder = class( TInterfacedObject, IGenericCandleBuilder ) + TVolumeAccumulationBuilder = class(TInterfacedObject, IGenericCandleBuilder) private FTargetTickVolumePerBar: Integer; FCurrentTickInBarCount: Integer; FDescriptionText: string; // User-friendly description public - constructor Create( ATargetTickVolume: Integer ); - function Init( const ADataSet: TArray ): Boolean; - function IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean; + constructor Create(ATargetTickVolume: Integer); + function Init(const ADataSet: TArray): Boolean; + function IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; function GetCaption: string; // Returns the Description for UI property Caption: string read GetCaption; // Interface property uses GetCaption end; - { TEveryNTicksBuilder } -constructor TEveryNTicksBuilder.Create( ATickCount: Integer ); +{ TEveryNTicksBuilder } +constructor TEveryNTicksBuilder.Create(ATickCount: Integer); begin inherited Create; FTickCountForNewBar := ATickCount; if FTickCountForNewBar <= 0 then FTickCountForNewBar := 1; // The Caption property of IGenericCandleBuilder is used as the Description when registering - FDescriptionText := Format( 'Every %d Ticks', [FTickCountForNewBar] ); + FDescriptionText := Format('Every %d Ticks', [FTickCountForNewBar]); end; -function TEveryNTicksBuilder.Init( const ADataSet: TArray ): Boolean; +function TEveryNTicksBuilder.Init(const ADataSet: TArray): Boolean; begin Result := True; end; -function TEveryNTicksBuilder.IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean; +function TEveryNTicksBuilder.IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; begin - Result := ( AIndex mod FTickCountForNewBar = 0 ); + Result := (AIndex mod FTickCountForNewBar = 0); end; function TEveryNTicksBuilder.GetCaption: string; @@ -148,25 +162,25 @@ begin end; { TVolumeAccumulationBuilder } -constructor TVolumeAccumulationBuilder.Create( ATargetTickVolume: Integer ); +constructor TVolumeAccumulationBuilder.Create(ATargetTickVolume: Integer); begin inherited Create; FTargetTickVolumePerBar := ATargetTickVolume; if FTargetTickVolumePerBar <= 0 then FTargetTickVolumePerBar := 1; - FDescriptionText := Format( 'Ticks/Bar: %d', [FTargetTickVolumePerBar] ); + FDescriptionText := Format('Ticks/Bar: %d', [FTargetTickVolumePerBar]); FCurrentTickInBarCount := 0; end; -function TVolumeAccumulationBuilder.Init( const ADataSet: TArray ): Boolean; +function TVolumeAccumulationBuilder.Init(const ADataSet: TArray): Boolean; begin FCurrentTickInBarCount := 0; Result := True; end; -function TVolumeAccumulationBuilder.IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean; +function TVolumeAccumulationBuilder.IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; begin - Inc( FCurrentTickInBarCount ); + Inc(FCurrentTickInBarCount); if FCurrentTickInBarCount >= FTargetTickVolumePerBar then begin FCurrentTickInBarCount := 0; @@ -190,34 +204,34 @@ var PreferredDefaultCaptionKey: string; // The short KEY caption for the preferred default begin TimeframeComboBox.Items.Clear; - if not Assigned( FDataController ) then + if not Assigned(FDataController) then begin - Memo.Lines.Add( 'Error: DataController not initialized for PopulateTimeframeComboBox.' ); - Exit; + Memo.Lines.Add('Error: DataController not initialized for PopulateTimeframeComboBox.'); + exit; end; // Get all available timeframes, including base and registered generic ones - FTimeframeSettings := FDataController.GetAvailableTimeframes( ); + FTimeframeSettings := FDataController.GetAvailableTimeframes(); DefaultItemIndex := -1; PreferredDefaultCaptionKey := 'Auto'; // Short key for "Auto (Screen Fit)" - for i := Low( FTimeframeSettings ) to High( FTimeframeSettings ) do + for i := Low(FTimeframeSettings) to High(FTimeframeSettings) do begin // Add the user-friendly Description to the ComboBox - TimeframeComboBox.Items.Add( FTimeframeSettings[i].Description ); + TimeframeComboBox.Items.Add(FTimeframeSettings[i].Description); // Check against the short Caption (key) for default selection - if CompareText( FTimeframeSettings[i].Caption, PreferredDefaultCaptionKey ) = 0 then + if CompareText(FTimeframeSettings[i].Caption, PreferredDefaultCaptionKey) = 0 then DefaultItemIndex := i; end; if TimeframeComboBox.Items.Count > 0 then begin - if ( DefaultItemIndex = -1 ) then + if (DefaultItemIndex = -1) then DefaultItemIndex := 0; // Fallback to the first item TimeframeComboBox.ItemIndex := DefaultItemIndex; - if ( TimeframeComboBox.ItemIndex >= 0 ) and ( TimeframeComboBox.ItemIndex < Length( FTimeframeSettings ) ) then + if (TimeframeComboBox.ItemIndex >= 0) and (TimeframeComboBox.ItemIndex < Length(FTimeframeSettings)) then FSelectedTimeframeSeconds := FTimeframeSettings[TimeframeComboBox.ItemIndex].Seconds else FSelectedTimeframeSeconds := TF_AUTO_AGGREGATION; // Should ideally not happen @@ -225,18 +239,20 @@ begin else begin FSelectedTimeframeSeconds := TF_AUTO_AGGREGATION; // Default if no timeframes - Memo.Lines.Add( 'Warning: No timeframes available to populate ComboBox.' ); + Memo.Lines.Add('Warning: No timeframes available to populate ComboBox.'); end; if TimeframeComboBox.Items.Count > 0 then // Log current selection - Memo.Lines.Add( 'Timeframes populated. Default UI: "' + TimeframeComboBox.Text + - Format( '", Key: "%s", AggSecs: %d', - [FTimeframeSettings[TimeframeComboBox.ItemIndex].Caption, FSelectedTimeframeSeconds] ) ) + Memo.Lines.Add( + 'Timeframes populated. Default UI: "' + + TimeframeComboBox.Text + + Format('", Key: "%s", AggSecs: %d', [FTimeframeSettings[TimeframeComboBox.ItemIndex].Caption, FSelectedTimeframeSeconds]) + ) else - Memo.Lines.Add( 'Timeframes populated, but ComboBox is empty. No default set.' ); + Memo.Lines.Add('Timeframes populated, but ComboBox is empty. No default set.'); end; -procedure TChartForm.FormCreate( Sender: TObject ); +procedure TChartForm.FormCreate(Sender: TObject); var BuilderN: IGenericCandleBuilder; BuilderV: IGenericCandleBuilder; @@ -250,60 +266,64 @@ begin FDragStartX := 0; FDragStartDisplayIndex := 0; - FDataController := TChartDataController.Create( Memo.Lines ); + FDataController := TChartDataController.Create(Memo.Lines); // Create and register generic builders - BuilderN := TEveryNTicksBuilder.Create( DefaultEveryNTicksCount ); - BuilderNCaptionKey := Format( 'NTICKS_%d', [DefaultEveryNTicksCount] ); // e.g., "NTICKS_20" - this is the unique key + BuilderN := TEveryNTicksBuilder.Create(DefaultEveryNTicksCount); + BuilderNCaptionKey := Format('NTICKS_%d', [DefaultEveryNTicksCount]); // e.g., "NTICKS_20" - this is the unique key BuilderNDescription := BuilderN.Caption; // BuilderN.Caption returns "Every 20 Ticks" - FDataController.RegisterGenericBuilder( BuilderNCaptionKey, BuilderNDescription, BuilderN ); + FDataController.RegisterGenericBuilder(BuilderNCaptionKey, BuilderNDescription, BuilderN); - BuilderV := TVolumeAccumulationBuilder.Create( DefaultVolumeAccumulationThreshold ); - BuilderVCaptionKey := Format( 'VOLACC_%d', [DefaultVolumeAccumulationThreshold] ); // e.g., "VOLACC_10" - unique key + BuilderV := TVolumeAccumulationBuilder.Create(DefaultVolumeAccumulationThreshold); + BuilderVCaptionKey := Format('VOLACC_%d', [DefaultVolumeAccumulationThreshold]); // e.g., "VOLACC_10" - unique key BuilderVDescription := BuilderV.Caption; // BuilderV.Caption returns "Ticks/Bar: 10" - FDataController.RegisterGenericBuilder( BuilderVCaptionKey, BuilderVDescription, BuilderV ); + FDataController.RegisterGenericBuilder(BuilderVCaptionKey, BuilderVDescription, BuilderV); PopulateTimeframeComboBox; // Populates from FDataController, now includes generic builders if TimeframeComboBox.ItemIndex <> -1 then - TimeframeComboBoxChange( TimeframeComboBox ) // Trigger initial cache build - else if Length( FTimeframeSettings ) > 0 then + TimeframeComboBoxChange(TimeframeComboBox) // Trigger initial cache build + else if Length(FTimeframeSettings) > 0 then begin // If ComboBox empty but settings exist (e.g. Populate didn't find default) TimeframeComboBox.ItemIndex := 0; - TimeframeComboBoxChange( TimeframeComboBox ); + TimeframeComboBoxChange(TimeframeComboBox); end else begin - Memo.Lines.Add( 'FormCreate: No timeframes to select after population. Ensuring cache for a default (likely empty).' ); - EnsureCacheForCurrentTimeframe( True ); // Attempt to build something + Memo.Lines.Add('FormCreate: No timeframes to select after population. Ensuring cache for a default (likely empty).'); + EnsureCacheForCurrentTimeframe(True); // Attempt to build something end; FActiveOHLCCache := nil; - FCurrentPriceRect := Rect( 0, 0, 0, 0 ); - FCurrentVolumeHistRect := Rect( 0, 0, 0, 0 ); + FCurrentPriceRect := Rect(0, 0, 0, 0); + FCurrentVolumeHistRect := Rect(0, 0, 0, 0); MouseHoverDataLabel.Caption := ''; - FMouseLastChartPos := Point( -1, -1 ); + FMouseLastChartPos := Point(-1, -1); FMouseCurrentlyInChartArea := False; PaintBox.OnMouseLeave := PaintBoxMouseLeave; FBufferBitmap := TBitmap.Create; + SetLength(FSymbolFiles, 0); // Initialize FSymbolFiles array + + // Assuming we have a valid default value in the Path-Edit: + PopulateSymbolComboBox; end; -procedure TChartForm.FormDestroy( Sender: TObject ); +procedure TChartForm.FormDestroy(Sender: TObject); begin - FreeAndNil( FDataController ); - FreeAndNil( FBufferBitmap ); + FreeAndNil(FDataController); + FreeAndNil(FBufferBitmap); FActiveOHLCCache := nil; end; procedure TChartForm.ClearAllCachesInForm; begin - Memo.Lines.Add( 'TChartForm: Clearing all caches via DataController...' ); - if Assigned( FDataController ) then - FDataController.ClearAllDataAndCaches( ); + Memo.Lines.Add('TChartForm: Clearing all caches via DataController...'); + if Assigned(FDataController) then + FDataController.ClearAllDataAndCaches(); FActiveOHLCCache := nil; end; -procedure TChartForm.EnsureCacheForCurrentTimeframe( ARebuildIfPresent: Boolean = False ); +procedure TChartForm.EnsureCacheForCurrentTimeframe(ARebuildIfPresent: Boolean = False); var Index: Integer; EffectivePaintBoxClientWidthForAuto: Integer; @@ -311,124 +331,114 @@ var SelectedTFDescription: string; // The UI description (e.g., "1 Minute (M1)") begin Index := TimeframeComboBox.ItemIndex; - if ( Index < Low( FTimeframeSettings ) ) or ( Index > High( FTimeframeSettings ) ) then + if (Index < Low(FTimeframeSettings)) or (Index > High(FTimeframeSettings)) then begin - Memo.Lines.Add( 'EnsureCache: Invalid TF ComboBox Index: ' + IntToStr( Index ) ); + Memo.Lines.Add('EnsureCache: Invalid TF ComboBox Index: ' + IntToStr(Index)); FActiveOHLCCache := nil; PaintBox.Invalidate; - Exit; + exit; end; - if not Assigned( FDataController ) then + if not Assigned(FDataController) then begin - Memo.Lines.Add( 'EnsureCache: FDataController not assigned.' ); + Memo.Lines.Add('EnsureCache: FDataController not assigned.'); FActiveOHLCCache := nil; PaintBox.Invalidate; - Exit; + exit; end; SelectedTFKeyCaption := FTimeframeSettings[Index].Caption; // This is the short key SelectedTFDescription := FTimeframeSettings[Index].Description; // This is for UI/logging FSelectedTimeframeSeconds := FTimeframeSettings[Index].Seconds; // Still useful for some logic - EffectivePaintBoxClientWidthForAuto := PaintBox.ClientWidth - ( 2 * DefaultChartMargin ); + EffectivePaintBoxClientWidthForAuto := PaintBox.ClientWidth - (2 * DefaultChartMargin); if EffectivePaintBoxClientWidthForAuto < 0 then EffectivePaintBoxClientWidthForAuto := 0; // Call GetOrBuildCache using the selected timeframe's short KEY CAPTION. // The ASelectedGenericBuilderCaption parameter was removed from GetOrBuildCache. - FActiveOHLCCache := FDataController.GetOrBuildCache( - SelectedTFKeyCaption, - FCandleWidthInPixels, - FCandleSpacing, - EffectivePaintBoxClientWidthForAuto, - ARebuildIfPresent - ); + FActiveOHLCCache := + FDataController.GetOrBuildCache( + SelectedTFKeyCaption, + FCandleWidthInPixels, + FCandleSpacing, + EffectivePaintBoxClientWidthForAuto, + ARebuildIfPresent + ); - if Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid then - Memo.Lines.Add( Format( 'Active Cache UI: "%s" (Key: %s). Candles: %d. AggSecs: %d. ApproxTime/Candle: %s', - [SelectedTFDescription, SelectedTFKeyCaption, FActiveOHLCCache.Count, - FActiveOHLCCache.AggregationTimeframeSeconds, FActiveOHLCCache.ApproxTimePerCandleText] ) ) + if Assigned(FActiveOHLCCache) and FActiveOHLCCache.IsValid then + Memo.Lines.Add( + Format( + 'Active Cache UI: "%s" (Key: %s). Candles: %d. AggSecs: %d. ApproxTime/Candle: %s', + [ + SelectedTFDescription, + SelectedTFKeyCaption, + FActiveOHLCCache.Count, + FActiveOHLCCache.AggregationTimeframeSeconds, + FActiveOHLCCache.ApproxTimePerCandleText + ] + ) + ) else - Memo.Lines.Add( Format( 'Active Cache for UI: "%s" (Key: %s) not valid or empty after EnsureCache.', - [SelectedTFDescription, SelectedTFKeyCaption] ) ); + Memo.Lines.Add( + Format( + 'Active Cache for UI: "%s" (Key: %s) not valid or empty after EnsureCache.', + [SelectedTFDescription, SelectedTFKeyCaption] + ) + ); end; -procedure TChartForm.TimeframeComboBoxChange( Sender: TObject ); +procedure TChartForm.TimeframeComboBoxChange(Sender: TObject); var CurrentTFKeyCaption: string; begin if TimeframeComboBox.ItemIndex < 0 then begin - Memo.Lines.Add( 'TimeframeComboBoxChange: Invalid ItemIndex (-1).' ); - Exit; + Memo.Lines.Add('TimeframeComboBoxChange: Invalid ItemIndex (-1).'); + exit; end; - if ( TimeframeComboBox.ItemIndex >= Low( FTimeframeSettings ) ) and - ( TimeframeComboBox.ItemIndex <= High( FTimeframeSettings ) ) then + if (TimeframeComboBox.ItemIndex >= Low(FTimeframeSettings)) and (TimeframeComboBox.ItemIndex <= High(FTimeframeSettings)) then begin FSelectedTimeframeSeconds := FTimeframeSettings[TimeframeComboBox.ItemIndex].Seconds; CurrentTFKeyCaption := FTimeframeSettings[TimeframeComboBox.ItemIndex].Caption; - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + - Format( ' - TF Change Event: UI shows "%s" (Key: %s, Resolved AggSecs: %d)', - [TimeframeComboBox.Text, // This is FTimeframeSettings[Index].Description - CurrentTFKeyCaption, - FSelectedTimeframeSeconds] ) ); + Memo.Lines.Add( + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + + Format( + ' - TF Change Event: UI shows "%s" (Key: %s, Resolved AggSecs: %d)', + [ + TimeframeComboBox.Text, // This is FTimeframeSettings[Index].Description + CurrentTFKeyCaption, + FSelectedTimeframeSeconds + ]) + ); end else begin - Memo.Lines.Add( Format( 'Error: Invalid ItemIndex %d in TimeframeComboBoxChange. FTimeframeSettings valid range %d to %d.', - [TimeframeComboBox.ItemIndex, Low( FTimeframeSettings ), High( FTimeframeSettings )] ) ); - if Length( FTimeframeSettings ) > 0 then + Memo.Lines.Add( + Format( + 'Error: Invalid ItemIndex %d in TimeframeComboBoxChange. FTimeframeSettings valid range %d to %d.', + [TimeframeComboBox.ItemIndex, Low(FTimeframeSettings), High(FTimeframeSettings)] + ) + ); + if Length(FTimeframeSettings) > 0 then FSelectedTimeframeSeconds := FTimeframeSettings[0].Seconds else FSelectedTimeframeSeconds := TF_AUTO_AGGREGATION; end; FDisplayStartIndex := 0; - EnsureCacheForCurrentTimeframe( True ); + EnsureCacheForCurrentTimeframe(True); PaintBox.Invalidate; end; -procedure TChartForm.LoadButtonClick( Sender: TObject ); +procedure TChartForm.LoadButtonClick(Sender: TObject); begin - Memo.Clear; - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - Starting data load...' ); - ClearAllCachesInForm( ); - FDisplayStartIndex := 0; - - if Trim( FileNameEdit.Text ) = '' then - begin - Memo.Lines.Add( 'Error: File name cannot be empty.' ); - MessageDlg( 'Please enter a file name.', mtWarning, [mbOK], 0 ); - PaintBox.Invalidate; - Exit; - end; - Memo.Lines.Add( 'Loading data from file: ' + FileNameEdit.Text ); - - if Assigned( FDataController ) then - begin - FDataController.LoadTickDataSeries( FileNameEdit.Text ); - if FDataController.HasData then - begin - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + Format( ' - Successfully loaded %d ticks by controller.', - [FDataController.GetTickDataCount] ) ); - EnsureCacheForCurrentTimeframe( True ); - end - else - begin - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + - ' - No tick data was loaded by controller, or an error occurred.' ); - FActiveOHLCCache := nil; - end; - end - else - Memo.Lines.Add( 'Error: DataController is not initialized. Cannot load data.' ); - - PaintBox.Invalidate; + // This button now triggers the loading based on the SymbolComboBox selection + SymbolComboBoxChange(SymbolComboBox); end; -procedure TChartForm.RefreshButtonClick( Sender: TObject ); +procedure TChartForm.RefreshButtonClick(Sender: TObject); var TargetStartTickIndex: Integer; VisibleStartIndexInOldCache: Integer; @@ -436,17 +446,20 @@ var ChartDrawWidth, OldDisplaySlotWidth, NumCandlesDrawableOld: Integer; NewOptimalStartIndex, i: Integer; begin - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - Refresh Button clicked...' ); + Memo.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - Refresh Button clicked...'); TargetStartTickIndex := -1; - if Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) and - Assigned( FDataController ) and FDataController.HasData then + if Assigned(FActiveOHLCCache) + and FActiveOHLCCache.IsValid + and (FActiveOHLCCache.Count > 0) + and Assigned(FDataController) + and FDataController.HasData then begin VisibleStartIndexInOldCache := FDisplayStartIndex; if FCurrentPriceRect.Width > 0 then ChartDrawWidth := FCurrentPriceRect.Width else - ChartDrawWidth := PaintBox.ClientWidth - ( 2 * DefaultChartMargin ); + ChartDrawWidth := PaintBox.ClientWidth - (2 * DefaultChartMargin); OldDisplaySlotWidth := FCandleWidthInPixels + FCandleSpacing; NumCandlesDrawableOld := 0; @@ -454,50 +467,57 @@ begin NumCandlesDrawableOld := ChartDrawWidth div OldDisplaySlotWidth; ActualNumVisibleCandlesOld := 0; - if ( VisibleStartIndexInOldCache >= 0 ) and ( VisibleStartIndexInOldCache < FActiveOHLCCache.Count ) then - ActualNumVisibleCandlesOld := Min( NumCandlesDrawableOld, FActiveOHLCCache.Count - VisibleStartIndexInOldCache ); + if (VisibleStartIndexInOldCache >= 0) and (VisibleStartIndexInOldCache < FActiveOHLCCache.Count) then + ActualNumVisibleCandlesOld := Min(NumCandlesDrawableOld, FActiveOHLCCache.Count - VisibleStartIndexInOldCache); if ActualNumVisibleCandlesOld > 0 then begin TargetStartTickIndex := FActiveOHLCCache.Candles[VisibleStartIndexInOldCache].OriginalDataIndexStart; - Memo.Lines.Add( Format( 'Refresh S1: Old Cache Index %d maps to Original Tick Index: %d', - [VisibleStartIndexInOldCache, TargetStartTickIndex] ) ); + Memo.Lines.Add( + Format( + 'Refresh S1: Old Cache Index %d maps to Original Tick Index: %d', + [VisibleStartIndexInOldCache, TargetStartTickIndex] + ) + ); end else begin - Memo.Lines.Add( 'Refresh S1: No candles visible in old cache or invalid start index. Defaulting to Tick Index 0.' ); + Memo.Lines.Add('Refresh S1: No candles visible in old cache or invalid start index. Defaulting to Tick Index 0.'); TargetStartTickIndex := 0; end; end else begin - Memo.Lines.Add( 'Refresh S1: No active cache or no raw data. Defaulting Target Tick Index to 0.' ); + Memo.Lines.Add('Refresh S1: No active cache or no raw data. Defaulting Target Tick Index to 0.'); TargetStartTickIndex := 0; end; - if Assigned( FDataController ) and FDataController.HasData then + if Assigned(FDataController) and FDataController.HasData then begin - TargetStartTickIndex := Max( 0, TargetStartTickIndex ); - if FDataController.GetTickDataLength > 0 then - TargetStartTickIndex := Min( TargetStartTickIndex, FDataController.GetTickDataLength - 1 ) + TargetStartTickIndex := Max(0, TargetStartTickIndex); + if FDataController.GetTickDataLength > 0 then // Use GetTickDataLength to get the count of ticks from the controller + TargetStartTickIndex := Min(TargetStartTickIndex, FDataController.GetTickDataLength - 1) else TargetStartTickIndex := 0; end else - TargetStartTickIndex := -1; + TargetStartTickIndex := -1; // Or handle as appropriate if no data - Memo.Lines.Add( Format( 'Refresh S1a: Final Target Original Tick Index: %d', [TargetStartTickIndex] ) ); + Memo.Lines.Add(Format('Refresh S1a: Final Target Original Tick Index: %d', [TargetStartTickIndex])); FCandleWidthInPixels := InitialCandleWidth; FCandleSpacing := InitialCandleSpacing; - Memo.Lines.Add( Format( 'Refresh S2: Visuals (Candle Width/Spacing) reset to W:%dpx, S:%dpx.', - [FCandleWidthInPixels, FCandleSpacing] ) ); + Memo.Lines.Add(Format('Refresh S2: Visuals (Candle Width/Spacing) reset to W:%dpx, S:%dpx.', [FCandleWidthInPixels, FCandleSpacing])); - EnsureCacheForCurrentTimeframe( True ); + EnsureCacheForCurrentTimeframe(True); NewOptimalStartIndex := 0; - if Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) and - ( TargetStartTickIndex >= 0 ) and Assigned( FDataController ) and FDataController.HasData then + if Assigned(FActiveOHLCCache) + and FActiveOHLCCache.IsValid + and (FActiveOHLCCache.Count > 0) + and (TargetStartTickIndex >= 0) // Ensure TargetStartTickIndex is valid + and Assigned(FDataController) // Ensure FDataController is assigned + and FDataController.HasData then // Ensure FDataController has data begin for i := 0 to FActiveOHLCCache.Count - 1 do begin @@ -506,94 +526,67 @@ begin NewOptimalStartIndex := i; Break; end - else if FActiveOHLCCache.Candles[i].OriginalDataIndexEnd >= TargetStartTickIndex then + else if FActiveOHLCCache.Candles[i].OriginalDataIndexEnd >= TargetStartTickIndex then // Check end index as well begin NewOptimalStartIndex := i; Break; end; - if i = FActiveOHLCCache.Count - 1 then + if i = FActiveOHLCCache.Count - 1 then // If no candle starts at or after, select the last one NewOptimalStartIndex := i; end; - Memo.Lines.Add( Format( 'Refresh S4a: New Optimal Start Index in new cache = %d (for Original Tick Index %d).', - [NewOptimalStartIndex, TargetStartTickIndex] ) ); + Memo.Lines.Add( + Format( + 'Refresh S4a: New Optimal Start Index in new cache = %d (for Original Tick Index %d).', + [NewOptimalStartIndex, TargetStartTickIndex] + ) + ); FDisplayStartIndex := NewOptimalStartIndex; - if FActiveOHLCCache.Count > 0 then + if FActiveOHLCCache.Count > 0 then // Clamp FDisplayStartIndex begin - FDisplayStartIndex := Max( 0, FDisplayStartIndex ); - FDisplayStartIndex := Min( FDisplayStartIndex, FActiveOHLCCache.Count - 1 ); + FDisplayStartIndex := Max(0, FDisplayStartIndex); + FDisplayStartIndex := Min(FDisplayStartIndex, FActiveOHLCCache.Count - 1); end else - FDisplayStartIndex := 0; - Memo.Lines.Add( Format( 'Refresh S4b: Final FDisplayStartIndex: %d (New Cache Candle Count: %d).', - [FDisplayStartIndex, FActiveOHLCCache.Count] ) ); + FDisplayStartIndex := 0; // Reset if cache is empty + + Memo.Lines.Add( + Format('Refresh S4b: Final FDisplayStartIndex: %d (New Cache Candle Count: %d).', [FDisplayStartIndex, FActiveOHLCCache.Count]) + ); end else begin - FDisplayStartIndex := 0; - Memo.Lines.Add( 'Refresh S4c: New cache invalid/empty, or no target tick. FDisplayStartIndex reset to 0.' ); + FDisplayStartIndex := 0; // Reset FDisplayStartIndex if no valid cache or data + Memo.Lines.Add('Refresh S4c: New cache invalid/empty, or no target tick. FDisplayStartIndex reset to 0.'); end; PaintBox.Invalidate; end; -procedure TChartForm.FileSelectButtonClick( Sender: TObject ); +procedure TChartForm.PathSelectButtonClick(Sender: TObject); var - OpenDialog: TOpenDialog; - InitialDirectory: string; - SelectedPath, SelectedSymbol: string; - SelectedYear: Integer; - SelectedMonth: Integer; - IsValidSyntax: Boolean; + selectedDirectory: string; + fileName: string; + pathValue, symbolValue: string; + yearValue, monthValue: Integer; + i: Integer; begin - OpenDialog := TOpenDialog.Create( Self ); - try - OpenDialog.Title := 'Open Tick Data File'; - OpenDialog.Filter := 'Tick Data Files (*.tab)|*.tab;*.tab_zip;*.tab_live|All Files (*.*)|*.*'; - OpenDialog.DefaultExt := 'tab'; - OpenDialog.Options := [ofFileMustExist, ofPathMustExist, ofHideReadOnly, ofEnableSizing]; - - if Trim( FileNameEdit.Text ) <> '' then - begin - InitialDirectory := System.IOUtils.TPath.GetDirectoryName( FileNameEdit.Text ); - if ( InitialDirectory <> '' ) and System.IOUtils.TDirectory.Exists( InitialDirectory ) then - begin - OpenDialog.InitialDir := InitialDirectory; - Memo.Lines.Add( 'FSButton: OpenDialog InitialDir set to: ' + InitialDirectory ); - end - else if InitialDirectory <> '' then - Memo.Lines.Add( 'FSButton: Directory from FileNameEdit not found: ' + InitialDirectory ) - else - Memo.Lines.Add( 'FSButton: No directory information in FileNameEdit.' ); - end - else - Memo.Lines.Add( 'FSButton: FileNameEdit is empty, using default InitialDir.' ); - - if OpenDialog.Execute then - begin - FileNameEdit.Text := OpenDialog.FileName; - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - File selected via dialog: ' + OpenDialog.FileName ); - - IsValidSyntax := Myc.TickLoader.TryParseFileName( OpenDialog.FileName, - SelectedPath, SelectedSymbol, SelectedYear, SelectedMonth ); - - if not IsValidSyntax then - begin - MessageDlg( Format( 'Warning: The selected filename ''%s'' does not match the expected format (Prefix_Symbol_Year_Month.tab).', - [System.IOUtils.TPath.GetFileName( OpenDialog.FileName )] ), mtWarning, [mbOK], 0 ); - Memo.Lines.Add( 'Warning: Incorrect filename syntax chosen by user.' ); - end; - - LoadButtonClick( Self ); - end - else - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - File selection dialog cancelled by user.' ); - finally - OpenDialog.Free; + selectedDirectory := DataPathEdit.Text; // Start with current path, if any + // Use Vcl.FileCtrl.SelectDirectory function for directory selection. + // The third parameter (selectedDirectory) is an "out" parameter that receives the selected path. + if SelectDirectory('Select Data Directory', '', selectedDirectory) then + begin + DataPathEdit.Text := selectedDirectory; // Update the edit box with the selected directory + Memo.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - Data directory selected: ' + selectedDirectory); + PopulateSymbolComboBox; + end + else + begin + Memo.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - Directory selection cancelled by user.'); end; end; -procedure TChartForm.PaintBoxMouseLeave( Sender: TObject ); +procedure TChartForm.PaintBoxMouseLeave(Sender: TObject); begin if FMouseCurrentlyInChartArea then begin @@ -601,36 +594,40 @@ begin MouseHoverDataLabel.Caption := ''; PaintBox.Invalidate; end; - FMouseLastChartPos := Point( -1, -1 ); + FMouseLastChartPos := Point(-1, -1); end; -procedure TChartForm.PaintBoxMouseDown( Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer ); +procedure TChartForm.PaintBoxMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var IsOverPrice, IsOverVolume: Boolean; begin if Button = mbLeft then begin - FMouseLastChartPos := Point( X, Y ); + FMouseLastChartPos := Point(X, Y); - IsOverPrice := ( FCurrentPriceRect.Width > 0 ) and PtInRect( FCurrentPriceRect, FMouseLastChartPos ); - IsOverVolume := ( FCurrentVolumeHistRect.Width > 0 ) and ( FCurrentVolumeHistRect.Height > 0 ) and - PtInRect( FCurrentVolumeHistRect, FMouseLastChartPos ); + IsOverPrice := (FCurrentPriceRect.Width > 0) and PtInRect(FCurrentPriceRect, FMouseLastChartPos); + IsOverVolume := + (FCurrentVolumeHistRect.Width > 0) + and (FCurrentVolumeHistRect.Height > 0) + and PtInRect(FCurrentVolumeHistRect, FMouseLastChartPos); FMouseCurrentlyInChartArea := IsOverPrice or IsOverVolume; - if FMouseCurrentlyInChartArea and Assigned( FActiveOHLCCache ) and - FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) then + if FMouseCurrentlyInChartArea and Assigned(FActiveOHLCCache) and FActiveOHLCCache.IsValid and (FActiveOHLCCache.Count > 0) then begin FIsDragging := True; FDragStartX := X; FDragStartDisplayIndex := FDisplayStartIndex; PaintBox.Cursor := crSizeWE; - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + - Format( ' - Drag Start: MouseX=%d, StartDisplayIdx=%d', [X, FDisplayStartIndex] ) ); + Memo.Lines.Add( + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - Drag Start: MouseX=%d, StartDisplayIdx=%d', [X, FDisplayStartIndex]) + ); end - else if Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) then + else if Assigned(FActiveOHLCCache) and FActiveOHLCCache.IsValid and (FActiveOHLCCache.Count > 0) then begin - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + - Format( ' - MouseDown at X=%d, Y=%d (outside chart drawing areas). No drag initiated.', [X, Y] ) ); + Memo.Lines.Add( + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + + Format(' - MouseDown at X=%d, Y=%d (outside chart drawing areas). No drag initiated.', [X, Y]) + ); end; if FMouseCurrentlyInChartArea then @@ -638,18 +635,26 @@ begin end; end; -procedure TChartForm.PaintBoxMouseMove( Sender: TObject; Shift: TShiftState; X, Y: Integer ); +procedure TChartForm.PaintBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var - DeltaX, NumCandlesToScroll, PrevDisplayStartIndex, CurrentFixedSlotWidth, - MaxPossibleStartIndex, ChartDrawWidthForDrag, DrawableCandlesInViewPort, - RelativeX, CandleIndexInViewPort, ActualCacheIndex, CurrentDrawingSlotWidthForHover: Integer; + DeltaX, + NumCandlesToScroll, + PrevDisplayStartIndex, + CurrentFixedSlotWidth, + MaxPossibleStartIndex, + ChartDrawWidthForDrag, + DrawableCandlesInViewPort, + RelativeX, + CandleIndexInViewPort, + ActualCacheIndex, + CurrentDrawingSlotWidthForHover: Integer; HoveredCandle: TCachedCandle; UTCTimeStart, UTCTimeEnd, LocalTimeStart, LocalTimeEnd: TDateTime; StartTimeStr, EndTimeStr, TimeZoneStr, HoverInfo: string; IsOverPrice, IsOverVolume, NeedsInvalidate, OldMouseCurrentlyInChartArea: Boolean; begin OldMouseCurrentlyInChartArea := FMouseCurrentlyInChartArea; - FMouseLastChartPos := Point( X, Y ); + FMouseLastChartPos := Point(X, Y); if FIsDragging then begin @@ -658,109 +663,130 @@ begin if CurrentFixedSlotWidth > 0 then begin - NumCandlesToScroll := Round( -DeltaX / CurrentFixedSlotWidth ); + NumCandlesToScroll := Round(-DeltaX / CurrentFixedSlotWidth); PrevDisplayStartIndex := FDisplayStartIndex; FDisplayStartIndex := FDragStartDisplayIndex + NumCandlesToScroll; if FCurrentPriceRect.Width > 0 then ChartDrawWidthForDrag := FCurrentPriceRect.Width else - ChartDrawWidthForDrag := PaintBox.ClientWidth - ( 2 * DefaultChartMargin ); + ChartDrawWidthForDrag := PaintBox.ClientWidth - (2 * DefaultChartMargin); - DrawableCandlesInViewPort := Max( 1, ChartDrawWidthForDrag div CurrentFixedSlotWidth ); + DrawableCandlesInViewPort := Max(1, ChartDrawWidthForDrag div CurrentFixedSlotWidth); - if Assigned( FActiveOHLCCache ) and ( FActiveOHLCCache.Count > 0 ) then + if Assigned(FActiveOHLCCache) and (FActiveOHLCCache.Count > 0) then begin MaxPossibleStartIndex := FActiveOHLCCache.Count - DrawableCandlesInViewPort; if MaxPossibleStartIndex < 0 then MaxPossibleStartIndex := 0; - FDisplayStartIndex := Max( 0, FDisplayStartIndex ); - FDisplayStartIndex := Min( FDisplayStartIndex, MaxPossibleStartIndex ); + FDisplayStartIndex := Max(0, FDisplayStartIndex); + FDisplayStartIndex := Min(FDisplayStartIndex, MaxPossibleStartIndex); end else FDisplayStartIndex := 0; if FDisplayStartIndex <> PrevDisplayStartIndex then begin - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + - Format( ' - Dragging: DeltaX=%d, NumScroll=%d, NewDisplayIdx=%d', - [DeltaX, NumCandlesToScroll, FDisplayStartIndex] ) ); + Memo.Lines.Add( + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + + Format(' - Dragging: DeltaX=%d, NumScroll=%d, NewDisplayIdx=%d', [DeltaX, NumCandlesToScroll, FDisplayStartIndex]) + ); PaintBox.Invalidate; end; end; - IsOverPrice := ( FCurrentPriceRect.Width > 0 ) and PtInRect( FCurrentPriceRect, FMouseLastChartPos ); - IsOverVolume := ( FCurrentVolumeHistRect.Width > 0 ) and PtInRect( FCurrentVolumeHistRect, FMouseLastChartPos ); + IsOverPrice := (FCurrentPriceRect.Width > 0) and PtInRect(FCurrentPriceRect, FMouseLastChartPos); + IsOverVolume := (FCurrentVolumeHistRect.Width > 0) and PtInRect(FCurrentVolumeHistRect, FMouseLastChartPos); FMouseCurrentlyInChartArea := IsOverPrice or IsOverVolume; if FMouseCurrentlyInChartArea <> OldMouseCurrentlyInChartArea then PaintBox.Invalidate; - MouseHoverDataLabel.Caption := ''; - Exit; + MouseHoverDataLabel.Caption := ''; // Clear hover label while dragging + exit; end; NeedsInvalidate := False; - IsOverPrice := ( FCurrentPriceRect.Width > 0 ) and PtInRect( FCurrentPriceRect, FMouseLastChartPos ); - IsOverVolume := ( FCurrentVolumeHistRect.Width > 0 ) and PtInRect( FCurrentVolumeHistRect, FMouseLastChartPos ); + IsOverPrice := (FCurrentPriceRect.Width > 0) and PtInRect(FCurrentPriceRect, FMouseLastChartPos); + IsOverVolume := (FCurrentVolumeHistRect.Width > 0) and PtInRect(FCurrentVolumeHistRect, FMouseLastChartPos); FMouseCurrentlyInChartArea := IsOverPrice or IsOverVolume; if FMouseCurrentlyInChartArea <> OldMouseCurrentlyInChartArea then NeedsInvalidate := True; + // Always invalidate if mouse is over chart area to draw crosshair or update hover info if FMouseCurrentlyInChartArea then NeedsInvalidate := True; - if FMouseCurrentlyInChartArea and Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and - ( FActiveOHLCCache.Count > 0 ) and Assigned( FDataController ) and FDataController.HasData then + if FMouseCurrentlyInChartArea + and Assigned(FActiveOHLCCache) + and FActiveOHLCCache.IsValid + and (FActiveOHLCCache.Count > 0) + and Assigned(FDataController) + and FDataController.HasData then begin CurrentDrawingSlotWidthForHover := FCandleWidthInPixels + FCandleSpacing; if CurrentDrawingSlotWidthForHover > 0 then begin - RelativeX := FMouseLastChartPos.X - FCurrentPriceRect.Left; + RelativeX := FMouseLastChartPos.X - FCurrentPriceRect.Left; // Relative to price chart area CandleIndexInViewPort := RelativeX div CurrentDrawingSlotWidthForHover; ActualCacheIndex := FDisplayStartIndex + CandleIndexInViewPort; - if ( ActualCacheIndex >= 0 ) and ( ActualCacheIndex < FActiveOHLCCache.Count ) then + if (ActualCacheIndex >= 0) and (ActualCacheIndex < FActiveOHLCCache.Count) then begin HoveredCandle := FActiveOHLCCache.Candles[ActualCacheIndex]; - if ( HoveredCandle.OriginalDataIndexStart >= 0 ) and - ( HoveredCandle.OriginalDataIndexStart < FDataController.GetTickDataLength ) and - ( HoveredCandle.OriginalDataIndexEnd >= 0 ) and - ( HoveredCandle.OriginalDataIndexEnd < FDataController.GetTickDataLength ) then + if (HoveredCandle.OriginalDataIndexStart >= 0) + and (HoveredCandle.OriginalDataIndexStart < FDataController.GetTickDataLength) // Use GetTickDataLength + and (HoveredCandle.OriginalDataIndexEnd >= 0) + and (HoveredCandle.OriginalDataIndexEnd < FDataController.GetTickDataLength) then // Use GetTickDataLength begin - UTCTimeStart := FDataController.GetTickDataItem( HoveredCandle.OriginalDataIndexStart ).OADateTime; - UTCTimeEnd := FDataController.GetTickDataItem( HoveredCandle.OriginalDataIndexEnd ).OADateTime; - LocalTimeStart := TTimeZone.Local.ToLocalTime( UTCTimeStart ); - LocalTimeEnd := TTimeZone.Local.ToLocalTime( UTCTimeEnd ); - TimeZoneStr := TTimeZone.Local.GetAbbreviation( LocalTimeStart ); - StartTimeStr := FormatDateTime( 'yy-mm-dd hh:nn:ss', LocalTimeStart ); - if System.DateUtils.DateOf( LocalTimeStart ) <> System.DateUtils.DateOf( LocalTimeEnd ) then - EndTimeStr := FormatDateTime( 'yy-mm-dd hh:nn:ss', LocalTimeEnd ) + UTCTimeStart := FDataController.GetTickDataItem(HoveredCandle.OriginalDataIndexStart).OADateTime; + UTCTimeEnd := FDataController.GetTickDataItem(HoveredCandle.OriginalDataIndexEnd).OADateTime; + LocalTimeStart := TTimeZone.Local.ToLocalTime(UTCTimeStart); + LocalTimeEnd := TTimeZone.Local.ToLocalTime(UTCTimeEnd); + TimeZoneStr := TTimeZone.Local.GetAbbreviation(LocalTimeStart); // Get abbreviation for the local time zone + StartTimeStr := FormatDateTime('yy-mm-dd hh:nn:ss', LocalTimeStart); + if System.DateUtils.DateOf(LocalTimeStart) <> System.DateUtils.DateOf(LocalTimeEnd) then + EndTimeStr := FormatDateTime('yy-mm-dd hh:nn:ss', LocalTimeEnd) else - EndTimeStr := FormatDateTime( 'hh:nn:ss', LocalTimeEnd ); - HoverInfo := Format( 'S: %s E: %s (%s) O:%.5f H:%.5f L:%.5f C:%.5f Vol:%d', - [StartTimeStr, EndTimeStr, TimeZoneStr, HoveredCandle.OpenPrice, - HoveredCandle.HighPrice, HoveredCandle.LowPrice, HoveredCandle.ClosePrice, - HoveredCandle.TickVolume] ); + EndTimeStr := FormatDateTime('hh:nn:ss', LocalTimeEnd); + + HoverInfo := + Format( + 'S: %s E: %s (%s) O:%.5f H:%.5f L:%.5f C:%.5f Vol:%d', + [ + StartTimeStr, + EndTimeStr, + TimeZoneStr, + HoveredCandle.OpenPrice, + HoveredCandle.HighPrice, + HoveredCandle.LowPrice, + HoveredCandle.ClosePrice, + HoveredCandle.TickVolume + ] + ); MouseHoverDataLabel.Caption := HoverInfo; end else MouseHoverDataLabel.Caption := 'Candle (Original Tick Index Error)'; end else - MouseHoverDataLabel.Caption := ''; + MouseHoverDataLabel.Caption := ''; // Mouse is outside valid candle range end else - MouseHoverDataLabel.Caption := ''; + MouseHoverDataLabel.Caption := ''; // Slot width is zero, cannot determine candle end else - MouseHoverDataLabel.Caption := ''; + begin + MouseHoverDataLabel.Caption := ''; // Not over chart, no cache, or no data + if not FMouseCurrentlyInChartArea and OldMouseCurrentlyInChartArea then // If mouse just left the chart area + NeedsInvalidate := True; // Ensure crosshair is cleared + end; if NeedsInvalidate then PaintBox.Invalidate; end; -procedure TChartForm.PaintBoxMouseUp( Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer ); +procedure TChartForm.PaintBoxMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin @@ -768,40 +794,115 @@ begin begin FIsDragging := False; PaintBox.Cursor := crDefault; - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + - ' - Drag End. Final DisplayIdx=' + IntToStr( FDisplayStartIndex ) ); - FMouseLastChartPos := Point( X, Y ); - PaintBoxMouseMove( Sender, Shift, X, Y ); + Memo.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - Drag End. Final DisplayIdx=' + IntToStr(FDisplayStartIndex)); + FMouseLastChartPos := Point(X, Y); // Update last mouse position + PaintBoxMouseMove(Sender, Shift, X, Y); // Trigger MouseMove to update hover info and crosshair based on final position end; end; end; -procedure TChartForm.PaintBoxPaint( Sender: TObject ); +procedure TChartForm.PaintBoxPaint(Sender: TObject); begin - if not Assigned( FBufferBitmap ) then + if not Assigned(FBufferBitmap) then begin - Memo.Lines.Add( 'Error: FBufferBitmap not assigned in PaintBoxPaint.' ); - Exit; + Memo.Lines.Add('Error: FBufferBitmap not assigned in PaintBoxPaint.'); + exit; end; - if ( PaintBox.ClientWidth <= 0 ) or ( PaintBox.ClientHeight <= 0 ) then + if (PaintBox.ClientWidth <= 0) or (PaintBox.ClientHeight <= 0) then begin - if Assigned( PaintBox.Canvas ) then - PaintBox.Canvas.FillRect( PaintBox.ClientRect ); - Exit; + if Assigned(PaintBox.Canvas) then // Ensure canvas is assigned before using + PaintBox.Canvas.FillRect(PaintBox.ClientRect); // Clear with default color if size is invalid + exit; end; - if ( FBufferBitmap.Width <> PaintBox.ClientWidth ) or - ( FBufferBitmap.Height <> PaintBox.ClientHeight ) then + // Resize buffer bitmap if PaintBox size changed + if (FBufferBitmap.Width <> PaintBox.ClientWidth) or (FBufferBitmap.Height <> PaintBox.ClientHeight) then begin - FBufferBitmap.SetSize( PaintBox.ClientWidth, PaintBox.ClientHeight ); + FBufferBitmap.SetSize(PaintBox.ClientWidth, PaintBox.ClientHeight); end; - DrawChart( FBufferBitmap.Canvas, Rect( 0, 0, FBufferBitmap.Width, FBufferBitmap.Height ) ); - PaintBox.Canvas.Draw( 0, 0, FBufferBitmap ); + // Draw chart onto the buffer + DrawChart(FBufferBitmap.Canvas, Rect(0, 0, FBufferBitmap.Width, FBufferBitmap.Height)); + // Copy buffer to screen + PaintBox.Canvas.Draw(0, 0, FBufferBitmap); end; -procedure TChartForm.DrawChart( ACanvas: TCanvas; ARect: TRect ); +procedure TChartForm.SymbolComboBoxChange(Sender: TObject); +var + selectedIndex: Integer; + selectedFileName: string; +begin + Memo.Clear; // Clear memo for new symbol load information + Memo.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - Symbol selection changed or load triggered.'); + ClearAllCachesInForm(); // Clear existing caches and data + FDisplayStartIndex := 0; // Reset display start index + + if SymbolComboBox.ItemIndex = -1 then + begin + Memo.Lines.Add('Error: No symbol selected in SymbolComboBox.'); + PaintBox.Invalidate; // Refresh chart (will show no data message) + exit; + end; + + // Retrieve stored index from the ComboBox item's Object property + selectedIndex := Integer(SymbolComboBox.Items.Objects[SymbolComboBox.ItemIndex]); + + // Validate the retrieved index against the FSymbolFiles array bounds + if (selectedIndex >= Low(FSymbolFiles)) and (selectedIndex <= High(FSymbolFiles)) then + begin + selectedFileName := FSymbolFiles[selectedIndex]; // Get the full path from our stored array + end + else + begin + Memo.Lines.Add(Format('Error: Invalid stored index %d in SymbolComboBox.', [selectedIndex])); + PaintBox.Invalidate; + exit; + end; + + if Trim(selectedFileName) = '' then + begin + Memo.Lines.Add('Error: File name for the selected symbol is empty.'); + MessageDlg('File name for the selected symbol is empty.', mtWarning, [mbOK], 0); + PaintBox.Invalidate; + exit; + end; + + Memo.Lines.Add('Loading data for symbol "' + SymbolComboBox.Text + '" from file: ' + selectedFileName); + + if Assigned(FDataController) then + begin + FDataController.LoadTickDataSeries(selectedFileName); // Load data using the controller + if FDataController.HasData then + begin + Memo.Lines.Add( + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + + Format( + ' - Successfully loaded %d ticks for symbol "%s" by controller.', + [FDataController.GetTickDataCount, SymbolComboBox.Text]) + ); + EnsureCacheForCurrentTimeframe(True); // Rebuild cache for the new data and current timeframe + end + else + begin + Memo.Lines.Add( + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + + ' - No tick data was loaded for symbol "' + + SymbolComboBox.Text + + '" by controller, or an error occurred.' + ); + FActiveOHLCCache := nil; // Ensure active cache is nil if no data + end; + end + else + begin + Memo.Lines.Add('Error: DataController is not initialized. Cannot load data for symbol "' + SymbolComboBox.Text + '".'); + end; + + PaintBox.Invalidate; // Refresh the chart display +end; + +procedure TChartForm.DrawChart(ACanvas: TCanvas; ARect: TRect); const DefaultVolumeAreaHeightRatio = 0.20; MinVolumeAreaPixelHeight = 30; @@ -843,89 +944,123 @@ var OriginalFontColor: TColor; OriginalBrushStyle: TBrushStyle; OriginalFontSize: Integer; // For saving and restoring font size + HorzYClamped: Integer; // For crosshair Y position clamping + ActualCacheIdxForCrosshair: Integer; // For crosshair candle index begin - ACanvas.Brush.Color := clWhite; - ACanvas.FillRect( ARect ); + ACanvas.Brush.Color := clWhite; // Set background color + ACanvas.FillRect(ARect); // Fill the entire canvas area - FCurrentPriceRect := Rect( 0, 0, 0, 0 ); - FCurrentVolumeHistRect := Rect( 0, 0, 0, 0 ); + FCurrentPriceRect := Rect(0, 0, 0, 0); // Reset current price chart area + FCurrentVolumeHistRect := Rect(0, 0, 0, 0); // Reset current volume histogram area - if not Assigned( FActiveOHLCCache ) or not FActiveOHLCCache.IsValid or ( FActiveOHLCCache.Count = 0 ) then + // Check if there's valid data to draw + if not Assigned(FActiveOHLCCache) or not FActiveOHLCCache.IsValid or (FActiveOHLCCache.Count = 0) then begin ACanvas.Font.Color := clGray; - ACanvas.TextOut( ARect.Left + 10, ARect.Top + 10, 'No data in active cache or cache is invalid.' ); - Exit; + ACanvas.TextOut(ARect.Left + 10, ARect.Top + 10, 'No data in active cache or cache is invalid.'); + exit; end; - DrawableContentRect := Rect( ARect.Left + DefaultChartMargin, ARect.Top + DefaultChartMargin, - ARect.Right - DefaultChartMargin, ARect.Bottom - DefaultChartMargin ); + // Define the main drawable content area with margins + DrawableContentRect := + Rect( + ARect.Left + DefaultChartMargin, + ARect.Top + DefaultChartMargin, + ARect.Right - DefaultChartMargin, + ARect.Bottom - DefaultChartMargin + ); - if ( DrawableContentRect.Width <= 0 ) or ( DrawableContentRect.Height <= 0 ) then + // Check if drawable area is too small + if (DrawableContentRect.Width <= 0) or (DrawableContentRect.Height <= 0) then begin ACanvas.Font.Color := clRed; - ACanvas.TextOut( ARect.Left + 10, ARect.Top + 10, 'Drawable area too small.' ); - Exit; + ACanvas.TextOut(ARect.Left + 10, ARect.Top + 10, 'Drawable area too small.'); + exit; end; - ShowVolumeBars := ( FActiveOHLCCache.AggregationTimeframeSeconds <> TF_AUTO_AGGREGATION ) and - ( FActiveOHLCCache.AggregationTimeframeSeconds <> TF_TICK_BY_TICK ); + // Determine if volume bars should be shown + ShowVolumeBars := + (FActiveOHLCCache.AggregationTimeframeSeconds <> TF_AUTO_AGGREGATION) // Not auto aggregation + and (FActiveOHLCCache.AggregationTimeframeSeconds <> TF_TICK_BY_TICK); // Not tick-by-tick ActualVolumeHistAreaHeight := 0; if ShowVolumeBars then begin - ActualVolumeHistAreaHeight := Round( DrawableContentRect.Height * DefaultVolumeAreaHeightRatio ); - ActualVolumeHistAreaHeight := Max( MinVolumeAreaPixelHeight, Min( ActualVolumeHistAreaHeight, MaxVolumeAreaPixelHeight ) ); - if DrawableContentRect.Height < ( ActualVolumeHistAreaHeight + PriceToVolumeSeparatorPixels + 50 ) then + // Calculate initial height for volume area + ActualVolumeHistAreaHeight := Round(DrawableContentRect.Height * DefaultVolumeAreaHeightRatio); + // Clamp volume area height to min/max pixel values + ActualVolumeHistAreaHeight := Max(MinVolumeAreaPixelHeight, Min(ActualVolumeHistAreaHeight, MaxVolumeAreaPixelHeight)); + + // Adjust if total height is insufficient for price, volume, and separator + if DrawableContentRect.Height < (ActualVolumeHistAreaHeight + PriceToVolumeSeparatorPixels + 50) then // 50 is arbitrary minimum for price chart begin - ActualVolumeHistAreaHeight := Max( 0, DrawableContentRect.Height - PriceToVolumeSeparatorPixels - 50 ); - if ActualVolumeHistAreaHeight < MinVolumeAreaPixelHeight then + ActualVolumeHistAreaHeight := Max(0, DrawableContentRect.Height - PriceToVolumeSeparatorPixels - 50); + if ActualVolumeHistAreaHeight < MinVolumeAreaPixelHeight then // If it becomes too small, don't show it ActualVolumeHistAreaHeight := 0; end; end; - if ActualVolumeHistAreaHeight <= 0 then + if ActualVolumeHistAreaHeight <= 0 then // If height is zero or less, disable volume bars ShowVolumeBars := False; + // Define drawing rectangles for price and volume areas if ShowVolumeBars then begin - VolumeHistRect_Local := Rect( DrawableContentRect.Left, DrawableContentRect.Bottom - ActualVolumeHistAreaHeight, - DrawableContentRect.Right, DrawableContentRect.Bottom ); - PriceRect_Local := Rect( DrawableContentRect.Left, DrawableContentRect.Top, - DrawableContentRect.Right, VolumeHistRect_Local.Top - PriceToVolumeSeparatorPixels ); + VolumeHistRect_Local := + Rect( + DrawableContentRect.Left, + DrawableContentRect.Bottom - ActualVolumeHistAreaHeight, // Positioned at the bottom + DrawableContentRect.Right, + DrawableContentRect.Bottom + ); + PriceRect_Local := + Rect( + DrawableContentRect.Left, + DrawableContentRect.Top, + DrawableContentRect.Right, + VolumeHistRect_Local.Top - PriceToVolumeSeparatorPixels // Price area is above volume area + ); end - else - begin // Corrected block for when ShowVolumeBars is false + else // If not showing volume bars, price area takes full drawable content height + begin PriceRect_Local := DrawableContentRect; - VolumeHistRect_Local := Rect( 0, 0, 0, 0 ); + VolumeHistRect_Local := Rect(0, 0, 0, 0); // No volume area end; - if ( PriceRect_Local.Height <= 0 ) then + // Check if price chart area is too small + if (PriceRect_Local.Height <= 0) then begin ACanvas.Font.Color := clRed; - ACanvas.TextOut( ARect.Left + 10, ARect.Top + 10, 'Price chart area too small to draw.' ); - Exit; + ACanvas.TextOut(ARect.Left + 10, ARect.Top + 10, 'Price chart area too small to draw.'); + exit; end; + // Store the calculated drawing rectangles globally for mouse interactions FCurrentPriceRect := PriceRect_Local; FCurrentVolumeHistRect := VolumeHistRect_Local; NumCachedCandlesInActiveCache := FActiveOHLCCache.Count; - CurrentDrawingSlotWidth := FCandleWidthInPixels + FCandleSpacing; - if CurrentDrawingSlotWidth <= 0 then - Exit; + CurrentDrawingSlotWidth := FCandleWidthInPixels + FCandleSpacing; // Total width for one candle + spacing + if CurrentDrawingSlotWidth <= 0 then // Prevent division by zero or infinite loop + exit; + // Calculate how many candles can be drawn in the viewport NumCandlesDrawableInViewPort := PriceRect_Local.Width div CurrentDrawingSlotWidth; ActualNumCandlesToDraw := 0; - if ( FDisplayStartIndex >= 0 ) and ( FDisplayStartIndex < NumCachedCandlesInActiveCache ) then - ActualNumCandlesToDraw := Min( NumCandlesDrawableInViewPort, NumCachedCandlesInActiveCache - FDisplayStartIndex ); + // Determine the actual number of candles to draw based on start index and available cache + if (FDisplayStartIndex >= 0) and (FDisplayStartIndex < NumCachedCandlesInActiveCache) then + ActualNumCandlesToDraw := Min(NumCandlesDrawableInViewPort, NumCachedCandlesInActiveCache - FDisplayStartIndex); - MaxVisibleTickVolume := 0; + MaxVisibleTickVolume := 0; // Initialize max volume for scaling + // Determine Min/Max Y for scaling price chart and Max Volume for volume chart if ActualNumCandlesToDraw > 0 then begin - VisibleCandleForScale := FActiveOHLCCache.Candles[FDisplayStartIndex]; + VisibleCandleForScale := FActiveOHLCCache.Candles[FDisplayStartIndex]; // First visible candle LocalMinYValue := VisibleCandleForScale.LowPrice; LocalMaxYValue := VisibleCandleForScale.HighPrice; if ShowVolumeBars then MaxVisibleTickVolume := VisibleCandleForScale.TickVolume; + + // Iterate through visible candles to find overall min/max prices and max volume for k_scale := 1 to ActualNumCandlesToDraw - 1 do begin CacheIdx := FDisplayStartIndex + k_scale; @@ -934,179 +1069,237 @@ begin LocalMinYValue := VisibleCandleForScale.LowPrice; if VisibleCandleForScale.HighPrice > LocalMaxYValue then LocalMaxYValue := VisibleCandleForScale.HighPrice; - if ShowVolumeBars and ( VisibleCandleForScale.TickVolume > MaxVisibleTickVolume ) then + if ShowVolumeBars and (VisibleCandleForScale.TickVolume > MaxVisibleTickVolume) then MaxVisibleTickVolume := VisibleCandleForScale.TickVolume; end; - if Abs( LocalMaxYValue - LocalMinYValue ) < 1E-6 then + + // Expand range slightly if min and max are too close (or equal) to prevent division by zero in scaling + if Abs(LocalMaxYValue - LocalMinYValue) < 1E-6 then // Using a small epsilon for float comparison begin - ExpansionValue := Max( Abs( LocalMinYValue ) * 0.001, 0.0001 ); + ExpansionValue := Max(Abs(LocalMinYValue) * 0.001, 0.0001); // Expand by 0.1% or a tiny fixed amount LocalMaxYValue := LocalMinYValue + ExpansionValue; LocalMinYValue := LocalMinYValue - ExpansionValue; end; CurrentMinYToUse := LocalMinYValue; CurrentMaxYToUse := LocalMaxYValue; - if ShowVolumeBars and ( MaxVisibleTickVolume = 0 ) then + + if ShowVolumeBars and (MaxVisibleTickVolume = 0) then // Ensure max volume is at least 1 for scaling MaxVisibleTickVolume := 1; end - else + else // If no candles to draw, use global min/max from cache or default values begin - CurrentMinYToUse := FActiveOHLCCache.GlobalMinY; + CurrentMinYToUse := FActiveOHLCCache.GlobalMinY; // These should be pre-calculated in TOHLC CurrentMaxYToUse := FActiveOHLCCache.GlobalMaxY; - if Abs( CurrentMaxYToUse - CurrentMinYToUse ) < 1E-6 then + if Abs(CurrentMaxYToUse - CurrentMinYToUse) < 1E-6 then // Handle flat data begin - CurrentMaxYToUse := CurrentMinYToUse + 0.0002; + CurrentMaxYToUse := CurrentMinYToUse + 0.0002; // Some small default range CurrentMinYToUse := CurrentMinYToUse - 0.0002; end; end; + // Draw Min/Max Price and Max Volume labels OriginalFontSize := ACanvas.Font.Size; // Save original font size - ACanvas.Font.Size := 8; + ACanvas.Font.Size := 8; // Use a smaller font for these labels ACanvas.Font.Color := clGray; - ACanvas.TextOut( PriceRect_Local.Left + 3, PriceRect_Local.Top + 3, Format( '%.5f', [CurrentMaxYToUse] ) ); - ACanvas.TextOut( PriceRect_Local.Left + 3, PriceRect_Local.Bottom - ACanvas.TextHeight( '0' ) - 3, - Format( '%.5f', [CurrentMinYToUse] ) ); - if ShowVolumeBars and ( VolumeHistRect_Local.Height > 0 ) then - ACanvas.TextOut( VolumeHistRect_Local.Left + 3, VolumeHistRect_Local.Top + 3, Format( '%d', [MaxVisibleTickVolume] ) ); + ACanvas.TextOut(PriceRect_Local.Left + 3, PriceRect_Local.Top + 3, Format('%.5f', [CurrentMaxYToUse])); + ACanvas.TextOut(PriceRect_Local.Left + 3, PriceRect_Local.Bottom - ACanvas.TextHeight('0') - 3, Format('%.5f', [CurrentMinYToUse])); + if ShowVolumeBars and (VolumeHistRect_Local.Height > 0) then // Only draw if volume area is visible + ACanvas.TextOut(VolumeHistRect_Local.Left + 3, VolumeHistRect_Local.Top + 3, Format('%d', [MaxVisibleTickVolume])); ACanvas.Font.Size := OriginalFontSize; // Restore original font size - if Abs( CurrentMaxYToUse - CurrentMinYToUse ) < 1E-7 then - ScaleY := 1.0 + // Calculate Y scaling factor for price + if Abs(CurrentMaxYToUse - CurrentMinYToUse) < 1E-7 then // Prevent division by zero + ScaleY := 1.0 // Default scale if range is too small else - ScaleY := PriceRect_Local.Height / ( CurrentMaxYToUse - CurrentMinYToUse ); - if ScaleY <= 0 then + ScaleY := PriceRect_Local.Height / (CurrentMaxYToUse - CurrentMinYToUse); + if ScaleY <= 0 then // Ensure ScaleY is positive ScaleY := 1.0; + + // Calculate Y scaling factor for volume VolumeScaleY := 0; - if ShowVolumeBars and ( VolumeHistRect_Local.Height > 0 ) and ( MaxVisibleTickVolume > 0 ) then + if ShowVolumeBars and (VolumeHistRect_Local.Height > 0) and (MaxVisibleTickVolume > 0) then VolumeScaleY := VolumeHistRect_Local.Height / MaxVisibleTickVolume; - CurrentCandleXStart := PriceRect_Local.Left; + // --- Draw Candles and Volume Bars --- + CurrentCandleXStart := PriceRect_Local.Left; // Starting X position for the first candle if ActualNumCandlesToDraw > 0 then begin for LoopIdx := 0 to ActualNumCandlesToDraw - 1 do begin CacheIdx := FDisplayStartIndex + LoopIdx; CachedCandleRec := FActiveOHLCCache.Candles[CacheIdx]; - Y_high := PriceRect_Local.Bottom - Round( ( CachedCandleRec.HighPrice - CurrentMinYToUse ) * ScaleY ); - Y_low := PriceRect_Local.Bottom - Round( ( CachedCandleRec.LowPrice - CurrentMinYToUse ) * ScaleY ); - Y_open := PriceRect_Local.Bottom - Round( ( CachedCandleRec.OpenPrice - CurrentMinYToUse ) * ScaleY ); - Y_close := PriceRect_Local.Bottom - Round( ( CachedCandleRec.ClosePrice - CurrentMinYToUse ) * ScaleY ); - Y_high := Max( PriceRect_Local.Top, Min( PriceRect_Local.Bottom, Y_high ) ); - Y_low := Max( PriceRect_Local.Top, Min( PriceRect_Local.Bottom, Y_low ) ); - Y_open := Max( PriceRect_Local.Top, Min( PriceRect_Local.Bottom, Y_open ) ); - Y_close := Max( PriceRect_Local.Top, Min( PriceRect_Local.Bottom, Y_close ) ); - X_wick_center := CurrentCandleXStart + ( FCandleWidthInPixels div 2 ); + + // Calculate Y coordinates for Open, High, Low, Close + Y_high := PriceRect_Local.Bottom - Round((CachedCandleRec.HighPrice - CurrentMinYToUse) * ScaleY); + Y_low := PriceRect_Local.Bottom - Round((CachedCandleRec.LowPrice - CurrentMinYToUse) * ScaleY); + Y_open := PriceRect_Local.Bottom - Round((CachedCandleRec.OpenPrice - CurrentMinYToUse) * ScaleY); + Y_close := PriceRect_Local.Bottom - Round((CachedCandleRec.ClosePrice - CurrentMinYToUse) * ScaleY); + + // Clamp Y coordinates to the price chart area + Y_high := Max(PriceRect_Local.Top, Min(PriceRect_Local.Bottom, Y_high)); + Y_low := Max(PriceRect_Local.Top, Min(PriceRect_Local.Bottom, Y_low)); + Y_open := Max(PriceRect_Local.Top, Min(PriceRect_Local.Bottom, Y_open)); + Y_close := Max(PriceRect_Local.Top, Min(PriceRect_Local.Bottom, Y_close)); + + // Draw wick (High-Low line) + X_wick_center := CurrentCandleXStart + (FCandleWidthInPixels div 2); ACanvas.Pen.Color := clBlack; ACanvas.Pen.Width := 1; - ACanvas.MoveTo( X_wick_center, Y_high ); - ACanvas.LineTo( X_wick_center, Y_low ); + ACanvas.MoveTo(X_wick_center, Y_high); + ACanvas.LineTo(X_wick_center, Y_low); + + // Define candle body rectangle BodyRect.Left := CurrentCandleXStart; BodyRect.Right := CurrentCandleXStart + FCandleWidthInPixels; - BodyRect.Top := Min( Y_open, Y_close ); - BodyRect.Bottom := Max( Y_open, Y_close ); - if Abs( Y_open - Y_close ) < 1 then + BodyRect.Top := Min(Y_open, Y_close); // Top of body is the higher of Open/Close in screen coords + BodyRect.Bottom := Max(Y_open, Y_close); // Bottom of body is the lower of Open/Close in screen coords + + // Handle Doji (Open equals Close) + if Abs(Y_open - Y_close) < 1 then // If body height is less than 1 pixel, draw as a line begin - ACanvas.Pen.Width := DojiPenWidth; - ACanvas.MoveTo( BodyRect.Left, Y_open ); - ACanvas.LineTo( BodyRect.Right, Y_open ); - ACanvas.Pen.Width := 1; + ACanvas.Pen.Width := DojiPenWidth; // Thicker pen for Doji line + ACanvas.MoveTo(BodyRect.Left, Y_open); + ACanvas.LineTo(BodyRect.Right, Y_open); + ACanvas.Pen.Width := 1; // Reset pen width end - else + else // Draw normal candle body begin - if BodyRect.Bottom <= BodyRect.Top then + if BodyRect.Bottom <= BodyRect.Top then // Ensure body has at least 1px height BodyRect.Bottom := BodyRect.Top + 1; - if CachedCandleRec.ClosePrice > CachedCandleRec.OpenPrice then - ACanvas.Brush.Color := clWhite - else - ACanvas.Brush.Color := clBlack; - ACanvas.Pen.Color := clBlack; - ACanvas.Rectangle( BodyRect ); + + if CachedCandleRec.ClosePrice > CachedCandleRec.OpenPrice then // Bullish candle + ACanvas.Brush.Color := clWhite // Or a bullish color like clLime + else // Bearish candle + ACanvas.Brush.Color := clBlack; // Or a bearish color like clRed + + ACanvas.Pen.Color := clBlack; // Outline color + ACanvas.Rectangle(BodyRect); // Draw the body end; - if ShowVolumeBars and ( VolumeScaleY > 0 ) then + + // Draw Volume Bar if enabled + if ShowVolumeBars and (VolumeScaleY > 0) then begin - VolumeBarHeight := Round( CachedCandleRec.TickVolume * VolumeScaleY ); - VolumeBarHeight := Max( 0, Min( VolumeBarHeight, VolumeHistRect_Local.Height ) ); - VolBarRect := Rect( CurrentCandleXStart, VolumeHistRect_Local.Bottom - VolumeBarHeight, - CurrentCandleXStart + FCandleWidthInPixels, VolumeHistRect_Local.Bottom ); + VolumeBarHeight := Round(CachedCandleRec.TickVolume * VolumeScaleY); + VolumeBarHeight := Max(0, Min(VolumeBarHeight, VolumeHistRect_Local.Height)); // Clamp height + + VolBarRect := + Rect( + CurrentCandleXStart, + VolumeHistRect_Local.Bottom - VolumeBarHeight, // From bottom of volume area upwards + CurrentCandleXStart + FCandleWidthInPixels, + VolumeHistRect_Local.Bottom + ); + + // Color volume bar based on candle type if CachedCandleRec.ClosePrice > CachedCandleRec.OpenPrice then - ACanvas.Brush.Color := TColor( $00B0F0B0 ) + ACanvas.Brush.Color := TColor($00B0F0B0) // Light green for bullish volume else if CachedCandleRec.ClosePrice < CachedCandleRec.OpenPrice then - ACanvas.Brush.Color := TColor( $00B0B0F0 ) + ACanvas.Brush.Color := TColor($00B0B0F0) // Light red for bearish volume else - ACanvas.Brush.Color := clSilver; - ACanvas.Pen.Color := clGray; - ACanvas.Rectangle( VolBarRect ); + ACanvas.Brush.Color := clSilver; // Neutral color + + ACanvas.Pen.Color := clGray; // Outline for volume bar + ACanvas.Rectangle(VolBarRect); end; - CurrentCandleXStart := CurrentCandleXStart + CurrentDrawingSlotWidth; + + CurrentCandleXStart := CurrentCandleXStart + CurrentDrawingSlotWidth; // Move to next candle position end; end; + // Draw borders around price and volume areas ACanvas.Pen.Style := psSolid; ACanvas.Pen.Width := 1; ACanvas.Pen.Color := clBlack; - ACanvas.Brush.Style := bsClear; - ACanvas.Rectangle( PriceRect_Local ); - if ShowVolumeBars and ( VolumeHistRect_Local.Width > 0 ) and ( VolumeHistRect_Local.Height > 0 ) then - ACanvas.Rectangle( VolumeHistRect_Local ); + ACanvas.Brush.Style := bsClear; // Transparent brush for drawing rectangles + ACanvas.Rectangle(PriceRect_Local); + if ShowVolumeBars and (VolumeHistRect_Local.Width > 0) and (VolumeHistRect_Local.Height > 0) then + ACanvas.Rectangle(VolumeHistRect_Local); - if FMouseCurrentlyInChartArea and Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) and - ( FMouseLastChartPos.X >= 0 ) and Assigned( FDataController ) and FDataController.HasData then + // --- Draw Crosshair and Hover Info --- + if FMouseCurrentlyInChartArea // Mouse is within the general paintbox area designated for chart + and Assigned(FActiveOHLCCache) + and FActiveOHLCCache.IsValid + and (FActiveOHLCCache.Count > 0) + and (FMouseLastChartPos.X >= 0) // Valid mouse position + and Assigned(FDataController) + and FDataController.HasData then begin + // Save current canvas settings OriginalPenStyle := ACanvas.Pen.Style; OriginalPenWidth := ACanvas.Pen.Width; OriginalFontColor := ACanvas.Font.Color; OriginalBrushStyle := ACanvas.Brush.Style; OriginalFontSize := ACanvas.Font.Size; + + // Set crosshair style ACanvas.Pen.Color := clGray; ACanvas.Pen.Style := psDot; ACanvas.Pen.Width := 1; - ACanvas.Font.Color := clWindowText; - ACanvas.Brush.Style := bsClear; - ACanvas.Font.Size := 8; - var - HorzYClamped := Max( DrawableContentRect.Top, Min( FMouseLastChartPos.Y, DrawableContentRect.Bottom ) ); - ACanvas.MoveTo( DrawableContentRect.Left, HorzYClamped ); - ACanvas.LineTo( DrawableContentRect.Right, HorzYClamped ); - if ( ScaleY > 1E-6 ) and ( PriceRect_Local.Height > 0 ) then + ACanvas.Font.Color := clWindowText; // Color for text labels + ACanvas.Brush.Style := bsClear; // Transparent background for text + ACanvas.Font.Size := 8; // Small font for labels + + // Horizontal Crosshair Line and Price Label + // Clamp Y position of horizontal line to the drawable content area (Price + Volume if shown) + HorzYClamped := Max(DrawableContentRect.Top, Min(FMouseLastChartPos.Y, DrawableContentRect.Bottom)); + ACanvas.MoveTo(DrawableContentRect.Left, HorzYClamped); + ACanvas.LineTo(DrawableContentRect.Right, HorzYClamped); + + // Draw price label only if mouse is within the price chart area for Y + if PtInRect(PriceRect_Local, Point(FMouseLastChartPos.X, HorzYClamped)) and (ScaleY > 1E-6) and (PriceRect_Local.Height > 0) then begin - PriceAtMouseY := CurrentMinYToUse + ( ( PriceRect_Local.Bottom - HorzYClamped ) / ScaleY ); - PriceStr := Format( '%.5f', [PriceAtMouseY] ); - TextX := DrawableContentRect.Right + CrosshairPriceTimeMargin; - TextY := HorzYClamped - ( ACanvas.TextHeight( PriceStr ) div 2 ); - ACanvas.TextOut( TextX, TextY, PriceStr ); + PriceAtMouseY := CurrentMinYToUse + ((PriceRect_Local.Bottom - HorzYClamped) / ScaleY); + PriceStr := Format('%.5f', [PriceAtMouseY]); + TextX := DrawableContentRect.Right + CrosshairPriceTimeMargin; // Position to the right of chart + TextY := HorzYClamped - (ACanvas.TextHeight(PriceStr) div 2); // Vertically centered + ACanvas.TextOut(TextX, TextY, PriceStr); end; - if CurrentDrawingSlotWidth > 0 then + + // Vertical Crosshair Line and Time Label (only if mouse X is within drawable content) + if (CurrentDrawingSlotWidth > 0) + and (FMouseLastChartPos.X >= DrawableContentRect.Left) + and (FMouseLastChartPos.X <= DrawableContentRect.Right) then begin + // Calculate candle index under mouse, relative to the *price chart area* specifically for X RelativeX := FMouseLastChartPos.X - PriceRect_Local.Left; - if RelativeX >= 0 then + if RelativeX >= 0 then // Ensure mouse is at or to the right of the price area's left edge begin CandleIndexInViewPortForCrosshair := RelativeX div CurrentDrawingSlotWidth; - VerticalLineX := PriceRect_Local.Left + ( CandleIndexInViewPortForCrosshair * CurrentDrawingSlotWidth ) + - ( FCandleWidthInPixels div 2 ); - if ( VerticalLineX >= DrawableContentRect.Left ) and ( VerticalLineX <= DrawableContentRect.Right ) then + // Calculate center X of the candle for the vertical line + VerticalLineX := + PriceRect_Local.Left + (CandleIndexInViewPortForCrosshair * CurrentDrawingSlotWidth) + (FCandleWidthInPixels div 2); + + // Ensure vertical line is within the overall drawable content area + if (VerticalLineX >= DrawableContentRect.Left) and (VerticalLineX <= DrawableContentRect.Right) then begin - ACanvas.MoveTo( VerticalLineX, DrawableContentRect.Top ); - ACanvas.LineTo( VerticalLineX, DrawableContentRect.Bottom ); - var + ACanvas.MoveTo(VerticalLineX, DrawableContentRect.Top); + ACanvas.LineTo(VerticalLineX, DrawableContentRect.Bottom); + + // Time Label ActualCacheIdxForCrosshair := FDisplayStartIndex + CandleIndexInViewPortForCrosshair; - if ( ActualCacheIdxForCrosshair >= 0 ) and ( ActualCacheIdxForCrosshair < FActiveOHLCCache.Count ) then + if (ActualCacheIdxForCrosshair >= 0) and (ActualCacheIdxForCrosshair < FActiveOHLCCache.Count) then begin CrosshairCandle := FActiveOHLCCache.Candles[ActualCacheIdxForCrosshair]; - if ( CrosshairCandle.OriginalDataIndexStart >= 0 ) and - ( CrosshairCandle.OriginalDataIndexStart < FDataController.GetTickDataLength ) then + if (CrosshairCandle.OriginalDataIndexStart >= 0) + and (CrosshairCandle.OriginalDataIndexStart < FDataController.GetTickDataLength) then // Use GetTickDataLength begin - UTCTimeStart := FDataController.GetTickDataItem( CrosshairCandle.OriginalDataIndexStart ).OADateTime; - LocalTimeStart := TTimeZone.Local.ToLocalTime( UTCTimeStart ); - TimeStr := FormatDateTime( 'hh:nn:ss', LocalTimeStart ); - TextX := VerticalLineX - ( ACanvas.TextWidth( TimeStr ) div 2 ); - TextY := DrawableContentRect.Top - ACanvas.TextHeight( TimeStr ) - CrosshairPriceTimeMargin; - if TextY < ARect.Top then + UTCTimeStart := FDataController.GetTickDataItem(CrosshairCandle.OriginalDataIndexStart).OADateTime; + LocalTimeStart := TTimeZone.Local.ToLocalTime(UTCTimeStart); + TimeStr := FormatDateTime('hh:nn:ss', LocalTimeStart); // Display time part + + TextX := VerticalLineX - (ACanvas.TextWidth(TimeStr) div 2); // Horizontally centered on line + TextY := + DrawableContentRect.Top - ACanvas.TextHeight(TimeStr) - CrosshairPriceTimeMargin; // Position above chart + if TextY < ARect.Top then // Ensure it's within the paintbox bounds TextY := ARect.Top; - ACanvas.TextOut( TextX, TextY, TimeStr ); + ACanvas.TextOut(TextX, TextY, TimeStr); end; end; end; end; end; + + // Restore original canvas settings ACanvas.Pen.Style := OriginalPenStyle; ACanvas.Pen.Width := OriginalPenWidth; ACanvas.Font.Color := OriginalFontColor; @@ -1115,107 +1308,200 @@ begin end; end; -procedure TChartForm.FormMouseWheel( Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean ); +procedure TChartForm.FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean); const MinDisplayCandleWidth = 1; MaxDisplayCandleWidth = 100; - ZoomStep = 1; + ZoomStep = 1; // Number of pixels to change candle width per wheel step var LMousePosInPaintBox: TPoint; OldCandleWidthInPixels, OriginalFDisplayStartIndex: Integer; MouseX_InChartArea_Relative: Integer; OldSlotWidth, NewSlotWidth: Integer; - GlobalCandleIndexUnderMouse_Float: Double; + GlobalCandleIndexUnderMouse_Float: Double; // For precise calculation of index under cursor ChartDrawWidth, DrawableCandlesInViewPort, MaxPossibleStartIndex: Integer; ZoomToCursorActive: Boolean; begin - Handled := False; - LMousePosInPaintBox := PaintBox.ScreenToClient( MousePos ); - if not PtInRect( PaintBox.ClientRect, LMousePosInPaintBox ) then - Exit; - ZoomToCursorActive := Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) and - ( FCurrentPriceRect.Width > 0 ) and PtInRect( FCurrentPriceRect, LMousePosInPaintBox ); + Handled := False; // Default to not handled + LMousePosInPaintBox := PaintBox.ScreenToClient(MousePos); // Convert mouse pos to PaintBox client coordinates + + // Only handle if mouse is over the PaintBox + if not PtInRect(PaintBox.ClientRect, LMousePosInPaintBox) then + exit; + + // Zoom to cursor is active if there's a valid cache, data, and mouse is over the price chart area + ZoomToCursorActive := + Assigned(FActiveOHLCCache) + and FActiveOHLCCache.IsValid + and (FActiveOHLCCache.Count > 0) + and (FCurrentPriceRect.Width > 0) // Ensure price rect is valid + and PtInRect(FCurrentPriceRect, LMousePosInPaintBox); // Mouse must be within price chart area + OriginalFDisplayStartIndex := FDisplayStartIndex; OldCandleWidthInPixels := FCandleWidthInPixels; - if WheelDelta > 0 then - FCandleWidthInPixels := Min( MaxDisplayCandleWidth, FCandleWidthInPixels + ZoomStep ) - else if WheelDelta < 0 then - FCandleWidthInPixels := Max( MinDisplayCandleWidth, FCandleWidthInPixels - ZoomStep ) + + // Adjust candle width based on wheel delta + if WheelDelta > 0 then // Zoom in + FCandleWidthInPixels := Min(MaxDisplayCandleWidth, FCandleWidthInPixels + ZoomStep) + else if WheelDelta < 0 then // Zoom out + FCandleWidthInPixels := Max(MinDisplayCandleWidth, FCandleWidthInPixels - ZoomStep) else - Exit; + exit; // No change in wheel delta + + // If candle width didn't change (e.g., at min/max limit), exit if FCandleWidthInPixels = OldCandleWidthInPixels then - Exit; + exit; + NewSlotWidth := FCandleWidthInPixels + FCandleSpacing; - if NewSlotWidth <= 0 then + if NewSlotWidth <= 0 then // Safety check for new slot width begin - FCandleWidthInPixels := OldCandleWidthInPixels; - Exit; + FCandleWidthInPixels := OldCandleWidthInPixels; // Revert to old width + exit; end; + + // Adjust FDisplayStartIndex to keep the candle under the mouse cursor stationary if ZoomToCursorActive then begin OldSlotWidth := OldCandleWidthInPixels + FCandleSpacing; - if OldSlotWidth <= 0 then - ZoomToCursorActive := False + if OldSlotWidth <= 0 then // Safety check for old slot width + ZoomToCursorActive := False // Disable zoom to cursor if old slot width is invalid else begin MouseX_InChartArea_Relative := LMousePosInPaintBox.X - FCurrentPriceRect.Left; - GlobalCandleIndexUnderMouse_Float := OriginalFDisplayStartIndex + ( MouseX_InChartArea_Relative / OldSlotWidth ); - FDisplayStartIndex := Round( GlobalCandleIndexUnderMouse_Float - ( MouseX_InChartArea_Relative / NewSlotWidth ) ); + // Calculate the fractional global index of the candle under the mouse before zoom + GlobalCandleIndexUnderMouse_Float := OriginalFDisplayStartIndex + (MouseX_InChartArea_Relative / OldSlotWidth); + // Calculate the new FDisplayStartIndex to keep this global index at the same relative mouse position + FDisplayStartIndex := Round(GlobalCandleIndexUnderMouse_Float - (MouseX_InChartArea_Relative / NewSlotWidth)); end; end; + + // Recalculate drawing parameters and clamp FDisplayStartIndex if FCurrentPriceRect.Width > 0 then ChartDrawWidth := FCurrentPriceRect.Width - else - ChartDrawWidth := PaintBox.ClientWidth - ( 2 * DefaultChartMargin ); - DrawableCandlesInViewPort := Max( 1, ChartDrawWidth div NewSlotWidth ); + else // Fallback if FCurrentPriceRect is not yet calculated or invalid + ChartDrawWidth := PaintBox.ClientWidth - (2 * DefaultChartMargin); + + DrawableCandlesInViewPort := Max(1, ChartDrawWidth div NewSlotWidth); // At least 1 candle drawable + MaxPossibleStartIndex := 0; - if Assigned( FActiveOHLCCache ) and ( FActiveOHLCCache.Count > 0 ) then + if Assigned(FActiveOHLCCache) and (FActiveOHLCCache.Count > 0) then MaxPossibleStartIndex := FActiveOHLCCache.Count - DrawableCandlesInViewPort; - if MaxPossibleStartIndex < 0 then + + if MaxPossibleStartIndex < 0 then // Ensure MaxPossibleStartIndex is not negative MaxPossibleStartIndex := 0; - FDisplayStartIndex := Max( 0, FDisplayStartIndex ); - FDisplayStartIndex := Min( FDisplayStartIndex, MaxPossibleStartIndex ); - if Assigned( FActiveOHLCCache ) and ( FActiveOHLCCache.Count = 0 ) then + + FDisplayStartIndex := Max(0, FDisplayStartIndex); // Clamp to minimum 0 + FDisplayStartIndex := Min(FDisplayStartIndex, MaxPossibleStartIndex); // Clamp to maximum possible start index + + // If cache is empty, reset display start index to 0 + if Assigned(FActiveOHLCCache) and (FActiveOHLCCache.Count = 0) then FDisplayStartIndex := 0; - Memo.Lines.Add( Format( 'MouseWheel: CW: %d->%d. NewStartIdx: %d. ZoomToCursor: %s', [OldCandleWidthInPixels, FCandleWidthInPixels, - FDisplayStartIndex, BoolToStr( ZoomToCursorActive, True )] ) ); - PaintBox.Invalidate; - Handled := True; - PaintBoxMouseMove( Sender, Shift, LMousePosInPaintBox.X, LMousePosInPaintBox.Y ); + + Memo.Lines.Add( + Format( + 'MouseWheel: CW: %d->%d. NewStartIdx: %d. ZoomToCursor: %s', + [OldCandleWidthInPixels, FCandleWidthInPixels, FDisplayStartIndex, BoolToStr(ZoomToCursorActive, True)] + ) + ); + + PaintBox.Invalidate; // Redraw the chart + Handled := True; // Indicate that the event was handled + + // Update mouse hover information as zoom might change what's under the cursor + PaintBoxMouseMove(Sender, Shift, LMousePosInPaintBox.X, LMousePosInPaintBox.Y); end; -procedure TChartForm.GoToCurrentButtonClick( Sender: TObject ); +procedure TChartForm.GoToCurrentButtonClick(Sender: TObject); var ChartDrawWidth, CurrentDrawingSlotWidth, NumCandlesDrawableInViewPort, NewDisplayStartIndex: Integer; begin - Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - GoToCurrentButton clicked.' ); - if not Assigned( FActiveOHLCCache ) or not FActiveOHLCCache.IsValid or ( FActiveOHLCCache.Count = 0 ) then + Memo.Lines.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - GoToCurrentButton clicked.'); + + if not Assigned(FActiveOHLCCache) or not FActiveOHLCCache.IsValid or (FActiveOHLCCache.Count = 0) then begin - Memo.Lines.Add( 'GoToCurrent: No data or cache invalid.' ); - Exit; + Memo.Lines.Add('GoToCurrent: No data or cache invalid.'); + exit; end; + + // Determine the chart drawing width if FCurrentPriceRect.Width > 0 then ChartDrawWidth := FCurrentPriceRect.Width - else - ChartDrawWidth := PaintBox.ClientWidth - ( 2 * DefaultChartMargin ); + else // Fallback if FCurrentPriceRect is not yet calculated + ChartDrawWidth := PaintBox.ClientWidth - (2 * DefaultChartMargin); + CurrentDrawingSlotWidth := FCandleWidthInPixels + FCandleSpacing; if CurrentDrawingSlotWidth <= 0 then begin - Memo.Lines.Add( 'GoToCurrent: Invalid candle slot width.' ); - Exit; + Memo.Lines.Add('GoToCurrent: Invalid candle slot width.'); + exit; end; - NumCandlesDrawableInViewPort := Max( 1, ChartDrawWidth div CurrentDrawingSlotWidth ); - NewDisplayStartIndex := Max( 0, FActiveOHLCCache.Count - NumCandlesDrawableInViewPort ); - if FActiveOHLCCache.Count > 0 then - NewDisplayStartIndex := Min( NewDisplayStartIndex, FActiveOHLCCache.Count - 1 ); + + // Calculate how many candles can be displayed in the viewport + NumCandlesDrawableInViewPort := Max(1, ChartDrawWidth div CurrentDrawingSlotWidth); + + // Calculate the start index to show the last N candles + NewDisplayStartIndex := Max(0, FActiveOHLCCache.Count - NumCandlesDrawableInViewPort); + + // Ensure NewDisplayStartIndex is within valid bounds if cache has items + if FActiveOHLCCache.Count > 0 then // Should be true based on earlier check, but good for safety + NewDisplayStartIndex := Min(NewDisplayStartIndex, FActiveOHLCCache.Count - 1); + if FDisplayStartIndex <> NewDisplayStartIndex then begin FDisplayStartIndex := NewDisplayStartIndex; - Memo.Lines.Add( Format( 'GoToCurrent: Scrolled to display last candles. New StartIdx: %d', [FDisplayStartIndex] ) ); - PaintBox.Invalidate; + Memo.Lines.Add(Format('GoToCurrent: Scrolled to display last candles. New StartIdx: %d', [FDisplayStartIndex])); + PaintBox.Invalidate; // Redraw the chart at the new position end else - Memo.Lines.Add( 'GoToCurrent: Already showing the latest candles.' ); + Memo.Lines.Add('GoToCurrent: Already showing the latest candles.'); +end; + +procedure TChartForm.PopulateSymbolComboBox; +var + selectedDirectory: string; + fileName: string; + pathValue, symbolValue: string; + yearValue, monthValue: Integer; + i: Integer; +begin + var currTxt := SymbolComboBox.Text; + + FSymbolFiles := FindOldestFilesPerSymbol(DataPathEdit.Text); // Find all relevant symbol files [cite: 238] + SymbolComboBox.Items.Clear; + SymbolComboBox.ClearSelection; // Ensure no previous selection remains + SymbolComboBox.Text := ''; + + if Length(FSymbolFiles) = 0 then + begin + Memo.Lines.Add('PathSelectButton: No symbol files found in the selected directory.'); + ClearAllCachesInForm; // Clear any existing data + PaintBox.Invalidate; // Refresh chart (will show no data) + exit; + end; + + Memo.Lines.Add(Format('PathSelectButton: Found %d symbol files.', [Length(FSymbolFiles)])); + var n := -1; + for i := Low(FSymbolFiles) to High(FSymbolFiles) do + begin + fileName := FSymbolFiles[i]; + // TryParseFileName extracts symbol from the filename. [cite: 237] + if TryParseFileName(fileName, pathValue, symbolValue, yearValue, monthValue) then + begin + SymbolComboBox.Items.AddObject( + Format('%s %.2d-%.4d', [symbolValue, monthValue, yearValue]), + TObject(i) + ); // Store index to FSymbolFiles in Object + if symbolValue = currTxt then + n := i; + Memo.Lines.Add(Format('PathSelectButton: Added symbol "%s" from file "%s".', [symbolValue, TPath.GetFileName(fileName)])); + end + else + begin + Memo.Lines.Add(Format('PathSelectButton: Could not parse symbol from filename "%s".', [TPath.GetFileName(fileName)])); + end; + end; + if n >= 0 then + SymbolComboBox.ItemIndex := n; end; end. diff --git a/common/Myc.ChartDataController.pas b/common/Myc.ChartDataController.pas index aff3f88..6530710 100644 --- a/common/Myc.ChartDataController.pas +++ b/common/Myc.ChartDataController.pas @@ -5,7 +5,7 @@ interface uses System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils, Myc.Futures, - Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants + Myc.Trade.DataSeries, Myc.OHLCCache; type TTimeframeSetting = record @@ -16,7 +16,7 @@ type TChartDataController = class private - FTickDataArray: TArray; + FTickDataArray: TArray; FOHLCCacheList: TList; FTimeframeSettingsArray: TArray; // Now includes registered generic builders FMemoLog: TStrings; @@ -39,9 +39,8 @@ type procedure ClearAllDataAndCaches; function GetTickDataCount: Integer; - function GetTickDataArrayPtr: PTTickData; function GetTickDataLength: Integer; - function GetTickDataItem(AIndex: Integer): TTickData; + function GetTickDataItem(AIndex: Integer): TAskBid.TTick; function HasData: Boolean; // ABuilderCaption is the unique key used for FTimeframeSettingsArray.Caption and FGenericBuilders key. @@ -55,7 +54,7 @@ type implementation uses - System.DateUtils; // For FormatDateTime in logging + System.DateUtils; const dcSecsPerDayConst = 24 * 60 * 60; @@ -211,7 +210,7 @@ procedure TChartDataController.LoadTickDataSeries(const AFileName: string); begin ClearAllDataAndCaches(); try - FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName).WaitFor; + FTickDataArray := TAskBid.LoadDataSeries(AFileName).WaitFor(); if FMemoLog <> nil then FMemoLog.Add(Format('DataController: Loading tick records from series: %s', [AFileName])); @@ -401,20 +400,12 @@ 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; +function TChartDataController.GetTickDataItem(AIndex: Integer): TAskBid.TTick; begin if (AIndex >= 0) and (AIndex < Length(FTickDataArray)) then Result := FTickDataArray[AIndex] @@ -422,7 +413,7 @@ begin 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); + Result := Default(TAskBid.TTick); end; end; diff --git a/common/Myc.OHLCCache.pas b/common/Myc.OHLCCache.pas index 5aa552a..7b93ffe 100644 --- a/common/Myc.OHLCCache.pas +++ b/common/Myc.OHLCCache.pas @@ -35,7 +35,7 @@ interface uses System.Classes, System.SysUtils, System.Math, System.Generics.Collections, System.DateUtils, - Myc.TickLoader; + Myc.Trade.DataSeries; const TF_AUTO_AGGREGATION = 0; @@ -56,9 +56,8 @@ type // Interface for generic candle building logic IGenericCandleBuilder = interface - ['{B4F6A9C1-0A7E-42D3-847B-170705A8CC8D}'] // Added GUID - function Init(const ADataSet: TArray): Boolean; // Changed DataSet to ADataSet for clarity - function IsNewBar(AIndex: Integer; const ATick: TTickData): Boolean; // Changed Idx to AIndex, Tick to ATick + function Init(const ADataSet: TArray): Boolean; // Changed DataSet to ADataSet for clarity + function IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; // Changed Idx to AIndex, Tick to ATick function GetCaption: string; // Method for the Caption property property Caption: string read GetCaption; end; @@ -81,7 +80,7 @@ type public constructor Create; procedure ClearCache; - procedure Build(const ATickData: TArray; + procedure Build(const ATickData: TArray; ASelectedTimeframeSeconds: Int64; AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; @@ -189,7 +188,7 @@ begin Result := Length(FCachedCandles); end; -procedure TOHLC.Build(const ATickData: TArray; +procedure TOHLC.Build(const ATickData: TArray; ASelectedTimeframeSeconds: Int64; AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; AAutoAggPaintBoxClientWidth: Integer; AMemoForLogging: TStrings; AGenericCandleBuilder: IGenericCandleBuilder = nil); // Parameter changed @@ -201,7 +200,7 @@ var CandleList: TList; TempCandle: TCachedCandle; LogTimePerCandleStr: string; - CurrentTick: TTickData; + CurrentTick: TAskBid.TTick; begin if AMemoForLogging <> nil then AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - TOHLC.Build: TF Secs: %d...', @@ -217,14 +216,14 @@ begin Exit; end; - FGlobalMinY := ATickData[0].Ask; - FGlobalMaxY := ATickData[0].Ask; + FGlobalMinY := ATickData[0].Data.Ask; // Changed + FGlobalMaxY := ATickData[0].Data.Ask; // Changed 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; + if ATickData[I].Data.Ask < FGlobalMinY then // Changed + FGlobalMinY := ATickData[I].Data.Ask; // Changed + if ATickData[I].Data.Ask > FGlobalMaxY then // Changed + FGlobalMaxY := ATickData[I].Data.Ask; // Changed end; if FGlobalMaxY = FGlobalMinY then begin @@ -257,10 +256,10 @@ begin if NumDataPoints > 0 then begin DataIndexStart := 0; - OpenPrice := ATickData[0].Ask; - HighPrice := ATickData[0].Ask; - LowPrice := ATickData[0].Ask; - ClosePrice := ATickData[0].Ask; + OpenPrice := ATickData[0].Data.Ask; // Changed + HighPrice := ATickData[0].Data.Ask; // Changed + LowPrice := ATickData[0].Data.Ask; // Changed + ClosePrice := ATickData[0].Data.Ask; // Changed for I := 0 to NumDataPoints - 1 do begin @@ -272,7 +271,7 @@ begin 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.ClosePrice := ATickData[I - 1].Data.Ask; // Close of previous candle is the tick before the new bar starts // Changed TempCandle.OriginalDataIndexStart := DataIndexStart; TempCandle.OriginalDataIndexEnd := I - 1; TempCandle.TickVolume := (I - 1) - DataIndexStart + 1; @@ -280,27 +279,27 @@ begin // Current tick 'I' starts a new candle DataIndexStart := I; - OpenPrice := CurrentTick.Ask; - HighPrice := CurrentTick.Ask; - LowPrice := CurrentTick.Ask; - ClosePrice := CurrentTick.Ask; + OpenPrice := CurrentTick.Data.Ask; // Changed + HighPrice := CurrentTick.Data.Ask; // Changed + LowPrice := CurrentTick.Data.Ask; // Changed + ClosePrice := CurrentTick.Data.Ask; // Changed 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 + OpenPrice := CurrentTick.Data.Ask; // Open is set // Changed + HighPrice := CurrentTick.Data.Ask; // Initial High // Changed + LowPrice := CurrentTick.Data.Ask; // Initial Low // Changed 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; + if CurrentTick.Data.Ask > HighPrice then // Changed + HighPrice := CurrentTick.Data.Ask; // Changed + if CurrentTick.Data.Ask < LowPrice then // Changed + LowPrice := CurrentTick.Data.Ask; // Changed end; - ClosePrice := CurrentTick.Ask; // Always update close to current tick + ClosePrice := CurrentTick.Data.Ask; // Always update close to current tick // Changed end; // If this is the last tick in the dataset, finalize the current candle @@ -360,11 +359,11 @@ begin 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); + var AvgCandleDurationDaysVal_A := TotalDataDurationDaysVal_A / NumCandlesToCacheAuto; + LogTimePerCandleStr := FormatTimeSpanFromSeconds(AvgCandleDurationDaysVal_A * SecsPerDay_Double); end else - LogTimePerCandleStr := 'N/A (Auto)'; + LogTimePerCandleStr := 'N/A (Auto)'; end; for I := 0 to NumCandlesToCacheAuto - 1 do begin @@ -379,15 +378,15 @@ begin Continue; end; - OpenPrice := ATickData[DataIndexStart].Ask; - ClosePrice := ATickData[DataIndexEnd].Ask; + OpenPrice := ATickData[DataIndexStart].Data.Ask; // Changed + ClosePrice := ATickData[DataIndexEnd].Data.Ask; // Changed 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; + if ATickData[J].Data.Ask > HighPrice then HighPrice := ATickData[J].Data.Ask; // Changed + if ATickData[J].Data.Ask < LowPrice then LowPrice := ATickData[J].Data.Ask; // Changed end; // Ensure High/Low encompass Open/Close after loop HighPrice := Max(HighPrice, Max(OpenPrice, ClosePrice)); @@ -409,10 +408,10 @@ begin 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].OpenPrice := ATickData[I].Data.Ask; // Changed + FCachedCandles[I].HighPrice := ATickData[I].Data.Ask; // Changed + FCachedCandles[I].LowPrice := ATickData[I].Data.Ask; // Changed + FCachedCandles[I].ClosePrice := ATickData[I].Data.Ask; // Changed FCachedCandles[I].OriginalDataIndexStart := I; FCachedCandles[I].OriginalDataIndexEnd := I; FCachedCandles[I].TickVolume := 1; @@ -438,10 +437,10 @@ begin 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 + OpenPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed + HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed + LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed + ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Initialize close with open // Changed DataIndexEnd := CurrentTickIndex; // Initialize end with start // Iterate through ticks that fall into the current bucket @@ -449,9 +448,9 @@ begin (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 + if ATickData[CurrentTickIndex].Data.Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed + if ATickData[CurrentTickIndex].Data.Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed + ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Continuously update close price // Changed DataIndexEnd := CurrentTickIndex; // Mark the last tick included in this candle Inc(CurrentTickIndex); end; @@ -465,8 +464,8 @@ begin // 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. + 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. diff --git a/common/Myc.TickLoader.pas b/common/Myc.TickLoader.pas deleted file mode 100644 index 6766a99..0000000 --- a/common/Myc.TickLoader.pas +++ /dev/null @@ -1,454 +0,0 @@ -unit Myc.TickLoader; - -(* ------------------------------------------------------------------------------ - Unit:         Myc.TickLoader - Author:       Delphi Algo Trading (Assistant) - Date:         May 27, 2025 - Description: - This unit provides functions to load tick data (TTickData) from - binary files. The data originates from a C# application - and has the structure OADateTime (Double), Ask (Single), and Bid (Single). - - The files follow the naming convention: - For .tab files:      Symbol_Year_MM.tab OR Symbol_Year_MM.tab_zip - For .tab-live files: Symbol_Year_MM.tab-live (NEVER compressed) - (MM represents the two-digit month, e.g., 01 for January, 12 for December) - - If both compressed (.tab_zip) and uncompressed (.tab) versions of a .tab - file exist, the compressed version is preferred. Files ending with _zip - are expected to be ZIP archives containing the actual data file - (e.g., Symbol_Year_MM.tab). - - Loading occurs in two phases: - 1. All .tab files (regular or _zip) are processed chronologically by - month and year. Unique ticks are loaded, and the timestamp of the - latest tick across all .tab files is determined (overallLastTabTickTime). - Assumes timestamps are unique across all .tab files. - 2. All .tab-live files (which are never compressed), for months/years - corresponding to found .tab files, are processed. Ticks from a - .tab-live file are only integrated if their timestamp is strictly later - than overallLastTabTickTime. Assumes such ticks are globally unique. - - If a .tab-live file results in no new data being added under these rules, - it is deleted after processing. - - The loading function automatically detects subsequent monthly/yearly files. - - Main Function: - LoadTickDataSeries - Loads a series of tick data files. - ------------------------------------------------------------------------------ *) - -interface - -uses - System.SysUtils, - System.Classes, - System.Generics.Collections, - System.IOUtils, - Myc.Futures, // Added for TFuture in ReadFileTickData - Myc.Signals; // Added for IMycState (dependency of TFuture) - -type - TTickData = packed record - OADateTime: Double; - Ask: Single; - Bid: Single; - end; - - PTTickData = ^TTickData; - - // TryParseFileName parses filenames like "Symbol_Year_MM.Extension" or "Symbol_Year_MM.Extension_zip" - // to extract Path, Symbol, Year, and Month. -function TryParseFileName( const FileName: string; - out PathValue, SymbolValue: string; - out YearValue, MonthValue: Integer ): Boolean; - -function LoadTickDataSeries(const FileName: string): TFuture>; - -// Asynchronously reads tick data from a single specified file. -// Returns a TFuture that will provide an array of TTickData. -function ReadFileTickData( var LoadGate: TLatch; const FileName: string ): TFuture>; - -implementation - -uses - System.Zip, - // Myc.Futures, Myc.Signals already in interface uses for TFuture type visibility - Myc.TaskManager; // Added for the global TaskManager used by TFuture.Construct - -function TryParseFileName( const FileName: string; - out PathValue, SymbolValue: string; - out YearValue, MonthValue: Integer ): Boolean; -var - fileNameNoPath: string; - nameForParsing: string; - parts: TArray; -begin - Result := False; // Default to failure - PathValue := ''; // Initialize all out parameters - SymbolValue := ''; - YearValue := 0; - MonthValue := 0; - - PathValue := TPath.GetDirectoryName( FileName ); - fileNameNoPath := TPath.GetFileName( FileName ); - - nameForParsing := fileNameNoPath; - if nameForParsing.EndsWith( '_zip', True ) then - begin - nameForParsing := nameForParsing.Substring( 0, nameForParsing.Length - 4 ); - end; - - nameForParsing := TPath.GetFileNameWithoutExtension( nameForParsing ); // e.g., "EURUSD_2023_01" - - parts := nameForParsing.Split( ['_'] ); - - if Length( parts ) < 3 then // Need at least Symbol_Year_Month - begin - exit; - end; - - // Month is the last part. - if not TryStrToInt( parts[High( parts )], MonthValue ) then - begin - exit; - end; - if ( MonthValue < 1 ) or ( MonthValue > 12 ) then // Validate month range - begin - MonthValue := 0; // Set to invalid if out of range - exit; - end; - - // Year is the second to last part. - if not TryStrToInt( parts[High( parts ) - 1], YearValue ) then - begin - MonthValue := 0; // Also invalidate month if year parsing fails - exit; - end; - - // Symbol consists of all parts before Year and Month. Count = Length(parts) - 2 - SymbolValue := string.Join( '_', Copy( parts, 0, Length( parts ) - 2 ) ); - - if SymbolValue = '' then // Ensure symbol is not empty - begin - exit; - end; - - Result := True; -end; - -function LoadTickDataSeries(const FileName: string): TFuture>; -// Local type declaration for storing file information for phase 1 processing -type - TPhase1FileEntry = record - Path: string; - Year: Integer; - Month: Integer; - end; -var - initialYear, initialMonth: Integer; - currentTabYear, currentTabMonth: Integer; - pathName, symbolName: string; - tabFileEntryRecord: TPhase1FileEntry; // Variable to construct record instances - loadedState: TState; - loadedFiles: TArray>>; - liveData: TFuture>; - -begin // Start of LoadTickDataSeries - if not TryParseFileName( FileName, pathName, symbolName, initialYear, initialMonth ) then - begin - raise EArgumentException.CreateFmt( - 'Invalid initial file name format: %s. Expected Symbol_Year_MM.Extension or Symbol_Year_MM.Extension_zip', [FileName] ); - end; - - currentTabYear := initialYear; - currentTabMonth := initialMonth; - var tabBaseNameFormat: string; - var compressedTabPath, uncompressedTabPath: string; - var actualTabFileToLoad: string; - var maxTabYearFound := -1; - var maxTabMonthFound := -1; - - var filesToLoadPhase1 := TList.Create; // Create list with the named record type - try - while True do - begin - tabBaseNameFormat := Format( '%s_%.4d_%.2d.tab', [symbolName, currentTabYear, currentTabMonth] ); - compressedTabPath := TPath.Combine( pathName, tabBaseNameFormat + '_zip' ); - uncompressedTabPath := TPath.Combine( pathName, tabBaseNameFormat ); - - actualTabFileToLoad := ''; - if TFile.Exists( compressedTabPath ) then - begin - actualTabFileToLoad := compressedTabPath; - end - else if TFile.Exists( uncompressedTabPath ) then - begin - actualTabFileToLoad := uncompressedTabPath; - end; - - if actualTabFileToLoad = '' then - break; // No more sequential .tab files - - // Construct the record and add it to the list - tabFileEntryRecord.Path := actualTabFileToLoad; - tabFileEntryRecord.Year := currentTabYear; - tabFileEntryRecord.Month := currentTabMonth; - filesToLoadPhase1.Add( tabFileEntryRecord ); - - // Advance to the next month - Inc( currentTabMonth ); - if currentTabMonth > 12 then - begin - currentTabMonth := 1; - Inc( currentTabYear ); - end; - end; - - // Now load and process identified .tab files - var loadedFileList := TList>>.Create; - var loadStates := TList.Create; - try - var LoadGate: TLatch; - - for var tabFileEntry: TPhase1FileEntry in filesToLoadPhase1 do // Iterate with the correct type - begin - var data := ReadFileTickData( LoadGate, tabFileEntry.Path ); - - loadedFileList.Add( TFuture>.Create( data ) ); - loadStates.Add( data.Done ); - - if tabFileEntry.Year > maxTabYearFound then - begin - maxTabYearFound := tabFileEntry.Year; - maxTabMonthFound := tabFileEntry.Month; - end - else if ( tabFileEntry.Year = maxTabYearFound ) and ( tabFileEntry.Month > maxTabMonthFound ) then - begin - maxTabMonthFound := tabFileEntry.Month; - end; - end; - - var liveFileBaseName := Format( '%s_%d_%02d.tab-live', [symbolName, maxTabYearFound, maxTabMonthFound] ); - liveData := ReadFileTickData( LoadGate, TPath.Combine( pathName, liveFileBaseName ) ); - loadStates.Add( liveData.Done ); - loadedFileList.Add( liveData ); - - loadedState := TState.All( loadStates.ToArray ); - loadedFiles := loadedFileList.ToArray; - finally - loadStates.Free; - loadedFileList.Free; - end; - finally - filesToLoadPhase1.Free; - end; - - Result := TFuture>.Construct( - loadedState, - function: TArray - begin - var tickList := TList.Create; - try - var totalTicks := Length(liveData.Result); - var overallLastTabTickTime := 0.0; - - for var P in loadedFiles do - inc( totalTicks, Length(P.Result) ); - - tickList.Capacity := totalTicks; - - for var P in loadedFiles do - begin - if Length(P.Result) > 0 then - begin - inc( totalTicks, Length(P.Result) ); - - for var iterTickData in P.Result do - if iterTickData.OADateTime > overallLastTabTickTime then - begin - tickList.Add( iterTickData ); - overallLastTabTickTime := iterTickData.OADateTime; - end; - end; - end; - - Result := tickList.ToArray; - finally - tickList.Free; - end; - end ); -end; - -function LoadSingleFileTickDataFromStream(const InputStream: TStream): TArray; -var - fileSize: Int64; - recordCount: Integer; - bytesRead: Integer; -begin - // Initialize Result record - SetLength(Result, 0); - - InputStream.Position := 0; - - fileSize := InputStream.Size; - if fileSize = 0 then - exit; - - if (fileSize mod SizeOf(TTickData)) <> 0 then - raise EReadError.CreateFmt('File corrupted. Size %d is not a multiple of record size %d.', - [fileSize, SizeOf(TTickData)]); - - recordCount := fileSize div SizeOf(TTickData); - if recordCount > 0 then - begin - SetLength(Result, recordCount); - bytesRead := InputStream.Read(Result[0], fileSize); - if bytesRead <> fileSize then - raise EReadError.CreateFmt('Read error. Expected to read %d bytes, but actually read %d bytes.', - [fileSize, bytesRead]); - end; -end; - -function LoadUncompressedSingleFileTickData(const FullFileName: string): TArray; -var - inputStream: TStream; -begin - // Initialize Result record - SetLength(Result, 0); - - if not TFile.Exists(FullFileName) then - begin - exit; - end; - - inputStream := TFileStream.Create(FullFileName, fmOpenRead or fmShareDenyWrite); - try - try - Result := LoadSingleFileTickDataFromStream( inputStream ); - - except - on E: EReadError do - begin - raise EReadError.CreateFmt('File %s: %s', [FullFileName, E.Message]); - end; - end; - finally - inputStream.Free; - end; -end; - -function UncompressTickDataFromStream(const InputStream: TStream): TArray; -var - decompressionStream: TStream; // Holds uncompressed data of a single zip entry - localHeader: TZipHeader; - entryIndex: Integer; - i: Integer; - zipFileInstance: TZipFile; -begin - // Initialize Result record - SetLength(Result, 0); - - decompressionStream := nil; - zipFileInstance := nil; - - try - InputStream.Position := 0; // Reset stream position to the beginning for TZipFile - - // Open TZipFile from this memory stream - zipFileInstance := TZipFile.Create; - zipFileInstance.Open(InputStream, TZipMode.zmRead); // Open the zip from the memory stream - - if zipFileInstance.FileCount = 0 then - begin - // If TZipFile opens an empty or invalid stream successfully but finds no files - exit; // FileSuccessfullyLoaded remains False - end; - - entryIndex := -1; - for i := 0 to zipFileInstance.FileCount - 1 do - begin - if zipFileInstance.FileNames[i].EndsWith('.tab', True) then - begin - entryIndex := i; - break; - end; - end; - - if entryIndex = -1 then // If specific name not found, default to the first entry - begin - if zipFileInstance.FileCount > 0 then - entryIndex := 0 - else - exit; // Should not happen if FileCount > 0 check passed - end; - - // Decompress the specific entry from the in-memory zip file. - // TZipFile.Read creates and populates decompressionStream with uncompressed data. - zipFileInstance.Read(entryIndex, decompressionStream, localHeader); - - if not Assigned(decompressionStream) then - exit; // Exit if no valid stream could be prepared - - Result := LoadSingleFileTickDataFromStream( decompressionStream ); - finally - // Ensure all potentially created objects are freed - if Assigned(decompressionStream) then // Holds uncompressed data from a zip entry - decompressionStream.Free; - if Assigned(zipFileInstance) then - zipFileInstance.Free; - end; -end; - -function ReadFileTickData( var LoadGate: TLatch; const FileName: string ): TFuture>; -var - parsedPath, parsedSymbol: string; - parsedYear, parsedMonth: Integer; -begin - Result := TFuture>.Null; - - // Attempt to parse year and month from the filename - if not TryParseFileName( FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth ) then - begin - // If filename parsing fails, return a future that resolves to an empty array. - exit; - end; - - if FileName.EndsWith('_zip', True) then - begin - var zipFileBytes := TFile.ReadAllBytes(FileName); // Read all bytes of the .zip file - if Length(zipFileBytes) = 0 then // Handle empty zip file case - exit; - - var capFileName := FileName; - var blob := TFuture.Construct( - TLatch.Enqueue( LoadGate ), - function: TBytes - begin - Result := TFile.ReadAllBytes(capFileName); // Read all bytes of the .zip file - end ); - - Result := blob.Chain>( - function( bytes: TBytes ): TArray - begin - var zipMemoryStream := TBytesStream.Create( zipFileBytes ); // Create a memory stream to hold the zip file content - try - zipMemoryStream.Position := 0; - Result := UncompressTickDataFromStream( zipMemoryStream ); - finally - zipMemoryStream.Free; - end; - end ); - end - else - begin - var capFileName := FileName; - Result := TFuture>.Construct( - function: TArray - begin - Result := LoadUncompressedSingleFileTickData( capFileName ); - end ); - end; -end; - -end.