在 GitHub 上查看

Fractured Fractals 策略

该策略移植自 MetaTrader 上的 “Fractured Fractals” 专家顾问。算法跟踪确认的威廉姆斯分形,在最新突破价位放置止损入场单,并使用相反方向的分形自动跟踪保护性止损单。

详情

  • 来源:由 MQL/20127/Fractured Fractals.mq5 转换而来。
  • 市场环境:适用于任意支持 StockSharp 的品种的突破延续行情。
  • 委托类型:入场使用止损委托,离场使用保护性止损委托。
  • 仓位规模:基于风险计算,由 MaximumRiskPercent 和连续亏损衰减系数 DecreaseFactor 控制。
  • 默认参数
    • MaximumRiskPercent = 2%
    • DecreaseFactor = 10
    • ExpirationHours = 1 小时
    • CandleType = 1 小时时间框架
  • 核心指标:即时计算的五根 K 线威廉姆斯分形。
  • 策略类型:多空双向突破并带动态止损。

策略逻辑

分形序列跟踪

  • 维护最近五根 K 线的高点和低点队列,复现 MT5 中的 iFractals 缓冲区。
  • 每当出现新分形,会将“最新 / 中间 / 最旧”三个槽位前移,并利用品种的最小报价步长忽略重复值。
  • 多头条件要求最新上分形高于中间分形;空头条件要求最新下分形低于前一分形。

入场委托与到期

  • 当不存在多头头寸或待执行的买入止损单时,在最新上分形放置买入止损,止损价设置为最近的下分形。
  • 空头逻辑对称:在最新下分形放置卖出止损,保护性止损位于最近的上分形。
  • 未成交的委托带有 ExpirationHours 定义的有效期。若收盘时间超过有效期,或结构被新的分形破坏(多头出现更低的上分形、空头出现更高的下分形),委托会被撤销。
  • 一旦有仓位建立,会立即取消方向相反的委托,避免帐面遗留多余订单。

保护性跟踪止损

  • 每个确认的相反方向分形都会更新保护性止损:多头追踪最新下分形,空头追踪最新上分形。
  • 仅在新分形带来更优价格时才会替换旧的止损委托,从而实现“只收紧、不放松”。
  • 仓位关闭后,所有剩余止损委托会被立刻取消。

风险管理与亏损序列控制

  • CalculateOrderVolume 复刻了 MT5 的风险计算:单位风险 = 入场价与止损价的差值(空头反向计算)。
  • 目标风险金额 = Portfolio.CurrentValue * MaximumRiskPercent / 100,若账户估值不可用则回退到策略的 Volume 属性。
  • 结果数量会依据 Security 的手数、成交步长、最小与最大交易量进行归一化。
  • 连续亏损会递增计数器,盈利或打平则清零。当亏损次数大于 1 时,仓位规模会按 losses / DecreaseFactor 的比例缩减。

交易结果跟踪

  • 重写 OnOwnTradeReceived,汇总持仓周期内的成交,判断最终是盈利、亏损还是持平。
  • 连续亏损计数与最近盈利时间会同步更新,完全保留原版专家顾问的逻辑,便于进一步统计分析。

