diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index 9425132..1b8b2c9 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -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. diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index 8d82b57..a29543d 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -4,7 +4,7 @@ 20.3 FMX True - Release + Debug Win64 AuraTrader 3 @@ -138,10 +138,10 @@ - -
TestChartForm
- fmx -
+ + + + Base diff --git a/AuraTrader/DynamicFMXControl.pas b/AuraTrader/DynamicFMXControl.pas new file mode 100644 index 0000000..df3a2a5 --- /dev/null +++ b/AuraTrader/DynamicFMXControl.pas @@ -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. diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas new file mode 100644 index 0000000..d7d87a5 --- /dev/null +++ b/AuraTrader/FirstStrategy.pas @@ -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 = interface + function Link(const Strategy: IMycProcessor): TTag; + procedure Unlink(Tag: TTag); + end; + + // A contained object that broadcasts Values to linked strategies + TMycBroadcast = class(TContainedObject, IMycBroadcast) + private + FLinkedStrategies: TMycNotifyList>; + protected + // Link a strategy + function Link(const Strategy: IMycProcessor): 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 = interface(IMycProcessor) + function GetObservers: IMycBroadcast; + property Observers: IMycBroadcast read GetObservers; + end; + + TMycConverter = class abstract(TInterfacedObject, IMycProcessor, IMycConverter) + private + FObservers: TMycBroadcast; + protected + procedure Broadcast(const Value: T); + function GetObservers: IMycBroadcast; + procedure ProcessData(const Value: S); virtual; abstract; + public + constructor Create; + destructor Destroy; override; + end; + + TMycGenericConverter = class(TMycConverter) + 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 = interface(IMycConverter, T>) + function GetLookback: Integer; + property Lookback: Integer read GetLookback; + end; + + // Indicator + + IMycIndicator = interface(IMycSeriesConverter>) + end; + + TMycIndicator = class abstract(TMycConverter, TArray>, IMycIndicator) + protected + function GetLookback: Integer; virtual; abstract; + procedure ProcessData(const Value: TArray); override; abstract; + public + property Lookback: Integer read GetLookback; + end; + + TMycGenericIndicator = class(TMycIndicator) + type + TConvertFunc = reference to function(const Value: S): T; + private + FLookback: Integer; + FFunc: TConvertFunc; + protected + function GetLookback: Integer; override; + procedure ProcessData(const Value: TArray); override; + public + constructor Create(ALookback: Integer; const AFunc: TConvertFunc); + end; + + ITicksToTimeframe = interface(IMycConverter>, TArray>>) + function GetCurrentBar: TDataPoint; + function GetStateText: TWriteable; + function GetTimeframe: TTimeframe; + property CurrentBar: TDataPoint read GetCurrentBar; + property StateText: TWriteable read GetStateText; + property Timeframe: TTimeframe read GetTimeframe; + end; + + TTicksToTimeframe = class(TMycConverter>, TArray>>, ITicksToTimeframe) + private + FTimeframe: TTimeframe; + FStateText: TWriteable; + // Stores the currently aggregating OHLC data. + FCurrentBar: TDataPoint; + function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; + function GetCurrentBar: TDataPoint; + function GetStateText: TWriteable; + function GetTimeframe: TTimeframe; + public + constructor Create(const ATimeframe: TTimeframe; const AStateText: TWriteable); + + // 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>); override; + + property CurrentBar: TDataPoint read GetCurrentBar; + property StateText: TWriteable read GetStateText; + property Timeframe: TTimeframe read GetTimeframe; + end; + + // Implements the Hull Moving Average indicator. + THullMovingAverage = class(TMycIndicator) + private + FPeriod: Integer; + FPeriodHalf: Integer; + FPeriodSqrt: Integer; + // Source data for HMA calculation + FSourceData: TMycDataArray; + // Intermediate data series for HMA calculation (2*WMA(n/2) - WMA(n)) + FDiffSeries: TMycDataArray; + // Calculates the Weighted Moving Average for the most recent data. + function CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; + protected + procedure ProcessData(const Values: TArray); override; + function GetLookback: Integer; override; + public + constructor Create(const APeriod: Integer); + end; + +implementation + +uses + System.SysUtils, + System.DateUtils, + System.Math; + +{ TMycBroadcast } + +constructor TMycBroadcast.Create(const Controller: IInterface); +begin + inherited Create(Controller); +end; + +destructor TMycBroadcast.Destroy; +begin + FLinkedStrategies.Finalize; + inherited Destroy; +end; + +procedure TMycBroadcast.Broadcast(const Value: T); +begin + var cValue := Value; + + FLinkedStrategies.Lock; + try + FLinkedStrategies.Notify( + function(const Processor: IMycProcessor): Boolean + begin + Processor.ProcessData(cValue); + Result := true; + end + ); + finally + FLinkedStrategies.Release; + end; +end; + +function TMycBroadcast.Link(const Strategy: IMycProcessor): TTag; +begin + // Add the strategy to the notification list + FLinkedStrategies.Lock; + try + Result := FLinkedStrategies.Advise(Strategy); + finally + FLinkedStrategies.Release; + end; +end; + +procedure TMycBroadcast.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); +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; +begin + Result := FCurrentBar; +end; + +function TTicksToTimeframe.GetStateText: TWriteable; +begin + Result := FStateText; +end; + +function TTicksToTimeframe.GetTimeframe: TTimeframe; +begin + Result := FTimeframe; +end; + +procedure TTicksToTimeframe.ProcessData(const Values: TArray>); +var + point: TDataPoint; + midPrice: Single; + barStartTime: TDateTime; + lastBarTime: TDateTime; + currentBar: TOhlcItem; + producedBars: TList>; +begin + producedBars := TList>.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.CreateEmpty; + FDiffSeries := TMycDataArray.CreateEmpty; +end; + +function THullMovingAverage.CalculateWMA(const Series: TMycDataArray; 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); +var + i: Integer; + price: Double; + wmaHalf, wmaFull, diff: Double; + hma: Double; + resultArray: TArray; +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 } + +constructor TMycGenericConverter.Create(const AFunc: TConvertFunc); +begin + inherited Create; + FFunc := AFunc; +end; + +procedure TMycGenericConverter.ProcessData(const Value: S); +begin + Broadcast(FFunc(Value)); +end; + +constructor TMycGenericIndicator.Create(ALookback: Integer; const AFunc: TConvertFunc); +begin + inherited Create; + FFunc := AFunc; + FLookback := ALookback; +end; + +function TMycGenericIndicator.GetLookback: Integer; +begin + Result := FLookback; +end; + +procedure TMycGenericIndicator.ProcessData(const Value: TArray); +var + Arr: TArray; +begin + inherited; + SetLength(Arr, Length(Value)); + for var i := 0 to High(Arr) do + Arr[i] := FFunc(Value[i]); + Broadcast(Arr); +end; + +{ TMycConverter } + +constructor TMycConverter.Create; +begin + inherited Create; + FObservers := TMycBroadcast.Create(Self); +end; + +destructor TMycConverter.Destroy; +begin + FObservers.Free; + inherited Destroy; +end; + +procedure TMycConverter.Broadcast(const Value: T); +begin + FObservers.Broadcast(Value); +end; + +function TMycConverter.GetObservers: IMycBroadcast; +begin + Result := FObservers; +end; + +end. diff --git a/AuraTrader/MainForm.fmx b/AuraTrader/MainForm.fmx index 6cdaca8..59ab2b2 100644 --- a/AuraTrader/MainForm.fmx +++ b/AuraTrader/MainForm.fmx @@ -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 diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index 0342aac..355133f 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -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; - Data: TMutable>; + Stream: IDataStream; + Data: TMutable>; Labl: TLabel; end; private FOnEvent: TNotifyEvent; { Private declarations } - FServer: IDataServer; + FServer: IDataServer; FSymbols: TFuture>; FRandom: TList; FTerminate: TEvent; - FLoadDone: TState; + FProcessDone: TState; FApplication: IAuraApplication; FModulesItem: TTreeViewItem; function SelectedSymbol: String; + function ExecuteStrategy(const Symbol: String; const Processor: IMycProcessor>>): TState; public procedure NewWorkspace; - function CurrLayout: TFlowLayout; + function CurrLayout: 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; + if Layout = nil then + exit; + + var Symbol := SelectedSymbol; + if Symbol = '' then + exit; + + var stateText := TWriteable.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, TOhlcItem> := TMycGenericIndicator, TOhlcItem>.Create( 0, + function( const Ohlc: TDataPoint ): TOhlcItem + begin + Result := Ohlc.Data; + end ); + + strategy.Observers.Link(Ohlc); + + var Closes: IMycIndicator := TMycGenericIndicator.Create( 0, + function( const Ohlc: TOhlcItem ): Double + begin + Result := Ohlc.Close; + end ); + + Ohlc.Observers.Link( Closes ); + + var Hull: IMycIndicator := 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; if Layout = nil then exit; var Stream := FServer.CreateStream(FSymbols.WaitFor[SymbolsComboBox.ItemIndex]); - var Data := TDataStreamProvider.Create(30000, 10000, Stream); + var Data := TDataStreamProvider.Create(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; if Layout = nil then exit; var rnd: TRndItem; rnd.Stream := FServer.CreateStream(FSymbols.WaitFor[Random(Length(FSymbols.WaitFor))]); - rnd.Data := TDataStreamProvider.Create(3000, 1000, rnd.Stream); + rnd.Data := TDataStreamProvider.Create(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; + 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; 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.CreateDataSeries(width); + var Prices := TDataSeries.CreateDataSeries(width); Result := FServer.ProcessData( Symbol, - FTerminate.Signal, - procedure(const Values: TArray>; const Terminated: TState) - begin - Prices := Prices.Add(Values); - currLog.Value := Prices.TotalCount.ToString; - - var PathData := TPathData.Create; - if Prices.Count > 0 then + terminated, + TMycGenericProcessor>>.Create( + procedure(const Values: TArray>) 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.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.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; 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>>): TState; +var + dataProvider: IMycConverter>, TArray>>; +begin + var terminated := TFlag.CreateObserver(FTerminate.Signal).State; + + dataProvider := TMycGenericConverter>, TArray>>.Create( + function( const Values: TArray> ): TArray> + 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. diff --git a/AuraTrader/Myc.Fmx.Chart.pas b/AuraTrader/Myc.Fmx.Chart.pas new file mode 100644 index 0000000..9ce2252 --- /dev/null +++ b/AuraTrader/Myc.Fmx.Chart.pas @@ -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); 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; + 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>>; + + // Creates a simple line series for double values + function CreateDoubleListener(const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5): + IMycProcessor>; + + // 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 = class(TMycChart.TSeries, IMycProcessor>) + strict private + FDataSeries: TMycDataArray; + FChanged: Boolean; + FLock: TSpinLock; + procedure ProcessData(const Values: TArray); + private + FData: TMycDataArray; + protected + function GetCount: Int64; override; + function Update: Boolean; override; + + public + constructor Create(AOwner: TMycChart); + property Data: TMycDataArray read FData; + end; + + { TChartOhlcSeries } + TChartOhlcSeries = class(TChartSeriesProcessor>) + 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); override; + public + constructor Create(AOwner: TMycChart; const AUpColor, ADownColor: TAlphaColor; AStyle: TCandleStyle); + end; + + { TChartLineSeries } + TChartLineSeries = class(TChartSeriesProcessor) + 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); 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.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>; +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>>; +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; + yTransform: TFunc; +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 } + +constructor TChartSeriesProcessor.Create(AOwner: TMycChart); +begin + inherited Create(AOwner); + FLock := TSpinLock.Create(false); + FDataSeries := TMycDataArray.CreateEmpty; + FData := FDataSeries; +end; + +function TChartSeriesProcessor.GetCount: Int64; +begin + Result := FData.Count; +end; + +procedure TChartSeriesProcessor.ProcessData(const Values: TArray); +begin + FLock.Enter; + try + FDataSeries := FDataSeries.Add(Values, 0, Length(Values), Owner.Lookback); + FChanged := true; + finally + FLock.Exit; + end; +end; + +function TChartSeriesProcessor.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); +var + i, displayCount: Int64; + x, candleWidth: Single; + yOpen, yHigh, yLow, yClose: Single; + item: TDataPoint; + 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); +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. diff --git a/Src/Myc.Core.Notifier.pas b/Src/Myc.Core.Notifier.pas index 99b4eb9..2774de6 100644 --- a/Src/Myc.Core.Notifier.pas +++ b/Src/Myc.Core.Notifier.pas @@ -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); // 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); + procedure Notify(const Func: TNotifyProc); end; implementation -procedure TMycNotifyList.Create; +class operator TMycNotifyList.Initialize(out Dest: TMycNotifyList); begin - NativeUInt(FList) := 1; + NativeUInt(Dest.FList) := 1; end; -procedure TMycNotifyList.Destroy; +procedure TMycNotifyList.Finalize; begin Lock; UnadviseAll; @@ -114,7 +117,7 @@ begin Result := NativeUInt(FList) and 1 = 0; end; -procedure TMycNotifyList.Notify(Func: TPredicate); +procedure TMycNotifyList.Notify(const Func: TNotifyProc); var Item: PItem; Last: PItem; diff --git a/Src/Myc.Core.Signals.pas b/Src/Myc.Core.Signals.pas index aa0450e..85704ef 100644 --- a/Src/Myc.Core.Signals.pas +++ b/Src/Myc.Core.Signals.pas @@ -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; diff --git a/Src/Myc.Signals.FMX.pas b/Src/Myc.Signals.FMX.pas index 8663628..314ddf2 100644 --- a/Src/Myc.Signals.FMX.pas +++ b/Src/Myc.Signals.FMX.pas @@ -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; diff --git a/Src/Myc.Signals.pas b/Src/Myc.Signals.pas index b8e851f..0cbd751 100644 --- a/Src/Myc.Signals.pas +++ b/Src/Myc.Signals.pas @@ -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; diff --git a/Src/Myc.Test.Trade.DataPoint.pas b/Src/Myc.Test.Trade.DataPoint.pas index a4a94f4..25e0b1c 100644 --- a/Src/Myc.Test.Trade.DataPoint.pas +++ b/Src/Myc.Test.Trade.DataPoint.pas @@ -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. diff --git a/Src/Myc.Test.Trade.DataStream.pas b/Src/Myc.Test.Trade.DataStream.pas index 526c8a3..737f6ce 100644 --- a/Src/Myc.Test.Trade.DataStream.pas +++ b/Src/Myc.Test.Trade.DataStream.pas @@ -19,8 +19,8 @@ type [IgnoreMemoryLeaks(true)] TTest_TABFileServer_Equivalence = class(TObject) private - FServer: IDataServer; - FStream: IDataStream; + FServer: IDataServer; + FStream: IDataStream; 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; - dst: TArray>; + chunk: array[0..C_MAX_FETCH - 1] of TDataPoint; + dst: TArray>; i: Int64; n: Integer; cnt: Int64; timeout: Integer; filename: TAuraDataFile; - expectedData: TArray>; + expectedData: TArray>; begin // 1. Expected data is loaded directly using LoadDataSeries. - filename := (FServer as TAuraDataServer).FindFirstFile(C_TEST_SYMBOL).WaitFor; + filename := (FServer as TAuraDataServer).FindFirstFile(C_TEST_SYMBOL).WaitFor; Assert.IsTrue(filename.IsValid, 'Test data file could not be found.'); expectedData := (FServer as TAuraTABFileServer).LoadDataSeries(filename).WaitFor; diff --git a/Src/Myc.Trade.Core.DataPoint.pas b/Src/Myc.Trade.Core.DataPoint.pas index 0ed1ab6..35bdf51 100644 --- a/Src/Myc.Trade.Core.DataPoint.pas +++ b/Src/Myc.Trade.Core.DataPoint.pas @@ -5,35 +5,14 @@ interface uses System.Generics.Collections, System.TimeSpan, - Myc.Trade.DataPoint; + Myc.Trade.DataPoint, + Myc.Trade.DataArray; type - TMycDataArray = record - private - const - ChunkSize = 1024; - type - TChunk = TArray>; - private - FChunks: TArray; - FCount: Int64; - - function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline; - function GetItems(Idx: Int64): TDataPoint; inline; - public - constructor Create(const AChunks: TArray; ACount: Int64); - function Add(const Data: array of TDataPoint; First, Count, Lookback: Int64): TMycDataArray; - class function CreateEmpty: TMycDataArray; static; - // Helper to create a data array from a raw TArray. - class function CreateFromArray(const AData: TArray>; First, Count: Integer): TMycDataArray; static; - property Count: Int64 read FCount; - property Items[Idx: Int64]: TDataPoint read GetItems; default; - end; - // The implementation class for IDataSeries. TMycDataSeries = class(TInterfacedObject, IDataSeries) private - FData: TMycDataArray; + FData: TMycDataArray>; 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; ATotalCount: Int64); + constructor Create(ALookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64); destructor Destroy; override; function Add(const Data: TArray>; First, Count: Integer): IDataSeries; - class function CreateDataSeries(Lookback: Int64; const AData: TMycDataArray; ATotalCount: Int64): IDataSeries; static; + class function CreateDataSeries( + Lookback: Int64; + const AData: TMycDataArray>; + ATotalCount: Int64 + ): IDataSeries; static; end; // Null object implementation for IDataSeries @@ -69,7 +52,7 @@ type TCompositeDataSeries = class(TInterfacedObject, IDataSeries) private FBaseSeries: IDataSeries; - FAddedData: TMycDataArray; + FAddedData: TMycDataArray>; FLookback: Int64; FCount: Int64; function GetCount: Int64; @@ -77,7 +60,7 @@ type function GetLookback: Int64; function GetTotalCount: Int64; public - constructor Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray); + constructor Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray>); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class function CreateComposite( const BaseSeries: IDataSeries; @@ -128,97 +111,9 @@ uses System.SysUtils, System.Math; -{ TMycDataArray } - -constructor TMycDataArray.Create(const AChunks: TArray; ACount: Int64); -begin - FChunks := AChunks; - FCount := ACount; -end; - -class function TMycDataArray.CreateEmpty: TMycDataArray; -begin - Result.FChunks := nil; - Result.FCount := 0; -end; - -class function TMycDataArray.CreateFromArray(const AData: TArray>; First, Count: Integer): TMycDataArray; -begin - // Use the Add method on an empty array to perform the chunking logic. - Result := CreateEmpty.Add(AData, First, Count, Count); -end; - -function TMycDataArray.Add(const Data: array of TDataPoint; First, Count, Lookback: Int64): TMycDataArray; -var - destPhysicalIdx, sourcePhysicalIdx: Int64; - itemsToSkip: Int64; - numNewChunks: Integer; - newChunks: TArray; - 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.Create(newChunks, newCount); -end; - -function TMycDataArray.GetItems(Idx: Int64): TDataPoint; -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.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; -begin - Result := FCount - LogicalIndex - 1; -end; - { TMycDataSeries } -constructor TMycDataSeries.Create(ALookback: Int64; const AData: TMycDataArray; ATotalCount: Int64); +constructor TMycDataSeries.Create(ALookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64); begin inherited Create; FLookback := ALookback; @@ -231,7 +126,11 @@ begin inherited; end; -class function TMycDataSeries.CreateDataSeries(Lookback: Int64; const AData: TMycDataArray; ATotalCount: Int64): IDataSeries; +class function TMycDataSeries.CreateDataSeries( + Lookback: Int64; + const AData: TMycDataArray>; + ATotalCount: Int64 +): IDataSeries; begin if Lookback > 0 then Result := TMycDataSeries.Create(Lookback, AData, ATotalCount) @@ -241,7 +140,7 @@ end; function TMycDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; var - newData: TMycDataArray; + newData: TMycDataArray>; newTotalCount: Int64; begin if Count < 0 then @@ -319,7 +218,7 @@ end; { TCompositeDataSeries } -constructor TCompositeDataSeries.Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray); +constructor TCompositeDataSeries.Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray>); begin inherited Create; FBaseSeries := ABaseSeries; @@ -334,7 +233,7 @@ end; function TCompositeDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; var - newAddedData: TMycDataArray; + newAddedData: TMycDataArray>; itemsInBase: Int64; lookbackForAdd: Int64; begin @@ -373,7 +272,7 @@ begin if Count = 0 then exit(BaseSeries); - Result := TCompositeDataSeries.Create(BaseSeries, TMycDataArray.CreateFromArray(Data, First, Count)); + Result := TCompositeDataSeries.Create(BaseSeries, TMycDataArray>.CreateFromArray(Data, First, Count)); end; function TCompositeDataSeries.GetCount: Int64; diff --git a/Src/Myc.Trade.DataArray.pas b/Src/Myc.Trade.DataArray.pas new file mode 100644 index 0000000..393fd09 --- /dev/null +++ b/Src/Myc.Trade.DataArray.pas @@ -0,0 +1,123 @@ +unit Myc.Trade.DataArray; + +interface + +uses + Myc.Trade.DataPoint; + +type + TMycDataArray = record + private + const + ChunkSize = 1024; + type + TChunk = TArray; + private + FChunks: TArray; + FCount: Int64; + + function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline; + function GetItems(Idx: Int64): T; inline; + public + constructor Create(const AChunks: TArray; ACount: Int64); + function Add(const Data: T; Lookback: Int64): TMycDataArray; overload; + function Add(const Data: array of T; First, Count, Lookback: Int64): TMycDataArray; overload; + class function CreateEmpty: TMycDataArray; static; + // Helper to create a data array from a raw TArray. + class function CreateFromArray(const AData: TArray; First, Count: Integer): TMycDataArray; static; + property Count: Int64 read FCount; + property Items[Idx: Int64]: T read GetItems; default; + end; + +implementation + +{ TMycDataArray } + +constructor TMycDataArray.Create(const AChunks: TArray; ACount: Int64); +begin + FChunks := AChunks; + FCount := ACount; +end; + +class function TMycDataArray.CreateEmpty: TMycDataArray; +begin + Result.FChunks := nil; + Result.FCount := 0; +end; + +class function TMycDataArray.CreateFromArray(const AData: TArray; First, Count: Integer): TMycDataArray; +begin + // Use the Add method on an empty array to perform the chunking logic. + Result := CreateEmpty.Add(AData, First, Count, Count); +end; + +function TMycDataArray.Add(const Data: array of T; First, Count, Lookback: Int64): TMycDataArray; +var + destPhysicalIdx, sourcePhysicalIdx: Int64; + itemsToSkip: Int64; + numNewChunks: Integer; + newChunks: TArray; + 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.Create(newChunks, newCount); +end; + +function TMycDataArray.Add(const Data: T; Lookback: Int64): TMycDataArray; +begin + Result := Add([Data], 0, 1, Lookback); +end; + +function TMycDataArray.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.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; +begin + Result := FCount - LogicalIndex - 1; +end; + +end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 9ffe086..6aea09a 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -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 = interface + procedure ProcessData(const Value: T); + end; + + TMycGenericProcessor = class(TInterfacedObject, IMycProcessor) + type + TProc = reference to procedure(const Value: T); + private + FProc: TProc; + procedure ProcessData(const Value: T); + public + constructor Create(const AProc: TProc); + end; + + TMycContainedProcessor = class(TContainedObject, IMycProcessor) + 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 = interface function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; function GetLookback: Int64; + // Add data and result the new series. function Add(const Data: TArray>; First, Count: Integer): IDataSeries; property Count: Int64 read GetCount; // Accesses data points by their logical index. @@ -74,7 +99,6 @@ type function Add(const Data: TArray>): TDataSeries; overload; function Add(const Data: TArray>; First, Count: Integer): TDataSeries; overload; - function Convert(const Func: TConvertFunc): TDataSeries; // Searches for a data point by its timestamp. @@ -99,12 +123,21 @@ type function ToOhlc(TimeFrame: TTimeSpan): TDataSeries; end; + TDataSeriesOhlcHelper = record helper for TDataSeries + function ToOpen: TDataSeries; + function ToClose: TDataSeries; + function ToHigh: TDataSeries; + function ToLow: TDataSeries; + function ToVolume: TDataSeries; + 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.Create(windowEndTime, currentBar)); - var dataArray := TMycDataArray.CreateFromArray(ohlcPoints.ToArray, 0, ohlcPoints.Count); - var seriesImpl := TMycDataSeries.Create(ohlcPoints.Count, dataArray, ohlcPoints.Count); - Result := TDataSeries.Create(seriesImpl); + Result := TDataSeries.CreateDataSeries(ohlcPoints.Count, ohlcPoints.ToArray); finally ohlcPoints.Free; end; end; +function TDataSeriesOhlcHelper.ToClose: TDataSeries; +begin + Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Close; end); +end; + +function TDataSeriesOhlcHelper.ToHigh: TDataSeries; +begin + Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.High; end); +end; + +function TDataSeriesOhlcHelper.ToLow: TDataSeries; +begin + Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Low; end); +end; + +function TDataSeriesOhlcHelper.ToOpen: TDataSeries; +begin + Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Open; end); +end; + +function TDataSeriesOhlcHelper.ToVolume: TDataSeries; +begin + Result := Self.Convert(function(const Ohlc: TDataPoint): 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.Add(const Data: TArray>; First, Count: Integer): TDataSeries; 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.Add(const Data: TArray>): TDataSeries; begin - Result := FDataSeries.Add(Data, 0, Length(Data)); + Result := Add(Data, 0, Length(Data)); end; function TDataSeries.Convert(const Func: TConvertFunc): TDataSeries; @@ -240,7 +303,8 @@ end; class function TDataSeries.CreateDataSeries(Lookback: Int64; const Data: TArray> = nil): TDataSeries; begin - Result := TMycDataSeries.CreateDataSeries(Lookback, TMycDataArray.CreateFromArray(Data, 0, Length(Data)), Length(Data)); + Result := + TMycDataSeries.CreateDataSeries(Lookback, TMycDataArray>.CreateFromArray(Data, 0, Length(Data)), Length(Data)); end; class operator TDataSeries.Finalize(var Dest: TDataSeries); @@ -350,4 +414,26 @@ begin Result[i] := FDataSeries[n - i].Data; end; +constructor TMycGenericProcessor.Create(const AProc: TProc); +begin + inherited Create; + FProc := AProc; +end; + +procedure TMycGenericProcessor.ProcessData(const Value: T); +begin + FProc(Value); +end; + +constructor TMycContainedProcessor.Create(const Controller: IInterface; const AProc: TProc); +begin + inherited Create(Controller); + FProc := AProc; +end; + +procedure TMycContainedProcessor.ProcessData(const Value: T); +begin + FProc(Value); +end; + end. diff --git a/Src/Myc.Trade.DataStream.pas b/Src/Myc.Trade.DataStream.pas index 7747db1..4187778 100644 --- a/Src/Myc.Trade.DataStream.pas +++ b/Src/Myc.Trade.DataStream.pas @@ -54,14 +54,12 @@ type function IsHistory: Boolean; virtual; abstract; end; - TDataProc = reference to procedure(const Values: TArray>; const Terminated: TState); - // Interface for an instantiable data server. IDataServer = interface ['{1F8E5A9D-E92A-44C1-9F3F-C4B82A6E94B3}'] function CreateStream(const Symbol: String): IDataStream; procedure ClearCache; - function ProcessData(const Symbol: String; const Terminate: TSignal; const Proc: TDataProc): TState; + function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IMycProcessor>>): TState; function EnumerateSymbols: TFuture>; end; @@ -104,8 +102,8 @@ type function ProcessFile( FileInfo: TAuraDataFile; const DataFile: TFuture>>; - Terminated: TState; - Proc: TDataProc + const Terminated: TState; + Processor: IMycProcessor>> ): TState; strict private @@ -129,7 +127,7 @@ type function EnumerateSymbols: TFuture>; function LoadDataFile(const DataFile: TAuraDataFile): TFuture>>; - function ProcessData(const Symbol: String; const Terminate: TSignal; const Proc: TDataProc): TState; + function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IMycProcessor>>): TState; property Path: String read GetPath; end; @@ -166,14 +164,19 @@ type // Aura tick data file Ask-Bid - TAuraTABFileServer = class(TAuraDataServer) + TAuraAskBidFileItem = packed record + Ask: Single; + Bid: Single; + end; + + TAuraTABFileServer = class(TAuraDataServer) protected // Scans a directory and returns the oldest file found for each symbol. - class function ReadCompressedData(const InputStream: TStream): TArray>; override; - class function ReadUncompressedData(const InputStream: TStream): TArray>; override; + class function ReadCompressedData(const InputStream: TStream): TArray>; override; + class function ReadUncompressedData(const InputStream: TStream): TArray>; override; public - function CreateStream(const Symbol: String): IDataStream; override; - function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture>>; + function CreateStream(const Symbol: String): IDataStream; override; + function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture>>; function ParseFileName(const FileName: string): TAuraDataFile; override; end; @@ -390,17 +393,21 @@ begin Result := FPath; end; -function TAuraDataServer.ProcessData(const Symbol: String; const Terminate: TSignal; const Proc: TDataProc): TState; +function TAuraDataServer.ProcessData( + const Symbol: String; + const Terminated: TState; + const Processor: IMycProcessor>> +): 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.ProcessFile( FileInfo: TAuraDataFile; const DataFile: TFuture>>; - Terminated: TState; - Proc: TDataProc + const Terminated: TState; + Processor: IMycProcessor>> ): 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>): 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; +function TAuraTABFileServer.CreateStream(const Symbol: String): IDataStream; begin - Result := TAuraFileStream.Create(Self, Symbol); + Result := TAuraFileStream.Create(Self, Symbol); end; -function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture>>; +function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture>>; var loadedState: TState; - loadedFiles: TArray>>>; - liveData: TFuture>>; + loadedFiles: TArray>>>; + liveData: TFuture>>; tabFiles: TList; liveFile: TAuraDataFile; currentFileInfo: TAuraDataFile; @@ -607,7 +615,7 @@ begin liveFile := ParseFileName(potentialLivePath); end; - var loadedFileList := TList>>>.Create; + var loadedFileList := TList>>>.Create; var loadStates := TList.Create; try for var fileInfo in tabFiles do @@ -617,7 +625,7 @@ begin loadStates.Add(data.Done); end; - liveData := TFuture>>.Null; + liveData := TFuture>>.Null; if liveFile.IsValid then begin liveData := LoadDataFile(liveFile); @@ -636,11 +644,11 @@ begin end; Result := - TFuture>>.Construct( + TFuture>>.Construct( loadedState, - function: TArray> + function: TArray> begin - var tickList := TList>.Create; + var tickList := TList>.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>; +class function TAuraTABFileServer.ReadCompressedData(const InputStream: TStream): TArray>; var decompressionStream: TStream; localHeader: TZipHeader; @@ -749,11 +757,11 @@ begin end; end; -class function TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray>; +class function TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray>; type TFileRecord = packed record TimeStamp: TDateTime; - Data: TAskBidItem; + Data: TAuraAskBidFileItem; end; var fileSize: Int64; diff --git a/Test/TestNotifier.pas b/Test/TestNotifier.pas index 3bf48c0..dcb9bfb 100644 --- a/Test/TestNotifier.pas +++ b/Test/TestNotifier.pas @@ -74,8 +74,8 @@ end; procedure TMycTestNotifierTests.Setup; begin - FNotifier.Create; // FNotifier's internal FFirst is now 1 (unlocked, not finalized) + FNotifier := Default(TMycNotifyList); 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; + dummyPredicate: TMycNotifyList.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; diff --git a/Test/TestNotifier_ChaosStress.pas b/Test/TestNotifier_ChaosStress.pas index 3baf431..7ac4aac 100644 --- a/Test/TestNotifier_ChaosStress.pas +++ b/Test/TestNotifier_ChaosStress.pas @@ -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.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; diff --git a/Test/TestNotifier_Threading.pas b/Test/TestNotifier_Threading.pas index d5057b0..b07f4fa 100644 --- a/Test/TestNotifier_Threading.pas +++ b/Test/TestNotifier_Threading.pas @@ -154,7 +154,6 @@ end; procedure TMycNotifierThreadingTests.Setup; begin Randomize; - FNotifier.Create; FReceiverMasterList := TList.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;