Files
cTrader-DataExport/ChartDisplay/ChartDisplayMain.pas
T
Michael Schimmel 06ec0db105 1st commit
2025-05-23 12:51:32 +02:00

1221 lines
54 KiB
ObjectPascal

unit ChartDisplayMain;
// ------------------------------------------------------------------------------
// Unit Name: ChartDisplayMain
// Author: Delphi Algo Trading Assistant
// Date: May 22, 2025
// 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.
// ------------------------------------------------------------------------------
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
Myc.OHLCCache, // For TOHLC, IGenericCandleBuilder, TF_ constants
Myc.ChartDataController, // Uses the updated DataController
Vcl.Buttons;
type
// TTimeframeSetting is defined in Myc.ChartDataController.pas
// IGenericCandleBuilder is defined in Myc.OHLCCache.pas
TChartForm = class( TForm )
FileNameEdit: TEdit;
LoadButton: TButton;
Memo: TMemo;
RefreshButton: TButton;
TimeframeComboBox: TComboBox;
GoToCurrentButton: TButton;
FileSelectButton: 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 );
private
FActiveOHLCCache: TOHLC;
FCandleWidthInPixels: Integer;
FCandleSpacing: Integer;
FDisplayStartIndex: Integer;
FIsDragging: Boolean;
FDragStartX: Integer;
FDragStartDisplayIndex: Integer;
FSelectedTimeframeSeconds: Int64; // Stores the .Seconds value of the currently selected timeframe setting
FTimeframeSettings: TArray<Myc.ChartDataController.TTimeframeSetting>; // Local copy of all available timeframe settings
FCurrentPriceRect: TRect;
FCurrentVolumeHistRect: TRect;
FMouseLastChartPos: TPoint;
FMouseCurrentlyInChartArea: Boolean;
FBufferBitmap: TBitmap;
FDataController: TChartDataController;
procedure PopulateTimeframeComboBox;
procedure EnsureCacheForCurrentTimeframe( ARebuildIfPresent: Boolean = False );
procedure ClearAllCachesInForm;
procedure DrawChart( ACanvas: TCanvas; ARect: TRect );
public
end;
var
ChartForm: TChartForm;
implementation
{$R *.dfm}
const
DefaultChartMargin = 20;
DojiPenWidth = 2;
InitialCandleWidth = 5;
InitialCandleSpacing = 2;
DefaultEveryNTicksCount = 20;
DefaultVolumeAccumulationThreshold = 10;
// --- Generic Candle Builder Implementations ---
type
TEveryNTicksBuilder = class( TInterfacedObject, IGenericCandleBuilder )
private
FTickCountForNewBar: Integer;
FDescriptionText: string; // User-friendly description
public
constructor Create( ATickCount: Integer );
function Init( const ADataSet: TArray<TTickData> ): Boolean;
function IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean;
function GetCaption: string; // Returns the Description for UI
property Caption: string read GetCaption; // Interface property uses GetCaption
end;
TVolumeAccumulationBuilder = class( TInterfacedObject, IGenericCandleBuilder )
private
FTargetTickVolumePerBar: Integer;
FCurrentTickInBarCount: Integer;
FDescriptionText: string; // User-friendly description
public
constructor Create( ATargetTickVolume: Integer );
function Init( const ADataSet: TArray<TTickData> ): Boolean;
function IsNewBar( AIndex: Integer; const ATick: TTickData ): 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 );
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] );
end;
function TEveryNTicksBuilder.Init( const ADataSet: TArray<TTickData> ): Boolean;
begin
Result := True;
end;
function TEveryNTicksBuilder.IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean;
begin
Result := ( AIndex mod FTickCountForNewBar = 0 );
end;
function TEveryNTicksBuilder.GetCaption: string;
begin
Result := FDescriptionText; // This is what TChartDataController gets if it calls ABuilder.Caption
end;
{ TVolumeAccumulationBuilder }
constructor TVolumeAccumulationBuilder.Create( ATargetTickVolume: Integer );
begin
inherited Create;
FTargetTickVolumePerBar := ATargetTickVolume;
if FTargetTickVolumePerBar <= 0 then
FTargetTickVolumePerBar := 1;
FDescriptionText := Format( 'Ticks/Bar: %d', [FTargetTickVolumePerBar] );
FCurrentTickInBarCount := 0;
end;
function TVolumeAccumulationBuilder.Init( const ADataSet: TArray<TTickData> ): Boolean;
begin
FCurrentTickInBarCount := 0;
Result := True;
end;
function TVolumeAccumulationBuilder.IsNewBar( AIndex: Integer; const ATick: TTickData ): Boolean;
begin
Inc( FCurrentTickInBarCount );
if FCurrentTickInBarCount >= FTargetTickVolumePerBar then
begin
FCurrentTickInBarCount := 0;
Result := True;
end
else
Result := False;
end;
function TVolumeAccumulationBuilder.GetCaption: string;
begin
Result := FDescriptionText; // This is what TChartDataController gets if it calls ABuilder.Caption
end;
{ TChartForm }
procedure TChartForm.PopulateTimeframeComboBox;
var
i: Integer;
DefaultItemIndex: Integer;
PreferredDefaultCaptionKey: string; // The short KEY caption for the preferred default
begin
TimeframeComboBox.Items.Clear;
if not Assigned( FDataController ) then
begin
Memo.Lines.Add( 'Error: DataController not initialized for PopulateTimeframeComboBox.' );
Exit;
end;
// Get all available timeframes, including base and registered generic ones
FTimeframeSettings := FDataController.GetAvailableTimeframes( );
DefaultItemIndex := -1;
PreferredDefaultCaptionKey := 'Auto'; // Short key for "Auto (Screen Fit)"
for i := Low( FTimeframeSettings ) to High( FTimeframeSettings ) do
begin
// Add the user-friendly Description to the ComboBox
TimeframeComboBox.Items.Add( FTimeframeSettings[i].Description );
// Check against the short Caption (key) for default selection
if CompareText( FTimeframeSettings[i].Caption, PreferredDefaultCaptionKey ) = 0 then
DefaultItemIndex := i;
end;
if TimeframeComboBox.Items.Count > 0 then
begin
if ( DefaultItemIndex = -1 ) then
DefaultItemIndex := 0; // Fallback to the first item
TimeframeComboBox.ItemIndex := DefaultItemIndex;
if ( TimeframeComboBox.ItemIndex >= 0 ) and ( TimeframeComboBox.ItemIndex < Length( FTimeframeSettings ) ) then
FSelectedTimeframeSeconds := FTimeframeSettings[TimeframeComboBox.ItemIndex].Seconds
else
FSelectedTimeframeSeconds := TF_AUTO_AGGREGATION; // Should ideally not happen
end
else
begin
FSelectedTimeframeSeconds := TF_AUTO_AGGREGATION; // Default if no timeframes
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] ) )
else
Memo.Lines.Add( 'Timeframes populated, but ComboBox is empty. No default set.' );
end;
procedure TChartForm.FormCreate( Sender: TObject );
var
BuilderN: IGenericCandleBuilder;
BuilderV: IGenericCandleBuilder;
BuilderNCaptionKey, BuilderVCaptionKey: string; // These will be the unique keys
BuilderNDescription, BuilderVDescription: string;
begin
FCandleWidthInPixels := InitialCandleWidth;
FCandleSpacing := InitialCandleSpacing;
FDisplayStartIndex := 0;
FIsDragging := False;
FDragStartX := 0;
FDragStartDisplayIndex := 0;
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
BuilderNDescription := BuilderN.Caption; // BuilderN.Caption returns "Every 20 Ticks"
FDataController.RegisterGenericBuilder( BuilderNCaptionKey, BuilderNDescription, BuilderN );
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 );
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
begin // If ComboBox empty but settings exist (e.g. Populate didn't find default)
TimeframeComboBox.ItemIndex := 0;
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
end;
FActiveOHLCCache := nil;
FCurrentPriceRect := Rect( 0, 0, 0, 0 );
FCurrentVolumeHistRect := Rect( 0, 0, 0, 0 );
MouseHoverDataLabel.Caption := '';
FMouseLastChartPos := Point( -1, -1 );
FMouseCurrentlyInChartArea := False;
PaintBox.OnMouseLeave := PaintBoxMouseLeave;
FBufferBitmap := TBitmap.Create;
end;
procedure TChartForm.FormDestroy( Sender: TObject );
begin
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( );
FActiveOHLCCache := nil;
end;
procedure TChartForm.EnsureCacheForCurrentTimeframe( ARebuildIfPresent: Boolean = False );
var
Index: Integer;
EffectivePaintBoxClientWidthForAuto: Integer;
SelectedTFKeyCaption: string; // The short key caption (e.g., "M1", "NTICKS_20")
SelectedTFDescription: string; // The UI description (e.g., "1 Minute (M1)")
begin
Index := TimeframeComboBox.ItemIndex;
if ( Index < Low( FTimeframeSettings ) ) or ( Index > High( FTimeframeSettings ) ) then
begin
Memo.Lines.Add( 'EnsureCache: Invalid TF ComboBox Index: ' + IntToStr( Index ) );
FActiveOHLCCache := nil;
PaintBox.Invalidate;
Exit;
end;
if not Assigned( FDataController ) then
begin
Memo.Lines.Add( 'EnsureCache: FDataController not assigned.' );
FActiveOHLCCache := nil;
PaintBox.Invalidate;
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 );
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
);
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] ) );
end;
procedure TChartForm.TimeframeComboBoxChange( Sender: TObject );
var
CurrentTFKeyCaption: string;
begin
if TimeframeComboBox.ItemIndex < 0 then
begin
Memo.Lines.Add( 'TimeframeComboBoxChange: Invalid ItemIndex (-1).' );
Exit;
end;
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] ) );
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
FSelectedTimeframeSeconds := FTimeframeSettings[0].Seconds
else
FSelectedTimeframeSeconds := TF_AUTO_AGGREGATION;
end;
FDisplayStartIndex := 0;
EnsureCacheForCurrentTimeframe( True );
PaintBox.Invalidate;
end;
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;
end;
procedure TChartForm.RefreshButtonClick( Sender: TObject );
var
TargetStartTickIndex: Integer;
VisibleStartIndexInOldCache: Integer;
ActualNumVisibleCandlesOld: Integer;
ChartDrawWidth, OldDisplaySlotWidth, NumCandlesDrawableOld: Integer;
NewOptimalStartIndex, i: Integer;
begin
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
begin
VisibleStartIndexInOldCache := FDisplayStartIndex;
if FCurrentPriceRect.Width > 0 then
ChartDrawWidth := FCurrentPriceRect.Width
else
ChartDrawWidth := PaintBox.ClientWidth - ( 2 * DefaultChartMargin );
OldDisplaySlotWidth := FCandleWidthInPixels + FCandleSpacing;
NumCandlesDrawableOld := 0;
if OldDisplaySlotWidth > 0 then
NumCandlesDrawableOld := ChartDrawWidth div OldDisplaySlotWidth;
ActualNumVisibleCandlesOld := 0;
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] ) );
end
else
begin
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.' );
TargetStartTickIndex := 0;
end;
if Assigned( FDataController ) and FDataController.HasData then
begin
TargetStartTickIndex := Max( 0, TargetStartTickIndex );
if FDataController.GetTickDataLength > 0 then
TargetStartTickIndex := Min( TargetStartTickIndex, FDataController.GetTickDataLength - 1 )
else
TargetStartTickIndex := 0;
end
else
TargetStartTickIndex := -1;
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] ) );
EnsureCacheForCurrentTimeframe( True );
NewOptimalStartIndex := 0;
if Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) and
( TargetStartTickIndex >= 0 ) and Assigned( FDataController ) and FDataController.HasData then
begin
for i := 0 to FActiveOHLCCache.Count - 1 do
begin
if FActiveOHLCCache.Candles[i].OriginalDataIndexStart >= TargetStartTickIndex then
begin
NewOptimalStartIndex := i;
Break;
end
else if FActiveOHLCCache.Candles[i].OriginalDataIndexEnd >= TargetStartTickIndex then
begin
NewOptimalStartIndex := i;
Break;
end;
if i = FActiveOHLCCache.Count - 1 then
NewOptimalStartIndex := i;
end;
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
begin
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] ) );
end
else
begin
FDisplayStartIndex := 0;
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 );
var
OpenDialog: TOpenDialog;
InitialDirectory: string;
SelectedPath, SelectedSymbol: string;
SelectedYear: Integer;
IsValidSyntax: Boolean;
begin
OpenDialog := TOpenDialog.Create( Self );
try
OpenDialog.Title := 'Open Tick Data File';
OpenDialog.Filter := 'Tick Data Files (*.tab)|*.TAB|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 );
if not IsValidSyntax then
begin
MessageDlg( Format( 'Warning: The selected filename ''%s'' does not match the expected format (Prefix_Symbol_Year.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;
end;
end;
procedure TChartForm.PaintBoxMouseLeave( Sender: TObject );
begin
if FMouseCurrentlyInChartArea then
begin
FMouseCurrentlyInChartArea := False;
MouseHoverDataLabel.Caption := '';
PaintBox.Invalidate;
end;
FMouseLastChartPos := Point( -1, -1 );
end;
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 );
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
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] ) );
end
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] ) );
end;
if FMouseCurrentlyInChartArea then
PaintBox.Invalidate;
end;
end;
procedure TChartForm.PaintBoxMouseMove( Sender: TObject; Shift: TShiftState; X, Y: Integer );
var
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 );
if FIsDragging then
begin
DeltaX := X - FDragStartX;
CurrentFixedSlotWidth := FCandleWidthInPixels + FCandleSpacing;
if CurrentFixedSlotWidth > 0 then
begin
NumCandlesToScroll := Round( -DeltaX / CurrentFixedSlotWidth );
PrevDisplayStartIndex := FDisplayStartIndex;
FDisplayStartIndex := FDragStartDisplayIndex + NumCandlesToScroll;
if FCurrentPriceRect.Width > 0 then
ChartDrawWidthForDrag := FCurrentPriceRect.Width
else
ChartDrawWidthForDrag := PaintBox.ClientWidth - ( 2 * DefaultChartMargin );
DrawableCandlesInViewPort := Max( 1, ChartDrawWidthForDrag div CurrentFixedSlotWidth );
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 );
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] ) );
PaintBox.Invalidate;
end;
end;
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;
end;
NeedsInvalidate := False;
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;
if FMouseCurrentlyInChartArea then
NeedsInvalidate := True;
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;
CandleIndexInViewPort := RelativeX div CurrentDrawingSlotWidthForHover;
ActualCacheIndex := FDisplayStartIndex + CandleIndexInViewPort;
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
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 )
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] );
MouseHoverDataLabel.Caption := HoverInfo;
end
else
MouseHoverDataLabel.Caption := 'Candle (Original Tick Index Error)';
end
else
MouseHoverDataLabel.Caption := '';
end
else
MouseHoverDataLabel.Caption := '';
end
else
MouseHoverDataLabel.Caption := '';
if NeedsInvalidate then
PaintBox.Invalidate;
end;
procedure TChartForm.PaintBoxMouseUp( Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer );
begin
if Button = mbLeft then
begin
if FIsDragging then
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 );
end;
end;
end;
procedure TChartForm.PaintBoxPaint( Sender: TObject );
begin
if not Assigned( FBufferBitmap ) then
begin
Memo.Lines.Add( 'Error: FBufferBitmap not assigned in PaintBoxPaint.' );
Exit;
end;
if ( PaintBox.ClientWidth <= 0 ) or ( PaintBox.ClientHeight <= 0 ) then
begin
if Assigned( PaintBox.Canvas ) then
PaintBox.Canvas.FillRect( PaintBox.ClientRect );
Exit;
end;
if ( FBufferBitmap.Width <> PaintBox.ClientWidth ) or
( FBufferBitmap.Height <> PaintBox.ClientHeight ) then
begin
FBufferBitmap.SetSize( PaintBox.ClientWidth, PaintBox.ClientHeight );
end;
DrawChart( FBufferBitmap.Canvas, Rect( 0, 0, FBufferBitmap.Width, FBufferBitmap.Height ) );
PaintBox.Canvas.Draw( 0, 0, FBufferBitmap );
end;
procedure TChartForm.DrawChart( ACanvas: TCanvas; ARect: TRect );
const
DefaultVolumeAreaHeightRatio = 0.20;
MinVolumeAreaPixelHeight = 30;
MaxVolumeAreaPixelHeight = 80;
PriceToVolumeSeparatorPixels = 5;
CrosshairPriceTimeMargin = 3;
var
LoopIdx, CacheIdx: Integer;
ScaleY: Double;
Y_open, Y_high, Y_low, Y_close: Integer;
NumCachedCandlesInActiveCache: Integer;
CurrentDrawingSlotWidth: Integer;
ActualNumCandlesToDraw: Integer;
CurrentCandleXStart: Integer;
X_wick_center: Integer;
BodyRect: TRect;
CachedCandleRec: TCachedCandle;
NumCandlesDrawableInViewPort: Integer;
LocalMinYValue, LocalMaxYValue: Single;
VisibleCandleForScale: TCachedCandle;
k_scale: Integer;
CurrentMinYToUse, CurrentMaxYToUse: Single;
ExpansionValue: Single;
DrawableContentRect, PriceRect_Local, VolumeHistRect_Local: TRect;
ActualVolumeHistAreaHeight: Integer;
MaxVisibleTickVolume: Integer;
VolumeScaleY: Double;
VolumeBarHeight: Integer;
VolBarRect: TRect;
RelativeX, CandleIndexInViewPortForCrosshair, VerticalLineX: Integer;
ShowVolumeBars: Boolean;
PriceAtMouseY: Double;
PriceStr, TimeStr: string;
TextX, TextY: Integer;
CrosshairCandle: TCachedCandle;
UTCTimeStart, LocalTimeStart: TDateTime;
OriginalPenStyle: TPenStyle;
OriginalPenWidth: Integer;
OriginalFontColor: TColor;
OriginalBrushStyle: TBrushStyle;
OriginalFontSize: Integer; // For saving and restoring font size
begin
ACanvas.Brush.Color := clWhite;
ACanvas.FillRect( ARect );
FCurrentPriceRect := Rect( 0, 0, 0, 0 );
FCurrentVolumeHistRect := Rect( 0, 0, 0, 0 );
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;
end;
DrawableContentRect := Rect( ARect.Left + DefaultChartMargin, ARect.Top + DefaultChartMargin,
ARect.Right - DefaultChartMargin, ARect.Bottom - DefaultChartMargin );
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;
end;
ShowVolumeBars := ( FActiveOHLCCache.AggregationTimeframeSeconds <> TF_AUTO_AGGREGATION ) and
( FActiveOHLCCache.AggregationTimeframeSeconds <> TF_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
begin
ActualVolumeHistAreaHeight := Max( 0, DrawableContentRect.Height - PriceToVolumeSeparatorPixels - 50 );
if ActualVolumeHistAreaHeight < MinVolumeAreaPixelHeight then
ActualVolumeHistAreaHeight := 0;
end;
end;
if ActualVolumeHistAreaHeight <= 0 then
ShowVolumeBars := False;
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 );
end
else
begin // Corrected block for when ShowVolumeBars is false
PriceRect_Local := DrawableContentRect;
VolumeHistRect_Local := Rect( 0, 0, 0, 0 );
end;
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;
end;
FCurrentPriceRect := PriceRect_Local;
FCurrentVolumeHistRect := VolumeHistRect_Local;
NumCachedCandlesInActiveCache := FActiveOHLCCache.Count;
CurrentDrawingSlotWidth := FCandleWidthInPixels + FCandleSpacing;
if CurrentDrawingSlotWidth <= 0 then
Exit;
NumCandlesDrawableInViewPort := PriceRect_Local.Width div CurrentDrawingSlotWidth;
ActualNumCandlesToDraw := 0;
if ( FDisplayStartIndex >= 0 ) and ( FDisplayStartIndex < NumCachedCandlesInActiveCache ) then
ActualNumCandlesToDraw := Min( NumCandlesDrawableInViewPort, NumCachedCandlesInActiveCache - FDisplayStartIndex );
MaxVisibleTickVolume := 0;
if ActualNumCandlesToDraw > 0 then
begin
VisibleCandleForScale := FActiveOHLCCache.Candles[FDisplayStartIndex];
LocalMinYValue := VisibleCandleForScale.LowPrice;
LocalMaxYValue := VisibleCandleForScale.HighPrice;
if ShowVolumeBars then
MaxVisibleTickVolume := VisibleCandleForScale.TickVolume;
for k_scale := 1 to ActualNumCandlesToDraw - 1 do
begin
CacheIdx := FDisplayStartIndex + k_scale;
VisibleCandleForScale := FActiveOHLCCache.Candles[CacheIdx];
if VisibleCandleForScale.LowPrice < LocalMinYValue then
LocalMinYValue := VisibleCandleForScale.LowPrice;
if VisibleCandleForScale.HighPrice > LocalMaxYValue then
LocalMaxYValue := VisibleCandleForScale.HighPrice;
if ShowVolumeBars and ( VisibleCandleForScale.TickVolume > MaxVisibleTickVolume ) then
MaxVisibleTickVolume := VisibleCandleForScale.TickVolume;
end;
if Abs( LocalMaxYValue - LocalMinYValue ) < 1E-6 then
begin
ExpansionValue := Max( Abs( LocalMinYValue ) * 0.001, 0.0001 );
LocalMaxYValue := LocalMinYValue + ExpansionValue;
LocalMinYValue := LocalMinYValue - ExpansionValue;
end;
CurrentMinYToUse := LocalMinYValue;
CurrentMaxYToUse := LocalMaxYValue;
if ShowVolumeBars and ( MaxVisibleTickVolume = 0 ) then
MaxVisibleTickVolume := 1;
end
else
begin
CurrentMinYToUse := FActiveOHLCCache.GlobalMinY;
CurrentMaxYToUse := FActiveOHLCCache.GlobalMaxY;
if Abs( CurrentMaxYToUse - CurrentMinYToUse ) < 1E-6 then
begin
CurrentMaxYToUse := CurrentMinYToUse + 0.0002;
CurrentMinYToUse := CurrentMinYToUse - 0.0002;
end;
end;
OriginalFontSize := ACanvas.Font.Size; // Save original font size
ACanvas.Font.Size := 8;
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.Font.Size := OriginalFontSize; // Restore original font size
if Abs( CurrentMaxYToUse - CurrentMinYToUse ) < 1E-7 then
ScaleY := 1.0
else
ScaleY := PriceRect_Local.Height / ( CurrentMaxYToUse - CurrentMinYToUse );
if ScaleY <= 0 then
ScaleY := 1.0;
VolumeScaleY := 0;
if ShowVolumeBars and ( VolumeHistRect_Local.Height > 0 ) and ( MaxVisibleTickVolume > 0 ) then
VolumeScaleY := VolumeHistRect_Local.Height / MaxVisibleTickVolume;
CurrentCandleXStart := PriceRect_Local.Left;
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 );
ACanvas.Pen.Color := clBlack;
ACanvas.Pen.Width := 1;
ACanvas.MoveTo( X_wick_center, Y_high );
ACanvas.LineTo( X_wick_center, Y_low );
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
begin
ACanvas.Pen.Width := DojiPenWidth;
ACanvas.MoveTo( BodyRect.Left, Y_open );
ACanvas.LineTo( BodyRect.Right, Y_open );
ACanvas.Pen.Width := 1;
end
else
begin
if BodyRect.Bottom <= BodyRect.Top then
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 );
end;
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 );
if CachedCandleRec.ClosePrice > CachedCandleRec.OpenPrice then
ACanvas.Brush.Color := TColor( $00B0F0B0 )
else if CachedCandleRec.ClosePrice < CachedCandleRec.OpenPrice then
ACanvas.Brush.Color := TColor( $00B0B0F0 )
else
ACanvas.Brush.Color := clSilver;
ACanvas.Pen.Color := clGray;
ACanvas.Rectangle( VolBarRect );
end;
CurrentCandleXStart := CurrentCandleXStart + CurrentDrawingSlotWidth;
end;
end;
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 );
if FMouseCurrentlyInChartArea and Assigned( FActiveOHLCCache ) and FActiveOHLCCache.IsValid and ( FActiveOHLCCache.Count > 0 ) and
( FMouseLastChartPos.X >= 0 ) and Assigned( FDataController ) and FDataController.HasData then
begin
OriginalPenStyle := ACanvas.Pen.Style;
OriginalPenWidth := ACanvas.Pen.Width;
OriginalFontColor := ACanvas.Font.Color;
OriginalBrushStyle := ACanvas.Brush.Style;
OriginalFontSize := ACanvas.Font.Size;
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
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 );
end;
if CurrentDrawingSlotWidth > 0 then
begin
RelativeX := FMouseLastChartPos.X - PriceRect_Local.Left;
if RelativeX >= 0 then
begin
CandleIndexInViewPortForCrosshair := RelativeX div CurrentDrawingSlotWidth;
VerticalLineX := PriceRect_Local.Left + ( CandleIndexInViewPortForCrosshair * CurrentDrawingSlotWidth ) +
( FCandleWidthInPixels div 2 );
if ( VerticalLineX >= DrawableContentRect.Left ) and ( VerticalLineX <= DrawableContentRect.Right ) then
begin
ACanvas.MoveTo( VerticalLineX, DrawableContentRect.Top );
ACanvas.LineTo( VerticalLineX, DrawableContentRect.Bottom );
var
ActualCacheIdxForCrosshair := FDisplayStartIndex + CandleIndexInViewPortForCrosshair;
if ( ActualCacheIdxForCrosshair >= 0 ) and ( ActualCacheIdxForCrosshair < FActiveOHLCCache.Count ) then
begin
CrosshairCandle := FActiveOHLCCache.Candles[ActualCacheIdxForCrosshair];
if ( CrosshairCandle.OriginalDataIndexStart >= 0 ) and
( CrosshairCandle.OriginalDataIndexStart < FDataController.GetTickDataLength ) then
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
TextY := ARect.Top;
ACanvas.TextOut( TextX, TextY, TimeStr );
end;
end;
end;
end;
end;
ACanvas.Pen.Style := OriginalPenStyle;
ACanvas.Pen.Width := OriginalPenWidth;
ACanvas.Font.Color := OriginalFontColor;
ACanvas.Brush.Style := OriginalBrushStyle;
ACanvas.Font.Size := OriginalFontSize;
end;
end;
procedure TChartForm.FormMouseWheel( Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean );
const
MinDisplayCandleWidth = 1;
MaxDisplayCandleWidth = 100;
ZoomStep = 1;
var
LMousePosInPaintBox: TPoint;
OldCandleWidthInPixels, OriginalFDisplayStartIndex: Integer;
MouseX_InChartArea_Relative: Integer;
OldSlotWidth, NewSlotWidth: Integer;
GlobalCandleIndexUnderMouse_Float: Double;
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 );
OriginalFDisplayStartIndex := FDisplayStartIndex;
OldCandleWidthInPixels := FCandleWidthInPixels;
if WheelDelta > 0 then
FCandleWidthInPixels := Min( MaxDisplayCandleWidth, FCandleWidthInPixels + ZoomStep )
else if WheelDelta < 0 then
FCandleWidthInPixels := Max( MinDisplayCandleWidth, FCandleWidthInPixels - ZoomStep )
else
Exit;
if FCandleWidthInPixels = OldCandleWidthInPixels then
Exit;
NewSlotWidth := FCandleWidthInPixels + FCandleSpacing;
if NewSlotWidth <= 0 then
begin
FCandleWidthInPixels := OldCandleWidthInPixels;
Exit;
end;
if ZoomToCursorActive then
begin
OldSlotWidth := OldCandleWidthInPixels + FCandleSpacing;
if OldSlotWidth <= 0 then
ZoomToCursorActive := False
else
begin
MouseX_InChartArea_Relative := LMousePosInPaintBox.X - FCurrentPriceRect.Left;
GlobalCandleIndexUnderMouse_Float := OriginalFDisplayStartIndex + ( MouseX_InChartArea_Relative / OldSlotWidth );
FDisplayStartIndex := Round( GlobalCandleIndexUnderMouse_Float - ( MouseX_InChartArea_Relative / NewSlotWidth ) );
end;
end;
if FCurrentPriceRect.Width > 0 then
ChartDrawWidth := FCurrentPriceRect.Width
else
ChartDrawWidth := PaintBox.ClientWidth - ( 2 * DefaultChartMargin );
DrawableCandlesInViewPort := Max( 1, ChartDrawWidth div NewSlotWidth );
MaxPossibleStartIndex := 0;
if Assigned( FActiveOHLCCache ) and ( FActiveOHLCCache.Count > 0 ) then
MaxPossibleStartIndex := FActiveOHLCCache.Count - DrawableCandlesInViewPort;
if MaxPossibleStartIndex < 0 then
MaxPossibleStartIndex := 0;
FDisplayStartIndex := Max( 0, FDisplayStartIndex );
FDisplayStartIndex := Min( FDisplayStartIndex, MaxPossibleStartIndex );
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 );
end;
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
begin
Memo.Lines.Add( 'GoToCurrent: No data or cache invalid.' );
Exit;
end;
if FCurrentPriceRect.Width > 0 then
ChartDrawWidth := FCurrentPriceRect.Width
else
ChartDrawWidth := PaintBox.ClientWidth - ( 2 * DefaultChartMargin );
CurrentDrawingSlotWidth := FCandleWidthInPixels + FCandleSpacing;
if CurrentDrawingSlotWidth <= 0 then
begin
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 );
if FDisplayStartIndex <> NewDisplayStartIndex then
begin
FDisplayStartIndex := NewDisplayStartIndex;
Memo.Lines.Add( Format( 'GoToCurrent: Scrolled to display last candles. New StartIdx: %d', [FDisplayStartIndex] ) );
PaintBox.Invalidate;
end
else
Memo.Lines.Add( 'GoToCurrent: Already showing the latest candles.' );
end;
end.