使用建议

  1. 将策略绑定到目标证券与投资组合,按照账户规模调整 CandleType 及风险参数。
  2. 确认经纪商或适配器支持止损委托;若不支持,可在 UpdateTrailingStops 中改为手动平仓逻辑。
  3. 策略仅处理已完成的 K 线,细小的盘中波动不会像 MT5 的逐笔回测那样触发委托。
  4. 建议开启日志查看移植后的提示信息,以便验证与原 MQL 版本的一致性。
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the "Fractured Fractals" MetaTrader strategy using high-level StockSharp API.
/// Places stop orders on newly confirmed fractals and trails the stop with the opposite fractal.
/// </summary>
public class FracturedFractalsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maximumRiskPercent;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<int> _expirationHours;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _highBuffer = new();
	private readonly List<decimal> _lowBuffer = new();

	private decimal? _lastUpFractal;
	private decimal? _lastDownFractal;
	private decimal? _upYoungest;
	private decimal? _upMiddle;
	private decimal? _upOld;
	private decimal? _downYoungest;
	private decimal? _downMiddle;
	private decimal? _downOld;

	private decimal? _buyStopLevel;
	private decimal? _sellStopLevel;
	private decimal? _longStopLevel;
	private decimal? _shortStopLevel;
	private DateTimeOffset? _buyStopExpiry;
	private DateTimeOffset? _sellStopExpiry;
	private decimal _buyStopVolume;
	private decimal _sellStopVolume;

	private decimal _entryPrice;
	private int _consecutiveLosses;

	/// <summary>
	/// Maximum risk per trade expressed as percentage of portfolio value.
	/// </summary>
	public decimal MaximumRiskPercent
	{
		get => _maximumRiskPercent.Value;
		set => _maximumRiskPercent.Value = value;
	}

	/// <summary>
	/// Factor that reduces position size after consecutive losing trades.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Pending order lifetime in hours.
	/// </summary>
	public int ExpirationHours
	{
		get => _expirationHours.Value;
		set => _expirationHours.Value = value;
	}

	/// <summary>
	/// Candle type used for fractal calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="FracturedFractalsStrategy"/> with default parameters.
	/// </summary>
	public FracturedFractalsStrategy()
	{
		_maximumRiskPercent = Param(nameof(MaximumRiskPercent), 2m)
		.SetRange(0.0001m, 100m)
		.SetDisplay("Max Risk %", "Maximum risk per trade", "Risk");

		_decreaseFactor = Param(nameof(DecreaseFactor), 10m)
		.SetRange(0m, 1000m)
		.SetDisplay("Decrease Factor", "Loss streak position size dampener", "Risk");

		_expirationHours = Param(nameof(ExpirationHours), 1)
		.SetRange(0, 240)
		.SetDisplay("Expiration", "Pending order lifetime (hours)", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_highBuffer.Clear();
		_lowBuffer.Clear();

		_lastUpFractal = null;
		_lastDownFractal = null;
		_upYoungest = null;
		_upMiddle = null;
		_upOld = null;
		_downYoungest = null;
		_downMiddle = null;
		_downOld = null;

		_buyStopLevel = null;
		_sellStopLevel = null;
		_longStopLevel = null;
		_shortStopLevel = null;
		_buyStopExpiry = null;
		_sellStopExpiry = null;
		_buyStopVolume = 0m;
		_sellStopVolume = 0m;

		_entryPrice = 0m;
		_consecutiveLosses = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		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;

		_highBuffer.Add(candle.HighPrice);
		_lowBuffer.Add(candle.LowPrice);

		if (_highBuffer.Count > 5)
			_highBuffer.RemoveAt(0);
		if (_lowBuffer.Count > 5)
			_lowBuffer.RemoveAt(0);

		if (_highBuffer.Count < 5 || _lowBuffer.Count < 5)
			return;

		DetectFractals();

		// Check protective stop levels
		CheckProtectiveStops(candle);

		// Validate pending levels
		ValidatePendingLevels(candle.CloseTime);

		// Check if pending buy/sell stop levels are triggered
		CheckPendingTriggers(candle);

		// Update trailing stops
		UpdateTrailingStops();

		// Try to set new pending levels
		if (Position == 0)
		{
			if (!TrySetBuyStopLevel(candle.CloseTime))
				TrySetSellStopLevel(candle.CloseTime);
		}
	}

	private void DetectFractals()
	{
		var highs = _highBuffer.ToArray();
		var lows = _lowBuffer.ToArray();

		decimal? upFractal = null;
		decimal? downFractal = null;

		if (highs[2] > highs[0] && highs[2] > highs[1] && highs[2] > highs[3] && highs[2] > highs[4])
			upFractal = highs[2];

		if (lows[2] < lows[0] && lows[2] < lows[1] && lows[2] < lows[3] && lows[2] < lows[4])
			downFractal = lows[2];

		if (upFractal is decimal up && !AreEqual(_lastUpFractal, up))
		{
			_lastUpFractal = up;
			_upOld = _upMiddle;
			_upMiddle = _upYoungest;
			_upYoungest = up;
		}

		if (downFractal is decimal down && !AreEqual(_lastDownFractal, down))
		{
			_lastDownFractal = down;
			_downOld = _downMiddle;
			_downMiddle = _downYoungest;
			_downYoungest = down;
		}
	}

	private void CheckProtectiveStops(ICandleMessage candle)
	{
		if (Position > 0 && _longStopLevel.HasValue)
		{
			if (candle.LowPrice <= _longStopLevel.Value)
			{
				SellMarket(Math.Abs(Position));
				_longStopLevel = null;
				_consecutiveLosses++;
				return;
			}
		}

		if (Position < 0 && _shortStopLevel.HasValue)
		{
			if (candle.HighPrice >= _shortStopLevel.Value)
			{
				BuyMarket(Math.Abs(Position));
				_shortStopLevel = null;
				_consecutiveLosses++;
				return;
			}
		}
	}

	private void UpdateTrailingStops()
	{
		if (Position > 0 && _downYoungest.HasValue)
		{
			if (!_longStopLevel.HasValue || _downYoungest.Value > _longStopLevel.Value)
				_longStopLevel = _downYoungest.Value;
		}
		else if (Position <= 0)
		{
			_longStopLevel = null;
		}

		if (Position < 0 && _upYoungest.HasValue)
		{
			if (!_shortStopLevel.HasValue || _upYoungest.Value < _shortStopLevel.Value)
				_shortStopLevel = _upYoungest.Value;
		}
		else if (Position >= 0)
		{
			_shortStopLevel = null;
		}
	}

	private void ValidatePendingLevels(DateTimeOffset currentTime)
	{
		if (_buyStopLevel.HasValue && _upYoungest.HasValue)
		{
			if (_upYoungest.Value < _buyStopLevel.Value && !AreEqual(_upYoungest, _buyStopLevel.Value))
			{
				_buyStopLevel = null;
				_buyStopExpiry = null;
			}
		}

		if (_sellStopLevel.HasValue && _downYoungest.HasValue)
		{
			if (_downYoungest.Value > _sellStopLevel.Value && !AreEqual(_downYoungest, _sellStopLevel.Value))
			{
				_sellStopLevel = null;
				_sellStopExpiry = null;
			}
		}

		if (_buyStopLevel.HasValue && _buyStopExpiry.HasValue && currentTime >= _buyStopExpiry.Value)
		{
			_buyStopLevel = null;
			_buyStopExpiry = null;
		}

		if (_sellStopLevel.HasValue && _sellStopExpiry.HasValue && currentTime >= _sellStopExpiry.Value)
		{
			_sellStopLevel = null;
			_sellStopExpiry = null;
		}

		if (Position != 0)
		{
			_buyStopLevel = null;
			_sellStopLevel = null;
			_buyStopExpiry = null;
			_sellStopExpiry = null;
		}
	}

	private void CheckPendingTriggers(ICandleMessage candle)
	{
		if (_buyStopLevel.HasValue && candle.HighPrice >= _buyStopLevel.Value && Position <= 0)
		{
			var buyLevel = _buyStopLevel.Value;
			var vol = _buyStopVolume > 0m ? _buyStopVolume : Volume;
			if (vol > 0m)
			{
				if (Position < 0)
					BuyMarket(Math.Abs(Position));
				BuyMarket(vol);
				_entryPrice = buyLevel;
				_longStopLevel = _downYoungest;
			}
			_buyStopLevel = null;
			_buyStopExpiry = null;
		}

		if (_sellStopLevel.HasValue && candle.LowPrice <= _sellStopLevel.Value && Position >= 0)
		{
			var sellLevel = _sellStopLevel.Value;
			var vol = _sellStopVolume > 0m ? _sellStopVolume : Volume;
			if (vol > 0m)
			{
				if (Position > 0)
					SellMarket(Math.Abs(Position));
				SellMarket(vol);
				_entryPrice = sellLevel;
				_shortStopLevel = _upYoungest;
			}
			_sellStopLevel = null;
			_sellStopExpiry = null;
		}
	}

	private bool TrySetBuyStopLevel(DateTimeOffset time)
	{
		if (Position > 0 || _buyStopLevel.HasValue)
			return false;

		if (_upYoungest is not decimal up || _upMiddle is not decimal middle || _downYoungest is not decimal stop)
			return false;

		if (up <= middle || stop >= up)
			return false;

		var volume = CalculateOrderVolume(up, stop, Sides.Buy);
		if (volume <= 0m)
			return false;

		_buyStopLevel = up;
		_buyStopVolume = volume;
		_buyStopExpiry = ExpirationHours > 0 ? time + TimeSpan.FromHours(ExpirationHours) : null;
		return true;
	}

	private void TrySetSellStopLevel(DateTimeOffset time)
	{
		if (Position < 0 || _sellStopLevel.HasValue)
			return;

		if (_downYoungest is not decimal down || _downMiddle is not decimal middle || _upYoungest is not decimal stop)
			return;

		if (down >= middle || stop <= down)
			return;

		var volume = CalculateOrderVolume(down, stop, Sides.Sell);
		if (volume <= 0m)
			return;

		_sellStopLevel = down;
		_sellStopVolume = volume;
		_sellStopExpiry = ExpirationHours > 0 ? time + TimeSpan.FromHours(ExpirationHours) : null;
	}

	private decimal CalculateOrderVolume(decimal entryPrice, decimal stopPrice, Sides direction)
	{
		var riskPerUnit = direction == Sides.Buy ? entryPrice - stopPrice : stopPrice - entryPrice;
		if (riskPerUnit <= 0m)
			return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		if (portfolioValue <= 0m)
			portfolioValue = Volume > 0m ? Volume * entryPrice : 0m;

		var riskAmount = portfolioValue * (MaximumRiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		var volume = riskAmount / riskPerUnit;

		if (DecreaseFactor > 0m && _consecutiveLosses > 1)
			volume -= volume * (_consecutiveLosses / DecreaseFactor);

		if (volume <= 0m)
			return 0m;

		return Math.Max(volume, Volume > 0 ? Volume : 1m);
	}

	private bool AreEqual(decimal? first, decimal second)
	{
		if (first is not decimal value)
			return false;

		var step = Security?.PriceStep ?? 0.00000001m;
		return Math.Abs(value - second) <= step / 2m;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0m)
			_entryPrice = 0m;
	}
}