1st commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
*.dcu
|
||||
*.local
|
||||
*.identcache
|
||||
*.exe
|
||||
*.rsm
|
||||
__history
|
||||
__recovery
|
||||
Win32
|
||||
Win64
|
||||
@@ -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
@@ -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.
@@ -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.
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user