Schmetterlingsmuster-Strategie
Überblick
Die Butterfly-Pattern-Strategie wandelt die ursprüngliche harmonische Musterlogik von MetaTrader „Cypher EA“ in die hohe Ebene API von StockSharp um. Die Strategie durchsucht eine konfigurierbare Kerzenreihe nach bullischen und bärischen Schmetterlingsformationen, validiert die harmonischen Verhältnisse und eröffnet Marktpositionen mit drei abgestuften Take-Profit-Zielen. Optionale Risikomanagementfunktionen spiegeln den MetaTrader-Experten wider: Break-Even-Sperre und Trailing-Stop-Updates sind nach Teilausstiegen verfügbar.
Wie es funktioniert
- Kerzen werden gepuffert, bis ein Pivotpunkt mithilfe des Fensters
PivotLeft/PivotRightbestätigt werden kann. - Wenn fünf alternierende Pivots verfügbar sind, prüft die Strategie die Fibonacci-Verhältnisse, die für ein Schmetterlingsmuster erforderlich sind.
- Qualifizierte Setups werden erneut validiert (optional) und anhand eines harmonischen Qualitätsfaktors (
MinPatternQuality) bewertet. - Sobald ein Muster bei einer geschlossenen Kerze bestätigt wird:
- Eine Marktorder wird entweder mit festem Volumen oder mit risikobasierter Größenbestimmung platziert.
- Das Positionsvolumen ist auf drei Take-Profit-Level (
TP1/TP2/TP3) aufgeteilt. - Aus der Musterstruktur wird ein geometrischer Stop-Loss abgeleitet.
- Während der Lebensdauer der Position überwacht die Strategie Kerzen, um Teilausstiege, Break-even-Sperrungen und nachlaufende Anpassungen gemäß den konfigurierten Schwellenwerten auszulösen.
Tipp: Die MetaTrader-Version funktioniert mit mehreren Zeitrahmen gleichzeitig. Um dieses Verhalten in StockSharp zu reproduzieren, starten Sie mehrere Instanzen der Strategie mit unterschiedlichen
CandleType-Werten.
Schlüsselparameter
| Parameter | Beschreibung |
|---|---|
CandleType |
Zeitrahmen zur Erkennung von Pivots und Mustern. |
PivotLeft / PivotRight |
Anzahl der Kerzen links/rechts, die erforderlich sind, um ein Pivot-Hoch/Tief zu bestätigen. |
Tolerance |
Maximal zulässige Abweichung des harmonischen Verhältnisses bei der Validierung des Schmetterlingsmusters. |
AllowTrading |
Aktiviert oder deaktiviert die Auftragsgenerierung nach einer Musterbestätigung. |
UseFixedVolume / FixedVolume |
Erzwingt ein konstantes Handelsvolumen. Wenn die Strategie deaktiviert ist, werden die Positionen über RiskPercent dimensioniert. |
RiskPercent |
Prozentsatz des pro Trade riskierten Portfoliowerts (wird nur verwendet, wenn UseFixedVolume falsch ist). |
AdjustLotsForTakeProfits |
Normalisiert die Teilvolumina, um sicherzustellen, dass die Summe der Eintragsgröße entspricht. |
Tp1Percent / Tp2Percent / Tp3Percent |
Verteilung des Gesamtvolumens auf die drei Take-Profit-Ebenen. |
MinPatternQuality |
Mindestharmonische Punktzahl (0–1), die erforderlich ist, um ein erkanntes Muster zu akzeptieren. |
UseSessionFilter, SessionStartHour, SessionEndHour |
Beschränken Sie den Handel auf ein bestimmtes Börsensitzungsfenster. |
RevalidatePattern |
Erzwingt eine zweite Preisprüfung vor der Eröffnung einer Position. |
UseBreakEven, BreakEvenAfterTp, BreakEvenTrigger, BreakEvenProfit |
Steuert die Break-Even-Aktivierung nach dem angegebenen Take-Profit-Level und dem zusätzlichen Gewinnpuffer. |
UseTrailingStop, TrailAfterTp, TrailStart, TrailStep |
Ermöglicht Trailing Stops, sobald ein Take-Profit-Level erreicht wurde und die minimale günstige Abweichung erreicht ist. |
Risikomanagement
- Stop-Loss-, Break-Even- und Trailing-Levels werden intern verwaltet, ohne dass zusätzliche Aufträge erstellt werden. Teilausstiege und Stop-Closings werden mit Marktaufträgen ausgelöst, um die MetaTrader-Logik zu emulieren.
- Wenn
UseFixedVolumedeaktiviert ist, wird die Positionsgröße aus der Stoppdistanz, dem Tick-Wert des Instruments und der EinstellungRiskPercentberechnet.
Nutzungshinweise
- Stellen Sie sicher, dass das angeschlossene Instrument die konfigurierte
CandleType- und Preisstufe unterstützt, andernfalls kann die Validierungslogik Signale aufgrund von Mindestabstandsprüfungen zurückweisen. - Für Break-Even- und Trailing-Funktionen müssen die jeweiligen Take-Profit-Levels gefüllt sein (
BreakEvenAfterTpundTrailAfterTp). - Mehrere Strategieinstanzen können gleichzeitig auf verschiedenen Wertpapieren oder Zeitrahmen ausgeführt werden, um das Scannen mehrerer Zeitrahmen des ursprünglichen EA zu reproduzieren.
namespace StockSharp.Samples.Strategies;
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Detects bullish and bearish butterfly harmonic patterns on a configurable timeframe.
/// Distributes positions across three take-profit levels and supports optional break-even
/// and trailing-stop management.
/// </summary>
public class ButterflyPatternStrategy : Strategy
{
private sealed class Pivot
{
public Pivot(DateTimeOffset time, decimal price, bool isHigh)
{
Time = time;
Price = price;
IsHigh = isHigh;
}
public DateTimeOffset Time { get; }
public decimal Price { get; }
public bool IsHigh { get; }
}
private sealed class PatternState
{
private readonly List<ICandleMessage> _candles = new();
private readonly List<Pivot> _pivots = new();
public Sides? Side { get; set; }
public decimal RemainingVolume { get; set; }
public decimal Lot1 { get; set; }
public decimal Lot2 { get; set; }
public decimal Lot3 { get; set; }
public bool Tp1Filled { get; set; }
public bool Tp2Filled { get; set; }
public bool Tp3Filled { get; set; }
public decimal? EntryPrice { get; set; }
public decimal? StopPrice { get; set; }
public decimal Tp1Price { get; set; }
public decimal Tp2Price { get; set; }
public decimal Tp3Price { get; set; }
public bool BreakEvenApplied { get; set; }
public bool TrailingActivated { get; set; }
public DateTimeOffset? LastPatternTime { get; set; }
public void ResetPosition()
{
Side = null;
RemainingVolume = 0m;
Lot1 = 0m;
Lot2 = 0m;
Lot3 = 0m;
Tp1Filled = false;
Tp2Filled = false;
Tp3Filled = false;
EntryPrice = null;
StopPrice = null;
Tp1Price = 0m;
Tp2Price = 0m;
Tp3Price = 0m;
BreakEvenApplied = false;
TrailingActivated = false;
}
public void ResetSeries()
{
ResetPosition();
_candles.Clear();
_pivots.Clear();
LastPatternTime = null;
}
public void AddCandle(ICandleMessage candle)
{
_candles.Add(candle);
}
public bool TryExtractPivot(int left, int right, out Pivot pivot)
{
pivot = default;
var required = left + right + 1;
if (_candles.Count < required)
return false;
var index = _candles.Count - 1 - right;
if (index < left)
return false;
var middle = _candles[index];
if (middle == null)
return false;
var isHigh = true;
var isLow = true;
var from = index - left;
var to = index + right;
for (var i = from; i <= to; i++)
{
if (i < 0 || i >= _candles.Count)
continue;
if (i == index)
continue;
var c = _candles[i];
if (c == null)
continue;
if (c.HighPrice > middle.HighPrice)
isHigh = false;
if (c.LowPrice < middle.LowPrice)
isLow = false;
}
if (!isHigh && !isLow)
return false;
pivot = new Pivot(middle.OpenTime, isHigh ? middle.HighPrice : middle.LowPrice, isHigh);
if (_candles.Count > required)
_candles.RemoveAt(0);
return true;
}
public void AddPivot(Pivot pivot)
{
_pivots.Add(pivot);
if (_pivots.Count > 5)
_pivots.RemoveAt(0);
}
public bool TryGetPattern(out Pivot x, out Pivot a, out Pivot b, out Pivot c, out Pivot d)
{
x = default;
a = default;
b = default;
c = default;
d = default;
if (_pivots.Count < 5)
return false;
x = _pivots[^5];
a = _pivots[^4];
b = _pivots[^3];
c = _pivots[^2];
d = _pivots[^1];
return true;
}
}
private PatternState _state;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _pivotLeft;
private readonly StrategyParam<int> _pivotRight;
private readonly StrategyParam<decimal> _tolerance;
private readonly StrategyParam<bool> _allowTrading;
private readonly StrategyParam<bool> _useFixedVolume;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<bool> _adjustLots;
private readonly StrategyParam<decimal> _tp1Percent;
private readonly StrategyParam<decimal> _tp2Percent;
private readonly StrategyParam<decimal> _tp3Percent;
private readonly StrategyParam<decimal> _minPatternQuality;
private readonly StrategyParam<bool> _useSessionFilter;
private readonly StrategyParam<int> _sessionStartHour;
private readonly StrategyParam<int> _sessionEndHour;
private readonly StrategyParam<bool> _revalidatePattern;
private readonly StrategyParam<bool> _useBreakEven;
private readonly StrategyParam<int> _breakEvenAfterTp;
private readonly StrategyParam<decimal> _breakEvenTrigger;
private readonly StrategyParam<decimal> _breakEvenProfit;
private readonly StrategyParam<bool> _useTrailingStop;
private readonly StrategyParam<int> _trailAfterTp;
private readonly StrategyParam<decimal> _trailStart;
private readonly StrategyParam<decimal> _trailStep;
public ButterflyPatternStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for pattern detection", "General");
_pivotLeft = Param(nameof(PivotLeft), 1)
.SetGreaterThanZero()
.SetDisplay("Pivot Left", "Bars to the left when validating a pivot", "Pattern");
_pivotRight = Param(nameof(PivotRight), 1)
.SetGreaterThanZero()
.SetDisplay("Pivot Right", "Bars to the right when validating a pivot", "Pattern");
_tolerance = Param(nameof(Tolerance), 0.50m)
.SetGreaterThanZero()
.SetDisplay("Ratio Tolerance", "Maximum deviation allowed for Fibonacci ratios", "Pattern");
_allowTrading = Param(nameof(AllowTrading), true)
.SetDisplay("Allow Trading", "Enable order generation when patterns are confirmed", "Trading");
_useFixedVolume = Param(nameof(UseFixedVolume), true)
.SetDisplay("Use Fixed Volume", "Use fixed trade volume instead of risk-based sizing", "Risk");
_fixedVolume = Param(nameof(FixedVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Fixed Volume", "Volume to trade when fixed sizing is active", "Risk");
_riskPercent = Param(nameof(RiskPercent), 1m)
.SetGreaterThanZero()
.SetDisplay("Risk Percent", "Risk per trade as a percentage of portfolio value", "Risk");
_adjustLots = Param(nameof(AdjustLotsForTakeProfits), true)
.SetDisplay("Adjust Lots", "Normalize take-profit allocations to match total volume", "Risk");
_tp1Percent = Param(nameof(Tp1Percent), 50m)
.SetNotNegative()
.SetDisplay("TP1 %", "Share of volume closed at the first take-profit", "Targets");
_tp2Percent = Param(nameof(Tp2Percent), 30m)
.SetNotNegative()
.SetDisplay("TP2 %", "Share of volume closed at the second take-profit", "Targets");
_tp3Percent = Param(nameof(Tp3Percent), 20m)
.SetNotNegative()
.SetDisplay("TP3 %", "Share of volume closed at the third take-profit", "Targets");
_minPatternQuality = Param(nameof(MinPatternQuality), 0.01m)
.SetDisplay("Minimum Quality", "Minimum harmonic score required to trade", "Pattern");
_useSessionFilter = Param(nameof(UseSessionFilter), false)
.SetDisplay("Use Session Filter", "Only trade within configured session hours", "Trading");
_sessionStartHour = Param(nameof(SessionStartHour), 8)
.SetDisplay("Session Start", "Session start hour in exchange time", "Trading");
_sessionEndHour = Param(nameof(SessionEndHour), 16)
.SetDisplay("Session End", "Session end hour in exchange time", "Trading");
_revalidatePattern = Param(nameof(RevalidatePattern), false)
.SetDisplay("Revalidate Pattern", "Confirm that price has not invalidated the setup", "Pattern");
_useBreakEven = Param(nameof(UseBreakEven), false)
.SetDisplay("Use Break-Even", "Enable break-even management", "Risk");
_breakEvenAfterTp = Param(nameof(BreakEvenAfterTp), 1)
.SetGreaterThanZero()
.SetDisplay("Break-Even After TP", "Activate break-even after the specified take-profit", "Risk");
_breakEvenTrigger = Param(nameof(BreakEvenTrigger), 30m)
.SetDisplay("Break-Even Trigger", "Points required to lock break-even", "Risk");
_breakEvenProfit = Param(nameof(BreakEvenProfit), 5m)
.SetDisplay("Break-Even Profit", "Profit offset applied to break-even", "Risk");
_useTrailingStop = Param(nameof(UseTrailingStop), false)
.SetDisplay("Use Trailing", "Enable trailing stop management", "Risk");
_trailAfterTp = Param(nameof(TrailAfterTp), 2)
.SetGreaterThanZero()
.SetDisplay("Trail After TP", "Activate trailing after the specified take-profit", "Risk");
_trailStart = Param(nameof(TrailStart), 20m)
.SetDisplay("Trail Start", "Points required before trailing", "Risk");
_trailStep = Param(nameof(TrailStep), 5m)
.SetDisplay("Trail Step", "Trailing step in price points", "Risk");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int PivotLeft
{
get => _pivotLeft.Value;
set => _pivotLeft.Value = value;
}
public int PivotRight
{
get => _pivotRight.Value;
set => _pivotRight.Value = value;
}
public decimal Tolerance
{
get => _tolerance.Value;
set => _tolerance.Value = value;
}
public bool AllowTrading
{
get => _allowTrading.Value;
set => _allowTrading.Value = value;
}
public bool UseFixedVolume
{
get => _useFixedVolume.Value;
set => _useFixedVolume.Value = value;
}
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
public bool AdjustLotsForTakeProfits
{
get => _adjustLots.Value;
set => _adjustLots.Value = value;
}
public decimal Tp1Percent
{
get => _tp1Percent.Value;
set => _tp1Percent.Value = value;
}
public decimal Tp2Percent
{
get => _tp2Percent.Value;
set => _tp2Percent.Value = value;
}
public decimal Tp3Percent
{
get => _tp3Percent.Value;
set => _tp3Percent.Value = value;
}
public decimal MinPatternQuality
{
get => _minPatternQuality.Value;
set => _minPatternQuality.Value = value;
}
public bool UseSessionFilter
{
get => _useSessionFilter.Value;
set => _useSessionFilter.Value = value;
}
public int SessionStartHour
{
get => _sessionStartHour.Value;
set => _sessionStartHour.Value = value;
}
public int SessionEndHour
{
get => _sessionEndHour.Value;
set => _sessionEndHour.Value = value;
}
public bool RevalidatePattern
{
get => _revalidatePattern.Value;
set => _revalidatePattern.Value = value;
}
public bool UseBreakEven
{
get => _useBreakEven.Value;
set => _useBreakEven.Value = value;
}
public int BreakEvenAfterTp
{
get => _breakEvenAfterTp.Value;
set => _breakEvenAfterTp.Value = value;
}
public decimal BreakEvenTrigger
{
get => _breakEvenTrigger.Value;
set => _breakEvenTrigger.Value = value;
}
public decimal BreakEvenProfit
{
get => _breakEvenProfit.Value;
set => _breakEvenProfit.Value = value;
}
public bool UseTrailingStop
{
get => _useTrailingStop.Value;
set => _useTrailingStop.Value = value;
}
public int TrailAfterTp
{
get => _trailAfterTp.Value;
set => _trailAfterTp.Value = value;
}
public decimal TrailStart
{
get => _trailStart.Value;
set => _trailStart.Value = value;
}
public decimal TrailStep
{
get => _trailStep.Value;
set => _trailStep.Value = value;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
protected override void OnReseted()
{
base.OnReseted();
_state = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_state = new PatternState();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateRiskManagement(candle);
if (!IsWithinSession(candle.OpenTime))
return;
_state.AddCandle(candle);
if (_state.TryExtractPivot(PivotLeft, PivotRight, out var pivot))
{
_state.AddPivot(pivot);
TryDetectPattern(candle);
}
}
private bool IsWithinSession(DateTimeOffset time)
{
if (!UseSessionFilter)
return true;
var hour = time.Hour;
if (SessionStartHour < SessionEndHour)
return hour >= SessionStartHour && hour < SessionEndHour;
return hour >= SessionStartHour || hour < SessionEndHour;
}
private void TryDetectPattern(ICandleMessage candle)
{
if (!_state.TryGetPattern(out var x, out var a, out var b, out var c, out var d))
return;
if (_state.LastPatternTime is DateTimeOffset last && last == d.Time)
return;
var side = DetectPatternType(x, a, b, c, d);
if (side == null)
return;
var quality = AssessPatternQuality(x, a, b, c, d, side.Value);
if (quality < MinPatternQuality)
{
LogInfo($"Pattern discarded: quality {quality:F3} below threshold {MinPatternQuality:F3}.");
return;
}
if (RevalidatePattern && !RevalidateBeforeTrading(candle.ClosePrice, c.Price, a.Price, x.Price, side.Value))
{
LogInfo("Pattern invalidated by price action.");
return;
}
_state.LastPatternTime = d.Time;
if (!AllowTrading)
{
LogInfo("Trading disabled. Pattern ignored.");
return;
}
if (_state.Side != null && _state.RemainingVolume > 0m)
{
LogInfo("Active position detected. New signal skipped.");
return;
}
ExecutePattern(candle, side.Value, a, c);
}
private Sides? DetectPatternType(Pivot x, Pivot a, Pivot b, Pivot c, Pivot d)
{
var diffBear = x.Price - a.Price;
if (x.IsHigh && !a.IsHigh && b.IsHigh && !c.IsHigh && d.IsHigh && diffBear > 0m)
{
var idealB = a.Price + 0.786m * diffBear;
if (Math.Abs(b.Price - idealB) <= Tolerance * diffBear)
{
var bc = b.Price - c.Price;
if (bc >= 0.1m * diffBear && bc <= 2m * diffBear)
{
var cd = d.Price - c.Price;
if (cd >= 0.5m * diffBear && cd <= 3m * diffBear)
return Sides.Sell;
}
}
}
var diffBull = a.Price - x.Price;
if (!x.IsHigh && a.IsHigh && !b.IsHigh && c.IsHigh && !d.IsHigh && diffBull > 0m)
{
var idealB = a.Price - 0.786m * diffBull;
if (Math.Abs(b.Price - idealB) <= Tolerance * diffBull)
{
var bc = c.Price - b.Price;
if (bc >= 0.1m * diffBull && bc <= 2m * diffBull)
{
var cd = c.Price - d.Price;
if (cd >= 0.5m * diffBull && cd <= 3m * diffBull)
return Sides.Buy;
}
}
}
return null;
}
private decimal AssessPatternQuality(Pivot x, Pivot a, Pivot b, Pivot c, Pivot d, Sides side)
{
var diff = side == Sides.Buy ? a.Price - x.Price : x.Price - a.Price;
if (diff == 0m)
return 0m;
var score = 1m;
var idealB = side == Sides.Buy ? a.Price - 0.786m * diff : a.Price + 0.786m * diff;
var bDeviation = Math.Abs(b.Price - idealB) / diff;
score -= bDeviation * 0.2m;
var idealC = side == Sides.Buy
? b.Price + 0.618m * (a.Price - b.Price)
: b.Price - 0.618m * (b.Price - a.Price);
var cDeviation = Math.Abs(c.Price - idealC) / diff;
score -= cDeviation * 0.2m;
var idealD = side == Sides.Buy
? c.Price - 1.414m * (c.Price - b.Price)
: c.Price + 1.414m * (b.Price - c.Price);
var dDeviation = Math.Abs(d.Price - idealD) / diff;
score -= dDeviation * 0.2m;
var abDuration = (b.Time - a.Time).TotalSeconds;
var cdDuration = (d.Time - c.Time).TotalSeconds;
if (abDuration > 0 && cdDuration > 0)
score -= (decimal)Math.Abs(1.0 - abDuration / cdDuration) * 0.1m;
var xaDuration = (a.Time - x.Time).TotalSeconds;
var bcDuration = (c.Time - b.Time).TotalSeconds;
if (xaDuration > 0 && bcDuration > 0)
score -= (decimal)Math.Abs(1.0 - xaDuration / bcDuration) * 0.1m;
return Math.Max(0m, Math.Min(1m, score));
}
private bool RevalidateBeforeTrading(decimal currentPrice, decimal dPrice, decimal aPrice, decimal xPrice, Sides side)
{
var direction = side == Sides.Buy ? 1m : -1m;
var diff = side == Sides.Buy ? aPrice - xPrice : xPrice - aPrice;
if (diff <= 0m)
return false;
var priceMovement = (currentPrice - dPrice) * direction;
if (priceMovement < 0m)
return false;
return Math.Abs(priceMovement) <= 0.3m * diff;
}
private void ExecutePattern(ICandleMessage candle, Sides side, Pivot a, Pivot c)
{
var entryPrice = candle.ClosePrice;
var tp3 = c.Price;
var diff = side == Sides.Buy ? tp3 - entryPrice : entryPrice - tp3;
if (diff <= 0m)
{
LogInfo("Pattern skipped: invalid take-profit distance.");
return;
}
var tp1 = side == Sides.Buy ? entryPrice + diff / 3m : entryPrice - diff / 3m;
var tp2 = side == Sides.Buy ? entryPrice + diff * 2m / 3m : entryPrice - diff * 2m / 3m;
var stop = side == Sides.Buy ? entryPrice - (tp2 - entryPrice) * 3m : entryPrice + (entryPrice - tp2) * 3m;
var step = Security.PriceStep ?? 0.0001m;
var minDistance = step;
if (Math.Abs(entryPrice - stop) < minDistance || Math.Abs(tp1 - entryPrice) < minDistance || Math.Abs(tp2 - entryPrice) < minDistance || Math.Abs(tp3 - entryPrice) < minDistance)
{
LogInfo("Pattern skipped: protective distances below minimal step.");
return;
}
var volume = CalculatePositionVolume(entryPrice, stop);
if (volume <= 0m)
{
LogInfo("Pattern skipped: volume calculation returned zero.");
return;
}
SplitVolumes(volume, out var lot1, out var lot2, out var lot3);
var total = lot1 + lot2 + lot3;
if (total <= 0m)
{
LogInfo("Pattern skipped: no tradable volume.");
return;
}
var order = side == Sides.Buy ? BuyMarket(total) : SellMarket(total);
if (order == null)
{
LogInfo("Failed to place entry order.");
return;
}
_state.Side = side;
_state.EntryPrice = entryPrice;
_state.StopPrice = stop;
_state.Lot1 = lot1;
_state.Lot2 = lot2;
_state.Lot3 = lot3;
_state.RemainingVolume = total;
_state.Tp1Filled = lot1 <= 0m;
_state.Tp2Filled = lot2 <= 0m;
_state.Tp3Filled = lot3 <= 0m;
_state.Tp1Price = tp1;
_state.Tp2Price = tp2;
_state.Tp3Price = tp3;
_state.BreakEvenApplied = false;
_state.TrailingActivated = false;
LogInfo($"{side} entry at {entryPrice:F5}, stop {stop:F5}, TP1 {tp1:F5}, TP2 {tp2:F5}, TP3 {tp3:F5}, volume {total:F2}.");
}
private decimal CalculatePositionVolume(decimal entryPrice, decimal stopPrice)
{
var minVolume = Security.MinVolume ?? 0.01m;
var maxVolume = Security.MaxVolume ?? 0m;
var step = Security.VolumeStep ?? 0.01m;
decimal volume;
if (UseFixedVolume)
{
volume = FixedVolume;
}
else
{
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
var riskAmount = portfolioValue * RiskPercent / 100m;
var stepPrice = 1m;
var priceStep = Security.PriceStep ?? 1m;
var distance = Math.Abs(entryPrice - stopPrice);
if (distance <= 0m || priceStep <= 0m || stepPrice <= 0m)
return 0m;
var riskPerUnit = distance / priceStep * stepPrice;
if (riskPerUnit <= 0m)
return 0m;
volume = riskAmount / riskPerUnit;
}
if (step > 0m)
volume = Math.Floor(volume / step) * step;
if (maxVolume > 0m)
volume = Math.Min(volume, maxVolume);
return Math.Max(volume, minVolume);
}
private void SplitVolumes(decimal total, out decimal lot1, out decimal lot2, out decimal lot3)
{
var percents = Tp1Percent + Tp2Percent + Tp3Percent;
if (percents <= 0m)
{
lot1 = total / 3m;
lot2 = total / 3m;
lot3 = total - lot1 - lot2;
}
else
{
lot1 = total * Tp1Percent / percents;
lot2 = total * Tp2Percent / percents;
lot3 = total - lot1 - lot2;
}
if (AdjustLotsForTakeProfits)
{
var sum = lot1 + lot2 + lot3;
if (sum != 0m)
{
var scale = total / sum;
lot1 *= scale;
lot2 *= scale;
lot3 = total - lot1 - lot2;
}
}
var step = Security.VolumeStep ?? 0.01m;
if (step > 0m)
{
lot1 = Math.Round(lot1 / step) * step;
lot2 = Math.Round(lot2 / step) * step;
lot3 = Math.Round(lot3 / step) * step;
}
var minVolume = Security.MinVolume ?? 0.01m;
if (lot1 > 0m && lot1 < minVolume)
lot1 = minVolume;
if (lot2 > 0m && lot2 < minVolume)
lot2 = minVolume;
if (lot3 > 0m && lot3 < minVolume)
lot3 = minVolume;
var sumAfter = lot1 + lot2 + lot3;
if (sumAfter > total && sumAfter > 0m)
{
var scale = total / sumAfter;
lot1 *= scale;
lot2 *= scale;
lot3 = total - lot1 - lot2;
}
lot1 = Math.Max(0m, lot1);
lot2 = Math.Max(0m, lot2);
lot3 = Math.Max(0m, lot3);
}
private void UpdateRiskManagement(ICandleMessage candle)
{
if (_state.Side == null || _state.RemainingVolume <= 0m || _state.EntryPrice is not decimal entry)
return;
var side = _state.Side.Value;
var direction = side == Sides.Buy ? 1m : -1m;
var step = Security.PriceStep ?? 1m;
if (_state.StopPrice is decimal stop)
{
var hit = side == Sides.Buy ? candle.LowPrice <= stop : candle.HighPrice >= stop;
if (hit)
{
ExitAll();
LogInfo($"Stop-loss hit at {stop:F5}.");
return;
}
}
if (!_state.Tp1Filled && _state.Lot1 > 0m)
{
var reached = side == Sides.Buy ? candle.HighPrice >= _state.Tp1Price : candle.LowPrice <= _state.Tp1Price;
if (reached)
ExitPartial(_state.Lot1, _state.Tp1Price, 1);
}
if (!_state.Tp2Filled && _state.Lot2 > 0m)
{
var reached = side == Sides.Buy ? candle.HighPrice >= _state.Tp2Price : candle.LowPrice <= _state.Tp2Price;
if (reached)
ExitPartial(_state.Lot2, _state.Tp2Price, 2);
}
if (!_state.Tp3Filled && _state.Lot3 > 0m)
{
var reached = side == Sides.Buy ? candle.HighPrice >= _state.Tp3Price : candle.LowPrice <= _state.Tp3Price;
if (reached)
ExitPartial(_state.Lot3, _state.Tp3Price, 3);
}
if (_state.RemainingVolume <= 0m)
{
_state.ResetPosition();
return;
}
ApplyBreakEven(candle, entry, direction, step);
ApplyTrailing(candle, entry, direction, step);
}
private void ExitPartial(decimal volume, decimal price, int tpIndex)
{
if (volume <= 0m)
return;
Order order = _state.Side == Sides.Buy ? SellMarket(volume) : BuyMarket(volume);
if (order == null)
{
LogInfo($"Failed to exit partial position for TP{tpIndex}.");
return;
}
_state.RemainingVolume = Math.Max(0m, _state.RemainingVolume - volume);
switch (tpIndex)
{
case 1:
_state.Tp1Filled = true;
break;
case 2:
_state.Tp2Filled = true;
break;
case 3:
_state.Tp3Filled = true;
break;
}
LogInfo($"TP{tpIndex} executed at {price:F5}.");
}
private void ExitAll()
{
if (_state.Side == null || _state.RemainingVolume <= 0m)
{
_state.ResetPosition();
return;
}
var volume = _state.RemainingVolume;
Order order = _state.Side == Sides.Buy ? SellMarket(volume) : BuyMarket(volume);
if (order == null)
{
LogInfo("Failed to close position at stop.");
return;
}
_state.ResetPosition();
}
private void ApplyBreakEven(ICandleMessage candle, decimal entry, decimal direction, decimal step)
{
if (!UseBreakEven || _state.BreakEvenApplied || _state.StopPrice is not decimal currentStop)
return;
if (!IsGatePassed(BreakEvenAfterTp, _state.Tp1Filled, _state.Tp2Filled))
return;
if (BreakEvenTrigger <= 0m)
return;
var movement = (candle.ClosePrice - entry) * direction;
if (movement < BreakEvenTrigger * step)
return;
var newStop = entry + direction * BreakEvenProfit * step;
if (direction > 0m)
{
if (newStop <= currentStop)
return;
}
else if (newStop >= currentStop)
{
return;
}
_state.StopPrice = newStop;
_state.BreakEvenApplied = true;
LogInfo($"Break-even adjusted to {newStop:F5}.");
}
private void ApplyTrailing(ICandleMessage candle, decimal entry, decimal direction, decimal step)
{
if (!UseTrailingStop || _state.StopPrice is not decimal currentStop)
return;
if (!IsGatePassed(TrailAfterTp, _state.Tp1Filled, _state.Tp2Filled))
return;
if (TrailStart <= 0m || TrailStep <= 0m)
return;
var movement = (candle.ClosePrice - entry) * direction;
if (movement < TrailStart * step)
return;
var newStop = candle.ClosePrice - direction * TrailStep * step;
if (direction > 0m)
{
if (newStop <= currentStop)
return;
}
else if (newStop >= currentStop)
{
return;
}
_state.StopPrice = newStop;
_state.TrailingActivated = true;
LogInfo($"Trailing stop updated to {newStop:F5}.");
}
private static bool IsGatePassed(int gate, bool tp1Filled, bool tp2Filled)
{
var normalized = gate < 1 ? 1 : gate > 2 ? 2 : gate;
return normalized switch
{
1 => tp1Filled,
2 => tp2Filled,
_ => false,
};
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class butterfly_pattern_strategy(Strategy):
"""Butterfly harmonic pattern detection with partial take-profits and break-even/trailing management."""
def __init__(self):
super(butterfly_pattern_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used for pattern detection", "General")
self._pivot_left = self.Param("PivotLeft", 1) \
.SetGreaterThanZero() \
.SetDisplay("Pivot Left", "Bars to the left when validating a pivot", "Pattern")
self._pivot_right = self.Param("PivotRight", 1) \
.SetGreaterThanZero() \
.SetDisplay("Pivot Right", "Bars to the right when validating a pivot", "Pattern")
self._tolerance = self.Param("Tolerance", 0.50) \
.SetGreaterThanZero() \
.SetDisplay("Ratio Tolerance", "Maximum deviation allowed for Fibonacci ratios", "Pattern")
self._use_fixed_volume = self.Param("UseFixedVolume", True) \
.SetDisplay("Use Fixed Volume", "Use fixed trade volume instead of risk-based sizing", "Risk")
self._fixed_volume = self.Param("FixedVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Fixed Volume", "Volume to trade when fixed sizing is active", "Risk")
self._tp1_percent = self.Param("Tp1Percent", 50.0) \
.SetNotNegative() \
.SetDisplay("TP1 %", "Share of volume closed at the first take-profit", "Targets")
self._tp2_percent = self.Param("Tp2Percent", 30.0) \
.SetNotNegative() \
.SetDisplay("TP2 %", "Share of volume closed at the second take-profit", "Targets")
self._tp3_percent = self.Param("Tp3Percent", 20.0) \
.SetNotNegative() \
.SetDisplay("TP3 %", "Share of volume closed at the third take-profit", "Targets")
self._use_break_even = self.Param("UseBreakEven", False) \
.SetDisplay("Use Break-Even", "Enable break-even management", "Risk")
self._break_even_after_tp = self.Param("BreakEvenAfterTp", 1) \
.SetGreaterThanZero() \
.SetDisplay("Break-Even After TP", "Activate break-even after the specified take-profit", "Risk")
self._break_even_trigger = self.Param("BreakEvenTrigger", 30.0) \
.SetDisplay("Break-Even Trigger", "Points required to lock break-even", "Risk")
self._break_even_profit = self.Param("BreakEvenProfit", 5.0) \
.SetDisplay("Break-Even Profit", "Profit offset applied to break-even", "Risk")
self._use_trailing_stop = self.Param("UseTrailingStop", False) \
.SetDisplay("Use Trailing", "Enable trailing stop management", "Risk")
self._trail_after_tp = self.Param("TrailAfterTp", 2) \
.SetGreaterThanZero() \
.SetDisplay("Trail After TP", "Activate trailing after the specified take-profit", "Risk")
self._trail_start = self.Param("TrailStart", 20.0) \
.SetDisplay("Trail Start", "Points required before trailing", "Risk")
self._trail_step = self.Param("TrailStep", 5.0) \
.SetDisplay("Trail Step", "Trailing step in price points", "Risk")
self._candles = []
self._pivots = []
self._side = None
self._remaining_volume = 0.0
self._lot1 = 0.0
self._lot2 = 0.0
self._lot3 = 0.0
self._tp1_filled = False
self._tp2_filled = False
self._tp3_filled = False
self._entry_price = None
self._stop_price = None
self._tp1_price = 0.0
self._tp2_price = 0.0
self._tp3_price = 0.0
self._break_even_applied = False
self._trailing_activated = False
self._last_pattern_time = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def PivotLeft(self):
return self._pivot_left.Value
@property
def PivotRight(self):
return self._pivot_right.Value
@property
def Tolerance(self):
return self._tolerance.Value
@property
def UseFixedVolume(self):
return self._use_fixed_volume.Value
@property
def FixedVolume(self):
return self._fixed_volume.Value
@property
def Tp1Percent(self):
return self._tp1_percent.Value
@property
def Tp2Percent(self):
return self._tp2_percent.Value
@property
def Tp3Percent(self):
return self._tp3_percent.Value
@property
def UseBreakEven(self):
return self._use_break_even.Value
@property
def BreakEvenAfterTp(self):
return self._break_even_after_tp.Value
@property
def BreakEvenTrigger(self):
return self._break_even_trigger.Value
@property
def BreakEvenProfit(self):
return self._break_even_profit.Value
@property
def UseTrailingStop(self):
return self._use_trailing_stop.Value
@property
def TrailAfterTp(self):
return self._trail_after_tp.Value
@property
def TrailStart(self):
return self._trail_start.Value
@property
def TrailStep(self):
return self._trail_step.Value
def OnReseted(self):
super(butterfly_pattern_strategy, self).OnReseted()
self._candles = []
self._pivots = []
self._reset_position()
self._last_pattern_time = None
def OnStarted2(self, time):
super(butterfly_pattern_strategy, self).OnStarted2(time)
self._candles = []
self._pivots = []
self._reset_position()
self._last_pattern_time = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_risk_management(candle)
self._candles.append(candle)
pivot = self._try_extract_pivot()
if pivot is not None:
self._pivots.append(pivot)
if len(self._pivots) > 5:
self._pivots.pop(0)
self._try_detect_pattern(candle)
def _try_extract_pivot(self):
left = self.PivotLeft
right = self.PivotRight
required = left + right + 1
if len(self._candles) < required:
return None
index = len(self._candles) - 1 - right
if index < left:
return None
middle = self._candles[index]
if middle is None:
return None
is_high = True
is_low = True
from_idx = index - left
to_idx = index + right
for i in range(from_idx, to_idx + 1):
if i < 0 or i >= len(self._candles) or i == index:
continue
c = self._candles[i]
if c is None:
continue
if float(c.HighPrice) > float(middle.HighPrice):
is_high = False
if float(c.LowPrice) < float(middle.LowPrice):
is_low = False
if not is_high and not is_low:
return None
price = float(middle.HighPrice) if is_high else float(middle.LowPrice)
pivot = (middle.OpenTime, price, is_high)
if len(self._candles) > required:
self._candles.pop(0)
return pivot
def _try_detect_pattern(self, candle):
if len(self._pivots) < 5:
return
x = self._pivots[-5]
a = self._pivots[-4]
b = self._pivots[-3]
c = self._pivots[-2]
d = self._pivots[-1]
if self._last_pattern_time is not None and self._last_pattern_time == d[0]:
return
side = self._detect_pattern_type(x, a, b, c, d)
if side is None:
return
self._last_pattern_time = d[0]
if self._side is not None and self._remaining_volume > 0:
return
self._execute_pattern(candle, side, a, c)
def _detect_pattern_type(self, x, a, b, c, d):
x_price, a_price, b_price, c_price, d_price = x[1], a[1], b[1], c[1], d[1]
x_is_high, a_is_high, b_is_high, c_is_high, d_is_high = x[2], a[2], b[2], c[2], d[2]
tol = float(self.Tolerance)
diff_bear = x_price - a_price
if x_is_high and not a_is_high and b_is_high and not c_is_high and d_is_high and diff_bear > 0:
ideal_b = a_price + 0.786 * diff_bear
if abs(b_price - ideal_b) <= tol * diff_bear:
bc = b_price - c_price
if bc >= 0.1 * diff_bear and bc <= 2 * diff_bear:
cd = d_price - c_price
if cd >= 0.5 * diff_bear and cd <= 3 * diff_bear:
return Sides.Sell
diff_bull = a_price - x_price
if not x_is_high and a_is_high and not b_is_high and c_is_high and not d_is_high and diff_bull > 0:
ideal_b = a_price - 0.786 * diff_bull
if abs(b_price - ideal_b) <= tol * diff_bull:
bc = c_price - b_price
if bc >= 0.1 * diff_bull and bc <= 2 * diff_bull:
cd = c_price - d_price
if cd >= 0.5 * diff_bull and cd <= 3 * diff_bull:
return Sides.Buy
return None
def _execute_pattern(self, candle, side, a, c):
entry_price = float(candle.ClosePrice)
tp3 = c[1]
diff = (tp3 - entry_price) if side == Sides.Buy else (entry_price - tp3)
if diff <= 0:
return
if side == Sides.Buy:
tp1 = entry_price + diff / 3.0
tp2 = entry_price + diff * 2.0 / 3.0
stop = entry_price - (tp2 - entry_price) * 3.0
else:
tp1 = entry_price - diff / 3.0
tp2 = entry_price - diff * 2.0 / 3.0
stop = entry_price + (entry_price - tp2) * 3.0
volume = float(self.FixedVolume) if self.UseFixedVolume else 1.0
if volume <= 0:
return
percents = float(self.Tp1Percent) + float(self.Tp2Percent) + float(self.Tp3Percent)
if percents <= 0:
lot1 = volume / 3.0
lot2 = volume / 3.0
lot3 = volume - lot1 - lot2
else:
lot1 = volume * float(self.Tp1Percent) / percents
lot2 = volume * float(self.Tp2Percent) / percents
lot3 = volume - lot1 - lot2
lot1 = max(0.0, lot1)
lot2 = max(0.0, lot2)
lot3 = max(0.0, lot3)
total = lot1 + lot2 + lot3
if total <= 0:
return
if side == Sides.Buy:
self.BuyMarket(total)
else:
self.SellMarket(total)
self._side = side
self._entry_price = entry_price
self._stop_price = stop
self._lot1 = lot1
self._lot2 = lot2
self._lot3 = lot3
self._remaining_volume = total
self._tp1_filled = lot1 <= 0
self._tp2_filled = lot2 <= 0
self._tp3_filled = lot3 <= 0
self._tp1_price = tp1
self._tp2_price = tp2
self._tp3_price = tp3
self._break_even_applied = False
self._trailing_activated = False
def _update_risk_management(self, candle):
if self._side is None or self._remaining_volume <= 0 or self._entry_price is None:
return
side = self._side
direction = 1.0 if side == Sides.Buy else -1.0
step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
step = ps
if self._stop_price is not None:
if side == Sides.Buy:
hit = float(candle.LowPrice) <= self._stop_price
else:
hit = float(candle.HighPrice) >= self._stop_price
if hit:
self._exit_all()
return
if not self._tp1_filled and self._lot1 > 0:
if side == Sides.Buy:
reached = float(candle.HighPrice) >= self._tp1_price
else:
reached = float(candle.LowPrice) <= self._tp1_price
if reached:
self._exit_partial(self._lot1, 1)
if not self._tp2_filled and self._lot2 > 0:
if side == Sides.Buy:
reached = float(candle.HighPrice) >= self._tp2_price
else:
reached = float(candle.LowPrice) <= self._tp2_price
if reached:
self._exit_partial(self._lot2, 2)
if not self._tp3_filled and self._lot3 > 0:
if side == Sides.Buy:
reached = float(candle.HighPrice) >= self._tp3_price
else:
reached = float(candle.LowPrice) <= self._tp3_price
if reached:
self._exit_partial(self._lot3, 3)
if self._remaining_volume <= 0:
self._reset_position()
return
entry = self._entry_price
self._apply_break_even(candle, entry, direction, step)
self._apply_trailing(candle, entry, direction, step)
def _exit_partial(self, volume, tp_index):
if volume <= 0:
return
if self._side == Sides.Buy:
self.SellMarket(volume)
else:
self.BuyMarket(volume)
self._remaining_volume = max(0.0, self._remaining_volume - volume)
if tp_index == 1:
self._tp1_filled = True
elif tp_index == 2:
self._tp2_filled = True
elif tp_index == 3:
self._tp3_filled = True
def _exit_all(self):
if self._side is None or self._remaining_volume <= 0:
self._reset_position()
return
volume = self._remaining_volume
if self._side == Sides.Buy:
self.SellMarket(volume)
else:
self.BuyMarket(volume)
self._reset_position()
def _apply_break_even(self, candle, entry, direction, step):
if not self.UseBreakEven or self._break_even_applied or self._stop_price is None:
return
if not self._is_gate_passed(self.BreakEvenAfterTp):
return
if float(self.BreakEvenTrigger) <= 0:
return
movement = (float(candle.ClosePrice) - entry) * direction
if movement < float(self.BreakEvenTrigger) * step:
return
new_stop = entry + direction * float(self.BreakEvenProfit) * step
current_stop = self._stop_price
if direction > 0:
if new_stop <= current_stop:
return
elif new_stop >= current_stop:
return
self._stop_price = new_stop
self._break_even_applied = True
def _apply_trailing(self, candle, entry, direction, step):
if not self.UseTrailingStop or self._stop_price is None:
return
if not self._is_gate_passed(self.TrailAfterTp):
return
if float(self.TrailStart) <= 0 or float(self.TrailStep) <= 0:
return
movement = (float(candle.ClosePrice) - entry) * direction
if movement < float(self.TrailStart) * step:
return
new_stop = float(candle.ClosePrice) - direction * float(self.TrailStep) * step
current_stop = self._stop_price
if direction > 0:
if new_stop <= current_stop:
return
elif new_stop >= current_stop:
return
self._stop_price = new_stop
self._trailing_activated = True
def _is_gate_passed(self, gate):
normalized = max(1, min(2, gate))
if normalized == 1:
return self._tp1_filled
if normalized == 2:
return self._tp2_filled
return False
def _reset_position(self):
self._side = None
self._remaining_volume = 0.0
self._lot1 = 0.0
self._lot2 = 0.0
self._lot3 = 0.0
self._tp1_filled = False
self._tp2_filled = False
self._tp3_filled = False
self._entry_price = None
self._stop_price = None
self._tp1_price = 0.0
self._tp2_price = 0.0
self._tp3_price = 0.0
self._break_even_applied = False
self._trailing_activated = False
def CreateClone(self):
return butterfly_pattern_strategy()