TestStrategy refactored

This commit is contained in:
Michael Schimmel
2025-07-22 18:04:15 +02:00
parent e6d41260f9
commit d1d3393392
+105 -103
View File
@@ -20,12 +20,13 @@ uses
function CreateStrategy1(Timeframe: TTimeframe): TConverter<TDataPoint<TOhlcItem>, Double>; function CreateStrategy1(Timeframe: TTimeframe): TConverter<TDataPoint<TOhlcItem>, Double>;
type type
TSignal = record // A record to transfer a detected signal event and the required price data to the next stage.
Sig: Double; TSignalEvent = record
SL: Double; Signal: Integer; // -1 for short, 1 for long, 0 for no new signal
Entry: Double; Close, Low, High, ATR: Double;
pnl: Double; InitialSL: Double; // The calculated SL (Highest/Lowest) at the time of the signal
end; end;
begin begin
var ticker := TConverter.CreateIdentity<TDataPoint<TOhlcItem>>; var ticker := TConverter.CreateIdentity<TDataPoint<TOhlcItem>>;
@@ -37,133 +38,134 @@ begin
var Hull := Closes.Chain<Double>(TIndicators.CreateHMA(250)).MakeParallel; var Hull := Closes.Chain<Double>(TIndicators.CreateHMA(250)).MakeParallel;
var Sma := Closes.Chain<Double>(TIndicators.CreateSMA(200)).MakeParallel; var Sma := Closes.Chain<Double>(TIndicators.CreateSMA(200)).MakeParallel;
var Lowest: Double := Double.MaxValue;
var Highest: Double := Double.MinValue;
var ATR := Ohlc.Chain<Double>(TIndicators.CreateATR(50)).MakeParallel; var ATR := Ohlc.Chain<Double>(TIndicators.CreateATR(50)).MakeParallel;
// next stage
var ATREndPoint := TConverter.CreateEndpoint<Double>(ATR.Sender, 5);
var ATRSeries: TSeries<Double>;
var HullEndPoint := TConverter.CreateEndpoint<Double>(Hull.Sender, 5);
var HullSeries: TSeries<Double>;
var SmaEndPoint := TConverter.CreateEndpoint<Double>(Sma.Sender, 5);
var SmaSeries: TSeries<Double>;
var curr: TSignal;
curr.SL := Double.NaN;
curr.Entry := Double.NaN;
var lastHull, lastSma: Double;
var conv := var conv :=
TConverter.Join<Double>( TConverter.Join<Double>(
[Ohlc.Field<Double>('Low').Sender, Ohlc.Field<Double>('High').Sender, Closes.Sender, ATR.Sender, Hull.Sender, Sma.Sender] [Ohlc.Field<Double>('Low').Sender, Ohlc.Field<Double>('High').Sender, Closes.Sender, ATR.Sender, Hull.Sender, Sma.Sender]
); );
var Signal := // STAGE 1: Signal Generation. This converter is stateless regarding the trade itself.
TConverter<TArray<Double>, TSignal>.CreateGeneric( // It only detects the crossover event and prepares the data for the next stage.
function(const Values: TArray<Double>): TSignal var Lowest: Double := Double.MaxValue;
var Highest: Double := Double.MinValue;
var lastHull, lastSma: Double;
var signalGenerator :=
TConverter<TArray<Double>, TSignalEvent>.CreateGeneric(
function(const Values: TArray<Double>): TSignalEvent
begin begin
var low := Values[0]; Result.Low := Values[0];
var high := Values[1]; Result.High := Values[1];
var close := Values[2]; Result.Close := Values[2];
var atr := Values[3]; Result.ATR := Values[3];
var hull := Values[4]; var hull := Values[4];
var sma := Values[5]; var sma := Values[5];
if low < Lowest then if Result.Low < Lowest then
Lowest := low; Lowest := Result.Low;
if high > Highest then if Result.High > Highest then
Highest := high; Highest := Result.High;
Result := curr; Result.Signal := 0;
Result.Sig := 0; Result.InitialSL := Double.NaN;
var pnl: double := NaN;
if (hull < sma) and (lastHull >= lastSma) then if (hull < sma) and (lastHull >= lastSma) then
begin begin
if curr.Sig > 0 then Result.Signal := -1;
pnl := close - curr.Entry; Result.InitialSL := Highest;
Highest := Double.MinValue; // Reset for next trend
curr.Sig := -1; Lowest := Double.MaxValue;
curr.SL := Highest;
curr.Entry := close;
Result := curr;
end end
else if (hull > sma) and (lastHull <= lastSma) then else if (hull > sma) and (lastHull <= lastSma) then
begin begin
if curr.Sig < 0 then Result.Signal := 1;
pnl := curr.Entry - close; Result.InitialSL := Lowest;
Highest := Double.MinValue; // Reset for next trend
curr.Sig := 1;
curr.SL := Lowest;
curr.Entry := close;
Result := curr;
end;
atr := 15 * atr;
if curr.Sig > 0 then
begin
if close > curr.SL then
begin
if curr.SL < close - atr then
curr.SL := close - atr;
Result.SL := curr.SL;
end;
if 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 close < curr.SL then
begin
if curr.SL > close + atr then
curr.SL := close + atr;
Result.SL := curr.SL;
end;
if 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; Lowest := Double.MaxValue;
Highest := Double.MinValue;
Result.SL := Double.NaN;
Result.Entry := Double.NaN;
end; end;
Result.pnl := pnl;
lastHull := hull; lastHull := hull;
lastSma := sma; lastSma := sma;
end end
); );
conv.Link(Signal); // STAGE 2: Position Management. This stateful converter manages the lifecycle
// of a single trade (entry, trailing stop, exit) and outputs the PnL.
// State variables for the position manager
var currSig: Integer := 0;
var currSL := Double.NaN;
var currEntry := Double.NaN;
var pnl := Signal.Field<Double>('pnl'); var positionManager :=
TConverter<TSignalEvent, Double>.CreateAggregation(
function(const Value: TSignalEvent; const Broadcast: TConverter<TSignalEvent, Double>.TBroadcastProc): TState
var
pnl: Double;
begin
Result := TState.Null;
pnl := Double.NaN;
// 1. Check for a new signal to open or reverse a position
if Value.Signal <> 0 then
begin
// If a position is already open, close it first
if currSig > 0 then
pnl := Value.Close - currEntry
else if currSig < 0 then
pnl := currEntry - Value.Close;
// Open new position
currSig := Value.Signal;
currEntry := Value.Close;
currSL := Value.InitialSL;
end
// 2. If no new signal, manage the currently open position
else
begin
var atrValue := 15 * Value.ATR;
if currSig > 0 then // Manage long position
begin
if Value.Close > currSL then
if currSL < Value.Close - atrValue then
currSL := Value.Close - atrValue;
if Value.Low <= currSL then
begin
pnl := currSL - currEntry;
currSig := 0; // Close position
end;
end
else if currSig < 0 then // Manage short position
begin
if Value.Close < currSL then
if currSL > Value.Close + atrValue then
currSL := Value.Close + atrValue;
if Value.High >= currSL then
begin
pnl := currEntry - currSL;
currSig := 0; // Close position
end;
end;
end;
// 3. If a PnL was generated (trade closed), broadcast it
if not IsNan(pnl) then
begin
currSL := Double.NaN;
Broadcast(pnl);
end;
end
);
// Chain the stages together
conv.Link(signalGenerator);
signalGenerator.Sender.Link(positionManager);
// The final equity calculation remains the same, it just consumes the PnL from the position manager
var FEquity: Double := 10000; var FEquity: Double := 10000;
var FInit: Boolean := false; var FInit: Boolean := false;
var equity := var equity :=
TConverter<Double, Double>.CreateAggregation( TConverter<Double, Double>.CreateAggregation(
function(const Value: Double; const Broadcast: TConverter<Double, Double>.TBroadcastProc): TState function(const Value: Double; const Broadcast: TConverter<Double, Double>.TBroadcastProc): TState
@@ -182,7 +184,7 @@ begin
end end
); );
pnl.Sender.Link(equity); positionManager.Sender.Link(equity);
Result := TConverter<TDataPoint<TOhlcItem>, Double>.Construct(ticker, equity.Sender); Result := TConverter<TDataPoint<TOhlcItem>, Double>.Construct(ticker, equity.Sender);
end; end;