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