Work in Progress

This commit is contained in:
Michael Schimmel
2025-07-15 20:29:19 +02:00
parent 8ebcd81561
commit bc75f08477
13 changed files with 597 additions and 320 deletions
-1
View File
@@ -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',
-1
View File
@@ -137,7 +137,6 @@
<DCCReference Include="..\Src\Myc.Aura.Parameter.pas"/>
<DCCReference Include="TestModule.pas"/>
<DCCReference Include="DynamicFMXControl.pas"/>
<DCCReference Include="FirstStrategy.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataArray.pas"/>
<DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Indicators.pas"/>
-141
View File
@@ -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<Double>, TDataPoint<TOhlcItem>>)
private
FTimeframe: TTimeframe;
FCurrentBar: TDataPoint<TOhlcItem>;
function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetTimeframe: TTimeframe;
public
constructor Create(const ATimeframe: TTimeframe);
function ProcessData(const Value: TDataPoint<Double>): TState; override;
property CurrentBar: TDataPoint<TOhlcItem> 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<TOhlcItem>;
begin
Result := FCurrentBar;
end;
function TTickAggregation.GetTimeframe: TTimeframe;
begin
Result := FTimeframe;
end;
function TTickAggregation.ProcessData(const Value: TDataPoint<Double>): 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.
+165 -143
View File
@@ -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<Double, Double>)
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<TDataPoint<TOhlcItem>>(TTickAggregation.Create(timeframe));
var OhlcPoint := lastPrice.Chain<TDataPoint<TOhlcItem>>(TConverter.CreateAggregation(timeframe));
var Timestamps := OhlcPoint.Field<TDateTime>('Time');
var Ohlc := OhlcPoint.Field<TOhlcItem>('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<TDataPoint<Double>>(
function(const Tick: TDataPoint<TAskBidItem>): TDataPoint<Double>
begin
Result.Time := Tick.Time;
Result.Data := 0.5 * (Tick.Data.Ask + Tick.Data.Bid);
end
TConverter.CreateDataPointConverter<TAskBidItem, Double>(
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<TDataPoint<TOhlcItem>>(TConverter.CreateAggregation(timeframe));
var Closes :=
TConverter<TDataPoint<TOhlcItem>, Double>
.CreateGeneric(function(const Ohlc: TDataPoint<TOhlcItem>): Double begin Result := Ohlc.Data.Close; end);
var Ohlc := TConverter.CreateSequence<TOhlcItem>(2, OhlcPoint.Field<TOhlcItem>('Data').Sender);
var Hull := TConverter<Double, Double>.CreateGeneric(TIndicators.CreateHMA(150));
var Closes := Ohlc[0].Field<Double>('Close');
var Timestamps :=
TConverter<TDataPoint<TOhlcItem>, TDateTime>
.CreateGeneric(function(const Ohlc: TDataPoint<TOhlcItem>): TDateTime begin Result := Ohlc.Time; end);
OhlcPoint.Sender.Link(TimeStamps);
var Hull := Closes.Chain<Double>(TIndicators.CreateHMA(250));
var Sma := Closes.Chain<Double>(TIndicators.CreateSMA(200));
var HullSeries := TConverter.CreateEndpoint<Double>(Hull.Sender, 5);
var SmaSeries := TConverter.CreateEndpoint<Double>(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<Double>(TIndicators.CreateATR(50));
var ATRSeries := TConverter.CreateEndpoint<Double>(ATR.Sender, 5);
// next stage
var Signal :=
Ohlc[1]
.Chain<TSignal>(
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<Double>('pnl');
var equity: TConverter<Double, Double> := TEquitySum.Create(10000);
pnl.Sender.Link(equity);
var Layout := CurrLayout<TVertScrollBox>;
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<TDateTime>('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<Double>('SL').Sender, TAlphaColors.Red, 2);
panel.AddDoubleSeries(Signal.Field<Double>('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<Double>(equity.Sender);
OhlcPoint.Sender.Link(Ohlc);
Panel.AddOhlcSeries(Ohlc.Sender);
Ohlc.Sender.Link(Closes);
var Hull: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateHMA(150));
Closes.Sender.Link(Hull);
Panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue);
// Add SMA (Simple Moving Average)
var Sma: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateSMA(50));
Closes.Sender.Link(Sma);
Panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow);
// Add EMA (Exponential Moving Average)
var Ema: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateEMA(21));
Closes.Sender.Link(Ema);
Panel.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua);
// Add Bollinger Bands (20, 2.0)
var Boli: IMycConverter<Double, TBollingerBandsResult> :=
TGenericIndicator<Double, TBollingerBandsResult>.Create(TIndicators.CreateBollingerBands(20, 2.0));
Closes.Sender.Link(Boli);
var BoliUpper: IMycConverter<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end);
Boli.Sender.Link(BoliUpper);
Panel.AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray);
var BoliMiddle: IMycConverter<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double>
.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<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double>
.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<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateRSI(14));
Closes.Sender.Link(Rsi);
Panel.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia);
// Add MACD (12, 26, 9)
var Macd: IMycConverter<Double, TMacdResult> := TGenericIndicator<Double, TMacdResult>.Create(TIndicators.CreateMACD(12, 26, 9));
Closes.Sender.Link(Macd);
Panel := chart.AddPanel;
var MacdLine: IMycConverter<TMacdResult, Double> :=
TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.MacdLine; end);
Macd.Sender.Link(MacdLine);
Panel.AddDoubleSeries(MacdLine.Sender, TAlphaColors.Orange);
var MacdSignal: IMycConverter<TMacdResult, Double> :=
TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.SignalLine; end);
Macd.Sender.Link(MacdSignal);
Panel.AddDoubleSeries(MacdSignal.Sender, TAlphaColors.Dodgerblue);
var MacdHist: IMycConverter<TMacdResult, Double> :=
TMycGenericConverter<TMacdResult, Double>.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<TOhlcItem, TStochasticResult> :=
TGenericIndicator<TOhlcItem, TStochasticResult>.Create(TIndicators.CreateStochastic(14, 3));
Ohlc.Sender.Link(Stoch);
var StochK: IMycConverter<TStochasticResult, Double> :=
TMycGenericConverter<TStochasticResult, Double>.Create(function(const Item: TStochasticResult): Double begin Result := Item.K; end);
Stoch.Sender.Link(StochK);
Panel.AddDoubleSeries(StochK.Sender, TAlphaColors.Green);
var StochD: IMycConverter<TStochasticResult, Double> :=
TMycGenericConverter<TStochasticResult, Double>.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<TDataPoint<TAskBidItem>, TDateTime> :=
TMycGenericConverter<TDataPoint<TAskBidItem>, TDateTime>
.Create(function(const Tick: TDataPoint<TAskBidItem>): TDateTime begin Result := Tick.Time; end);
var TickAsk: IMycConverter<TDataPoint<TAskBidItem>, Double> :=
TMycGenericConverter<TDataPoint<TAskBidItem>, Double>
.Create(function(const Tick: TDataPoint<TAskBidItem>): Double begin Result := Tick.Data.Ask; end);
var TickBid: IMycConverter<TDataPoint<TAskBidItem>, Double> :=
TMycGenericConverter<TDataPoint<TAskBidItem>, Double>
.Create(function(const Tick: TDataPoint<TAskBidItem>): Double begin Result := Tick.Data.Bid; end);
var TickSpread: IMycConverter<TDataPoint<TAskBidItem>, Double> :=
TMycGenericConverter<TDataPoint<TAskBidItem>, Double>
.Create(function(const Tick: TDataPoint<TAskBidItem>): 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<S, T> }
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.
+6 -1
View File
@@ -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<T>);
destructor Destroy; override;
@@ -374,6 +374,11 @@ begin
inherited;
end;
function TChartXAxisLayer<T>.GetCaption(Idx: Int64): String;
begin
Result := IntToStr(Idx);
end;
function TChartXAxisLayer<T>.GetSeries: TMycChart.TSeries;
begin
Result := FSeries;
+12 -1
View File
@@ -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<TDateTime>): TMycChart.TXAxisLayer;
function SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider<TDateTime>): TMycChart.TXAxisLayer; overload;
function SetXAxisCounter<T>(const DataProvider: TDataProvider<T>): TMycChart.TXAxisLayer; overload;
property Lookback: TWriteable<Int64> read FLookback write FLookback;
property NeedRepaint: TFlag read FNeedRepaint;
@@ -639,6 +640,16 @@ begin
Repaint;
end;
function TMycChart.SetXAxisCounter<T>(const DataProvider: TDataProvider<T>): TMycChart.TXAxisLayer;
begin
FXAxisSeries.Free;
var counter := TConverter.CreateCounter<T>;
DataProvider.Link(counter);
FXAxisSeries := TChartXAxisLayer<Int64>.Create(Self, counter.Sender);
Result := FXAxisSeries;
end;
function TMycChart.SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider<TDateTime>): TMycChart.TXAxisLayer;
begin
FXAxisSeries.Free;
+6 -1
View File
@@ -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>): TState; static;
class function Any(const States: TArray<TState>; 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;
+11
View File
@@ -31,6 +31,8 @@ type
// Returns a State to await thread completion.
function RunTask(const Gate: TState; const Proc: TFunc<TState>): TState;
class function RunSequence(const Gate: TState; First, Count: Integer; const Proc: TFunc<Integer, TState>): 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<Integer, TState>): TState;
begin
if First >= Count then
exit;
var cProc: TFunc<Integer, TState> := 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>): TState;
begin
var cProc: TFunc<TState> := Proc;
+6 -3
View File
@@ -24,7 +24,7 @@ type
class operator Initialize(out Dest: TSeries<T>);
// Add a singe item
function Add(const Data: T; Lookback: Int64): TSeries<T>; overload;
function Add(const Data: T; Lookback: Int64 = -1): TSeries<T>; overload;
// Add a ranmge of items
function Add(const Data: array of T; First, Count, Lookback: Int64): TSeries<T>; overload;
// Helper to create a data array from a raw TArray.
@@ -36,6 +36,9 @@ type
implementation
uses
System.Math;
{ TSeries<T> }
constructor TSeries<T>.Create(const AChunks: TArray<TChunk>; 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;
+198 -10
View File
@@ -19,7 +19,7 @@ type
end;
// Concrete data provider that manages a list of processors (listeners).
TMycDataProvider<T> = class abstract(TContainedObject, TDataProvider<T>.IDataProvider)
TMycContainedDataProvider<T> = class abstract(TContainedObject, TDataProvider<T>.IDataProvider)
private
FListeners: TMycNotifyList<IMycProcessor<T>>;
public
@@ -33,6 +33,19 @@ type
procedure Unlink(Tag: TDataProvider<T>.TTag);
end;
TMycSequence<T> = class(TMycProcessor<T>, IMycDataSequence<T>)
private
FDataProviders: TArray<TMycContainedDataProvider<T>>;
function GetCount: Integer;
function GetDataProvider(Idx: Integer): TDataProvider<T>;
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<T> = class(TInterfacedObject, TDataProvider<T>.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<S, T> = class abstract(TMycProcessor<S>, TConverter<S, T>.IConverter)
private
FSender: TMycDataProvider<T>;
FSender: TMycContainedDataProvider<T>;
function GetSender: TDataProvider<T>.IDataProvider;
protected
function ProcessData(const Value: S): TState; override; abstract;
@@ -73,6 +86,12 @@ type
constructor Create(const AFunc: TConstFunc<S, T>);
end;
// A converter specialized for calculating indicators.
TMycIdentityConverter<T> = class(TMycConverter<T, T>)
protected
function ProcessData(const Value: T): TState; override; final;
end;
// A converter specialized for calculating indicators.
TMycIndicator<S, T> = class(TMycConverter<S, T>)
protected
@@ -145,26 +164,43 @@ type
destructor Destroy; override;
end;
TTickAggregation = class(TMycConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>)
private
FTimeframe: TTimeframe;
FCurrentBar: TDataPoint<TOhlcItem>;
function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetTimeframe: TTimeframe;
public
constructor Create(const ATimeframe: TTimeframe);
function ProcessData(const Value: TDataPoint<Double>): TState; override;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property Timeframe: TTimeframe read GetTimeframe;
end;
implementation
uses
System.TypInfo,
System.RTTI;
System.RTTI,
System.DateUtils,
System.Math,
Myc.TaskManager;
{ TMycDataProvider<T> }
{ TMycContainedDataProvider<T> }
constructor TMycDataProvider<T>.Create(const Controller: IInterface);
constructor TMycContainedDataProvider<T>.Create(const Controller: IInterface);
begin
inherited Create(Controller);
end;
destructor TMycDataProvider<T>.Destroy;
destructor TMycContainedDataProvider<T>.Destroy;
begin
FListeners.Finalize;
inherited Destroy;
end;
function TMycDataProvider<T>.Broadcast(const Value: T): TState;
function TMycContainedDataProvider<T>.Broadcast(const Value: T): TState;
begin
FListeners.Lock;
try
@@ -192,7 +228,7 @@ begin
end;
end;
function TMycDataProvider<T>.Link(const Processor: IMycProcessor<T>): TDataProvider<T>.TTag;
function TMycContainedDataProvider<T>.Link(const Processor: IMycProcessor<T>): TDataProvider<T>.TTag;
begin
// Add the Processor to the notification list
FListeners.Lock;
@@ -203,7 +239,7 @@ begin
end;
end;
procedure TMycDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
procedure TMycContainedDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
begin
FListeners.Lock;
try
@@ -230,7 +266,7 @@ end;
constructor TMycConverter<S, T>.Create;
begin
inherited Create;
FSender := TMycDataProvider<T>.Create(Self);
FSender := TMycContainedDataProvider<T>.Create(Self);
end;
destructor TMycConverter<S, T>.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<TOhlcItem>;
begin
Result := FCurrentBar;
end;
function TTickAggregation.GetTimeframe: TTimeframe;
begin
Result := FTimeframe;
end;
function TTickAggregation.ProcessData(const Value: TDataPoint<Double>): 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<T> }
constructor TMycSequence<T>.Create(ACount: Integer);
begin
inherited Create;
SetLength(FDataProviders, ACount);
for var i := 0 to High(FDataProviders) do
FDataProviders[i] := TMycContainedDataProvider<T>.Create(Self);
end;
destructor TMycSequence<T>.Destroy;
begin
for var i := High(FDataProviders) downto 0 do
FDataProviders[i].Free;
inherited;
end;
function TMycSequence<T>.GetCount: Integer;
begin
Result := Length(FDataProviders);
end;
function TMycSequence<T>.GetDataProvider(Idx: Integer): TDataProvider<T>;
begin
Result := FDataProviders[Idx];
end;
function TMycSequence<T>.ProcessData(const Value: T): TState;
begin
Result := ProcessDataProvider(0, Value);
end;
function TMycSequence<T>.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<T>.ProcessData(const Value: T): TState;
begin
Result := Broadcast(Value);
end;
end.
+68 -8
View File
@@ -48,6 +48,13 @@ type
class property Null: IDataProvider read FNull;
end;
IMycDataSequence<T> = interface(IMycProcessor<T>)
function GetCount: Integer;
function GetDataProvider(Idx: Integer): TDataProvider<T>;
property Count: Integer read GetCount;
property DataProvider[Idx: Integer]: TDataProvider<T> read GetDataProvider; default;
end;
// Interface helper for IConverter<S,T> providing the null object pattern.
TConverter<S, T> = record
public
@@ -59,7 +66,6 @@ type
property Sender: TDataProvider<T>.IDataProvider read GetSender;
end;
{$region 'private'}
strict private
class var
FNull: IConverter;
@@ -67,7 +73,6 @@ type
private
FConverter: IConverter;
function GetSender: TDataProvider<T>; inline;
{$endregion}
public
constructor Create(const AConverter: IConverter);
@@ -78,15 +83,15 @@ type
class function CreateGeneric(const Func: TConstFunc<S, T>): TConverter<S, T>; static;
// Wrapper for IMycProcessor.ProcessData
function ProcessData(const Value: S): TState; inline;
function Chain<R>(const Next: TConverter<T, R>): TConverter<T, R>; overload; inline;
function Chain<R>(const Func: TConstFunc<T, R>): TConverter<T, R>; overload; inline;
// Extracts the field of a record by it's name (using RTTI).
function Field<R>(const FieldName: String): TConverter<T, R>; overload; inline;
function Sequence(Count: Integer): IMycDataSequence<T>; overload;
function Sequence(const Items: TArray<IConverter>): IMycDataSequence<S>; 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<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TMutable<TSeries<T>>; static;
class function CreateCounter<T>: TConverter<T, Int64>; static;
class function CreateTicker<T>: TConverter<TArray<T>, T>; static;
class function CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>; static;
class function CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TMutable<TSeries<T>>; static;
class function CreateIdentity<T>: TConverter<T, T>; static;
class function CreateAggregation(Timeframe: TTimeframe): TConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>; static;
class function CreateDataPointConverter<S, T>(const Func: TConstFunc<S, T>): TConverter<TDataPoint<S>, TDataPoint<T>>; static;
class function CreateSequence<T>(Count: Integer; const Parent: TDataProvider<T>): TArray<TConverter<T, T>>; overload; static;
end;
implementation
@@ -186,6 +199,22 @@ begin
Result := FConverter.Sender;
end;
function TConverter<S, T>.Sequence(Count: Integer): IMycDataSequence<T>;
begin
Result := TMycSequence<T>.Create(Count);
FConverter.Sender.Link(Result);
end;
function TConverter<S, T>.Sequence(const Items: TArray<IConverter>): IMycDataSequence<S>;
begin
var seq: IMycDataSequence<S> := TMycSequence<S>.Create(Length(Items));
for var i := 0 to High(Items) do
seq[i].Link(Items[i]);
Result := seq;
end;
class operator TConverter<S, T>.Initialize(out Dest: TConverter<S, T>);
begin
Dest.FConverter := FNull;
@@ -201,9 +230,9 @@ begin
Result := A.FConverter;
end;
function TConverter<S, T>.ProcessData(const Value: S): TState;
class function TConverter.CreateAggregation(Timeframe: TTimeframe): TConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>;
begin
Result := FConverter.ProcessData(Value);
Result := TTickAggregation.Create(Timeframe);
end;
{ TConverter }
@@ -213,11 +242,29 @@ begin
Result := TMycDataCounter<T>.Create;
end;
class function TConverter.CreateDataPointConverter<S, T>(const Func: TConstFunc<S, T>): TConverter<TDataPoint<S>, TDataPoint<T>>;
begin
var cFunc: TConstFunc<S, T> := Func;
Result :=
TMycGenericConverter<TDataPoint<S>, TDataPoint<T>>.Create(
function(const Value: TDataPoint<S>): TDataPoint<T>
begin
Result.Time := Value.Time;
Result.Data := cFunc(Value.Data);
end
);
end;
class function TConverter.CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TMutable<TSeries<T>>;
begin
Result := TMycDataEndpoint<T>.Create(DataProvider, Lookback);
end;
class function TConverter.CreateIdentity<T>: TConverter<T, T>;
begin
Result := TMycIdentityConverter<T>.Create;
end;
class function TConverter.CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>;
begin
Result := TMycRecordFieldReader<S, T>.Create(FieldName);
@@ -228,4 +275,17 @@ begin
Result := TMycTicker<T>.Create;
end;
class function TConverter.CreateSequence<T>(Count: Integer; const Parent: TDataProvider<T>): TArray<TConverter<T, T>>;
begin
var seq: IMycDataSequence<T> := TMycSequence<T>.Create(Count);
SetLength(Result, Count);
for var i := 0 to High(Result) do
begin
Result[i] := TMycIdentityConverter<T>.Create;
seq[i].Link(Result[i]);
end;
Parent.Link(seq);
end;
end.
+124 -10
View File
@@ -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<Double>; const Period: Integer): Double; static;
@@ -44,11 +51,33 @@ type
// Relative Strength Index
class function CreateRSI(Period: Integer): TConstFunc<Double, Double>; static;
// Moving Average Convergence Divergence
class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc<Double, TMacdResult>; static;
class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc<Double, TMacdResult>; overload; static;
class function CreateMACD(
const EmaFast,
EmaSlow,
EmaSignal: TConstFunc<Double, Double>
): TConstFunc<Double, TMacdResult>; overload; static;
// Stochastic Oscillator
class function CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc<TOhlcItem, TStochasticResult>; static;
class function CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc<TOhlcItem, TStochasticResult>; overload; static;
class function CreateStochastic(
KPeriod: Integer;
const SmaD: TConstFunc<Double, Double>
): TConstFunc<TOhlcItem, TStochasticResult>; overload; static;
// Bollinger Bands
class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConstFunc<Double, TBollingerBandsResult>; static;
// Average True Range
class function CreateATR(Period: Integer): TConstFunc<TOhlcItem, Double>; overload; static;
class function CreateATR(const MovAvgTR: TConstFunc<Double, Double>): TConstFunc<TOhlcItem, Double>; overload; static;
// Keltner Channels
class function CreateKeltnerChannels(
Period: Integer;
Multiplier: Double
): TConstFunc<TOhlcItem, TKeltnerChannelsResult>; overload; static;
class function CreateKeltnerChannels(
const MovAvgMiddle: TConstFunc<Double, Double>;
const AtrFunc: TConstFunc<TOhlcItem, Double>;
Multiplier: Double
): TConstFunc<TOhlcItem, TKeltnerChannelsResult>; overload; static;
end;
implementation
@@ -208,19 +237,22 @@ begin
end;
end;
// Standard MACD using EMAs.
class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc<Double, TMacdResult>;
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<Double, Double>): TConstFunc<Double, TMacdResult>;
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<TOhlcItem, TStochasticResult>;
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<Double, Double>
): TConstFunc<TOhlcItem, TStochasticResult>;
begin
var sourceData: TSeries<TOhlcItem>;
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<TOhlcItem, Double>;
begin
Result := CreateATR(CreateEMA(Period));
end;
// Calculates the Average True Range (ATR) using an injectable moving average.
class function TIndicators.CreateATR(const MovAvgTR: TConstFunc<Double, Double>): TConstFunc<TOhlcItem, Double>;
begin
var sourceData: TSeries<TOhlcItem>;
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<TOhlcItem, TKeltnerChannelsResult>;
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<Double, Double>;
const AtrFunc: TConstFunc<TOhlcItem, Double>;
Multiplier: Double
): TConstFunc<TOhlcItem, TKeltnerChannelsResult>;
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;
+1
View File
@@ -29,6 +29,7 @@ type
end;
TConstFunc<S, T> = reference to function(const Value: S): T;
TConstFuncPredicate<S, T> = reference to function(const Value: S; out Res: T): Boolean;
implementation