Butterfly Pattern Strategy
Overview
The Butterfly Pattern Strategy converts the original MetaTrader "Cypher EA" harmonic pattern logic to StockSharp's high level API. The strategy scans a configurable candle series for bullish and bearish butterfly formations, validates the harmonic ratios, and opens market positions with three staged take-profit targets. Optional risk management features mirror the MetaTrader expert: break-even locking and trailing stop updates are available after partial exits.
How it works
- Candles are buffered until a pivot point can be confirmed using the
PivotLeft/PivotRightwindow. - When five alternating pivots are available, the strategy checks the Fibonacci ratios required for a butterfly pattern.
- Qualified setups are revalidated (optional) and evaluated by a harmonic quality score (
MinPatternQuality). - Once a pattern is confirmed on a closed candle:
- A market order is placed using either fixed volume or risk-based sizing.
- The position volume is split between three take-profit levels (
TP1/TP2/TP3). - A geometric stop-loss is derived from the pattern structure.
- During the lifetime of the position the strategy monitors candles to trigger partial exits, break-even locking, and trailing adjustments according to the configured thresholds.
Tip: The MetaTrader version works with multiple timeframes simultaneously. To replicate this behaviour in StockSharp, launch several instances of the strategy with different
CandleTypevalues.
Key parameters
| Parameter | Description |
|---|---|
CandleType |
Timeframe used for detecting pivots and patterns. |
PivotLeft / PivotRight |
Number of candles to the left/right required to confirm a pivot high/low. |
Tolerance |
Maximum harmonic ratio deviation allowed when validating the butterfly pattern. |
AllowTrading |
Enables or disables order generation after a pattern confirmation. |
UseFixedVolume / FixedVolume |
Forces a constant trade volume. When disabled, the strategy sizes positions via RiskPercent. |
RiskPercent |
Percent of portfolio value risked per trade (used only when UseFixedVolume is false). |
AdjustLotsForTakeProfits |
Normalises the partial volumes to ensure the sum matches the entry size. |
Tp1Percent / Tp2Percent / Tp3Percent |
Distribution of the total volume between the three take-profit levels. |
MinPatternQuality |
Minimum harmonic score (0–1) required to accept a detected pattern. |
UseSessionFilter, SessionStartHour, SessionEndHour |
Restrict trading to a specific exchange session window. |
RevalidatePattern |
Forces a secondary price check before opening a position. |
UseBreakEven, BreakEvenAfterTp, BreakEvenTrigger, BreakEvenProfit |
Controls break-even activation after the specified take-profit level and the additional profit buffer. |
UseTrailingStop, TrailAfterTp, TrailStart, TrailStep |
Enables trailing stops once a take-profit level has been reached and the minimum favourable excursion is achieved. |
Risk management
- Stop-loss, break-even, and trailing levels are managed internally without creating additional orders. Partial exits and stop closes are triggered with market orders to emulate the MetaTrader logic.
- When
UseFixedVolumeis disabled, the position size is calculated from the stop distance, instrument tick value and theRiskPercentsetting.
Usage notes
- Ensure the connected instrument supports the configured
CandleTypeand price step, otherwise the validation logic may reject signals due to minimum distance checks. - Break-even and trailing features require the respective take-profit levels to be filled (
BreakEvenAfterTpandTrailAfterTp). - Multiple strategy instances can run concurrently on different securities or timeframes to reproduce the multi-timeframe scanning of the original EA.
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()