1st commit

This commit is contained in:
Michael Schimmel
2025-05-23 12:51:32 +02:00
parent 94753a720c
commit 06ec0db105
17 changed files with 6639 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
*.dcu
*.local
*.identcache
*.exe
*.rsm
__history
__recovery
Win32
Win64
+121
View File
@@ -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
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+14
View File
@@ -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.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+88
View File
@@ -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
+463
View File
@@ -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.
+66
View File
@@ -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<TTickData>;
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.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+435
View File
@@ -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<TTickData>;
FOHLCCacheList: TList<TOHLC>;
FTimeframeSettingsArray: TArray<TTimeframeSetting>; // Now includes registered generic builders
FMemoLog: TStrings;
FGenericBuilders: TDictionary<string, IGenericCandleBuilder>; // 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<TTimeframeSetting>;
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<string, IGenericCandleBuilder>.Create;
InitializeBaseTimeframeSettings; // This populates FTimeframeSettingsArray
FOHLCCacheList := TList<TOHLC>.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<TTimeframeSetting>;
ts: TTimeframeSetting;
begin
InitialSettings := TList<TTimeframeSetting>.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<TTimeframeSetting>;
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.
+513
View File
@@ -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<TTickData>): 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<TCachedCandle>;
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<TTickData>;
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<TTickData>;
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<TCachedCandle>;
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<TCachedCandle>.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<TCachedCandle>.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.
+254
View File
@@ -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<TTickData>;
implementation
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean;
var
fileNameOnly: string;
parts: TArray<string>;
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<string>) 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<TTickData>;
var
tickList: TList<TTickData>;
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<TTickData>;
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<TTickData>.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<TTickData>;
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<TTickData>;
var maxTimeInThisLiveFile: Double;
var ticksToActuallyAddFromLive: TList<TTickData>;
var liveDataAddedThisFile: Boolean;
var iterLiveTickData: TTickData;
ReadFileContentAndGetMaxTime(liveFileName, liveFileRecords, maxTimeInThisLiveFile);
liveDataAddedThisFile := False;
if Length(liveFileRecords) > 0 then
begin
ticksToActuallyAddFromLive := TList<TTickData>.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.