implementing DataStream

This commit is contained in:
Michael Schimmel
2025-06-07 23:54:10 +02:00
parent d53742fd12
commit c4152d25cd
2 changed files with 174 additions and 69 deletions
+163 -56
View File
@@ -3,15 +3,20 @@ unit Myc.ChartDataController;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.StrUtils,
Myc.Futures,
Myc.Trade.DataPoint, Myc.Trade.DataStream, Myc.OHLCCache;
Myc.Trade.DataPoint,
Myc.Trade.DataStream,
Myc.OHLCCache;
type
TTimeframeSetting = record
Caption: string; // Unique key identifier (e.g., "M1", "Auto", "Every 20 Ticks")
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
Seconds: Int64; // TF_AUTO_AGGREGATION, TF_TICK_BY_TICK, TF_GENERIC_CALLBACK, or >0
end;
TChartDataController = class
@@ -21,22 +26,24 @@ type
FTimeframeSettingsArray: TArray<TTimeframeSetting>; // Now includes registered generic builders
FMemoLog: TStrings;
FGenericBuilders: TDictionary<string, IGenericCandleBuilder>; // Keyed by TTimeframeSetting.Caption
FDataServer: IDataServer<TAskBidItem>;
FDataServer: IAuraDataServer<TAskBidItem>;
procedure InitializeBaseTimeframeSettings; // Renamed for clarity
function GetGenericBuilder(const ABuilderCaption: string): IGenericCandleBuilder;
public
constructor Create(const ADataServer: IDataServer<TAskBidItem>; AMemoTarget: TStrings);
constructor Create(const ADataServer: IAuraDataServer<TAskBidItem>; 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;
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;
@@ -45,9 +52,7 @@ type
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);
procedure RegisterGenericBuilder(const ABuilderCaption: string; const ABuilderDescription: string; ABuilder: IGenericCandleBuilder);
// GetGenericBuilderCaptions is removed. Use GetAvailableTimeframes.
function GetAvailableTimeframes: TArray<TTimeframeSetting>;
end;
@@ -62,12 +67,12 @@ const
{ TChartDataController }
constructor TChartDataController.Create(const ADataServer: IDataServer<TAskBidItem>; AMemoTarget: TStrings);
constructor TChartDataController.Create(const ADataServer: IAuraDataServer<TAskBidItem>; AMemoTarget: TStrings);
var
I: Integer; // Standard loop variable
begin
inherited Create;
FDataServer:=ADataServer;
FDataServer := ADataServer;
FMemoLog := AMemoTarget;
FGenericBuilders := TDictionary<string, IGenericCandleBuilder>.Create;
@@ -105,20 +110,59 @@ 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);
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);
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;
@@ -130,9 +174,11 @@ 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);
procedure TChartDataController.RegisterGenericBuilder(
const ABuilderCaption: string;
const ABuilderDescription: string;
ABuilder: IGenericCandleBuilder
);
var
NewTimeframeSetting: TTimeframeSetting;
I: Integer;
@@ -150,7 +196,12 @@ 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]));
FMemoLog.Add(
Format(
'DataController: Cannot register generic builder. A timeframe (base or generic) with caption "%s" already exists.',
[ABuilderCaption]
)
);
Exit;
end;
end;
@@ -173,7 +224,12 @@ begin
// 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]));
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
@@ -194,7 +250,12 @@ begin
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]));
FMemoLog.Add(
Format(
'DataController: Registered new generic builder timeframe: Caption="%s", Description="%s".',
[ABuilderCaption, ABuilderDescription]
)
);
end;
end;
@@ -214,8 +275,7 @@ begin
try
FTickDataArray := FDataServer.LoadDataSeries(AFileName).WaitFor();
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Loading tick records from series: %s',
[AFileName]));
FMemoLog.Add(Format('DataController: Loading tick records from series: %s', [AFileName]));
except
on E: Exception do
begin
@@ -226,9 +286,11 @@ begin
end;
// ASelectedTimeframeCaption is the UNIQUE key (e.g. "M1", "Auto", or "Every 20 Ticks")
function TChartDataController.GetOrBuildCache(const ASelectedTimeframeCaption: string;
function TChartDataController.GetOrBuildCache(
const ASelectedTimeframeCaption: string;
AAutoAggCandleWidth, AAutoAggCandleSpacing, AAutoAggPaintBoxClientWidth: Integer;
AForceRebuild: Boolean): TOHLC;
AForceRebuild: Boolean
): TOHLC;
var
Index: Integer;
CacheNeedsRebuild: Boolean;
@@ -269,28 +331,48 @@ begin
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]));
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]));
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]));
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
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.
@@ -301,8 +383,17 @@ begin
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]));
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;
@@ -312,8 +403,12 @@ begin
if HasData then
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Building cache for [%s] (Desc: "%s")...',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
FMemoLog.Add(
Format(
'DataController: Building cache for [%s] (Desc: "%s")...',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]
)
);
if CurrentTimeframeSetting.Seconds = TF_GENERIC_CALLBACK then
begin
@@ -322,8 +417,12 @@ begin
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]));
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;
@@ -350,8 +449,12 @@ begin
else
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Using existing valid cache for [%s] (Desc: "%s")',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]));
FMemoLog.Add(
Format(
'DataController: Using existing valid cache for [%s] (Desc: "%s")',
[CurrentTimeframeSetting.Caption, CurrentTimeframeSetting.Description]
)
);
end;
Result := CacheAtIndex;
end;
@@ -394,7 +497,9 @@ begin
// 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.');
FMemoLog.Add(
'DataController: Instance data and OHLC caches cleared. Registered builders and timeframe definitions (including custom ones) remain.'
);
end;
function TChartDataController.GetTickDataCount: Integer;
@@ -414,7 +519,9 @@ begin
else
begin
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(TDataRecord<TAskBidItem>);
end;
end;