DataPoint

This commit is contained in:
Michael Schimmel
2025-06-24 08:11:46 +02:00
parent 6c7cc2569b
commit a3da63ad6a
14 changed files with 902 additions and 633 deletions
+95 -20
View File
@@ -13,15 +13,18 @@ type
TMycNullSignal = class(TInterfacedObject, TSignal.ISignal)
public
function Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure Unsubscribe(Tag: Pointer);
end;
// TMycNullState implements a "null object" pattern for IMycState.
// This state is always considered "set".
TMycNullState = class(TMycNullSignal, TState.IState)
TMycNullState = class(TInterfacedObject, TSignal.ISignal, TState.IState)
protected
// TState.IState
function GetIsSet: Boolean;
public
function Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
procedure Unsubscribe(Tag: Pointer);
end;
// A state that acts as an event. It's state ist always set and it will always Trigger it's subscribers, if it gets notified by itself.
@@ -35,7 +38,7 @@ type
constructor Create;
destructor Destroy; override;
function Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure Unsubscribe(Tag: Pointer);
function Notify: Boolean;
end;
@@ -45,6 +48,16 @@ type
function Notify: Boolean;
end;
TMycRouterEvent = class(TMycEvent)
private
FSignal: TSignal.ISignal;
FSubscriptionTag: TSignal.TSubscriptionTag;
public
constructor Create(const ASignal: TSignal.ISignal);
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
// TMycLatch implements a countdown latch.
// It is initialized with a count. Calls to its Notify method (as an TSignal.ISubscriber)
// decrement this count. When the count reaches zero, the latch transitions to the "set"
@@ -66,7 +79,7 @@ type
// ISignal implementation
function Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure Unsubscribe(Tag: Pointer);
// TSignal.ISubscriber implementation for TMycLatch itself.
// Decrements the internal count. If the count reaches zero,
@@ -101,7 +114,7 @@ type
class function CreateDirty: TFlag.IFlag; static;
function Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
procedure Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure Unsubscribe(Tag: Pointer);
// TSignal.ISubscriber implementation for TMycFlag.
// Sets this flag to dirty (true). Notifies subscribers if it transitioned from clean to dirty.
@@ -118,16 +131,54 @@ type
function Reset: Boolean;
end;
TMycObserverFlag = class(TMycFlag)
private
FSignal: TSignal.ISignal;
FSubscriptionTag: TSignal.TSubscriptionTag;
public
constructor Create(const ASignal: TSignal.ISignal);
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
implementation
uses
System.SyncObjs; // For TInterlocked
{ TMycNullSignal }
function TMycNullSignal.Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
begin
// No ongoing subscription is necessary or meaningful for a null state.
Result := nil;
end;
procedure TMycNullSignal.Unsubscribe(Tag: Pointer);
begin
// Unsubscribing from a null signal has no effect.
end;
{ TMycNullState }
function TMycNullState.GetIsSet: Boolean;
begin
Result := true; // Null state is always set.
end;
function TMycNullState.Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
begin
// Since the state is always set, notify the subscriber immediately.
if Assigned(Subscriber) then
Subscriber.Notify;
Result := nil;
end;
procedure TMycNullState.Unsubscribe(Tag: Pointer);
begin
// Unsubscribing from a null state has no effect.
end;
{ TMycEvent }
constructor TMycEvent.Create;
@@ -178,7 +229,7 @@ begin
end;
end;
procedure TMycEvent.Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure TMycEvent.Unsubscribe(Tag: Pointer);
begin
if Tag = nil then
exit;
@@ -191,6 +242,26 @@ begin
end;
end;
{ TMycRouterEvent }
constructor TMycRouterEvent.Create(const ASignal: TSignal.ISignal);
begin
inherited Create;
FSignal := ASignal;
end;
procedure TMycRouterEvent.AfterConstruction;
begin
inherited;
FSubscriptionTag := FSignal.Subscribe(Self);
end;
procedure TMycRouterEvent.BeforeDestruction;
begin
FSignal.Unsubscribe(FSubscriptionTag);
inherited;
end;
{ TMycNullLatch }
function TMycNullLatch.GetState: TState;
@@ -289,7 +360,7 @@ begin
end;
end;
procedure TMycLatch.Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure TMycLatch.Unsubscribe(Tag: Pointer);
begin
if Tag = nil then
exit;
@@ -324,7 +395,7 @@ end;
constructor TMycFlag.Create;
begin
inherited Create;
FFlag := true; // A new dirty flag is initially considered dirty.
FFlag := false;
FSubscribers.Create;
end;
@@ -371,9 +442,9 @@ begin
begin
FSubscribers.Notify(function(Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
end;
Result := true;
finally
FSubscribers.Release;
Result := true;
end;
end;
@@ -399,7 +470,7 @@ begin
end;
end;
procedure TMycFlag.Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure TMycFlag.Unsubscribe(Tag: Pointer);
begin
if Tag = nil then
exit;
@@ -422,20 +493,24 @@ begin
Result := false;
end;
function TMycNullSignal.Subscribe(Subscriber: TSignal.ISubscriber): Pointer;
{ TMycObserverFlag }
constructor TMycObserverFlag.Create(const ASignal: TSignal.ISignal);
begin
// Since the state is always set, notify the subscriber immediately.
if Assigned(Subscriber) then
begin
Subscriber.Notify;
end;
// No ongoing subscription is necessary or meaningful for a null state.
Result := nil;
inherited Create;
FSignal := ASignal;
end;
procedure TMycNullSignal.Unsubscribe(Tag: TMycNotifyList<TSignal.ISubscriber>.TTag);
procedure TMycObserverFlag.AfterConstruction;
begin
// Unsubscribing from a null state has no effect.
inherited;
FSubscriptionTag := FSignal.Subscribe(Self);
end;
procedure TMycObserverFlag.BeforeDestruction;
begin
FSignal.Unsubscribe(FSubscriptionTag);
inherited;
end;
end.
+3 -3
View File
@@ -34,7 +34,7 @@ type
private
FFuture: IFuture;
function GetDone: TState; inline;
function GetDone: TState.IState; inline;
function GetValue: T; inline;
{$ENDREGION}
public
@@ -55,7 +55,7 @@ type
function Chain<S>(const Proc: TFuncConst<T, S>): TFuture<S>; overload;
function WaitFor: T;
property Done: TState read GetDone;
property Done: TState.IState read GetDone;
property Value: T read GetValue;
end;
@@ -105,7 +105,7 @@ begin
Result := TMycGateFuncFuture<T>.Create(TaskManager, Gate, Proc);
end;
function TFuture<T>.GetDone: TState;
function TFuture<T>.GetDone: TState.IState;
begin
Result := FFuture.Done;
end;
+19 -17
View File
@@ -5,23 +5,25 @@ interface
uses
System.Classes,
System.SysUtils,
System.Messaging,
FMX.Controls,
Myc.Signals;
type
TFMXValidationState = class(TComponent, TSignal.ISubscriber)
TFMXValidationState = class(TComponent)
private
[volatile]
FInvalidated: Integer;
FSignal: TSignal.ISignal;
FSubscription: TSignal.TSubscriptionTag;
FSignal: TSignal;
FInvalidated: TFlag;
FSubscription: TSignal.TSubscription;
FProc: TProc;
procedure CallProc;
FIdleSubsription: TMessageSubscriptionId;
procedure DoIdle(const Sender: TObject; const M: TMessage);
protected
function Notify: Boolean;
public
constructor Create(AOwner: TControl; const ASignal: TSignal.ISignal; const AProc: TProc); reintroduce;
constructor Create(AOwner: TControl; const ASignal: TSignal; const AProc: TProc); reintroduce;
destructor Destroy; override;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
@@ -30,47 +32,47 @@ type
implementation
uses
System.SyncObjs;
System.SyncObjs,
FMX.Types;
{ TFMXValidationState }
constructor TFMXValidationState.Create(AOwner: TControl; const ASignal: TSignal.ISignal; const AProc: TProc);
constructor TFMXValidationState.Create(AOwner: TControl; const ASignal: TSignal; const AProc: TProc);
begin
inherited Create(AOwner);
FInvalidated := 0;
FSignal := ASignal;
FInvalidated := TFlag.CreateFlag;
FProc := AProc;
end;
destructor TFMXValidationState.Destroy;
begin
TThread.RemoveQueuedEvents(CallProc);
inherited;
end;
procedure TFMXValidationState.AfterConstruction;
begin
inherited;
FSubscription := FSignal.Subscribe(Self);
FSubscription := FSignal.Subscribe(FInvalidated);
FIdleSubsription := TMessageManager.DefaultManager.SubscribeToMessage(TIdleMessage, DoIdle);
end;
procedure TFMXValidationState.BeforeDestruction;
begin
FSignal.Unsubscribe(FSubscription);
TMessageManager.DefaultManager.Unsubscribe(TIdleMessage, FIdleSubsription);
FSubscription.Unsubscribe;
inherited;
end;
procedure TFMXValidationState.CallProc;
procedure TFMXValidationState.DoIdle(const Sender: TObject; const M: TMessage);
begin
if TInterlocked.Exchange(FInvalidated, 0) > 0 then
if FInvalidated.Reset then
FProc;
end;
function TFMXValidationState.Notify: Boolean;
begin
if TInterlocked.Exchange(FInvalidated, 1) = 0 then
TThread.Queue(nil, CallProc);
exit(true);
Result := FInvalidated.Notify;
end;
end.
+40 -18
View File
@@ -69,6 +69,7 @@ type
private
FState: IState;
function GetIsSet: Boolean; inline;
function GetAsSignal: TSignal; inline;
{$ENDREGION}
public
constructor Create(const AState: IState);
@@ -84,6 +85,10 @@ type
function Subscribe(Subscriber: TSignal.ISubscriber): TSignal.TSubscription; inline;
property IsSet: Boolean read GetIsSet;
// Explicitly cast a state to a signal. This is dangerous, use with care!
// States only notify, when they change. This behaviour is obscured by treating it like a signal.
property AsSignal: TSignal read GetAsSignal;
end;
// An event is a stateless signal that forwards all notifications to the subscribers.
@@ -113,6 +118,7 @@ type
class operator Implicit(const A: TEvent): IEvent; overload;
class function CreateEvent: TEvent; static;
class function CreateRouter(const Signal: TSignal): TEvent; static;
class property Null: IEvent read FNull;
@@ -141,7 +147,7 @@ type
private
FLatch: ILatch;
function GetState: TState; inline;
function GetState: TState.IState; inline;
{$ENDREGION}
public
constructor Create(const ALatch: ILatch);
@@ -149,7 +155,7 @@ type
class operator Implicit(const A: ILatch): TLatch; overload;
class operator Implicit(const A: TLatch): ILatch; overload;
class function CreateLatch(Count: Integer): TLatch; static;
class function CreateLatch(Count: Integer): ILatch; static;
class function Enqueue1(var Gate: TLatch): TState; overload; static;
function Enqueue: TState; overload;
@@ -158,7 +164,7 @@ type
function Notify: Boolean; inline;
property State: TState read GetState;
property State: TState.IState read GetState;
end;
// TFlag represents a resettable flag, typically indicating if a State is "dirty" (requiring attention) or "clean".
@@ -179,17 +185,18 @@ type
class constructor ClassCreate;
private
FDirty: IFlag;
FFlag: IFlag;
function GetState: TState; inline;
{$ENDREGION}
public
constructor Create(const ADirty: IFlag);
constructor Create(const AFlag: IFlag);
class operator Initialize(out Dest: TFlag);
class operator Implicit(const A: IFlag): TFlag; overload;
class operator Implicit(const A: TFlag): IFlag; overload;
class function CreateFlag: IFlag; static;
class function CreateFlag: TFlag; static;
class function CreateObserver(const Signal: TSignal): TFlag; static;
class property Null: IFlag read FNull;
function Notify: Boolean; inline;
@@ -268,6 +275,11 @@ begin
end;
end;
function TState.GetAsSignal: TSignal;
begin
Result := FState;
end;
function TState.GetIsSet: Boolean;
begin
Result := FState.IsSet;
@@ -312,6 +324,11 @@ begin
Result := TMycEvent.Create;
end;
class function TEvent.CreateRouter(const Signal: TSignal): TEvent;
begin
Result := TMycRouterEvent.Create(Signal);
end;
function TEvent.GetSignal: TSignal;
begin
Result := FEvent.Signal;
@@ -344,31 +361,36 @@ begin
FNull := TMycNullFlag.Create;
end;
constructor TFlag.Create(const ADirty: IFlag);
constructor TFlag.Create(const AFlag: IFlag);
begin
FDirty := ADirty;
if not Assigned(FDirty) then
FDirty := FNull;
FFlag := AFlag;
if not Assigned(FFlag) then
FFlag := FNull;
end;
class function TFlag.CreateFlag: IFlag;
class function TFlag.CreateFlag: TFlag;
begin
Result := TMycFlag.Create;
end;
class function TFlag.CreateObserver(const Signal: TSignal): TFlag;
begin
Result.Create(TMycObserverFlag.Create(Signal));
end;
function TFlag.GetState: TState;
begin
Result := FDirty.State;
Result := FFlag.State;
end;
function TFlag.Notify: Boolean;
begin
Result := FDirty.Notify;
Result := FFlag.Notify;
end;
function TFlag.Reset: Boolean;
begin
Result := FDirty.Reset;
Result := FFlag.Reset;
end;
class operator TFlag.Implicit(const A: IFlag): TFlag;
@@ -378,12 +400,12 @@ end;
class operator TFlag.Implicit(const A: TFlag): IFlag;
begin
Result := A.FDirty;
Result := A.FFlag;
end;
class operator TFlag.Initialize(out Dest: TFlag);
begin
Dest.FDirty := FNull;
Dest.FFlag := FNull;
end;
{ TLatch }
@@ -402,7 +424,7 @@ begin
FLatch := FNull;
end;
class function TLatch.CreateLatch(Count: Integer): TLatch;
class function TLatch.CreateLatch(Count: Integer): ILatch;
begin
if Count > 0 then
Result := TMycLatch.Create(Count)
@@ -452,7 +474,7 @@ begin
end;
end;
function TLatch.GetState: TState;
function TLatch.GetState: TState.IState;
begin
Result := FLatch.State;
end;
+307 -272
View File
@@ -11,9 +11,9 @@ uses
type
[TestFixture]
TTestTDataSeries = class(TObject)
TTestDataArray = class(TObject)
private
FSeries: TDataSeries<TAskBidItem>;
FArray: IDataArray<TAskBidItem>;
procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
public
[Setup]
@@ -21,6 +21,7 @@ type
[Teardown]
procedure Teardown;
// Tests for Setup and basic Add/Count
[Test]
procedure TestSetupSeriesWithDataVerification;
[Test]
@@ -30,22 +31,6 @@ type
procedure TestAddOrderAssertion;
[Test]
procedure TestGetItemsIndexing;
[Test]
procedure TestIndexOfExistingTimeStamp;
[Test]
procedure TestIndexOfNonExistingBetween;
[Test]
procedure TestIndexOfBeforeFirstItem;
[Test]
procedure TestIndexOfAfterLastItem;
[Test]
procedure TestIndexOfExactOldestItem;
[Test]
procedure TestIndexOfExactNewestItem;
[Test]
procedure TestIndexOfEmptySeries;
[Test]
procedure TestIndexOfSingleItemSeries;
// Tests for MaxLookback
[Test]
@@ -55,8 +40,6 @@ type
[Test]
[IgnoreMemoryLeaks]
procedure TestLoopIsSafeWithMaxLookback;
[Test]
procedure TestIndexOfRespectsMaxLookback;
// Tests for bulk Add
[Test]
@@ -73,48 +56,79 @@ type
procedure TestBulkAddWithMaxLookback;
// Tests for TotalCount
[Test]
procedure TestTotalCountIncrementsCorrectly;
[Test]
procedure TestTotalCountIgnoresTrimming;
[Test]
[Test]
procedure TestTotalCountIncrementsCorrectly;
[Test]
procedure TestTotalCountIgnoresTrimming;
[Test]
procedure TestTotalCountResetsOnClear;
end;
[TestFixture]
TTestDataSeries = class(TObject)
private
FArray: IDataArray<TAskBidItem>;
FSeries: TDataSeries<TAskBidItem>;
procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
public
[Setup]
procedure Setup;
[Teardown]
procedure Teardown;
// Tests for IndexOf
[Test]
procedure TestIndexOfExistingTimeStamp;
[Test]
procedure TestIndexOfNonExistingBetween;
[Test]
procedure TestIndexOfBeforeFirstItem;
[Test]
procedure TestIndexOfAfterLastItem;
[Test]
procedure TestIndexOfExactOldestItem;
[Test]
procedure TestIndexOfExactNewestItem;
[Test]
procedure TestIndexOfEmptySeries;
[Test]
procedure TestIndexOfSingleItemSeries;
[Test]
procedure TestIndexOfRespectsMaxLookback;
end;
implementation
{ TTestTDataSeries }
{ TTestDataArray }
procedure TTestTDataSeries.Setup;
procedure TTestDataArray.Setup;
begin
// Managed record is implicitly initialized.
FArray := TDataSeries<TAskBidItem>.CreateArray(0);
end;
procedure TTestTDataSeries.Teardown;
procedure TTestDataArray.Teardown;
begin
// Ensure managed record is finalized to prevent false leak reports.
FSeries := Default(TDataSeries<TAskBidItem>);
FArray := nil;
end;
procedure TTestTDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
procedure TTestDataArray.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
var
i: Integer;
DataPoint: TDataPoint<TAskBidItem>;
baseTime: TDateTime;
begin
FSeries := TDataSeries<TAskBidItem>.Create( MaxLookBack );
FArray := TDataSeries<TAskBidItem>.CreateArray(MaxLookBack);
baseTime := EncodeDate(2020, 7, 7);
// Add data points with increasing timestamps.
for i := 0 to ItemCount - 1 do
begin
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
FSeries.Add(DataPoint);
FArray.Add(DataPoint);
end;
end;
// Verifies that the test data setup helper works as expected.
procedure TTestTDataSeries.TestSetupSeriesWithDataVerification;
procedure TTestDataArray.TestSetupSeriesWithDataVerification;
var
i: Integer;
baseTime, expectedTime: TDateTime;
@@ -123,20 +137,19 @@ begin
SetupSeriesWithData;
baseTime := EncodeDate(2020, 7, 7);
Assert.AreEqual(Int64(10), FSeries.Count, 'Setup should create exactly 10 items');
Assert.AreEqual(Int64(10), FArray.Count, 'Setup should create exactly 10 items');
// Check all items to ensure correct reverse chronological order.
for i := 0 to FSeries.Count - 1 do
for i := 0 to FArray.Count - 1 do
begin
expectedTime := baseTime + (9 - i);
expectedAsk := Single(9 - i);
Assert.AreEqual(expectedTime, FSeries[i].Time, 'Item at logical index should have correct reversed timestamp');
Assert.AreEqual(expectedAsk, FSeries[i].Data.Ask, 'Item at logical index should have correct reversed data');
Assert.AreEqual(expectedTime, FArray[i].Time, 'Item at logical index should have correct reversed timestamp');
Assert.AreEqual(expectedAsk, FArray[i].Data.Ask, 'Item at logical index should have correct reversed data');
end;
end;
// Tests adding new items and the resulting count.
procedure TTestTDataSeries.TestAddAndCount;
procedure TTestDataArray.TestAddAndCount;
var
DataPoint: TDataPoint<TAskBidItem>;
newTime: TDateTime;
@@ -144,17 +157,16 @@ begin
SetupSeriesWithData;
newTime := EncodeDate(2020, 7, 17);
Assert.AreEqual(Int64(10), FSeries.Count, 'Initial count should be 10');
Assert.AreEqual(Int64(10), FArray.Count, 'Initial count should be 10');
DataPoint := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(100.0, 100.1));
FSeries.Add(DataPoint);
FArray.Add(DataPoint);
Assert.AreEqual(Int64(11), FSeries.Count, 'Count should be 11 after adding one more');
Assert.AreEqual(newTime, FSeries.Items[0].Time, 'Newest item should be at index 0');
Assert.AreEqual(Int64(11), FArray.Count, 'Count should be 11 after adding one more');
Assert.AreEqual(newTime, FArray.Items[0].Time, 'Newest item should be at index 0');
end;
// Ensures that adding an item with an older timestamp raises an assertion.
procedure TTestTDataSeries.TestAddOrderAssertion;
procedure TTestDataArray.TestAddOrderAssertion;
var
olderTime: TDateTime;
begin
@@ -165,15 +177,14 @@ begin
procedure
begin
var DataPoint := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(0.0, 0.0));
FSeries.Add(DataPoint);
FArray.Add(DataPoint);
end,
EAssertionFailed,
'Adding item with older timestamp should raise an assert error'
);
end;
// Verifies that the logical-to-physical index mapping is correct.
procedure TTestTDataSeries.TestGetItemsIndexing;
procedure TTestDataArray.TestGetItemsIndexing;
var
i: Integer;
expectedTime: TDateTime;
@@ -185,13 +196,241 @@ begin
for i := 0 to 9 do
begin
expectedTime := baseTime + (9 - i);
Assert.AreEqual(expectedTime, FSeries.Items[i].Time, 'Item at logical index should have reversed chronological time');
Assert.AreEqual(Single(9 - i), FSeries.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
Assert.AreEqual(expectedTime, FArray.Items[i].Time, 'Item at logical index should have reversed chronological time');
Assert.AreEqual(Single(9 - i), FArray.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
end;
end;
// Tests finding an existing item (in this case, the oldest).
procedure TTestTDataSeries.TestIndexOfExistingTimeStamp;
procedure TTestDataArray.TestCountIsCappedByMaxLookback_NoTrim;
begin
SetupSeriesWithData(500, 400); // Less than one chunk
// The public Count must now be the MaxLookback value, even though
// no chunk was freed and FCount is still 500 internally.
Assert.AreEqual(Int64(400), FArray.Count, 'Public count should be capped by MaxLookback even if no chunk is freed');
end;
procedure TTestDataArray.TestCountIsCappedByMaxLookback_WithTrim;
const
ITEMS_TO_ADD = 2000;
LOOKBACK_LIMIT = 900;
begin
SetupSeriesWithData(ITEMS_TO_ADD, LOOKBACK_LIMIT);
// Internally, FCount will be 976 after one chunk is freed.
// The public Count should still be capped at the lookback limit.
Assert.AreEqual(Int64(LOOKBACK_LIMIT), FArray.Count, 'Public count should be capped by MaxLookback after trimming');
end;
procedure TTestDataArray.TestLoopIsSafeWithMaxLookback;
var
dummy: TDataPoint<TAskBidItem>;
begin
SetupSeriesWithData(500, 400);
Assert.AreEqual(Int64(400), FArray.Count, 'Pre-condition: Count must be capped at 400');
// This test proves that a standard loop based on the public Count is safe
// and will not access an invalid index.
Assert.WillNotRaise(
procedure
begin
for var i := 0 to FArray.Count - 1 do
begin
dummy := FArray[i];
end;
end,
nil,
'Looping up to the public Count should not raise an exception'
);
// Also test the assert when going one item beyond the public count
Assert.WillRaise(
procedure begin dummy := FArray[400]; end,
EAssertionFailed,
'Accessing index at the limit of MaxLookback should raise an exception'
);
end;
procedure TTestDataArray.TestBulkAddIncreasesCount;
var
newData: TArray<TDataPoint<TAskBidItem>>;
baseTime, newTime: TDateTime;
i: Integer;
begin
SetupSeriesWithData(10);
baseTime := EncodeDate(2020, 7, 7);
SetLength(newData, 5);
for i := 0 to High(newData) do
begin
newTime := baseTime + 10 + i;
newData[i] := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(i, i));
end;
FArray.Add(newData);
Assert.AreEqual(Int64(15), FArray.Count, 'Count should be increased by the number of items in the bulk add');
Assert.AreEqual(baseTime + 10 + High(newData), FArray[0].Time, 'Newest item should be the last item from the added array');
end;
procedure TTestDataArray.TestBulkAddChronologicalAssertion_Internal;
var
newData: TArray<TDataPoint<TAskBidItem>>;
begin
SetupSeriesWithData(10);
SetLength(newData, 2);
newData[0] := TDataPoint<TAskBidItem>.Create(Now + 1, TAskBidItem.Create(1, 1));
newData[1] := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(2, 2)); // Not sorted
Assert.WillRaise(
procedure begin FArray.Add(newData); end,
EAssertionFailed,
'Bulk Add should fail if the input array is not chronologically sorted'
);
end;
procedure TTestDataArray.TestBulkAddChronologicalAssertion_External;
var
newData: TArray<TDataPoint<TAskBidItem>>;
olderTime: TDateTime;
begin
SetupSeriesWithData(10); // Newest is baseTime + 9
olderTime := EncodeDate(2020, 7, 7) + 8;
SetLength(newData, 1);
newData[0] := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(1, 1));
Assert.WillRaise(
procedure begin FArray.Add(newData); end,
EAssertionFailed,
'Bulk Add should fail if its first item is older than the series last item'
);
end;
procedure TTestDataArray.TestBulkAddSpanningMultipleChunks;
const
CHUNK_SIZE = 1024;
var
newData: TArray<TDataPoint<TAskBidItem>>;
lastTimeBeforeAdd, firstNewTime, lastNewTime: TDateTime;
i: Integer;
begin
SetupSeriesWithData(CHUNK_SIZE - 4); // Almost fill the first chunk
lastTimeBeforeAdd := FArray[0].Time;
SetLength(newData, 8); // Add 8 items, which will span the chunk boundary
for i := 0 to High(newData) do
begin
newData[i] := TDataPoint<TAskBidItem>.Create(lastTimeBeforeAdd + 1 + i, TAskBidItem.Create(i, i));
end;
firstNewTime := newData[0].Time;
lastNewTime := newData[High(newData)].Time;
FArray.Add(newData);
Assert.AreEqual(Int64(CHUNK_SIZE + 4), FArray.Count, 'Count should be correct after spanning a chunk');
Assert.AreEqual(lastNewTime, FArray[0].Time, 'Newest item should be correct');
Assert.AreEqual(firstNewTime, FArray[7].Time, 'Oldest of the new items should be at the correct logical index');
end;
procedure TTestDataArray.TestBulkAddWithMaxLookback;
var
newData: TArray<TDataPoint<TAskBidItem>>;
i: Integer;
baseTime: TDateTime;
begin
SetupSeriesWithData(400, 500);
baseTime := FArray[0].Time;
SetLength(newData, 200);
for i := 0 to High(newData) do
begin
newData[i] := TDataPoint<TAskBidItem>.Create(baseTime + 1 + i, TAskBidItem.Create(i, i));
end;
FArray.Add(newData);
// Total items would be 600, but MaxLookback is 500.
// Trim does not free chunks (100 to remove < 1024).
// The public Count must be capped at 500.
Assert.AreEqual(Int64(500), FArray.Count, 'Count should be capped by MaxLookback after bulk add');
end;
procedure TTestDataArray.TestTotalCountIncrementsCorrectly;
var
newData: TArray<TDataPoint<TAskBidItem>>;
i: Integer;
begin
Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 on creation');
// Test single add
FArray.Add(TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(1, 1)));
Assert.AreEqual(Int64(1), FArray.TotalCount, 'TotalCount should be 1 after single add');
// Test bulk add
SetLength(newData, 10);
for i := 0 to High(newData) do
newData[i] := TDataPoint<TAskBidItem>.Create(Now + 1 + i, TAskBidItem.Create(i, i));
FArray.Add(newData);
Assert.AreEqual(Int64(11), FArray.TotalCount, 'TotalCount should be 11 after bulk add');
end;
procedure TTestDataArray.TestTotalCountIgnoresTrimming;
begin
// Create series with a lookback that will cause trimming
SetupSeriesWithData(20, 10);
// The visible count is capped by MaxLookback
Assert.AreEqual(Int64(10), FArray.Count, 'Count should be capped by MaxLookback');
// The total count should reflect all items that were ever added
Assert.AreEqual(Int64(20), FArray.TotalCount, 'TotalCount must not be affected by trimming');
end;
procedure TTestDataArray.TestTotalCountResetsOnClear;
begin
SetupSeriesWithData(15);
Assert.IsTrue(FArray.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0');
FArray.Clear;
Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 after Clear');
Assert.AreEqual(Int64(0), FArray.Count, 'Count should be 0 after Clear');
end;
{ TTestDataSeries }
procedure TTestDataSeries.Setup;
begin
FArray := TDataSeries<TAskBidItem>.CreateArray(0);
FSeries.Create(FArray);
end;
procedure TTestDataSeries.Teardown;
begin
FSeries := Default(TDataSeries<TAskBidItem>);
FArray := nil;
end;
procedure TTestDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
var
i: Integer;
DataPoint: TDataPoint<TAskBidItem>;
baseTime: TDateTime;
begin
FArray := TDataSeries<TAskBidItem>.CreateArray(MaxLookBack);
FSeries.Create(FArray);
baseTime := EncodeDate(2020, 7, 7);
// Add data points with increasing timestamps.
for i := 0 to ItemCount - 1 do
begin
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
FArray.Add(DataPoint);
end;
end;
procedure TTestDataSeries.TestIndexOfExistingTimeStamp;
var
ATimeStamp: TDateTime;
expectedIndex: Int64;
@@ -207,8 +446,7 @@ begin
);
end;
// Tests finding the index for a timestamp that falls between two existing items.
procedure TTestTDataSeries.TestIndexOfNonExistingBetween;
procedure TTestDataSeries.TestIndexOfNonExistingBetween;
var
ATimeStamp: TDateTime;
expectedIndex: Int64;
@@ -224,8 +462,7 @@ begin
);
end;
// Tests finding a timestamp that is older than any item in the series.
procedure TTestTDataSeries.TestIndexOfBeforeFirstItem;
procedure TTestDataSeries.TestIndexOfBeforeFirstItem;
var
ATimeStamp: TDateTime;
baseTime: TDateTime;
@@ -237,8 +474,7 @@ begin
Assert.AreEqual(Int64(-1), FSeries.IndexOf(ATimeStamp), 'IndexOf for a timestamp before the oldest item should return -1');
end;
// Tests finding a timestamp that is newer than any item in the series.
procedure TTestTDataSeries.TestIndexOfAfterLastItem;
procedure TTestDataSeries.TestIndexOfAfterLastItem;
var
ATimeStamp: TDateTime;
baseTime: TDateTime;
@@ -254,8 +490,7 @@ begin
);
end;
// Specifically tests finding the exact oldest item.
procedure TTestTDataSeries.TestIndexOfExactOldestItem;
procedure TTestDataSeries.TestIndexOfExactOldestItem;
var
ATimeStamp: TDateTime;
begin
@@ -265,8 +500,7 @@ begin
Assert.AreEqual(Int64(9), FSeries.IndexOf(ATimeStamp), 'IndexOf for the exact oldest timestamp should return the last index');
end;
// Specifically tests finding the exact newest item.
procedure TTestTDataSeries.TestIndexOfExactNewestItem;
procedure TTestDataSeries.TestIndexOfExactNewestItem;
var
ATimeStamp: TDateTime;
begin
@@ -276,24 +510,22 @@ begin
Assert.AreEqual(Int64(0), FSeries.IndexOf(ATimeStamp), 'IndexOf for the exact newest timestamp should return index 0');
end;
// Tests IndexOf on an empty series.
procedure TTestTDataSeries.TestIndexOfEmptySeries;
procedure TTestDataSeries.TestIndexOfEmptySeries;
begin
FSeries := Default(TDataSeries<TAskBidItem>);
// Use default setup with no data
Assert.AreEqual(Int64(0), FSeries.Count);
Assert.AreEqual(Int64(-1), FSeries.IndexOf(Now), 'IndexOf on empty series should return -1');
end;
// Tests IndexOf on a series with only one item.
procedure TTestTDataSeries.TestIndexOfSingleItemSeries;
procedure TTestDataSeries.TestIndexOfSingleItemSeries;
var
DataPoint: TDataPoint<TAskBidItem>;
testTime: TDateTime;
begin
FSeries := Default(TDataSeries<TAskBidItem>);
// Use default setup, but add one item
testTime := EncodeDate(2025, 1, 1);
DataPoint := TDataPoint<TAskBidItem>.Create(testTime, TAskBidItem.Create(1.0, 2.0));
FSeries.Add(DataPoint);
FArray.Add(DataPoint);
Assert.AreEqual(Int64(1), FSeries.Count);
Assert.AreEqual(Int64(0), FSeries.IndexOf(testTime), 'IndexOf for exact single item should be 0');
@@ -301,57 +533,7 @@ begin
Assert.AreEqual(Int64(-1), FSeries.IndexOf(testTime - 1), 'IndexOf for time before single item should be -1');
end;
procedure TTestTDataSeries.TestCountIsCappedByMaxLookback_NoTrim;
begin
SetupSeriesWithData(500, 400); // Less than one chunk
// The public Count must now be the MaxLookback value, even though
// no chunk was freed and FCount is still 500 internally.
Assert.AreEqual(Int64(400), FSeries.Count, 'Public count should be capped by MaxLookback even if no chunk is freed');
end;
procedure TTestTDataSeries.TestCountIsCappedByMaxLookback_WithTrim;
const
ITEMS_TO_ADD = 2000;
LOOKBACK_LIMIT = 900;
begin
SetupSeriesWithData(ITEMS_TO_ADD, LOOKBACK_LIMIT);
// Internally, FCount will be 976 after one chunk is freed.
// The public Count should still be capped at the lookback limit.
Assert.AreEqual(Int64(LOOKBACK_LIMIT), FSeries.Count, 'Public count should be capped by MaxLookback after trimming');
end;
procedure TTestTDataSeries.TestLoopIsSafeWithMaxLookback;
var
dummy: TDataPoint<TAskBidItem>;
begin
SetupSeriesWithData(500, 400);
Assert.AreEqual(Int64(400), FSeries.Count, 'Pre-condition: Count must be capped at 400');
// This test proves that a standard loop based on the public Count is safe
// and will not access an invalid index.
Assert.WillNotRaise(
procedure
begin
for var i := 0 to FSeries.Count - 1 do
begin
dummy := FSeries[i];
end;
end,
nil,
'Looping up to the public Count should not raise an exception'
);
// Also test the assert when going one item beyond the public count
Assert.WillRaise(
procedure begin dummy := FSeries[400]; end,
EAssertionFailed,
'Accessing index at the limit of MaxLookback should raise an exception'
);
end;
procedure TTestTDataSeries.TestIndexOfRespectsMaxLookback;
procedure TTestDataSeries.TestIndexOfRespectsMaxLookback;
var
baseTime, searchTime: TDateTime;
begin
@@ -373,154 +555,7 @@ begin
Assert.AreEqual(Int64(399), FSeries.IndexOf(searchTime), 'IndexOf should find the oldest valid item at the edge of MaxLookback');
end;
procedure TTestTDataSeries.TestBulkAddIncreasesCount;
var
newData: TArray<TDataPoint<TAskBidItem>>;
baseTime, newTime: TDateTime;
i: Integer;
begin
SetupSeriesWithData(10);
baseTime := EncodeDate(2020, 7, 7);
SetLength(newData, 5);
for i := 0 to High(newData) do
begin
newTime := baseTime + 10 + i;
newData[i] := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(i, i));
end;
FSeries.Add(newData);
Assert.AreEqual(Int64(15), FSeries.Count, 'Count should be increased by the number of items in the bulk add');
Assert.AreEqual(baseTime + 10 + High(newData), FSeries.Time[0], 'Newest item should be the last item from the added array');
end;
procedure TTestTDataSeries.TestBulkAddChronologicalAssertion_Internal;
var
newData: TArray<TDataPoint<TAskBidItem>>;
begin
SetupSeriesWithData(10);
SetLength(newData, 2);
newData[0] := TDataPoint<TAskBidItem>.Create(Now + 1, TAskBidItem.Create(1, 1));
newData[1] := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(2, 2)); // Not sorted
Assert.WillRaise(
procedure begin FSeries.Add(newData); end,
EAssertionFailed,
'Bulk Add should fail if the input array is not chronologically sorted'
);
end;
procedure TTestTDataSeries.TestBulkAddChronologicalAssertion_External;
var
newData: TArray<TDataPoint<TAskBidItem>>;
olderTime: TDateTime;
begin
SetupSeriesWithData(10); // Newest is baseTime + 9
olderTime := EncodeDate(2020, 7, 7) + 8;
SetLength(newData, 1);
newData[0] := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(1, 1));
Assert.WillRaise(
procedure begin FSeries.Add(newData); end,
EAssertionFailed,
'Bulk Add should fail if its first item is older than the series last item'
);
end;
procedure TTestTDataSeries.TestBulkAddSpanningMultipleChunks;
const
CHUNK_SIZE = 1024;
var
newData: TArray<TDataPoint<TAskBidItem>>;
lastTimeBeforeAdd, firstNewTime, lastNewTime: TDateTime;
i: Integer;
begin
SetupSeriesWithData(CHUNK_SIZE - 4); // Almost fill the first chunk
lastTimeBeforeAdd := FSeries.Time[0];
SetLength(newData, 8); // Add 8 items, which will span the chunk boundary
for i := 0 to High(newData) do
begin
newData[i] := TDataPoint<TAskBidItem>.Create(lastTimeBeforeAdd + 1 + i, TAskBidItem.Create(i, i));
end;
firstNewTime := newData[0].Time;
lastNewTime := newData[High(newData)].Time;
FSeries.Add(newData);
Assert.AreEqual(Int64(CHUNK_SIZE + 4), FSeries.Count, 'Count should be correct after spanning a chunk');
Assert.AreEqual(lastNewTime, FSeries.Time[0], 'Newest item should be correct');
Assert.AreEqual(firstNewTime, FSeries.Time[7], 'Oldest of the new items should be at the correct logical index');
end;
procedure TTestTDataSeries.TestBulkAddWithMaxLookback;
var
newData: TArray<TDataPoint<TAskBidItem>>;
i: Integer;
baseTime: TDateTime;
begin
SetupSeriesWithData(400, 500);
baseTime := FSeries.Time[0];
SetLength(newData, 200);
for i := 0 to High(newData) do
begin
newData[i] := TDataPoint<TAskBidItem>.Create(baseTime + 1 + i, TAskBidItem.Create(i, i));
end;
FSeries.Add(newData);
// Total items would be 600, but MaxLookback is 500.
// Trim does not free chunks (100 to remove < 1024).
// The public Count must be capped at 500.
Assert.AreEqual(Int64(500), FSeries.Count, 'Count should be capped by MaxLookback after bulk add');
end;
procedure TTestTDataSeries.TestTotalCountIncrementsCorrectly;
var
newData: TArray<TDataPoint<TAskBidItem>>;
i: Integer;
begin
FSeries := TDataSeries<TAskBidItem>.Create( 0 );
Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 on creation');
// Test single add
FSeries.Add(TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(1,1)));
Assert.AreEqual(Int64(1), FSeries.TotalCount, 'TotalCount should be 1 after single add');
// Test bulk add
SetLength(newData, 10);
for i := 0 to High(newData) do
newData[i] := TDataPoint<TAskBidItem>.Create(Now + 1 + i, TAskBidItem.Create(i,i));
FSeries.Add(newData);
Assert.AreEqual(Int64(11), FSeries.TotalCount, 'TotalCount should be 11 after bulk add');
end;
procedure TTestTDataSeries.TestTotalCountIgnoresTrimming;
begin
// Create series with a lookback that will cause trimming
SetupSeriesWithData(20, 10);
// The visible count is capped by MaxLookback
Assert.AreEqual(Int64(10), FSeries.Count, 'Count should be capped by MaxLookback');
// The total count should reflect all items that were ever added
Assert.AreEqual(Int64(20), FSeries.TotalCount, 'TotalCount must not be affected by trimming');
end;
procedure TTestTDataSeries.TestTotalCountResetsOnClear;
begin
SetupSeriesWithData(15);
Assert.IsTrue(FSeries.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0');
FSeries.Clear;
Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 after Clear');
Assert.AreEqual(Int64(0), FSeries.Count, 'Count should be 0 after Clear');
end;
initialization
TDUnitX.RegisterTestFixture(TTestTDataSeries);
TDUnitX.RegisterTestFixture(TTestDataArray);
TDUnitX.RegisterTestFixture(TTestDataSeries);
end.
+226
View File
@@ -0,0 +1,226 @@
unit Myc.Trade.Core.DataPoint;
interface
uses
Myc.Trade.DataPoint;
type
// The implementation class for IDataSeries<T>.
TDataArray<T> = class(TInterfacedObject, IDataSeries<T>, IDataArray<T>)
private
const
ChunkSize = 1024;
type
TChunk = TArray<TDataPoint<T>>;
private
FChunks: TArray<TChunk>;
FCount: Int64;
FMaxLookback: Int64;
FTotalCount: Int64;
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
function IsIndexInLimits(LogicalIndex: Int64): Boolean;
procedure Trim;
// IDataSeries<T> implementation
function GetCount: Int64;
function GetItems(Idx: Int64): TDataPoint<T>;
function GetMaxLookback: Int64;
function GetTotalCount: Int64;
public
constructor Create(AMaxLookback: Int64);
procedure Add(const Data: TDataPoint<T>); overload;
procedure Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1); overload;
procedure Clear;
property MaxLookback: Int64 read GetMaxLookback;
end;
type
// Null object implementation for IDataSeries<T>
TNullDataSeries<T> = class(TInterfacedObject, IDataSeries<T>)
public
function GetCount: Int64;
function GetItems(Idx: Int64): TDataPoint<T>;
function GetTotalCount: Int64;
function IndexOf(TimeStamp: TDateTime): Int64;
end;
implementation
{ TDataArray<T> }
constructor TDataArray<T>.Create(AMaxLookback: Int64);
begin
inherited Create;
FMaxLookback := AMaxLookback;
FCount := 0;
FTotalCount := 0;
FChunks := nil;
end;
procedure TDataArray<T>.Add(const Data: TDataPoint<T>);
begin
Assert((FCount = 0) or (Data.Time >= GetItems(0).Time), 'Time stamp older than last item');
var ci := FCount div ChunkSize;
var di := FCount mod ChunkSize;
if (di = 0) then
begin
SetLength(FChunks, ci + 1);
SetLength(FChunks[ci], ChunkSize);
end;
FChunks[ci][di] := Data;
Inc(FCount);
Inc(FTotalCount);
Trim;
end;
procedure TDataArray<T>.Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1);
var
sourceIdx, itemsToCopy, spaceInChunk: Integer;
ci, di: Int64;
i: Integer;
resolvedNumToAdd: Integer;
begin
if NumToAdd < 0 then
resolvedNumToAdd := Length(Data)
else
resolvedNumToAdd := NumToAdd;
if resolvedNumToAdd = 0 then
Exit;
Assert(resolvedNumToAdd <= Length(Data), 'NumToAdd cannot be larger than the source array');
for i := 1 to resolvedNumToAdd - 1 do
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
Assert((FCount = 0) or (Data[0].Time >= GetItems(0).Time), 'First new item is older than last existing item');
Inc(FTotalCount, resolvedNumToAdd);
var newTotalCount := FCount + resolvedNumToAdd;
var requiredChunks := (newTotalCount + ChunkSize - 1) div ChunkSize;
if requiredChunks = 0 then
requiredChunks := 1;
if requiredChunks > Length(FChunks) then
SetLength(FChunks, requiredChunks);
sourceIdx := 0;
while sourceIdx < resolvedNumToAdd do
begin
ci := FCount div ChunkSize;
di := FCount mod ChunkSize;
if di = 0 then
SetLength(FChunks[ci], ChunkSize);
spaceInChunk := ChunkSize - di;
itemsToCopy := resolvedNumToAdd - sourceIdx;
if spaceInChunk < itemsToCopy then
itemsToCopy := spaceInChunk;
System.Move(Data[sourceIdx], FChunks[ci][di], itemsToCopy * SizeOf(TDataPoint<T>));
Inc(FCount, itemsToCopy);
Inc(sourceIdx, itemsToCopy);
Trim;
end;
end;
procedure TDataArray<T>.Clear;
begin
FChunks := nil;
FCount := 0;
FTotalCount := 0;
end;
function TDataArray<T>.GetCount: Int64;
begin
Result := FCount;
if (FMaxLookback > 0) and (Result > FMaxLookback) then
Result := FMaxLookback;
end;
function TDataArray<T>.GetItems(Idx: Int64): TDataPoint<T>;
var
physicalIndex: Int64;
begin
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
end;
function TDataArray<T>.GetMaxLookback: Int64;
begin
Result := FMaxLookback;
end;
function TDataArray<T>.GetTotalCount: Int64;
begin
Result := FTotalCount;
end;
function TDataArray<T>.IsIndexInLimits(LogicalIndex: Int64): Boolean;
begin
Result := (FMaxLookback <= 0) or (LogicalIndex < FMaxLookback);
end;
function TDataArray<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
begin
Assert((LogicalIndex >= 0) and (LogicalIndex < FCount), 'Logical index is out of bounds.');
Result := FCount - LogicalIndex - 1;
end;
procedure TDataArray<T>.Trim;
var
itemsToRemove, chunksToRemove: Int64;
begin
if (FCount = 0) or (FMaxLookback <= 0) then
Exit;
itemsToRemove := 0;
if FCount > FMaxLookback then
itemsToRemove := FCount - FMaxLookback;
if itemsToRemove <= 0 then
Exit;
chunksToRemove := itemsToRemove div ChunkSize;
if chunksToRemove > 0 then
begin
var itemsInFreedChunks := chunksToRemove * ChunkSize;
FChunks := Copy(FChunks, chunksToRemove, Length(FChunks) - chunksToRemove);
FCount := FCount - itemsInFreedChunks;
end;
end;
function TNullDataSeries<T>.GetCount: Int64;
begin
Result := 0;
end;
function TNullDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
begin
Assert(false, 'Index out of bounds.');
Result := Default(TDataPoint<T>);
end;
function TNullDataSeries<T>.GetTotalCount: Int64;
begin
Result := 0;
end;
function TNullDataSeries<T>.IndexOf(TimeStamp: TDateTime): Int64;
begin
Result := -1;
end;
end.
+103 -222
View File
@@ -2,9 +2,6 @@ unit Myc.Trade.DataPoint;
interface
uses
System.SysUtils;
type
// A data record for an Ask/Bid price pair.
TAskBidItem = packed record
@@ -22,60 +19,71 @@ type
// A time-ordered series of data points, optimized for chronological additions.
// The most recently added element has the logical index 0.
TDataSeries<T> = record
private
const
ChunkSize = 1024;
type
TChunk = TArray<TDataPoint<T>>;
private
FChunks: TArray<TChunk>;
FCount: Int64;
FMaxLookback: Int64;
FTotalCount: Int64;
// Converts a logical index (0=newest) to a physical storage index (0=oldest).
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
IDataSeries<T> = interface
function GetCount: Int64;
function GetData(Idx: Int64): T;
function GetItems(Idx: Int64): TDataPoint<T>;
function GetTime(Idx: Int64): TDateTime;
procedure SetData(Idx: Int64; const Value: T);
// Checks if a logical index is within the defined limits.
function IsIndexInLimits(LogicalIndex: Int64): Boolean;
// Removes old chunks that are outside the Lookback limits.
procedure Trim;
public
constructor Create(AMaxLookback: Int64);
// Adds a new data point to the series.
function GetTotalCount: Int64;
property Count: Int64 read GetCount;
// Accesses data points by their logical index.
// Index 0 is the newest element, Index (Count - 1) is the oldest.
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
// The total number of items ever added to the series.
property TotalCount: Int64 read GetTotalCount;
end;
IDataArray<T> = interface(IDataSeries<T>)
procedure Add(const Data: TDataPoint<T>); overload;
procedure Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1); overload;
// Clears all data from the series.
procedure Clear;
end;
// Interface Helper for IDataSeries<T>.
// Provides a safe, value-type-like wrapper around the interface.
TDataSeries<T> = record
strict private
class var
FNull: IDataSeries<T>;
private
FDataSeries: IDataSeries<T>;
FMaxLookback: Int64;
class constructor CreateClass;
function GetCount: Int64;
function GetItems(Idx: Int64): TDataPoint<T>;
function GetData(Idx: Int64): T;
function GetTime(Idx: Int64): TDateTime;
function GetMaxLookback: Int64;
function GetTotalCount: Int64;
public
constructor Create(ADataSeries: IDataSeries<T>);
class operator Initialize(out Dest: TDataSeries<T>);
class operator Finalize(var Dest: TDataSeries<T>);
class operator Implicit(const A: TDataSeries<T>): IDataSeries<T>;
class operator Implicit(const A: IDataSeries<T>): TDataSeries<T>;
class function CreateArray(MaxLookback: Int64): IDataArray<T>; static;
// Searches for a data point by its timestamp.
// Returns the logical index of the matching item.
// If no exact match, returns the index of the item immediately preceding the timestamp.
// Returns -1 if the timestamp is before the oldest item in the series.
function IndexOf(TimeStamp: TDateTime): Int64;
class operator Initialize(out Dest: TDataSeries<T>);
class operator Finalize(var Dest: TDataSeries<T>);
class property Null: IDataSeries<T> read FNull;
property Count: Int64 read GetCount;
// Accesses data points by their logical index.
// Index 0 is the newest element, Index (Count - 1) is the oldest.
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
property Data[Idx: Int64]: T read GetData;
property Time[Idx: Int64]: TDateTime read GetTime;
property Data[Idx: Int64]: T read GetData write SetData;
// The maximum number of data points to keep (0 = unlimited).
property MaxLookback: Int64 read FMaxLookback;
// The total number of items ever added to the series (resets on Clear).
property TotalCount: Int64 read FTotalCount;
property MaxLookback: Int64 read GetMaxLookback;
property TotalCount: Int64 read GetTotalCount;
end;
implementation
uses
Myc.Trade.Core.DataPoint;
{ TAskBidItem }
constructor TAskBidItem.Create(AAsk, ABid: Single);
@@ -92,153 +100,74 @@ begin
Data := AData;
end;
constructor TDataSeries<T>.Create(AMaxLookback: Int64);
{ TDataSeries<T> Interface Helper }
class constructor TDataSeries<T>.CreateClass;
begin
FMaxLookback := AMaxLookback;
FCount := 0;
FTotalCount := 0;
FChunks := nil;
FNull := TNullDataSeries<T>.Create;
end;
{ TDataSeries<T> }
procedure TDataSeries<T>.Add(const Data: TDataPoint<T>);
constructor TDataSeries<T>.Create(ADataSeries: IDataSeries<T>);
begin
// Enforce chronological order for new items.
Assert((FCount = 0) or (Data.Time >= GetTime(0)), 'Time stamp older than last item');
var ci := FCount div ChunkSize;
var di := FCount mod ChunkSize;
if (di = 0) then
begin
SetLength(FChunks, ci + 1);
SetLength(FChunks[ci], ChunkSize);
end;
FChunks[ci][di] := Data;
Inc(FCount);
Inc(FTotalCount);
// Remove old data that is now out of limits.
Trim;
end;
procedure TDataSeries<T>.Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1);
var
sourceIdx, itemsToCopy, spaceInChunk: Integer;
ci, di: Int64;
i: Integer;
resolvedNumToAdd: Integer;
begin
if NumToAdd < 0 then
resolvedNumToAdd := Length(Data)
if Assigned(ADataSeries) then
FDataSeries := ADataSeries
else
resolvedNumToAdd := NumToAdd;
if resolvedNumToAdd = 0 then
Exit;
Assert(resolvedNumToAdd <= Length(Data), 'NumToAdd cannot be larger than the source array');
// Check chronological order of the input array itself
for i := 1 to resolvedNumToAdd - 1 do
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
// Enforce chronological order for new items against existing series.
Assert((FCount = 0) or (Data[0].Time >= GetTime(0)), 'First new item is older than last existing item');
// Increment total count immediately.
Inc(FTotalCount, resolvedNumToAdd);
var newTotalCount := FCount + resolvedNumToAdd;
var requiredChunks := (newTotalCount + ChunkSize - 1) div ChunkSize;
if requiredChunks = 0 then // case for newTotalCount = 0
requiredChunks := 1;
// Pre-allocate chunk array structure
if requiredChunks > Length(FChunks) then
SetLength(FChunks, requiredChunks);
sourceIdx := 0;
while sourceIdx < resolvedNumToAdd do
begin
ci := FCount div ChunkSize;
di := FCount mod ChunkSize;
// Allocate the actual chunk if we are at its beginning
if di = 0 then
SetLength(FChunks[ci], ChunkSize);
spaceInChunk := ChunkSize - di;
itemsToCopy := resolvedNumToAdd - sourceIdx;
if spaceInChunk < itemsToCopy then
itemsToCopy := spaceInChunk;
// Move a block of data into the available space in the current chunk.
System.Move(Data[sourceIdx], FChunks[ci][di], itemsToCopy * SizeOf(TDataPoint<T>));
Inc(FCount, itemsToCopy);
Inc(sourceIdx, itemsToCopy);
// Remove old data that is now out of limits
Trim;
end;
FDataSeries := FNull;
end;
procedure TDataSeries<T>.Clear;
class function TDataSeries<T>.CreateArray(MaxLookback: Int64): IDataArray<T>;
begin
FChunks := nil;
FCount := 0;
FTotalCount := 0;
Result := TDataArray<T>.Create(MaxLookback);
end;
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
begin
Dest.Clear;
end;
function TDataSeries<T>.GetCount: Int64;
begin
// The public Count must always reflect the addressable range.
Result := FCount;
if (FMaxLookback > 0) and (Result > FMaxLookback) then
Result := FMaxLookback;
end;
function TDataSeries<T>.GetData(Idx: Int64): T;
var
physicalIndex: Int64;
begin
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data;
end;
function TDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
var
physicalIndex: Int64;
begin
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
end;
function TDataSeries<T>.GetTime(Idx: Int64): TDateTime;
var
physicalIndex: Int64;
begin
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Time;
Dest.FDataSeries := nil;
end;
class operator TDataSeries<T>.Initialize(out Dest: TDataSeries<T>);
begin
Dest.FCount := 0;
Dest.FTotalCount := 0;
Dest.FChunks := nil;
Dest.FMaxLookback := 0;
Dest.FDataSeries := FNull;
end;
class operator TDataSeries<T>.Implicit(const A: TDataSeries<T>): IDataSeries<T>;
begin
Result := A.FDataSeries;
end;
class operator TDataSeries<T>.Implicit(const A: IDataSeries<T>): TDataSeries<T>;
begin
Result.Create(A);
end;
function TDataSeries<T>.GetCount: Int64;
begin
Result := FDataSeries.GetCount;
end;
function TDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
begin
Result := FDataSeries[Idx];
end;
function TDataSeries<T>.GetData(Idx: Int64): T;
begin
Result := FDataSeries[Idx].Data;
end;
function TDataSeries<T>.GetTime(Idx: Int64): TDateTime;
begin
Result := FDataSeries[Idx].Time;
end;
function TDataSeries<T>.GetMaxLookback: Int64;
begin
Result := FMaxLookback;
end;
function TDataSeries<T>.GetTotalCount: Int64;
begin
Result := FDataSeries.GetTotalCount;
end;
function TDataSeries<T>.IndexOf(TimeStamp: TDateTime): Int64;
@@ -247,16 +176,16 @@ var
dataPointTime: TDateTime;
begin
Result := -1;
if Count = 0 then // Use public Count to respect limits
if Count = 0 then
Exit;
low := 0;
high := Count - 1; // Use public Count to respect limits
high := Count - 1;
while (low <= high) do
begin
mid := low + (high - low) div 2;
dataPointTime := GetTime(mid);
dataPointTime := GetItems(mid).Time; // Use GetItems to stay within the class
if (dataPointTime = TimeStamp) then
begin
@@ -268,59 +197,11 @@ begin
Result := mid;
high := mid - 1;
end
else // dataPointTime > TimeStamp
else
begin
low := mid + 1;
end;
end;
end;
function TDataSeries<T>.IsIndexInLimits(LogicalIndex: Int64): Boolean;
begin
// Check Lookback limit
Result := (FMaxLookback <= 0) or (LogicalIndex < FMaxLookback);
end;
function TDataSeries<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
begin
Assert((LogicalIndex >= 0) and (LogicalIndex < Count), 'Logical index is out of bounds.');
Result := FCount - LogicalIndex - 1;
end;
procedure TDataSeries<T>.SetData(Idx: Int64; const Value: T);
var
physicalIndex: Int64;
begin
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
physicalIndex := LogicalToPhysicalIndex(Idx);
FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data := Value;
end;
procedure TDataSeries<T>.Trim;
var
itemsToRemove, chunksToRemove: Int64;
begin
if (FCount = 0) or (FMaxLookback <= 0) then
Exit;
itemsToRemove := 0;
if FCount > FMaxLookback then
begin
itemsToRemove := FCount - FMaxLookback;
end;
if itemsToRemove <= 0 then
Exit;
// We only free full chunks to avoid expensive partial copy operations.
chunksToRemove := itemsToRemove div ChunkSize;
if chunksToRemove > 0 then
begin
var itemsInFreedChunks := chunksToRemove * ChunkSize;
FChunks := Copy(FChunks, chunksToRemove, Length(FChunks) - chunksToRemove);
FCount := FCount - itemsInFreedChunks;
end;
end;
end.
+14 -18
View File
@@ -12,37 +12,37 @@ type
TDataStreamProvider<T> = class(TInterfacedObject, TMutable<TDataSeries<T>>.IMutable)
private
FStream: IDataStream<T>;
FDataSeries: TDataSeries<T>;
FDataArray: IDataArray<T>;
FNewDataAvailable: TFlag;
FHasDataSubscription: TSignal.TSubscription;
FChunk: TArray<TDataPoint<T>>;
FLookback: Int64;
function GetChanged: TSignal;
function GetValue: TDataSeries<T>;
public
constructor Create(ALookback: Int64; const AStream: IDataStream<T>);
constructor Create(ALookback, AMaxChunkSize: Int64; const AStream: IDataStream<T>);
destructor Destroy; override;
end;
TDataStreamProvider = record
class function Create<T>(Lookback: Int64; const Stream: IDataStream<T>): TMutable<TDataSeries<T>>; static;
class function Create<T>(Lookback, MaxChunkSize: Int64; const Stream: IDataStream<T>): TMutable<TDataSeries<T>>; static;
end;
implementation
{ TDataStreamProvider<T> }
constructor TDataStreamProvider<T>.Create(ALookback: Int64; const AStream: IDataStream<T>);
constructor TDataStreamProvider<T>.Create(ALookback, AMaxChunkSize: Int64; const AStream: IDataStream<T>);
begin
inherited Create;
FLookback := ALookback;
SetLength(FChunk, AMaxChunkSize);
FStream := AStream;
FHasDataSubscription := FStream.HasData.Subscribe(FNewDataAvailable);
FDataSeries := TDataSeries<T>.Create( ALookback );
FNewDataAvailable := TFlag.CreateObserver(FStream.HasData);
FDataArray := TDataSeries<T>.CreateArray(ALookback);
end;
destructor TDataStreamProvider<T>.Destroy;
begin
FHasDataSubscription.Unsubscribe;
inherited;
end;
@@ -55,22 +55,18 @@ function TDataStreamProvider<T>.GetValue: TDataSeries<T>;
begin
if FNewDataAvailable.Reset then
begin
var Data: array[0..1024 - 1] of TDataPoint<T>;
var cnt := FStream.GetChunk(Data);
while cnt > 0 do
begin
FDataSeries.Add(Data, cnt);
cnt := FStream.GetChunk(Data);
end;
var cnt := FStream.GetChunk(FChunk);
if cnt > 0 then
FDataArray.Add(FChunk, cnt);
end;
Result := FDataSeries;
Result := FDataArray;
end;
{ TDataStreamProvider }
class function TDataStreamProvider.Create<T>(Lookback: Int64; const Stream: IDataStream<T>): TMutable<TDataSeries<T>>;
class function TDataStreamProvider.Create<T>(Lookback, MaxChunkSize: Int64; const Stream: IDataStream<T>): TMutable<TDataSeries<T>>;
begin
Result := TDataStreamProvider<T>.Create(Lookback, Stream);
Result := TDataStreamProvider<T>.Create(Lookback, MaxChunkSize, Stream);
end;
end.
+9 -8
View File
@@ -145,6 +145,7 @@ type
FDataServer: TAuraDataServer<T>;
FSymbol: String;
FHasData: TEvent;
FNextReady: TFlag;
FCurrent: TFuture<TDataStream>;
FNext: TFuture<TDataStream>;
FCurrPosInFile: Int64;
@@ -166,13 +167,13 @@ type
TAuraTABFileServer = class(TAuraDataServer<TAskBidItem>)
protected
function ParseFileName(const FileName: string): TAuraDataFile; override;
// Scans a directory and returns the oldest file found for each symbol.
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<TAskBidItem>>; override;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAskBidItem>>; override;
public
function CreateStream(const Symbol: String): IDataStream<TAskBidItem>; override;
function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
function ParseFileName(const FileName: string): TAuraDataFile; override;
end;
implementation
@@ -363,9 +364,6 @@ begin
end;
function TAuraDataServer<T>.EnumerateSymbols: TFuture<TArray<String>>;
var
dataFiles: TArray<TAuraDataFile>;
i: Integer;
begin
Result :=
FSymbols.Chain<TArray<String>>(
@@ -404,7 +402,8 @@ begin
Assert(Assigned(ADataServer));
FDataServer := ADataServer;
FSymbol := ASymbol;
FHasData := TEvent.CreateEvent;
FNextReady := TFlag.CreateFlag;
FHasData := TEvent.CreateRouter(FNextReady.State.AsSignal);
end;
destructor TAuraFileStream<T>.Destroy;
@@ -429,7 +428,7 @@ begin
begin
Result.FileInfo := FileInfo;
Result.Data := FDataServer.LoadDataFile(FileInfo);
Result.Data.Done.Subscribe(FHasData);
Result.Data.Done.Subscribe(FNextReady);
end;
end
);
@@ -488,7 +487,7 @@ begin
Inc(FCurrPosInFile);
end;
if FCurrPosInFile < Length(currData) then
if (FCurrPosInFile < Length(currData)) or FNextReady.Reset then
begin
FHasData.Notify;
end;
@@ -511,6 +510,8 @@ end;
procedure TAuraFileStream<T>.PreloadNext;
begin
FNextReady.Reset;
FNext :=
FCurrent.Chain<TDataStream>(
function(const Prev: TDataStream): TDataStream
@@ -524,7 +525,7 @@ begin
begin
Result.FileInfo := nextFileInfo;
Result.Data := FDataServer.LoadDataFile(nextFileInfo);
Result.Data.Done.Subscribe(FHasData);
Result.Data.Done.Subscribe(FNextReady);
end;
end;
end