Refactoring to new TDataSeries
This commit is contained in:
@@ -16,14 +16,14 @@ object ChartForm: TChartForm
|
|||||||
900
|
900
|
||||||
835)
|
835)
|
||||||
TextHeight = 15
|
TextHeight = 15
|
||||||
object FileSelectButton: TSpeedButton
|
object PathSelectButton: TSpeedButton
|
||||||
Left = 647
|
Left = 423
|
||||||
Top = 17
|
Top = 18
|
||||||
Width = 23
|
Width = 23
|
||||||
Height = 22
|
Height = 23
|
||||||
Anchors = [akTop, akRight]
|
Anchors = [akTop, akRight]
|
||||||
Caption = '...'
|
Caption = '...'
|
||||||
OnClick = FileSelectButtonClick
|
OnClick = PathSelectButtonClick
|
||||||
end
|
end
|
||||||
object MouseHoverDataLabel: TLabel
|
object MouseHoverDataLabel: TLabel
|
||||||
Left = 32
|
Left = 32
|
||||||
@@ -34,14 +34,14 @@ object ChartForm: TChartForm
|
|||||||
AutoSize = False
|
AutoSize = False
|
||||||
ExplicitTop = 359
|
ExplicitTop = 359
|
||||||
end
|
end
|
||||||
object FileNameEdit: TEdit
|
object DataPathEdit: TEdit
|
||||||
Left = 32
|
Left = 32
|
||||||
Top = 17
|
Top = 17
|
||||||
Width = 609
|
Width = 385
|
||||||
Height = 23
|
Height = 23
|
||||||
Anchors = [akLeft, akTop, akRight]
|
Anchors = [akLeft, akTop, akRight]
|
||||||
TabOrder = 0
|
TabOrder = 0
|
||||||
Text = '\\COFFEE\TickData\Pepperstone\EURUSD_2017_02.tab_zip'
|
Text = '\\COFFEE\TickData\Pepperstone'
|
||||||
end
|
end
|
||||||
object LoadButton: TButton
|
object LoadButton: TButton
|
||||||
Left = 686
|
Left = 686
|
||||||
@@ -116,4 +116,14 @@ object ChartForm: TChartForm
|
|||||||
ExplicitHeight = 97
|
ExplicitHeight = 97
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
object SymbolComboBox: TComboBox
|
||||||
|
Left = 452
|
||||||
|
Top = 18
|
||||||
|
Width = 228
|
||||||
|
Height = 23
|
||||||
|
Anchors = [akTop, akRight]
|
||||||
|
TabOrder = 7
|
||||||
|
Text = 'GER40'
|
||||||
|
OnChange = SymbolComboBoxChange
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+774
-488
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ interface
|
|||||||
uses
|
uses
|
||||||
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
|
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
|
||||||
Myc.Futures,
|
Myc.Futures,
|
||||||
Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants
|
Myc.Trade.DataSeries, Myc.OHLCCache;
|
||||||
|
|
||||||
type
|
type
|
||||||
TTimeframeSetting = record
|
TTimeframeSetting = record
|
||||||
@@ -16,7 +16,7 @@ type
|
|||||||
|
|
||||||
TChartDataController = class
|
TChartDataController = class
|
||||||
private
|
private
|
||||||
FTickDataArray: TArray<TTickData>;
|
FTickDataArray: TArray<TAskBid.TTick>;
|
||||||
FOHLCCacheList: TList<TOHLC>;
|
FOHLCCacheList: TList<TOHLC>;
|
||||||
FTimeframeSettingsArray: TArray<TTimeframeSetting>; // Now includes registered generic builders
|
FTimeframeSettingsArray: TArray<TTimeframeSetting>; // Now includes registered generic builders
|
||||||
FMemoLog: TStrings;
|
FMemoLog: TStrings;
|
||||||
@@ -39,9 +39,8 @@ type
|
|||||||
|
|
||||||
procedure ClearAllDataAndCaches;
|
procedure ClearAllDataAndCaches;
|
||||||
function GetTickDataCount: Integer;
|
function GetTickDataCount: Integer;
|
||||||
function GetTickDataArrayPtr: PTTickData;
|
|
||||||
function GetTickDataLength: Integer;
|
function GetTickDataLength: Integer;
|
||||||
function GetTickDataItem(AIndex: Integer): TTickData;
|
function GetTickDataItem(AIndex: Integer): TAskBid.TTick;
|
||||||
function HasData: Boolean;
|
function HasData: Boolean;
|
||||||
|
|
||||||
// ABuilderCaption is the unique key used for FTimeframeSettingsArray.Caption and FGenericBuilders key.
|
// ABuilderCaption is the unique key used for FTimeframeSettingsArray.Caption and FGenericBuilders key.
|
||||||
@@ -55,7 +54,7 @@ type
|
|||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
System.DateUtils; // For FormatDateTime in logging
|
System.DateUtils;
|
||||||
|
|
||||||
const
|
const
|
||||||
dcSecsPerDayConst = 24 * 60 * 60;
|
dcSecsPerDayConst = 24 * 60 * 60;
|
||||||
@@ -211,7 +210,7 @@ procedure TChartDataController.LoadTickDataSeries(const AFileName: string);
|
|||||||
begin
|
begin
|
||||||
ClearAllDataAndCaches();
|
ClearAllDataAndCaches();
|
||||||
try
|
try
|
||||||
FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName).WaitFor;
|
FTickDataArray := TAskBid.LoadDataSeries(AFileName).WaitFor();
|
||||||
if FMemoLog <> nil then
|
if FMemoLog <> nil then
|
||||||
FMemoLog.Add(Format('DataController: Loading tick records from series: %s',
|
FMemoLog.Add(Format('DataController: Loading tick records from series: %s',
|
||||||
[AFileName]));
|
[AFileName]));
|
||||||
@@ -401,20 +400,12 @@ begin
|
|||||||
Result := Length(FTickDataArray);
|
Result := Length(FTickDataArray);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TChartDataController.GetTickDataArrayPtr: PTTickData;
|
|
||||||
begin
|
|
||||||
if Length(FTickDataArray) > 0 then
|
|
||||||
Result := @FTickDataArray[0]
|
|
||||||
else
|
|
||||||
Result := nil;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TChartDataController.GetTickDataLength: Integer;
|
function TChartDataController.GetTickDataLength: Integer;
|
||||||
begin
|
begin
|
||||||
Result := Length(FTickDataArray);
|
Result := Length(FTickDataArray);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TChartDataController.GetTickDataItem(AIndex: Integer): TTickData;
|
function TChartDataController.GetTickDataItem(AIndex: Integer): TAskBid.TTick;
|
||||||
begin
|
begin
|
||||||
if (AIndex >= 0) and (AIndex < Length(FTickDataArray)) then
|
if (AIndex >= 0) and (AIndex < Length(FTickDataArray)) then
|
||||||
Result := FTickDataArray[AIndex]
|
Result := FTickDataArray[AIndex]
|
||||||
@@ -422,7 +413,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
if FMemoLog <> nil then
|
if FMemoLog <> nil then
|
||||||
FMemoLog.Add(Format('DataController: Attempted to access invalid tick data index %d. Count is %d.', [AIndex, Length(FTickDataArray)]));
|
FMemoLog.Add(Format('DataController: Attempted to access invalid tick data index %d. Count is %d.', [AIndex, Length(FTickDataArray)]));
|
||||||
Result := Default(TTickData);
|
Result := Default(TAskBid.TTick);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+44
-45
@@ -35,7 +35,7 @@ interface
|
|||||||
uses
|
uses
|
||||||
System.Classes, System.SysUtils, System.Math,
|
System.Classes, System.SysUtils, System.Math,
|
||||||
System.Generics.Collections, System.DateUtils,
|
System.Generics.Collections, System.DateUtils,
|
||||||
Myc.TickLoader;
|
Myc.Trade.DataSeries;
|
||||||
|
|
||||||
const
|
const
|
||||||
TF_AUTO_AGGREGATION = 0;
|
TF_AUTO_AGGREGATION = 0;
|
||||||
@@ -56,9 +56,8 @@ type
|
|||||||
|
|
||||||
// Interface for generic candle building logic
|
// Interface for generic candle building logic
|
||||||
IGenericCandleBuilder = interface
|
IGenericCandleBuilder = interface
|
||||||
['{B4F6A9C1-0A7E-42D3-847B-170705A8CC8D}'] // Added GUID
|
function Init(const ADataSet: TArray<TAskBid.TTick>): Boolean; // Changed DataSet to ADataSet for clarity
|
||||||
function Init(const ADataSet: TArray<TTickData>): Boolean; // Changed DataSet to ADataSet for clarity
|
function IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; // Changed Idx to AIndex, Tick to ATick
|
||||||
function IsNewBar(AIndex: Integer; const ATick: TTickData): Boolean; // Changed Idx to AIndex, Tick to ATick
|
|
||||||
function GetCaption: string; // Method for the Caption property
|
function GetCaption: string; // Method for the Caption property
|
||||||
property Caption: string read GetCaption;
|
property Caption: string read GetCaption;
|
||||||
end;
|
end;
|
||||||
@@ -81,7 +80,7 @@ type
|
|||||||
public
|
public
|
||||||
constructor Create;
|
constructor Create;
|
||||||
procedure ClearCache;
|
procedure ClearCache;
|
||||||
procedure Build(const ATickData: TArray<TTickData>;
|
procedure Build(const ATickData: TArray<TAskBid.TTick>;
|
||||||
ASelectedTimeframeSeconds: Int64;
|
ASelectedTimeframeSeconds: Int64;
|
||||||
AAutoAggCandleWidth: Integer;
|
AAutoAggCandleWidth: Integer;
|
||||||
AAutoAggCandleSpacing: Integer;
|
AAutoAggCandleSpacing: Integer;
|
||||||
@@ -189,7 +188,7 @@ begin
|
|||||||
Result := Length(FCachedCandles);
|
Result := Length(FCachedCandles);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TOHLC.Build(const ATickData: TArray<TTickData>;
|
procedure TOHLC.Build(const ATickData: TArray<TAskBid.TTick>;
|
||||||
ASelectedTimeframeSeconds: Int64;
|
ASelectedTimeframeSeconds: Int64;
|
||||||
AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; AAutoAggPaintBoxClientWidth: Integer;
|
AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; AAutoAggPaintBoxClientWidth: Integer;
|
||||||
AMemoForLogging: TStrings; AGenericCandleBuilder: IGenericCandleBuilder = nil); // Parameter changed
|
AMemoForLogging: TStrings; AGenericCandleBuilder: IGenericCandleBuilder = nil); // Parameter changed
|
||||||
@@ -201,7 +200,7 @@ var
|
|||||||
CandleList: TList<TCachedCandle>;
|
CandleList: TList<TCachedCandle>;
|
||||||
TempCandle: TCachedCandle;
|
TempCandle: TCachedCandle;
|
||||||
LogTimePerCandleStr: string;
|
LogTimePerCandleStr: string;
|
||||||
CurrentTick: TTickData;
|
CurrentTick: TAskBid.TTick;
|
||||||
begin
|
begin
|
||||||
if AMemoForLogging <> nil then
|
if AMemoForLogging <> nil then
|
||||||
AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - TOHLC.Build: TF Secs: %d...',
|
AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - TOHLC.Build: TF Secs: %d...',
|
||||||
@@ -217,14 +216,14 @@ begin
|
|||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
FGlobalMinY := ATickData[0].Ask;
|
FGlobalMinY := ATickData[0].Data.Ask; // Changed
|
||||||
FGlobalMaxY := ATickData[0].Ask;
|
FGlobalMaxY := ATickData[0].Data.Ask; // Changed
|
||||||
for I := 1 to High(ATickData) do
|
for I := 1 to High(ATickData) do
|
||||||
begin
|
begin
|
||||||
if ATickData[I].Ask < FGlobalMinY then
|
if ATickData[I].Data.Ask < FGlobalMinY then // Changed
|
||||||
FGlobalMinY := ATickData[I].Ask;
|
FGlobalMinY := ATickData[I].Data.Ask; // Changed
|
||||||
if ATickData[I].Ask > FGlobalMaxY then
|
if ATickData[I].Data.Ask > FGlobalMaxY then // Changed
|
||||||
FGlobalMaxY := ATickData[I].Ask;
|
FGlobalMaxY := ATickData[I].Data.Ask; // Changed
|
||||||
end;
|
end;
|
||||||
if FGlobalMaxY = FGlobalMinY then
|
if FGlobalMaxY = FGlobalMinY then
|
||||||
begin
|
begin
|
||||||
@@ -257,10 +256,10 @@ begin
|
|||||||
if NumDataPoints > 0 then
|
if NumDataPoints > 0 then
|
||||||
begin
|
begin
|
||||||
DataIndexStart := 0;
|
DataIndexStart := 0;
|
||||||
OpenPrice := ATickData[0].Ask;
|
OpenPrice := ATickData[0].Data.Ask; // Changed
|
||||||
HighPrice := ATickData[0].Ask;
|
HighPrice := ATickData[0].Data.Ask; // Changed
|
||||||
LowPrice := ATickData[0].Ask;
|
LowPrice := ATickData[0].Data.Ask; // Changed
|
||||||
ClosePrice := ATickData[0].Ask;
|
ClosePrice := ATickData[0].Data.Ask; // Changed
|
||||||
|
|
||||||
for I := 0 to NumDataPoints - 1 do
|
for I := 0 to NumDataPoints - 1 do
|
||||||
begin
|
begin
|
||||||
@@ -272,7 +271,7 @@ begin
|
|||||||
TempCandle.OpenPrice := OpenPrice;
|
TempCandle.OpenPrice := OpenPrice;
|
||||||
TempCandle.HighPrice := HighPrice;
|
TempCandle.HighPrice := HighPrice;
|
||||||
TempCandle.LowPrice := LowPrice;
|
TempCandle.LowPrice := LowPrice;
|
||||||
TempCandle.ClosePrice := ATickData[I - 1].Ask; // Close of previous candle is the tick before the new bar starts
|
TempCandle.ClosePrice := ATickData[I - 1].Data.Ask; // Close of previous candle is the tick before the new bar starts // Changed
|
||||||
TempCandle.OriginalDataIndexStart := DataIndexStart;
|
TempCandle.OriginalDataIndexStart := DataIndexStart;
|
||||||
TempCandle.OriginalDataIndexEnd := I - 1;
|
TempCandle.OriginalDataIndexEnd := I - 1;
|
||||||
TempCandle.TickVolume := (I - 1) - DataIndexStart + 1;
|
TempCandle.TickVolume := (I - 1) - DataIndexStart + 1;
|
||||||
@@ -280,27 +279,27 @@ begin
|
|||||||
|
|
||||||
// Current tick 'I' starts a new candle
|
// Current tick 'I' starts a new candle
|
||||||
DataIndexStart := I;
|
DataIndexStart := I;
|
||||||
OpenPrice := CurrentTick.Ask;
|
OpenPrice := CurrentTick.Data.Ask; // Changed
|
||||||
HighPrice := CurrentTick.Ask;
|
HighPrice := CurrentTick.Data.Ask; // Changed
|
||||||
LowPrice := CurrentTick.Ask;
|
LowPrice := CurrentTick.Data.Ask; // Changed
|
||||||
ClosePrice := CurrentTick.Ask;
|
ClosePrice := CurrentTick.Data.Ask; // Changed
|
||||||
end
|
end
|
||||||
else // Current tick continues the existing candle OR is the first tick of the very first candle
|
else // Current tick continues the existing candle OR is the first tick of the very first candle
|
||||||
begin
|
begin
|
||||||
if I = DataIndexStart then // This is the first tick of the current (or overall first) candle
|
if I = DataIndexStart then // This is the first tick of the current (or overall first) candle
|
||||||
begin
|
begin
|
||||||
OpenPrice := CurrentTick.Ask; // Open is set
|
OpenPrice := CurrentTick.Data.Ask; // Open is set // Changed
|
||||||
HighPrice := CurrentTick.Ask; // Initial High
|
HighPrice := CurrentTick.Data.Ask; // Initial High // Changed
|
||||||
LowPrice := CurrentTick.Ask; // Initial Low
|
LowPrice := CurrentTick.Data.Ask; // Initial Low // Changed
|
||||||
end
|
end
|
||||||
else // Update High/Low for ongoing candle
|
else // Update High/Low for ongoing candle
|
||||||
begin
|
begin
|
||||||
if CurrentTick.Ask > HighPrice then
|
if CurrentTick.Data.Ask > HighPrice then // Changed
|
||||||
HighPrice := CurrentTick.Ask;
|
HighPrice := CurrentTick.Data.Ask; // Changed
|
||||||
if CurrentTick.Ask < LowPrice then
|
if CurrentTick.Data.Ask < LowPrice then // Changed
|
||||||
LowPrice := CurrentTick.Ask;
|
LowPrice := CurrentTick.Data.Ask; // Changed
|
||||||
end;
|
end;
|
||||||
ClosePrice := CurrentTick.Ask; // Always update close to current tick
|
ClosePrice := CurrentTick.Data.Ask; // Always update close to current tick // Changed
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// If this is the last tick in the dataset, finalize the current candle
|
// If this is the last tick in the dataset, finalize the current candle
|
||||||
@@ -379,15 +378,15 @@ begin
|
|||||||
Continue;
|
Continue;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
OpenPrice := ATickData[DataIndexStart].Ask;
|
OpenPrice := ATickData[DataIndexStart].Data.Ask; // Changed
|
||||||
ClosePrice := ATickData[DataIndexEnd].Ask;
|
ClosePrice := ATickData[DataIndexEnd].Data.Ask; // Changed
|
||||||
HighPrice := OpenPrice; // Initialize with Open
|
HighPrice := OpenPrice; // Initialize with Open
|
||||||
LowPrice := OpenPrice; // Initialize with Open
|
LowPrice := OpenPrice; // Initialize with Open
|
||||||
|
|
||||||
for J := DataIndexStart to DataIndexEnd do
|
for J := DataIndexStart to DataIndexEnd do
|
||||||
begin
|
begin
|
||||||
if ATickData[J].Ask > HighPrice then HighPrice := ATickData[J].Ask;
|
if ATickData[J].Data.Ask > HighPrice then HighPrice := ATickData[J].Data.Ask; // Changed
|
||||||
if ATickData[J].Ask < LowPrice then LowPrice := ATickData[J].Ask;
|
if ATickData[J].Data.Ask < LowPrice then LowPrice := ATickData[J].Data.Ask; // Changed
|
||||||
end;
|
end;
|
||||||
// Ensure High/Low encompass Open/Close after loop
|
// Ensure High/Low encompass Open/Close after loop
|
||||||
HighPrice := Max(HighPrice, Max(OpenPrice, ClosePrice));
|
HighPrice := Max(HighPrice, Max(OpenPrice, ClosePrice));
|
||||||
@@ -409,10 +408,10 @@ begin
|
|||||||
SetLength(FCachedCandles, NumDataPoints);
|
SetLength(FCachedCandles, NumDataPoints);
|
||||||
for I := 0 to NumDataPoints - 1 do
|
for I := 0 to NumDataPoints - 1 do
|
||||||
begin
|
begin
|
||||||
FCachedCandles[I].OpenPrice := ATickData[I].Ask;
|
FCachedCandles[I].OpenPrice := ATickData[I].Data.Ask; // Changed
|
||||||
FCachedCandles[I].HighPrice := ATickData[I].Ask;
|
FCachedCandles[I].HighPrice := ATickData[I].Data.Ask; // Changed
|
||||||
FCachedCandles[I].LowPrice := ATickData[I].Ask;
|
FCachedCandles[I].LowPrice := ATickData[I].Data.Ask; // Changed
|
||||||
FCachedCandles[I].ClosePrice := ATickData[I].Ask;
|
FCachedCandles[I].ClosePrice := ATickData[I].Data.Ask; // Changed
|
||||||
FCachedCandles[I].OriginalDataIndexStart := I;
|
FCachedCandles[I].OriginalDataIndexStart := I;
|
||||||
FCachedCandles[I].OriginalDataIndexEnd := I;
|
FCachedCandles[I].OriginalDataIndexEnd := I;
|
||||||
FCachedCandles[I].TickVolume := 1;
|
FCachedCandles[I].TickVolume := 1;
|
||||||
@@ -438,10 +437,10 @@ begin
|
|||||||
BucketEndTimeOADate := BucketStartTimeOADate + TimeframeIntervalOADays;
|
BucketEndTimeOADate := BucketStartTimeOADate + TimeframeIntervalOADays;
|
||||||
|
|
||||||
DataIndexStart := CurrentTickIndex;
|
DataIndexStart := CurrentTickIndex;
|
||||||
OpenPrice := ATickData[CurrentTickIndex].Ask;
|
OpenPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
||||||
HighPrice := ATickData[CurrentTickIndex].Ask;
|
HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
||||||
LowPrice := ATickData[CurrentTickIndex].Ask;
|
LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
||||||
ClosePrice := ATickData[CurrentTickIndex].Ask; // Initialize close with open
|
ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Initialize close with open // Changed
|
||||||
DataIndexEnd := CurrentTickIndex; // Initialize end with start
|
DataIndexEnd := CurrentTickIndex; // Initialize end with start
|
||||||
|
|
||||||
// Iterate through ticks that fall into the current bucket
|
// Iterate through ticks that fall into the current bucket
|
||||||
@@ -449,9 +448,9 @@ begin
|
|||||||
(ATickData[CurrentTickIndex].OADateTime >= BucketStartTimeOADate) and
|
(ATickData[CurrentTickIndex].OADateTime >= BucketStartTimeOADate) and
|
||||||
(ATickData[CurrentTickIndex].OADateTime < BucketEndTimeOADate) do
|
(ATickData[CurrentTickIndex].OADateTime < BucketEndTimeOADate) do
|
||||||
begin
|
begin
|
||||||
if ATickData[CurrentTickIndex].Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Ask;
|
if ATickData[CurrentTickIndex].Data.Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
||||||
if ATickData[CurrentTickIndex].Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Ask;
|
if ATickData[CurrentTickIndex].Data.Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
|
||||||
ClosePrice := ATickData[CurrentTickIndex].Ask; // Continuously update close price
|
ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Continuously update close price // Changed
|
||||||
DataIndexEnd := CurrentTickIndex; // Mark the last tick included in this candle
|
DataIndexEnd := CurrentTickIndex; // Mark the last tick included in this candle
|
||||||
Inc(CurrentTickIndex);
|
Inc(CurrentTickIndex);
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -1,454 +0,0 @@
|
|||||||
unit Myc.TickLoader;
|
|
||||||
|
|
||||||
(* ------------------------------------------------------------------------------
|
|
||||||
Unit: Myc.TickLoader
|
|
||||||
Author: Delphi Algo Trading (Assistant)
|
|
||||||
Date: May 27, 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 follow the naming convention:
|
|
||||||
For .tab files: Symbol_Year_MM.tab OR Symbol_Year_MM.tab_zip
|
|
||||||
For .tab-live files: Symbol_Year_MM.tab-live (NEVER compressed)
|
|
||||||
(MM represents the two-digit month, e.g., 01 for January, 12 for December)
|
|
||||||
|
|
||||||
If both compressed (.tab_zip) and uncompressed (.tab) versions of a .tab
|
|
||||||
file exist, the compressed version is preferred. Files ending with _zip
|
|
||||||
are expected to be ZIP archives containing the actual data file
|
|
||||||
(e.g., Symbol_Year_MM.tab).
|
|
||||||
|
|
||||||
Loading occurs in two phases:
|
|
||||||
1. All .tab files (regular or _zip) are processed chronologically by
|
|
||||||
month and year. 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 (which are never compressed), for months/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 monthly/yearly files.
|
|
||||||
|
|
||||||
Main Function:
|
|
||||||
LoadTickDataSeries - Loads a series of tick data files.
|
|
||||||
------------------------------------------------------------------------------ *)
|
|
||||||
|
|
||||||
interface
|
|
||||||
|
|
||||||
uses
|
|
||||||
System.SysUtils,
|
|
||||||
System.Classes,
|
|
||||||
System.Generics.Collections,
|
|
||||||
System.IOUtils,
|
|
||||||
Myc.Futures, // Added for TFuture in ReadFileTickData
|
|
||||||
Myc.Signals; // Added for IMycState (dependency of TFuture)
|
|
||||||
|
|
||||||
type
|
|
||||||
TTickData = packed record
|
|
||||||
OADateTime: Double;
|
|
||||||
Ask: Single;
|
|
||||||
Bid: Single;
|
|
||||||
end;
|
|
||||||
|
|
||||||
PTTickData = ^TTickData;
|
|
||||||
|
|
||||||
// TryParseFileName parses filenames like "Symbol_Year_MM.Extension" or "Symbol_Year_MM.Extension_zip"
|
|
||||||
// to extract Path, Symbol, Year, and Month.
|
|
||||||
function TryParseFileName( const FileName: string;
|
|
||||||
out PathValue, SymbolValue: string;
|
|
||||||
out YearValue, MonthValue: Integer ): Boolean;
|
|
||||||
|
|
||||||
function LoadTickDataSeries(const FileName: string): TFuture<TArray<TTickData>>;
|
|
||||||
|
|
||||||
// Asynchronously reads tick data from a single specified file.
|
|
||||||
// Returns a TFuture that will provide an array of TTickData.
|
|
||||||
function ReadFileTickData( var LoadGate: TLatch; const FileName: string ): TFuture<TArray<TTickData>>;
|
|
||||||
|
|
||||||
implementation
|
|
||||||
|
|
||||||
uses
|
|
||||||
System.Zip,
|
|
||||||
// Myc.Futures, Myc.Signals already in interface uses for TFuture type visibility
|
|
||||||
Myc.TaskManager; // Added for the global TaskManager used by TFuture.Construct
|
|
||||||
|
|
||||||
function TryParseFileName( const FileName: string;
|
|
||||||
out PathValue, SymbolValue: string;
|
|
||||||
out YearValue, MonthValue: Integer ): Boolean;
|
|
||||||
var
|
|
||||||
fileNameNoPath: string;
|
|
||||||
nameForParsing: string;
|
|
||||||
parts: TArray<string>;
|
|
||||||
begin
|
|
||||||
Result := False; // Default to failure
|
|
||||||
PathValue := ''; // Initialize all out parameters
|
|
||||||
SymbolValue := '';
|
|
||||||
YearValue := 0;
|
|
||||||
MonthValue := 0;
|
|
||||||
|
|
||||||
PathValue := TPath.GetDirectoryName( FileName );
|
|
||||||
fileNameNoPath := TPath.GetFileName( FileName );
|
|
||||||
|
|
||||||
nameForParsing := fileNameNoPath;
|
|
||||||
if nameForParsing.EndsWith( '_zip', True ) then
|
|
||||||
begin
|
|
||||||
nameForParsing := nameForParsing.Substring( 0, nameForParsing.Length - 4 );
|
|
||||||
end;
|
|
||||||
|
|
||||||
nameForParsing := TPath.GetFileNameWithoutExtension( nameForParsing ); // e.g., "EURUSD_2023_01"
|
|
||||||
|
|
||||||
parts := nameForParsing.Split( ['_'] );
|
|
||||||
|
|
||||||
if Length( parts ) < 3 then // Need at least Symbol_Year_Month
|
|
||||||
begin
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Month is the last part.
|
|
||||||
if not TryStrToInt( parts[High( parts )], MonthValue ) then
|
|
||||||
begin
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
if ( MonthValue < 1 ) or ( MonthValue > 12 ) then // Validate month range
|
|
||||||
begin
|
|
||||||
MonthValue := 0; // Set to invalid if out of range
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Year is the second to last part.
|
|
||||||
if not TryStrToInt( parts[High( parts ) - 1], YearValue ) then
|
|
||||||
begin
|
|
||||||
MonthValue := 0; // Also invalidate month if year parsing fails
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Symbol consists of all parts before Year and Month. Count = Length(parts) - 2
|
|
||||||
SymbolValue := string.Join( '_', Copy( parts, 0, Length( parts ) - 2 ) );
|
|
||||||
|
|
||||||
if SymbolValue = '' then // Ensure symbol is not empty
|
|
||||||
begin
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := True;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function LoadTickDataSeries(const FileName: string): TFuture<TArray<TTickData>>;
|
|
||||||
// Local type declaration for storing file information for phase 1 processing
|
|
||||||
type
|
|
||||||
TPhase1FileEntry = record
|
|
||||||
Path: string;
|
|
||||||
Year: Integer;
|
|
||||||
Month: Integer;
|
|
||||||
end;
|
|
||||||
var
|
|
||||||
initialYear, initialMonth: Integer;
|
|
||||||
currentTabYear, currentTabMonth: Integer;
|
|
||||||
pathName, symbolName: string;
|
|
||||||
tabFileEntryRecord: TPhase1FileEntry; // Variable to construct record instances
|
|
||||||
loadedState: TState;
|
|
||||||
loadedFiles: TArray<TFuture<TArray<TTickData>>>;
|
|
||||||
liveData: TFuture<TArray<TTickData>>;
|
|
||||||
|
|
||||||
begin // Start of LoadTickDataSeries
|
|
||||||
if not TryParseFileName( FileName, pathName, symbolName, initialYear, initialMonth ) then
|
|
||||||
begin
|
|
||||||
raise EArgumentException.CreateFmt(
|
|
||||||
'Invalid initial file name format: %s. Expected Symbol_Year_MM.Extension or Symbol_Year_MM.Extension_zip', [FileName] );
|
|
||||||
end;
|
|
||||||
|
|
||||||
currentTabYear := initialYear;
|
|
||||||
currentTabMonth := initialMonth;
|
|
||||||
var tabBaseNameFormat: string;
|
|
||||||
var compressedTabPath, uncompressedTabPath: string;
|
|
||||||
var actualTabFileToLoad: string;
|
|
||||||
var maxTabYearFound := -1;
|
|
||||||
var maxTabMonthFound := -1;
|
|
||||||
|
|
||||||
var filesToLoadPhase1 := TList<TPhase1FileEntry>.Create; // Create list with the named record type
|
|
||||||
try
|
|
||||||
while True do
|
|
||||||
begin
|
|
||||||
tabBaseNameFormat := Format( '%s_%.4d_%.2d.tab', [symbolName, currentTabYear, currentTabMonth] );
|
|
||||||
compressedTabPath := TPath.Combine( pathName, tabBaseNameFormat + '_zip' );
|
|
||||||
uncompressedTabPath := TPath.Combine( pathName, tabBaseNameFormat );
|
|
||||||
|
|
||||||
actualTabFileToLoad := '';
|
|
||||||
if TFile.Exists( compressedTabPath ) then
|
|
||||||
begin
|
|
||||||
actualTabFileToLoad := compressedTabPath;
|
|
||||||
end
|
|
||||||
else if TFile.Exists( uncompressedTabPath ) then
|
|
||||||
begin
|
|
||||||
actualTabFileToLoad := uncompressedTabPath;
|
|
||||||
end;
|
|
||||||
|
|
||||||
if actualTabFileToLoad = '' then
|
|
||||||
break; // No more sequential .tab files
|
|
||||||
|
|
||||||
// Construct the record and add it to the list
|
|
||||||
tabFileEntryRecord.Path := actualTabFileToLoad;
|
|
||||||
tabFileEntryRecord.Year := currentTabYear;
|
|
||||||
tabFileEntryRecord.Month := currentTabMonth;
|
|
||||||
filesToLoadPhase1.Add( tabFileEntryRecord );
|
|
||||||
|
|
||||||
// Advance to the next month
|
|
||||||
Inc( currentTabMonth );
|
|
||||||
if currentTabMonth > 12 then
|
|
||||||
begin
|
|
||||||
currentTabMonth := 1;
|
|
||||||
Inc( currentTabYear );
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Now load and process identified .tab files
|
|
||||||
var loadedFileList := TList<TFuture<TArray<TTickData>>>.Create;
|
|
||||||
var loadStates := TList<TState>.Create;
|
|
||||||
try
|
|
||||||
var LoadGate: TLatch;
|
|
||||||
|
|
||||||
for var tabFileEntry: TPhase1FileEntry in filesToLoadPhase1 do // Iterate with the correct type
|
|
||||||
begin
|
|
||||||
var data := ReadFileTickData( LoadGate, tabFileEntry.Path );
|
|
||||||
|
|
||||||
loadedFileList.Add( TFuture<TArray<TTickData>>.Create( data ) );
|
|
||||||
loadStates.Add( data.Done );
|
|
||||||
|
|
||||||
if tabFileEntry.Year > maxTabYearFound then
|
|
||||||
begin
|
|
||||||
maxTabYearFound := tabFileEntry.Year;
|
|
||||||
maxTabMonthFound := tabFileEntry.Month;
|
|
||||||
end
|
|
||||||
else if ( tabFileEntry.Year = maxTabYearFound ) and ( tabFileEntry.Month > maxTabMonthFound ) then
|
|
||||||
begin
|
|
||||||
maxTabMonthFound := tabFileEntry.Month;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
var liveFileBaseName := Format( '%s_%d_%02d.tab-live', [symbolName, maxTabYearFound, maxTabMonthFound] );
|
|
||||||
liveData := ReadFileTickData( LoadGate, TPath.Combine( pathName, liveFileBaseName ) );
|
|
||||||
loadStates.Add( liveData.Done );
|
|
||||||
loadedFileList.Add( liveData );
|
|
||||||
|
|
||||||
loadedState := TState.All( loadStates.ToArray );
|
|
||||||
loadedFiles := loadedFileList.ToArray;
|
|
||||||
finally
|
|
||||||
loadStates.Free;
|
|
||||||
loadedFileList.Free;
|
|
||||||
end;
|
|
||||||
finally
|
|
||||||
filesToLoadPhase1.Free;
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := TFuture<TArray<TTickData>>.Construct(
|
|
||||||
loadedState,
|
|
||||||
function: TArray<TTickData>
|
|
||||||
begin
|
|
||||||
var tickList := TList<TTickData>.Create;
|
|
||||||
try
|
|
||||||
var totalTicks := Length(liveData.Result);
|
|
||||||
var overallLastTabTickTime := 0.0;
|
|
||||||
|
|
||||||
for var P in loadedFiles do
|
|
||||||
inc( totalTicks, Length(P.Result) );
|
|
||||||
|
|
||||||
tickList.Capacity := totalTicks;
|
|
||||||
|
|
||||||
for var P in loadedFiles do
|
|
||||||
begin
|
|
||||||
if Length(P.Result) > 0 then
|
|
||||||
begin
|
|
||||||
inc( totalTicks, Length(P.Result) );
|
|
||||||
|
|
||||||
for var iterTickData in P.Result do
|
|
||||||
if iterTickData.OADateTime > overallLastTabTickTime then
|
|
||||||
begin
|
|
||||||
tickList.Add( iterTickData );
|
|
||||||
overallLastTabTickTime := iterTickData.OADateTime;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := tickList.ToArray;
|
|
||||||
finally
|
|
||||||
tickList.Free;
|
|
||||||
end;
|
|
||||||
end );
|
|
||||||
end;
|
|
||||||
|
|
||||||
function LoadSingleFileTickDataFromStream(const InputStream: TStream): TArray<TTickData>;
|
|
||||||
var
|
|
||||||
fileSize: Int64;
|
|
||||||
recordCount: Integer;
|
|
||||||
bytesRead: Integer;
|
|
||||||
begin
|
|
||||||
// Initialize Result record
|
|
||||||
SetLength(Result, 0);
|
|
||||||
|
|
||||||
InputStream.Position := 0;
|
|
||||||
|
|
||||||
fileSize := InputStream.Size;
|
|
||||||
if fileSize = 0 then
|
|
||||||
exit;
|
|
||||||
|
|
||||||
if (fileSize mod SizeOf(TTickData)) <> 0 then
|
|
||||||
raise EReadError.CreateFmt('File corrupted. Size %d is not a multiple of record size %d.',
|
|
||||||
[fileSize, SizeOf(TTickData)]);
|
|
||||||
|
|
||||||
recordCount := fileSize div SizeOf(TTickData);
|
|
||||||
if recordCount > 0 then
|
|
||||||
begin
|
|
||||||
SetLength(Result, recordCount);
|
|
||||||
bytesRead := InputStream.Read(Result[0], fileSize);
|
|
||||||
if bytesRead <> fileSize then
|
|
||||||
raise EReadError.CreateFmt('Read error. Expected to read %d bytes, but actually read %d bytes.',
|
|
||||||
[fileSize, bytesRead]);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function LoadUncompressedSingleFileTickData(const FullFileName: string): TArray<TTickData>;
|
|
||||||
var
|
|
||||||
inputStream: TStream;
|
|
||||||
begin
|
|
||||||
// Initialize Result record
|
|
||||||
SetLength(Result, 0);
|
|
||||||
|
|
||||||
if not TFile.Exists(FullFileName) then
|
|
||||||
begin
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
inputStream := TFileStream.Create(FullFileName, fmOpenRead or fmShareDenyWrite);
|
|
||||||
try
|
|
||||||
try
|
|
||||||
Result := LoadSingleFileTickDataFromStream( inputStream );
|
|
||||||
|
|
||||||
except
|
|
||||||
on E: EReadError do
|
|
||||||
begin
|
|
||||||
raise EReadError.CreateFmt('File %s: %s', [FullFileName, E.Message]);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
finally
|
|
||||||
inputStream.Free;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function UncompressTickDataFromStream(const InputStream: TStream): TArray<TTickData>;
|
|
||||||
var
|
|
||||||
decompressionStream: TStream; // Holds uncompressed data of a single zip entry
|
|
||||||
localHeader: TZipHeader;
|
|
||||||
entryIndex: Integer;
|
|
||||||
i: Integer;
|
|
||||||
zipFileInstance: TZipFile;
|
|
||||||
begin
|
|
||||||
// Initialize Result record
|
|
||||||
SetLength(Result, 0);
|
|
||||||
|
|
||||||
decompressionStream := nil;
|
|
||||||
zipFileInstance := nil;
|
|
||||||
|
|
||||||
try
|
|
||||||
InputStream.Position := 0; // Reset stream position to the beginning for TZipFile
|
|
||||||
|
|
||||||
// Open TZipFile from this memory stream
|
|
||||||
zipFileInstance := TZipFile.Create;
|
|
||||||
zipFileInstance.Open(InputStream, TZipMode.zmRead); // Open the zip from the memory stream
|
|
||||||
|
|
||||||
if zipFileInstance.FileCount = 0 then
|
|
||||||
begin
|
|
||||||
// If TZipFile opens an empty or invalid stream successfully but finds no files
|
|
||||||
exit; // FileSuccessfullyLoaded remains False
|
|
||||||
end;
|
|
||||||
|
|
||||||
entryIndex := -1;
|
|
||||||
for i := 0 to zipFileInstance.FileCount - 1 do
|
|
||||||
begin
|
|
||||||
if zipFileInstance.FileNames[i].EndsWith('.tab', True) then
|
|
||||||
begin
|
|
||||||
entryIndex := i;
|
|
||||||
break;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
if entryIndex = -1 then // If specific name not found, default to the first entry
|
|
||||||
begin
|
|
||||||
if zipFileInstance.FileCount > 0 then
|
|
||||||
entryIndex := 0
|
|
||||||
else
|
|
||||||
exit; // Should not happen if FileCount > 0 check passed
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Decompress the specific entry from the in-memory zip file.
|
|
||||||
// TZipFile.Read creates and populates decompressionStream with uncompressed data.
|
|
||||||
zipFileInstance.Read(entryIndex, decompressionStream, localHeader);
|
|
||||||
|
|
||||||
if not Assigned(decompressionStream) then
|
|
||||||
exit; // Exit if no valid stream could be prepared
|
|
||||||
|
|
||||||
Result := LoadSingleFileTickDataFromStream( decompressionStream );
|
|
||||||
finally
|
|
||||||
// Ensure all potentially created objects are freed
|
|
||||||
if Assigned(decompressionStream) then // Holds uncompressed data from a zip entry
|
|
||||||
decompressionStream.Free;
|
|
||||||
if Assigned(zipFileInstance) then
|
|
||||||
zipFileInstance.Free;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function ReadFileTickData( var LoadGate: TLatch; const FileName: string ): TFuture<TArray<TTickData>>;
|
|
||||||
var
|
|
||||||
parsedPath, parsedSymbol: string;
|
|
||||||
parsedYear, parsedMonth: Integer;
|
|
||||||
begin
|
|
||||||
Result := TFuture<TArray<TTickData>>.Null;
|
|
||||||
|
|
||||||
// Attempt to parse year and month from the filename
|
|
||||||
if not TryParseFileName( FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth ) then
|
|
||||||
begin
|
|
||||||
// If filename parsing fails, return a future that resolves to an empty array.
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
if FileName.EndsWith('_zip', True) then
|
|
||||||
begin
|
|
||||||
var zipFileBytes := TFile.ReadAllBytes(FileName); // Read all bytes of the .zip file
|
|
||||||
if Length(zipFileBytes) = 0 then // Handle empty zip file case
|
|
||||||
exit;
|
|
||||||
|
|
||||||
var capFileName := FileName;
|
|
||||||
var blob := TFuture<TBytes>.Construct(
|
|
||||||
TLatch.Enqueue( LoadGate ),
|
|
||||||
function: TBytes
|
|
||||||
begin
|
|
||||||
Result := TFile.ReadAllBytes(capFileName); // Read all bytes of the .zip file
|
|
||||||
end );
|
|
||||||
|
|
||||||
Result := blob.Chain<TArray<TTickData>>(
|
|
||||||
function( bytes: TBytes ): TArray<TTickData>
|
|
||||||
begin
|
|
||||||
var zipMemoryStream := TBytesStream.Create( zipFileBytes ); // Create a memory stream to hold the zip file content
|
|
||||||
try
|
|
||||||
zipMemoryStream.Position := 0;
|
|
||||||
Result := UncompressTickDataFromStream( zipMemoryStream );
|
|
||||||
finally
|
|
||||||
zipMemoryStream.Free;
|
|
||||||
end;
|
|
||||||
end );
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
var capFileName := FileName;
|
|
||||||
Result := TFuture<TArray<TTickData>>.Construct(
|
|
||||||
function: TArray<TTickData>
|
|
||||||
begin
|
|
||||||
Result := LoadUncompressedSingleFileTickData( capFileName );
|
|
||||||
end );
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
end.
|
|
||||||
Reference in New Issue
Block a user