From bc75f08477bbb8fd2833da021e6f960b09e380e6 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Tue, 15 Jul 2025 20:29:19 +0200 Subject: [PATCH] Work in Progress --- AuraTrader/AuraTrader.dpr | 1 - AuraTrader/AuraTrader.dproj | 1 - AuraTrader/FirstStrategy.pas | 141 -------------- AuraTrader/MainForm.pas | 308 +++++++++++++++++-------------- Src/Myc.Fmx.Chart.Series.pas | 7 +- Src/Myc.Fmx.Chart.pas | 13 +- Src/Myc.Signals.pas | 7 +- Src/Myc.TaskManager.pas | 11 ++ Src/Myc.Trade.DataArray.pas | 9 +- Src/Myc.Trade.DataPoint.Impl.pas | 208 ++++++++++++++++++++- Src/Myc.Trade.DataPoint.pas | 76 +++++++- Src/Myc.Trade.Indicators.pas | 134 +++++++++++++- Src/Myc.Trade.Types.pas | 1 + 13 files changed, 597 insertions(+), 320 deletions(-) delete mode 100644 AuraTrader/FirstStrategy.pas diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index 1add917..1a532d2 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -9,7 +9,6 @@ uses Myc.Aura.Parameter in '..\Src\Myc.Aura.Parameter.pas', TestModule in 'TestModule.pas', DynamicFMXControl in 'DynamicFMXControl.pas', - FirstStrategy in 'FirstStrategy.pas', 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', diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index bc46de6..de77ac4 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -137,7 +137,6 @@ - diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas deleted file mode 100644 index 249c9a0..0000000 --- a/AuraTrader/FirstStrategy.pas +++ /dev/null @@ -1,141 +0,0 @@ -unit FirstStrategy; - -interface - -uses - System.SysUtils, - System.Generics.Collections, - Myc.Signals, - Myc.Mutable, - Myc.TaskManager, - Myc.Trade.Types, - Myc.Trade.DataPoint, - Myc.Trade.DataArray, - Myc.Trade.DataPoint.Impl; - -type - TTickAggregation = class(TMycConverter, TDataPoint>) - private - FTimeframe: TTimeframe; - FCurrentBar: TDataPoint; - function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; - function GetCurrentBar: TDataPoint; - function GetTimeframe: TTimeframe; - public - constructor Create(const ATimeframe: TTimeframe); - function ProcessData(const Value: TDataPoint): TState; override; - property CurrentBar: TDataPoint read GetCurrentBar; - property Timeframe: TTimeframe read GetTimeframe; - end; - -implementation - -uses - System.DateUtils, - System.Math; - -{ TTickAggregation } - -constructor TTickAggregation.Create(const ATimeframe: TTimeframe); -begin - inherited Create; - FTimeframe := ATimeframe; -end; - -function TTickAggregation.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; -var - baseTime: TDateTime; -begin - // Align the time grid to UTC 0:00 using functions from System.DateUtils - baseTime := RecodeMilliSecond(TimeStamp, 0); - - case Timeframe of - S: Result := baseTime; - S5: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 5)); - S15: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 15)); - S30: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 30)); - - M: Result := RecodeSecond(baseTime, 0); - M2: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 2)); - M3: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 3)); - M5: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 5)); - M10: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 10)); - M15: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 15)); - M30: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 30)); - - H: Result := RecodeMinute(RecodeSecond(baseTime, 0), 0); - H2: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 2)); - H3: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 3)); - H4: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 4)); - H8: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 8)); - H12: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 12)); - - D: Result := StartOfTheDay(TimeStamp); - // D2, D3 are uncommon; this is a simple modulo-based approach relative to TDateTime's epoch. - D2: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 2); - D3: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 3); - - W: Result := TimeStamp.StartOfTheWeek; - - MN: Result := TimeStamp.StartOfTheMonth; - // Quarter alignment - MN3: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 3 * 3 + 1); - // Half-year alignment - MN6: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 6 * 6 + 1); - - Y: Result := TimeStamp.StartOfTheYear; - else - // Fallback for any undefined timeframe - Result := 0; - end; -end; - -function TTickAggregation.GetCurrentBar: TDataPoint; -begin - Result := FCurrentBar; -end; - -function TTickAggregation.GetTimeframe: TTimeframe; -begin - Result := FTimeframe; -end; - -function TTickAggregation.ProcessData(const Value: TDataPoint): TState; -var - barStartTime: TDateTime; - lastBarTime: TDateTime; -begin - // Update bar for the strategy's timeframe - barStartTime := GetBarStartTime(Value.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 - Result := Broadcast(FCurrentBar); - end; - - // Start a new bar, Volume is 1 because this is the first tick. - 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 - 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. - FCurrentBar.Data.Volume := FCurrentBar.Data.Volume + 1; - end; -end; - -end. diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index 0c37bf8..5997f3d 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -11,6 +11,7 @@ uses System.DateUtils, System.Generics.Collections, System.Rtti, + System.Math, FMX.Types, FMX.Controls, FMX.Forms, @@ -32,9 +33,11 @@ uses Myc.Trade.DataPoint, Myc.Signals, Myc.Mutable, + Myc.Trade.DataArray, Myc.Signals.FMX, Myc.TaskManager, Myc.Aura.Module, + Myc.Trade.DataPoint.Impl, FMX.ListBox, FMX.Layouts, FMX.TreeView, @@ -45,7 +48,6 @@ uses System.Actions, FMX.ActnList, DynamicFMXControl, - FirstStrategy, Myc.FMX.Chart; type @@ -103,6 +105,15 @@ type property OnEvent: TNotifyEvent read FOnEvent write FOnEvent; end; + TEquitySum = class(TMycConverter) + private + FEquity: Double; + protected + function ProcessData(const Value: Double): TState; override; + public + constructor Create(AEquity: Double); + end; + var Form1: TForm1; @@ -343,7 +354,7 @@ begin end ); - var OhlcPoint := lastPrice.Chain>(TTickAggregation.Create(timeframe)); + var OhlcPoint := lastPrice.Chain>(TConverter.CreateAggregation(timeframe)); var Timestamps := OhlcPoint.Field('Time'); var Ohlc := OhlcPoint.Field('Data'); @@ -410,6 +421,13 @@ begin end; procedure TForm1.Strat2ButtonClick(Sender: TObject); +type + TSignal = record + Sig: Double; + SL: Double; + Entry: Double; + pnl: Double; + end; begin var timeframe := TTimeframe.M15; @@ -417,26 +435,123 @@ begin 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 + TConverter.CreateDataPointConverter( + function(const Tick: TAskBidItem): Double begin Result := 0.5 * (Tick.Ask + Tick.Bid); end + ) ); - var OhlcPoint := TTickAggregation.Create(timeframe); - lastPrice.Sender.Link(OhlcPoint); + var OhlcPoint := lastPrice.Chain>(TConverter.CreateAggregation(timeframe)); - var Closes := - TConverter, Double> - .CreateGeneric(function(const Ohlc: TDataPoint): Double begin Result := Ohlc.Data.Close; end); + var Ohlc := TConverter.CreateSequence(2, OhlcPoint.Field('Data').Sender); - var Hull := TConverter.CreateGeneric(TIndicators.CreateHMA(150)); + var Closes := Ohlc[0].Field('Close'); - var Timestamps := - TConverter, TDateTime> - .CreateGeneric(function(const Ohlc: TDataPoint): TDateTime begin Result := Ohlc.Time; end); - OhlcPoint.Sender.Link(TimeStamps); + var Hull := Closes.Chain(TIndicators.CreateHMA(250)); + var Sma := Closes.Chain(TIndicators.CreateSMA(200)); + + var HullSeries := TConverter.CreateEndpoint(Hull.Sender, 5); + var SmaSeries := TConverter.CreateEndpoint(Sma.Sender, 5); + + var Lowest: Double := Double.MaxValue; + var Highest: Double := Double.MinValue; + + var curr: TSignal; + curr.SL := Double.NaN; + curr.Entry := Double.NaN; + + var ATR := Ohlc[0].Chain(TIndicators.CreateATR(50)); + var ATRSeries := TConverter.CreateEndpoint(ATR.Sender, 5); + + // next stage + + var Signal := + Ohlc[1] + .Chain( + function(const Ohlc: TOhlcItem): TSignal + begin + var pnl: Double := 0; + + if Ohlc.Low < Lowest then + Lowest := Ohlc.Low; + if Ohlc.High > Highest then + Highest := Ohlc.High; + + Result := curr; + Result.Sig := 0; + pnl := NaN; + + if (HullSeries.Value[0] < SmaSeries.Value[0]) and (HullSeries.Value[1] >= SmaSeries.Value[1]) then + begin + if curr.Sig > 0 then + pnl := Ohlc.Close - curr.Entry; + + curr.Sig := -1; + curr.SL := Highest; + curr.Entry := Ohlc.Close; + Result := curr; + end + else if (HullSeries.Value[0] > SmaSeries.Value[0]) and (HullSeries.Value[1] <= SmaSeries.Value[1]) then + begin + if curr.Sig < 0 then + pnl := curr.Entry - Ohlc.Close; + + curr.Sig := 1; + curr.SL := Lowest; + curr.Entry := Ohlc.Close; + Result := curr; + end; + + var atr := 15 * ATRSeries.Value[0]; + if curr.Sig > 0 then + begin + if Ohlc.Close > curr.SL then + begin + if curr.SL < Ohlc.Close - atr then + curr.SL := Ohlc.Close - atr; + Result.SL := curr.SL; + end; + + if Ohlc.Low <= curr.SL then + begin + pnl := curr.SL - curr.Entry; + curr.Sig := 0; + Result.Sig := 0; + curr.SL := NaN; + end; + end + else if curr.Sig < 0 then + begin + if Ohlc.Close < curr.SL then + begin + if curr.SL > Ohlc.Close + atr then + curr.SL := Ohlc.Close + atr; + Result.SL := curr.SL; + end; + + if Ohlc.High >= curr.SL then + begin + pnl := curr.Entry - curr.SL; + curr.Sig := 0; + Result.Sig := 0; + curr.SL := NaN; + end; + end; + + if Result.Sig <> 0 then + begin + Lowest := Double.MaxValue; + Highest := Double.MinValue; + Result.SL := Double.NaN; + Result.Entry := Double.NaN; + end; + + Result.pnl := pnl; + end); + + var pnl := Signal.Field('pnl'); + + var equity: TConverter := TEquitySum.Create(10000); + pnl.Sender.Link(equity); var Layout := CurrLayout; if Layout = nil then @@ -451,142 +566,49 @@ begin chart.Height := Layout.ChildrenRect.Width * 9 / 16; chart.Lookback.Value := 50000; - ///// + chart.SetXAxisSeries(M15, OhlcPoint.Field('Time').Sender); - { + var panel := chart.AddPanel; + panel.AddOhlcSeries(Ohlc[0].Sender); + panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Cornflowerblue, 2); + panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Brown, 1.5); + panel.AddDoubleSeries(Signal.Field('SL').Sender, TAlphaColors.Red, 2); + panel.AddDoubleSeries(Signal.Field('Entry').Sender, TAlphaColors.Green, 1); - chart.SetXAxisSeries(timeframe, Timestamps.Sender); + // panel := chart.AddPanel; + // panel.AddDoubleSeries( equity.Sender, TAlphaColors.Blue, 3 ); - var Panel := chart.AddPanel; + var pnlChart := TMycChart.Create(Self); + AlignControl(pnlChart); + pnlChart.Height := Layout.ChildrenRect.Width * 9 / 24; + pnlChart.Lookback.Value := 50000; + pnlChart.SetXAxisCounter(equity.Sender); - OhlcPoint.Sender.Link(Ohlc); - Panel.AddOhlcSeries(Ohlc.Sender); - - Ohlc.Sender.Link(Closes); - - var Hull: IMycConverter := TGenericIndicator.Create(TIndicators.CreateHMA(150)); - Closes.Sender.Link(Hull); - Panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue); - - // Add SMA (Simple Moving Average) - var Sma: IMycConverter := TGenericIndicator.Create(TIndicators.CreateSMA(50)); - Closes.Sender.Link(Sma); - Panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow); - - // Add EMA (Exponential Moving Average) - var Ema: IMycConverter := TGenericIndicator.Create(TIndicators.CreateEMA(21)); - Closes.Sender.Link(Ema); - Panel.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua); - - // Add Bollinger Bands (20, 2.0) - var Boli: IMycConverter := - TGenericIndicator.Create(TIndicators.CreateBollingerBands(20, 2.0)); - Closes.Sender.Link(Boli); - - var BoliUpper: IMycConverter := - TMycGenericConverter - .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end); - Boli.Sender.Link(BoliUpper); - Panel.AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray); - - var BoliMiddle: IMycConverter := - TMycGenericConverter - .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.MiddleBand; end); - Boli.Sender.Link(BoliMiddle); - Panel.AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0); - - var BoliLower: IMycConverter := - TMycGenericConverter - .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.LowerBand; end); - Boli.Sender.Link(BoliLower); - Panel.AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray); - - Panel := chart.AddPanel; - - // Add RSI (Relative Strength Index) - var Rsi: IMycConverter := TGenericIndicator.Create(TIndicators.CreateRSI(14)); - Closes.Sender.Link(Rsi); - Panel.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia); - - // Add MACD (12, 26, 9) - var Macd: IMycConverter := TGenericIndicator.Create(TIndicators.CreateMACD(12, 26, 9)); - Closes.Sender.Link(Macd); - - Panel := chart.AddPanel; - - var MacdLine: IMycConverter := - TMycGenericConverter.Create(function(const Item: TMacdResult): Double begin Result := Item.MacdLine; end); - Macd.Sender.Link(MacdLine); - Panel.AddDoubleSeries(MacdLine.Sender, TAlphaColors.Orange); - - var MacdSignal: IMycConverter := - TMycGenericConverter.Create(function(const Item: TMacdResult): Double begin Result := Item.SignalLine; end); - Macd.Sender.Link(MacdSignal); - Panel.AddDoubleSeries(MacdSignal.Sender, TAlphaColors.Dodgerblue); - - var MacdHist: IMycConverter := - TMycGenericConverter.Create(function(const Item: TMacdResult): Double begin Result := Item.Histogram; end); - Macd.Sender.Link(MacdHist); - Panel.AddDoubleSeries(MacdHist.Sender, TAlphaColors.Lightgreen, 1.0); - - Panel := chart.AddPanel; - - // Add Stochastic Oscillator (14, 3) - This needs OHLC data, not just Close prices. - var Stoch: IMycConverter := - TGenericIndicator.Create(TIndicators.CreateStochastic(14, 3)); - Ohlc.Sender.Link(Stoch); - - var StochK: IMycConverter := - TMycGenericConverter.Create(function(const Item: TStochasticResult): Double begin Result := Item.K; end); - Stoch.Sender.Link(StochK); - Panel.AddDoubleSeries(StochK.Sender, TAlphaColors.Green); - - var StochD: IMycConverter := - TMycGenericConverter.Create(function(const Item: TStochasticResult): Double begin Result := Item.D; end); - Stoch.Sender.Link(StochD); - Panel.AddDoubleSeries(StochD.Sender, TAlphaColors.Red); - - ///// - - var tickChart := TMycChart.Create(Self); - tickChart.Height := Layout.ChildrenRect.Width * 9 / 16; - AlignControl(tickChart); - tickChart.Lookback.Value := 1000000; - - var TickTime: IMycConverter, TDateTime> := - TMycGenericConverter, TDateTime> - .Create(function(const Tick: TDataPoint): TDateTime begin Result := Tick.Time; end); - - var TickAsk: IMycConverter, Double> := - TMycGenericConverter, Double> - .Create(function(const Tick: TDataPoint): Double begin Result := Tick.Data.Ask; end); - - var TickBid: IMycConverter, Double> := - TMycGenericConverter, Double> - .Create(function(const Tick: TDataPoint): Double begin Result := Tick.Data.Bid; end); - - var TickSpread: IMycConverter, Double> := - TMycGenericConverter, Double> - .Create(function(const Tick: TDataPoint): Double begin Result := Tick.Data.Bid-Tick.Data.Ask; end); - - ticker.Sender.Link( TickTime ); - ticker.Sender.Link( TickAsk ); - ticker.Sender.Link( TickBid ); - ticker.Sender.Link( TickSpread ); - - 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); + panel := pnlChart.AddPanel; + panel.AddDoubleSeries(equity.Sender, TAlphaColors.Blue, 3); ///// var done := ExecuteStrategy(Symbol, ticker); FProcessDone := TState.All([FProcessDone, done]); - } +end; + +{ TEquitySum } + +constructor TEquitySum.Create(AEquity: Double); +begin + inherited Create; + FEquity := AEquity; +end; + +function TEquitySum.ProcessData(const Value: Double): TState; +begin + if not IsNan(Value) then + begin + FEquity := FEquity + Value; + Result := Broadcast(FEquity); + end; end; end. diff --git a/Src/Myc.Fmx.Chart.Series.pas b/Src/Myc.Fmx.Chart.Series.pas index aaa9dc1..cd428a3 100644 --- a/Src/Myc.Fmx.Chart.Series.pas +++ b/Src/Myc.Fmx.Chart.Series.pas @@ -116,7 +116,7 @@ type protected function GetSeries: TMycChart.TSeries; override; procedure Update; override; - function GetCaption(Idx: Int64): String; override; abstract; + function GetCaption(Idx: Int64): String; override; public constructor Create(AOwner: TMycChart; const ADataProvider: TDataProvider); destructor Destroy; override; @@ -374,6 +374,11 @@ begin inherited; end; +function TChartXAxisLayer.GetCaption(Idx: Int64): String; +begin + Result := IntToStr(Idx); +end; + function TChartXAxisLayer.GetSeries: TMycChart.TSeries; begin Result := FSeries; diff --git a/Src/Myc.Fmx.Chart.pas b/Src/Myc.Fmx.Chart.pas index 10efe72..eb69e03 100644 --- a/Src/Myc.Fmx.Chart.pas +++ b/Src/Myc.Fmx.Chart.pas @@ -166,7 +166,8 @@ type function AddPanel: TPanel; // Sets the master series that defines the time scale (X-axis). - function SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider): TMycChart.TXAxisLayer; + function SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider): TMycChart.TXAxisLayer; overload; + function SetXAxisCounter(const DataProvider: TDataProvider): TMycChart.TXAxisLayer; overload; property Lookback: TWriteable read FLookback write FLookback; property NeedRepaint: TFlag read FNeedRepaint; @@ -639,6 +640,16 @@ begin Repaint; end; +function TMycChart.SetXAxisCounter(const DataProvider: TDataProvider): TMycChart.TXAxisLayer; +begin + FXAxisSeries.Free; + + var counter := TConverter.CreateCounter; + DataProvider.Link(counter); + FXAxisSeries := TChartXAxisLayer.Create(Self, counter.Sender); + Result := FXAxisSeries; +end; + function TMycChart.SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider): TMycChart.TXAxisLayer; begin FXAxisSeries.Free; diff --git a/Src/Myc.Signals.pas b/Src/Myc.Signals.pas index d2b477b..5a913d4 100644 --- a/Src/Myc.Signals.pas +++ b/Src/Myc.Signals.pas @@ -77,7 +77,7 @@ type class operator Initialize(out Dest: TState); class operator Implicit(const A: IState): TState; overload; class operator Implicit(const A: TState): IState; overload; - + class operator Add(const A, B: TState): TState; class function All(const States: TArray): TState; static; class function Any(const States: TArray; Count: Integer = 1): TState; static; @@ -271,6 +271,11 @@ begin Result := FState.IsSet; end; +class operator TState.Add(const A, B: TState): TState; +begin + Result := All([A, B]); +end; + class operator TState.Implicit(const A: TState): IState; begin Result := A.FState; diff --git a/Src/Myc.TaskManager.pas b/Src/Myc.TaskManager.pas index c3e8ede..b8dc021 100644 --- a/Src/Myc.TaskManager.pas +++ b/Src/Myc.TaskManager.pas @@ -31,6 +31,8 @@ type // Returns a State to await thread completion. function RunTask(const Gate: TState; const Proc: TFunc): TState; + class function RunSequence(const Gate: TState; First, Count: Integer; const Proc: TFunc): TState; static; + // Waits for the operation associated with State to complete. // Must not be called from a task of this factory. // After waiting, or if the state is already set, any first stored exception @@ -146,6 +148,15 @@ begin ); end; +class function TTaskManager.RunSequence(const Gate: TState; First, Count: Integer; const Proc: TFunc): TState; +begin + if First >= Count then + exit; + + var cProc: TFunc := Proc; + Result := TaskManager.RunTask(Gate, function: TState begin Result := RunSequence(Proc(First), 1 + First, Count, cProc); end); +end; + function TTaskManager.RunTask(const Gate: TState; const Proc: TFunc): TState; begin var cProc: TFunc := Proc; diff --git a/Src/Myc.Trade.DataArray.pas b/Src/Myc.Trade.DataArray.pas index 1c628f5..e4db488 100644 --- a/Src/Myc.Trade.DataArray.pas +++ b/Src/Myc.Trade.DataArray.pas @@ -24,7 +24,7 @@ type class operator Initialize(out Dest: TSeries); // Add a singe item - function Add(const Data: T; Lookback: Int64): TSeries; overload; + function Add(const Data: T; Lookback: Int64 = -1): TSeries; overload; // Add a ranmge of items function Add(const Data: array of T; First, Count, Lookback: Int64): TSeries; overload; // Helper to create a data array from a raw TArray. @@ -36,6 +36,9 @@ type implementation +uses + System.Math; + { TSeries } constructor TSeries.Create(const AChunks: TArray; ACount, ATotalCount: Int64); @@ -62,14 +65,14 @@ var begin if Count < 0 then Count := Length(Data) - First; - if (Lookback <= 0) or (Count = 0) then + if 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 + if (Lookback >= 0) and (newCount > Lookback) then newCount := Lookback; itemsToSkip := sumCount - newCount; diff --git a/Src/Myc.Trade.DataPoint.Impl.pas b/Src/Myc.Trade.DataPoint.Impl.pas index f795677..c567d84 100644 --- a/Src/Myc.Trade.DataPoint.Impl.pas +++ b/Src/Myc.Trade.DataPoint.Impl.pas @@ -19,7 +19,7 @@ type end; // Concrete data provider that manages a list of processors (listeners). - TMycDataProvider = class abstract(TContainedObject, TDataProvider.IDataProvider) + TMycContainedDataProvider = class abstract(TContainedObject, TDataProvider.IDataProvider) private FListeners: TMycNotifyList>; public @@ -33,6 +33,19 @@ type procedure Unlink(Tag: TDataProvider.TTag); end; + TMycSequence = class(TMycProcessor, IMycDataSequence) + private + FDataProviders: TArray>; + function GetCount: Integer; + function GetDataProvider(Idx: Integer): TDataProvider; + protected + function ProcessData(const Value: T): TState; override; + function ProcessDataProvider(Idx: Integer; const Value: T): TState; + public + constructor Create(ACount: Integer); + destructor Destroy; override; + end; + // Null object implementation for IDataProvider. TNullDataProvider = class(TInterfacedObject, TDataProvider.IDataProvider) public @@ -43,7 +56,7 @@ type // Abstract base class for components that process data of type S and provide data of type T. TMycConverter = class abstract(TMycProcessor, TConverter.IConverter) private - FSender: TMycDataProvider; + FSender: TMycContainedDataProvider; function GetSender: TDataProvider.IDataProvider; protected function ProcessData(const Value: S): TState; override; abstract; @@ -73,6 +86,12 @@ type constructor Create(const AFunc: TConstFunc); end; + // A converter specialized for calculating indicators. + TMycIdentityConverter = class(TMycConverter) + protected + function ProcessData(const Value: T): TState; override; final; + end; + // A converter specialized for calculating indicators. TMycIndicator = class(TMycConverter) protected @@ -145,26 +164,43 @@ type destructor Destroy; override; end; + TTickAggregation = class(TMycConverter, TDataPoint>) + private + FTimeframe: TTimeframe; + FCurrentBar: TDataPoint; + function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; + function GetCurrentBar: TDataPoint; + function GetTimeframe: TTimeframe; + public + constructor Create(const ATimeframe: TTimeframe); + function ProcessData(const Value: TDataPoint): TState; override; + property CurrentBar: TDataPoint read GetCurrentBar; + property Timeframe: TTimeframe read GetTimeframe; + end; + implementation uses System.TypInfo, - System.RTTI; + System.RTTI, + System.DateUtils, + System.Math, + Myc.TaskManager; -{ TMycDataProvider } +{ TMycContainedDataProvider } -constructor TMycDataProvider.Create(const Controller: IInterface); +constructor TMycContainedDataProvider.Create(const Controller: IInterface); begin inherited Create(Controller); end; -destructor TMycDataProvider.Destroy; +destructor TMycContainedDataProvider.Destroy; begin FListeners.Finalize; inherited Destroy; end; -function TMycDataProvider.Broadcast(const Value: T): TState; +function TMycContainedDataProvider.Broadcast(const Value: T): TState; begin FListeners.Lock; try @@ -192,7 +228,7 @@ begin end; end; -function TMycDataProvider.Link(const Processor: IMycProcessor): TDataProvider.TTag; +function TMycContainedDataProvider.Link(const Processor: IMycProcessor): TDataProvider.TTag; begin // Add the Processor to the notification list FListeners.Lock; @@ -203,7 +239,7 @@ begin end; end; -procedure TMycDataProvider.Unlink(Tag: TDataProvider.TTag); +procedure TMycContainedDataProvider.Unlink(Tag: TDataProvider.TTag); begin FListeners.Lock; try @@ -230,7 +266,7 @@ end; constructor TMycConverter.Create; begin inherited Create; - FSender := TMycDataProvider.Create(Self); + FSender := TMycContainedDataProvider.Create(Self); end; destructor TMycConverter.Destroy; @@ -411,4 +447,156 @@ begin FChanged.Notify; end; +{ TTickAggregation } + +constructor TTickAggregation.Create(const ATimeframe: TTimeframe); +begin + inherited Create; + FTimeframe := ATimeframe; +end; + +function TTickAggregation.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; +var + baseTime: TDateTime; +begin + // Align the time grid to UTC 0:00 using functions from System.DateUtils + baseTime := RecodeMilliSecond(TimeStamp, 0); + + case Timeframe of + S: Result := baseTime; + S5: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 5)); + S15: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 15)); + S30: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 30)); + + M: Result := RecodeSecond(baseTime, 0); + M2: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 2)); + M3: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 3)); + M5: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 5)); + M10: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 10)); + M15: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 15)); + M30: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 30)); + + H: Result := RecodeMinute(RecodeSecond(baseTime, 0), 0); + H2: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 2)); + H3: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 3)); + H4: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 4)); + H8: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 8)); + H12: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 12)); + + D: Result := StartOfTheDay(TimeStamp); + // D2, D3 are uncommon; this is a simple modulo-based approach relative to TDateTime's epoch. + D2: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 2); + D3: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 3); + + W: Result := TimeStamp.StartOfTheWeek; + + MN: Result := TimeStamp.StartOfTheMonth; + // Quarter alignment + MN3: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 3 * 3 + 1); + // Half-year alignment + MN6: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 6 * 6 + 1); + + Y: Result := TimeStamp.StartOfTheYear; + else + // Fallback for any undefined timeframe + Result := 0; + end; +end; + +function TTickAggregation.GetCurrentBar: TDataPoint; +begin + Result := FCurrentBar; +end; + +function TTickAggregation.GetTimeframe: TTimeframe; +begin + Result := FTimeframe; +end; + +function TTickAggregation.ProcessData(const Value: TDataPoint): TState; +var + barStartTime: TDateTime; + lastBarTime: TDateTime; +begin + // Update bar for the strategy's timeframe + barStartTime := GetBarStartTime(Value.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 + Result := Broadcast(FCurrentBar); + end; + + // Start a new bar, Volume is 1 because this is the first tick. + 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 + 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. + FCurrentBar.Data.Volume := FCurrentBar.Data.Volume + 1; + end; +end; + +{ TMycSequence } + +constructor TMycSequence.Create(ACount: Integer); +begin + inherited Create; + + SetLength(FDataProviders, ACount); + for var i := 0 to High(FDataProviders) do + FDataProviders[i] := TMycContainedDataProvider.Create(Self); +end; + +destructor TMycSequence.Destroy; +begin + for var i := High(FDataProviders) downto 0 do + FDataProviders[i].Free; + inherited; +end; + +function TMycSequence.GetCount: Integer; +begin + Result := Length(FDataProviders); +end; + +function TMycSequence.GetDataProvider(Idx: Integer): TDataProvider; +begin + Result := FDataProviders[Idx]; +end; + +function TMycSequence.ProcessData(const Value: T): TState; +begin + Result := ProcessDataProvider(0, Value); +end; + +function TMycSequence.ProcessDataProvider(Idx: Integer; const Value: T): TState; +begin + if Idx >= Length(FDataProviders) then + exit; + + Result := + TaskManager + .RunTask(FDataProviders[idx].Broadcast(Value), function: TState begin Result := ProcessDataProvider(1 + idx, Value); end); +end; + +function TMycIdentityConverter.ProcessData(const Value: T): TState; +begin + Result := Broadcast(Value); +end; + end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 38e3697..8725cba 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -48,6 +48,13 @@ type class property Null: IDataProvider read FNull; end; + IMycDataSequence = interface(IMycProcessor) + function GetCount: Integer; + function GetDataProvider(Idx: Integer): TDataProvider; + property Count: Integer read GetCount; + property DataProvider[Idx: Integer]: TDataProvider read GetDataProvider; default; + end; + // Interface helper for IConverter providing the null object pattern. TConverter = record public @@ -59,7 +66,6 @@ type property Sender: TDataProvider.IDataProvider read GetSender; end; - {$region 'private'} strict private class var FNull: IConverter; @@ -67,7 +73,6 @@ type private FConverter: IConverter; function GetSender: TDataProvider; inline; - {$endregion} public constructor Create(const AConverter: IConverter); @@ -78,15 +83,15 @@ type 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; // Extracts the field of a record by it's name (using RTTI). function Field(const FieldName: String): TConverter; overload; inline; + function Sequence(Count: Integer): IMycDataSequence; overload; + function Sequence(const Items: TArray): IMycDataSequence; overload; + // Provides access to the null object instance. class property Null: IConverter read FNull; // Wrapper for IConverter.Sender @@ -95,10 +100,18 @@ type // Factory for creating specific converter instances. TConverter = record + class function CreateEndpoint(const DataProvider: TDataProvider; Lookback: Int64): TMutable>; static; + class function CreateCounter: TConverter; static; class function CreateTicker: TConverter, T>; static; class function CreateRecordField(const FieldName: String): TConverter; static; - class function CreateEndpoint(const DataProvider: TDataProvider; Lookback: Int64): TMutable>; static; + class function CreateIdentity: TConverter; static; + + class function CreateAggregation(Timeframe: TTimeframe): TConverter, TDataPoint>; static; + + class function CreateDataPointConverter(const Func: TConstFunc): TConverter, TDataPoint>; static; + + class function CreateSequence(Count: Integer; const Parent: TDataProvider): TArray>; overload; static; end; implementation @@ -186,6 +199,22 @@ begin Result := FConverter.Sender; end; +function TConverter.Sequence(Count: Integer): IMycDataSequence; +begin + Result := TMycSequence.Create(Count); + FConverter.Sender.Link(Result); +end; + +function TConverter.Sequence(const Items: TArray): IMycDataSequence; +begin + var seq: IMycDataSequence := TMycSequence.Create(Length(Items)); + + for var i := 0 to High(Items) do + seq[i].Link(Items[i]); + + Result := seq; +end; + class operator TConverter.Initialize(out Dest: TConverter); begin Dest.FConverter := FNull; @@ -201,9 +230,9 @@ begin Result := A.FConverter; end; -function TConverter.ProcessData(const Value: S): TState; +class function TConverter.CreateAggregation(Timeframe: TTimeframe): TConverter, TDataPoint>; begin - Result := FConverter.ProcessData(Value); + Result := TTickAggregation.Create(Timeframe); end; { TConverter } @@ -213,11 +242,29 @@ begin Result := TMycDataCounter.Create; end; +class function TConverter.CreateDataPointConverter(const Func: TConstFunc): TConverter, TDataPoint>; +begin + var cFunc: TConstFunc := Func; + Result := + TMycGenericConverter, TDataPoint>.Create( + function(const Value: TDataPoint): TDataPoint + begin + Result.Time := Value.Time; + Result.Data := cFunc(Value.Data); + end + ); +end; + class function TConverter.CreateEndpoint(const DataProvider: TDataProvider; Lookback: Int64): TMutable>; begin Result := TMycDataEndpoint.Create(DataProvider, Lookback); end; +class function TConverter.CreateIdentity: TConverter; +begin + Result := TMycIdentityConverter.Create; +end; + class function TConverter.CreateRecordField(const FieldName: String): TConverter; begin Result := TMycRecordFieldReader.Create(FieldName); @@ -228,4 +275,17 @@ begin Result := TMycTicker.Create; end; +class function TConverter.CreateSequence(Count: Integer; const Parent: TDataProvider): TArray>; +begin + var seq: IMycDataSequence := TMycSequence.Create(Count); + + SetLength(Result, Count); + for var i := 0 to High(Result) do + begin + Result[i] := TMycIdentityConverter.Create; + seq[i].Link(Result[i]); + end; + Parent.Link(seq); +end; + end. diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas index 0802274..0e20f68 100644 --- a/Src/Myc.Trade.Indicators.pas +++ b/Src/Myc.Trade.Indicators.pas @@ -29,6 +29,13 @@ type LowerBand: Double; end; + // Result for the Keltner Channels indicator. + TKeltnerChannelsResult = record + UpperBand: Double; + MiddleBand: Double; + LowerBand: Double; + end; + TIndicators = record private class function CalculateSMA(const Series: TSeries; const Period: Integer): Double; static; @@ -44,11 +51,33 @@ type // Relative Strength Index class function CreateRSI(Period: Integer): TConstFunc; static; // Moving Average Convergence Divergence - class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc; static; + class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc; overload; static; + class function CreateMACD( + const EmaFast, + EmaSlow, + EmaSignal: TConstFunc + ): TConstFunc; overload; static; // Stochastic Oscillator - class function CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc; static; + class function CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc; overload; static; + class function CreateStochastic( + KPeriod: Integer; + const SmaD: TConstFunc + ): TConstFunc; overload; static; // Bollinger Bands class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConstFunc; static; + // Average True Range + class function CreateATR(Period: Integer): TConstFunc; overload; static; + class function CreateATR(const MovAvgTR: TConstFunc): TConstFunc; overload; static; + // Keltner Channels + class function CreateKeltnerChannels( + Period: Integer; + Multiplier: Double + ): TConstFunc; overload; static; + class function CreateKeltnerChannels( + const MovAvgMiddle: TConstFunc; + const AtrFunc: TConstFunc; + Multiplier: Double + ): TConstFunc; overload; static; end; implementation @@ -208,19 +237,22 @@ begin end; end; +// Standard MACD using EMAs. class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc; begin - var emaFast := CreateEMA(FastPeriod); - var emaSlow := CreateEMA(SlowPeriod); - var emaSignal := CreateEMA(SignalPeriod); + Result := CreateMACD(CreateEMA(FastPeriod), CreateEMA(SlowPeriod), CreateEMA(SignalPeriod)); +end; +// Creates a MACD indicator from three provided moving average functions. +class function TIndicators.CreateMACD(const EmaFast, EmaSlow, EmaSignal: TConstFunc): TConstFunc; +begin Result := function(const Value: Double): TMacdResult var fastVal, slowVal: Double; begin - fastVal := emaFast(Value); - slowVal := emaSlow(Value); + fastVal := EmaFast(Value); + slowVal := EmaSlow(Value); if IsNan(slowVal) then // slowVal will be the last one to become non-NaN begin @@ -231,7 +263,7 @@ begin else begin Result.MacdLine := fastVal - slowVal; - Result.SignalLine := emaSignal(Result.MacdLine); + Result.SignalLine := EmaSignal(Result.MacdLine); if not IsNan(Result.SignalLine) then Result.Histogram := Result.MacdLine - Result.SignalLine else @@ -313,10 +345,19 @@ begin end; end; +// Standard Stochastic Oscillator using an SMA for the %D line. class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc; +begin + Result := CreateStochastic(KPeriod, CreateSMA(DPeriod)); +end; + +// Creates a Stochastic Oscillator using an injectable moving average for the %D line. +class function TIndicators.CreateStochastic( + KPeriod: Integer; + const SmaD: TConstFunc +): TConstFunc; begin var sourceData: TSeries; - var smaD := CreateSMA(DPeriod); Result := function(const Value: TOhlcItem): TStochasticResult @@ -347,7 +388,80 @@ begin else Result.K := 100; // Or 50, depends on convention - Result.D := smaD(Result.K); + Result.D := SmaD(Result.K); + end; + end; +end; + +// Standard ATR using an EMA for smoothing. +class function TIndicators.CreateATR(Period: Integer): TConstFunc; +begin + Result := CreateATR(CreateEMA(Period)); +end; + +// Calculates the Average True Range (ATR) using an injectable moving average. +class function TIndicators.CreateATR(const MovAvgTR: TConstFunc): TConstFunc; +begin + var sourceData: TSeries; + + Result := + function(const Value: TOhlcItem): Double + var + tr: Double; + begin + // We only need the previous bar to calculate true range. + sourceData := sourceData.Add(Value, 2); + + if (sourceData.Count < 2) then + begin + // Feed a dummy value to keep the moving average count in sync. It will correctly return NaN. + Result := MovAvgTR(0); + Exit; + end; + + // Calculate current True Range. + tr := Max(Value.High - Value.Low, Max(Abs(Value.High - sourceData[1].Close), Abs(Value.Low - sourceData[1].Close))); + + // Feed the calculated TR into the provided moving average function. + Result := MovAvgTR(tr); + end; +end; + +// Standard Keltner Channels using an EMA for the middle line and an EMA-based ATR. +class function TIndicators.CreateKeltnerChannels(Period: Integer; Multiplier: Double): TConstFunc; +begin + Result := CreateKeltnerChannels(CreateEMA(Period), CreateATR(Period), Multiplier); +end; + +// Calculates Keltner Channels using an injectable ATR and middle band moving average. +class function TIndicators.CreateKeltnerChannels( + const MovAvgMiddle: TConstFunc; + const AtrFunc: TConstFunc; + Multiplier: Double +): TConstFunc; +begin + Result := + function(const Value: TOhlcItem): TKeltnerChannelsResult + var + atrValue, middleValue, typicalPrice: Double; + begin + // Calculate Typical Price for the middle band. + typicalPrice := (Value.High + Value.Low + Value.Close) / 3.0; + + // Get values from the provided indicator functions. + middleValue := MovAvgMiddle(typicalPrice); + atrValue := AtrFunc(Value); + + // Set default NaN values for the warm-up period. + Result.MiddleBand := middleValue; + Result.UpperBand := Double.NaN; + Result.LowerBand := Double.NaN; + + // Once both middle band and ATR have valid (non-NaN) values, calculate the channels. + if not IsNan(middleValue) and not IsNan(atrValue) then + begin + Result.UpperBand := middleValue + (atrValue * Multiplier); + Result.LowerBand := middleValue - (atrValue * Multiplier); end; end; end; diff --git a/Src/Myc.Trade.Types.pas b/Src/Myc.Trade.Types.pas index fa7c27c..551f2d4 100644 --- a/Src/Myc.Trade.Types.pas +++ b/Src/Myc.Trade.Types.pas @@ -29,6 +29,7 @@ type end; TConstFunc = reference to function(const Value: S): T; + TConstFuncPredicate = reference to function(const Value: S; out Res: T): Boolean; implementation