在 GitHub 上查看

e-TurboFx 动量策略

概述

e-TurboFx 是原始 MetaTrader 4 智能交易系统的移植版本。策略会检查最近收盘的蜡烛线,寻找蜡烛实体持续扩大的趋势段。若连续出现实体不断放大的空头蜡烛,说明下跌动能正在衰竭,可以考虑做多;若连续出现实体不断放大的多头蜡烛,则意味着上涨过度,可以尝试做空。StockSharp 版本通过蜡烛订阅保持事件驱动结构,并可选地自动附加止损和止盈。

交易逻辑

  1. 订阅可配置的蜡烛类型,仅处理 CandleStates.Finished 的蜡烛。
  2. 分别维护多头和空头两个序列计数器。
  3. 计算每根蜡烛的实体长度 |Close - Open|
  4. 一旦出现反向收盘的蜡烛,就重置对应的反方向序列。
  5. 序列内部要求实体严格放大:新蜡烛的实体必须大于上一根,同方向实体缩小时序列会重置为 1。
  6. 当某个序列的计数达到 DepthAnalysis,在该序列的反方向发出市价订单(空头序列触发买入,多头序列触发卖出)。
  7. 有持仓时暂停信号检测,等待回到空仓再重新开始。StartProtection 在启动时只调用一次,可按价格步长配置止损/止盈,输入 0 表示关闭。

这种实现方式完整复刻了 MQL4 脚本:它逐根检查最近的 N 根蜡烛,确认方向相同且实体逐渐变大。

实现细节

  • 使用高层 API:SubscribeCandlesBind,无自建集合或历史遍历,符合项目要求。
  • 状态信息保存在四个字段中(_bearishSequence_bullishSequence_previousBearishBody_previousBullishBody),避免额外的列表或队列。
  • 保护模块只在 OnStarted 中配置一次,支持按步长设置止损与止盈;与原策略一致,填 0 即表示不下达对应委托。
  • 源码中提供了详细的英文注释,说明何时重置序列、何时触发交易。
  • 在 Designer 或 UI 中运行时,会绘制蜡烛及自身成交,方便验证行为。

参数

参数 说明 默认值
DepthAnalysis 需要多少根同向且实体递增的蜡烛才会触发交易。 3
TakeProfitSteps 止盈距离(价格步长),设置为 0 时关闭。 120
StopLossSteps 止损距离(价格步长),设置为 0 时关闭。 70
TradeVolume 每次市价单的交易量,同时会更新基础 Strategy.Volume 0.1
CandleType 用于分析的蜡烛类型(时间框架)。 1 hour

所有数值型参数都包含优化元数据,可直接在 StockSharp 优化器中调整。

注意事项

  • 策略对蜡烛实体敏感,因此时间框架越短,信号越频繁,但保护距离也需相应调整。
  • 请确保交易工具提供有效的 PriceStep,否则步长无法转换为真实价格距离。
  • 建议在实盘前先在 Designer 中回测,确认止损和止盈距离符合预期。
  • 策略一次只持有一笔仓位。平仓后计数器会重置,必须重新累积新的序列才能再次交易,这与原版行为一致。
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>
/// Momentum reversal strategy that tracks consecutive candles with expanding bodies.
/// </summary>
public class ETurboFxMomentumStrategy : Strategy
{
	private readonly StrategyParam<int> _depthAnalysis;
	private readonly StrategyParam<decimal> _takeProfitSteps;
	private readonly StrategyParam<decimal> _stopLossSteps;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;

	private int _bearishSequence;
	private int _bullishSequence;
	private decimal _previousBearishBody;
	private decimal _previousBullishBody;

