Files
MycLib/AuraTrader/StrategyTest.pas
T
2025-07-25 08:58:05 +02:00

190 lines
7.3 KiB
ObjectPascal

unit StrategyTest;
interface
uses
Myc.Signals,
Myc.Trade.Types,
Myc.Trade.DataArray,
Myc.DataFlow,
Myc.DataRecord,
Myc.Trade.Indicators;
function CreateStrategy1(Timeframe: TTimeframe): TConverter<TDataPoint<TOhlcItem>, Double>; overload;
implementation
uses
System.SysUtils,
System.Math;
function CreateStrategy1(Timeframe: TTimeframe): TConverter<TDataPoint<TOhlcItem>, Double>;
type
// A record to transfer a detected signal event and the required price data to the next stage.
TSignalEvent = record
Signal: Integer; // -1 for short, 1 for long, 0 for no new signal
Close, Low, High, ATR: Double;
InitialSL: Double; // The calculated SL (Highest/Lowest) at the time of the signal
end;
begin
var ticker := TConverter.CreateIdentity<TDataPoint<TOhlcItem>>;
var OhlcPoint := ticker.Producer.Chain<TDataPoint<TOhlcItem>>(TConverter.CreateOhlcAggregation(Timeframe));
var Ohlc := OhlcPoint.Field<TOhlcItem>('Data');
var Closes := Ohlc.Field<Double>('Close');
var Hull := Closes.Chain<Double>(TIndicators.CreateHMA(250)).MakeParallel;
var Sma := Closes.Chain<Double>(TIndicators.CreateSMA(200)).MakeParallel;
var ATR := Ohlc.Chain<Double>(TIndicators.CreateATR(50)).MakeParallel;
var conv := TConverter.Join<Double>([Ohlc.Field<Double>('Low'), Ohlc.Field<Double>('High'), Closes, ATR, Hull, Sma]);
// STAGE 1: Signal Generation. This converter is stateless regarding the trade itself.
// It only detects the crossover event and prepares the data for the next stage.
var Lowest: Double := Double.MaxValue;
var Highest: Double := Double.MinValue;
var lastHull, lastSma: Double;
var signalGenerator :=
conv.Chain<TSignalEvent>(
TConverter<TArray<Double>, TSignalEvent>.CreateConverter(
function(const Values: TArray<Double>): TSignalEvent
begin
Result.Low := Values[0];
Result.High := Values[1];
Result.Close := Values[2];
Result.ATR := Values[3];
var hull := Values[4];
var sma := Values[5];
if Result.Low < Lowest then
Lowest := Result.Low;
if Result.High > Highest then
Highest := Result.High;
Result.Signal := 0;
Result.InitialSL := Double.NaN;
if (hull < sma) and (lastHull >= lastSma) then
begin
Result.Signal := -1;
Result.InitialSL := Highest;
Highest := Double.MinValue; // Reset for next trend
Lowest := Double.MaxValue;
end
else if (hull > sma) and (lastHull <= lastSma) then
begin
Result.Signal := 1;
Result.InitialSL := Lowest;
Highest := Double.MinValue; // Reset for next trend
Lowest := Double.MaxValue;
end;
lastHull := hull;
lastSma := sma;
end
)
);
// 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 positionManager :=
signalGenerator.Chain<Double>(
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
)
);
// The final equity calculation remains the same, it just consumes the PnL from the position manager
var FEquity: Double := 10000;
var FInit: Boolean := false;
var equity :=
positionManager.Chain<Double>(
TConverter<Double, Double>.CreateAggregation(
function(const Value: Double; const Broadcast: TConverter<Double, Double>.TBroadcastProc): TState
begin
if not FInit then
begin
FInit := true;
Broadcast(FEquity);
end;
if not IsNan(Value) then
begin
FEquity := FEquity + Value;
Result := Broadcast(FEquity);
end;
end
)
);
Result := TConverter<TDataPoint<TOhlcItem>, Double>.Construct(ticker.Consumer, equity);
end;
end.