Work in Progress
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user