diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index 018becb..3a2f499 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -5,7 +5,6 @@ uses System.StartUpCopy, FMX.Forms, MainForm in 'MainForm.pas' {Form1}, - Myc.Trade.Core.DataPoint in '..\Src\Myc.Trade.Core.DataPoint.pas', Myc.Aura.Module in '..\Src\Myc.Aura.Module.pas', Myc.Aura.Parameter in '..\Src\Myc.Aura.Parameter.pas', TestModule in 'TestModule.pas', @@ -14,7 +13,9 @@ uses Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas', Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas', - Myc.Trade.Types in '..\Src\Myc.Trade.Types.pas'; + Myc.Trade.Types in '..\Src\Myc.Trade.Types.pas', + Myc.Trade.DataConverter in '..\Src\Myc.Trade.DataConverter.pas', + Myc.Trade.Core.DataConverter in '..\Src\Myc.Trade.Core.DataConverter.pas'; {$R *.res} diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index b01b2ad..8c03150 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -4,7 +4,7 @@ 20.3 FMX True - Debug + Release Win64 AuraTrader 3 @@ -133,7 +133,6 @@
Form1
- @@ -143,6 +142,8 @@ + + Base diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas index 3f6ab48..8f14917 100644 --- a/AuraTrader/FirstStrategy.pas +++ b/AuraTrader/FirstStrategy.pas @@ -10,36 +10,12 @@ uses Myc.TaskManager, Myc.Trade.Types, Myc.Trade.DataPoint, - Myc.Trade.DataArray; + Myc.Trade.DataArray, + Myc.Trade.DataConverter, + Myc.Trade.Core.DataConverter; type - TMycGenericConverter = class(TMycConverter) - type - TConvertFunc = reference to function(const Value: S): T; - private - FFunc: TConvertFunc; - protected - function ProcessData(const Value: S): TState; override; - public - constructor Create(const AFunc: TConvertFunc); - end; - - TIndicator = class(TMycConverter) - protected - function ProcessData(const Value: S): TState; override; final; - function Calculate(const Value: S): T; virtual; abstract; - end; - - TGenericIndicator = class(TIndicator) - private - FFunc: TIndicatorFunc; - protected - function Calculate(const Value: S): T; override; final; - public - constructor Create(const AFunc: TIndicatorFunc); - end; - - TTicksToBars = class(TMycConverter, TDataPoint>) + TTickAggregation = class(TMycConverter, TDataPoint>) private FTimeframe: TTimeframe; FCurrentBar: TDataPoint; @@ -48,64 +24,26 @@ type function GetTimeframe: TTimeframe; public constructor Create(const ATimeframe: TTimeframe); - function ProcessData(const Value: TDataPoint): TState; override; + function ProcessData(const Value: TDataPoint): TState; override; property CurrentBar: TDataPoint read GetCurrentBar; property Timeframe: TTimeframe read GetTimeframe; end; - TTicker = class(TMycConverter, T>) - public - function ProcessData(const Values: TArray): TState; override; - end; - implementation uses System.DateUtils, System.Math; -{ TMycGenericConverter } +{ TTickAggregation } -constructor TMycGenericConverter.Create(const AFunc: TConvertFunc); -begin - inherited Create; - FFunc := AFunc; -end; - -function TMycGenericConverter.ProcessData(const Value: S): TState; -begin - Result := Broadcast(FFunc(Value)); -end; - -{ TIndicator } - -function TIndicator.ProcessData(const Value: S): TState; -begin - Result := Broadcast(Calculate(Value)); -end; - -{ TGenericIndicator } - -constructor TGenericIndicator.Create(const AFunc: TIndicatorFunc); -begin - inherited Create; - FFunc := AFunc; -end; - -function TGenericIndicator.Calculate(const Value: S): T; -begin - Result := FFunc(Value); -end; - -{ TTicksToBars } - -constructor TTicksToBars.Create(const ATimeframe: TTimeframe); +constructor TTickAggregation.Create(const ATimeframe: TTimeframe); begin inherited Create; FTimeframe := ATimeframe; end; -function TTicksToBars.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; +function TTickAggregation.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; var baseTime: TDateTime; begin @@ -153,25 +91,21 @@ begin end; end; -function TTicksToBars.GetCurrentBar: TDataPoint; +function TTickAggregation.GetCurrentBar: TDataPoint; begin Result := FCurrentBar; end; -function TTicksToBars.GetTimeframe: TTimeframe; +function TTickAggregation.GetTimeframe: TTimeframe; begin Result := FTimeframe; end; -function TTicksToBars.ProcessData(const Value: TDataPoint): TState; +function TTickAggregation.ProcessData(const Value: TDataPoint): TState; var - midPrice: Single; barStartTime: TDateTime; lastBarTime: TDateTime; - currentBar: TOhlcItem; begin - midPrice := (Value.Data.Ask + Value.Data.Bid) / 2; - // Update bar for the strategy's timeframe barStartTime := GetBarStartTime(Value.Time, FTimeframe); lastBarTime := FCurrentBar.Time; @@ -185,32 +119,24 @@ begin 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.Data.Open := Value.Data; + FCurrentBar.Data.High := Value.Data; + FCurrentBar.Data.Low := Value.Data; + FCurrentBar.Data.Close := Value.Data; + FCurrentBar.Data.Volume := 1; 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; + if Value.Data > FCurrentBar.Data.High then + FCurrentBar.Data.High := Value.Data; + if Value.Data < FCurrentBar.Data.Low then + FCurrentBar.Data.Low := Value.Data; + FCurrentBar.Data.Close := Value.Data; // Volume is the number of ticks needed to build the complete bar. - currentBar.Volume := currentBar.Volume + 1; - FCurrentBar.Data := currentBar; + FCurrentBar.Data.Volume := FCurrentBar.Data.Volume + 1; end; end; -function TTicker.ProcessData(const Values: TArray): TState; -begin - var done := TLatch.CreateLatch( Length(Values) ); - - // Process each incoming data point - for var i:=0 to High(Values) do - Broadcast(Values[i]).Signal.Subscribe(done); - - Result := done.State; -end; - end. diff --git a/AuraTrader/MainForm.fmx b/AuraTrader/MainForm.fmx index 8b924e8..9d6f252 100644 --- a/AuraTrader/MainForm.fmx +++ b/AuraTrader/MainForm.fmx @@ -146,6 +146,16 @@ object Form1: TForm1 TextSettings.Trimming = None OnClick = StopButtonClick end + object Strat2Button: TSpeedButton + Align = FitLeft + Position.X = 639.119262695312500000 + Size.Width = 123.636352539062500000 + Size.Height = 34.000000000000000000 + Size.PlatformDefault = False + Text = 'Strat 2' + TextSettings.Trimming = None + OnClick = Strat2ButtonClick + end end end object ObjectsPanel: TPanel diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index 4b92402..316dc64 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -30,6 +30,7 @@ uses Myc.Trade.Types, Myc.Trade.DataStream, Myc.Trade.DataPoint, + Myc.Trade.DataConverter, Myc.Signals, Myc.Mutable, Myc.Signals.FMX, @@ -74,11 +75,13 @@ type TestPopup: TPopup; FlowLayout: TFlowLayout; StrategyButton: TSpeedButton; + Strat2Button: TSpeedButton; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure StopButtonClick(Sender: TObject); procedure TreeViewDblClick(Sender: TObject); procedure AddWorkspaceActionExecute(Sender: TObject); + procedure Strat2ButtonClick(Sender: TObject); procedure TestActionExecute(Sender: TObject); procedure StrategyButtonClick(Sender: TObject); private @@ -279,12 +282,12 @@ end; function TForm1.ExecuteStrategy(const Symbol: String; const Processor: IMycProcessor>>): TState; var - dataProvider: IMycConverter>, TArray>>; + dataProvider: TConverter>, TArray>>; begin var terminated := TFlag.CreateObserver(FTerminate.Signal).State; dataProvider := - TMycGenericConverter>, TArray>>.Create( + TConverter>, TArray>>.CreateGeneric( function(const Values: TArray>): TArray> begin SetLength(Result, Length(Values)); @@ -328,32 +331,141 @@ begin ///// - var timeframe := TTimeframe.S15; + var timeframe := TTimeframe.H4; - var ticker := TTicker>.Create; + var ticker := TConverter.CreateTicker>; - var OhlcPoint := TTicksToBars.Create(timeframe); - ticker.Sender.Link(OhlcPoint); + var lastPrice := + ticker.Chain>( + function(const Tick: TDataPoint): TDataPoint + begin + Result.Time := Tick.Time; + Result.Data := 0.5 * (Tick.Data.Ask + Tick.Data.Bid); + end + ); - var Timestamps: IMycConverter, TDateTime> := - TMycGenericConverter, TDateTime> - .Create(function(const Ohlc: TDataPoint): TDateTime begin Result := Ohlc.Time; end); + var OhlcPoint := lastPrice.Chain>(TTickAggregation.Create(timeframe)); + var Timestamps := OhlcPoint.Chain(function(const Ohlc: TDataPoint): TDateTime begin Result := Ohlc.Time; end); + + var Ohlc := OhlcPoint.Chain(function(const Ohlc: TDataPoint): TOhlcItem begin Result := Ohlc.Data; end); + + var Closes := Ohlc.Chain(function(const Ohlc: TOhlcItem): Double begin Result := Ohlc.Close; end); + + var Hull := Closes.Chain(TIndicators.CreateHMA(150)); + var Sma := Closes.Chain(TIndicators.CreateSMA(50)); + var Ema := Closes.Chain(TIndicators.CreateEMA(21)); + var Boli := Closes.Chain(TIndicators.CreateBollingerBands(20, 2.0)); + var Rsi := Closes.Chain(TIndicators.CreateRSI(14)); + var Macd := Closes.Chain(TIndicators.CreateMACD(12, 26, 9)); + var Stoch := Ohlc.Chain(TIndicators.CreateStochastic(14, 3)); + + chart.SetXAxisSeries(timeframe, Timestamps.Sender); + + var Panel := chart.AddPanel; + Panel.AddOhlcSeries(Ohlc.Sender); + Panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue); + Panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow); + Panel.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua); + Panel.AddDoubleSeries(Boli.Field('UpperBand').Sender, TAlphaColors.Gray); + Panel.AddDoubleSeries(Boli.Field('MiddleBand').Sender, TAlphaColors.Darkgray, 1.0); + Panel.AddDoubleSeries(Boli.Field('LowerBand').Sender, TAlphaColors.Gray); + + Panel := chart.AddPanel; + Panel.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia); + + Panel := chart.AddPanel; + Panel.AddDoubleSeries(Macd.Field('MacdLine').Sender, TAlphaColors.Orange); + Panel.AddDoubleSeries(Macd.Field('SignalLine').Sender, TAlphaColors.Dodgerblue); + Panel.AddDoubleSeries(Macd.Field('Histogram').Sender, TAlphaColors.Lightgreen); + + Panel := chart.AddPanel; + Panel.AddDoubleSeries(Stoch.Field('K').Sender, TAlphaColors.Green); + Panel.AddDoubleSeries(Stoch.Field('D').Sender, TAlphaColors.Red); + + ///// + + var tickChart := TMycChart.Create(Self); + tickChart.Height := Layout.ChildrenRect.Width * 9 / 16; + AlignControl(tickChart); + tickChart.Lookback.Value := 1000000; + + var TickTime := ticker.Field('Time'); + + var TickData := ticker.Field('Data'); + var TickAsk := TickData.Field('Ask'); + var TickBid := TickData.Field('Bid'); + + var TickSpread := TickData.Chain(function(const Tick: TAskBidItem): Double begin Result := Tick.Bid - Tick.Ask; end); + + { tickChart.SetXAxisSeries(TTimeframe.S, TickTime.Sender); + panel := tickChart.AddPanel; + panel.AddDoubleSeries(TickAsk.Sender, TAlphaColors.Blue); + panel.AddDoubleSeries(TickBid.Sender, TAlphaColors.Red); + + panel := tickChart.AddPanel; + panel.AddDoubleSeries(TickSpread.Sender); +} + ///// + + var done := ExecuteStrategy(Symbol, ticker); + FProcessDone := TState.All([FProcessDone, done]); +end; + +procedure TForm1.Strat2ButtonClick(Sender: TObject); +begin + var timeframe := TTimeframe.M15; + + var ticker := TConverter.CreateTicker>; + + var lastPrice := + ticker.Chain>( + function(const Tick: TDataPoint): TDataPoint + begin + Result.Time := Tick.Time; + Result.Data := 0.5 * (Tick.Data.Ask + Tick.Data.Bid); + end + ); + + var OhlcPoint := TTickAggregation.Create(timeframe); + lastPrice.Sender.Link(OhlcPoint); + + var Closes := + TConverter, Double> + .CreateGeneric(function(const Ohlc: TDataPoint): Double begin Result := Ohlc.Data.Close; end); + + var Hull := TConverter.CreateGeneric(TIndicators.CreateHMA(150)); + + var Timestamps := + TConverter, TDateTime> + .CreateGeneric(function(const Ohlc: TDataPoint): TDateTime begin Result := Ohlc.Time; end); OhlcPoint.Sender.Link(TimeStamps); + + var Layout := CurrLayout; + if Layout = nil then + exit; + + var Symbol := SelectedSymbol; + if Symbol = '' then + exit; + + var chart := TMycChart.Create(Self); + AlignControl(chart); + chart.Height := Layout.ChildrenRect.Width * 9 / 16; + chart.Lookback.Value := 50000; + + ///// + + { + chart.SetXAxisSeries(timeframe, Timestamps.Sender); var Panel := chart.AddPanel; - var Ohlc: IMycConverter, TOhlcItem> := - TMycGenericConverter, TOhlcItem> - .Create(function(const Ohlc: TDataPoint): TOhlcItem begin Result := Ohlc.Data; end); OhlcPoint.Sender.Link(Ohlc); Panel.AddOhlcSeries(Ohlc.Sender); - var Closes: IMycConverter := - TMycGenericConverter.Create(function(const Ohlc: TOhlcItem): Double begin Result := Ohlc.Close; end); - Ohlc.Sender.Link(Closes); var Hull: IMycConverter := TGenericIndicator.Create(TIndicators.CreateHMA(150)); @@ -477,6 +589,7 @@ begin var done := ExecuteStrategy(Symbol, ticker); FProcessDone := TState.All([FProcessDone, done]); + } end; end. diff --git a/Src/Myc.Aura.Module.pas b/Src/Myc.Aura.Module.pas index 0acef8b..2f4cfc6 100644 --- a/Src/Myc.Aura.Module.pas +++ b/Src/Myc.Aura.Module.pas @@ -236,9 +236,6 @@ type // A bot executes a strategy IAuraBot = interface(IAuraLiveObject) - function GetPnL: TMutable>; - // Zero-based PnL series - property PnL: TMutable> read GetPnL; end; // A strategy creates a bot that executes that strategy with given parameters and within a given time slot. @@ -262,11 +259,8 @@ type IAuraTradeResult = interface function GetPerformance: TAuraTradePerformance; - function GetTrades: TDataSeries; // Perform Monte Carlo Simulation and generate performance distribution function CalcMonteCarloSimulation(Steps: Integer): TFuture; - // PnLs of each trade = Equity curve - property Trades: TDataSeries read GetTrades; // The performance of this equity curve property Performance: TAuraTradePerformance read GetPerformance; end; diff --git a/Src/Myc.Fmx.Chart.Series.pas b/Src/Myc.Fmx.Chart.Series.pas index 69274e2..653176c 100644 --- a/Src/Myc.Fmx.Chart.Series.pas +++ b/Src/Myc.Fmx.Chart.Series.pas @@ -4,7 +4,6 @@ interface uses System.SysUtils, - System.SyncObjs, System.UITypes, FMX.Graphics, Myc.Signals, @@ -12,18 +11,11 @@ uses Myc.Trade.Types, Myc.Trade.DataArray, Myc.Trade.DataPoint, + Myc.Trade.DataConverter, + Myc.Trade.Core.DataConverter, Myc.Fmx.Chart; type - TChartSeriesCounter = class(TMycConverter) - private - FCount: Int64; - protected - function ProcessData(const Value: T): TState; override; - public - constructor Create; - end; - TChartSeriesReceiver = class(TMycProcessor) strict private FCurrData: TMycDataArray; @@ -40,19 +32,18 @@ type TChartSeriesProcessor = class(TMycChart.TSeries) strict private FDataSeries: TMycDataArray; - FLock: TSpinLock; private FData: TMycDataArray; - FDataProvider: IMycDataProvider; + FDataProvider: TDataProvider; FReceiver: TChartSeriesReceiver; - FReceiverTag: TTag; + FReceiverTag: TDataProvider.TTag; protected function GetCount: Int64; override; function GetTotalCount: Int64; override; procedure Update; public - constructor Create(const ADataProvider: IMycDataProvider; const ALookback: TMutable); + constructor Create(const ADataProvider: TDataProvider; const ALookback: TMutable); destructor Destroy; override; property Data: TMycDataArray read FData; end; @@ -65,10 +56,14 @@ type function GetSeries: TMycChart.TSeries; override; function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; abstract; procedure Update; override; - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc); - override; abstract; + procedure Paint( + const Canvas: TCanvas; + First, Last: Int64; + const XForm: TFunc; + const YForm: TFunc + ); override; abstract; public - constructor Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider); + constructor Create(AParent: TMycChart.TPanel; const ADataProvider: TDataProvider); destructor Destroy; override; end; @@ -79,11 +74,16 @@ type FDownColor: TMutable; protected function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc); override; + procedure Paint( + const Canvas: TCanvas; + First, Last: Int64; + const XForm: TFunc; + const YForm: TFunc + ); override; public constructor Create( AParent: TMycChart.TPanel; - const ADataProvider: IMycDataProvider; + const ADataProvider: TDataProvider; const AUpColor, ADownColor: TMutable ); end; @@ -95,11 +95,16 @@ type FLineWidth: Single; protected function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc); override; + procedure Paint( + const Canvas: TCanvas; + First, Last: Int64; + const XForm: TFunc; + const YForm: TFunc + ); override; public constructor Create( AParent: TMycChart.TPanel; - const ADataProvider: IMycDataProvider; + const ADataProvider: TDataProvider; const ALineColor: TAlphaColor; ALineWidth: Single ); @@ -114,7 +119,7 @@ type procedure Update; override; function GetCaption(Idx: Int64): String; override; abstract; public - constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); + constructor Create(AOwner: TMycChart; const ADataProvider: TDataProvider); destructor Destroy; override; end; @@ -126,7 +131,7 @@ type // Provide a formatted timestamp string for a given data index. function GetCaption(Idx: Int64): String; override; final; public - constructor Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: IMycDataProvider); + constructor Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: TDataProvider); end; implementation @@ -136,18 +141,6 @@ uses System.Math, FMX.Types; -constructor TChartSeriesCounter.Create; -begin - inherited Create; - FCount := 0; -end; - -function TChartSeriesCounter.ProcessData(const Value: T): TState; -begin - Result := Broadcast(FCount); - inc(FCount); -end; - { TChartSeriesReceiver } constructor TChartSeriesReceiver.Create(const ALookback: TMutable); @@ -167,13 +160,13 @@ end; { TChartSeriesProcessor } -constructor TChartSeriesProcessor.Create(const ADataProvider: IMycDataProvider; const ALookback: TMutable); +constructor TChartSeriesProcessor.Create(const ADataProvider: TDataProvider; const ALookback: TMutable); begin inherited Create; FDataProvider := ADataProvider; - FLock := TSpinLock.Create(false); FDataSeries := TMycDataArray.CreateEmpty; FData := FDataSeries; + FReceiver := TChartSeriesReceiver.Create(ALookback); FReceiverTag := FDataProvider.Link(FReceiver); end; @@ -203,7 +196,7 @@ end; constructor TChartLineLayer.Create( AParent: TMycChart.TPanel; - const ADataProvider: IMycDataProvider; + const ADataProvider: TDataProvider; const ALineColor: TAlphaColor; ALineWidth: Single ); @@ -231,8 +224,12 @@ begin Result := (MinValue < MaxValue); end; -procedure TChartLineLayer.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc); +procedure TChartLineLayer.Paint( + const Canvas: TCanvas; + First, Last: Int64; + const XForm: TFunc; + const YForm: TFunc +); var points: TPathData; n: Int64; @@ -249,13 +246,13 @@ begin if n < Last then begin points.MoveTo(TPointF.Create(XForm(n), YForm(FSeries.Data[n]))); - inc( n ); + inc(n); while n <= Last do begin if IsNaN(FSeries.Data[n]) then break; points.LineTo(TPointF.Create(XForm(n), YForm(FSeries.Data[n]))); - inc( n ); + inc(n); end; end; end; @@ -273,7 +270,7 @@ end; constructor TChartOhlcLayer.Create( AParent: TMycChart.TPanel; - const ADataProvider: IMycDataProvider; + const ADataProvider: TDataProvider; const AUpColor, ADownColor: TMutable ); begin @@ -301,8 +298,12 @@ begin Result := (MinValue <> MaxDouble); end; -procedure TChartOhlcLayer.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc); +procedure TChartOhlcLayer.Paint( + const Canvas: TCanvas; + First, Last: Int64; + const XForm: TFunc; + const YForm: TFunc +); var i: Int64; x, candleWidth: Single; @@ -340,7 +341,7 @@ end; { TChartCustomLayer } -constructor TChartCustomLayer.Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider); +constructor TChartCustomLayer.Create(AParent: TMycChart.TPanel; const ADataProvider: TDataProvider); begin inherited Create(AParent); FSeries := TChartSeriesProcessor.Create(ADataProvider, AParent.Owner.Lookback.AsMutable); @@ -364,7 +365,7 @@ end; { TChartXAxisLayer } -constructor TChartXAxisLayer.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); +constructor TChartXAxisLayer.Create(AOwner: TMycChart; const ADataProvider: TDataProvider); begin inherited Create(AOwner); FSeries := TChartSeriesProcessor.Create(ADataProvider, AOwner.Lookback.AsMutable); @@ -386,7 +387,7 @@ begin FSeries.Update; end; -constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: IMycDataProvider); +constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: TDataProvider); begin inherited Create(AOwner, ADataProvider); FTimeframe := ATimeframe; diff --git a/Src/Myc.Fmx.Chart.pas b/Src/Myc.Fmx.Chart.pas index 21f92b4..10efe72 100644 --- a/Src/Myc.Fmx.Chart.pas +++ b/Src/Myc.Fmx.Chart.pas @@ -54,8 +54,12 @@ type protected function GetOwner: TMycChart; override; final; function GetValueRange(First, Last: Int64; out Min, Max: Double): Boolean; virtual; abstract; - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc); virtual; - abstract; + procedure Paint( + const Canvas: TCanvas; + First, Last: Int64; + const XForm: TFunc; + const YForm: TFunc + ); virtual; abstract; public constructor Create(AParent: TPanel); end; @@ -105,13 +109,13 @@ type destructor Destroy; override; function AddOhlcSeries( - const DataProvider: IMycDataProvider; + const DataProvider: TDataProvider; const AUpColor: TAlphaColor = TAlphaColors.Green; const ADownColor: TAlphaColor = TAlphaColors.Red ): TMycChart.TDataLayer; function AddDoubleSeries( - const DataProvider: IMycDataProvider; + const DataProvider: TDataProvider; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5 ): TMycChart.TDataLayer; @@ -162,7 +166,7 @@ type function AddPanel: TPanel; // Sets the master series that defines the time scale (X-axis). - function SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: IMycDataProvider): TMycChart.TXAxisLayer; + function SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider): TMycChart.TXAxisLayer; property Lookback: TWriteable read FLookback write FLookback; property NeedRepaint: TFlag read FNeedRepaint; @@ -220,7 +224,7 @@ end; function TMycChart.AddPanel: TPanel; begin - Result := TPanel.Create(Self, IfThen(FPanelList.Count=0, 1, 0.2)); + Result := TPanel.Create(Self, IfThen(FPanelList.Count = 0, 1, 0.2)); FPanelList.Add(Result); Repaint; end; @@ -635,7 +639,7 @@ begin Repaint; end; -function TMycChart.SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: IMycDataProvider): TMycChart.TXAxisLayer; +function TMycChart.SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider): TMycChart.TXAxisLayer; begin FXAxisSeries.Free; @@ -665,7 +669,7 @@ begin end; function TMycChart.TPanel.AddDoubleSeries( - const DataProvider: IMycDataProvider; + const DataProvider: TDataProvider; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5 ): TMycChart.TDataLayer; @@ -675,7 +679,7 @@ begin end; function TMycChart.TPanel.AddOhlcSeries( - const DataProvider: IMycDataProvider; + const DataProvider: TDataProvider; const AUpColor: TAlphaColor = TAlphaColors.Green; const ADownColor: TAlphaColor = TAlphaColors.Red ): TMycChart.TDataLayer; @@ -742,13 +746,9 @@ begin // 2. Create transforms for this panel const padding = 2; var top: Double := Viewport.Top + padding; - var height: Double := Viewport.Height-(2*padding); + var height: Double := Viewport.Height - (2 * padding); var range: Double := localMax - localMin; - yTransform := - function(value: Double): Single - begin - Result := top + (1 - (value - localMin) / range) * height; - end; + yTransform := function(value: Double): Single begin Result := top + (1 - (value - localMin) / range) * height; end; // 3. Paint all layers in this panel for var series in FSeriesList do diff --git a/Src/Myc.Trade.Core.DataConverter.pas b/Src/Myc.Trade.Core.DataConverter.pas new file mode 100644 index 0000000..f84d46d --- /dev/null +++ b/Src/Myc.Trade.Core.DataConverter.pas @@ -0,0 +1,197 @@ +unit Myc.Trade.Core.DataConverter; + +interface + +uses + Myc.Signals, + Myc.Trade.Types, + Myc.Trade.DataPoint, + Myc.Trade.DataConverter; + +type + // Null object implementation for IMycConverter + TNullConverter = class(TInterfacedObject, TConverter.IConverter) + private + function GetSender: TDataProvider.IDataProvider; + public + function ProcessData(const Value: S): TState; + end; + + TMycConverter = class abstract(TMycProcessor, TConverter.IConverter) + private + FSender: TMycDataProvider; + function GetSender: TDataProvider.IDataProvider; + protected + function ProcessData(const Value: S): TState; override; abstract; + + // Broadcasts the given data to all linked processors. + function Broadcast(const Value: T): TState; + + public + constructor Create; + destructor Destroy; override; + property Sender: TDataProvider.IDataProvider read GetSender; + end; + + TMycGenericConverter = class(TMycConverter) + private + FFunc: TConstFunc; + protected + function ProcessData(const Value: S): TState; override; + public + constructor Create(const AFunc: TConstFunc); + end; + + TMycIndicator = class(TMycConverter) + protected + function ProcessData(const Value: S): TState; override; final; + function Calculate(const Value: S): T; virtual; abstract; + end; + + TMycDataCounter = class(TMycConverter) + private + FCount: Int64; + protected + function ProcessData(const Value: T): TState; override; + public + constructor Create; + end; + + TMycTicker = class(TMycConverter, T>) + public + function ProcessData(const Values: TArray): TState; override; + end; + + TMycRecordFieldReader = class(TMycConverter) + private + FOffset: Integer; + + public + constructor Create(const AFieldName: String); + function ProcessData(const Values: S): TState; override; + end; + +implementation + +uses + System.TypInfo, + System.SysUtils, + System.RTTI; + +{ TMycConverter } + +constructor TMycConverter.Create; +begin + inherited Create; + FSender := TMycDataProvider.Create(Self); +end; + +destructor TMycConverter.Destroy; +begin + FSender.Free; + inherited Destroy; +end; + +function TMycConverter.Broadcast(const Value: T): TState; +begin + Result := FSender.Broadcast(Value); +end; + +function TMycConverter.GetSender: TDataProvider.IDataProvider; +begin + Result := FSender; +end; + +{ TNullConverter } + +function TNullConverter.GetSender: TDataProvider.IDataProvider; +begin + Result := TDataProvider.Null; +end; + +function TNullConverter.ProcessData(const Value: S): TState; +begin + Result := TState.Null; +end; + +{ TMycGenericConverter } + +constructor TMycGenericConverter.Create(const AFunc: TConstFunc); +begin + inherited Create; + FFunc := AFunc; +end; + +function TMycGenericConverter.ProcessData(const Value: S): TState; +begin + Result := Broadcast(FFunc(Value)); +end; + +{ TMycIndicator } + +function TMycIndicator.ProcessData(const Value: S): TState; +begin + Result := Broadcast(Calculate(Value)); +end; + +constructor TMycDataCounter.Create; +begin + inherited Create; + FCount := 0; +end; + +function TMycDataCounter.ProcessData(const Value: T): TState; +begin + Result := Broadcast(FCount); + inc(FCount); +end; + +function TMycTicker.ProcessData(const Values: TArray): TState; +begin + var done := TLatch.CreateLatch(Length(Values)); + + // Process each incoming data point + for var i := 0 to High(Values) do + Broadcast(Values[i]).Signal.Subscribe(done); + + Result := done.State; +end; + +constructor TMycRecordFieldReader.Create(const AFieldName: String); +begin + inherited Create; + + var Context := TRttiContext.Create; + var Field := Context.GetType(TypeInfo(S)).GetField(AFieldName); + var TypeT := Context.GetType(TypeInfo(T)); + + var Fields := Context.GetType(TypeInfo(S)).GetFields; + var name := ''; + if AFieldName = 'Time' then + for var i := 0 to High(Fields) do + begin + name := name + ' ' + Fields[i].Name; + end; + + Assert(Assigned(Field), 'Field ' + AFieldName + ' not found'); + Assert(Field.FieldType.TypeKind = TypeT.TypeKind, 'Incorrect type'); + + if Assigned(Field) and (Field.FieldType.TypeKind = TypeT.TypeKind) then + FOffset := Field.Offset + else + FOffset := -1; +end; + +function TMycRecordFieldReader.ProcessData(const Values: S): TState; +type + PT = ^T; +begin + if FOffset < 0 then + exit(TState.Null); + + var fieldPtr := PByte(@Values); + inc(fieldPtr, FOffset); + Result := Broadcast(PT(fieldPtr)^); +end; + +end. diff --git a/Src/Myc.Trade.Core.DataPoint.pas b/Src/Myc.Trade.Core.DataPoint.pas deleted file mode 100644 index 35bdf51..0000000 --- a/Src/Myc.Trade.Core.DataPoint.pas +++ /dev/null @@ -1,460 +0,0 @@ -unit Myc.Trade.Core.DataPoint; - -interface - -uses - System.Generics.Collections, - System.TimeSpan, - Myc.Trade.DataPoint, - Myc.Trade.DataArray; - -type - // The implementation class for IDataSeries. - TMycDataSeries = class(TInterfacedObject, IDataSeries) - private - FData: TMycDataArray>; - FLookback: Int64; - FTotalCount: Int64; - function GetCount: Int64; - function GetItems(Idx: Int64): TDataPoint; - function GetLookback: Int64; - function GetTotalCount: Int64; - public - 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; - end; - - // Null object implementation for IDataSeries - TNullDataSeries = class(TInterfacedObject, IDataSeries) - strict private - class var - FNull: IDataSeries; - private - FTotalCount: Int64; - function GetCount: Int64; - function GetItems(Idx: Int64): TDataPoint; - function GetTotalCount: Int64; - function GetLookback: Int64; - class constructor CreateClass; - public - constructor Create(ATotalCount: Int64); - function Add(const Data: TArray>; First, Count: Integer): IDataSeries; - class property Null: IDataSeries read FNull; - end; - - // A virtual series that combines a base series and an array of new data without copying. - TCompositeDataSeries = class(TInterfacedObject, IDataSeries) - private - FBaseSeries: IDataSeries; - FAddedData: TMycDataArray>; - FLookback: Int64; - FCount: Int64; - function GetCount: Int64; - function GetItems(Idx: Int64): TDataPoint; - function GetLookback: Int64; - function GetTotalCount: Int64; - public - constructor Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray>); - function Add(const Data: TArray>; First, Count: Integer): IDataSeries; - class function CreateComposite( - const BaseSeries: IDataSeries; - const Data: TArray>; - First, Count: Integer - ): IDataSeries; static; - end; - - TConvertSeries = class(TInterfacedObject, IDataSeries) - private - FSource: IDataSeries; - FConvertFunc: TDataSeries.TConvertFunc; - function GetCount: Int64; - function GetItems(Idx: Int64): TDataPoint; - function GetLookback: Int64; - function GetTotalCount: Int64; - public - constructor Create(const ASource: IDataSeries; const AConvertFunc: TDataSeries.TConvertFunc); - function Add(const Data: TArray>; First, Count: Integer): IDataSeries; - end; - - TAggregateDataSeries = class(TInterfacedObject, IDataSeries) - public - // Defines the function signature for aggregating a set of source data points into a single target value. - type - TAggregateFunc = reference to function(const ASourcePoints: TArray>): S; - private - FSource: IDataSeries; - FTimeFrame: TDateTime; - FAggregateFunc: TAggregateFunc; - FCachedItems: TDictionary>; - FBaseTime: TDateTime; - FAggregatedCount: Int64; - function GetCount: Int64; - function GetItems(Idx: Int64): TDataPoint; - function GetLookback: Int64; - function GetTotalCount: Int64; - procedure CalculateAggregatedCount; - public - constructor Create(const ASource: IDataSeries; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc); - destructor Destroy; override; - function Add(const Data: TArray>; First, Count: Integer): IDataSeries; - end; - -implementation - -uses - System.SysUtils, - System.Math; - -{ TMycDataSeries } - -constructor TMycDataSeries.Create(ALookback: Int64; const AData: TMycDataArray>; ATotalCount: Int64); -begin - inherited Create; - FLookback := ALookback; - FData := AData; - FTotalCount := ATotalCount; -end; - -destructor TMycDataSeries.Destroy; -begin - inherited; -end; - -class function TMycDataSeries.CreateDataSeries( - Lookback: Int64; - const AData: TMycDataArray>; - ATotalCount: Int64 -): IDataSeries; -begin - if Lookback > 0 then - Result := TMycDataSeries.Create(Lookback, AData, ATotalCount) - else - Result := TNullDataSeries.Null; -end; - -function TMycDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; -var - newData: TMycDataArray>; - newTotalCount: Int64; -begin - if Count < 0 then - Count := Length(Data) - First; - newData := FData.Add(Data, First, Count, FLookback); - newTotalCount := FTotalCount + Count; - Result := TMycDataSeries.Create(FLookback, newData, newTotalCount); -end; - -function TMycDataSeries.GetCount: Int64; -begin - Result := FData.Count; -end; - -function TMycDataSeries.GetItems(Idx: Int64): TDataPoint; -begin - Assert((Idx >= 0) and (Idx < FData.Count), 'Index is out of bounds.'); - Result := FData.Items[Idx]; -end; - -function TMycDataSeries.GetLookback: Int64; -begin - Result := FLookback; -end; - -function TMycDataSeries.GetTotalCount: Int64; -begin - Result := FTotalCount; -end; - -{ TNullDataSeries } - -class constructor TNullDataSeries.CreateClass; -begin - FNull := TNullDataSeries.Create(0); -end; - -constructor TNullDataSeries.Create(ATotalCount: Int64); -begin - inherited Create; - FTotalCount := ATotalCount; -end; - -function TNullDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; -begin - if Count < 0 then - Count := Length(Data) - First; - - if Count > 0 then - Result := TNullDataSeries.Create(FTotalCount + Count) - else - Result := Self; -end; - -function TNullDataSeries.GetCount: Int64; -begin - Result := 0; -end; - -function TNullDataSeries.GetItems(Idx: Int64): TDataPoint; -begin - Assert(false, 'Data series is empty.'); - Result := Default(TDataPoint); -end; - -function TNullDataSeries.GetLookback: Int64; -begin - Result := 0; -end; - -function TNullDataSeries.GetTotalCount: Int64; -begin - Result := FTotalCount; -end; - -{ TCompositeDataSeries } - -constructor TCompositeDataSeries.Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray>); -begin - inherited Create; - FBaseSeries := ABaseSeries; - FAddedData := AAddedData; - FLookback := FBaseSeries.Lookback; - Assert(FLookback > 0); - - FCount := FBaseSeries.Count + FAddedData.Count; - if FCount > FLookback then - FCount := FLookback; -end; - -function TCompositeDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; -var - newAddedData: TMycDataArray>; - itemsInBase: Int64; - lookbackForAdd: Int64; -begin - if Count < 0 then - Count := Length(Data) - First; - if Count = 0 then - exit(Self); - - itemsInBase := FBaseSeries.Count; - lookbackForAdd := FLookback - itemsInBase; - if lookbackForAdd < 0 then - lookbackForAdd := 0; - - newAddedData := FAddedData.Add(Data, First, Count, lookbackForAdd); - - // Optimization: If the added data fills the entire lookback window, - // the base series is no longer relevant. We can return a simpler TMycDataSeries. - if newAddedData.Count >= FLookback then - begin - Result := TMycDataSeries.Create(FLookback, newAddedData, GetTotalCount + Count); - end - else - begin - Result := TCompositeDataSeries.Create(FBaseSeries, newAddedData); - end; -end; - -class function TCompositeDataSeries.CreateComposite( - const BaseSeries: IDataSeries; - const Data: TArray>; - First, Count: Integer -): IDataSeries; -begin - if Count < 0 then - Count := Length(Data) - First; - if Count = 0 then - exit(BaseSeries); - - Result := TCompositeDataSeries.Create(BaseSeries, TMycDataArray>.CreateFromArray(Data, First, Count)); -end; - -function TCompositeDataSeries.GetCount: Int64; -begin - Result := FCount; -end; - -function TCompositeDataSeries.GetItems(Idx: Int64): TDataPoint; -var - addedCount: Int64; -begin - Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.'); - addedCount := FAddedData.Count; - if Idx < addedCount then - begin - Result := FAddedData.Items[Idx]; - end - else - begin - Result := FBaseSeries.Items[Idx - addedCount]; - end; -end; - -function TCompositeDataSeries.GetLookback: Int64; -begin - Result := FLookback; -end; - -function TCompositeDataSeries.GetTotalCount: Int64; -begin - Result := FBaseSeries.TotalCount + FAddedData.Count; -end; - -{ TConvertSeries } - -constructor TConvertSeries.Create(const ASource: IDataSeries; const AConvertFunc: TDataSeries.TConvertFunc); -begin - inherited Create; - FSource := ASource; - FConvertFunc := AConvertFunc; -end; - -function TConvertSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; -begin - Result := TCompositeDataSeries.CreateComposite(Self, Data, First, Count); -end; - -function TConvertSeries.GetCount: Int64; -begin - Result := FSource.Count; -end; - -function TConvertSeries.GetItems(Idx: Int64): TDataPoint; -var - P: TDataPoint; -begin - P := FSource[Idx]; - Result.Create(P.Time, FConvertFunc(P)); -end; - -function TConvertSeries.GetLookback: Int64; -begin - Result := FSource.Lookback; -end; - -function TConvertSeries.GetTotalCount: Int64; -begin - Result := FSource.TotalCount; -end; - -{ TAggregateDataSeries } - -constructor TAggregateDataSeries.Create(const ASource: IDataSeries; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc); -begin - inherited Create; - FSource := ASource; - FTimeFrame := ATimeFrame; - FAggregateFunc := AAggregateFunc; - FCachedItems := TDictionary>.Create; - FAggregatedCount := -1; // -1 indicates that it has not been calculated yet -end; - -destructor TAggregateDataSeries.Destroy; -begin - FCachedItems.Free; - inherited; -end; - -procedure TAggregateDataSeries.CalculateAggregatedCount; -var - totalTimeSpan: Double; -begin - if FSource.Count = 0 then - begin - FBaseTime := 0; - FAggregatedCount := 0; - end - else - begin - // The timestamp of the oldest element serves as the anchor for our time grid. - FBaseTime := FSource.Items[FSource.Count - 1].Time; - // Total duration covered by the source series. - totalTimeSpan := FSource.Items[0].Time - FBaseTime; - if totalTimeSpan >= 0 then - FAggregatedCount := Trunc(totalTimeSpan / FTimeFrame) + 1 - else - FAggregatedCount := 0; - end; -end; - -function TAggregateDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; -begin - // Adding to an aggregated series is complex. - // The most straightforward approach is to create a composite series. - Result := TCompositeDataSeries.CreateComposite(Self, Data, First, Count); -end; - -function TAggregateDataSeries.GetCount: Int64; -begin - if FAggregatedCount = -1 then - CalculateAggregatedCount; - Result := FAggregatedCount; -end; - -function TAggregateDataSeries.GetItems(Idx: Int64): TDataPoint; -var - startTime, endTime: TDateTime; - sourcePoints: TList>; - sourceIdx: Int64; -begin - Assert((Idx >= 0) and (Idx < GetCount), 'Index is out of bounds.'); - - if FCachedItems.TryGetValue(Idx, Result) then - exit; - - // Calculate the time window for the requested aggregated data point. - // Index 0 is the newest, so we calculate backwards from the total count. - endTime := FBaseTime + (FAggregatedCount - Idx) * FTimeFrame; - startTime := endTime - FTimeFrame; - - sourcePoints := TList>.Create; - try - // Find all source data points that fall into this time window. - // We can optimize the start of the search using the IndexOf function. - sourceIdx := TDataSeries(FSource).IndexOf(endTime); - if sourceIdx = -1 then - sourceIdx := 0; // If endTime is after the last element, start at the newest. - - while (sourceIdx < FSource.Count) do - begin - var P := FSource.Items[sourceIdx]; - if P.Time < startTime then - break; // We have moved past our time window - - if P.Time < endTime then // Time is within [startTime, endTime) - begin - sourcePoints.Add(P); - end; - Inc(sourceIdx); - end; - - // The aggregation function expects the data in chronological order (oldest first), - // but we collected it in reverse. So we reverse the list. - sourcePoints.Reverse; - Result.Create(endTime, FAggregateFunc(sourcePoints.ToArray)); - finally - sourcePoints.Free; - end; - - // Cache the result for future calls - FCachedItems.Add(Idx, Result); -end; - -function TAggregateDataSeries.GetLookback: Int64; -begin - Result := FSource.Lookback; // The lookback is defined by the source series. -end; - -function TAggregateDataSeries.GetTotalCount: Int64; -begin - // The total count of aggregated items is simply its current count, as it's a view. - Result := GetCount; -end; - -end. diff --git a/Src/Myc.Trade.DataConverter.pas b/Src/Myc.Trade.DataConverter.pas new file mode 100644 index 0000000..2340881 --- /dev/null +++ b/Src/Myc.Trade.DataConverter.pas @@ -0,0 +1,145 @@ +unit Myc.Trade.DataConverter; + +interface + +uses + Myc.Signals, + Myc.Trade.Types, + Myc.Trade.DataPoint; + +type + // Interface helper for IMycConverter providing the null object pattern. + TConverter = record + type + IConverter = interface(IMycProcessor) + function GetSender: TDataProvider.IDataProvider; + property Sender: TDataProvider.IDataProvider read GetSender; + end; + + {$REGION 'private'} + strict private + class var + FNull: IConverter; + class constructor CreateClass; + private + FConverter: IConverter; + function GetSender: TDataProvider; inline; + {$ENDREGION} + public + constructor Create(const AConverter: IConverter); + + // Managed record operators + class operator Initialize(out Dest: TConverter); + class operator Implicit(const A: IConverter): TConverter; overload; + class operator Implicit(const A: TConverter): IConverter; overload; + + class function CreateGeneric(const Func: TConstFunc): TConverter; static; + + // Wrapper for IMycProcessor.ProcessData + function ProcessData(const Value: S): TState; inline; + + function Chain(const Next: TConverter): TConverter; overload; inline; + function Chain(const Func: TConstFunc): TConverter; overload; inline; + + function Field(const FieldName: String): TConverter; overload; inline; + + // Provides access to the null object instance. + class property Null: IConverter read FNull; + // Wrapper for IMycConverter.Sender + property Sender: TDataProvider read GetSender; + end; + + TConverter = record + class function CreateCounter: TConverter; static; + class function CreateTicker: TConverter, T>; static; + class function CreateRecordField(const FieldName: String): TConverter; static; + end; + +implementation + +uses + Myc.Trade.Core.DataConverter; + +{ TConverter } + +class constructor TConverter.CreateClass; +begin + // Create the singleton null object instance. + FNull := TNullConverter.Create; +end; + +constructor TConverter.Create(const AConverter: IConverter); +begin + FConverter := AConverter; + // Ensure that the internal interface is never nil. + if not Assigned(FConverter) then + FConverter := FNull; +end; + +function TConverter.Chain(const Next: TConverter): TConverter; +begin + FConverter.Sender.Link(Next); + Result := Next; +end; + +function TConverter.Chain(const Func: TConstFunc): TConverter; +begin + Result := Chain(TMycGenericConverter.Create(Func)); +end; + +class function TConverter.CreateGeneric(const Func: TConstFunc): TConverter; +begin + Result := TMycGenericConverter.Create(Func); +end; + +function TConverter.Field(const FieldName: String): TConverter; +begin + Result := Chain(TMycRecordFieldReader.Create(FieldName)); +end; + +function TConverter.GetSender: TDataProvider; +begin + // Forward the call to the wrapped interface. + Result := FConverter.Sender; +end; + +class operator TConverter.Initialize(out Dest: TConverter); +begin + // Initialize new record instances with the null object. + Dest.FConverter := FNull; +end; + +class operator TConverter.Implicit(const A: IConverter): TConverter; +begin + // Allow implicit conversion from the interface to the helper. + Result.Create(A); +end; + +class operator TConverter.Implicit(const A: TConverter): IConverter; +begin + // Allow implicit conversion from the helper to the interface. + Result := A.FConverter; +end; + +function TConverter.ProcessData(const Value: S): TState; +begin + // Forward the call to the wrapped interface. + Result := FConverter.ProcessData(Value); +end; + +class function TConverter.CreateCounter: TConverter; +begin + Result := TMycDataCounter.Create; +end; + +class function TConverter.CreateRecordField(const FieldName: String): TConverter; +begin + Result := TMycRecordFieldReader.Create(FieldName); +end; + +class function TConverter.CreateTicker: TConverter, T>; +begin + Result := TMycTicker.Create; +end; + +end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index a02b888..9598165 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -3,10 +3,8 @@ unit Myc.Trade.DataPoint; interface uses - System.TimeSpan, Myc.Signals, - Myc.Core.Notifier, - Myc.Trade.Types; + Myc.Core.Notifier; type // Represents a time-stamped data point in a series. @@ -25,14 +23,39 @@ type function ProcessData(const Value: T): TState; virtual; abstract; end; - TTag = Pointer; + TDataProvider = record + type + TTag = Pointer; + IDataProvider = interface + function Link(const Receiver: IMycProcessor): TTag; + procedure Unlink(Tag: TTag); + end; - IMycDataProvider = interface - function Link(const Receiver: IMycProcessor): TTag; - procedure Unlink(Tag: TTag); + {$REGION 'private'} + strict private + class var + FNull: IDataProvider; + class constructor CreateClass; + private + FDataProvider: IDataProvider; + {$ENDREGION} + public + constructor Create(const ADataProvider: IDataProvider); + + // Managed record operators + class operator Initialize(out Dest: TDataProvider); + class operator Implicit(const A: IDataProvider): TDataProvider; overload; + class operator Implicit(const A: TDataProvider): IDataProvider; overload; + + // Wrapper for IMycDataProvider methods + function Link(const Receiver: IMycProcessor): TTag; inline; + procedure Unlink(Tag: TTag); inline; + + // Provides access to the null object instance. + class property Null: IDataProvider read FNull; end; - TMycDataProvider = class abstract(TContainedObject, IMycDataProvider) + TMycDataProvider = class abstract(TContainedObject, TDataProvider.IDataProvider) private FListeners: TMycNotifyList>; public @@ -42,9 +65,9 @@ type function Broadcast(const Value: T): TState; procedure Notify(const Func: TMycNotifyList>.TNotifyProc); // Link a Processor - function Link(const Processor: IMycProcessor): TTag; + function Link(const Processor: IMycProcessor): TDataProvider.TTag; // Unlink a linked strategy - procedure Unlink(Tag: TTag); + procedure Unlink(Tag: TDataProvider.TTag); end; TMycGenericProcessor = class(TMycProcessor) @@ -68,216 +91,24 @@ type constructor Create(const Controller: IInterface; const AProc: TProc); end; - IMycConverter = interface(IMycProcessor) - function GetSender: IMycDataProvider; - property Sender: IMycDataProvider read GetSender; - end; - - TMycConverter = class abstract(TMycProcessor, IMycConverter) - private - FSender: TMycDataProvider; - function GetSender: IMycDataProvider; - protected - function ProcessData(const Value: S): TState; override; abstract; - - // Broadcasts the given data to all linked processors. - function Broadcast(const Value: T): TState; - + TNullDataProvider = class(TInterfacedObject, TDataProvider.IDataProvider) public - constructor Create; - destructor Destroy; override; - property Sender: IMycDataProvider read GetSender; - 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. - // Index 0 is the newest element, Index (Count - 1) is the oldest. - property Items[Idx: Int64]: TDataPoint read GetItems; default; - // The maximum number of adressable items. Count will never be bigger than the Lookback. - property Lookback: Int64 read GetLookback; - // The total number of items ever added to the series. - property TotalCount: Int64 read GetTotalCount; - end; - - // Interface Helper for IDataSeries. - // Provides a safe, value-type-like wrapper around the interface. - TDataSeries = record - type - TConvertFunc = reference to function(const Val: TDataPoint): S; - private - FDataSeries: IDataSeries; - function GetCount: Int64; - function GetItems(Idx: Int64): TDataPoint; - function GetData(Idx: Int64): T; - function GetTime(Idx: Int64): TDateTime; - function GetTotalCount: Int64; - function GetLookback: Int64; - class function GetNull: IDataSeries; static; - public - constructor Create(ADataSeries: IDataSeries); - - class operator Initialize(out Dest: TDataSeries); - class operator Finalize(var Dest: TDataSeries); - - class operator Implicit(const A: TDataSeries): IDataSeries; - class operator Implicit(const A: IDataSeries): TDataSeries; - - class function CreateDataSeries(Lookback: Int64; const Data: TArray> = nil): TDataSeries; static; - - 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. - // Returns the logical index of the matching item. - // If no exact match, returns the index of the item immediately preceding the timestamp. - // Returns -1 if the timestamp is before the oldest item in the series. - function IndexOf(TimeStamp: TDateTime): Int64; - - function ToArray: TArray>; - function ToDataArray: TArray; - - class property Null: IDataSeries read GetNull; - property Count: Int64 read GetCount; - property TotalCount: Int64 read GetTotalCount; - property Lookback: Int64 read GetLookback; - property Items[Idx: Int64]: TDataPoint read GetItems; default; - property Data[Idx: Int64]: T read GetData; - property Time[Idx: Int64]: TDateTime read GetTime; - end; - - TDataSeriesDoubleHelper = record helper for TDataSeries - 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; + function Link(const Receiver: IMycProcessor): TDataProvider.TTag; + procedure Unlink(Tag: TDataProvider.TTag); end; implementation -uses - System.SysUtils, - System.Math, - System.Generics.Collections, - Myc.Trade.DataArray, - Myc.Trade.Core.DataPoint; +{ TNullDataProvider } -// Optimized helper function using direct TTimeSpan features and integer arithmetic. -function CeilToTimeSpan(const ATime: TDateTime; const ATimeSpan: TTimeSpan): TDateTime; -var - timeSinceMidnight: TTimeSpan; - timeSpanTicks: Int64; - numIntervals, ceiledTicks: Int64; +function TNullDataProvider.Link(const Receiver: IMycProcessor): TDataProvider.TTag; begin - timeSpanTicks := ATimeSpan.Ticks; - Assert(timeSpanTicks > 0, 'TimeSpan must be positive.'); - - // Get the time portion of ATime directly as a TTimeSpan. - timeSinceMidnight := TTimeSpan.Subtract(ATime, Trunc(ATime)); - - // Using integer arithmetic to find the ceiling is robust. - // This is a standard formula for integer ceiling division: (numerator + denominator - 1) / denominator - numIntervals := (timeSinceMidnight.Ticks + timeSpanTicks - 1) div timeSpanTicks; - - ceiledTicks := numIntervals * timeSpanTicks; - - // Construct the final DateTime from the date part and the new, aligned time part. - Result := Trunc(ATime) + TTimeSpan.FromTicks(ceiledTicks); + Result := nil; end; -function TDataSeriesDoubleHelper.ToOhlc(TimeFrame: TTimeSpan): TDataSeries; -var - ohlcPoints: TList>; - currentBar: TOhlcItem; - windowEndTime: TDateTime; - sourceIdx: Int64; - firstPointInBar: Boolean; +procedure TNullDataProvider.Unlink(Tag: TDataProvider.TTag); begin - if Self.Count = 0 then - exit(TDataSeries.Create(TNullDataSeries.Null)); - - ohlcPoints := TList>.Create; - try - if TimeFrame.Ticks <= 0 then - raise EArgumentException.Create('Invalid TimeFrame for OHLC aggregation.'); - - sourceIdx := Self.Count - 1; // Start with the oldest data point - firstPointInBar := True; - windowEndTime := 0; - - // Iterate through all source points chronologically (oldest to newest) - while sourceIdx >= 0 do - begin - var P := Self.Items[sourceIdx]; - - if firstPointInBar then - begin - windowEndTime := CeilToTimeSpan(P.Time, TimeFrame); - currentBar.Create(P.Data, P.Data, P.Data, P.Data, 0); - firstPointInBar := False; - end; - - if P.Time >= windowEndTime then - begin - ohlcPoints.Add(TDataPoint.Create(windowEndTime, currentBar)); - firstPointInBar := True; - Continue; // Re-evaluate the same point for the next bar - end; - - currentBar.High := Max(currentBar.High, P.Data); - currentBar.Low := Min(currentBar.Low, P.Data); - currentBar.Close := P.Data; - currentBar.Volume := currentBar.Volume + 1; - - Dec(sourceIdx); - end; - - if not firstPointInBar then - ohlcPoints.Add(TDataPoint.Create(windowEndTime, currentBar)); - - 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); + // Do nothing in the null implementation. end; { TDataPoint } @@ -288,148 +119,53 @@ begin Data := AData; end; -constructor TDataSeries.Create(ADataSeries: IDataSeries); +{ TDataProvider } + +class constructor TDataProvider.CreateClass; begin - if Assigned(ADataSeries) then - FDataSeries := ADataSeries - else - FDataSeries := Null; + // Create the singleton null object instance. + FNull := TNullDataProvider.Create; end; -function TDataSeries.Add(const Data: TArray>; First, Count: Integer): TDataSeries; +constructor TDataProvider.Create(const ADataProvider: IDataProvider); 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); + FDataProvider := ADataProvider; + // Ensure that the internal interface is never nil. + if not Assigned(FDataProvider) then + FDataProvider := FNull; end; -function TDataSeries.Add(const Data: TArray>): TDataSeries; +class operator TDataProvider.Initialize(out Dest: TDataProvider); begin - Result := Add(Data, 0, Length(Data)); + // Initialize new record instances with the null object. + Dest.FDataProvider := FNull; end; -function TDataSeries.Convert(const Func: TConvertFunc): TDataSeries; -begin - Result := TConvertSeries.Create(FDataSeries, Func); -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)); -end; - -class operator TDataSeries.Finalize(var Dest: TDataSeries); -begin - Dest.FDataSeries := nil; -end; - -class operator TDataSeries.Initialize(out Dest: TDataSeries); -begin - Dest.FDataSeries := Null; -end; - -class operator TDataSeries.Implicit(const A: TDataSeries): IDataSeries; -begin - Result := A.FDataSeries; -end; - -class operator TDataSeries.Implicit(const A: IDataSeries): TDataSeries; +class operator TDataProvider.Implicit(const A: IDataProvider): TDataProvider; begin + // Allow implicit conversion from the interface to the helper. Result.Create(A); end; -function TDataSeries.GetCount: Int64; +class operator TDataProvider.Implicit(const A: TDataProvider): IDataProvider; begin - Result := FDataSeries.GetCount; + // Allow implicit conversion from the helper to the interface. + Result := A.FDataProvider; end; -function TDataSeries.GetItems(Idx: Int64): TDataPoint; +function TDataProvider.Link(const Receiver: IMycProcessor): TTag; begin - Result := FDataSeries[Idx]; + // Forward the call to the wrapped interface. + Result := FDataProvider.Link(Receiver); end; -function TDataSeries.GetData(Idx: Int64): T; +procedure TDataProvider.Unlink(Tag: TTag); begin - Result := FDataSeries[Idx].Data; + // Forward the call to the wrapped interface. + FDataProvider.Unlink(Tag); end; -function TDataSeries.GetLookback: Int64; -begin - Result := FDataSeries.Lookback; -end; - -function TDataSeries.GetTime(Idx: Int64): TDateTime; -begin - Result := FDataSeries[Idx].Time; -end; - -class function TDataSeries.GetNull: IDataSeries; -begin - Result := TNullDataSeries.Null; -end; - -function TDataSeries.GetTotalCount: Int64; -begin - Result := FDataSeries.GetTotalCount; -end; - -function TDataSeries.IndexOf(TimeStamp: TDateTime): Int64; -var - low, high, mid: Int64; - dataPointTime: TDateTime; -begin - Result := -1; - if Count = 0 then - Exit; - - low := 0; - high := Count - 1; - - while (low <= high) do - begin - mid := low + (high - low) div 2; - dataPointTime := FDataSeries[mid].Time; - - if (dataPointTime = TimeStamp) then - begin - Result := mid; - break; - end - else if (dataPointTime < TimeStamp) then - begin - Result := mid; - high := mid - 1; - end - else - begin - low := mid + 1; - end; - end; -end; - -function TDataSeries.ToArray: TArray>; -begin - var n := FDataSeries.Count; - SetLength(Result, n); - dec(n); - for var i := 0 to n do - Result[i] := FDataSeries[n - i]; -end; - -function TDataSeries.ToDataArray: TArray; -begin - var n := FDataSeries.Count; - SetLength(Result, n); - dec(n); - for var i := 0 to n do - Result[i] := FDataSeries[n - i].Data; -end; +{ TMycGenericProcessor } constructor TMycGenericProcessor.Create(const AProc: TProc); begin @@ -442,6 +178,8 @@ begin Result := FProc(Value); end; +{ TMycContainedProcessor } + constructor TMycContainedProcessor.Create(const Controller: IInterface; const AProc: TProc); begin inherited Create(Controller); @@ -453,7 +191,7 @@ begin Result := FProc(Value); end; -{ TMycDataProvider } +{ TMycDataProvider } constructor TMycDataProvider.Create(const Controller: IInterface); begin @@ -504,7 +242,7 @@ begin end; end; -function TMycDataProvider.Link(const Processor: IMycProcessor): TTag; +function TMycDataProvider.Link(const Processor: IMycProcessor): TDataProvider.TTag; begin // Add the Processor to the notification list FListeners.Lock; @@ -515,7 +253,7 @@ begin end; end; -procedure TMycDataProvider.Unlink(Tag: TTag); +procedure TMycDataProvider.Unlink(Tag: TDataProvider.TTag); begin FListeners.Lock; try @@ -525,28 +263,4 @@ begin end; end; -{ TMycConverter } - -constructor TMycConverter.Create; -begin - inherited Create; - FSender := TMycDataProvider.Create(Self); -end; - -destructor TMycConverter.Destroy; -begin - FSender.Free; - inherited Destroy; -end; - -function TMycConverter.Broadcast(const Value: T): TState; -begin - Result := FSender.Broadcast(Value); -end; - -function TMycConverter.GetSender: IMycDataProvider; -begin - Result := FSender; -end; - end. diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas index 71f67ae..bd18d32 100644 --- a/Src/Myc.Trade.Indicators.pas +++ b/Src/Myc.Trade.Indicators.pas @@ -6,7 +6,8 @@ uses System.SysUtils, System.Math, Myc.Trade.Types, - Myc.Trade.DataArray; + Myc.Trade.DataArray, + Myc.Trade.DataConverter; type // Result for the Moving Average Convergence Divergence (MACD) indicator. @@ -36,19 +37,19 @@ type class function CalculateWMA(const Series: TMycDataArray; const Period: Integer): Double; static; public // Simple Moving Average - class function CreateSMA(Period: Integer): TIndicatorFunc; static; + class function CreateSMA(Period: Integer): TConstFunc; static; // Exponential Moving Average - class function CreateEMA(Period: Integer): TIndicatorFunc; static; + class function CreateEMA(Period: Integer): TConstFunc; static; // Hull Moving Average - class function CreateHMA(Period: Integer): TIndicatorFunc; static; + class function CreateHMA(Period: Integer): TConstFunc; static; // Relative Strength Index - class function CreateRSI(Period: Integer): TIndicatorFunc; static; + class function CreateRSI(Period: Integer): TConstFunc; static; // Moving Average Convergence Divergence - class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TIndicatorFunc; static; + class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc; static; // Stochastic Oscillator - class function CreateStochastic(KPeriod, DPeriod: Integer): TIndicatorFunc; static; + class function CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc; static; // Bollinger Bands - class function CreateBollingerBands(Period: Integer; Multiplier: Double): TIndicatorFunc; static; + class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConstFunc; static; end; implementation @@ -112,7 +113,7 @@ begin Result := numerator / denominator; end; -class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TIndicatorFunc; +class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TConstFunc; begin var sourceData := TMycDataArray.CreateEmpty; Result := @@ -135,7 +136,7 @@ begin end; end; -class function TIndicators.CreateEMA(Period: Integer): TIndicatorFunc; +class function TIndicators.CreateEMA(Period: Integer): TConstFunc; begin var lastEma: Double := Double.NaN; var sourceData := TMycDataArray.CreateEmpty; @@ -166,7 +167,7 @@ begin end; end; -class function TIndicators.CreateHMA(Period: Integer): TIndicatorFunc; +class function TIndicators.CreateHMA(Period: Integer): TConstFunc; begin var periodHalf := Period div 2; var periodSqrt := Round(Sqrt(Period)); @@ -208,7 +209,7 @@ begin end; end; -class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TIndicatorFunc; +class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc; begin var emaFast := CreateEMA(FastPeriod); var emaSlow := CreateEMA(SlowPeriod); @@ -240,7 +241,7 @@ begin end; end; -class function TIndicators.CreateRSI(Period: Integer): TIndicatorFunc; +class function TIndicators.CreateRSI(Period: Integer): TConstFunc; begin var avgGain: Double := Double.NaN; var avgLoss: Double := Double.NaN; @@ -299,7 +300,7 @@ begin end; end; -class function TIndicators.CreateSMA(Period: Integer): TIndicatorFunc; +class function TIndicators.CreateSMA(Period: Integer): TConstFunc; begin var sourceData := TMycDataArray.CreateEmpty; Result := @@ -313,7 +314,7 @@ begin end; end; -class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TIndicatorFunc; +class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc; begin var sourceData := TMycDataArray.CreateEmpty; var smaD := CreateSMA(DPeriod); diff --git a/Src/Myc.Trade.Types.pas b/Src/Myc.Trade.Types.pas index a7a4874..305c197 100644 --- a/Src/Myc.Trade.Types.pas +++ b/Src/Myc.Trade.Types.pas @@ -5,8 +5,6 @@ interface type TTimeframe = (S, S5, S15, S30, M, M2, M3, M5, M10, M15, M30, H, H2, H3, H4, H8, H12, D, D2, D3, W, MN, MN3, MN6, Y); - TIndicatorFunc = reference to function(const Value: S): T; - // A data record for an Ask/Bid price pair. TAskBidItem = packed record Ask: Double; @@ -23,6 +21,8 @@ type constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double); end; + TConstFunc = reference to function(const Value: S): T; + implementation { TAskBidItem } diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index d48091f..0893423 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -5,32 +5,32 @@ program MycTests; {$ENDIF} {$STRONGLINKTYPES ON} uses - FastMM5, - DUnitX.MemoryLeakMonitor.FastMM5, - System.SysUtils, - {$IFDEF TESTINSIGHT} - TestInsight.DUnitX, - {$ELSE} - DUnitX.Loggers.Console, - {$ENDIF } - DUnitX.TestFramework, - TestNotifier in 'TestNotifier.pas', - TestNotifier_Threading in 'TestNotifier_Threading.pas' {/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',}, - TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas', - TestTasks in 'TestTasks.pas', - Myc.Futures in '..\Src\Myc.Futures.pas', - TestCoreFutures in 'TestCoreFutures.pas', - Myc.TaskManager in '..\Src\Myc.TaskManager.pas', - TestFutures in 'TestFutures.pas', - Myc.Test.Core.Atomic in '..\Src\Myc.Test.Core.Atomic.pas', - Myc.Test.Signals.Latch in '..\Src\Myc.Test.Signals.Latch.pas', - Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas', - Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas', - Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas', - Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas', - Myc.Test.Trade.DataPoint in '..\Src\Myc.Test.Trade.DataPoint.pas', - Myc.Mutable in '..\Src\Myc.Mutable.pas', - Test.Core.Mutable in 'Test.Core.Mutable.pas'; + FastMM5, + DUnitX.MemoryLeakMonitor.FastMM5, + System.SysUtils, +{$IFDEF TESTINSIGHT} + TestInsight.DUnitX, +{$ELSE} + DUnitX.Loggers.Console, +{$ENDIF } + DUnitX.TestFramework, + TestNotifier in 'TestNotifier.pas', + TestNotifier_Threading in 'TestNotifier_Threading.pas' {/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',}, + TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas', + TestTasks in 'TestTasks.pas', + Myc.Futures in '..\Src\Myc.Futures.pas', + TestCoreFutures in 'TestCoreFutures.pas', + Myc.TaskManager in '..\Src\Myc.TaskManager.pas', + TestFutures in 'TestFutures.pas', + Myc.Test.Core.Atomic in '..\Src\Myc.Test.Core.Atomic.pas', + Myc.Test.Signals.Latch in '..\Src\Myc.Test.Signals.Latch.pas', + Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas', + Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas', + Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas', + Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas', + Myc.Test.Trade.DataPoint in '..\Src\Myc.Test.Trade.DataPoint.pas', + Myc.Mutable in '..\Src\Myc.Mutable.pas', + Test.Core.Mutable in 'Test.Core.Mutable.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT}