1222 lines
54 KiB
ObjectPascal
1222 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;
|
|
SelectedMonth: Integer;
|
|
IsValidSyntax: Boolean;
|
|
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;
|
|
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.
|