504 lines
20 KiB
ObjectPascal
504 lines
20 KiB
ObjectPascal
unit Strategy2;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Classes,
|
|
Myc.Signals,
|
|
Myc.Data.Pipeline,
|
|
Myc.Trade.Types,
|
|
Myc.FMX.Chart,
|
|
Myc.Trade.Pipeline;
|
|
|
|
function CreateStrategy2(
|
|
const Ticker: TProducer<TDataPoint<TOhlcItem>>;
|
|
const Log: TStrings;
|
|
const Chart1H, Chart24H: TMycChart
|
|
): TProducer<Double>;
|
|
|
|
implementation
|
|
|
|
uses
|
|
System.SysUtils,
|
|
System.Rtti,
|
|
System.Math,
|
|
System.Generics.Collections,
|
|
System.Types,
|
|
System.UITypes,
|
|
FMX.Types,
|
|
Myc.Trade.Indicators.Common,
|
|
Myc.Data.Records;
|
|
|
|
// Calculates the difference between the current value and the value N periods ago.
|
|
// (Value[0] - Value[N])
|
|
function Delta(N: Integer): TConverter<Double, Double>;
|
|
var
|
|
history: TQueue<Double>; // Captured state for the history of values
|
|
begin
|
|
if N <= 0 then
|
|
raise EArgumentException.Create('Delta period N must be positive.');
|
|
|
|
history := TQueue<Double>.Create;
|
|
|
|
Result :=
|
|
TConverter<Double, Double>.CreateAggregation(
|
|
// This function is the core of the aggregator. It is called for each value.
|
|
function(const Value: Double; const Broadcast: TBroadcastFunc<Double>): TState
|
|
var
|
|
deltaValue, oldestValue: Double;
|
|
begin
|
|
// Add current value to the history
|
|
history.Enqueue(Value);
|
|
|
|
// If the history is larger than needed, remove the oldest element.
|
|
// We need N+1 elements to have the current and the Nth previous value.
|
|
if history.Count > (N + 1) then
|
|
history.Dequeue;
|
|
|
|
// Calculate delta if we have enough data
|
|
if history.Count = (N + 1) then
|
|
begin
|
|
oldestValue := history.Peek; // Oldest value is at the front
|
|
deltaValue := Value - oldestValue;
|
|
Broadcast(deltaValue);
|
|
end
|
|
else
|
|
begin
|
|
// Not enough data yet, broadcast a neutral value.
|
|
Broadcast(0.0);
|
|
end;
|
|
Result := TState.Null;
|
|
end
|
|
);
|
|
end;
|
|
|
|
type
|
|
// Defines the direction of the slope between two points
|
|
TSlope = (sFalling, sFlat, sRising);
|
|
|
|
// Represents the last found high and low point in a series.
|
|
TExtrema = record
|
|
High: Double;
|
|
Low: Double;
|
|
constructor Create(AHigh, ALow: Double);
|
|
end;
|
|
|
|
constructor TExtrema.Create(AHigh, ALow: Double);
|
|
begin
|
|
High := AHigh;
|
|
Low := ALow;
|
|
end;
|
|
|
|
// Finds the last peak/trough. A peak is a value surrounded by N lower values on both sides.
|
|
// This introduces a signal lag of N periods.
|
|
function ExtremaFinder(N: Integer): TConverter<Double, TExtrema>;
|
|
var
|
|
// Captured state for the aggregator
|
|
history: TQueue<Double>;
|
|
lastHigh, lastLow: Double;
|
|
initialized: Boolean;
|
|
begin
|
|
if N <= 0 then
|
|
raise EArgumentException.Create('ExtremaFinder period N must be positive.');
|
|
|
|
initialized := False;
|
|
history := TQueue<Double>.Create;
|
|
lastHigh := 0;
|
|
lastLow := 0;
|
|
|
|
Result :=
|
|
TConverter<Double, TExtrema>.CreateAggregation(
|
|
function(const Value: Double; const Broadcast: TBroadcastFunc<TExtrema>): TState
|
|
var
|
|
data: TArray<Double>;
|
|
centerValue: Double;
|
|
isPeak, isTrough: Boolean;
|
|
i: Integer;
|
|
begin
|
|
history.Enqueue(Value);
|
|
// Keep the history buffer at the required size for the sliding window
|
|
if history.Count > (2 * N + 1) then
|
|
history.Dequeue;
|
|
|
|
// Initialize extrema with the first value once the buffer is full
|
|
if not initialized and (history.Count = (2 * N + 1)) then
|
|
begin
|
|
lastHigh := history.Peek;
|
|
lastLow := history.Peek;
|
|
initialized := True;
|
|
end;
|
|
|
|
// Only proceed if the window is full
|
|
if history.Count = (2 * N + 1) then
|
|
begin
|
|
data := history.ToArray;
|
|
centerValue := data[N]; // The value to be tested is in the middle of the window
|
|
|
|
// Test for a peak (center value is highest in the window)
|
|
isPeak := True;
|
|
for i := 0 to High(data) do
|
|
begin
|
|
if i <> N then
|
|
begin
|
|
if centerValue <= data[i] then
|
|
begin
|
|
isPeak := False;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if isPeak then
|
|
lastHigh := centerValue;
|
|
|
|
// Test for a trough (center value is lowest in the window)
|
|
isTrough := True;
|
|
if not isPeak then // Small optimization
|
|
begin
|
|
for i := 0 to High(data) do
|
|
begin
|
|
if i <> N then
|
|
begin
|
|
if centerValue >= data[i] then
|
|
begin
|
|
isTrough := False;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
isTrough := False;
|
|
|
|
if isTrough then
|
|
lastLow := centerValue;
|
|
end;
|
|
|
|
// Broadcast the latest known extrema.
|
|
if initialized then
|
|
Broadcast(TExtrema.Create(lastHigh, lastLow));
|
|
|
|
Result := TState.Null;
|
|
end
|
|
);
|
|
end;
|
|
|
|
type
|
|
TValueHelper = record helper for TValue
|
|
class function AsValue<T>(const P: TProducer<T>): TProducer<TValue>; static;
|
|
end;
|
|
|
|
// Helper to convert a producer of any type T into a producer of TValue.
|
|
class function TValueHelper.AsValue<T>(const P: TProducer<T>): TProducer<TValue>;
|
|
begin
|
|
Result := P.Chain<TValue>(function(const V: T): TValue begin Result := TValue.From<T>(V); end);
|
|
end;
|
|
|
|
function CreateStrategy2(
|
|
const Ticker: TProducer<TDataPoint<TOhlcItem>>;
|
|
const Log: TStrings;
|
|
const Chart1H, Chart24H: TMycChart
|
|
): TProducer<Double>;
|
|
const
|
|
// Timeframe independent constants
|
|
HMA_BIAS_PERIOD = 250;
|
|
SMA_BIAS_PERIOD = 200;
|
|
ATR_PERIOD = 50;
|
|
ATR_FILTER_MULTIPLIER = 3.0;
|
|
ATR_SL_MULTIPLIER = 4.0;
|
|
|
|
// 1H specific constants
|
|
HMA_ENTRY_PERIOD = 20;
|
|
|
|
type
|
|
// Represents the current state of the trading logic
|
|
TTradeStatus = (tsFlat, tsLong, tsShort);
|
|
|
|
// Holds all necessary data for the stateful trade management aggregator
|
|
TTradeState = record
|
|
Status: TTradeStatus;
|
|
EntryPrice: Double;
|
|
StopLoss: Double;
|
|
TakeProfit: Double;
|
|
end;
|
|
|
|
var
|
|
{$region 'Producers for Indicators and Logic Signals'}
|
|
// Timeframe specific producers
|
|
Ohlc1H, Ohlc24H: TProducer<TDataPoint<TOhlcItem>>;
|
|
Close1H, Close24H: TProducer<Double>;
|
|
Time1H, Time24H: TProducer<TDateTime>;
|
|
|
|
// 24H Indicators
|
|
Hma250_24H, Sma200_24H, Atr50_24H: TProducer<Double>;
|
|
// 1H Indicators
|
|
Hma250_1H, Sma200_1H, Atr50_1H, Hma20_1H: TProducer<Double>;
|
|
|
|
// Bias producers
|
|
Bias24H_Bullish, Bias24H_Bearish: TProducer<Boolean>;
|
|
Bias1H_Bullish, Bias1H_Bearish: TProducer<Boolean>;
|
|
OverallBias_Bullish, OverallBias_Bearish: TProducer<Boolean>;
|
|
|
|
// Filter producers
|
|
Filter24H, Filter1H, TradeAllowed: TProducer<Boolean>;
|
|
|
|
// Entry producers
|
|
Hma20_Extrema: TProducer<TExtrema>;
|
|
LastLow_HMA20, LastHigh_HMA20: TProducer<Double>;
|
|
TriggerBuy, TriggerShort: TProducer<Boolean>;
|
|
GoLong, GoShort: TProducer<Boolean>;
|
|
{$endregion}
|
|
begin
|
|
{$region 'Helper function implementations'}
|
|
var All :=
|
|
function(const Prods: TArray<TProducer<Boolean>>): TProducer<Boolean>
|
|
begin
|
|
// TConverter.Join combines multiple producers of the same type into a producer of an array of that type.
|
|
Result :=
|
|
TConverter
|
|
.Join<Boolean>(TConverter.TJoinMode.jmAll, Prods)
|
|
.Chain<Boolean>(
|
|
function(const V: TArray<Boolean>): Boolean
|
|
var
|
|
i: Integer;
|
|
begin
|
|
// The output is true only if all input booleans are true.
|
|
Result := True;
|
|
for i := 0 to High(V) do
|
|
begin
|
|
if not V[i] then
|
|
begin
|
|
Result := False;
|
|
Exit;
|
|
end;
|
|
end;
|
|
end);
|
|
end;
|
|
|
|
var IsRising :=
|
|
function(const P: TProducer<Double>): TProducer<Boolean>
|
|
begin
|
|
// A series is rising if the difference to its previous value is positive.
|
|
// Assumes the existence of a Delta(1) converter.
|
|
Result := P.Chain<Double>(Delta(1)).Chain<Boolean>(function(const D: Double): Boolean begin Result := D > 0; end);
|
|
end;
|
|
|
|
var IsFalling :=
|
|
function(const P: TProducer<Double>): TProducer<Boolean>
|
|
begin
|
|
// A series is falling if the difference to its previous value is negative.
|
|
Result := P.Chain<Double>(Delta(1)).Chain<Boolean>(function(const D: Double): Boolean begin Result := D < 0; end);
|
|
end;
|
|
{$endregion}
|
|
|
|
{$region '1. Timeframe Aggregation & Chart Setup'}
|
|
// Aggregate Ticker data to 1H and 24H timeframes using a trade-specific converter.
|
|
Ohlc1H := Ticker.Chain<TDataPoint<TOhlcItem>>(TTradeConverter.CreateOhlcAggregation(TTimeframe.H));
|
|
Ohlc24H := Ticker.Chain<TDataPoint<TOhlcItem>>(TTradeConverter.CreateOhlcAggregation(TTimeframe.D));
|
|
|
|
var Ohlc1HData := Ohlc1H.Field<TOhlcItem>('Data');
|
|
var Ohlc24HData := Ohlc24H.Field<TOhlcItem>('Data');
|
|
|
|
// Extract relevant data fields (Close price and Time) from the aggregated data points.
|
|
// The Field helper uses RTTI to access nested record fields.
|
|
Close1H := Ohlc1HData.Field<Double>('Close');
|
|
Time1H := Ohlc1H.Field<TDateTime>('Time');
|
|
Close24H := Ohlc24HData.Field<Double>('Close');
|
|
Time24H := Ohlc24H.Field<TDateTime>('Time');
|
|
|
|
// Setup the 1H chart
|
|
Chart1H.SetXAxisSeries(TTimeframe.H, Time1H);
|
|
var Panel1H_Main := Chart1H.AddPanel;
|
|
Panel1H_Main.AddOhlcSeries(Ohlc1HData);
|
|
var Panel1H_ATR := Chart1H.AddPanel;
|
|
Panel1H_ATR.Weight := 0.25;
|
|
|
|
// Setup the 24H chart
|
|
Chart24H.SetXAxisSeries(TTimeframe.D, Time24H);
|
|
var Panel24H_Main := Chart24H.AddPanel;
|
|
Panel24H_Main.AddOhlcSeries(Ohlc24HData);
|
|
var Panel24H_ATR := Chart24H.AddPanel;
|
|
Panel24H_ATR.Weight := 0.25;
|
|
{$endregion}
|
|
|
|
{$region '2. Indicator Calculation'}
|
|
// Calculate indicators for the 24H timeframe and add them to the chart.
|
|
Hma250_24H := Close24H.Chain<Double>(THMA.CreateHMA(HMA_BIAS_PERIOD));
|
|
Sma200_24H := Close24H.Chain<Double>(TSMA.CreateSMA(SMA_BIAS_PERIOD));
|
|
// ATR is calculated from OHLC data, not just the close price.
|
|
Atr50_24H := Ohlc24H.Field<TOhlcItem>('Data').Chain<Double>(TATR.CreateATR(ATR_PERIOD));
|
|
|
|
Panel24H_Main.AddDoubleSeries(Hma250_24H, TAlphaColors.Aqua);
|
|
Panel24H_Main.AddDoubleSeries(Sma200_24H, TAlphaColors.Orange);
|
|
Panel24H_ATR.AddDoubleSeries(Atr50_24H, TAlphaColors.Magenta);
|
|
|
|
// Calculate indicators for the 1H timeframe and add them to the chart.
|
|
Hma250_1H := Close1H.Chain<Double>(THMA.CreateHMA(HMA_BIAS_PERIOD));
|
|
Sma200_1H := Close1H.Chain<Double>(TSMA.CreateSMA(SMA_BIAS_PERIOD));
|
|
Atr50_1H := Ohlc1H.Field<TOhlcItem>('Data').Chain<Double>(TATR.CreateATR(ATR_PERIOD));
|
|
Hma20_1H := Close1H.Chain<Double>(THMA.CreateHMA(HMA_ENTRY_PERIOD));
|
|
|
|
Panel1H_Main.AddDoubleSeries(Hma250_1H, TAlphaColors.Aqua);
|
|
Panel1H_Main.AddDoubleSeries(Sma200_1H, TAlphaColors.Orange);
|
|
Panel1H_ATR.AddDoubleSeries(Atr50_1H, TAlphaColors.Magenta);
|
|
Panel1H_Main.AddDoubleSeries(Hma20_1H, TAlphaColors.Yellow, 2.0);
|
|
{$endregion}
|
|
|
|
{$region '3. Bias and Filter Logic'}
|
|
// Determine bullish/bearish bias for each timeframe based on moving average slopes.
|
|
Bias24H_Bullish := All([IsRising(Hma250_24H), IsRising(Sma200_24H)]);
|
|
Bias24H_Bearish := All([IsFalling(Hma250_24H), IsFalling(Sma200_24H)]);
|
|
|
|
Bias1H_Bullish := All([IsRising(Hma250_1H), IsRising(Sma200_1H)]);
|
|
Bias1H_Bearish := All([IsFalling(Hma250_1H), IsFalling(Sma200_1H)]);
|
|
|
|
// Determine overall bias: both timeframes must agree.
|
|
OverallBias_Bullish := All([Bias24H_Bullish, Bias1H_Bullish]);
|
|
OverallBias_Bearish := All([Bias24H_Bearish, Bias1H_Bearish]);
|
|
|
|
// Define the ATR filter logic.
|
|
var GetFilter :=
|
|
function(const Hma, Sma, Atr: TProducer<Double>): TProducer<Boolean>
|
|
begin
|
|
var Dist :=
|
|
TConverter
|
|
.Join<Double>(jmAll, [Hma, Sma])
|
|
.Chain<Double>(function(const V: TArray<Double>): Double begin Result := Abs(V[0] - V[1]); end);
|
|
|
|
var Threshold := Atr.Chain<Double>(function(const V: Double): Double begin Result := V * ATR_FILTER_MULTIPLIER; end);
|
|
|
|
Result :=
|
|
TConverter
|
|
.Join<Double>(jmAll, [Dist, Threshold])
|
|
.Chain<Boolean>(function(const V: TArray<Double>): Boolean begin Result := V[0] > V[1]; end);
|
|
end;
|
|
|
|
// A trade is only allowed if the HMA/SMA distance is wide enough on both timeframes.
|
|
Filter24H := GetFilter(Hma250_24H, Sma200_24H, Atr50_24H);
|
|
Filter1H := GetFilter(Hma250_1H, Sma200_1H, Atr50_1H);
|
|
TradeAllowed := All([Filter1H, Filter24H]);
|
|
{$endregion}
|
|
|
|
{$region '4. Entry Logic'}
|
|
// Find the last high and low points of the 20-period HMA on the 1H chart.
|
|
// Assumes an ExtremaFinder converter that returns a TExtrema record.
|
|
Hma20_Extrema := Hma20_1H.Chain<TExtrema>(ExtremaFinder(1));
|
|
LastLow_HMA20 := Hma20_Extrema.Field<Double>('Low');
|
|
LastHigh_HMA20 := Hma20_Extrema.Field<Double>('High');
|
|
|
|
// Define entry triggers: price crossing below the last low (for longs) or above the last high (for shorts).
|
|
TriggerBuy :=
|
|
TConverter
|
|
.Join<Double>(jmAll, [Close1H, LastLow_HMA20])
|
|
.Chain<Boolean>(function(const V: TArray<Double>): Boolean begin Result := V[0] < V[1]; end);
|
|
|
|
TriggerShort :=
|
|
TConverter
|
|
.Join<Double>(jmAll, [Close1H, LastHigh_HMA20])
|
|
.Chain<Boolean>(function(const V: TArray<Double>): Boolean begin Result := V[0] > V[1]; end);
|
|
|
|
// Combine all conditions for the final entry signals.
|
|
GoLong := All([OverallBias_Bullish, TradeAllowed, TriggerBuy]);
|
|
GoShort := All([OverallBias_Bearish, TradeAllowed, TriggerShort]);
|
|
{$endregion}
|
|
|
|
{$region '5. Trade Management via Aggregation'}
|
|
// The core state machine of the strategy. It's implemented as an aggregate function
|
|
// that captures a state record and processes a stream of combined input data.
|
|
var tradeState: TTradeState;
|
|
tradeState.Status := tsFlat;
|
|
tradeState.EntryPrice := 0;
|
|
tradeState.StopLoss := 0;
|
|
tradeState.TakeProfit := 0;
|
|
|
|
// The aggregator function processes an array of TValue, where each element
|
|
// corresponds to an input producer in a defined order.
|
|
var aggregatorFunc: TAggregateFunc<TArray<TValue>, Double> :=
|
|
function(const Value: TArray<TValue>; const Broadcast: TBroadcastFunc<Double>): TState
|
|
var
|
|
goLong, goShort: Boolean;
|
|
close, atr, lastHigh, lastLow: Double;
|
|
begin
|
|
// Not enough data, or data types are incorrect -> do nothing.
|
|
if (Length(Value) <> 6) or not Value[0].IsType<Boolean> then
|
|
begin
|
|
Result := TState.Null;
|
|
Exit;
|
|
end;
|
|
|
|
// Extract current values from the TValue array by index.
|
|
// This order must match the order in the 'producers' array below.
|
|
goLong := Value[0].AsBoolean;
|
|
goShort := Value[1].AsBoolean;
|
|
close := Value[2].AsExtended;
|
|
atr := Value[3].AsExtended;
|
|
lastHigh := Value[4].AsExtended;
|
|
lastLow := Value[5].AsExtended;
|
|
|
|
// The state machine logic for trade management remains identical.
|
|
case tradeState.Status of
|
|
tsFlat:
|
|
begin
|
|
if goLong then
|
|
begin
|
|
tradeState.Status := tsLong;
|
|
tradeState.EntryPrice := close;
|
|
tradeState.TakeProfit := lastHigh;
|
|
tradeState.StopLoss := close - atr * ATR_SL_MULTIPLIER;
|
|
end
|
|
else if goShort then
|
|
begin
|
|
tradeState.Status := tsShort;
|
|
tradeState.EntryPrice := close;
|
|
tradeState.TakeProfit := lastLow;
|
|
tradeState.StopLoss := close + atr * ATR_SL_MULTIPLIER;
|
|
end;
|
|
end;
|
|
tsLong:
|
|
begin
|
|
var newSL := close - atr * ATR_SL_MULTIPLIER;
|
|
if (newSL > tradeState.StopLoss) then
|
|
tradeState.StopLoss := newSL;
|
|
|
|
if (close >= tradeState.TakeProfit) or (close <= tradeState.StopLoss) then
|
|
tradeState.Status := tsFlat;
|
|
end;
|
|
tsShort:
|
|
begin
|
|
var newSL := close + atr * ATR_SL_MULTIPLIER;
|
|
if (newSL < tradeState.StopLoss) then
|
|
tradeState.StopLoss := newSL;
|
|
|
|
if (close <= tradeState.TakeProfit) or (close >= tradeState.StopLoss) then
|
|
tradeState.Status := tsFlat;
|
|
end;
|
|
end;
|
|
|
|
// Broadcast the current position status.
|
|
Broadcast(Integer(tradeState.Status) - Integer(tsLong));
|
|
Result := TState.Null;
|
|
end;
|
|
|
|
// To feed the aggregator, combine all required data streams into one.
|
|
// 1. Convert each producer to TProducer<TValue>.
|
|
var producers: TArray<TProducer<TValue>> :=
|
|
[
|
|
TValue.AsValue<Boolean>(GoLong),
|
|
TValue.AsValue<Boolean>(GoShort),
|
|
TValue.AsValue<Double>(Close1H),
|
|
TValue.AsValue<Double>(Atr50_1H),
|
|
TValue.AsValue<Double>(LastHigh_HMA20),
|
|
TValue.AsValue<Double>(LastLow_HMA20)
|
|
];
|
|
|
|
// 2. Join them into a single producer of an array of TValue.
|
|
var combinedProducer := TConverter.Join<TValue>(jmAll, producers);
|
|
|
|
// 3. Create the aggregator converter and chain it to the combined producer.
|
|
var strategyAggregator := TConverter<TArray<TValue>, Double>.CreateAggregation(aggregatorFunc);
|
|
Result := combinedProducer.Chain<Double>(strategyAggregator);
|
|
{$endregion}
|
|
end;
|
|
|
|
end.
|