	/// <summary>
	/// Number of recent candles analysed for momentum confirmation.
	/// </summary>
	public int DepthAnalysis
	{
		get => _depthAnalysis.Value;
		set => _depthAnalysis.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in price steps (ticks).
	/// A value of zero disables the take profit order.
	/// </summary>
	public decimal TakeProfitSteps
	{
		get => _takeProfitSteps.Value;
		set => _takeProfitSteps.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in price steps (ticks).
	/// A value of zero disables the protective stop.
	/// </summary>
	public decimal StopLossSteps
	{
		get => _stopLossSteps.Value;
		set => _stopLossSteps.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Candle type analysed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ETurboFxMomentumStrategy" /> class.
	/// </summary>
	public ETurboFxMomentumStrategy()
	{
		_depthAnalysis = Param(nameof(DepthAnalysis), 3)
			.SetGreaterThanZero()
			.SetDisplay("Depth Analysis", "Number of finished candles used for pattern detection", "Trading Rules")
			
			.SetOptimize(2, 6, 1);

		_takeProfitSteps = Param(nameof(TakeProfitSteps), 120m)
			.SetNotNegative()
			.SetDisplay("Take Profit (steps)", "Take profit distance in price steps (ticks)", "Risk Management")
			
			.SetOptimize(60m, 180m, 20m);

		_stopLossSteps = Param(nameof(StopLossSteps), 70m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (steps)", "Stop loss distance in price steps (ticks)", "Risk Management")
			
			.SetOptimize(40m, 120m, 10m);

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume used for entries", "Trading Rules")
			
			.SetOptimize(0.1m, 0.5m, 0.1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe of the candles analysed by the strategy", "Market Data");
	}

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

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

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

		ResetState();
		Volume = TradeVolume;

		var takeProfitUnit = CreateStepUnit(TakeProfitSteps);
		var stopLossUnit = CreateStepUnit(StopLossSteps);

		if (takeProfitUnit != null || stopLossUnit != null)
		{
			// Configure protective orders once the strategy starts.
			StartProtection(
				takeProfit: takeProfitUnit,
				stopLoss: stopLossUnit,
				isStopTrailing: false,
				useMarketOrders: true);
		}

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private Unit CreateStepUnit(decimal steps)
	{
		if (steps <= 0)
			return null;

		// Convert the user-friendly tick distance into a StockSharp Unit instance.
		return new Unit(steps, UnitTypes.Absolute);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// indicators formed check removed

		if (Position != 0)
		{
			// Do not look for new signals while a position is active.
			ResetState();
			return;
		}

		var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);

		// The original expert compared absolute bodies of the latest N closed candles.
		// Measuring the body here reproduces that behaviour candle by candle.

		if (candle.ClosePrice < candle.OpenPrice)
		{
			HandleBearishCandle(bodySize);
		}
		else if (candle.ClosePrice > candle.OpenPrice)
		{
			HandleBullishCandle(bodySize);
		}
		else
		{
			// Neutral candle breaks both sequences.
			ResetState();
		}
	}

	private void HandleBearishCandle(decimal bodySize)
	{
		// Bearish candles reset the bullish path and allow the downside streak to continue.
		ResetBullishSequence();

		if (bodySize <= 0)
		{
			ResetBearishSequence();
			return;
		}

		if (_bearishSequence == 0 || bodySize > _previousBearishBody)
		{
			// Body is larger than the previous bearish candle, extend the sequence.
			_bearishSequence++;
		}
		else
		{
			// Sequence restarts because body did not expand.
			_bearishSequence = 1;
		}

		_previousBearishBody = bodySize;

		if (_bearishSequence >= DepthAnalysis)
		{
			// Expanding bearish bodies suggest exhaustion that can trigger a long entry.
			BuyMarket();
			ResetBearishSequence();
		}
	}

	private void HandleBullishCandle(decimal bodySize)
	{
		// Bullish candles reset the bearish path and allow the upside streak to continue.
		ResetBearishSequence();

		if (bodySize <= 0)
		{
			ResetBullishSequence();
			return;
		}

		if (_bullishSequence == 0 || bodySize > _previousBullishBody)
		{
			// Body is larger than the previous bullish candle, extend the sequence.
			_bullishSequence++;
		}
		else
		{
			// Sequence restarts because body did not expand.
			_bullishSequence = 1;
		}

		_previousBullishBody = bodySize;

		if (_bullishSequence >= DepthAnalysis)
		{
			// Expanding bullish bodies suggest potential reversal to the downside.
			SellMarket();
			ResetBullishSequence();
		}
	}

	private void ResetBearishSequence()
	{
		_bearishSequence = 0;
		_previousBearishBody = 0m;
	}

	private void ResetBullishSequence()
	{
		_bullishSequence = 0;
		_previousBullishBody = 0m;
	}

	private void ResetState()
	{
		ResetBearishSequence();
		ResetBullishSequence();
	}
}