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', Myc.Aura.Parameter in '..\Src\Myc.Aura.Parameter.pas',
TestModule in 'TestModule.pas', TestModule in 'TestModule.pas',
DynamicFMXControl in 'DynamicFMXControl.pas', DynamicFMXControl in 'DynamicFMXControl.pas',
FirstStrategy in 'FirstStrategy.pas',
Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas',
Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas', Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas',
Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.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="..\Src\Myc.Aura.Parameter.pas"/>
<DCCReference Include="TestModule.pas"/> <DCCReference Include="TestModule.pas"/>
<DCCReference Include="DynamicFMXControl.pas"/> <DCCReference Include="DynamicFMXControl.pas"/>
<DCCReference Include="FirstStrategy.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataArray.pas"/> <DCCReference Include="..\Src\Myc.Trade.DataArray.pas"/>
<DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/> <DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Indicators.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.DateUtils,
System.Generics.Collections, System.Generics.Collections,
System.Rtti, System.Rtti,
System.Math,
FMX.Types, FMX.Types,
FMX.Controls, FMX.Controls,
FMX.Forms, FMX.Forms,
@@ -32,9 +33,11 @@ uses
Myc.Trade.DataPoint, Myc.Trade.DataPoint,
Myc.Signals, Myc.Signals,
Myc.Mutable, Myc.Mutable,
Myc.Trade.DataArray,
Myc.Signals.FMX, Myc.Signals.FMX,
Myc.TaskManager, Myc.TaskManager,
Myc.Aura.Module, Myc.Aura.Module,
Myc.Trade.DataPoint.Impl,
FMX.ListBox, FMX.ListBox,
FMX.Layouts, FMX.Layouts,
FMX.TreeView, FMX.TreeView,
@@ -45,7 +48,6 @@ uses
System.Actions, System.Actions,
FMX.ActnList, FMX.ActnList,
DynamicFMXControl, DynamicFMXControl,
FirstStrategy,
Myc.FMX.Chart; Myc.FMX.Chart;
type type
@@ -103,6 +105,15 @@ type
property OnEvent: TNotifyEvent read FOnEvent write FOnEvent; property OnEvent: TNotifyEvent read FOnEvent write FOnEvent;
end; end;
TEquitySum = class(TMycConverter<Double, Double>)
private
FEquity: Double;
protected
function ProcessData(const Value: Double): TState; override;
public
constructor Create(AEquity: Double);
end;
var var
Form1: TForm1; Form1: TForm1;
@@ -343,7 +354,7 @@ begin
end 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 Timestamps := OhlcPoint.Field<TDateTime>('Time');
var Ohlc := OhlcPoint.Field<TOhlcItem>('Data'); var Ohlc := OhlcPoint.Field<TOhlcItem>('Data');
@@ -410,6 +421,13 @@ begin
end; end;
procedure TForm1.Strat2ButtonClick(Sender: TObject); procedure TForm1.Strat2ButtonClick(Sender: TObject);
type
TSignal = record
Sig: Double;
SL: Double;
Entry: Double;
pnl: Double;
end;
begin begin
var timeframe := TTimeframe.M15; var timeframe := TTimeframe.M15;
@@ -417,26 +435,123 @@ begin
var lastPrice := var lastPrice :=
ticker.Chain<TDataPoint<Double>>( ticker.Chain<TDataPoint<Double>>(
function(const Tick: TDataPoint<TAskBidItem>): TDataPoint<Double> TConverter.CreateDataPointConverter<TAskBidItem, Double>(
begin function(const Tick: TAskBidItem): Double begin Result := 0.5 * (Tick.Ask + Tick.Bid); end
Result.Time := Tick.Time; )
Result.Data := 0.5 * (Tick.Data.Ask + Tick.Data.Bid);
end
); );
var OhlcPoint := TTickAggregation.Create(timeframe); var OhlcPoint := lastPrice.Chain<TDataPoint<TOhlcItem>>(TConverter.CreateAggregation(timeframe));
lastPrice.Sender.Link(OhlcPoint);
var Closes := var Ohlc := TConverter.CreateSequence<TOhlcItem>(2, OhlcPoint.Field<TOhlcItem>('Data').Sender);
TConverter<TDataPoint<TOhlcItem>, Double>
.CreateGeneric(function(const Ohlc: TDataPoint<TOhlcItem>): Double begin Result := Ohlc.Data.Close; end);
var Hull := TConverter<Double, Double>.CreateGeneric(TIndicators.CreateHMA(150)); var Closes := Ohlc[0].Field<Double>('Close');
var Timestamps := var Hull := Closes.Chain<Double>(TIndicators.CreateHMA(250));
TConverter<TDataPoint<TOhlcItem>, TDateTime> var Sma := Closes.Chain<Double>(TIndicators.CreateSMA(200));
.CreateGeneric(function(const Ohlc: TDataPoint<TOhlcItem>): TDateTime begin Result := Ohlc.Time; end);
OhlcPoint.Sender.Link(TimeStamps); 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>; var Layout := CurrLayout<TVertScrollBox>;
if Layout = nil then if Layout = nil then
@@ -451,142 +566,49 @@ begin
chart.Height := Layout.ChildrenRect.Width * 9 / 16; chart.Height := Layout.ChildrenRect.Width * 9 / 16;
chart.Lookback.Value := 50000; 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 := pnlChart.AddPanel;
Panel.AddOhlcSeries(Ohlc.Sender); panel.AddDoubleSeries(equity.Sender, TAlphaColors.Blue, 3);
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);
///// /////
var done := ExecuteStrategy(Symbol, ticker); var done := ExecuteStrategy(Symbol, ticker);
FProcessDone := TState.All([FProcessDone, done]); 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;
end. end.
+6 -1
View File
@@ -116,7 +116,7 @@ type
protected protected
function GetSeries: TMycChart.TSeries; override; function GetSeries: TMycChart.TSeries; override;
procedure Update; override; procedure Update; override;
function GetCaption(Idx: Int64): String; override; abstract; function GetCaption(Idx: Int64): String; override;
public public
constructor Create(AOwner: TMycChart; const ADataProvider: TDataProvider<T>); constructor Create(AOwner: TMycChart; const ADataProvider: TDataProvider<T>);
destructor Destroy; override; destructor Destroy; override;
@@ -374,6 +374,11 @@ begin
inherited; inherited;
end; end;
function TChartXAxisLayer<T>.GetCaption(Idx: Int64): String;
begin
Result := IntToStr(Idx);
end;
function TChartXAxisLayer<T>.GetSeries: TMycChart.TSeries; function TChartXAxisLayer<T>.GetSeries: TMycChart.TSeries;
begin begin
Result := FSeries; Result := FSeries;
+12 -1
View File
@@ -166,7 +166,8 @@ type
function AddPanel: TPanel; function AddPanel: TPanel;
// Sets the master series that defines the time scale (X-axis). // 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 Lookback: TWriteable<Int64> read FLookback write FLookback;
property NeedRepaint: TFlag read FNeedRepaint; property NeedRepaint: TFlag read FNeedRepaint;
@@ -639,6 +640,16 @@ begin
Repaint; Repaint;
end; 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; function TMycChart.SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: TDataProvider<TDateTime>): TMycChart.TXAxisLayer;
begin begin
FXAxisSeries.Free; FXAxisSeries.Free;
+6 -1
View File
@@ -77,7 +77,7 @@ type
class operator Initialize(out Dest: TState); class operator Initialize(out Dest: TState);
class operator Implicit(const A: IState): TState; overload; class operator Implicit(const A: IState): TState; overload;
class operator Implicit(const A: TState): IState; 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 All(const States: TArray<TState>): TState; static;
class function Any(const States: TArray<TState>; Count: Integer = 1): TState; static; class function Any(const States: TArray<TState>; Count: Integer = 1): TState; static;
@@ -271,6 +271,11 @@ begin
Result := FState.IsSet; Result := FState.IsSet;
end; end;
class operator TState.Add(const A, B: TState): TState;
begin
Result := All([A, B]);
end;
class operator TState.Implicit(const A: TState): IState; class operator TState.Implicit(const A: TState): IState;
begin begin
Result := A.FState; Result := A.FState;
+11
View File
@@ -31,6 +31,8 @@ type
// Returns a State to await thread completion. // Returns a State to await thread completion.
function RunTask(const Gate: TState; const Proc: TFunc<TState>): TState; 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. // Waits for the operation associated with State to complete.
// Must not be called from a task of this factory. // Must not be called from a task of this factory.
// After waiting, or if the state is already set, any first stored exception // After waiting, or if the state is already set, any first stored exception
@@ -146,6 +148,15 @@ begin
); );
end; 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; function TTaskManager.RunTask(const Gate: TState; const Proc: TFunc<TState>): TState;
begin begin
var cProc: TFunc<TState> := Proc; var cProc: TFunc<TState> := Proc;
+6 -3
View File
@@ -24,7 +24,7 @@ type
class operator Initialize(out Dest: TSeries<T>); class operator Initialize(out Dest: TSeries<T>);
// Add a singe item // 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 // Add a ranmge of items
function Add(const Data: array of T; First, Count, Lookback: Int64): TSeries<T>; overload; function Add(const Data: array of T; First, Count, Lookback: Int64): TSeries<T>; overload;
// Helper to create a data array from a raw TArray. // Helper to create a data array from a raw TArray.
@@ -36,6 +36,9 @@ type
implementation implementation
uses
System.Math;
{ TSeries<T> } { TSeries<T> }
constructor TSeries<T>.Create(const AChunks: TArray<TChunk>; ACount, ATotalCount: Int64); constructor TSeries<T>.Create(const AChunks: TArray<TChunk>; ACount, ATotalCount: Int64);
@@ -62,14 +65,14 @@ var
begin begin
if Count < 0 then if Count < 0 then
Count := Length(Data) - First; Count := Length(Data) - First;
if (Lookback <= 0) or (Count = 0) then if Count = 0 then
exit(Self); exit(Self);
Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array'); Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array');
sumCount := FCount + Count; sumCount := FCount + Count;
newCount := sumCount; newCount := sumCount;
if (Lookback > 0) and (newCount > Lookback) then if (Lookback >= 0) and (newCount > Lookback) then
newCount := Lookback; newCount := Lookback;
itemsToSkip := sumCount - newCount; itemsToSkip := sumCount - newCount;
+198 -10
View File
@@ -19,7 +19,7 @@ type
end; end;
// Concrete data provider that manages a list of processors (listeners). // 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 private
FListeners: TMycNotifyList<IMycProcessor<T>>; FListeners: TMycNotifyList<IMycProcessor<T>>;
public public
@@ -33,6 +33,19 @@ type
procedure Unlink(Tag: TDataProvider<T>.TTag); procedure Unlink(Tag: TDataProvider<T>.TTag);
end; 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. // Null object implementation for IDataProvider.
TNullDataProvider<T> = class(TInterfacedObject, TDataProvider<T>.IDataProvider) TNullDataProvider<T> = class(TInterfacedObject, TDataProvider<T>.IDataProvider)
public public
@@ -43,7 +56,7 @@ type
// Abstract base class for components that process data of type S and provide data of type T. // 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) TMycConverter<S, T> = class abstract(TMycProcessor<S>, TConverter<S, T>.IConverter)
private private
FSender: TMycDataProvider<T>; FSender: TMycContainedDataProvider<T>;
function GetSender: TDataProvider<T>.IDataProvider; function GetSender: TDataProvider<T>.IDataProvider;
protected protected
function ProcessData(const Value: S): TState; override; abstract; function ProcessData(const Value: S): TState; override; abstract;
@@ -73,6 +86,12 @@ type
constructor Create(const AFunc: TConstFunc<S, T>); constructor Create(const AFunc: TConstFunc<S, T>);
end; 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. // A converter specialized for calculating indicators.
TMycIndicator<S, T> = class(TMycConverter<S, T>) TMycIndicator<S, T> = class(TMycConverter<S, T>)
protected protected
@@ -145,26 +164,43 @@ type
destructor Destroy; override; destructor Destroy; override;
end; 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 implementation
uses uses
System.TypInfo, 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 begin
inherited Create(Controller); inherited Create(Controller);
end; end;
destructor TMycDataProvider<T>.Destroy; destructor TMycContainedDataProvider<T>.Destroy;
begin begin
FListeners.Finalize; FListeners.Finalize;
inherited Destroy; inherited Destroy;
end; end;
function TMycDataProvider<T>.Broadcast(const Value: T): TState; function TMycContainedDataProvider<T>.Broadcast(const Value: T): TState;
begin begin
FListeners.Lock; FListeners.Lock;
try try
@@ -192,7 +228,7 @@ begin
end; end;
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 begin
// Add the Processor to the notification list // Add the Processor to the notification list
FListeners.Lock; FListeners.Lock;
@@ -203,7 +239,7 @@ begin
end; end;
end; end;
procedure TMycDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag); procedure TMycContainedDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
begin begin
FListeners.Lock; FListeners.Lock;
try try
@@ -230,7 +266,7 @@ end;
constructor TMycConverter<S, T>.Create; constructor TMycConverter<S, T>.Create;
begin begin
inherited Create; inherited Create;
FSender := TMycDataProvider<T>.Create(Self); FSender := TMycContainedDataProvider<T>.Create(Self);
end; end;
destructor TMycConverter<S, T>.Destroy; destructor TMycConverter<S, T>.Destroy;
@@ -411,4 +447,156 @@ begin
FChanged.Notify; FChanged.Notify;
end; 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. end.
+68 -8
View File
@@ -48,6 +48,13 @@ type
class property Null: IDataProvider read FNull; class property Null: IDataProvider read FNull;
end; 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. // Interface helper for IConverter<S,T> providing the null object pattern.
TConverter<S, T> = record TConverter<S, T> = record
public public
@@ -59,7 +66,6 @@ type
property Sender: TDataProvider<T>.IDataProvider read GetSender; property Sender: TDataProvider<T>.IDataProvider read GetSender;
end; end;
{$region 'private'}
strict private strict private
class var class var
FNull: IConverter; FNull: IConverter;
@@ -67,7 +73,6 @@ type
private private
FConverter: IConverter; FConverter: IConverter;
function GetSender: TDataProvider<T>; inline; function GetSender: TDataProvider<T>; inline;
{$endregion}
public public
constructor Create(const AConverter: IConverter); constructor Create(const AConverter: IConverter);
@@ -78,15 +83,15 @@ type
class function CreateGeneric(const Func: TConstFunc<S, T>): TConverter<S, T>; static; 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 Next: TConverter<T, R>): TConverter<T, R>; overload; inline;
function Chain<R>(const Func: TConstFunc<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). // Extracts the field of a record by it's name (using RTTI).
function Field<R>(const FieldName: String): TConverter<T, R>; overload; inline; 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. // Provides access to the null object instance.
class property Null: IConverter read FNull; class property Null: IConverter read FNull;
// Wrapper for IConverter.Sender // Wrapper for IConverter.Sender
@@ -95,10 +100,18 @@ type
// Factory for creating specific converter instances. // Factory for creating specific converter instances.
TConverter = record 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 CreateCounter<T>: TConverter<T, Int64>; static;
class function CreateTicker<T>: TConverter<TArray<T>, T>; static; class function CreateTicker<T>: TConverter<TArray<T>, T>; static;
class function CreateRecordField<S, T>(const FieldName: String): TConverter<S, 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; end;
implementation implementation
@@ -186,6 +199,22 @@ begin
Result := FConverter.Sender; Result := FConverter.Sender;
end; 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>); class operator TConverter<S, T>.Initialize(out Dest: TConverter<S, T>);
begin begin
Dest.FConverter := FNull; Dest.FConverter := FNull;
@@ -201,9 +230,9 @@ begin
Result := A.FConverter; Result := A.FConverter;
end; end;
function TConverter<S, T>.ProcessData(const Value: S): TState; class function TConverter.CreateAggregation(Timeframe: TTimeframe): TConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>;
begin begin
Result := FConverter.ProcessData(Value); Result := TTickAggregation.Create(Timeframe);
end; end;
{ TConverter } { TConverter }
@@ -213,11 +242,29 @@ begin
Result := TMycDataCounter<T>.Create; Result := TMycDataCounter<T>.Create;
end; 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>>; class function TConverter.CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TMutable<TSeries<T>>;
begin begin
Result := TMycDataEndpoint<T>.Create(DataProvider, Lookback); Result := TMycDataEndpoint<T>.Create(DataProvider, Lookback);
end; 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>; class function TConverter.CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>;
begin begin
Result := TMycRecordFieldReader<S, T>.Create(FieldName); Result := TMycRecordFieldReader<S, T>.Create(FieldName);
@@ -228,4 +275,17 @@ begin
Result := TMycTicker<T>.Create; Result := TMycTicker<T>.Create;
end; 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. end.
+124 -10
View File
@@ -29,6 +29,13 @@ type
LowerBand: Double; LowerBand: Double;
end; end;
// Result for the Keltner Channels indicator.
TKeltnerChannelsResult = record
UpperBand: Double;
MiddleBand: Double;
LowerBand: Double;
end;
TIndicators = record TIndicators = record
private private
class function CalculateSMA(const Series: TSeries<Double>; const Period: Integer): Double; static; class function CalculateSMA(const Series: TSeries<Double>; const Period: Integer): Double; static;
@@ -44,11 +51,33 @@ type
// Relative Strength Index // Relative Strength Index
class function CreateRSI(Period: Integer): TConstFunc<Double, Double>; static; class function CreateRSI(Period: Integer): TConstFunc<Double, Double>; static;
// Moving Average Convergence Divergence // 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 // 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 // Bollinger Bands
class function CreateBollingerBands(Period: Integer; Multiplier: Double): TConstFunc<Double, TBollingerBandsResult>; static; 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; end;
implementation implementation
@@ -208,19 +237,22 @@ begin
end; end;
end; end;
// Standard MACD using EMAs.
class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc<Double, TMacdResult>; class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TConstFunc<Double, TMacdResult>;
begin begin
var emaFast := CreateEMA(FastPeriod); Result := CreateMACD(CreateEMA(FastPeriod), CreateEMA(SlowPeriod), CreateEMA(SignalPeriod));
var emaSlow := CreateEMA(SlowPeriod); end;
var emaSignal := CreateEMA(SignalPeriod);
// 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 := Result :=
function(const Value: Double): TMacdResult function(const Value: Double): TMacdResult
var var
fastVal, slowVal: Double; fastVal, slowVal: Double;
begin begin
fastVal := emaFast(Value); fastVal := EmaFast(Value);
slowVal := emaSlow(Value); slowVal := EmaSlow(Value);
if IsNan(slowVal) then // slowVal will be the last one to become non-NaN if IsNan(slowVal) then // slowVal will be the last one to become non-NaN
begin begin
@@ -231,7 +263,7 @@ begin
else else
begin begin
Result.MacdLine := fastVal - slowVal; Result.MacdLine := fastVal - slowVal;
Result.SignalLine := emaSignal(Result.MacdLine); Result.SignalLine := EmaSignal(Result.MacdLine);
if not IsNan(Result.SignalLine) then if not IsNan(Result.SignalLine) then
Result.Histogram := Result.MacdLine - Result.SignalLine Result.Histogram := Result.MacdLine - Result.SignalLine
else else
@@ -313,10 +345,19 @@ begin
end; end;
end; end;
// Standard Stochastic Oscillator using an SMA for the %D line.
class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TConstFunc<TOhlcItem, TStochasticResult>; 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 begin
var sourceData: TSeries<TOhlcItem>; var sourceData: TSeries<TOhlcItem>;
var smaD := CreateSMA(DPeriod);
Result := Result :=
function(const Value: TOhlcItem): TStochasticResult function(const Value: TOhlcItem): TStochasticResult
@@ -347,7 +388,80 @@ begin
else else
Result.K := 100; // Or 50, depends on convention 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; end;
end; end;
+1
View File
@@ -29,6 +29,7 @@ type
end; end;
TConstFunc<S, T> = reference to function(const Value: S): T; TConstFunc<S, T> = reference to function(const Value: S): T;
TConstFuncPredicate<S, T> = reference to function(const Value: S; out Res: T): Boolean;
implementation implementation