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