diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc293d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.dcu +*.local +*.identcache +*.exe +*.rsm +__history +__recovery +Win32 +Win64 \ No newline at end of file diff --git a/ChartDisplay/ChartDisplayMain.dfm b/ChartDisplay/ChartDisplayMain.dfm new file mode 100644 index 0000000..d65acd2 --- /dev/null +++ b/ChartDisplay/ChartDisplayMain.dfm @@ -0,0 +1,121 @@ +object ChartForm: TChartForm + Left = 0 + Top = 0 + Caption = 'ChartForm' + ClientHeight = 835 + ClientWidth = 900 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -12 + Font.Name = 'Segoe UI' + Font.Style = [] + OnCreate = FormCreate + OnMouseWheel = FormMouseWheel + DesignSize = ( + 900 + 835) + TextHeight = 15 + object FileSelectButton: TSpeedButton + Left = 647 + Top = 17 + Width = 23 + Height = 22 + Anchors = [akTop, akRight] + Caption = '...' + OnClick = FileSelectButtonClick + end + object MouseHoverDataLabel: TLabel + Left = 32 + Top = 565 + Width = 593 + Height = 15 + Anchors = [akLeft, akRight, akBottom] + AutoSize = False + ExplicitTop = 359 + end + object FileNameEdit: TEdit + Left = 32 + Top = 17 + Width = 609 + Height = 23 + Anchors = [akLeft, akTop, akRight] + TabOrder = 0 + Text = + 'C:\Users\Brummel\Documents\cAlgo\TickData\Pepperstone\EURUSD_201' + + '7.tab' + end + object LoadButton: TButton + Left = 686 + Top = 16 + Width = 75 + Height = 25 + Anchors = [akTop, akRight] + Caption = 'Load' + TabOrder = 1 + OnClick = LoadButtonClick + end + object Memo: TMemo + Left = 32 + Top = 606 + Width = 817 + Height = 221 + Anchors = [akLeft, akRight, akBottom] + ScrollBars = ssVertical + TabOrder = 2 + end + object RefreshButton: TButton + Left = 774 + Top = 16 + Width = 75 + Height = 25 + Anchors = [akTop, akRight] + Caption = 'Refresh' + TabOrder = 3 + OnClick = RefreshButtonClick + end + object TimeframeComboBox: TComboBox + Left = 649 + Top = 562 + Width = 145 + Height = 23 + Anchors = [akRight, akBottom] + TabOrder = 4 + OnChange = TimeframeComboBoxChange + end + object GoToCurrentButton: TButton + Left = 800 + Top = 561 + Width = 49 + Height = 25 + Anchors = [akRight, akBottom] + Caption = '-->' + TabOrder = 5 + OnClick = GoToCurrentButtonClick + end + object ChartScrollBox: TScrollBox + Left = 33 + Top = 47 + Width = 815 + Height = 496 + Anchors = [akLeft, akTop, akRight, akBottom] + DoubleBuffered = True + ParentDoubleBuffered = False + TabOrder = 6 + object PaintBox: TPaintBox + Left = 0 + Top = 0 + Width = 811 + Height = 492 + Align = alClient + OnMouseDown = PaintBoxMouseDown + OnMouseMove = PaintBoxMouseMove + OnMouseUp = PaintBoxMouseUp + OnPaint = PaintBoxPaint + ExplicitLeft = 32 + ExplicitTop = 34 + ExplicitWidth = 377 + ExplicitHeight = 97 + end + end +end diff --git a/ChartDisplay/ChartDisplayMain.pas b/ChartDisplay/ChartDisplayMain.pas new file mode 100644 index 0000000..acc548f --- /dev/null +++ b/ChartDisplay/ChartDisplayMain.pas @@ -0,0 +1,1220 @@ +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; // 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 ): 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 ): 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 ): 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 ): 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. diff --git a/ChartDisplay/ChartDisplayProject.dpr b/ChartDisplay/ChartDisplayProject.dpr new file mode 100644 index 0000000..9d12255 --- /dev/null +++ b/ChartDisplay/ChartDisplayProject.dpr @@ -0,0 +1,16 @@ +program ChartDisplayProject; + +uses + Vcl.Forms, + ChartDisplayMain in 'ChartDisplayMain.pas' {ChartForm}, + Myc.OHLCCache in '..\common\Myc.OHLCCache.pas', + Myc.ChartDataController in '..\common\Myc.ChartDataController.pas'; + +{$R *.res} + +begin + Application.Initialize; + Application.MainFormOnTaskbar := True; + Application.CreateForm(TChartForm, ChartForm); + Application.Run; +end. diff --git a/ChartDisplay/ChartDisplayProject.dproj b/ChartDisplay/ChartDisplayProject.dproj new file mode 100644 index 0000000..80778da --- /dev/null +++ b/ChartDisplay/ChartDisplayProject.dproj @@ -0,0 +1,1143 @@ + + + {969CBF7A-035D-4583-9619-6E58BDF60C88} + 20.3 + VCL + True + Release + Win64 + ChartDisplayProject + 2 + Application + ChartDisplayProject.dpr + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + ChartDisplayProject + ..\common;$(DCC_UnitSearchPath) + 1031 + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + $(BDS)\bin\default_app.manifest + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + true + 1033 + $(BDS)\bin\default_app.manifest + + + DEBUG;$(DCC_Define) + true + false + true + true + true + true + true + + + false + PerMonitorV2 + true + 1033 + + + PerMonitorV2 + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + PerMonitorV2 + + + PerMonitorV2 + + + + MainSource + + +
ChartForm
+ dfm +
+ + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + +
+ + Delphi.Personality.12 + Application + + + + ChartDisplayProject.dpr + + + Embarcadero C++Builder Office 2000 Servers Package + Embarcadero C++Builder Office XP Servers Package + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + + + + + + ChartDisplayProject.exe + true + + + + + ChartDisplayProject.exe + true + + + + + ChartDisplayProject.rsm + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\values-v35 + 1 + + + res\values-v35 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + + False + True + + + 12 + + + + +
diff --git a/ChartDisplay/ChartDisplayProject.res b/ChartDisplay/ChartDisplayProject.res new file mode 100644 index 0000000..9fda4f1 Binary files /dev/null and b/ChartDisplay/ChartDisplayProject.res differ diff --git a/FIX-Connect/FIXConnectProject.dpr b/FIX-Connect/FIXConnectProject.dpr new file mode 100644 index 0000000..dbe2b28 --- /dev/null +++ b/FIX-Connect/FIXConnectProject.dpr @@ -0,0 +1,14 @@ +program FIXConnectProject; + +uses + Vcl.Forms, + MainUnit in 'MainUnit.pas' {Form1}; + +{$R *.res} + +begin + Application.Initialize; + Application.MainFormOnTaskbar := True; + Application.CreateForm(TForm1, Form1); + Application.Run; +end. diff --git a/FIX-Connect/FIXConnectProject.dproj b/FIX-Connect/FIXConnectProject.dproj new file mode 100644 index 0000000..c24eb00 --- /dev/null +++ b/FIX-Connect/FIXConnectProject.dproj @@ -0,0 +1,1125 @@ + + + {47B13125-8991-4584-9CFD-EF4C93BDAF21} + 20.3 + VCL + True + Debug + Win64 + FIXConnectProject + 3 + Application + FIXConnectProject.dpr + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + FIXConnectProject + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + $(BDS)\bin\default_app.manifest + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + $(BDS)\bin\default_app.manifest + + + DEBUG;$(DCC_Define) + true + false + true + true + true + true + true + + + false + PerMonitorV2 + + + PerMonitorV2 + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + PerMonitorV2 + + + PerMonitorV2 + + + + MainSource + + +
Form1
+ dfm +
+ + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + +
+ + Delphi.Personality.12 + Application + + + + FIXConnectProject.dpr + + + + + + FIXConnectProject.exe + true + + + + + FIXConnectProject.rsm + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\values-v35 + 1 + + + res\values-v35 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + + True + True + + + 12 + + + + +
diff --git a/FIX-Connect/FIXConnectProject.res b/FIX-Connect/FIXConnectProject.res new file mode 100644 index 0000000..f07804f Binary files /dev/null and b/FIX-Connect/FIXConnectProject.res differ diff --git a/FIX-Connect/MainUnit.dfm b/FIX-Connect/MainUnit.dfm new file mode 100644 index 0000000..03c322a --- /dev/null +++ b/FIX-Connect/MainUnit.dfm @@ -0,0 +1,88 @@ +object Form1: TForm1 + Left = 0 + Top = 0 + Caption = 'Form1' + ClientHeight = 633 + ClientWidth = 803 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -12 + Font.Name = 'Segoe UI' + Font.Style = [] + OnCreate = FormCreate + OnDestroy = FormDestroy + TextHeight = 15 + object Label1: TLabel + Left = 88 + Top = 11 + Width = 34 + Height = 15 + Caption = 'Label1' + end + object Label2: TLabel + Left = 88 + Top = 75 + Width = 34 + Height = 15 + Caption = 'Label2' + end + object Label3: TLabel + Left = 568 + Top = 80 + Width = 34 + Height = 15 + Caption = 'Label3' + end + object ConnectButton: TButton + Left = 88 + Top = 112 + Width = 137 + Height = 25 + Caption = 'Connect' + TabOrder = 0 + OnClick = ConnectButtonClick + end + object PasswordEdit: TEdit + Left = 160 + Top = 72 + Width = 121 + Height = 23 + TabOrder = 1 + Text = 'wBz687@6rXdKkNK' + end + object LogMemo: TMemo + Left = 88 + Top = 160 + Width = 513 + Height = 345 + Lines.Strings = ( + 'LogMemo') + TabOrder = 2 + end + object SenderCompIDEdit: TEdit + Left = 160 + Top = 8 + Width = 121 + Height = 23 + TabOrder = 3 + Text = 'demo.pepperstoneuk.4132560' + end + object DisconnectButton: TButton + Left = 231 + Top = 112 + Width = 137 + Height = 25 + Caption = 'Disconnect' + TabOrder = 4 + OnClick = DisconnectButtonClick + end + object UserIdEdit: TEdit + Left = 160 + Top = 43 + Width = 121 + Height = 23 + TabOrder = 5 + Text = '4132560' + end +end diff --git a/FIX-Connect/MainUnit.pas b/FIX-Connect/MainUnit.pas new file mode 100644 index 0000000..3c30cc1 --- /dev/null +++ b/FIX-Connect/MainUnit.pas @@ -0,0 +1,463 @@ +unit MainUnit; + +interface + +uses + Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, + Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, + IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient, IdGlobal, IdException, + System.DateUtils, System.StrUtils; + +const + FIX_SEPARATOR = #1; // SOH character for FIX messages + + // Broker specific connection details - SET TO DEMO & PLAIN TEXT + BROKER_HOST = 'demo-uk-eqx-01.p.c-trader.com'; + BROKER_PLAINTEXT_PORT = 5201; // Plain text port + + TARGET_COMP_ID_CONST = 'CSERVER'; + SENDER_SUB_ID_CONST = 'QUOTE'; // For Price Connection + // Based on your observation that "NO exception" occurs with 57=QUOTE + // Standard cTrader examples often use 'TRADE' here. + TARGET_SUB_ID_CONST = 'QUOTE'; // << ACHTUNG: Testwert basierend auf Ihrer letzten Beobachtung + +type + TForm1 = class(TForm) + ConnectButton: TButton; + PasswordEdit: TEdit; + LogMemo: TMemo; + Label1: TLabel; + SenderCompIDEdit: TEdit; + Label2: TLabel; + DisconnectButton: TButton; + UserIdEdit: TEdit; + Label3: TLabel; + procedure ConnectButtonClick(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure DisconnectButtonClick(Sender: TObject); + private + FIdTCPClient: TIdTCPClient; + FMsgSeqNum: Integer; + + function CreateFixTag(Tag: Integer; Value: string): string; + function CalculateChecksum(const MessageBody: string): string; + function GetUtcDateTimeString: string; + procedure Log(const Text: string); + procedure SendFixMessage(const MessageType: string; const AdditionalBodyFields: TStrings); + procedure SendMarketDataRequest(const Symbol: string; const MDReqID: string); // NEU + public + end; + +var + Form1: TForm1; + +implementation + +{$R *.dfm} + +function TForm1.CreateFixTag(Tag: Integer; Value: string): string; +begin + Result := IntToStr(Tag) + '=' + Value + FIX_SEPARATOR; +end; + +function TForm1.GetUtcDateTimeString: string; +var + nowUtc: TDateTime; +begin + nowUtc := TTimeZone.Local.ToUniversalTime(Now); + Result := FormatDateTime('yyyymmdd-hh:nn:ss.zzz', nowUtc); +end; + +function TForm1.CalculateChecksum(const MessageBody: string): string; +var + sum: Cardinal; + i: Integer; + checkSumVal: Integer; +begin + sum := 0; + for i := 1 to Length(MessageBody) do + begin + sum := sum + Ord(MessageBody[i]); + end; + checkSumVal := sum mod 256; + Result := Format('%.3d', [checkSumVal]); +end; + +procedure TForm1.Log(const Text: string); +begin + if Assigned(LogMemo) then + begin + LogMemo.Lines.Add(FormatDateTime('[hh:nn:ss.zzz] ', Time) + Text); + end; +end; + +procedure TForm1.FormCreate(Sender: TObject); +begin + FMsgSeqNum := 1; + Log('FormCreate: Initializing components for PLAIN TEXT connection.'); + + try + FIdTCPClient := TIdTCPClient.Create(nil); + Log('FormCreate: TCPClient created.'); + except + on E: Exception do + begin + Log('CRITICAL ERROR during FormCreate TCPClient creation: ' + E.Message); + ShowMessage('CRITICAL ERROR during FormCreate: ' + E.Message); + FIdTCPClient := nil; + Exit; + end; + end; + + if Assigned(FIdTCPClient) then + begin + FIdTCPClient.Host := BROKER_HOST; + FIdTCPClient.Port := BROKER_PLAINTEXT_PORT; + FIdTCPClient.ReadTimeout := 10000; // 10 Sekunden Timeout für Leseoperationen + FIdTCPClient.ConnectTimeout := 10000; + Log('FormCreate: TCPClient configured for PLAIN TEXT - Host: ' + FIdTCPClient.Host + ' Port: ' + IntToStr(FIdTCPClient.Port)); + end + else + begin + Log('FormCreate: FIdTCPClient is NIL after creation attempt. Network functions disabled.'); + end; + + // --- WICHTIG: DIESE MIT IHREN EXAKTEN DEMO-ZUGANGSDATEN ERSETZEN --- + SenderCompIDEdit.Text := 'demo.pepperstoneuk.4132560'; + UserIdEdit.Text := '4132560'; + PasswordEdit.Text := 'testing'; // BITTE DAS VON PEPPERSTONE BESTÄTIGTE DEMO-PASSWORT VERWENDEN! + Log('FormCreate: Default credentials set. SenderCompID: ' + SenderCompIDEdit.Text + ', UserID: ' + UserIdEdit.Text); +end; + +procedure TForm1.FormDestroy(Sender: TObject); +var + logoutFields: TStringList; + currentSenderCompID: string; +begin + Log('FormDestroy: Cleaning up.'); + if Assigned(FIdTCPClient) and FIdTCPClient.Connected then + begin + Log('FormDestroy: Still connected. Sending Logout message...'); + currentSenderCompID := Trim(SenderCompIDEdit.Text); + if currentSenderCompID <> '' then + begin + logoutFields := TStringList.Create; + try + logoutFields.Add(CreateFixTag(49, currentSenderCompID)); + logoutFields.Add(CreateFixTag(56, TARGET_COMP_ID_CONST)); + logoutFields.Add(CreateFixTag(34, IntToStr(FMsgSeqNum))); + logoutFields.Add(CreateFixTag(52, GetUtcDateTimeString)); + SendFixMessage('5', logoutFields); + finally + FreeAndNil(logoutFields); + end; + end + else + begin + Log('FormDestroy: Cannot send Logout - SenderCompID is empty on form.'); + end; + FIdTCPClient.Disconnect; + Log('FormDestroy: Disconnected.'); + end; + FreeAndNil(FIdTCPClient); + Log('FormDestroy: Components freed.'); +end; + +procedure TForm1.SendFixMessage(const MessageType: string; const AdditionalBodyFields: TStrings); +var + headerBuilder: TStringBuilder; + bodyProperBuilder: TStringBuilder; + fullMessageBuilder: TStringBuilder; + actualBodyLength: Integer; + checksum: string; + i: Integer; + rawMessage: string; +begin + if not (Assigned(FIdTCPClient) and FIdTCPClient.Connected) then + begin + Log('Error: Not connected to server (SendFixMessage for MsgType: ' + MessageType + ').'); + Exit; + end; + + headerBuilder := TStringBuilder.Create; + bodyProperBuilder := TStringBuilder.Create; + fullMessageBuilder := TStringBuilder.Create; + + try + bodyProperBuilder.Append(CreateFixTag(35, MessageType)); + if Assigned(AdditionalBodyFields) then + begin + for i := 0 to AdditionalBodyFields.Count - 1 do + begin + bodyProperBuilder.Append(AdditionalBodyFields[i]); + end; + end; + + actualBodyLength := Length(bodyProperBuilder.ToString); + Log('SendFixMessage: MsgType=' + MessageType + ', Calculated actualBodyLength (Tag 9 Value) = ' + IntToStr(actualBodyLength)); + + headerBuilder.Append(CreateFixTag(8, 'FIX.4.4')); + headerBuilder.Append(CreateFixTag(9, IntToStr(actualBodyLength))); + + fullMessageBuilder.Append(headerBuilder.ToString); + fullMessageBuilder.Append(bodyProperBuilder.ToString); + + checksum := CalculateChecksum(fullMessageBuilder.ToString); + fullMessageBuilder.Append(CreateFixTag(10, checksum)); + + rawMessage := fullMessageBuilder.ToString; + Log('Sending (MsgType ' + MessageType + '): ' + StringReplace(rawMessage, FIX_SEPARATOR, '|', [rfReplaceAll, rfIgnoreCase])); + + try + FIdTCPClient.IOHandler.Write(rawMessage, IndyTextEncoding_UTF8); + FIdTCPClient.IOHandler.WriteBufferFlush(); + except + on E: EIdException do + begin + Log('Error sending message (MsgType ' + MessageType + ', Indy Exception): ' + E.ClassName + ': ' + E.Message); + Exit; + end; + on E: Exception do + begin + Log('Error sending message (MsgType ' + MessageType + ', General Exception): ' + E.Message); + Exit; + end; + end; + Inc(FMsgSeqNum); + finally + FreeAndNil(headerBuilder); + FreeAndNil(bodyProperBuilder); + FreeAndNil(fullMessageBuilder); + end; +end; + +// NEUE Prozedur für Market Data Request +procedure TForm1.SendMarketDataRequest(const Symbol: string; const MDReqID: string); +var + mdFields: TStringList; + tempFieldValue: string; +begin + if not (Assigned(FIdTCPClient) and FIdTCPClient.Connected) then + begin + Log('Error: Not connected to server (SendMarketDataRequest).'); + Exit; + end; + + Log('SendMarketDataRequest: Preparing for Symbol: ' + Symbol + ', MDReqID: ' + MDReqID); + mdFields := TStringList.Create; + try + // Tag 262: MDReqID + mdFields.Add(CreateFixTag(262, MDReqID)); + Log('SendMarketDataRequest: Tag 262 Value="' + MDReqID + '" (Length=' + IntToStr(Length(MDReqID)) + ')'); + + // Tag 263: SubscriptionRequestType (0=Snapshot, 1=Snapshot+Updates, 2=Disable previous) + tempFieldValue := '1'; // Snapshot + Updates + mdFields.Add(CreateFixTag(263, tempFieldValue)); + Log('SendMarketDataRequest: Tag 263 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + // Tag 264: MarketDepth (0=Full Book, 1=Top of Book) + tempFieldValue := '1'; // Top of Book + mdFields.Add(CreateFixTag(264, tempFieldValue)); + Log('SendMarketDataRequest: Tag 264 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + // Tag 267: NoMDEntryTypes (Anzahl der MDEntryType-Gruppen) + tempFieldValue := '2'; // Für Bid und Offer + mdFields.Add(CreateFixTag(267, tempFieldValue)); + Log('SendMarketDataRequest: Tag 267 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + // Tag 269: MDEntryType (0=Bid) + tempFieldValue := '0'; + mdFields.Add(CreateFixTag(269, tempFieldValue)); + Log('SendMarketDataRequest: Tag 269 (Bid) Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + // Tag 269: MDEntryType (1=Offer) + tempFieldValue := '1'; + mdFields.Add(CreateFixTag(269, tempFieldValue)); + Log('SendMarketDataRequest: Tag 269 (Offer) Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + // Tag 146: NoRelatedSym (Anzahl der Instrumente) + tempFieldValue := '1'; // Für ein Instrument + mdFields.Add(CreateFixTag(146, tempFieldValue)); + Log('SendMarketDataRequest: Tag 146 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + // Tag 55: Symbol + // WICHTIG: Ersetzen Sie '1' durch eine gültige cTrader Symbol ID für die Demo-Umgebung! + // Dies ist oft eine numerische ID (z.B. "1" für EURUSD, "22" für GER30). + // Fragen Sie Ihren Broker (Pepperstone) nach den korrekten IDs für die Demo. + mdFields.Add(CreateFixTag(55, Symbol)); + Log('SendMarketDataRequest: Tag 55 Value="' + Symbol + '" (Length=' + IntToStr(Length(Symbol)) + ')'); + + SendFixMessage('V', mdFields); // 'V' ist MsgType für Market Data Request + finally + FreeAndNil(mdFields); + end; +end; + +procedure TForm1.ConnectButtonClick(Sender: TObject); +var + logonFields: TStringList; + currentSenderCompID: string; + currentPasswordValue: string; + currentUsernameFromEdit: string; + sendingTime: string; + response: string; + tempFieldValue: string; + // Für Market Data Request + symbolToRequest: string; + marketDataRequestID: string; +begin + Log('ConnectButtonClick: Initiated for PLAIN TEXT.'); + if not Assigned(FIdTCPClient) then + begin + Log('ConnectButtonClick: TCP Client not initialized. Check FormCreate logs.'); + Exit; + end; + + if FIdTCPClient.Connected then + begin + Log('ConnectButtonClick: Already connected. Please disconnect first.'); + Exit; + end; + + currentSenderCompID := Trim(SenderCompIDEdit.Text); + currentPasswordValue := PasswordEdit.Text; + currentUsernameFromEdit := Trim(UserIdEdit.Text); + + Log('ConnectButtonClick: Input Values - SenderCompID="' + currentSenderCompID + '" (Length=' + IntToStr(Length(currentSenderCompID)) + + '), UserID="' + currentUsernameFromEdit + '" (Length=' + IntToStr(Length(currentUsernameFromEdit)) + + '), Password="' + currentPasswordValue + '" (Length=' + IntToStr(Length(currentPasswordValue)) + ')'); + + if (currentSenderCompID = '') or (currentPasswordValue = '') or (currentUsernameFromEdit = '') then + begin + Log('ConnectButtonClick: SenderCompID, UserID, and Password are required.'); + Exit; + end; + + FMsgSeqNum := 1; + Log('Attempting to connect to DEMO (PLAIN TEXT): ' + FIdTCPClient.Host + ':' + IntToStr(FIdTCPClient.Port) + '...'); + try + FIdTCPClient.Connect; + Log('Connected successfully (PLAIN TEXT).'); + except + on E: EIdException do + begin + Log('Connection failed (Indy Exception): ' + E.ClassName + ': ' + E.Message); + Exit; + end; + on E: Exception do + begin + Log('Connection failed (General Exception): ' + E.ClassName + ': ' + E.Message); + Exit; + end; + end; + + logonFields := TStringList.Create; + try + sendingTime := GetUtcDateTimeString; + Log('ConnectButtonClick: SendingTime (Tag 52 Value)="' + sendingTime + '" (Length=' + IntToStr(Length(sendingTime)) + ')'); + + logonFields.Add(CreateFixTag(49, currentSenderCompID)); + logonFields.Add(CreateFixTag(56, TARGET_COMP_ID_CONST)); + Log('ConnectButtonClick: Tag 56 Value="' + TARGET_COMP_ID_CONST + '" (Length=' + IntToStr(Length(TARGET_COMP_ID_CONST)) + ')'); + logonFields.Add(CreateFixTag(50, SENDER_SUB_ID_CONST)); // 'QUOTE' für Preis-Feed + Log('ConnectButtonClick: Tag 50 Value="' + SENDER_SUB_ID_CONST + '" (Length=' + IntToStr(Length(SENDER_SUB_ID_CONST)) + ')'); + logonFields.Add(CreateFixTag(57, TARGET_SUB_ID_CONST)); // 'QUOTE' oder 'TRADE' - Testen Sie, was funktioniert! + Log('ConnectButtonClick: Tag 57 Value="' + TARGET_SUB_ID_CONST + '" (Length=' + IntToStr(Length(TARGET_SUB_ID_CONST)) + ')'); + tempFieldValue := IntToStr(FMsgSeqNum); + logonFields.Add(CreateFixTag(34, tempFieldValue)); + Log('ConnectButtonClick: Tag 34 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + logonFields.Add(CreateFixTag(52, sendingTime)); + + tempFieldValue := '0'; + logonFields.Add(CreateFixTag(98, tempFieldValue)); + Log('ConnectButtonClick: Tag 98 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + tempFieldValue := '30'; + logonFields.Add(CreateFixTag(108, tempFieldValue)); + Log('ConnectButtonClick: Tag 108 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + tempFieldValue := 'Y'; // ResetSeqNumFlag=Y + logonFields.Add(CreateFixTag(141, tempFieldValue)); + Log('ConnectButtonClick: Tag 141 Value="' + tempFieldValue + '" (Length=' + IntToStr(Length(tempFieldValue)) + ')'); + + logonFields.Add(CreateFixTag(553, currentUsernameFromEdit)); + logonFields.Add(CreateFixTag(554, currentPasswordValue)); + + SendFixMessage('A', logonFields); // Logon senden + + // Kurze Pause und dann Market Data Request senden + //Sleep(500); // 0.5 Sekunden warten + + // WICHTIG: Ersetzen Sie '1' durch eine gültige cTrader Symbol ID für Pepperstone Demo + // und "MDReq_001" durch eine eindeutige ID für Ihren Request. +// symbolToRequest := '1'; // Beispiel: EURUSD (numerische ID von Pepperstone erfragen!) +// marketDataRequestID := currentUsernameFromEdit + '_' + symbolToRequest + '_' + IntToStr(FMsgSeqNum); // Eindeutige MDReqID + +// Log('ConnectButtonClick: Proceeding to send Market Data Request.'); +// SendMarketDataRequest(symbolToRequest, marketDataRequestID); + + // Auf Antwort warten + try + Log('Waiting for response after Logon and MD Request (PLAIN TEXT)...'); + repeat + response := FIdTCPClient.IOHandler.ReadLn(FIX_SEPARATOR, FIdTCPClient.ReadTimeout, -1, IndyTextEncoding_UTF8); +// response := FIdTCPClient.IOHandler.AllData(IndyTextEncoding_UTF8); + Log('Received (PLAIN TEXT): ' + StringReplace(response, FIX_SEPARATOR, '|', [rfReplaceAll, rfIgnoreCase])); + until (response = '') or (LeftStr(response, 3)='10='); + + // Hier Parsing-Logik für die Antwort einfügen + // z.B. auf 35=W (Snapshot), 35=X (Incremental), 35=Y (Reject) prüfen + + except + on E: EIdException do + begin + Log('Error reading response or timeout (PLAIN TEXT - Indy Exception): ' + E.ClassName + ': ' + E.Message); + end; + on E: Exception do + begin + Log('Error reading response (PLAIN TEXT - General Exception): ' + E.ClassName + ': ' + E.Message); + end; + end; + finally + FreeAndNil(logonFields); + end; +end; + +procedure TForm1.DisconnectButtonClick(Sender: TObject); +var + logoutFields: TStringList; + currentSenderCompID: string; +begin + Log('DisconnectButtonClick: Initiated.'); + if Assigned(FIdTCPClient) and FIdTCPClient.Connected then + begin + Log('DisconnectButtonClick: Sending Logout message...'); + currentSenderCompID := Trim(SenderCompIDEdit.Text); + if currentSenderCompID <> '' then + begin + logoutFields := TStringList.Create; + try + logoutFields.Add(CreateFixTag(49, currentSenderCompID)); + logoutFields.Add(CreateFixTag(56, TARGET_COMP_ID_CONST)); + logoutFields.Add(CreateFixTag(34, IntToStr(FMsgSeqNum))); + logoutFields.Add(CreateFixTag(52, GetUtcDateTimeString)); + SendFixMessage('5', logoutFields); + finally + FreeAndNil(logoutFields); + end; + end + else + begin + Log('DisconnectButtonClick: Cannot send Logout - SenderCompID is empty.'); + end; + FIdTCPClient.Disconnect; + Log('DisconnectButtonClick: Disconnected from server.'); + end + else + begin + Log('DisconnectButtonClick: Not connected.'); + end; +end; + +end. diff --git a/Loader/TickLoader.dpr b/Loader/TickLoader.dpr new file mode 100644 index 0000000..59047ce --- /dev/null +++ b/Loader/TickLoader.dpr @@ -0,0 +1,66 @@ +program TickLoader; + +{$APPTYPE CONSOLE} + +uses + System.SysUtils, + System.Math, // Added for Min function + Myc.TickLoader in 'Myc.TickLoader.pas'; + +var + TickData: TArray; + DataFile: string; + I: Integer; // Loop counter + RecordsToDisplay: Integer; +begin + ReportMemoryLeaksOnShutdown := True; + + DataFile := 'C:\Users\Brummel\Documents\cAlgo\TickDataBacktests\Pep_GER40_2017.tab'; + + try + WriteLn('Attempting to load tick data from: ', DataFile); + TickData := LoadTickDataSeries(DataFile); + WriteLn(Format('%d tick data records loaded.', [Length(TickData)])); + WriteLn(''); // Add a blank line for better readability + + if Length(TickData) > 0 then + begin + WriteLn('--- Sample Tick Data Output ---'); + WriteLn('Index | OADateTime | Ask | Bid'); + WriteLn('------------------------------------'); + + // Determine how many records to display, e.g., first 10 or all if fewer than 10 + RecordsToDisplay := Min(Length(TickData), 10); // Display up to the first 10 records + + for I := 0 to RecordsToDisplay - 1 do // Iterate through the selected number of records + begin + // Output each field of the TTickData record, formatted for alignment + WriteLn(Format('%-5d | %-14.5f | %-8.5f | %.5f', [ + I, + TickData[I].OADateTime, + TickData[I].Ask, + TickData[I].Bid + ])); + end; + WriteLn('------------------------------------'); + if Length(TickData) > RecordsToDisplay then + begin + WriteLn(Format('(Showing first %d of %d records)', [RecordsToDisplay, Length(TickData)])); + end; + end + else + begin + WriteLn('No tick data loaded to display.'); + end; + + except + on E: Exception do + begin + WriteLn(Format('Error: %s', [E.Message])); + end; + end; + + WriteLn(''); + WriteLn('Press Enter to exit.'); + ReadLn; +end. diff --git a/Loader/TickLoader.dproj b/Loader/TickLoader.dproj new file mode 100644 index 0000000..494f008 --- /dev/null +++ b/Loader/TickLoader.dproj @@ -0,0 +1,1172 @@ + + + {E0687624-A418-4998-ACCB-000BC8C02CE8} + 20.3 + None + True + Debug + Win64 + TickLoader + 2 + Console + TickLoader.dpr + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + TickLoader + + + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png + true + true + $(BDS)\bin\Artwork\Android\FM_AdaptiveIcon_Monochrome.xml + $(BDS)\bin\Artwork\Android\FM_AdaptiveIcon_Foreground.xml + $(BDS)\bin\Artwork\Android\FM_AdaptiveIcon_Background.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplash.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplashDark.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplashV31.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplashV31Dark.xml + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_24x24.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_96x96.png + false + true + $(BDS)\bin\Artwork\Android\FM_VectorizedNotificationIcon.xml + + + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_96x96.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_144x144.png + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_426x320.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_470x320.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_640x480.png + $(BDS)\bin\Artwork\Android\FM_SplashImage_960x720.png + true + true + $(BDS)\bin\Artwork\Android\FM_AdaptiveIcon_Monochrome.xml + $(BDS)\bin\Artwork\Android\FM_AdaptiveIcon_Foreground.xml + $(BDS)\bin\Artwork\Android\FM_AdaptiveIcon_Background.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplash.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplashDark.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplashV31.xml + $(BDS)\bin\Artwork\Android\FM_VectorizedSplashV31Dark.xml + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_24x24.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_36x36.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_48x48.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_72x72.png + $(BDS)\bin\Artwork\Android\FM_NotificationIcon_96x96.png + false + true + $(BDS)\bin\Artwork\Android\FM_VectorizedNotificationIcon.xml + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + true + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;appanalytics;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + true + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + DEBUG;$(DCC_Define) + true + false + true + true + true + true + true + + + false + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + + MainSource + + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + Application + + + + TickLoader.dpr + + + + + + true + + + + + true + + + + + true + + + + + TickLoader.exe + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\values-v35 + 1 + + + res\values-v35 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + + False + False + False + True + + + 12 + + + + + diff --git a/Loader/TickLoader.res b/Loader/TickLoader.res new file mode 100644 index 0000000..bf57230 Binary files /dev/null and b/Loader/TickLoader.res differ diff --git a/common/Myc.ChartDataController.pas b/common/Myc.ChartDataController.pas new file mode 100644 index 0000000..cbcef03 --- /dev/null +++ b/common/Myc.ChartDataController.pas @@ -0,0 +1,435 @@ +unit Myc.ChartDataController; + +interface + +uses + System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils, + Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants + +type + TTimeframeSetting = record + Caption: string; // Unique key identifier (e.g., "M1", "Auto", "Every 20 Ticks") + Description: string; // User-friendly description for UI + Seconds: Int64; // TF_AUTO_AGGREGATION, TF_TICK_BY_TICK, TF_GENERIC_CALLBACK, or >0 + end; + + TChartDataController = class + private + FTickDataArray: TArray; + FOHLCCacheList: TList; + FTimeframeSettingsArray: TArray; // Now includes registered generic builders + FMemoLog: TStrings; + FGenericBuilders: TDictionary; // Keyed by TTimeframeSetting.Caption + + procedure InitializeBaseTimeframeSettings; // Renamed for clarity + function GetGenericBuilder(const ABuilderCaption: string): IGenericCandleBuilder; + public + constructor Create(AMemoTarget: TStrings); + destructor Destroy; override; + + procedure LoadTickDataSeries(const AFileName: string); + + // GetOrBuildCache now uses a single ASelectedTimeframeCaption for all types of timeframes. + function GetOrBuildCache(const ASelectedTimeframeCaption: string; // e.g., "M1", "Auto", "Every 20 Ticks" + AAutoAggCandleWidth: Integer; + AAutoAggCandleSpacing: Integer; + AAutoAggPaintBoxClientWidth: Integer; + AForceRebuild: Boolean): TOHLC; + + procedure ClearAllDataAndCaches; + function GetTickDataCount: Integer; + function GetTickDataArrayPtr: PTTickData; + function GetTickDataLength: Integer; + function GetTickDataItem(AIndex: Integer): TTickData; + function HasData: Boolean; + + // ABuilderCaption is the unique key used for FTimeframeSettingsArray.Caption and FGenericBuilders key. + procedure RegisterGenericBuilder(const ABuilderCaption: string; + const ABuilderDescription: string; + ABuilder: IGenericCandleBuilder); + // GetGenericBuilderCaptions is removed. Use GetAvailableTimeframes. + function GetAvailableTimeframes: TArray; + end; + +implementation + +uses + System.DateUtils; // For FormatDateTime in logging + +const + dcSecsPerDayConst = 24 * 60 * 60; + +{ TChartDataController } + +constructor TChartDataController.Create(AMemoTarget: TStrings); +var + I: Integer; // Standard loop variable +begin + inherited Create; + FMemoLog := AMemoTarget; + FGenericBuilders := TDictionary.Create; + + InitializeBaseTimeframeSettings; // This populates FTimeframeSettingsArray + + FOHLCCacheList := TList.Create; // Create the TList object + // Add nil placeholders for each timeframe setting defined in FTimeframeSettingsArray + // This ensures FOHLCCacheList is parallel to FTimeframeSettingsArray from the start. + for I := 0 to High(FTimeframeSettingsArray) do + begin + FOHLCCacheList.Add(nil); // Add a nil item to the list + end; + + SetLength(FTickDataArray, 0); + if FMemoLog <> nil then + FMemoLog.Add('TChartDataController instance created. Base timeframes initialized.'); +end; + +destructor TChartDataController.Destroy; +begin + if FMemoLog <> nil then + FMemoLog.Add('TChartDataController instance destroying...'); + ClearAllDataAndCaches(); // This will free TOHLC objects + FreeAndNil(FOHLCCacheList); + FreeAndNil(FGenericBuilders); // Frees dictionary, ARC handles interfaces + if FMemoLog <> nil then + FMemoLog.Add('TChartDataController instance destroyed.'); + inherited Destroy; +end; + +procedure TChartDataController.InitializeBaseTimeframeSettings; +var + InitialSettings: TList; + ts: TTimeframeSetting; +begin + InitialSettings := TList.Create; + try + // Define standard, non-generic timeframes + ts.Caption := 'Auto'; ts.Description := 'Auto (Screen Fit)'; ts.Seconds := TF_AUTO_AGGREGATION; InitialSettings.Add(ts); + ts.Caption := 'Tick'; ts.Description := 'Tick-by-Tick'; ts.Seconds := TF_TICK_BY_TICK; InitialSettings.Add(ts); + // The old "Generic Rule" placeholder is removed. Generic rules are added via RegisterGenericBuilder. + ts.Caption := 'M1'; ts.Description := '1 Minute (M1)'; ts.Seconds := 60; InitialSettings.Add(ts); + ts.Caption := 'M5'; ts.Description := '5 Minutes (M5)'; ts.Seconds := 5 * 60; InitialSettings.Add(ts); + ts.Caption := 'M10'; ts.Description := '10 Minutes (M10)'; ts.Seconds := 10 * 60; InitialSettings.Add(ts); + ts.Caption := 'M15'; ts.Description := '15 Minutes (M15)'; ts.Seconds := 15 * 60; InitialSettings.Add(ts); + ts.Caption := 'M30'; ts.Description := '30 Minutes (M30)'; ts.Seconds := 30 * 60; InitialSettings.Add(ts); + ts.Caption := 'H1'; ts.Description := '1 Hour (H1)'; ts.Seconds := 60 * 60; InitialSettings.Add(ts); + ts.Caption := 'H4'; ts.Description := '4 Hours (H4)'; ts.Seconds := 4 * 60 * 60; InitialSettings.Add(ts); + ts.Caption := 'D1'; ts.Description := '1 Day (D1)'; ts.Seconds := dcSecsPerDayConst; InitialSettings.Add(ts); + ts.Caption := 'W1'; ts.Description := '1 Week (W1)'; ts.Seconds := 7 * dcSecsPerDayConst; InitialSettings.Add(ts); + ts.Caption := 'MN1'; ts.Description := '1 Month (MN1)'; ts.Seconds := 30 * dcSecsPerDayConst; InitialSettings.Add(ts); + ts.Caption := 'Y1'; ts.Description := '1 Year (Y1)'; ts.Seconds := 365 * dcSecsPerDayConst; InitialSettings.Add(ts); + FTimeframeSettingsArray := InitialSettings.ToArray; + finally + InitialSettings.Free; + end; +end; + +function TChartDataController.GetAvailableTimeframes: TArray; +begin + Result := Copy(FTimeframeSettingsArray); // Returns a copy of the current (potentially dynamic) list +end; + +procedure TChartDataController.RegisterGenericBuilder(const ABuilderCaption: string; + const ABuilderDescription: string; + ABuilder: IGenericCandleBuilder); +var + NewTimeframeSetting: TTimeframeSetting; + I: Integer; +begin + if not Assigned(ABuilder) then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Attempt to register a nil generic builder for caption "%s".', [ABuilderCaption])); + Exit; + end; + + // Check for duplicate caption in existing timeframe settings (both base and previously registered builders) + for I := 0 to High(FTimeframeSettingsArray) do + begin + if CompareText(FTimeframeSettingsArray[I].Caption, ABuilderCaption) = 0 then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Cannot register generic builder. A timeframe (base or generic) with caption "%s" already exists.', [ABuilderCaption])); + Exit; + end; + end; + + // Although the loop above should catch caption clashes for FTimeframeSettingsArray, + // FGenericBuilders is a separate dictionary. A builder might be re-registered + // with the same caption but perhaps a different description or instance. + // The current logic is to prevent adding a new TimeframeSetting if the caption exists. + // If a builder with the same caption is already in FGenericBuilders (but somehow not in FTimeframeSettingsArray, + // which would be an inconsistent state), the Add below would fail. + // For robustness, we ensure it's not in FGenericBuilders OR handle replacement. + // Given the check against FTimeframeSettingsArray already prevents duplicate *timeframe captions*, + // if ABuilderCaption is new for timeframes, it must be new for FGenericBuilders too. + + if FGenericBuilders.ContainsKey(ABuilderCaption) then + begin + // This case should ideally not be reached if the previous loop correctly exits + // for existing captions in FTimeframeSettingsArray. + // If it is reached, it implies an inconsistent state or a desire to replace only the builder object + // for an existing generic timeframe caption that was somehow registered differently. + // For safety, let's log and replace if this happens. + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Warning - Generic builder key "%s" already in dictionary, but not found as a TimeframeSetting caption. Replacing builder interface.', [ABuilderCaption])); + FGenericBuilders.Remove(ABuilderCaption); + FGenericBuilders.Add(ABuilderCaption, ABuilder); + end + else + begin + // Add to builder dictionary + FGenericBuilders.Add(ABuilderCaption, ABuilder); + + // Add to FTimeframeSettingsArray + NewTimeframeSetting.Caption := ABuilderCaption; + NewTimeframeSetting.Description := ABuilderDescription; + NewTimeframeSetting.Seconds := TF_GENERIC_CALLBACK; + + SetLength(FTimeframeSettingsArray, Length(FTimeframeSettingsArray) + 1); + FTimeframeSettingsArray[High(FTimeframeSettingsArray)] := NewTimeframeSetting; // This is line 181 from your error reference. + + // Extend FOHLCCacheList with a nil placeholder - CORRECTED PART + FOHLCCacheList.Add(nil); // Use TList.Add method + + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Registered new generic builder timeframe: Caption="%s", Description="%s".', [ABuilderCaption, ABuilderDescription])); + end; +end; + +function TChartDataController.GetGenericBuilder(const ABuilderCaption: string): IGenericCandleBuilder; +begin + Result := nil; + if not FGenericBuilders.TryGetValue(ABuilderCaption, Result) then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Generic builder with caption/key "%s" not found in dictionary.', [ABuilderCaption])); + end; +end; + +procedure TChartDataController.LoadTickDataSeries(const AFileName: string); +begin + ClearAllDataAndCaches(); + try + FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName); + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Loaded %d tick records from series: %s', + [Length(FTickDataArray), AFileName])); + except + on E: Exception do + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Error loading tick data from %s - %s', [AFileName, E.Message])); + SetLength(FTickDataArray, 0); + end; + end; +end; + +// ASelectedTimeframeCaption is the UNIQUE key (e.g. "M1", "Auto", or "Every 20 Ticks") +function TChartDataController.GetOrBuildCache(const ASelectedTimeframeCaption: string; + AAutoAggCandleWidth, AAutoAggCandleSpacing, AAutoAggPaintBoxClientWidth: Integer; + AForceRebuild: Boolean): TOHLC; +var + Index: Integer; + CacheNeedsRebuild: Boolean; + FoundTimeframe: Boolean; + CacheAtIndex: TOHLC; + LGenericBuilder: IGenericCandleBuilder; + CurrentTimeframeSetting: TTimeframeSetting; +begin + Result := nil; + FoundTimeframe := False; + Index := -1; + LGenericBuilder := nil; + + for var I := 0 to High(FTimeframeSettingsArray) do + begin + if CompareText(FTimeframeSettingsArray[I].Caption, ASelectedTimeframeCaption) = 0 then + begin + Index := I; + CurrentTimeframeSetting := FTimeframeSettingsArray[I]; + FoundTimeframe := True; + Break; + end; + end; + + if not FoundTimeframe then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Timeframe setting for caption "%s" not found.', [ASelectedTimeframeCaption])); + Exit; + end; + + CacheAtIndex := FOHLCCacheList[Index]; + CacheNeedsRebuild := AForceRebuild; + + if CacheAtIndex = nil then + begin + CacheAtIndex := TOHLC.Create; + FOHLCCacheList[Index] := CacheAtIndex; + CacheNeedsRebuild := True; + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Cache for [%s] (Desc: "%s") is nil. Creating & Building...', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description])); + end + else if not CacheAtIndex.IsValid then + begin + CacheNeedsRebuild := True; + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Cache for [%s] (Desc: "%s") exists but is invalid. Rebuilding...', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description])); + end + else if CacheAtIndex.AggregationTimeframeSeconds <> CurrentTimeframeSetting.Seconds then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Aggregation timeframe (seconds) mismatch for [%s] (Desc: "%s"). Cache has %ds, requested %ds. Forcing rebuild.', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description, CacheAtIndex.AggregationTimeframeSeconds, CurrentTimeframeSetting.Seconds])); + CacheNeedsRebuild := True; + end; + + // If it's a generic callback timeframe, an additional check for builder matching might be needed if not forced. + // The TOHLC's ApproxTimePerCandleText stores "(BuilderCaption)" for generic builds. + // ASelectedTimeframeCaption *is* the builder caption for generic rules. + if (CurrentTimeframeSetting.Seconds = TF_GENERIC_CALLBACK) and (not CacheNeedsRebuild) and Assigned(CacheAtIndex) and CacheAtIndex.IsValid then + begin + // For generic, CurrentTimeframeSetting.Caption is the Builder's caption. + // We check if the existing cache was built with this same builder. + // TOHLC.ApproxTimePerCandleText stores "(BuilderCaption)". + var ExpectedCacheBuilderInfo: string; + ExpectedCacheBuilderInfo := Format('(%s)', [CurrentTimeframeSetting.Caption]); // Caption is the builder's key + + if CacheAtIndex.ApproxTimePerCandleText <> ExpectedCacheBuilderInfo then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Generic builder changed for timeframe key [%s] (Desc: "%s"). Cache has info "%s", current builder key implies info "%s". Forcing rebuild.', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description, CacheAtIndex.ApproxTimePerCandleText, ExpectedCacheBuilderInfo])); + CacheNeedsRebuild := True; + end; + end; + + if CacheNeedsRebuild then + begin + if HasData then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Building cache for [%s] (Desc: "%s")...', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description])); + + if CurrentTimeframeSetting.Seconds = TF_GENERIC_CALLBACK then + begin + // ASelectedTimeframeCaption IS the key for the generic builder in this new design. + LGenericBuilder := GetGenericBuilder(ASelectedTimeframeCaption); + if not Assigned(LGenericBuilder) then + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Cannot build cache for generic timeframe [%s] (Desc: "%s"). Builder with this caption not found in dictionary.', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description])); + CacheAtIndex.ClearCache(); + Result := CacheAtIndex; + Exit; + end; + end; + + CacheAtIndex.Build( + FTickDataArray, + CurrentTimeframeSetting.Seconds, + AAutoAggCandleWidth, + AAutoAggCandleSpacing, + AAutoAggPaintBoxClientWidth, + FMemoLog, + LGenericBuilder // This will be nil if not TF_GENERIC_CALLBACK, or the found builder + ); + end + else + begin + if FMemoLog <> nil then + FMemoLog.Add('DataController: No TickData to build cache for ' + CurrentTimeframeSetting.Description); + CacheAtIndex.ClearCache(); + end; + end + else + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Using existing valid cache for [%s] (Desc: "%s")', + [CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description])); + end; + Result := CacheAtIndex; +end; + +procedure TChartDataController.ClearAllDataAndCaches; +var + I: Integer; +begin + if FMemoLog <> nil then + FMemoLog.Add('DataController: Clearing all data and caches from instance...'); + SetLength(FTickDataArray, 0); // Clear the raw tick data + + if Assigned(FOHLCCacheList) then + begin + // First, free all TOHLC objects currently in the list + for I := 0 to FOHLCCacheList.Count - 1 do + begin + FreeAndNil(FOHLCCacheList[I]); // Free the TOHLC object + end; + FOHLCCacheList.Clear; // Clears the list, setting its Count to 0. + + // Re-populate FOHLCCacheList with nil placeholders, + // one for each setting in FTimeframeSettingsArray. + // This ensures FOHLCCacheList remains parallel to FTimeframeSettingsArray. + if Assigned(FTimeframeSettingsArray) then // FTimeframeSettingsArray should be assigned + begin + for I := 0 to High(FTimeframeSettingsArray) do // Iterate based on the actual array + begin + FOHLCCacheList.Add(nil); // Add a nil placeholder + end; + end; + end; + + // Note on FGenericBuilders and FTimeframeSettingsArray: + // This procedure currently does NOT clear registered generic builders from FGenericBuilders + // nor does it remove their corresponding entries from FTimeframeSettingsArray. + // If a full reset including unregistering builders is desired, FGenericBuilders.Clear and + // re-calling InitializeBaseTimeframeSettings (to reset FTimeframeSettingsArray) would be needed here, + // followed by re-initializing FOHLCCacheList. + // The current behavior is that builder registrations and the full list of timeframe definitions persist. + + if FMemoLog <> nil then // This is the line (approx. 376) the user reported + FMemoLog.Add('DataController: Instance data and OHLC caches cleared. Registered builders and timeframe definitions (including custom ones) remain.'); +end; + +function TChartDataController.GetTickDataCount: Integer; +begin + Result := Length(FTickDataArray); +end; + +function TChartDataController.GetTickDataArrayPtr: PTTickData; +begin + if Length(FTickDataArray) > 0 then + Result := @FTickDataArray[0] + else + Result := nil; +end; + +function TChartDataController.GetTickDataLength: Integer; +begin + Result := Length(FTickDataArray); +end; + +function TChartDataController.GetTickDataItem(AIndex: Integer): TTickData; +begin + if (AIndex >= 0) and (AIndex < Length(FTickDataArray)) then + Result := FTickDataArray[AIndex] + else + begin + if FMemoLog <> nil then + FMemoLog.Add(Format('DataController: Attempted to access invalid tick data index %d. Count is %d.', [AIndex, Length(FTickDataArray)])); + Result := Default(TTickData); + end; +end; + +function TChartDataController.HasData: Boolean; +begin + Result := Length(FTickDataArray) > 0; +end; + +end. diff --git a/common/Myc.OHLCCache.pas b/common/Myc.OHLCCache.pas new file mode 100644 index 0000000..5aa552a --- /dev/null +++ b/common/Myc.OHLCCache.pas @@ -0,0 +1,513 @@ +(* ------------------------------------------------------------------------------ + Unit Name: Myc.OHLCCache + Author: Delphi Algo Trading Assistant (Generated by AI) + Date: May 21, 2025 + Purpose: This unit provides a class, TOHLC, designed to aggregate raw + tick data into OHLC (Open, High, Low, Close) candle data and + manage this cached candle data. It supports various aggregation + timeframes, including a generic callback mode using an interface, + and provides helper functions for data interpretation. + + Main Features: + - OHLC Candle Structure: Defines TCachedCandle record. + - TOHLC Class: Encapsulates OHLC candle building and storage. + * Aggregation Modes: + - Fixed Timeframes: Aggregates ticks into candles of a specified duration. + - Tick-by-Tick: Each incoming tick forms a separate OHLC candle. + - Auto Aggregation: Calculates aggregation to fit display width. + - Generic Builder: Uses a user-defined class implementing IGenericCandleBuilder + to determine new candle breaks. + * Data Caching, Global Price Range, Validity Status, Metadata, Informational Properties. + - Time Formatting: Helper function FormatTimeSpanFromSeconds. + + Key Constants: + - TF_AUTO_AGGREGATION, TF_TICK_BY_TICK, TF_GENERIC_CALLBACK. + Key Types: + - TCachedCandle, IGenericCandleBuilder, TOHLC. + Key Methods in TOHLC: + - Create, ClearCache, Build. + Dependencies: (As before) + ------------------------------------------------------------------------------ *) +unit Myc.OHLCCache; + +interface + +uses + System.Classes, System.SysUtils, System.Math, + System.Generics.Collections, System.DateUtils, + Myc.TickLoader; + +const + TF_AUTO_AGGREGATION = 0; + TF_TICK_BY_TICK = -1; + TF_GENERIC_CALLBACK = -2; // Constant remains, signifies generic mode + SecsPerDay_Double = 24.0 * 60.0 * 60.0; + +type + TCachedCandle = record + OpenPrice: Single; + HighPrice: Single; + LowPrice: Single; + ClosePrice: Single; + OriginalDataIndexStart: Integer; + OriginalDataIndexEnd: Integer; + TickVolume: Integer; + end; + + // Interface for generic candle building logic + IGenericCandleBuilder = interface + ['{B4F6A9C1-0A7E-42D3-847B-170705A8CC8D}'] // Added GUID + function Init(const ADataSet: TArray): Boolean; // Changed DataSet to ADataSet for clarity + function IsNewBar(AIndex: Integer; const ATick: TTickData): Boolean; // Changed Idx to AIndex, Tick to ATick + function GetCaption: string; // Method for the Caption property + property Caption: string read GetCaption; + end; + + TOHLC = class + private + FCachedCandles: TArray; + FGlobalMinY: Single; + FGlobalMaxY: Single; + FIsValid: Boolean; + FAggregationTimeframeSeconds: Int64; + FAutoAggCandleDisplayWidth: Integer; + FAutoAggPaintBoxClientWidth: Integer; + FAutoAggNumCandlesInCache: Integer; + FApproxTimePerCandleText: string; + FTotalDataTimeSpanText: string; + + function GetCount: Integer; + function GetCandle(Index: Integer): TCachedCandle; + public + constructor Create; + procedure ClearCache; + procedure Build(const ATickData: TArray; + ASelectedTimeframeSeconds: Int64; + AAutoAggCandleWidth: Integer; + AAutoAggCandleSpacing: Integer; + AAutoAggPaintBoxClientWidth: Integer; + AMemoForLogging: TStrings; + // Parameter changed from TGenericCandleProc to IGenericCandleBuilder + AGenericCandleBuilder: IGenericCandleBuilder = nil); + + property Candles[Index: Integer]: TCachedCandle read GetCandle; default; + property Count: Integer read GetCount; + property IsValid: Boolean read FIsValid; + property GlobalMinY: Single read FGlobalMinY; + property GlobalMaxY: Single read FGlobalMaxY; + property AggregationTimeframeSeconds: Int64 read FAggregationTimeframeSeconds; + property ApproxTimePerCandleText: string read FApproxTimePerCandleText; + property TotalDataTimeSpanText: string read FTotalDataTimeSpanText; + property AutoAggNumCandlesInCache: Integer read FAutoAggNumCandlesInCache; + end; + +implementation + +function FormatTimeSpanFromSeconds(const TotalSecondsInput: Double): string; +const + SecsPerMin = 60; + SecsPerHour = SecsPerMin * 60; + SecsPerDayConst = SecsPerHour * 24; +var + Days, Hours, Minutes, SecComponent: Int64; + sTotalSeconds, FracSec: Double; + ResultBuilder: TStringBuilder; +begin // Standard implementation as previously provided + sTotalSeconds := Abs(TotalSecondsInput); + if sTotalSeconds < 0.001 then + Result := 'less than 1 ms' + else if sTotalSeconds < 1.0 then + Result := Format('%.0f ms', [sTotalSeconds * 1000.0]) + else if sTotalSeconds < SecsPerMin then + Result := Format('%.2f seconds', [sTotalSeconds]) + else if sTotalSeconds < SecsPerHour then + Result := Format('%.1f minutes (approx. %d secs)', [sTotalSeconds / SecsPerMin, Floor(sTotalSeconds)]) + else if sTotalSeconds < SecsPerDayConst then + Result := Format('%.1f hours (approx. %d mins)', [sTotalSeconds / SecsPerHour, Floor(sTotalSeconds / SecsPerMin)]) + else + begin + ResultBuilder := TStringBuilder.Create; + try + Days := Trunc(sTotalSeconds / SecsPerDayConst); + sTotalSeconds := sTotalSeconds - (Days * SecsPerDayConst); + Hours := Trunc(sTotalSeconds / SecsPerHour); + sTotalSeconds := sTotalSeconds - (Hours * SecsPerHour); + Minutes := Trunc(sTotalSeconds / SecsPerMin); + sTotalSeconds := sTotalSeconds - (Minutes * SecsPerMin); + SecComponent := Trunc(sTotalSeconds); + FracSec := Frac(sTotalSeconds); + if Days > 0 then + ResultBuilder.Append(Format('%d days, ', [Days])); + if Hours > 0 then + ResultBuilder.Append(Format('%d hours, ', [Hours])); + if Minutes > 0 then + ResultBuilder.Append(Format('%d minutes, ', [Minutes])); + ResultBuilder.Append(Format('%d', [SecComponent])); + if FracSec > 0.001 then + ResultBuilder.Append(Format('.%02d', [Round(FracSec * 100)])); + ResultBuilder.Append(' seconds'); + Result := ResultBuilder.ToString; + finally + ResultBuilder.Free; + end; + end; + if TotalSecondsInput < 0.0 then + Result := '-' + Result; +end; + +{ TOHLC } + +constructor TOHLC.Create; +begin + ClearCache; +end; + +procedure TOHLC.ClearCache; +begin + FIsValid := False; + SetLength(FCachedCandles, 0); + FGlobalMinY := 0; + FGlobalMaxY := 0; + FAggregationTimeframeSeconds := 0; + FAutoAggCandleDisplayWidth := 0; + FAutoAggPaintBoxClientWidth := 0; + FAutoAggNumCandlesInCache := 0; + FApproxTimePerCandleText := 'N/A'; + FTotalDataTimeSpanText := 'N/A'; +end; + +function TOHLC.GetCandle(Index: Integer): TCachedCandle; +begin + if (Index >= 0) and (Index < Length(FCachedCandles)) then + Result := FCachedCandles[Index] + else + Result := Default(TCachedCandle); +end; + +function TOHLC.GetCount: Integer; +begin + Result := Length(FCachedCandles); +end; + +procedure TOHLC.Build(const ATickData: TArray; + ASelectedTimeframeSeconds: Int64; + AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; AAutoAggPaintBoxClientWidth: Integer; + AMemoForLogging: TStrings; AGenericCandleBuilder: IGenericCandleBuilder = nil); // Parameter changed +var + I, CurrentTickIndex, DataIndexStart, DataIndexEnd, J, NumDataPoints, ChartPixelWidthForAutoAgg, NumCandlesToCacheAuto, + SlotWidthForAutoAgg: Integer; + OpenPrice, HighPrice, LowPrice, ClosePrice: Single; + BucketStartTimeOADate, BucketEndTimeOADate, TimeframeIntervalOADays: Double; + CandleList: TList; + TempCandle: TCachedCandle; + LogTimePerCandleStr: string; + CurrentTick: TTickData; +begin + if AMemoForLogging <> nil then + AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - TOHLC.Build: TF Secs: %d...', + [ASelectedTimeframeSeconds])); + ClearCache(); + FAggregationTimeframeSeconds := ASelectedTimeframeSeconds; + NumDataPoints := Length(ATickData); + + if NumDataPoints < 1 then + begin + if AMemoForLogging <> nil then + AMemoForLogging.Add('TOHLC.Build: No tick data.'); + Exit; + end; + + FGlobalMinY := ATickData[0].Ask; + FGlobalMaxY := ATickData[0].Ask; + for I := 1 to High(ATickData) do + begin + if ATickData[I].Ask < FGlobalMinY then + FGlobalMinY := ATickData[I].Ask; + if ATickData[I].Ask > FGlobalMaxY then + FGlobalMaxY := ATickData[I].Ask; + end; + if FGlobalMaxY = FGlobalMinY then + begin + FGlobalMaxY := FGlobalMinY + 0.0001; // Ensure a minimal range + FGlobalMinY := FGlobalMinY - 0.0001; + end; + if FGlobalMaxY = FGlobalMinY then // Still equal (e.g. if original was 0) + FGlobalMaxY := FGlobalMinY + 1; // Ensure a non-zero range + + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build: Global Y-Range: %.5f to %.5f', [FGlobalMinY, FGlobalMaxY])); + LogTimePerCandleStr := 'N/A'; + + // Generic Candle Building using IGenericCandleBuilder + if (ASelectedTimeframeSeconds = TF_GENERIC_CALLBACK) and Assigned(AGenericCandleBuilder) then + begin + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Generic Builder: %s): Aggregating %d ticks.', [AGenericCandleBuilder.Caption, NumDataPoints])); + + if not AGenericCandleBuilder.Init(ATickData) then + begin + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Generic Builder: %s): Init method failed.', [AGenericCandleBuilder.Caption])); + FIsValid := False; // Ensure cache is marked as invalid + Exit; // Exit if Init fails + end; + + CandleList := TList.Create; + try + if NumDataPoints > 0 then + begin + DataIndexStart := 0; + OpenPrice := ATickData[0].Ask; + HighPrice := ATickData[0].Ask; + LowPrice := ATickData[0].Ask; + ClosePrice := ATickData[0].Ask; + + for I := 0 to NumDataPoints - 1 do + begin + CurrentTick := ATickData[I]; + // Use IGenericCandleBuilder.IsNewBar + if (I > DataIndexStart) and AGenericCandleBuilder.IsNewBar(I, CurrentTick) then // Current tick starts a NEW candle + begin + // Finalize the PREVIOUS candle (ending at I-1) + TempCandle.OpenPrice := OpenPrice; + TempCandle.HighPrice := HighPrice; + TempCandle.LowPrice := LowPrice; + TempCandle.ClosePrice := ATickData[I - 1].Ask; // Close of previous candle is the tick before the new bar starts + TempCandle.OriginalDataIndexStart := DataIndexStart; + TempCandle.OriginalDataIndexEnd := I - 1; + TempCandle.TickVolume := (I - 1) - DataIndexStart + 1; + CandleList.Add(TempCandle); + + // Current tick 'I' starts a new candle + DataIndexStart := I; + OpenPrice := CurrentTick.Ask; + HighPrice := CurrentTick.Ask; + LowPrice := CurrentTick.Ask; + ClosePrice := CurrentTick.Ask; + end + else // Current tick continues the existing candle OR is the first tick of the very first candle + begin + if I = DataIndexStart then // This is the first tick of the current (or overall first) candle + begin + OpenPrice := CurrentTick.Ask; // Open is set + HighPrice := CurrentTick.Ask; // Initial High + LowPrice := CurrentTick.Ask; // Initial Low + end + else // Update High/Low for ongoing candle + begin + if CurrentTick.Ask > HighPrice then + HighPrice := CurrentTick.Ask; + if CurrentTick.Ask < LowPrice then + LowPrice := CurrentTick.Ask; + end; + ClosePrice := CurrentTick.Ask; // Always update close to current tick + end; + + // If this is the last tick in the dataset, finalize the current candle + if I = NumDataPoints - 1 then + begin + TempCandle.OpenPrice := OpenPrice; + TempCandle.HighPrice := HighPrice; + TempCandle.LowPrice := LowPrice; + TempCandle.ClosePrice := ClosePrice; // Close price is this tick's price + TempCandle.OriginalDataIndexStart := DataIndexStart; + TempCandle.OriginalDataIndexEnd := I; + TempCandle.TickVolume := I - DataIndexStart + 1; + CandleList.Add(TempCandle); + end; + end; + end; + FCachedCandles := CandleList.ToArray; + LogTimePerCandleStr := Format('(%s)', [AGenericCandleBuilder.Caption]); // Use Builder's Caption + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Generic Builder: %s): Created %d candles.', [AGenericCandleBuilder.Caption, Length(FCachedCandles)])); + finally + CandleList.Free; + end; + end + else if FAggregationTimeframeSeconds = TF_AUTO_AGGREGATION then + begin // Auto Aggregation logic + FAutoAggCandleDisplayWidth := AAutoAggCandleWidth; + FAutoAggPaintBoxClientWidth := AAutoAggPaintBoxClientWidth; + ChartPixelWidthForAutoAgg := FAutoAggPaintBoxClientWidth; + if ChartPixelWidthForAutoAgg <= 0 then + begin + if AMemoForLogging <> nil then AMemoForLogging.Add('TOHLC.Build (Auto): PaintBox width too small.'); + Exit; + end; + SlotWidthForAutoAgg := FAutoAggCandleDisplayWidth + AAutoAggCandleSpacing; + if SlotWidthForAutoAgg <= 0 then + begin + if AMemoForLogging <> nil then AMemoForLogging.Add('TOHLC.Build (Auto): SlotWidth zero or negative.'); + Exit; + end; + NumCandlesToCacheAuto := ChartPixelWidthForAutoAgg div SlotWidthForAutoAgg; + FAutoAggNumCandlesInCache := NumCandlesToCacheAuto; + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Auto): Aggregating with CW %dpx, Sp %dpx. SlotW %dpx. NumCandles: %d.', + [FAutoAggCandleDisplayWidth, AAutoAggCandleSpacing, SlotWidthForAutoAgg, NumCandlesToCacheAuto])); + + if NumCandlesToCacheAuto <= 0 then + begin + if AMemoForLogging <> nil then AMemoForLogging.Add('TOHLC.Build (Auto): Not enough space for candles.'); + Exit; + end; + SetLength(FCachedCandles, NumCandlesToCacheAuto); + if (NumDataPoints > 1) and (NumCandlesToCacheAuto > 0) then + begin + var FirstTickTimeVal_A := ATickData[0].OADateTime; + var LastTickTimeVal_A := ATickData[High(ATickData)].OADateTime; + var TotalDataDurationDaysVal_A := LastTickTimeVal_A - FirstTickTimeVal_A; + if NumCandlesToCacheAuto > 0 then // Avoid division by zero if NumCandlesToCacheAuto is somehow 0 here + begin + var AvgCandleDurationDaysVal_A := TotalDataDurationDaysVal_A / NumCandlesToCacheAuto; + LogTimePerCandleStr := FormatTimeSpanFromSeconds(AvgCandleDurationDaysVal_A * SecsPerDay_Double); + end + else + LogTimePerCandleStr := 'N/A (Auto)'; + end; + for I := 0 to NumCandlesToCacheAuto - 1 do + begin + DataIndexStart := Floor((I * SlotWidthForAutoAgg) * (NumDataPoints / ChartPixelWidthForAutoAgg)); + DataIndexEnd := Floor(((I + 1) * SlotWidthForAutoAgg) * (NumDataPoints / ChartPixelWidthForAutoAgg)) - 1; + DataIndexStart := Max(0, DataIndexStart); + DataIndexEnd := Min(High(ATickData), Max(DataIndexStart, DataIndexEnd)); + + if (DataIndexStart > High(ATickData)) or (DataIndexStart > DataIndexEnd) then + begin + FCachedCandles[I] := Default(TCachedCandle); // Initialize with default values + Continue; + end; + + OpenPrice := ATickData[DataIndexStart].Ask; + ClosePrice := ATickData[DataIndexEnd].Ask; + HighPrice := OpenPrice; // Initialize with Open + LowPrice := OpenPrice; // Initialize with Open + + for J := DataIndexStart to DataIndexEnd do + begin + if ATickData[J].Ask > HighPrice then HighPrice := ATickData[J].Ask; + if ATickData[J].Ask < LowPrice then LowPrice := ATickData[J].Ask; + end; + // Ensure High/Low encompass Open/Close after loop + HighPrice := Max(HighPrice, Max(OpenPrice, ClosePrice)); + LowPrice := Min(LowPrice, Min(OpenPrice, ClosePrice)); + + FCachedCandles[I].OpenPrice := OpenPrice; + FCachedCandles[I].HighPrice := HighPrice; + FCachedCandles[I].LowPrice := LowPrice; + FCachedCandles[I].ClosePrice := ClosePrice; + FCachedCandles[I].OriginalDataIndexStart := DataIndexStart; + FCachedCandles[I].OriginalDataIndexEnd := DataIndexEnd; + FCachedCandles[I].TickVolume := DataIndexEnd - DataIndexStart + 1; + end; + end + else if FAggregationTimeframeSeconds = TF_TICK_BY_TICK then + begin // Tick-by-Tick logic + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Tick-by-Tick): Creating %d candles.', [NumDataPoints])); + SetLength(FCachedCandles, NumDataPoints); + for I := 0 to NumDataPoints - 1 do + begin + FCachedCandles[I].OpenPrice := ATickData[I].Ask; + FCachedCandles[I].HighPrice := ATickData[I].Ask; + FCachedCandles[I].LowPrice := ATickData[I].Ask; + FCachedCandles[I].ClosePrice := ATickData[I].Ask; + FCachedCandles[I].OriginalDataIndexStart := I; + FCachedCandles[I].OriginalDataIndexEnd := I; + FCachedCandles[I].TickVolume := 1; + end; + LogTimePerCandleStr := '(duration of one tick)'; + end + else // Fixed Timeframe (FAggregationTimeframeSeconds > 0) + begin // Fixed Timeframe logic + CandleList := TList.Create; + try + TimeframeIntervalOADays := FAggregationTimeframeSeconds / SecsPerDay_Double; + if TimeframeIntervalOADays <= 1E-9 then // Avoid division by zero or too small interval + begin + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Fixed TF): Error - Interval too small (%.10f).', [TimeframeIntervalOADays])); + Exit; + end; + + CurrentTickIndex := 0; + while CurrentTickIndex <= High(ATickData) do + begin + BucketStartTimeOADate := Floor(ATickData[CurrentTickIndex].OADateTime / TimeframeIntervalOADays) * TimeframeIntervalOADays; + BucketEndTimeOADate := BucketStartTimeOADate + TimeframeIntervalOADays; + + DataIndexStart := CurrentTickIndex; + OpenPrice := ATickData[CurrentTickIndex].Ask; + HighPrice := ATickData[CurrentTickIndex].Ask; + LowPrice := ATickData[CurrentTickIndex].Ask; + ClosePrice := ATickData[CurrentTickIndex].Ask; // Initialize close with open + DataIndexEnd := CurrentTickIndex; // Initialize end with start + + // Iterate through ticks that fall into the current bucket + while (CurrentTickIndex <= High(ATickData)) and + (ATickData[CurrentTickIndex].OADateTime >= BucketStartTimeOADate) and + (ATickData[CurrentTickIndex].OADateTime < BucketEndTimeOADate) do + begin + if ATickData[CurrentTickIndex].Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Ask; + if ATickData[CurrentTickIndex].Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Ask; + ClosePrice := ATickData[CurrentTickIndex].Ask; // Continuously update close price + DataIndexEnd := CurrentTickIndex; // Mark the last tick included in this candle + Inc(CurrentTickIndex); + end; + + // If no ticks were processed in the inner loop (e.g., if the first tick was already >= BucketEndTimeOADate) + // ensure we advance CurrentTickIndex to avoid an infinite loop if DataIndexStart remains unchanged. + // This situation should ideally be rare if data is sorted and dense enough for the timeframe. + // However, if DataIndexStart equals CurrentTickIndex here, it means the inner loop didn't run. + // This could happen if ATickData[CurrentTickIndex].OADateTime was already >= BucketEndTimeOADate. + // In such case, we still form a candle with the single tick at DataIndexStart, and then CurrentTickIndex must advance. + // If the loop DID run, CurrentTickIndex is already pointing to the next tick outside the bucket. + if (DataIndexStart = DataIndexEnd) And (CurrentTickIndex = DataIndexStart) then // Only one tick considered, and it might not have advanced CurrentTickIndex + begin + if CurrentTickIndex <= High(ATickData) then // If it wasn't the very last tick. + Inc(CurrentTickIndex); // Ensure progress if loop condition makes it stick. + end; + // If the inner while loop exited because CurrentTickIndex > High(ATickData), DataIndexStart might still be a valid index + // for the last candle. + // Or, if the loop exited due to time boundary, CurrentTickIndex is correctly positioned for the next bucket. + + TempCandle.OpenPrice := OpenPrice; + TempCandle.HighPrice := HighPrice; + TempCandle.LowPrice := LowPrice; + TempCandle.ClosePrice := ClosePrice; + TempCandle.OriginalDataIndexStart := DataIndexStart; + TempCandle.OriginalDataIndexEnd := DataIndexEnd; + TempCandle.TickVolume := (DataIndexEnd - DataIndexStart) + 1; + CandleList.Add(TempCandle); + + if CurrentTickIndex > High(ATickData) then Break; // All ticks processed + end; + FCachedCandles := CandleList.ToArray; + LogTimePerCandleStr := FormatTimeSpanFromSeconds(FAggregationTimeframeSeconds); + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build (Fixed TF): Created %d candles.', [Length(FCachedCandles)])); + finally + CandleList.Free; + end; + end; + + if Length(ATickData) > 0 then + begin + var FirstTickTimeVal := ATickData[0].OADateTime; + var LastTickTimeVal := ATickData[High(ATickData)].OADateTime; + FTotalDataTimeSpanText := FormatTimeSpanFromSeconds((LastTickTimeVal - FirstTickTimeVal) * SecsPerDay_Double); + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build: Total data time span: %s.', [FTotalDataTimeSpanText])); + end; + + FApproxTimePerCandleText := LogTimePerCandleStr; + if AMemoForLogging <> nil then + AMemoForLogging.Add(Format('TOHLC.Build: Time per cached candle: %s.', [FApproxTimePerCandleText])); + + FIsValid := True; + if AMemoForLogging <> nil then + AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - TOHLC.Build: OHLC cache built.'); +end; + +end. diff --git a/common/Myc.TickLoader.pas b/common/Myc.TickLoader.pas new file mode 100644 index 0000000..fb85cb2 --- /dev/null +++ b/common/Myc.TickLoader.pas @@ -0,0 +1,254 @@ +unit Myc.TickLoader; + +(*------------------------------------------------------------------------------ + Unit: Myc.TickLoader + Author: Delphi Algo Trading (Assistant) + Date: May 22, 2025 + Description: + This unit provides functions to load tick data (TTickData) from + binary files. The data originates from a C# application + and has the structure OADateTime (Double), Ask (Single), and Bid (Single). + + The files now follow the naming convention: Symbol_Year.tab. + (Prefix has been removed from the convention). + Loading occurs in two phases: + 1. All .tab files are processed chronologically. Unique ticks are loaded, + and the timestamp of the latest tick across all .tab files is determined + (overallLastTabTickTime). Assumes timestamps are unique across all .tab files. + 2. All .tab-live files (for years corresponding to found .tab files) are + processed. Ticks from a .tab-live file are only integrated if their + timestamp is strictly later than overallLastTabTickTime. Assumes such + ticks are globally unique. + + If a .tab-live file results in no new data being added under these rules, + it is deleted after processing. + + The loading function automatically detects subsequent yearly .tab files. + + Main Function: + LoadTickDataSeries - Loads a series of tick data files. +------------------------------------------------------------------------------*) + +interface + +uses + System.SysUtils, + System.Classes, + System.Generics.Collections, + System.IOUtils; + +type + TTickData = packed record + OADateTime: Double; + Ask: Single; + Bid: Single; + end; + PTTickData = ^TTickData; + +// TryParseFileName now expects filenames like "Symbol_Year" (or "Symbol_Part2_Year") +// PrefixValue will be returned as an empty string. +function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean; +function LoadTickDataSeries(const FileName: string): TArray; + +implementation + +function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean; +var + fileNameOnly: string; + parts: TArray; +begin + Result := False; + PathValue := TPath.GetDirectoryName(FileName); + fileNameOnly := TPath.GetFileNameWithoutExtension(FileName); // Handles any extension for parsing basename + + parts := fileNameOnly.Split(['_']); + + // Expected format is Symbol_Year or SymbolPart1_SymbolPart2_Year, so at least 2 parts. + if Length(parts) < 2 then + begin + SymbolValue := ''; YearValue := 0; // Clear out params on minimum parts failure + Exit; + end; + + // Year is the last part. + if not TryStrToInt(parts[High(parts)], YearValue) then + begin + SymbolValue := ''; // Clear SymbolValue as well on year parse failure + Exit; + end; + + // Symbol consists of all parts before the year part. + // Copy all parts except the last one (which is the Year). + // The second parameter of Copy (for TArray) is the starting index, + // the third parameter is the count of elements to copy. + // We want to copy elements from index 0 up to (but not including) the last element. + // So, the count is High(parts) (which is Length(parts) - 1). + if High(parts) > 0 then // Ensure there's at least one part for the symbol + SymbolValue := string.Join('_', Copy(parts, 0, High(parts))) + else if Length(parts) = 1 then // Edge case: File is "Year.tab" - this is invalid by new rule "Symbol_Year" + begin SymbolValue := ''; Exit; end // Or handle as Symbol = parts[0] if "Year" itself is a symbol + else // Should not happen if Length(parts) < 2 already exited + SymbolValue := ''; + + + Result := True; +end; + +function LoadTickDataSeries(const FileName: string): TArray; +var + tickList: TList; + initialYear, currentTabYear, liveProcessingYear: Integer; + pathName, symbolName: string; // prefixNameOut will be an empty out param + tabFileName, liveFileName: string; + overallLastTabTickTime: Double; + maxTabYearFound: Integer; + + procedure ReadFileContentAndGetMaxTime(const AFileName: string; + out Records: TArray; + out ActualMaxTickTimeInFile: Double); + var + fs: TFileStream; + fileSize: Int64; + recordCount: Integer; + bytesRead: Integer; + iterTickData: TTickData; + currentFileMaxOA: Double; + begin + SetLength(Records, 0); + ActualMaxTickTimeInFile := 0.0; + currentFileMaxOA := 0.0; + + if not TFile.Exists(AFileName) then // Added check for file existence + begin + // Silently exit or log if preferred, but don't try to open non-existent file. + Exit; + end; + + fs := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite); + try + fileSize := fs.Size; + if fileSize = 0 then Exit; + + if (fileSize mod SizeOf(TTickData)) <> 0 then + raise EReadError.CreateFmt('File %s: Corrupted. Size %d is not a multiple of record size %d.', + [AFileName, fileSize, SizeOf(TTickData)]); + + recordCount := fileSize div SizeOf(TTickData); + if recordCount > 0 then + begin + SetLength(Records, recordCount); + bytesRead := fs.Read(Records[0], fileSize); + if bytesRead <> fileSize then + raise EReadError.CreateFmt('File %s: Read error. Expected to read %d bytes, but actually read %d bytes.', + [AFileName, fileSize, bytesRead]); + + for iterTickData in Records do + begin + if iterTickData.OADateTime > currentFileMaxOA then + currentFileMaxOA := iterTickData.OADateTime; + end; + ActualMaxTickTimeInFile := currentFileMaxOA; + end; + finally + fs.Free; + end; + end; + +begin // Start of LoadTickDataSeries + tickList := TList.Create; + overallLastTabTickTime := 0.0; + maxTabYearFound := -1; + + try + // Initial FileName must be the path to the first .tab file (e.g., Symbol_Year.tab) + // prefixNameOut will be an out parameter from TryParseFileName but will be empty. + if not TryParseFileName(FileName, pathName, symbolName, initialYear) then + begin + raise EArgumentException.CreateFmt('Invalid initial file name format: %s. Expected Symbol_Year.tab (or Symbol_Part2_Year.tab)', [FileName]); + end; + + // --- PHASE 1: Load all .tab files and determine overallLastTabTickTime --- + currentTabYear := initialYear; + // Construct the first .tab file name using the parsed components (symbol, year). + // Prefix is no longer used in the format string. + tabFileName := TPath.Combine(pathName, Format('%s_%d.tab', [symbolName, currentTabYear])); + + var tabFileRecords: TArray; + var maxTimeInThisTabFile: Double; + + while TFile.Exists(tabFileName) do + begin + maxTabYearFound := currentTabYear; + ReadFileContentAndGetMaxTime(tabFileName, tabFileRecords, maxTimeInThisTabFile); + + if Length(tabFileRecords) > 0 then + begin + tickList.AddRange(tabFileRecords); + end; + + if maxTimeInThisTabFile > overallLastTabTickTime then + overallLastTabTickTime := maxTimeInThisTabFile; + + Inc(currentTabYear); + // Construct next tab filename without prefix + tabFileName := TPath.Combine(pathName, Format('%s_%d.tab', [symbolName, currentTabYear])); + end; + + // --- PHASE 2: Load .tab-live files --- + if maxTabYearFound >= initialYear then + begin + for liveProcessingYear := initialYear to maxTabYearFound do + begin + // Construct .tab-live filename without prefix + liveFileName := TPath.Combine(pathName, Format('%s_%d.tab-live', [symbolName, liveProcessingYear])); + + if TFile.Exists(liveFileName) then + begin + var liveFileRecords: TArray; + var maxTimeInThisLiveFile: Double; + var ticksToActuallyAddFromLive: TList; + var liveDataAddedThisFile: Boolean; + var iterLiveTickData: TTickData; + + ReadFileContentAndGetMaxTime(liveFileName, liveFileRecords, maxTimeInThisLiveFile); + liveDataAddedThisFile := False; + + if Length(liveFileRecords) > 0 then + begin + ticksToActuallyAddFromLive := TList.Create; + try + for iterLiveTickData in liveFileRecords do + begin + if iterLiveTickData.OADateTime > overallLastTabTickTime then + begin + ticksToActuallyAddFromLive.Add(iterLiveTickData); + liveDataAddedThisFile := True; + end; + end; + + if ticksToActuallyAddFromLive.Count > 0 then + tickList.AddRange(ticksToActuallyAddFromLive); + finally + ticksToActuallyAddFromLive.Free; + end; + end; + + if not liveDataAddedThisFile then + begin + try + TFile.Delete(liveFileName); + except + on E: Exception do {/* Optional: Log error, e.g., using a global logger or passed logger */} + end; + end; + end; + end; + end; + + Result := tickList.ToArray; + finally + tickList.Free; + end; +end; + +end.