DataFeed Refactoring
This commit is contained in:
+36
-28
@@ -115,8 +115,8 @@ type
|
|||||||
FProcessDone: TState;
|
FProcessDone: TState;
|
||||||
FApplication: IAuraApplication;
|
FApplication: IAuraApplication;
|
||||||
FModulesItem: TTreeViewItem;
|
FModulesItem: TTreeViewItem;
|
||||||
FFileCache: IDataFileCache<TArray<TScalarRecordFeed.TBatch>>;
|
FFileCache: IDataFileCache<TArray<TScalarBatch>>;
|
||||||
FFeed: IScalarRecordFeed;
|
FFeed: IScalarBatchLoader;
|
||||||
function SelectedSymbol: String;
|
function SelectedSymbol: String;
|
||||||
procedure ExecuteStrategy(const Symbol: String; Timeframe: TTimeframe; const Consumer: IConsumer<TDataPoint<TOhlcItem>>);
|
procedure ExecuteStrategy(const Symbol: String; Timeframe: TTimeframe; const Consumer: IConsumer<TDataPoint<TOhlcItem>>);
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ procedure TForm1.FormCreate(Sender: TObject);
|
|||||||
begin
|
begin
|
||||||
FApplication := TMycAuraApplication.Create;
|
FApplication := TMycAuraApplication.Create;
|
||||||
|
|
||||||
FFileCache := TDataFileCache<TArray<TScalarRecordFeed.TBatch>>.Create;
|
FFileCache := TDataFileCache<TArray<TScalarBatch>>.Create;
|
||||||
|
|
||||||
FModulesItem := TTreeViewItem.Create(Self);
|
FModulesItem := TTreeViewItem.Create(Self);
|
||||||
FModulesItem.Text := 'Modules';
|
FModulesItem.Text := 'Modules';
|
||||||
@@ -386,11 +386,40 @@ begin
|
|||||||
var terminated := TFlag.CreateObserver(FTerminate.Signal).State;
|
var terminated := TFlag.CreateObserver(FTerminate.Signal).State;
|
||||||
var path := '\\COFFEE\TickData\Pepperstone'; // Or FServer.Path if compatible
|
var path := '\\COFFEE\TickData\Pepperstone'; // Or FServer.Path if compatible
|
||||||
|
|
||||||
FFeed := TScalarRecordFeed.Create(FFileCache, path, '.m1', true, def);
|
FFeed :=
|
||||||
|
TScalarBatchLoader.Create(
|
||||||
|
FFileCache,
|
||||||
|
path,
|
||||||
|
'.m1',
|
||||||
|
true,
|
||||||
|
SizeOf(TM1FileItem),
|
||||||
|
procedure(const Src: Pointer; Dst: TScalar.PValue)
|
||||||
|
var
|
||||||
|
Item: ^TM1FileItem;
|
||||||
|
begin
|
||||||
|
Item := Src;
|
||||||
|
|
||||||
|
// Field 0: Time (DateTime is stored as Double/OADate in TScalar)
|
||||||
|
Dst.AsDouble := Item.OADateTime;
|
||||||
|
|
||||||
|
// Move to next field in destination (TScalar values are contiguous)
|
||||||
|
Inc(Dst);
|
||||||
|
|
||||||
|
// Field 1: Close (Float)
|
||||||
|
// Convert Int64 price to Double using Digits: Price / 10^Digits
|
||||||
|
if Item.Digits > 0 then
|
||||||
|
Dst.AsDouble := Item.Close / Power(10, Item.Digits)
|
||||||
|
else
|
||||||
|
Dst.AsDouble := Item.Close;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
// Converter: Transforms raw IRecordSeries (Columns) into TArray<TDataPoint>
|
// Converter: Transforms raw IRecordSeries (Columns) into TArray<TDataPoint>
|
||||||
|
|
||||||
|
var unpacker: TConverter<TScalarBatch, IScalarRecord> := TScalarBatchUnpacker.Create(def);
|
||||||
|
|
||||||
var ticker :=
|
var ticker :=
|
||||||
FFeed.Ticker.Chain<TDataPoint<Double>>(
|
unpacker.Producer.Chain<TDataPoint<Double>>(
|
||||||
function(const Tick: IScalarRecord): TDataPoint<Double>
|
function(const Tick: IScalarRecord): TDataPoint<Double>
|
||||||
begin
|
begin
|
||||||
Result.Time := Tick[iTime].Value.Value.AsDouble;
|
Result.Time := Tick[iTime].Value.Value.AsDouble;
|
||||||
@@ -398,6 +427,8 @@ begin
|
|||||||
end
|
end
|
||||||
);
|
);
|
||||||
|
|
||||||
|
FFeed.BatchProducer.Chain(unpacker.Consumer);
|
||||||
|
|
||||||
// Link Chart to Ticker
|
// Link Chart to Ticker
|
||||||
chart
|
chart
|
||||||
.SetXAxisSeries(TTimeframe.M, ticker.Chain<TDateTime>(function(const p: TDataPoint<Double>): TDateTime begin Result := p.Time end));
|
.SetXAxisSeries(TTimeframe.M, ticker.Chain<TDateTime>(function(const p: TDataPoint<Double>): TDateTime begin Result := p.Time end));
|
||||||
@@ -408,29 +439,6 @@ begin
|
|||||||
TAlphaColors.Orange
|
TAlphaColors.Orange
|
||||||
);
|
);
|
||||||
|
|
||||||
FFeed.SetImportOptions(
|
|
||||||
SizeOf(TM1FileItem),
|
|
||||||
procedure(const Src: Pointer; Dst: TScalar.PValue)
|
|
||||||
var
|
|
||||||
Item: ^TM1FileItem;
|
|
||||||
begin
|
|
||||||
Item := Src;
|
|
||||||
|
|
||||||
// Field 0: Time (DateTime is stored as Double/OADate in TScalar)
|
|
||||||
Dst.AsDouble := Item.OADateTime;
|
|
||||||
|
|
||||||
// Move to next field in destination (TScalar values are contiguous)
|
|
||||||
Inc(Dst);
|
|
||||||
|
|
||||||
// Field 1: Close (Float)
|
|
||||||
// Convert Int64 price to Double using Digits: Price / 10^Digits
|
|
||||||
if Item.Digits > 0 then
|
|
||||||
Dst.AsDouble := Item.Close / Power(10, Item.Digits)
|
|
||||||
else
|
|
||||||
Dst.AsDouble := Item.Close;
|
|
||||||
end
|
|
||||||
);
|
|
||||||
|
|
||||||
// Connect pipeline: Server -> Converter
|
// Connect pipeline: Server -> Converter
|
||||||
FProcessDone := FFeed.ProcessData(symbol, terminated);
|
FProcessDone := FFeed.ProcessData(symbol, terminated);
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -130,16 +130,6 @@ type
|
|||||||
class operator Trunc(const A: TScalar): TScalar; inline;
|
class operator Trunc(const A: TScalar): TScalar; inline;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// An array of scalar values of the same kind.
|
|
||||||
TScalarArray = record
|
|
||||||
public
|
|
||||||
Kind: TScalar.TKind;
|
|
||||||
Items: TArray<TScalar.TValue>;
|
|
||||||
constructor Create(AKind: TScalar.TKind; const AItems: TArray<TScalar.TValue>);
|
|
||||||
end;
|
|
||||||
|
|
||||||
TScalarTuple = TArray<TScalar>;
|
|
||||||
|
|
||||||
// A field definition for a scalar record.
|
// A field definition for a scalar record.
|
||||||
TScalarRecordField = TPair<IKeyword, TScalar.TKind>;
|
TScalarRecordField = TPair<IKeyword, TScalar.TKind>;
|
||||||
IScalarRecordDefinition = IKeywordMapping<TScalar.TKind>;
|
IScalarRecordDefinition = IKeywordMapping<TScalar.TKind>;
|
||||||
@@ -792,14 +782,6 @@ begin
|
|||||||
Result := FromInt64(System.Trunc(val));
|
Result := FromInt64(System.Trunc(val));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TScalarArray }
|
|
||||||
|
|
||||||
constructor TScalarArray.Create(AKind: TScalar.TKind; const AItems: TArray<TScalar.TValue>);
|
|
||||||
begin
|
|
||||||
Kind := AKind;
|
|
||||||
Items := AItems;
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TScalarRecordSeries }
|
{ TScalarRecordSeries }
|
||||||
|
|
||||||
constructor TScalarRecordSeries.Create(const ADef: IScalarRecordDefinition);
|
constructor TScalarRecordSeries.Create(const ADef: IScalarRecordDefinition);
|
||||||
|
|||||||
+191
-137
@@ -8,6 +8,7 @@ uses
|
|||||||
System.Generics.Collections,
|
System.Generics.Collections,
|
||||||
System.Generics.Defaults,
|
System.Generics.Defaults,
|
||||||
System.IOUtils,
|
System.IOUtils,
|
||||||
|
System.TimeSpan,
|
||||||
Myc.Futures,
|
Myc.Futures,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Mutable,
|
Myc.Mutable,
|
||||||
@@ -18,30 +19,43 @@ uses
|
|||||||
Myc.Core.FileCache;
|
Myc.Core.FileCache;
|
||||||
|
|
||||||
type
|
type
|
||||||
|
// Callback to convert raw bytes from a file into a TScalar.TValue sequence
|
||||||
TImportConverter = reference to procedure(const Src: Pointer; Dst: TScalar.PValue);
|
TImportConverter = reference to procedure(const Src: Pointer; Dst: TScalar.PValue);
|
||||||
|
|
||||||
IScalarRecordFeed = interface
|
// Represents a raw block of data read from disk
|
||||||
{$region 'private'}
|
TScalarBatch = record
|
||||||
function GetTicker: TProducer<IScalarRecord>;
|
Data: TArray<TScalar.TValue>;
|
||||||
{$endregion}
|
RecCount: Integer;
|
||||||
|
|
||||||
procedure SetImportOptions(RecordSize: Integer; const Converter: TImportConverter);
|
|
||||||
function EnumerateSymbols: TFuture<TArray<String>>;
|
|
||||||
|
|
||||||
// Streaming processing: Reads data from disk and broadcasts it via Ticker
|
|
||||||
function ProcessData(const Symbol: String; const Terminated: TState): TState;
|
|
||||||
|
|
||||||
property Ticker: TProducer<IScalarRecord> read GetTicker;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TScalarRecordFeed = class(TInterfacedObject, IScalarRecordFeed)
|
// Converts a TScalarBatch into a stream of IScalarRecord events using the cursor pattern.
|
||||||
|
// NOTE: This uses a zero-copy approach. Consumers must process data synchronously or copy it.
|
||||||
|
TScalarBatchUnpacker = class(TMycConverter<TScalarBatch, IScalarRecord>)
|
||||||
|
private
|
||||||
|
FDef: IScalarRecordDefinition;
|
||||||
|
protected
|
||||||
|
function Consume(const Value: TScalarBatch): TState; override;
|
||||||
|
public
|
||||||
|
constructor Create(const ADef: IScalarRecordDefinition);
|
||||||
|
end;
|
||||||
|
|
||||||
|
IScalarBatchLoader = interface
|
||||||
|
{$region 'private'}
|
||||||
|
function GetBatchProducer: TProducer<TScalarBatch>;
|
||||||
|
{$endregion}
|
||||||
|
|
||||||
|
function EnumerateSymbols: TFuture<TArray<String>>;
|
||||||
|
|
||||||
|
// Streaming processing: Reads data from disk and broadcasts it via BatchProducer
|
||||||
|
function ProcessData(const Symbol: String; const Terminated: TState): TState;
|
||||||
|
|
||||||
|
// Provides raw TScalarBatch events
|
||||||
|
property BatchProducer: TProducer<TScalarBatch> read GetBatchProducer;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TScalarBatchLoader = class(TInterfacedObject, IScalarBatchLoader)
|
||||||
public
|
public
|
||||||
type
|
type
|
||||||
TBatch = record
|
|
||||||
Data: TArray<TScalar.TValue>;
|
|
||||||
RecCount: Integer;
|
|
||||||
end;
|
|
||||||
|
|
||||||
TDataFile = record
|
TDataFile = record
|
||||||
private
|
private
|
||||||
FExtension: String;
|
FExtension: String;
|
||||||
@@ -65,16 +79,15 @@ type
|
|||||||
|
|
||||||
var
|
var
|
||||||
FPath: String;
|
FPath: String;
|
||||||
FDef: IScalarRecordDefinition;
|
|
||||||
FLookback: Int64;
|
FLookback: Int64;
|
||||||
FFileExtension: String;
|
FFileExtension: String;
|
||||||
FIsCompressed: Boolean;
|
FIsCompressed: Boolean;
|
||||||
|
|
||||||
FCachedFiles: IDataFileCache<TArray<TBatch>>;
|
FCachedFiles: IDataFileCache<TArray<TScalarBatch>>;
|
||||||
FSymbols: TFuture<TDictionary<String, TArray<TDataFile>>>;
|
FSymbols: TFuture<TDictionary<String, TArray<TDataFile>>>;
|
||||||
|
|
||||||
// Producer implementation
|
// Internal producer for raw batches
|
||||||
FTicker: TMycContainedProducer<IScalarRecord>;
|
FBatchProducer: TMycContainedProducer<TScalarBatch>;
|
||||||
|
|
||||||
class var
|
class var
|
||||||
FLoadGate: TLatch;
|
FLoadGate: TLatch;
|
||||||
@@ -82,54 +95,57 @@ type
|
|||||||
function GetPath: String;
|
function GetPath: String;
|
||||||
|
|
||||||
// Loading Logic
|
// Loading Logic
|
||||||
function DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TBatch>>;
|
function DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TScalarBatch>>;
|
||||||
function LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TBatch>>;
|
function LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TScalarBatch>>;
|
||||||
|
|
||||||
// Processing Logic
|
// Processing Logic
|
||||||
function ProcessFile(
|
function ProcessFile(
|
||||||
const Files: TArray<TDataFile>;
|
const Files: TArray<TDataFile>;
|
||||||
Index: Integer;
|
Index: Integer;
|
||||||
const DataFile: TFuture<TArray<TBatch>>;
|
const DataFile: TFuture<TArray<TScalarBatch>>;
|
||||||
const Terminated: TState
|
const Terminated: TState
|
||||||
): TState;
|
): TState;
|
||||||
|
|
||||||
function ProcessChunks(const Batches: TArray<TBatch>; Terminated: TState): TState;
|
function ProcessChunks(const Batches: TArray<TScalarBatch>; Terminated: TState): TState;
|
||||||
|
|
||||||
// Binary I/O
|
// Binary I/O
|
||||||
function ReadCompressedData(const InputStream: TStream): TArray<TBatch>;
|
function ReadCompressedData(const InputStream: TStream): TArray<TScalarBatch>;
|
||||||
function ReadUncompressedData(const InputStream: TStream): TArray<TBatch>;
|
function ReadUncompressedData(const InputStream: TStream): TArray<TScalarBatch>;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function ParseFileName(const FileName: string): TDataFile;
|
function ParseFileName(const FileName: string): TDataFile;
|
||||||
procedure UpdateSymbols;
|
procedure UpdateSymbols;
|
||||||
|
|
||||||
private
|
private
|
||||||
FImportRecordSize: Integer;
|
FRecordSize: Integer;
|
||||||
FImportConverter: TImportConverter;
|
FConverter: TImportConverter;
|
||||||
|
|
||||||
function GetTicker: TProducer<IScalarRecord>;
|
function GetBatchProducer: TProducer<TScalarBatch>;
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create(
|
constructor Create(
|
||||||
const ACachedFiles: IDataFileCache<TArray<TBatch>>;
|
const ACachedFiles: IDataFileCache<TArray<TScalarBatch>>;
|
||||||
const APath, AExtension: String;
|
const APath, AExtension: String;
|
||||||
IsCompressed: Boolean;
|
IsCompressed: Boolean;
|
||||||
const ADef: IScalarRecordDefinition
|
// RecordSize defines both the input bytes per record AND the output TValues per record
|
||||||
|
ARecordSize: Integer;
|
||||||
|
const AConverter: TImportConverter
|
||||||
);
|
);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
procedure AfterConstruction; override;
|
procedure AfterConstruction; override;
|
||||||
|
|
||||||
procedure SetImportOptions(RecordSize: Integer; const Converter: TImportConverter);
|
|
||||||
function EnumerateSymbols: TFuture<TArray<String>>;
|
function EnumerateSymbols: TFuture<TArray<String>>;
|
||||||
|
|
||||||
// Streaming processing: Reads data from disk and broadcasts it via Ticker
|
// Streaming processing: Reads data from disk and broadcasts it via BatchTicker
|
||||||
function ProcessData(const Symbol: String; const Terminated: TState): TState;
|
function ProcessData(const Symbol: String; const Terminated: TState): TState;
|
||||||
|
|
||||||
|
function CreateTicker(const Def: IScalarRecordDefinition): TConverter<TScalarBatch, IScalarRecord>;
|
||||||
|
|
||||||
property Path: String read GetPath;
|
property Path: String read GetPath;
|
||||||
property FileExtension: String read FFileExtension;
|
property FileExtension: String read FFileExtension;
|
||||||
property IsCompressed: Boolean read FIsCompressed;
|
property IsCompressed: Boolean read FIsCompressed;
|
||||||
property Lookback: Int64 read FLookback write FLookback;
|
property Lookback: Int64 read FLookback write FLookback;
|
||||||
property Ticker: TProducer<IScalarRecord> read GetTicker;
|
property BatchProducer: TProducer<TScalarBatch> read GetBatchProducer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -144,8 +160,8 @@ const
|
|||||||
BatchSize = 1024;
|
BatchSize = 1024;
|
||||||
|
|
||||||
type
|
type
|
||||||
// Private implementation of the view logic.
|
// Private implementation of the view logic (Cursor Pattern).
|
||||||
// This object is reused (cursor pattern) to avoid allocations per tick.
|
// This object is reused to avoid allocations per tick.
|
||||||
TScalarRecordView = class(TInterfacedObject, IScalarRecord)
|
TScalarRecordView = class(TInterfacedObject, IScalarRecord)
|
||||||
private
|
private
|
||||||
FDef: IScalarRecordDefinition;
|
FDef: IScalarRecordDefinition;
|
||||||
@@ -204,9 +220,42 @@ begin
|
|||||||
Result := FDef.IndexOf(Key);
|
Result := FDef.IndexOf(Key);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TScalarRecordFeed.TDataFile }
|
{ TScalarBatchUnpacker }
|
||||||
|
|
||||||
constructor TScalarRecordFeed.TDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer; const ABasename: String);
|
constructor TScalarBatchUnpacker.Create(const ADef: IScalarRecordDefinition);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FDef := ADef;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TScalarBatchUnpacker.Consume(const Value: TScalarBatch): TState;
|
||||||
|
begin
|
||||||
|
if (Length(Value.Data) > 0) and (Value.RecCount > 0) then
|
||||||
|
begin
|
||||||
|
var Stride := Length(Value.Data) div Value.RecCount;
|
||||||
|
var pCursor: TScalar.PValue := @Value.Data[0];
|
||||||
|
|
||||||
|
// Initialize reused View object (Cursor Pattern)
|
||||||
|
// This object implements IScalarRecord and is updated for each tick.
|
||||||
|
var ViewObj := TScalarRecordView.Create(FDef);
|
||||||
|
var View := ViewObj as IScalarRecord;
|
||||||
|
|
||||||
|
for var i := 0 to Value.RecCount - 1 do
|
||||||
|
begin
|
||||||
|
ViewObj.SetData(pCursor);
|
||||||
|
|
||||||
|
// Broadcast to all listeners
|
||||||
|
Broadcast(View);
|
||||||
|
|
||||||
|
Inc(pCursor, Stride);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Result := TState.Null;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TScalarBatchLoader.TDataFile }
|
||||||
|
|
||||||
|
constructor TScalarBatchLoader.TDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer; const ABasename: String);
|
||||||
begin
|
begin
|
||||||
FPath := APath;
|
FPath := APath;
|
||||||
FSymbol := ASymbol;
|
FSymbol := ASymbol;
|
||||||
@@ -216,70 +265,111 @@ begin
|
|||||||
FBasename := ABasename;
|
FBasename := ABasename;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.TDataFile.GetFullFileName: string;
|
function TScalarBatchLoader.TDataFile.GetFullFileName: string;
|
||||||
begin
|
begin
|
||||||
Result := TPath.Combine(FPath, FBasename + FExtension);
|
Result := TPath.Combine(FPath, FBasename + FExtension);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.TDataFile.GetIsValid: Boolean;
|
function TScalarBatchLoader.TDataFile.GetIsValid: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := (FSymbol <> '') and (FYear > 0);
|
Result := (FSymbol <> '') and (FYear > 0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TScalarRecordFeed }
|
{ TScalarBatchLoader }
|
||||||
|
|
||||||
constructor TScalarRecordFeed.Create(
|
constructor TScalarBatchLoader.Create(
|
||||||
const ACachedFiles: IDataFileCache<TArray<TBatch>>;
|
const ACachedFiles: IDataFileCache<TArray<TScalarBatch>>;
|
||||||
const APath, AExtension: String;
|
const APath, AExtension: String;
|
||||||
IsCompressed: Boolean;
|
IsCompressed: Boolean;
|
||||||
const ADef: IScalarRecordDefinition
|
ARecordSize: Integer;
|
||||||
|
const AConverter: TImportConverter
|
||||||
);
|
);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FPath := APath;
|
FPath := APath;
|
||||||
FDef := ADef;
|
|
||||||
FCachedFiles := ACachedFiles;
|
FCachedFiles := ACachedFiles;
|
||||||
FFileExtension := AExtension;
|
FFileExtension := AExtension;
|
||||||
FIsCompressed := IsCompressed;
|
FIsCompressed := IsCompressed;
|
||||||
FLookback := -1;
|
FLookback := -1;
|
||||||
FImportRecordSize := 0;
|
|
||||||
FImportConverter := nil;
|
|
||||||
|
|
||||||
FTicker := TMycContainedProducer<IScalarRecord>.Create(Self);
|
// RecordSize is used for both Input Bytes Stride AND Output TValues Count
|
||||||
|
FRecordSize := ARecordSize;
|
||||||
|
FConverter := AConverter;
|
||||||
|
|
||||||
FCachedFiles := TDataFileCache<TArray<TBatch>>.Create;
|
Assert(FRecordSize > 0);
|
||||||
|
Assert(Assigned(FConverter));
|
||||||
|
|
||||||
|
FBatchProducer := TMycContainedProducer<TScalarBatch>.Create(Self);
|
||||||
|
FCachedFiles := TDataFileCache<TArray<TScalarBatch>>.Create;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TScalarRecordFeed.Destroy;
|
destructor TScalarBatchLoader.Destroy;
|
||||||
begin
|
begin
|
||||||
FCachedFiles.Clear;
|
FCachedFiles.Clear;
|
||||||
FTicker.Free;
|
FBatchProducer.Free;
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TScalarRecordFeed.AfterConstruction;
|
procedure TScalarBatchLoader.AfterConstruction;
|
||||||
begin
|
begin
|
||||||
inherited;
|
inherited;
|
||||||
UpdateSymbols;
|
UpdateSymbols;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TScalarRecordFeed.SetImportOptions(RecordSize: Integer; const Converter: TImportConverter);
|
function TScalarBatchLoader.CreateTicker(const Def: IScalarRecordDefinition): TConverter<TScalarBatch, IScalarRecord>;
|
||||||
|
var
|
||||||
|
viewObj: TScalarRecordView;
|
||||||
|
view: IScalarRecord;
|
||||||
begin
|
begin
|
||||||
FImportRecordSize := RecordSize;
|
Result :=
|
||||||
FImportConverter := Converter;
|
TConverter<TScalarBatch, IScalarRecord>.CreateAggregation(
|
||||||
|
function(const Value: TScalarBatch; const Broadcast: TBroadcastFunc<IScalarRecord>): TState
|
||||||
|
begin
|
||||||
|
if (Length(Value.Data) > 0) and (Value.RecCount > 0) then
|
||||||
|
begin
|
||||||
|
var Stride := Length(Value.Data) div Value.RecCount;
|
||||||
|
var pCursor: TScalar.PValue := @Value.Data[0];
|
||||||
|
|
||||||
|
for var i := 0 to Value.RecCount - 1 do
|
||||||
|
begin
|
||||||
|
if view = nil then
|
||||||
|
begin
|
||||||
|
// Initialize reused View object (Cursor Pattern)
|
||||||
|
// This object implements IScalarRecord and is updated for each tick.
|
||||||
|
viewObj := TScalarRecordView.Create(Def);
|
||||||
|
view := viewObj as IScalarRecord;
|
||||||
|
end;
|
||||||
|
|
||||||
|
var rc := viewObj._AddRef;
|
||||||
|
try
|
||||||
|
// Broadcast tick
|
||||||
|
viewObj.SetData(pCursor);
|
||||||
|
Broadcast(view);
|
||||||
|
finally
|
||||||
|
// If client picked it up, we release it
|
||||||
|
if viewObj._Release >= rc then
|
||||||
|
view := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Inc(pCursor, Stride);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Result := TState.Null;
|
||||||
|
end
|
||||||
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.GetPath: String;
|
function TScalarBatchLoader.GetPath: String;
|
||||||
begin
|
begin
|
||||||
Result := FPath;
|
Result := FPath;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.GetTicker: TProducer<IScalarRecord>;
|
function TScalarBatchLoader.GetBatchProducer: TProducer<TScalarBatch>;
|
||||||
begin
|
begin
|
||||||
Result := FTicker;
|
Result := FBatchProducer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TScalarRecordFeed.UpdateSymbols;
|
procedure TScalarBatchLoader.UpdateSymbols;
|
||||||
begin
|
begin
|
||||||
FSymbols :=
|
FSymbols :=
|
||||||
TFuture<TDictionary<String, TArray<TDataFile>>>.Construct(
|
TFuture<TDictionary<String, TArray<TDataFile>>>.Construct(
|
||||||
@@ -340,7 +430,7 @@ begin
|
|||||||
FSymbols.Manage;
|
FSymbols.Manage;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.EnumerateSymbols: TFuture<TArray<String>>;
|
function TScalarBatchLoader.EnumerateSymbols: TFuture<TArray<String>>;
|
||||||
begin
|
begin
|
||||||
Result :=
|
Result :=
|
||||||
FSymbols.Chain<TArray<String>>(
|
FSymbols.Chain<TArray<String>>(
|
||||||
@@ -348,7 +438,7 @@ begin
|
|||||||
);
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.ParseFileName(const FileName: string): TDataFile;
|
function TScalarBatchLoader.ParseFileName(const FileName: string): TDataFile;
|
||||||
var
|
var
|
||||||
path, fileNameNoPath, baseName, ext, symbol: string;
|
path, fileNameNoPath, baseName, ext, symbol: string;
|
||||||
year, month: Integer;
|
year, month: Integer;
|
||||||
@@ -389,21 +479,21 @@ begin
|
|||||||
Result := TDataFile.Create(path, symbol, ext, year, month, baseName);
|
Result := TDataFile.Create(path, symbol, ext, year, month, baseName);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TBatch>>;
|
function TScalarBatchLoader.LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TScalarBatch>>;
|
||||||
begin
|
begin
|
||||||
if DataFile.IsValid then
|
if DataFile.IsValid then
|
||||||
Result :=
|
Result :=
|
||||||
FCachedFiles.GetOrAdd(
|
FCachedFiles.GetOrAdd(
|
||||||
DataFile.GetFullFileName,
|
DataFile.GetFullFileName,
|
||||||
function(const Filename: String): TFuture<TArray<TBatch>> begin Result := DoLoad(Filename, FLoadGate); end
|
function(const Filename: String): TFuture<TArray<TScalarBatch>> begin Result := DoLoad(Filename, FLoadGate); end
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Result := TFuture<TArray<TBatch>>.Construct(TArray<TBatch>(nil));
|
Result := TFuture<TArray<TScalarBatch>>.Construct(TArray<TScalarBatch>(nil));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TBatch>>;
|
function TScalarBatchLoader.DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TScalarBatch>>;
|
||||||
begin
|
begin
|
||||||
Result := TFuture<TArray<TBatch>>.Null;
|
Result := TFuture<TArray<TScalarBatch>>.Null;
|
||||||
if not TFile.Exists(FileName) then
|
if not TFile.Exists(FileName) then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
@@ -414,8 +504,8 @@ begin
|
|||||||
Result :=
|
Result :=
|
||||||
TFuture<TBytes>
|
TFuture<TBytes>
|
||||||
.Construct(Gate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
|
.Construct(Gate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
|
||||||
.Chain<TArray<TBatch>>(
|
.Chain<TArray<TScalarBatch>>(
|
||||||
function(bytes: TBytes): TArray<TBatch>
|
function(bytes: TBytes): TArray<TScalarBatch>
|
||||||
begin
|
begin
|
||||||
var ms := TBytesStream.Create(bytes);
|
var ms := TBytesStream.Create(bytes);
|
||||||
try
|
try
|
||||||
@@ -428,9 +518,9 @@ begin
|
|||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
Result :=
|
Result :=
|
||||||
TFuture<TArray<TBatch>>.Construct(
|
TFuture<TArray<TScalarBatch>>.Construct(
|
||||||
Gate.Enqueue,
|
Gate.Enqueue,
|
||||||
function: TArray<TBatch>
|
function: TArray<TScalarBatch>
|
||||||
begin
|
begin
|
||||||
var fs := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite);
|
var fs := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite);
|
||||||
try
|
try
|
||||||
@@ -443,7 +533,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.ReadCompressedData(const InputStream: TStream): TArray<TBatch>;
|
function TScalarBatchLoader.ReadCompressedData(const InputStream: TStream): TArray<TScalarBatch>;
|
||||||
var
|
var
|
||||||
decompressionStream: TStream;
|
decompressionStream: TStream;
|
||||||
localHeader: TZipHeader;
|
localHeader: TZipHeader;
|
||||||
@@ -489,11 +579,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.ReadUncompressedData(const InputStream: TStream): TArray<TBatch>;
|
function TScalarBatchLoader.ReadUncompressedData(const InputStream: TStream): TArray<TScalarBatch>;
|
||||||
var
|
var
|
||||||
fileSize, totalRecords: Int64;
|
fileSize, totalRecords: Int64;
|
||||||
fieldsPerRecord: Integer;
|
|
||||||
internalRecordSize, sourceRecordSize: Integer;
|
|
||||||
numBatches, batchIdx, k: Integer;
|
numBatches, batchIdx, k: Integer;
|
||||||
bytesToRead, valuesToWrite: Integer;
|
bytesToRead, valuesToWrite: Integer;
|
||||||
tempBuffer: TBytes;
|
tempBuffer: TBytes;
|
||||||
@@ -504,61 +592,46 @@ begin
|
|||||||
InputStream.Position := 0;
|
InputStream.Position := 0;
|
||||||
fileSize := InputStream.Size;
|
fileSize := InputStream.Size;
|
||||||
|
|
||||||
fieldsPerRecord := FDef.Count;
|
if (fileSize = 0) or ((fileSize mod FRecordSize) <> 0) then
|
||||||
internalRecordSize := fieldsPerRecord * sizeof(TScalar.TValue);
|
|
||||||
|
|
||||||
if (FImportRecordSize > 0) and Assigned(FImportConverter) then
|
|
||||||
sourceRecordSize := FImportRecordSize
|
|
||||||
else
|
|
||||||
sourceRecordSize := internalRecordSize;
|
|
||||||
|
|
||||||
if (fileSize = 0) or ((fileSize mod sourceRecordSize) <> 0) then
|
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
totalRecords := fileSize div sourceRecordSize;
|
totalRecords := fileSize div FRecordSize;
|
||||||
if totalRecords = 0 then
|
if totalRecords = 0 then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
numBatches := (totalRecords + BatchSize - 1) div BatchSize;
|
numBatches := (totalRecords + BatchSize - 1) div BatchSize;
|
||||||
SetLength(Result, numBatches);
|
SetLength(Result, numBatches);
|
||||||
|
|
||||||
if sourceRecordSize <> internalRecordSize then
|
// Buffer for reading raw data chunks before conversion
|
||||||
SetLength(tempBuffer, BatchSize * sourceRecordSize);
|
SetLength(tempBuffer, BatchSize * FRecordSize);
|
||||||
|
|
||||||
for batchIdx := 0 to numBatches - 1 do
|
for batchIdx := 0 to numBatches - 1 do
|
||||||
begin
|
begin
|
||||||
var recordsInBatch := Min(BatchSize, totalRecords - (batchIdx * BatchSize));
|
var recordsInBatch := Min(BatchSize, totalRecords - (batchIdx * BatchSize));
|
||||||
Result[batchIdx].RecCount := recordsInBatch;
|
Result[batchIdx].RecCount := recordsInBatch;
|
||||||
valuesToWrite := recordsInBatch * fieldsPerRecord;
|
|
||||||
|
|
||||||
|
// FRecordSize determines both allocation count and input byte stride
|
||||||
|
valuesToWrite := recordsInBatch * FRecordSize;
|
||||||
SetLength(Result[batchIdx].Data, valuesToWrite);
|
SetLength(Result[batchIdx].Data, valuesToWrite);
|
||||||
|
|
||||||
if sourceRecordSize = internalRecordSize then
|
bytesToRead := recordsInBatch * FRecordSize;
|
||||||
begin
|
if InputStream.Read(tempBuffer, 0, bytesToRead) <> bytesToRead then
|
||||||
bytesToRead := valuesToWrite * sizeof(TScalar.TValue);
|
raise EReadError.Create('Unexpected end of stream');
|
||||||
if InputStream.Read(Result[batchIdx].Data[0], bytesToRead) <> bytesToRead then
|
|
||||||
raise EReadError.Create('Unexpected end of stream');
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
bytesToRead := recordsInBatch * sourceRecordSize;
|
|
||||||
if InputStream.Read(tempBuffer, 0, bytesToRead) <> bytesToRead then
|
|
||||||
raise EReadError.Create('Unexpected end of stream');
|
|
||||||
|
|
||||||
pSrc := @tempBuffer[0];
|
pSrc := @tempBuffer[0];
|
||||||
pDst := @Result[batchIdx].Data[0];
|
pDst := @Result[batchIdx].Data[0];
|
||||||
|
|
||||||
for k := 0 to recordsInBatch - 1 do
|
// Always use the converter to transform raw bytes to TValues
|
||||||
begin
|
for k := 0 to recordsInBatch - 1 do
|
||||||
FImportConverter(pSrc, pDst);
|
begin
|
||||||
Inc(pSrc, sourceRecordSize);
|
FConverter(pSrc, pDst);
|
||||||
Inc(pDst, fieldsPerRecord);
|
Inc(pSrc, FRecordSize);
|
||||||
end;
|
Inc(pDst, FRecordSize);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.ProcessData(const Symbol: String; const Terminated: TState): TState;
|
function TScalarBatchLoader.ProcessData(const Symbol: String; const Terminated: TState): TState;
|
||||||
begin
|
begin
|
||||||
var cTerminated := Terminated;
|
var cTerminated := Terminated;
|
||||||
var cSymbol := Symbol;
|
var cSymbol := Symbol;
|
||||||
@@ -583,10 +656,10 @@ begin
|
|||||||
);
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.ProcessFile(
|
function TScalarBatchLoader.ProcessFile(
|
||||||
const Files: TArray<TDataFile>;
|
const Files: TArray<TDataFile>;
|
||||||
Index: Integer;
|
Index: Integer;
|
||||||
const DataFile: TFuture<TArray<TBatch>>;
|
const DataFile: TFuture<TArray<TScalarBatch>>;
|
||||||
const Terminated: TState
|
const Terminated: TState
|
||||||
): TState;
|
): TState;
|
||||||
begin
|
begin
|
||||||
@@ -600,19 +673,19 @@ begin
|
|||||||
|
|
||||||
// Prefetch logic: Prepare the next file immediately (if available)
|
// Prefetch logic: Prepare the next file immediately (if available)
|
||||||
var nextIndex := Index + 1;
|
var nextIndex := Index + 1;
|
||||||
var nextFuture: TFuture<TArray<TBatch>>;
|
var nextFuture: TFuture<TArray<TScalarBatch>>;
|
||||||
|
|
||||||
if nextIndex <= High(Files) then
|
if nextIndex <= High(Files) then
|
||||||
nextFuture := LoadDataFile(Files[nextIndex])
|
nextFuture := LoadDataFile(Files[nextIndex])
|
||||||
else
|
else
|
||||||
nextFuture := TFuture<TArray<TBatch>>.Construct(TArray<TBatch>(nil)); // End of stream
|
nextFuture := TFuture<TArray<TScalarBatch>>.Construct(TArray<TScalarBatch>(nil)); // End of stream
|
||||||
|
|
||||||
var cTerminated := Terminated;
|
var cTerminated := Terminated;
|
||||||
|
|
||||||
// Process current data, then recurse to next index
|
// Process current data, then recurse to next index
|
||||||
Result :=
|
Result :=
|
||||||
DataFile.Chain(
|
DataFile.Chain(
|
||||||
function(const Batches: TArray<TBatch>): TState
|
function(const Batches: TArray<TScalarBatch>): TState
|
||||||
begin
|
begin
|
||||||
var done := ProcessChunks(Batches, Terminated);
|
var done := ProcessChunks(Batches, Terminated);
|
||||||
|
|
||||||
@@ -623,39 +696,20 @@ begin
|
|||||||
);
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TScalarRecordFeed.ProcessChunks(const Batches: TArray<TBatch>; Terminated: TState): TState;
|
function TScalarBatchLoader.ProcessChunks(const Batches: TArray<TScalarBatch>; Terminated: TState): TState;
|
||||||
begin
|
begin
|
||||||
var callProcess :=
|
var callProcess :=
|
||||||
function(Idx: Integer): TFunc<TState>
|
function(Idx: Integer): TFunc<TState>
|
||||||
begin
|
begin
|
||||||
var cData := Batches[Idx].Data;
|
var cBatch := Batches[Idx];
|
||||||
var cCount := Batches[Idx].RecCount;
|
|
||||||
|
|
||||||
Result :=
|
Result :=
|
||||||
function: TState
|
function: TState
|
||||||
begin
|
begin
|
||||||
if not Terminated.IsSet then
|
if not Terminated.IsSet then
|
||||||
begin
|
begin
|
||||||
if (Length(cData) > 0) and (cCount > 0) then
|
// Broadcast the raw batch.
|
||||||
begin
|
FBatchProducer.Broadcast(cBatch);
|
||||||
var Stride := Length(cData) div cCount;
|
|
||||||
var pCursor: TScalar.PValue := @cData[0];
|
|
||||||
|
|
||||||
// Initialize reused View object (Cursor Pattern)
|
|
||||||
// This object implements IScalarRecord and is updated for each tick.
|
|
||||||
var ViewObj := TScalarRecordView.Create(FDef);
|
|
||||||
var View := ViewObj as IScalarRecord;
|
|
||||||
|
|
||||||
for var i := 0 to cCount - 1 do
|
|
||||||
begin
|
|
||||||
ViewObj.SetData(pCursor);
|
|
||||||
|
|
||||||
// Broadcast to all listeners
|
|
||||||
FTicker.Broadcast(View);
|
|
||||||
|
|
||||||
Inc(pCursor, Stride);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
Result := TState.Null;
|
Result := TState.Null;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ type
|
|||||||
[TestCase('Float_Negative', '-3.1415926535,Float')]
|
[TestCase('Float_Negative', '-3.1415926535,Float')]
|
||||||
procedure TestScalarCreation(const AValueStr: string; AExpectedKind: TScalar.TKind);
|
procedure TestScalarCreation(const AValueStr: string; AExpectedKind: TScalar.TKind);
|
||||||
|
|
||||||
// More detailed tests for complex POD types.
|
|
||||||
[Test]
|
|
||||||
procedure TestScalarArray;
|
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestScalarTuple;
|
procedure TestScalarTuple;
|
||||||
end;
|
end;
|
||||||
@@ -59,25 +56,6 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestPOD.TestScalarArray;
|
|
||||||
var
|
|
||||||
arr: TScalarArray;
|
|
||||||
values: TArray<TScalar.TValue>;
|
|
||||||
begin
|
|
||||||
// Test with values
|
|
||||||
values := [TScalar.FromInt64(10).Value, TScalar.FromInt64(20).Value, TScalar.FromInt64(30).Value];
|
|
||||||
arr := TScalarArray.Create(TScalar.TKind.Ordinal, values);
|
|
||||||
|
|
||||||
Assert.AreEqual(TScalar.TKind.Ordinal, arr.Kind, 'Array kind mismatch');
|
|
||||||
Assert.AreEqual(Int64(3), Length(arr.Items), 'Array length mismatch');
|
|
||||||
Assert.AreEqual(Int64(20), arr.Items[1].AsInt64, 'Array item content mismatch');
|
|
||||||
|
|
||||||
// Test empty
|
|
||||||
arr := TScalarArray.Create(TScalar.TKind.Float, []);
|
|
||||||
Assert.AreEqual(TScalar.TKind.Float, arr.Kind, 'Empty array kind mismatch');
|
|
||||||
Assert.AreEqual(Int64(0), Length(arr.Items), 'Empty array length should be zero');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestPOD.TestScalarTuple;
|
procedure TTestPOD.TestScalarTuple;
|
||||||
var
|
var
|
||||||
tuple: TScalarTuple;
|
tuple: TScalarTuple;
|
||||||
|
|||||||
Reference in New Issue
Block a user