Data Types

This commit is contained in:
Michael Schimmel
2025-08-25 10:26:48 +02:00
parent 75675b8dc1
commit 27f1cc5486
20 changed files with 2670 additions and 34 deletions
+503
View File
@@ -0,0 +1,503 @@
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.