Implementing first "strategy" for proof of concept

This commit is contained in:
Michael Schimmel
2025-07-11 11:06:18 +02:00
parent 644b6074d6
commit 487169afe0
20 changed files with 1761 additions and 274 deletions
+4 -2
View File
@@ -10,13 +10,15 @@ uses
Myc.Aura.Module in '..\Src\Myc.Aura.Module.pas',
Myc.Aura.Parameter in '..\Src\Myc.Aura.Parameter.pas',
TestModule in 'TestModule.pas',
TestChartControl in 'TestChartControl.pas' {TestChartForm};
DynamicFMXControl in 'DynamicFMXControl.pas',
FirstStrategy in 'FirstStrategy.pas',
Myc.Fmx.Chart in 'Myc.Fmx.Chart.pas',
Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas';
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.CreateForm(TTestChartForm, TestChartForm);
Application.Run;
end.
+5 -5
View File
@@ -4,7 +4,7 @@
<ProjectVersion>20.3</ProjectVersion>
<FrameworkType>FMX</FrameworkType>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Release</Config>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win64</Platform>
<ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName>
<TargetedPlatforms>3</TargetedPlatforms>
@@ -138,10 +138,10 @@
<DCCReference Include="..\Src\Myc.Aura.Module.pas"/>
<DCCReference Include="..\Src\Myc.Aura.Parameter.pas"/>
<DCCReference Include="TestModule.pas"/>
<DCCReference Include="TestChartControl.pas">
<Form>TestChartForm</Form>
<FormType>fmx</FormType>
</DCCReference>
<DCCReference Include="DynamicFMXControl.pas"/>
<DCCReference Include="FirstStrategy.pas"/>
<DCCReference Include="Myc.Fmx.Chart.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataArray.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
+379
View File
@@ -0,0 +1,379 @@
unit DynamicFMXControl;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
FMX.Controls, FMX.Graphics, FMX.Forms, FMX.StdCtrls, FMX.Types;
type
// Dezidierte Event-Typen für virtuelle Methoden von TControl
TControlPaintEvent = reference to procedure;
TControlMouseEvent = reference to procedure(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
TControlMouseMoveEvent = reference to procedure(Shift: TShiftState; X, Y: Single);
TControlMouseWheelEvent = reference to procedure(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
TControlKeyEvent = reference to procedure(var Key: Word; var KeyChar: WideChar; Shift: TShiftState);
TControlDragEvent = reference to procedure(const Data: TDragObject; const Point: TPointF);
TControlDragOverEvent = reference to procedure(const Data: TDragObject; const Point: TPointF; var Operation: TDragOperation);
TControlNotifyEvent = reference to procedure;
TControlCanFocusEvent = reference to function: Boolean;
TControlShowContextMenuEvent = reference to function(const ScreenPosition: TPointF): Boolean;
TControlSetHintEvent = reference to procedure(const AHint: string);
TControlGetHintStringEvent = reference to function: string;
TControlHasHintEvent = reference to function: Boolean;
TControlCanShowHintEvent = reference to function: Boolean;
TControlGetDefaultSizeEvent = reference to function: TSizeF;
TControlSetVisibleEvent = reference to procedure(const Value: Boolean);
TControlSetEnabledEvent = reference to procedure(const Value: Boolean);
TControlDoAbsoluteChangedEvent = reference to procedure;
TDynamicControl = class(TControl)
private
// Private Felder für die Event-Handler
FPaintEvent: TControlPaintEvent;
FMouseDownEvent: TControlMouseEvent;
FMouseUpEvent: TControlMouseEvent;
FMouseMoveEvent: TControlMouseMoveEvent;
FMouseWheelEvent: TControlMouseWheelEvent;
FKeyDownEvent: TControlKeyEvent;
FKeyUpEvent: TControlKeyEvent;
FClickEvent: TControlNotifyEvent;
FDblClickEvent: TControlNotifyEvent;
FDragEnterEvent: TControlDragEvent;
FDragOverEvent: TControlDragOverEvent;
FDragDropEvent: TControlDragEvent;
FDragLeaveEvent: TControlNotifyEvent;
FDragEndEvent: TControlNotifyEvent;
FEnterEvent: TControlNotifyEvent;
FExitEvent: TControlNotifyEvent;
FMouseEnterEvent: TControlNotifyEvent;
FMouseLeaveEvent: TControlNotifyEvent;
FResizeEvent: TControlNotifyEvent;
FResizedEvent: TControlNotifyEvent;
FCanFocusEvent: TControlCanFocusEvent;
FShowContextMenuEvent: TControlShowContextMenuEvent;
FSetHintEvent: TControlSetHintEvent;
FGetHintStringEvent: TControlGetHintStringEvent;
FHasHintEvent: TControlHasHintEvent;
FCanShowHintEvent: TControlCanShowHintEvent;
FGetDefaultSizeEvent: TControlGetDefaultSizeEvent;
FRecalculateAbsoluteMatricesEvent: TControlNotifyEvent;
FSetVisibleEvent: TControlSetVisibleEvent;
FSetEnabledEvent: TControlSetEnabledEvent;
FDoAbsoluteChangedEvent: TControlDoAbsoluteChangedEvent;
protected
// Überschriebene virtuelle Methoden, die die Events auslösen
procedure Paint; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override;
procedure Click; override;
procedure DblClick; override;
procedure KeyDown(var Key: Word; var KeyChar: WideChar; Shift: TShiftState); override;
procedure KeyUp(var Key: Word; var KeyChar: WideChar; Shift: TShiftState); override;
procedure DragEnter(const Data: TDragObject; const Point: TPointF); override;
procedure DragOver(const Data: TDragObject; const Point: TPointF; var Operation: TDragOperation); override;
procedure DragDrop(const Data: TDragObject; const Point: TPointF); override;
procedure DragLeave; override;
procedure DragEnd; override;
procedure DoEnter; override;
procedure DoExit; override;
procedure DoMouseEnter; override;
procedure DoMouseLeave; override;
procedure Resize; override;
procedure DoResized; override;
function GetCanFocus: Boolean; override;
function ShowContextMenu(const ScreenPosition: TPointF): Boolean; override;
procedure SetHint(const AHint: string); override;
function GetHintString: string; override;
function HasHint: Boolean; override;
function CanShowHint: Boolean; override;
function GetDefaultSize: TSizeF; override;
procedure RecalculateAbsoluteMatrices; override;
procedure SetVisible(const Value: Boolean); override;
procedure SetEnabled(const Value: Boolean); override;
procedure DoAbsoluteChanged; override;
public
constructor Create(AOwner: TComponent); override;
// Public Properties, die die Events exponieren
property OnPaint: TControlPaintEvent read FPaintEvent write FPaintEvent;
property OnMouseDown: TControlMouseEvent read FMouseDownEvent write FMouseDownEvent;
property OnMouseUp: TControlMouseEvent read FMouseUpEvent write FMouseUpEvent;
property OnMouseMove: TControlMouseMoveEvent read FMouseMoveEvent write FMouseMoveEvent;
property OnMouseWheel: TControlMouseWheelEvent read FMouseWheelEvent write FMouseWheelEvent;
property OnClick: TControlNotifyEvent read FClickEvent write FClickEvent;
property OnDblClick: TControlNotifyEvent read FDblClickEvent write FDblClickEvent;
property OnKeyDown: TControlKeyEvent read FKeyDownEvent write FKeyDownEvent;
property OnKeyUp: TControlKeyEvent read FKeyUpEvent write FKeyUpEvent;
property OnDragEnter: TControlDragEvent read FDragEnterEvent write FDragEnterEvent;
property OnDragOver: TControlDragOverEvent read FDragOverEvent write FDragOverEvent;
property OnDragDrop: TControlDragEvent read FDragDropEvent write FDragDropEvent;
property OnDragLeave: TControlNotifyEvent read FDragLeaveEvent write FDragLeaveEvent;
property OnDragEnd: TControlNotifyEvent read FDragEndEvent write FDragEndEvent;
property OnEnter: TControlNotifyEvent read FEnterEvent write FEnterEvent;
property OnExit: TControlNotifyEvent read FExitEvent write FExitEvent;
property OnMouseEnter: TControlNotifyEvent read FMouseEnterEvent write FMouseEnterEvent;
property OnMouseLeave: TControlNotifyEvent read FMouseLeaveEvent write FMouseLeaveEvent;
property OnResize: TControlNotifyEvent read FResizeEvent write FResizeEvent;
property OnResized: TControlNotifyEvent read FResizedEvent write FResizedEvent;
property OnCanFocus: TControlCanFocusEvent read FCanFocusEvent write FCanFocusEvent;
property OnShowContextMenu: TControlShowContextMenuEvent read FShowContextMenuEvent write FShowContextMenuEvent;
property OnSetHint: TControlSetHintEvent read FSetHintEvent write FSetHintEvent;
property OnGetHintString: TControlGetHintStringEvent read FGetHintStringEvent write FGetHintStringEvent;
property OnHasHint: TControlHasHintEvent read FHasHintEvent write FHasHintEvent;
property OnCanShowHint: TControlCanShowHintEvent read FCanShowHintEvent write FCanShowHintEvent;
property OnGetDefaultSize: TControlGetDefaultSizeEvent read FGetDefaultSizeEvent write FGetDefaultSizeEvent;
property OnRecalculateAbsoluteMatrices: TControlNotifyEvent read FRecalculateAbsoluteMatricesEvent write FRecalculateAbsoluteMatricesEvent;
property OnSetVisible: TControlSetVisibleEvent read FSetVisibleEvent write FSetVisibleEvent;
property OnSetEnabled: TControlSetEnabledEvent read FSetEnabledEvent write FSetEnabledEvent;
property OnDoAbsoluteChanged: TControlDoAbsoluteChangedEvent read FDoAbsoluteChangedEvent write FDoAbsoluteChangedEvent;
end;
implementation
{ TDynamicControl }
constructor TDynamicControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 150;
Height := 100;
HitTest := True;
end;
function TDynamicControl.CanShowHint: Boolean;
begin
if Assigned(FCanShowHintEvent) then
Result := FCanShowHintEvent
else
Result := inherited CanShowHint;
end;
procedure TDynamicControl.Click;
begin
inherited Click;
if Assigned(FClickEvent) then
FClickEvent;
end;
procedure TDynamicControl.DblClick;
begin
inherited DblClick;
if Assigned(FDblClickEvent) then
FDblClickEvent;
end;
procedure TDynamicControl.DoAbsoluteChanged;
begin
inherited DoAbsoluteChanged;
if Assigned(FDoAbsoluteChangedEvent) then
FDoAbsoluteChangedEvent;
end;
procedure TDynamicControl.DragDrop(const Data: TDragObject; const Point: TPointF);
begin
inherited DragDrop(Data, Point);
if Assigned(FDragDropEvent) then
FDragDropEvent(Data, Point);
end;
procedure TDynamicControl.DragEnd;
begin
inherited DragEnd;
if Assigned(FDragEndEvent) then
FDragEndEvent;
end;
procedure TDynamicControl.DragEnter(const Data: TDragObject; const Point: TPointF);
begin
inherited DragEnter(Data, Point);
if Assigned(FDragEnterEvent) then
FDragEnterEvent(Data, Point);
end;
procedure TDynamicControl.DragLeave;
begin
inherited DragLeave;
if Assigned(FDragLeaveEvent) then
FDragLeaveEvent;
end;
procedure TDynamicControl.DragOver(const Data: TDragObject; const Point: TPointF; var Operation: TDragOperation);
begin
inherited DragOver(Data, Point, Operation);
if Assigned(FDragOverEvent) then
FDragOverEvent(Data, Point, Operation);
end;
procedure TDynamicControl.DoEnter;
begin
inherited DoEnter;
if Assigned(FEnterEvent) then
FEnterEvent;
end;
procedure TDynamicControl.DoExit;
begin
inherited DoExit;
if Assigned(FExitEvent) then
FExitEvent;
end;
procedure TDynamicControl.DoMouseEnter;
begin
inherited DoMouseEnter;
if Assigned(FMouseEnterEvent) then
FMouseEnterEvent;
end;
procedure TDynamicControl.DoMouseLeave;
begin
inherited DoMouseLeave;
if Assigned(FMouseLeaveEvent) then
FMouseLeaveEvent;
end;
procedure TDynamicControl.DoResized;
begin
inherited DoResized;
if Assigned(FResizedEvent) then
FResizedEvent;
end;
function TDynamicControl.GetCanFocus: Boolean;
begin
if Assigned(FCanFocusEvent) then
Result := FCanFocusEvent
else
Result := inherited GetCanFocus;
end;
function TDynamicControl.GetDefaultSize: TSizeF;
begin
if Assigned(FGetDefaultSizeEvent) then
Result := FGetDefaultSizeEvent
else
Result := inherited GetDefaultSize;
end;
function TDynamicControl.GetHintString: string;
begin
if Assigned(FGetHintStringEvent) then
Result := FGetHintStringEvent
else
Result := inherited GetHintString;
end;
function TDynamicControl.HasHint: Boolean;
begin
if Assigned(FHasHintEvent) then
Result := FHasHintEvent
else
Result := inherited HasHint;
end;
procedure TDynamicControl.KeyDown(var Key: Word; var KeyChar: WideChar; Shift: TShiftState);
begin
inherited KeyDown(Key, KeyChar, Shift);
if Assigned(FKeyDownEvent) then
FKeyDownEvent(Key, KeyChar, Shift);
end;
procedure TDynamicControl.KeyUp(var Key: Word; var KeyChar: WideChar; Shift: TShiftState);
begin
inherited KeyUp(Key, KeyChar, Shift);
if Assigned(FKeyUpEvent) then
FKeyUpEvent(Key, KeyChar, Shift);
end;
procedure TDynamicControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
inherited MouseDown(Button, Shift, X, Y);
if Assigned(FMouseDownEvent) then
FMouseDownEvent(Button, Shift, X, Y);
end;
procedure TDynamicControl.MouseMove(Shift: TShiftState; X, Y: Single);
begin
inherited MouseMove(Shift, X, Y);
if Assigned(FMouseMoveEvent) then
FMouseMoveEvent(Shift, X, Y);
end;
procedure TDynamicControl.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
inherited MouseUp(Button, Shift, X, Y);
if Assigned(FMouseUpEvent) then
FMouseUpEvent(Button, Shift, X, Y);
end;
procedure TDynamicControl.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
begin
inherited MouseWheel(Shift, WheelDelta, Handled);
if Assigned(FMouseWheelEvent) then
FMouseWheelEvent(Shift, WheelDelta, Handled);
end;
procedure TDynamicControl.Paint;
begin
inherited Paint;
if Assigned(FPaintEvent) then
FPaintEvent
else
begin
Canvas.Fill.Color := TAlphaColors.LightSteelBlue;
Canvas.FillRect(LocalRect, 0, 0, [], 1);
Canvas.Stroke.Color := TAlphaColors.Gray;
Canvas.Stroke.Thickness := 1;
Canvas.DrawRect(LocalRect, 0, 0, [], 1);
end;
end;
procedure TDynamicControl.RecalculateAbsoluteMatrices;
begin
inherited RecalculateAbsoluteMatrices;
if Assigned(FRecalculateAbsoluteMatricesEvent) then
FRecalculateAbsoluteMatricesEvent;
end;
procedure TDynamicControl.Resize;
begin
inherited Resize;
if Assigned(FResizeEvent) then
FResizeEvent;
end;
procedure TDynamicControl.SetEnabled(const Value: Boolean);
begin
inherited SetEnabled(Value);
if Assigned(FSetEnabledEvent) then
FSetEnabledEvent(Value);
end;
procedure TDynamicControl.SetHint(const AHint: string);
begin
inherited SetHint(AHint);
if Assigned(FSetHintEvent) then
FSetHintEvent(AHint);
end;
procedure TDynamicControl.SetVisible(const Value: Boolean);
begin
inherited SetVisible(Value);
if Assigned(FSetVisibleEvent) then
FSetVisibleEvent(Value);
end;
function TDynamicControl.ShowContextMenu(const ScreenPosition: TPointF): Boolean;
begin
if Assigned(FShowContextMenuEvent) then
Result := FShowContextMenuEvent(ScreenPosition)
else
Result := inherited ShowContextMenu(ScreenPosition);
end;
end.
+457
View File
@@ -0,0 +1,457 @@
unit FirstStrategy;
interface
uses
System.Generics.Collections,
Myc.Signals,
Myc.Lazy,
Myc.Trade.DataPoint,
Myc.Trade.DataArray,
Myc.Core.Notifier;
type
TTimeframe = (M1, M5, H1, D);
TTag = Pointer;
IMycBroadcast<T> = interface
function Link(const Strategy: IMycProcessor<T>): TTag;
procedure Unlink(Tag: TTag);
end;
// A contained object that broadcasts Values to linked strategies
TMycBroadcast<T> = class(TContainedObject, IMycBroadcast<T>)
private
FLinkedStrategies: TMycNotifyList<IMycProcessor<T>>;
protected
// Link a strategy
function Link(const Strategy: IMycProcessor<T>): TTag;
// Unlink a linked strategy
procedure Unlink(Tag: TTag);
// Broadcasts the given data points to all linked strategies.
procedure Broadcast(const Value: T);
public
constructor Create(const Controller: IInterface);
destructor Destroy; override;
end;
IMycConverter<S, T> = interface(IMycProcessor<S>)
function GetObservers: IMycBroadcast<T>;
property Observers: IMycBroadcast<T> read GetObservers;
end;
TMycConverter<S, T> = class abstract(TInterfacedObject, IMycProcessor<S>, IMycConverter<S, T>)
private
FObservers: TMycBroadcast<T>;
protected
procedure Broadcast(const Value: T);
function GetObservers: IMycBroadcast<T>;
procedure ProcessData(const Value: S); virtual; abstract;
public
constructor Create;
destructor Destroy; override;
end;
TMycGenericConverter<S, T> = class(TMycConverter<S, T>)
type
TConvertFunc = reference to function(const Value: S): T;
private
FFunc: TConvertFunc;
protected
procedure ProcessData(const Value: S); override;
public
constructor Create(const AFunc: TConvertFunc);
end;
// Series
IMycSeriesConverter<S, T> = interface(IMycConverter<TArray<S>, T>)
function GetLookback: Integer;
property Lookback: Integer read GetLookback;
end;
// Indicator
IMycIndicator<S, T> = interface(IMycSeriesConverter<S, TArray<T>>)
end;
TMycIndicator<S, T> = class abstract(TMycConverter<TArray<S>, TArray<T>>, IMycIndicator<S, T>)
protected
function GetLookback: Integer; virtual; abstract;
procedure ProcessData(const Value: TArray<S>); override; abstract;
public
property Lookback: Integer read GetLookback;
end;
TMycGenericIndicator<S, T> = class(TMycIndicator<S, T>)
type
TConvertFunc = reference to function(const Value: S): T;
private
FLookback: Integer;
FFunc: TConvertFunc;
protected
function GetLookback: Integer; override;
procedure ProcessData(const Value: TArray<S>); override;
public
constructor Create(ALookback: Integer; const AFunc: TConvertFunc);
end;
ITicksToTimeframe = interface(IMycConverter<TArray<TDataPoint<TAskBidItem>>, TArray<TDataPoint<TOhlcItem>>>)
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetStateText: TWriteable<String>;
function GetTimeframe: TTimeframe;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property StateText: TWriteable<String> read GetStateText;
property Timeframe: TTimeframe read GetTimeframe;
end;
TTicksToTimeframe = class(TMycConverter<TArray<TDataPoint<TAskBidItem>>, TArray<TDataPoint<TOhlcItem>>>, ITicksToTimeframe)
private
FTimeframe: TTimeframe;
FStateText: TWriteable<String>;
// Stores the currently aggregating OHLC data.
FCurrentBar: TDataPoint<TOhlcItem>;
function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetStateText: TWriteable<String>;
function GetTimeframe: TTimeframe;
public
constructor Create(const ATimeframe: TTimeframe; const AStateText: TWriteable<String>);
// Process new data. This is called concurrently and must not have side effects out of the scope of this class!
procedure ProcessData(const Values: TArray<TDataPoint<TAskBidItem>>); override;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property StateText: TWriteable<String> read GetStateText;
property Timeframe: TTimeframe read GetTimeframe;
end;
// Implements the Hull Moving Average indicator.
THullMovingAverage = class(TMycIndicator<Double, Double>)
private
FPeriod: Integer;
FPeriodHalf: Integer;
FPeriodSqrt: Integer;
// Source data for HMA calculation
FSourceData: TMycDataArray<Double>;
// Intermediate data series for HMA calculation (2*WMA(n/2) - WMA(n))
FDiffSeries: TMycDataArray<Double>;
// Calculates the Weighted Moving Average for the most recent data.
function CalculateWMA(const Series: TMycDataArray<Double>; const Period: Integer): Double;
protected
procedure ProcessData(const Values: TArray<Double>); override;
function GetLookback: Integer; override;
public
constructor Create(const APeriod: Integer);
end;
implementation
uses
System.SysUtils,
System.DateUtils,
System.Math;
{ TMycBroadcast<T> }
constructor TMycBroadcast<T>.Create(const Controller: IInterface);
begin
inherited Create(Controller);
end;
destructor TMycBroadcast<T>.Destroy;
begin
FLinkedStrategies.Finalize;
inherited Destroy;
end;
procedure TMycBroadcast<T>.Broadcast(const Value: T);
begin
var cValue := Value;
FLinkedStrategies.Lock;
try
FLinkedStrategies.Notify(
function(const Processor: IMycProcessor<T>): Boolean
begin
Processor.ProcessData(cValue);
Result := true;
end
);
finally
FLinkedStrategies.Release;
end;
end;
function TMycBroadcast<T>.Link(const Strategy: IMycProcessor<T>): TTag;
begin
// Add the strategy to the notification list
FLinkedStrategies.Lock;
try
Result := FLinkedStrategies.Advise(Strategy);
finally
FLinkedStrategies.Release;
end;
end;
procedure TMycBroadcast<T>.Unlink(Tag: TTag);
begin
FLinkedStrategies.Lock;
try
FLinkedStrategies.Unadvise(Tag);
finally
FLinkedStrategies.Release;
end;
end;
{ TTicksToTimeframe }
constructor TTicksToTimeframe.Create(const ATimeframe: TTimeframe; const AStateText: TWriteable<String>);
begin
inherited Create;
FStateText := AStateText;
FTimeframe := ATimeframe;
end;
function TTicksToTimeframe.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
begin
// Align the time grid to UTC 0:00 using functions from System.DateUtils
case Timeframe of
M1: Result := RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0);
M5: Result := RecodeMinute(RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0), MinuteOf(TimeStamp) - MinuteOf(TimeStamp) mod 5);
H1: Result := RecodeMinute(RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0), 0);
D: Result := StartOfTheDay(TimeStamp);
else
Result := 0;
end;
end;
function TTicksToTimeframe.GetCurrentBar: TDataPoint<TOhlcItem>;
begin
Result := FCurrentBar;
end;
function TTicksToTimeframe.GetStateText: TWriteable<String>;
begin
Result := FStateText;
end;
function TTicksToTimeframe.GetTimeframe: TTimeframe;
begin
Result := FTimeframe;
end;
procedure TTicksToTimeframe.ProcessData(const Values: TArray<TDataPoint<TAskBidItem>>);
var
point: TDataPoint<TAskBidItem>;
midPrice: Single;
barStartTime: TDateTime;
lastBarTime: TDateTime;
currentBar: TOhlcItem;
producedBars: TList<TDataPoint<TOhlcItem>>;
begin
producedBars := TList<TDataPoint<TOhlcItem>>.Create;
try
// Process each incoming data point
for point in Values do
begin
midPrice := (point.Data.Ask + point.Data.Bid) / 2;
// Update bar for the strategy's timeframe
barStartTime := GetBarStartTime(point.Time, FTimeframe);
lastBarTime := FCurrentBar.Time;
if (barStartTime > lastBarTime) then
begin
// A new bar starts, so the previous one is now complete.
if (lastBarTime > 0) then
begin
producedBars.Add(FCurrentBar);
end;
// Start a new bar, Volume is 1 because this is the first tick.
currentBar := TOhlcItem.Create(midPrice, midPrice, midPrice, midPrice, 1);
FCurrentBar.Data := currentBar;
FCurrentBar.Time := barStartTime;
end
else
begin
// Update the currently aggregating bar
currentBar := FCurrentBar.Data;
currentBar.High := Max(currentBar.High, midPrice);
currentBar.Low := Min(currentBar.Low, midPrice);
currentBar.Close := midPrice;
// Volume is the number of ticks needed to build the complete bar.
currentBar.Volume := currentBar.Volume + 1;
FCurrentBar.Data := currentBar;
end;
end;
with FCurrentBar do
FStateText.Value :=
Format('Cuur Bar: O:%.5f H:%.5f L:%.5f C:%.5f V:%.0f', [Data.Open, Data.High, Data.Low, Data.Close, Data.Volume]);
Broadcast(producedBars.ToArray);
finally
producedBars.Free;
end;
end;
{ THullMovingAverage }
constructor THullMovingAverage.Create(const APeriod: Integer);
begin
inherited Create;
FPeriod := APeriod;
FPeriodHalf := APeriod div 2;
FPeriodSqrt := Round(Sqrt(APeriod));
// Initialize data arrays.
FSourceData := TMycDataArray<Double>.CreateEmpty;
FDiffSeries := TMycDataArray<Double>.CreateEmpty;
end;
function THullMovingAverage.CalculateWMA(const Series: TMycDataArray<Double>; const Period: Integer): Double;
var
i: Integer;
numerator: Double;
denominator: Int64;
begin
// Ensure there is enough data to calculate the WMA
if (Series.Count < Period) or (Period <= 0) then
Exit(0.0);
numerator := 0;
// The sum of weights (1 + 2 + ... + Period)
denominator := Period * (Period + 1) div 2;
if (denominator = 0) then
Exit(0.0);
for i := 0 to Period - 1 do
begin
// Newest data (index 0) gets the highest weight (Period)
numerator := numerator + Series[i] * (Period - i);
end;
Result := numerator / denominator;
end;
function THullMovingAverage.GetLookback: Integer;
begin
Result := FPeriod + FPeriodSqrt - 1;
end;
procedure THullMovingAverage.ProcessData(const Values: TArray<Double>);
var
i: Integer;
price: Double;
wmaHalf, wmaFull, diff: Double;
hma: Double;
resultArray: TArray<Double>;
begin
// Pre-allocate the result array since its size is known in advance.
SetLength(resultArray, Length(Values));
for i := 0 to High(Values) do
begin
price := Values[i];
// Default HMA to 0.0 for the warm-up period.
hma := 0.0;
// Add new price to the source data array, respecting the lookback period.
FSourceData := FSourceData.Add(price, FPeriod);
// Check if there is enough data to start the first stage of calculation.
if (FSourceData.Count >= FPeriod) then
begin
// Calculate the two WMAs for the first step.
wmaHalf := CalculateWMA(FSourceData, FPeriodHalf);
wmaFull := CalculateWMA(FSourceData, FPeriod);
// Calculate the difference and add to the intermediate series.
diff := 2 * wmaHalf - wmaFull;
FDiffSeries := FDiffSeries.Add(diff, FPeriodSqrt);
// Check if there is enough intermediate data for the final calculation.
if (FDiffSeries.Count >= FPeriodSqrt) then
begin
// Calculate the final HMA value, overwriting the default 0.0.
hma := CalculateWMA(FDiffSeries, FPeriodSqrt);
end;
end;
// Assign the result (either the calculated HMA or 0.0) directly into the array.
resultArray[i] := hma;
end;
// Broadcast the result array if the input was not empty.
if (Length(resultArray) > 0) then
begin
Broadcast(resultArray);
end;
end;
{ TMycGenericConverter<S, T> }
constructor TMycGenericConverter<S, T>.Create(const AFunc: TConvertFunc);
begin
inherited Create;
FFunc := AFunc;
end;
procedure TMycGenericConverter<S, T>.ProcessData(const Value: S);
begin
Broadcast(FFunc(Value));
end;
constructor TMycGenericIndicator<S, T>.Create(ALookback: Integer; const AFunc: TConvertFunc);
begin
inherited Create;
FFunc := AFunc;
FLookback := ALookback;
end;
function TMycGenericIndicator<S, T>.GetLookback: Integer;
begin
Result := FLookback;
end;
procedure TMycGenericIndicator<S, T>.ProcessData(const Value: TArray<S>);
var
Arr: TArray<T>;
begin
inherited;
SetLength(Arr, Length(Value));
for var i := 0 to High(Arr) do
Arr[i] := FFunc(Value[i]);
Broadcast(Arr);
end;
{ TMycConverter<S, T> }
constructor TMycConverter<S, T>.Create;
begin
inherited Create;
FObservers := TMycBroadcast<T>.Create(Self);
end;
destructor TMycConverter<S, T>.Destroy;
begin
FObservers.Free;
inherited Destroy;
end;
procedure TMycConverter<S, T>.Broadcast(const Value: T);
begin
FObservers.Broadcast(Value);
end;
function TMycConverter<S, T>.GetObservers: IMycBroadcast<T>;
begin
Result := FObservers;
end;
end.
+9 -6
View File
@@ -70,7 +70,7 @@ object Form1: TForm1
Size.Height = 400.00000000000000000
Size.PlatformDefault = False
TabOrder = 7
object FlowLayout1: TFlowLayout
object FlowLayout: TFlowLayout
Position.X = 224.00000000000000000
Position.Y = 167.00000000000000000
TabOrder = 4
@@ -128,12 +128,15 @@ object Form1: TForm1
end
end
end
object SpeedButton1: TSpeedButton
Position.X = 376.00000000000000000
Position.Y = 8.00000000000000000
Text = 'SpeedButton1'
object StrategyButton: TSpeedButton
Align = FitLeft
Position.X = 290.90911865234370000
Size.Width = 145.45446777343750000
Size.Height = 40.00000000000000000
Size.PlatformDefault = False
Text = 'Strategy'
TextSettings.Trimming = None
OnClick = SpeedButton1Click
OnClick = StrategyButtonClick
end
end
end
+145 -48
View File
@@ -44,7 +44,9 @@ uses
FMX.ImgList,
System.Actions,
FMX.ActnList,
TestChartControl;
DynamicFMXControl,
FirstStrategy,
Myc.FMX.Chart;
type
TForm1 = class(TForm)
@@ -69,9 +71,9 @@ type
TreeView: TTreeView;
TestButton: TSpeedButton;
TestAction: TAction;
SpeedButton1: TSpeedButton;
TestPopup: TPopup;
FlowLayout1: TFlowLayout;
FlowLayout: TFlowLayout;
StrategyButton: TSpeedButton;
procedure RandomButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
@@ -82,31 +84,33 @@ type
procedure TreeViewDblClick(Sender: TObject);
procedure AddWorkspaceActionExecute(Sender: TObject);
procedure TestActionExecute(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure StrategyButtonClick(Sender: TObject);
private
const
cnt = 20;
type
TRndItem = record
Stream: IDataStream<TAskBidItem>;
Data: TMutable<TDataSeries<TAskBidItem>>;
Stream: IDataStream<TAuraAskBidFileItem>;
Data: TMutable<TDataSeries<TAuraAskBidFileItem>>;
Labl: TLabel;
end;
private
FOnEvent: TNotifyEvent;
{ Private declarations }
FServer: IDataServer<TAskBidItem>;
FServer: IDataServer<TAuraAskBidFileItem>;
FSymbols: TFuture<TArray<String>>;
FRandom: TList<TRndItem>;
FTerminate: TEvent;
FLoadDone: TState;
FProcessDone: TState;
FApplication: IAuraApplication;
FModulesItem: TTreeViewItem;
function SelectedSymbol: String;
function ExecuteStrategy(const Symbol: String; const Processor: IMycProcessor<TArray<TDataPoint<TAskBidItem>>>): TState;
public
procedure NewWorkspace;
function CurrLayout: TFlowLayout;
function CurrLayout<T: TControl>: T;
procedure AlignControl(Control: TControl);
published
property OnEvent: TNotifyEvent read FOnEvent write FOnEvent;
end;
@@ -134,10 +138,6 @@ begin
scrollbox.Parent := tab;
scrollbox.Align := TAlignLayout.Client;
var flow := TFlowLayout.Create(Self);
flow.Parent := tab;
flow.Align := TAlignLayout.Client;
tab.ProcessSignal(ws.Name.Changed, procedure begin tab.Text := ws.Caption; end);
end;
@@ -146,6 +146,68 @@ begin
FTerminate.Notify;
end;
procedure TForm1.StrategyButtonClick(Sender: TObject);
begin
var Layout := CurrLayout<TVertScrollBox>;
if Layout = nil then
exit;
var Symbol := SelectedSymbol;
if Symbol = '' then
exit;
var stateText := TWriteable<String>.CreateWriteable.Protect;
var stateLabel := TLabel.Create(Self);
AlignControl( stateLabel );
stateLabel.Text := 'Initializing...';
var chart := TMycChart.Create(Self);
AlignControl( chart );
chart.Height := Layout.ChildrenRect.Width*9/16;
/////
var strategy: ITicksToTimeframe := TTicksToTimeframe.Create( D, stateText );
var Ohlc: IMycIndicator<TDataPoint<TOhlcItem>, TOhlcItem> := TMycGenericIndicator<TDataPoint<TOhlcItem>, TOhlcItem>.Create( 0,
function( const Ohlc: TDataPoint<TOhlcItem> ): TOhlcItem
begin
Result := Ohlc.Data;
end );
strategy.Observers.Link(Ohlc);
var Closes: IMycIndicator<TOhlcItem, Double> := TMycGenericIndicator<TOhlcItem, Double>.Create( 0,
function( const Ohlc: TOhlcItem ): Double
begin
Result := Ohlc.Close;
end );
Ohlc.Observers.Link( Closes );
var Hull: IMycIndicator<Double, Double> := THullMovingAverage.Create( 50 );
Closes.Observers.Link( Hull );
Hull.Observers.Link(chart.CreateDoubleListener);
var done := ExecuteStrategy( Symbol, strategy );
/////
stateLabel.ProcessSignal(
stateText.Changed, done,
procedure
begin
stateLabel.Text := stateText.Value;
end );
FProcessDone := TState.All([FProcessDone, done]);
strategy.Observers.Link(chart.CreateOhlcListener);
end;
procedure TForm1.TreeViewDblClick(Sender: TObject);
begin
var sel := TreeView.Selected as TTreeViewItem;
@@ -209,6 +271,7 @@ begin
// Create an instance of the TAuraTABFileServer. The server can be reused for multiple stream creations. [364]
FServer := TAuraTABFileServer.Create('\\COFFEE\TickData\Pepperstone');
SymbolsComboBox.Enabled := false;
ChartButton.Enabled := false;
LoadButton.Enabled := false;
@@ -244,7 +307,7 @@ procedure TForm1.FormDestroy(Sender: TObject);
begin
FSymbols.WaitFor;
FTerminate.Notify;
TaskManager.WaitFor(FLoadDone);
TaskManager.WaitFor(FProcessDone);
FRandom.Free;
end;
@@ -257,12 +320,12 @@ procedure TForm1.LoadButtonClick(Sender: TObject);
begin
if SymbolsComboBox.ItemIndex < 0 then
exit;
var Layout := CurrLayout;
var Layout := CurrLayout<TVertScrollBox>;
if Layout = nil then
exit;
var Stream := FServer.CreateStream(FSymbols.WaitFor[SymbolsComboBox.ItemIndex]);
var Data := TDataStreamProvider.Create<TAskBidItem>(30000, 10000, Stream);
var Data := TDataStreamProvider.Create<TAuraAskBidFileItem>(30000, 10000, Stream);
var path := TPath.Create(Self);
path.Parent := Layout;
@@ -292,14 +355,14 @@ end;
procedure TForm1.RandomButtonClick(Sender: TObject);
begin
var Layout := CurrLayout;
var Layout := CurrLayout<TVertScrollBox>;
if Layout = nil then
exit;
var rnd: TRndItem;
rnd.Stream := FServer.CreateStream(FSymbols.WaitFor[Random(Length(FSymbols.WaitFor))]);
rnd.Data := TDataStreamProvider.Create<TAskBidItem>(3000, 1000, rnd.Stream);
rnd.Data := TDataStreamProvider.Create<TAuraAskBidFileItem>(3000, 1000, rnd.Stream);
rnd.Labl := TLabel.Create(Self);
rnd.Labl.Parent := Layout;
@@ -346,9 +409,22 @@ begin
NewWorkspace;
end;
procedure TForm1.AlignControl(Control: TControl);
begin
var Layout := CurrLayout<TVertScrollBox>;
if Layout = nil then
exit;
Control.Parent := Layout;
Control.Width := Layout.Width;
Control.Position.Y := Layout.ChildrenRect.Bottom+1;
Control.Anchors := [TAnchorKind.akLeft, TAnchorKind.akRight];
Control.Align := TAlignLayout.Top;
end;
procedure TForm1.ChartButtonClick(Sender: TObject);
begin
var Layout := CurrLayout;
var Layout := CurrLayout<TVertScrollBox>;
if Layout = nil then
exit;
@@ -361,41 +437,44 @@ begin
const width = 300;
var terminated := TFlag.CreateObserver(FTerminate.Signal).State;
var done :=
TaskManager.RunTask(
nil,
function: TState
begin
var Prices := TDataSeries<TAskBidItem>.CreateDataSeries(width);
var Prices := TDataSeries<TAuraAskBidFileItem>.CreateDataSeries(width);
Result :=
FServer.ProcessData(
Symbol,
FTerminate.Signal,
procedure(const Values: TArray<TDataPoint<TAskBidItem>>; const Terminated: TState)
begin
Prices := Prices.Add(Values);
currLog.Value := Prices.TotalCount.ToString;
var PathData := TPathData.Create;
if Prices.Count > 0 then
terminated,
TMycGenericProcessor<TArray<TDataPoint<TAuraAskBidFileItem>>>.Create(
procedure(const Values: TArray<TDataPoint<TAuraAskBidFileItem>>)
begin
var n := Prices.Count;
PathData.MoveTo(PointF(n - 1, Prices[0].Data.Ask));
for var i := 1 to n - 1 do
PathData.LineTo(PointF(n - i - 1, Prices[i].Data.Ask));
Prices := Prices.Add(Values);
currLog.Value := Prices.TotalCount.ToString;
for var i := n - 1 downto 0 do
PathData.LineTo(PointF(n - i - 1, Prices[i].Data.Bid));
end;
PathData.ClosePath;
var PathData := TPathData.Create;
if Prices.Count > 0 then
begin
var n := Prices.Count;
PathData.MoveTo(PointF(n - 1, Prices[0].Data.Ask));
for var i := 1 to n - 1 do
PathData.LineTo(PointF(n - i - 1, Prices[i].Data.Ask));
currPathData.Value := TObjectRef<TPathData>.Create(PathData);
end
for var i := n - 1 downto 0 do
PathData.LineTo(PointF(n - i - 1, Prices[i].Data.Bid));
end;
PathData.ClosePath;
currPathData.Value := TObjectRef<TPathData>.Create(PathData);
end )
);
end
);
FLoadDone := TState.All([FLoadDone, done]);
FProcessDone := TState.All([FProcessDone, done]);
var path := TPath.Create(Self);
path.Parent := Layout;
@@ -412,19 +491,19 @@ begin
);
end;
function TForm1.CurrLayout: TFlowLayout;
function TForm1.CurrLayout<T>: T;
begin
if TabControl.ActiveTab = nil then
exit(nil);
var Res: TFlowLayout := nil;
var Res: T := nil;
TabControl.ActiveTab.EnumControls(
function(Control: TControl): TEnumControlsResult
begin
Result := TEnumControlsResult.Continue;
if Control is TFlowLayout then
if Control is T then
begin
Res := Control as TFlowLayout;
Res := Control as T;
Result := TEnumControlsResult.Stop;
end;
end
@@ -433,6 +512,29 @@ begin
Result := Res;
end;
function TForm1.ExecuteStrategy(const Symbol: String; const Processor: IMycProcessor<TArray<TDataPoint<TAskBidItem>>>): TState;
var
dataProvider: IMycConverter<TArray<TDataPoint<TAuraAskBidFileItem>>, TArray<TDataPoint<TAskBidItem>>>;
begin
var terminated := TFlag.CreateObserver(FTerminate.Signal).State;
dataProvider := TMycGenericConverter<TArray<TDataPoint<TAuraAskBidFileItem>>, TArray<TDataPoint<TAskBidItem>>>.Create(
function( const Values: TArray<TDataPoint<TAuraAskBidFileItem>> ): TArray<TDataPoint<TAskBidItem>>
begin
SetLength( Result, Length( Values ) );
for var i:=0 to High(Result) do
begin
Result[i].Time := Values[i].Time;
Result[i].Data.Ask := Values[i].Data.Ask;
Result[i].Data.Bid := Values[i].Data.Bid;
end;
end );
dataProvider.Observers.Link( Processor );
Result := FServer.ProcessData( Symbol, terminated, dataProvider );
end;
function TForm1.SelectedSymbol: String;
begin
Result := '';
@@ -442,9 +544,4 @@ begin
Result := FSymbols.WaitFor[SymbolsComboBox.ItemIndex];
end;
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
TestChartForm.Visible := true;
end;
end.
+419
View File
@@ -0,0 +1,419 @@
unit Myc.Fmx.Chart;
interface
uses
System.SysUtils,
System.Classes,
System.Types,
System.Generics.Collections,
System.UITypes,
System.UIConsts,
System.Messaging,
System.Math.Vectors,
FMX.Types,
FMX.Controls,
FMX.Graphics,
Myc.Trade.DataPoint,
Myc.Signals;
type
TCandleStyle = (csCandleStick, csHiLoBar);
TMycChart = class(TStyledControl)
type
TSeries = class abstract(TContainedObject)
private
function GetMainSeries: TSeries;
function GetOwner: TMycChart;
protected
function GetCount: Int64; virtual; abstract;
function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract;
function Update: Boolean; virtual; abstract;
procedure Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc<Double, Single>); virtual; abstract;
public
constructor Create(AOwner: TMycChart);
property Count: Int64 read GetCount;
property MainSeries: TSeries read GetMainSeries;
property Owner: TMycChart read GetOwner;
end;
private
FSeriesList: TList<TSeries>;
FLookback: Integer;
FIdleSubscrId: TMessageSubscriptionId;
protected
procedure Paint; override;
procedure DoIdle;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
// Creates an OHLC candlestick/bar series
function CreateOhlcListener(
const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red;
const AStyle: TCandleStyle = csCandleStick
): IMycProcessor<TArray<TDataPoint<TOhlcItem>>>;
// Creates a simple line series for double values
function CreateDoubleListener(const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5):
IMycProcessor<TArray<Double>>;
// The maximum number of data points to display from the main series.
property Lookback: Integer read FLookback write FLookback;
end;
implementation
uses
System.Math,
System.SyncObjs, WinApi.Windows,
Myc.Trade.DataArray;
type
TChartSeriesProcessor<T> = class(TMycChart.TSeries, IMycProcessor<TArray<T>>)
strict private
FDataSeries: TMycDataArray<T>;
FChanged: Boolean;
FLock: TSpinLock;
procedure ProcessData(const Values: TArray<T>);
private
FData: TMycDataArray<T>;
protected
function GetCount: Int64; override;
function Update: Boolean; override;
public
constructor Create(AOwner: TMycChart);
property Data: TMycDataArray<T> read FData;
end;
{ TChartOhlcSeries }
TChartOhlcSeries = class(TChartSeriesProcessor<TDataPoint<TOhlcItem>>)
private
FUpColor: TAlphaColor;
FDownColor: TAlphaColor;
FStyle: TCandleStyle;
protected
function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; override;
procedure Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc<Double, Single>); override;
public
constructor Create(AOwner: TMycChart; const AUpColor, ADownColor: TAlphaColor; AStyle: TCandleStyle);
end;
{ TChartLineSeries }
TChartLineSeries = class(TChartSeriesProcessor<Double>)
private
FLineColor: TAlphaColor;
FLineWidth: Single;
protected
function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; override;
procedure Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc<Double, Single>); override;
public
constructor Create(AOwner: TMycChart; const ALineColor: TAlphaColor; ALineWidth: Single);
end;
{ TMycChart.TSeries }
constructor TMycChart.TSeries.Create(AOwner: TMycChart);
begin
inherited Create(AOwner);
end;
function TMycChart.TSeries.GetMainSeries: TSeries;
begin
Result := nil;
if Owner.FSeriesList.Count > 0 then
Result := Owner.FSeriesList[0];
end;
function TMycChart.TSeries.GetOwner: TMycChart;
begin
Result := Controller as TMycChart;
end;
{ TMycChart }
constructor TMycChart.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FSeriesList := TObjectList<TSeries>.Create(true);
FLookback := 100; // Default lookback
FIdleSubscrId :=
TMessageManager
.DefaultManager
.SubscribeToMessage(TIdleMessage, procedure(const Sender: TObject; const M: TMessage) begin DoIdle; end);
end;
destructor TMycChart.Destroy;
begin
TMessageManager.DefaultManager.Unsubscribe(TIdleMessage, FIdleSubscrId);
FSeriesList.Free;
inherited;
end;
function TMycChart.CreateDoubleListener(const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5):
IMycProcessor<TArray<Double>>;
var
series: TChartLineSeries;
begin
series := TChartLineSeries.Create(Self, ALineColor, ALineWidth);
FSeriesList.Add(series);
Result := series;
end;
function TMycChart.CreateOhlcListener(
const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red;
const AStyle: TCandleStyle = csCandleStick
): IMycProcessor<TArray<TDataPoint<TOhlcItem>>>;
var
series: TChartOhlcSeries;
begin
series := TChartOhlcSeries.Create(Self, AUpColor, ADownColor, AStyle);
FSeriesList.Add(series);
Result := series;
end;
procedure TMycChart.DoIdle;
begin
var doRepaint := false;
for var series in FSeriesList do
if series.Update then
doRepaint := true;
if doRepaint then
Repaint;
end;
procedure TMycChart.Paint;
var
rect: TRectF;
series: TSeries;
globalMin, globalMax, seriesMin, seriesMax, padding: Double;
rangeInitialized: Boolean;
xTransform: TFunc<Double, Single>;
yTransform: TFunc<Double, Single>;
begin
inherited;
rect := Self.LocalRect;
if (FSeriesList.Count = 0) or (FLookback <= 1) then
begin
Canvas.Fill.Color := TAlphaColors.Gray;
Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center);
Exit;
end;
rangeInitialized := false;
for series in FSeriesList do
begin
if series.GetValueRange(0, FLookback, seriesMin, seriesMax) then
begin
if not rangeInitialized then
begin
globalMin := seriesMin;
globalMax := seriesMax;
rangeInitialized := true;
end
else
begin
globalMin := Min(globalMin, seriesMin);
globalMax := Max(globalMax, seriesMax);
end;
end;
end;
if not rangeInitialized then
Exit;
padding := (globalMax - globalMin) * 0.1;
globalMin := globalMin - padding;
globalMax := globalMax + padding;
if (globalMax - globalMin) = 0 then
exit;
xTransform := function(index: Double): Single begin Result := rect.Right - (index / (FLookback - 1)) * rect.Width; end;
yTransform :=
function(value: Double): Single
begin
Result := rect.Top + (1 - (value - globalMin) / (globalMax - globalMin)) * rect.Height;
end;
// var T :=
// TMatrix.CreateTranslation(rect.Left, rect.Top + rect.Height*globalMax / (globalMax - globalMin)) *
// TMatrix.CreateScaling(rect.Width / (FLookback - 1), -rect.Height / (globalMax - globalMin));
for series in FSeriesList do
begin
series.Paint(Self.Canvas, xTransform, yTransform);
end;
end;
{ TChartSeriesProcessor<T> }
constructor TChartSeriesProcessor<T>.Create(AOwner: TMycChart);
begin
inherited Create(AOwner);
FLock := TSpinLock.Create(false);
FDataSeries := TMycDataArray<T>.CreateEmpty;
FData := FDataSeries;
end;
function TChartSeriesProcessor<T>.GetCount: Int64;
begin
Result := FData.Count;
end;
procedure TChartSeriesProcessor<T>.ProcessData(const Values: TArray<T>);
begin
FLock.Enter;
try
FDataSeries := FDataSeries.Add(Values, 0, Length(Values), Owner.Lookback);
FChanged := true;
finally
FLock.Exit;
end;
end;
function TChartSeriesProcessor<T>.Update: Boolean;
begin
FLock.Enter;
try
Result := FChanged;
FChanged := false;
if Result then
FData := FDataSeries;
finally
FLock.Exit;
end;
end;
{ TChartOhlcSeries }
constructor TChartOhlcSeries.Create(AOwner: TMycChart; const AUpColor, ADownColor: TAlphaColor; AStyle: TCandleStyle);
begin
inherited Create(AOwner);
FUpColor := AUpColor;
FDownColor := ADownColor;
FStyle := AStyle;
end;
function TChartOhlcSeries.GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean;
var
i: Int64;
begin
Result := GetCount > 0;
if not Result then
Exit;
Min := MaxDouble;
Max := -MaxDouble;
for i := StartIndex to System.Math.Min(GetCount - 1, StartIndex + Count - 1) do
begin
Min := System.Math.Min(Min, Data.Items[i].Data.Low);
Max := System.Math.Max(Max, Data.Items[i].Data.High);
end;
Result := (Min <> MaxDouble);
end;
procedure TChartOhlcSeries.Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc<Double, Single>);
var
i, displayCount: Int64;
x, candleWidth: Single;
yOpen, yHigh, yLow, yClose: Single;
item: TDataPoint<TOhlcItem>;
isUp: boolean;
begin
displayCount := System.Math.Min(Data.Count, Owner.Lookback);
if displayCount <= 0 then
Exit;
if displayCount > 1 then
candleWidth := Max(2, 0.8 * Abs((AXForm(1) - AXForm(0))))
else
candleWidth := 10;
for i := 0 to displayCount - 1 do
begin
item := Data.Items[i];
x := AXForm(i);
yOpen := AYForm(item.Data.Open);
yClose := AYForm(item.Data.Close);
yHigh := AYForm(item.Data.High);
yLow := AYForm(item.Data.Low);
isUp := item.Data.Close >= item.Data.Open;
if isUp then
ACanvas.Stroke.Color := FUpColor
else
ACanvas.Stroke.Color := FDownColor;
ACanvas.Stroke.Thickness := 1.0;
ACanvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1);
if FStyle = csCandleStick then
begin
ACanvas.Fill.Color := ACanvas.Stroke.Color;
if isUp then
ACanvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1)
else
ACanvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1);
end;
end;
end;
{ TChartLineSeries }
constructor TChartLineSeries.Create(AOwner: TMycChart; const ALineColor: TAlphaColor; ALineWidth: Single);
begin
inherited Create(AOwner);
FLineColor := ALineColor;
FLineWidth := ALineWidth;
end;
function TChartLineSeries.GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean;
var
i: Int64;
begin
Result := GetCount > 0;
if not Result then
Exit;
Min := MaxDouble;
Max := -MaxDouble;
for i := StartIndex to System.Math.Min(GetCount - 1, StartIndex + Count - 1) do
begin
Min := System.Math.Min(Min, Data.Items[i]);
Max := System.Math.Max(Max, Data.Items[i]);
end;
Result := (Min <> MaxDouble);
end;
procedure TChartLineSeries.Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc<Double, Single>);
begin
if not Assigned(MainSeries) or (Data.Count = 0) or (MainSeries.Count = 0) then
Exit;
var points := TPathData.Create;
var displayCount := System.Math.Min(Data.Count, Owner.Lookback);
if displayCount < 2 then
Exit;
points.MoveTo( TPointF.Create(AXForm(0), AYForm(Data[0])) );
for var i := 1 to displayCount - 1 do
points.LineTo( TPointF.Create(AXForm(i), AYForm(Data[i])) );
ACanvas.Stroke.Color := FLineColor;
ACanvas.Stroke.Thickness := FLineWidth;
ACanvas.DrawPath(points, 1);
end;
end.
+10 -7
View File
@@ -23,6 +23,9 @@ type
Receiver: T; // The registered interface instance (the event sink).
end;
// Notify function. If this results false, it will be removed from the notify list
TNotifyProc = reference to function(const Obj: T): Boolean;
strict private
// Head of the linked list. The pointer value itself is repurposed for a spinlock.
// Bit 0 of the address stores the lock state (0 = locked, 1 = unlocked).
@@ -36,9 +39,9 @@ type
public
// Initializes the list in an unlocked state.
procedure Create;
class operator Initialize(out Dest: TMycNotifyList<T>);
// Safely clears all registered receivers and cleans up resources.
procedure Destroy;
procedure Finalize;
// Registers a receiver interface and returns an opaque tag for later unsubscription.
function Advise(const Receiver: T): TTag;
// Unregisters a single receiver using its subscription tag.
@@ -54,17 +57,17 @@ type
// Invokes a predicate for each registered receiver.
// If the predicate returns false, the receiver is detached from further notifications
// by setting its interface reference to nil. The list item itself is not freed here.
procedure Notify(Func: TPredicate<T>);
procedure Notify(const Func: TNotifyProc);
end;
implementation
procedure TMycNotifyList<T>.Create;
class operator TMycNotifyList<T>.Initialize(out Dest: TMycNotifyList<T>);
begin
NativeUInt(FList) := 1;
NativeUInt(Dest.FList) := 1;
end;
procedure TMycNotifyList<T>.Destroy;
procedure TMycNotifyList<T>.Finalize;
begin
Lock;
UnadviseAll;
@@ -114,7 +117,7 @@ begin
Result := NativeUInt(FList) and 1 = 0;
end;
procedure TMycNotifyList<T>.Notify(Func: TPredicate<T>);
procedure TMycNotifyList<T>.Notify(const Func: TNotifyProc);
var
Item: PItem;
Last: PItem;
+6 -9
View File
@@ -184,12 +184,11 @@ end;
constructor TMycEvent.Create;
begin
inherited Create;
FSubscribers.Create;
end;
destructor TMycEvent.Destroy;
begin
FSubscribers.Destroy;
FSubscribers.Finalize;
inherited;
end;
@@ -202,7 +201,7 @@ function TMycEvent.Notify: Boolean;
begin
FSubscribers.Lock;
try
FSubscribers.Notify(function(Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
FSubscribers.Notify(function(const Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
finally
FSubscribers.Release;
end;
@@ -275,12 +274,11 @@ begin
inherited Create;
Assert(ACount >= 0);
FCount := ACount;
FSubscribers.Create;
end;
destructor TMycLatch.Destroy;
begin
FSubscribers.Destroy;
FSubscribers.Finalize;
inherited;
end;
@@ -300,7 +298,7 @@ begin
if shouldNotifySubscribers then
begin
FSubscribers.Notify(function(Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
FSubscribers.Notify(function(const Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
end;
// Returns true if the latch has not yet been set by this Notify call (count > 0).
@@ -395,12 +393,11 @@ constructor TMycFlag.Create(AInit: Boolean);
begin
inherited Create;
FFlag := AInit;
FSubscribers.Create;
end;
destructor TMycFlag.Destroy;
begin
FSubscribers.Destroy; // Clean up the subscriber list.
FSubscribers.Finalize;
inherited;
end;
@@ -438,7 +435,7 @@ begin
if wasPreviouslyClean then // Only notify subscribers if state changed from clean to dirty.
begin
FSubscribers.Notify(function(Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
FSubscribers.Notify(function(const Subscriber: TSignal.ISubscriber): Boolean begin Result := Subscriber.Notify; end);
end;
finally
FSubscribers.Release;
+16
View File
@@ -14,6 +14,7 @@ type
public
function ProcessSignal(const Signal: TSignal; const Proc: TMsgProc): TComponent; overload;
function ProcessSignal(const Signal: TSignal; const Proc: TProc): TComponent; overload;
function ProcessSignal(const Signal: TSignal; const Done: TState; const Proc: TProc): TComponent; overload;
end;
TSignalSyncHelper = record helper for TSignal
@@ -198,6 +199,21 @@ begin
Result := TSignalSubscriber.Create(Self, Signal, Proc);
end;
function TSignalComponentHelper.ProcessSignal(const Signal: TSignal; const Done: TState; const Proc: TProc): TComponent;
begin
var cProc: TProc := Proc;
var cDone: TState := Done;
Result :=
ProcessSignal(
Signal,
procedure(out IsDone: Boolean)
begin
cProc();
IsDone := cDone.IsSet;
end
);
end;
{ TSignalSyncHelper }
function TSignalSyncHelper.Queue(const Proc: TProc; Delay: Integer = 0): TSignal.TSubscription;
+1 -1
View File
@@ -153,7 +153,7 @@ type
class function CreateLatch(Count: Integer): ILatch; static;
class function Enqueue1(var Gate: TLatch): TState; overload; static;
class function Enqueue1(var Gate: TLatch): TState; overload; static; deprecated; experimental;
function Enqueue: TState; overload;
class property Null: ILatch read FNull;
+4 -4
View File
@@ -134,7 +134,7 @@ procedure TTestDataSeries.TestSetupSeriesWithDataVerification;
var
i: Integer;
baseTime, expectedTime: TDateTime;
expectedAsk: Single;
expectedAsk: Double;
begin
SetupSeriesWithData;
baseTime := EncodeDate(2020, 7, 7);
@@ -200,7 +200,7 @@ begin
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(Double(9 - i), FSeries.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
end;
end;
@@ -648,9 +648,9 @@ procedure TTestDataSeries.TestDataAndTimePropertiesRespectLookback;
var
baseTime: TDateTime;
expectedOldestTime: TDateTime;
expectedOldestAsk: Single;
expectedOldestAsk: Double;
expectedNewestTime: TDateTime;
expectedNewestAsk: Single;
expectedNewestAsk: Double;
begin
SetupSeriesWithData(20, 15); // Add 20 items, lookback 15.
// Visible items are those originally at index 5 through 19.
+6 -6
View File
@@ -19,8 +19,8 @@ type
[IgnoreMemoryLeaks(true)]
TTest_TABFileServer_Equivalence = class(TObject)
private
FServer: IDataServer<TAskBidItem>;
FStream: IDataStream<TAskBidItem>;
FServer: IDataServer<TAuraAskBidFileItem>;
FStream: IDataStream<TAuraAskBidFileItem>;
public
[SetupFixture]
procedure SetupFixture;
@@ -77,17 +77,17 @@ end;
procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries;
var
chunk: array[0..C_MAX_FETCH - 1] of TDataPoint<TAskBidItem>;
dst: TArray<TDataPoint<TAskBidItem>>;
chunk: array[0..C_MAX_FETCH - 1] of TDataPoint<TAuraAskBidFileItem>;
dst: TArray<TDataPoint<TAuraAskBidFileItem>>;
i: Int64;
n: Integer;
cnt: Int64;
timeout: Integer;
filename: TAuraDataFile;
expectedData: TArray<TDataPoint<TAskBidItem>>;
expectedData: TArray<TDataPoint<TAuraAskBidFileItem>>;
begin
// 1. Expected data is loaded directly using LoadDataSeries.
filename := (FServer as TAuraDataServer<TAskBidItem>).FindFirstFile(C_TEST_SYMBOL).WaitFor;
filename := (FServer as TAuraDataServer<TAuraAskBidFileItem>).FindFirstFile(C_TEST_SYMBOL).WaitFor;
Assert.IsTrue(filename.IsValid, 'Test data file could not be found.');
expectedData := (FServer as TAuraTABFileServer).LoadDataSeries(filename).WaitFor;
+21 -122
View File
@@ -5,35 +5,14 @@ interface
uses
System.Generics.Collections,
System.TimeSpan,
Myc.Trade.DataPoint;
Myc.Trade.DataPoint,
Myc.Trade.DataArray;
type
TMycDataArray<T> = record
private
const
ChunkSize = 1024;
type
TChunk = TArray<TDataPoint<T>>;
private
FChunks: TArray<TChunk>;
FCount: Int64;
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
function GetItems(Idx: Int64): TDataPoint<T>; inline;
public
constructor Create(const AChunks: TArray<TChunk>; ACount: Int64);
function Add(const Data: array of TDataPoint<T>; First, Count, Lookback: Int64): TMycDataArray<T>;
class function CreateEmpty: TMycDataArray<T>; static;
// Helper to create a data array from a raw TArray.
class function CreateFromArray(const AData: TArray<TDataPoint<T>>; First, Count: Integer): TMycDataArray<T>; static;
property Count: Int64 read FCount;
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
end;
// The implementation class for IDataSeries<T>.
TMycDataSeries<T> = class(TInterfacedObject, IDataSeries<T>)
private
FData: TMycDataArray<T>;
FData: TMycDataArray<TDataPoint<T>>;
FLookback: Int64;
FTotalCount: Int64;
function GetCount: Int64;
@@ -41,10 +20,14 @@ type
function GetLookback: Int64;
function GetTotalCount: Int64;
public
constructor Create(ALookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64);
constructor Create(ALookback: Int64; const AData: TMycDataArray<TDataPoint<T>>; ATotalCount: Int64);
destructor Destroy; override;
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
class function CreateDataSeries(Lookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64): IDataSeries<T>; static;
class function CreateDataSeries(
Lookback: Int64;
const AData: TMycDataArray<TDataPoint<T>>;
ATotalCount: Int64
): IDataSeries<T>; static;
end;
// Null object implementation for IDataSeries<T>
@@ -69,7 +52,7 @@ type
TCompositeDataSeries<T> = class(TInterfacedObject, IDataSeries<T>)
private
FBaseSeries: IDataSeries<T>;
FAddedData: TMycDataArray<T>;
FAddedData: TMycDataArray<TDataPoint<T>>;
FLookback: Int64;
FCount: Int64;
function GetCount: Int64;
@@ -77,7 +60,7 @@ type
function GetLookback: Int64;
function GetTotalCount: Int64;
public
constructor Create(const ABaseSeries: IDataSeries<T>; const AAddedData: TMycDataArray<T>);
constructor Create(const ABaseSeries: IDataSeries<T>; const AAddedData: TMycDataArray<TDataPoint<T>>);
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
class function CreateComposite(
const BaseSeries: IDataSeries<T>;
@@ -128,97 +111,9 @@ uses
System.SysUtils,
System.Math;
{ TMycDataArray<T> }
constructor TMycDataArray<T>.Create(const AChunks: TArray<TChunk>; ACount: Int64);
begin
FChunks := AChunks;
FCount := ACount;
end;
class function TMycDataArray<T>.CreateEmpty: TMycDataArray<T>;
begin
Result.FChunks := nil;
Result.FCount := 0;
end;
class function TMycDataArray<T>.CreateFromArray(const AData: TArray<TDataPoint<T>>; First, Count: Integer): TMycDataArray<T>;
begin
// Use the Add method on an empty array to perform the chunking logic.
Result := CreateEmpty.Add(AData, First, Count, Count);
end;
function TMycDataArray<T>.Add(const Data: array of TDataPoint<T>; First, Count, Lookback: Int64): TMycDataArray<T>;
var
destPhysicalIdx, sourcePhysicalIdx: Int64;
itemsToSkip: Int64;
numNewChunks: Integer;
newChunks: TArray<TChunk>;
destChunkIdx, destSubIdx: Integer;
sumCount, newCount: Int64;
begin
if Count < 0 then
Count := Length(Data) - First;
if (Lookback <= 0) or (Count = 0) then
exit(Self);
Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array');
for var i := First + 1 to First + Count - 1 do
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
if FCount > 0 then
Assert(Data[First].Time >= Self.Items[0].Time, 'First new item is older than last existing item');
sumCount := FCount + Count;
newCount := sumCount;
if (Lookback > 0) and (newCount > Lookback) then
newCount := Lookback;
itemsToSkip := sumCount - newCount;
numNewChunks := 0;
if newCount > 0 then
numNewChunks := (newCount - 1) div ChunkSize + 1;
SetLength(newChunks, numNewChunks);
for destPhysicalIdx := 0 to newCount - 1 do
begin
destChunkIdx := destPhysicalIdx div ChunkSize;
destSubIdx := destPhysicalIdx mod ChunkSize;
if destSubIdx = 0 then
SetLength(newChunks[destChunkIdx], ChunkSize);
sourcePhysicalIdx := itemsToSkip + destPhysicalIdx;
if sourcePhysicalIdx < FCount then
begin
newChunks[destChunkIdx][destSubIdx] := FChunks[sourcePhysicalIdx div ChunkSize][sourcePhysicalIdx mod ChunkSize];
end
else
begin
newChunks[destChunkIdx][destSubIdx] := Data[First + (sourcePhysicalIdx - FCount)];
end;
end;
Result := TMycDataArray<T>.Create(newChunks, newCount);
end;
function TMycDataArray<T>.GetItems(Idx: Int64): TDataPoint<T>;
var
physicalIndex: Int64;
begin
Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.');
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
end;
function TMycDataArray<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
begin
Result := FCount - LogicalIndex - 1;
end;
{ TMycDataSeries<T> }
constructor TMycDataSeries<T>.Create(ALookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64);
constructor TMycDataSeries<T>.Create(ALookback: Int64; const AData: TMycDataArray<TDataPoint<T>>; ATotalCount: Int64);
begin
inherited Create;
FLookback := ALookback;
@@ -231,7 +126,11 @@ begin
inherited;
end;
class function TMycDataSeries<T>.CreateDataSeries(Lookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64): IDataSeries<T>;
class function TMycDataSeries<T>.CreateDataSeries(
Lookback: Int64;
const AData: TMycDataArray<TDataPoint<T>>;
ATotalCount: Int64
): IDataSeries<T>;
begin
if Lookback > 0 then
Result := TMycDataSeries<T>.Create(Lookback, AData, ATotalCount)
@@ -241,7 +140,7 @@ end;
function TMycDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
var
newData: TMycDataArray<T>;
newData: TMycDataArray<TDataPoint<T>>;
newTotalCount: Int64;
begin
if Count < 0 then
@@ -319,7 +218,7 @@ end;
{ TCompositeDataSeries<T> }
constructor TCompositeDataSeries<T>.Create(const ABaseSeries: IDataSeries<T>; const AAddedData: TMycDataArray<T>);
constructor TCompositeDataSeries<T>.Create(const ABaseSeries: IDataSeries<T>; const AAddedData: TMycDataArray<TDataPoint<T>>);
begin
inherited Create;
FBaseSeries := ABaseSeries;
@@ -334,7 +233,7 @@ end;
function TCompositeDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
var
newAddedData: TMycDataArray<T>;
newAddedData: TMycDataArray<TDataPoint<T>>;
itemsInBase: Int64;
lookbackForAdd: Int64;
begin
@@ -373,7 +272,7 @@ begin
if Count = 0 then
exit(BaseSeries);
Result := TCompositeDataSeries<T>.Create(BaseSeries, TMycDataArray<T>.CreateFromArray(Data, First, Count));
Result := TCompositeDataSeries<T>.Create(BaseSeries, TMycDataArray<TDataPoint<T>>.CreateFromArray(Data, First, Count));
end;
function TCompositeDataSeries<T>.GetCount: Int64;
+123
View File
@@ -0,0 +1,123 @@
unit Myc.Trade.DataArray;
interface
uses
Myc.Trade.DataPoint;
type
TMycDataArray<T> = record
private
const
ChunkSize = 1024;
type
TChunk = TArray<T>;
private
FChunks: TArray<TChunk>;
FCount: Int64;
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
function GetItems(Idx: Int64): T; inline;
public
constructor Create(const AChunks: TArray<TChunk>; ACount: Int64);
function Add(const Data: T; Lookback: Int64): TMycDataArray<T>; overload;
function Add(const Data: array of T; First, Count, Lookback: Int64): TMycDataArray<T>; overload;
class function CreateEmpty: TMycDataArray<T>; static;
// Helper to create a data array from a raw TArray.
class function CreateFromArray(const AData: TArray<T>; First, Count: Integer): TMycDataArray<T>; static;
property Count: Int64 read FCount;
property Items[Idx: Int64]: T read GetItems; default;
end;
implementation
{ TMycDataArray<T> }
constructor TMycDataArray<T>.Create(const AChunks: TArray<TChunk>; ACount: Int64);
begin
FChunks := AChunks;
FCount := ACount;
end;
class function TMycDataArray<T>.CreateEmpty: TMycDataArray<T>;
begin
Result.FChunks := nil;
Result.FCount := 0;
end;
class function TMycDataArray<T>.CreateFromArray(const AData: TArray<T>; First, Count: Integer): TMycDataArray<T>;
begin
// Use the Add method on an empty array to perform the chunking logic.
Result := CreateEmpty.Add(AData, First, Count, Count);
end;
function TMycDataArray<T>.Add(const Data: array of T; First, Count, Lookback: Int64): TMycDataArray<T>;
var
destPhysicalIdx, sourcePhysicalIdx: Int64;
itemsToSkip: Int64;
numNewChunks: Integer;
newChunks: TArray<TChunk>;
destChunkIdx, destSubIdx: Integer;
sumCount, newCount: Int64;
begin
if Count < 0 then
Count := Length(Data) - First;
if (Lookback <= 0) or (Count = 0) then
exit(Self);
Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array');
sumCount := FCount + Count;
newCount := sumCount;
if (Lookback > 0) and (newCount > Lookback) then
newCount := Lookback;
itemsToSkip := sumCount - newCount;
numNewChunks := 0;
if newCount > 0 then
numNewChunks := (newCount - 1) div ChunkSize + 1;
SetLength(newChunks, numNewChunks);
for destPhysicalIdx := 0 to newCount - 1 do
begin
destChunkIdx := destPhysicalIdx div ChunkSize;
destSubIdx := destPhysicalIdx mod ChunkSize;
if destSubIdx = 0 then
SetLength(newChunks[destChunkIdx], ChunkSize);
sourcePhysicalIdx := itemsToSkip + destPhysicalIdx;
if sourcePhysicalIdx < FCount then
begin
newChunks[destChunkIdx][destSubIdx] := FChunks[sourcePhysicalIdx div ChunkSize][sourcePhysicalIdx mod ChunkSize];
end
else
begin
newChunks[destChunkIdx][destSubIdx] := Data[First + (sourcePhysicalIdx - FCount)];
end;
end;
Result := TMycDataArray<T>.Create(newChunks, newCount);
end;
function TMycDataArray<T>.Add(const Data: T; Lookback: Int64): TMycDataArray<T>;
begin
Result := Add([Data], 0, 1, Lookback);
end;
function TMycDataArray<T>.GetItems(Idx: Int64): T;
var
physicalIndex: Int64;
begin
Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.');
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
end;
function TMycDataArray<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
begin
Result := FCount - LogicalIndex - 1;
end;
end.
+105 -19
View File
@@ -8,18 +8,18 @@ uses
type
// A data record for an Ask/Bid price pair.
TAskBidItem = packed record
Ask: Single;
Bid: Single;
constructor Create(AAsk, ABid: Single);
Ask: Double;
Bid: Double;
constructor Create(AAsk, ABid: Double);
end;
TOhlcItem = record
Open: Single;
High: Single;
Low: Single;
Close: Single;
Volume: Single;
constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Single);
Open: Double;
High: Double;
Low: Double;
Close: Double;
Volume: Double;
constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double);
end;
// Represents a time-stamped data point in a series.
@@ -29,13 +29,38 @@ type
constructor Create(ATime: TDateTime; const AData: T);
end;
// A time-ordered series of data points, optimized for chronological additions.
// The most recently added element has the logical index 0.
IMycProcessor<T> = interface
procedure ProcessData(const Value: T);
end;
TMycGenericProcessor<T> = class(TInterfacedObject, IMycProcessor<T>)
type
TProc = reference to procedure(const Value: T);
private
FProc: TProc;
procedure ProcessData(const Value: T);
public
constructor Create(const AProc: TProc);
end;
TMycContainedProcessor<T> = class(TContainedObject, IMycProcessor<T>)
type
TProc = procedure(const Value: T) of object;
private
FProc: TProc;
procedure ProcessData(const Value: T);
public
constructor Create(const Controller: IInterface; const AProc: TProc);
end;
// An immutable time-ordered series of data points.
// The most recent element has the logical index 0.
IDataSeries<T> = interface
function GetCount: Int64;
function GetItems(Idx: Int64): TDataPoint<T>;
function GetTotalCount: Int64;
function GetLookback: Int64;
// Add data and result the new series.
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
property Count: Int64 read GetCount;
// Accesses data points by their logical index.
@@ -74,7 +99,6 @@ type
function Add(const Data: TArray<TDataPoint<T>>): TDataSeries<T>; overload;
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): TDataSeries<T>; overload;
function Convert<S>(const Func: TConvertFunc<S>): TDataSeries<S>;
// Searches for a data point by its timestamp.
@@ -99,12 +123,21 @@ type
function ToOhlc(TimeFrame: TTimeSpan): TDataSeries<TOhlcItem>;
end;
TDataSeriesOhlcHelper = record helper for TDataSeries<TOhlcItem>
function ToOpen: TDataSeries<Single>;
function ToClose: TDataSeries<Single>;
function ToHigh: TDataSeries<Single>;
function ToLow: TDataSeries<Single>;
function ToVolume: TDataSeries<Single>;
end;
implementation
uses
System.SysUtils,
System.Math,
System.Generics.Collections,
Myc.Trade.DataArray,
Myc.Trade.Core.DataPoint;
// Optimized helper function using direct TTimeSpan features and integer arithmetic.
@@ -180,17 +213,40 @@ begin
if not firstPointInBar then
ohlcPoints.Add(TDataPoint<TOhlcItem>.Create(windowEndTime, currentBar));
var dataArray := TMycDataArray<TOhlcItem>.CreateFromArray(ohlcPoints.ToArray, 0, ohlcPoints.Count);
var seriesImpl := TMycDataSeries<TOhlcItem>.Create(ohlcPoints.Count, dataArray, ohlcPoints.Count);
Result := TDataSeries<TOhlcItem>.Create(seriesImpl);
Result := TDataSeries<TOhlcItem>.CreateDataSeries(ohlcPoints.Count, ohlcPoints.ToArray);
finally
ohlcPoints.Free;
end;
end;
function TDataSeriesOhlcHelper.ToClose: TDataSeries<Single>;
begin
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Close; end);
end;
function TDataSeriesOhlcHelper.ToHigh: TDataSeries<Single>;
begin
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.High; end);
end;
function TDataSeriesOhlcHelper.ToLow: TDataSeries<Single>;
begin
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Low; end);
end;
function TDataSeriesOhlcHelper.ToOpen: TDataSeries<Single>;
begin
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Open; end);
end;
function TDataSeriesOhlcHelper.ToVolume: TDataSeries<Single>;
begin
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Volume; end);
end;
{ TAskBidItem }
constructor TAskBidItem.Create(AAsk, ABid: Single);
constructor TAskBidItem.Create(AAsk, ABid: Double);
begin
Ask := AAsk;
Bid := ABid;
@@ -198,7 +254,7 @@ end;
{ TOhlcItem }
constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Single);
constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Double);
begin
Open := AOpen;
High := AHigh;
@@ -225,12 +281,19 @@ end;
function TDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): TDataSeries<T>;
begin
{$ifdef DEBUG}
for var i := First + 1 to First + Count - 1 do
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
if FDataSeries.Count > 0 then
Assert(Data[First].Time >= FDataSeries[0].Time, 'First new item is older than last existing item');
{$endif}
Result := FDataSeries.Add(Data, First, Count);
end;
function TDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>): TDataSeries<T>;
begin
Result := FDataSeries.Add(Data, 0, Length(Data));
Result := Add(Data, 0, Length(Data));
end;
function TDataSeries<T>.Convert<S>(const Func: TConvertFunc<S>): TDataSeries<S>;
@@ -240,7 +303,8 @@ end;
class function TDataSeries<T>.CreateDataSeries(Lookback: Int64; const Data: TArray<TDataPoint<T>> = nil): TDataSeries<T>;
begin
Result := TMycDataSeries<T>.CreateDataSeries(Lookback, TMycDataArray<T>.CreateFromArray(Data, 0, Length(Data)), Length(Data));
Result :=
TMycDataSeries<T>.CreateDataSeries(Lookback, TMycDataArray<TDataPoint<T>>.CreateFromArray(Data, 0, Length(Data)), Length(Data));
end;
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
@@ -350,4 +414,26 @@ begin
Result[i] := FDataSeries[n - i].Data;
end;
constructor TMycGenericProcessor<T>.Create(const AProc: TProc);
begin
inherited Create;
FProc := AProc;
end;
procedure TMycGenericProcessor<T>.ProcessData(const Value: T);
begin
FProc(Value);
end;
constructor TMycContainedProcessor<T>.Create(const Controller: IInterface; const AProc: TProc);
begin
inherited Create(Controller);
FProc := AProc;
end;
procedure TMycContainedProcessor<T>.ProcessData(const Value: T);
begin
FProc(Value);
end;
end.
+41 -33
View File
@@ -54,14 +54,12 @@ type
function IsHistory: Boolean; virtual; abstract;
end;
TDataProc<T> = reference to procedure(const Values: TArray<TDataPoint<T>>; const Terminated: TState);
// Interface for an instantiable data server.
IDataServer<T: record> = interface
['{1F8E5A9D-E92A-44C1-9F3F-C4B82A6E94B3}']
function CreateStream(const Symbol: String): IDataStream<T>;
procedure ClearCache;
function ProcessData(const Symbol: String; const Terminate: TSignal; const Proc: TDataProc<T>): TState;
function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IMycProcessor<TArray<TDataPoint<T>>>): TState;
function EnumerateSymbols: TFuture<TArray<String>>;
end;
@@ -104,8 +102,8 @@ type
function ProcessFile(
FileInfo: TAuraDataFile;
const DataFile: TFuture<TArray<TDataPoint<T>>>;
Terminated: TState;
Proc: TDataProc<T>
const Terminated: TState;
Processor: IMycProcessor<TArray<TDataPoint<T>>>
): TState;
strict private
@@ -129,7 +127,7 @@ type
function EnumerateSymbols: TFuture<TArray<String>>;
function LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TDataPoint<T>>>;
function ProcessData(const Symbol: String; const Terminate: TSignal; const Proc: TDataProc<T>): TState;
function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IMycProcessor<TArray<TDataPoint<T>>>): TState;
property Path: String read GetPath;
end;
@@ -166,14 +164,19 @@ type
// Aura tick data file Ask-Bid
TAuraTABFileServer = class(TAuraDataServer<TAskBidItem>)
TAuraAskBidFileItem = packed record
Ask: Single;
Bid: Single;
end;
TAuraTABFileServer = class(TAuraDataServer<TAuraAskBidFileItem>)
protected
// 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;
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>; override;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>; override;
public
function CreateStream(const Symbol: String): IDataStream<TAskBidItem>; override;
function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
function CreateStream(const Symbol: String): IDataStream<TAuraAskBidFileItem>; override;
function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>;
function ParseFileName(const FileName: string): TAuraDataFile; override;
end;
@@ -390,17 +393,21 @@ begin
Result := FPath;
end;
function TAuraDataServer<T>.ProcessData(const Symbol: String; const Terminate: TSignal; const Proc: TDataProc<T>): TState;
function TAuraDataServer<T>.ProcessData(
const Symbol: String;
const Terminated: TState;
const Processor: IMycProcessor<TArray<TDataPoint<T>>>
): TState;
begin
var capProc := Proc;
var terminated := TFlag.CreateObserver(Terminate).State;
var cProc := Processor;
var cTerminated := Terminated;
Result :=
FindFirstFile(Symbol)
.Chain(
function(const FirstFileInfo: TAuraDataFile): TState
begin
Result := ProcessFile(FirstFileInfo, LoadDataFile(FirstFileInfo), terminated, capProc);
Result := ProcessFile(FirstFileInfo, LoadDataFile(FirstFileInfo), cTerminated, cProc);
end);
end;
@@ -413,23 +420,24 @@ end;
function TAuraDataServer<T>.ProcessFile(
FileInfo: TAuraDataFile;
const DataFile: TFuture<TArray<TDataPoint<T>>>;
Terminated: TState;
Proc: TDataProc<T>
const Terminated: TState;
Processor: IMycProcessor<TArray<TDataPoint<T>>>
): TState;
begin
if not FileInfo.IsValid or Terminated.IsSet then
exit(TState.Null);
exit(DataFile.Done);
// Read ahead the next file, while processing the current one
var nextFileInfo := FileInfo.GetNextFile;
var nextFile := LoadDataFile(nextFileInfo);
var cTerminated := Terminated;
Result :=
DataFile.Chain(
function(const Data: TArray<TDataPoint<T>>): TState
begin
Proc(Data, Terminated);
Result := ProcessFile(nextFileInfo, nextFile, Terminated, Proc);
Processor.ProcessData(Data);
Result := ProcessFile(nextFileInfo, nextFile, cTerminated, Processor);
end
);
end;
@@ -574,16 +582,16 @@ end;
{ TAuraTABFileServer }
function TAuraTABFileServer.CreateStream(const Symbol: String): IDataStream<TAskBidItem>;
function TAuraTABFileServer.CreateStream(const Symbol: String): IDataStream<TAuraAskBidFileItem>;
begin
Result := TAuraFileStream<TAskBidItem>.Create(Self, Symbol);
Result := TAuraFileStream<TAuraAskBidFileItem>.Create(Self, Symbol);
end;
function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>;
var
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataPoint<TAskBidItem>>>>;
liveData: TFuture<TArray<TDataPoint<TAskBidItem>>>;
loadedFiles: TArray<TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>>;
liveData: TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>;
tabFiles: TList<TAuraDataFile>;
liveFile: TAuraDataFile;
currentFileInfo: TAuraDataFile;
@@ -607,7 +615,7 @@ begin
liveFile := ParseFileName(potentialLivePath);
end;
var loadedFileList := TList<TFuture<TArray<TDataPoint<TAskBidItem>>>>.Create;
var loadedFileList := TList<TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>>.Create;
var loadStates := TList<TState>.Create;
try
for var fileInfo in tabFiles do
@@ -617,7 +625,7 @@ begin
loadStates.Add(data.Done);
end;
liveData := TFuture<TArray<TDataPoint<TAskBidItem>>>.Null;
liveData := TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>.Null;
if liveFile.IsValid then
begin
liveData := LoadDataFile(liveFile);
@@ -636,11 +644,11 @@ begin
end;
Result :=
TFuture<TArray<TDataPoint<TAskBidItem>>>.Construct(
TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>.Construct(
loadedState,
function: TArray<TDataPoint<TAskBidItem>>
function: TArray<TDataPoint<TAuraAskBidFileItem>>
begin
var tickList := TList<TDataPoint<TAskBidItem>>.Create;
var tickList := TList<TDataPoint<TAuraAskBidFileItem>>.Create;
try
var overallLastTabTickTime: TDateTime := 0;
var cnt := 0;
@@ -710,7 +718,7 @@ begin
Result := TAuraDataFile.Create(path, symbol, fileExt, year, month);
end;
class function TAuraTABFileServer.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<TAskBidItem>>;
class function TAuraTABFileServer.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>;
var
decompressionStream: TStream;
localHeader: TZipHeader;
@@ -749,11 +757,11 @@ begin
end;
end;
class function TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAskBidItem>>;
class function TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>;
type
TFileRecord = packed record
TimeStamp: TDateTime;
Data: TAskBidItem;
Data: TAuraAskBidFileItem;
end;
var
fileSize: Int64;
+6 -6
View File
@@ -74,8 +74,8 @@ end;
procedure TMycTestNotifierTests.Setup;
begin
FNotifier.Create;
// FNotifier's internal FFirst is now 1 (unlocked, not finalized)
FNotifier := Default(TMycNotifyList<IMyTestInterface>);
end;
procedure TMycTestNotifierTests.TearDown;
@@ -92,7 +92,7 @@ procedure TMycTestNotifierTests.Test01_DestroyExecutesWithoutErrors;
begin
// This call is expected to succeed without raising EAssertionFailed or other exceptions
// if TMycNotifyList.Destroy is implemented correctly.
FNotifier.Destroy;
FNotifier.Finalize;
Assert.IsTrue(True, 'FNotifier.Destroy completed without raising an exception.');
end;
@@ -102,11 +102,11 @@ end;
procedure TMycTestNotifierTests.Test02_NotifyOnFinalizedLockedEmptyListExecutesWithoutErrors;
var
predicateCallCount: Integer;
dummyPredicate: TPredicate<IMyTestInterface>;
dummyPredicate: TMycNotifyList<IMyTestInterface>.TNotifyProc;
begin
predicateCallCount := 0;
dummyPredicate :=
function(Item: IMyTestInterface): Boolean
function(const Item: IMyTestInterface): Boolean
begin
Inc(predicateCallCount);
Result := True;
@@ -209,7 +209,7 @@ begin
itemsProcessedCount := 0;
FNotifier.Notify(
function(Item: IMyTestInterface): Boolean
function(const Item: IMyTestInterface): Boolean
begin
Inc(itemsProcessedCount);
TMyTestReceiver(Item).Foo;
@@ -229,7 +229,7 @@ begin
itemsProcessedCount := 0;
FNotifier.Notify(
function(Item: IMyTestInterface): Boolean
function(const Item: IMyTestInterface): Boolean
begin
Inc(itemsProcessedCount);
TMyTestReceiver(Item).Foo;
+2 -3
View File
@@ -213,7 +213,7 @@ begin
// if not FOwnerFixture.FNotifier.IsFinalized then // Don't notify if finalized
begin
FOwnerFixture.FNotifier.Notify(
function(Item: IMyStressTestInterface): Boolean
function(const Item: IMyStressTestInterface): Boolean
begin
Item.Foo(FThreadID); // Pass ThreadID as notification type for context
Result := True; // Keep item
@@ -255,7 +255,6 @@ end;
procedure TMycNotifierChaosStressTests.Setup;
begin
Randomize; // Initialize random number generator
FNotifier.Create;
FAllReceiversCreated := TList<IMyStressTestInterface>.Create;
FCriticalSectionForList := TCriticalSection.Create;
FNextReceiverInstanceID := 0;
@@ -359,7 +358,7 @@ begin
// if not FNotifier.IsFinalized then
begin
FNotifier.Notify(
function(Item: IMyStressTestInterface): Boolean
function(const Item: IMyStressTestInterface): Boolean
begin
actualLiveReceiversInNotifier.Add(Item);
Result := True;
+2 -3
View File
@@ -154,7 +154,6 @@ end;
procedure TMycNotifierThreadingTests.Setup;
begin
Randomize;
FNotifier.Create;
FReceiverMasterList := TList<IMyTestInterface>.Create;
end;
@@ -256,7 +255,7 @@ begin
FNotifier.Lock;
try
FNotifier.Notify(
function(Item: IMyTestInterface): Boolean
function(const Item: IMyTestInterface): Boolean
begin
Inc(currentNotifyCount);
Result := True; // Keep item in the Notifier
@@ -288,7 +287,7 @@ begin
FNotifier.Lock;
try
FNotifier.Notify(
function(Item: IMyTestInterface): Boolean
function(const Item: IMyTestInterface): Boolean
begin
Inc(currentNotifyCount);
Result := True;