在 GitHub 上查看

Farhad Crab 策略 (C#)

概述

Farhad Crab 是一套趋势型策略,通过考察价格相对指数移动平均线 (EMA) 的位置来寻找回调入场机会,并使用固定止损、止盈、移动止损以及日线过滤器进行风险控制。原始的 MT5 专家顾问使用小时级别的 K 线执行交易,同时参考日线数据来决定是否平仓。本 C# 版本保持了这一框架,将工作周期的 EMA 与日线 EMA 交叉过滤结合使用。

核心思想

  • 趋势过滤器: 在工作周期上计算 EMA(默认是 1 小时 K 线上的 15 周期 EMA)。若上一根 K 线的最低价高于 EMA,则允许做多;若上一根 K 线的最高价低于 EMA,则允许做空。
  • 日线过滤器: 在日线周期上再计算一条 EMA。当日线 EMA 从下向上穿越日线收盘价时,平掉所有多单;当日线 EMA 从上向下穿越收盘价时,平掉所有空单。
  • 风险管理: 止损和止盈以点 (pip) 为单位设置。移动止损在浮盈超过“移动止损距离 + 止损步长”时上移/下移保护价位,模拟 MT5 中 TrailingStopTrailingStep 的组合行为。
  • 单一净头寸: 策略只维持一个净持仓量。当方向反转时,会先平掉原有仓位,再按照 Volume 参数开出新仓。

交易规则

  1. 入场条件(工作周期):
    • 若上一根 K 线的最低价高于 EMA(考虑 MaShift 设定的位移),则做多。
    • 若上一根 K 线的最高价低于 EMA,则做空。
  2. 下单数量: Volume 参数定义基准下单量。若需要反向开仓,会自动加上现有反向仓位的数量以完成净头寸切换。
  3. 止损/止盈:
    • 距离以点 (pip) 表示。点值根据品种的最小价格变动 (PriceStep) 自动计算,对三位或五位报价的外汇品种会额外乘以 10,以符合 MT5 的处理方式。
    • 将参数设为 0 即可关闭相应的止损或止盈。
  4. 移动止损:
    • 仅当 TrailingStopPips 大于 0 时启用。
    • 做多时,当浮盈超过 TrailingStopPips + TrailingStepPips,止损价格更新为 当前价 - TrailingStopPips;做空时对称处理。
    • 止损步长可避免过于频繁地调整保护价。
  5. 日线平仓过滤:
    • 使用最近两根已完成的日线。
    • 如果两天前日线 EMA 低于收盘价,而昨天日线 EMA 高于收盘价,则平掉多单。
    • 如果出现相反的穿越,则平掉空单。

参数

名称 类型 默认值 说明
CandleType DataType 1 小时时间框架 用于计算信号的工作周期。
MaLength int 15 工作周期 EMA 的周期长度。
MaShift int 0 将 EMA 值向后偏移的已完成 K 线数量。
DailyMaLength int 15 日线 EMA 的周期长度,用于交叉平仓过滤。
StopLossPips decimal 50 止损距离(点)。设为 0 可禁用。
TakeProfitPips decimal 50 止盈距离(点)。设为 0 可禁用。
TrailingStopPips decimal 10 移动止损距离(点)。设为 0 可禁用移动止损。
TrailingStepPips decimal 5 重新上移/下移移动止损前所需的额外盈利(点)。
Volume decimal 0.1 每次下单的基准数量。

与 MT5 版本的差异

  • 仅实现指数移动平均,保持了原脚本的默认设置;其他平滑方式暂不支持。
  • MT5 版本基于逐笔报价执行止损/止盈,这里改为在 K 线完成后根据最高价和最低价判断是否触发。
  • 原代码中引用的 Parabolic SAR 指标未实际用于决策,因此在移植版本中移除。
  • 移动止损通过内部记录的价格水平工作,不直接下发经纪商止损单;当价格触及计算出的水平时,策略会在下一根 K 线上执行平仓。

使用建议

  • 根据目标交易节奏选择合适的 CandleType。默认的一小时 K 线可以重现原脚本的行为。
  • 同时调整 MaLengthDailyMaLength,在入场敏感度与趋势过滤之间找到平衡。
  • 对于五位报价的外汇产品,点值会自动换算为 0.0001。
  • 回测时请确保获取到日线数据,以便日线 EMA 过滤器能够正常运行。
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>
/// Trend-following strategy converted from the FarhadCrab1 MT5 expert advisor.
/// The strategy enters on pullbacks to an EMA, manages risk with pip-based levels,
/// and closes positions based on a daily EMA crossover filter.
/// </summary>
public class FarhadCrab1Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maLength;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<int> _dailyMaLength;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _orderVolume;

	private readonly Queue<decimal> _maValues = new();

	private readonly DataType _dailyCandleType = TimeSpan.FromHours(1).TimeFrame();

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	private decimal? _prevDailyClose;
	private decimal? _prevDailyMa;
	private decimal? _prevPrevDailyClose;
	private decimal? _prevPrevDailyMa;

	private ICandleMessage _previousCandle;
	private decimal _entryPrice;

	/// <summary>
	/// Working candle type for the execution timeframe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Period of the EMA used on the working timeframe.
	/// </summary>
	public int MaLength
	{
		get => _maLength.Value;
		set => _maLength.Value = value;
	}

	/// <summary>
	/// Number of completed candles to shift the EMA backwards.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Period of the daily EMA that closes positions on crossovers.
	/// </summary>
	public int DailyMaLength
	{
		get => _dailyMaLength.Value;
		set => _dailyMaLength.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional pip distance required before updating the trailing stop again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Base order volume in lots/contracts.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="FarhadCrab1Strategy"/> class.
	/// </summary>
	public FarhadCrab1Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Execution timeframe", "General");

		_maLength = Param(nameof(MaLength), 15)
			.SetGreaterThanZero()
			.SetDisplay("EMA Length", "EMA period on the working timeframe", "Indicators");

		_maShift = Param(nameof(MaShift), 0)
			.SetRange(0, 100)
			.SetDisplay("EMA Shift", "Shift EMA value backwards by N candles", "Indicators");

		_dailyMaLength = Param(nameof(DailyMaLength), 15)
			.SetGreaterThanZero()
			.SetDisplay("Daily EMA Length", "EMA period used on daily candles", "Indicators");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Protection");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Protection");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetRange(0m, 500m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Protection");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 500m)
			.SetDisplay("Trailing Step (pips)", "Extra gain in pips before updating the trailing stop", "Protection");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Base order volume", "Trading");
	}

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

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

		_maValues.Clear();
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_prevDailyClose = null;
		_prevDailyMa = null;
		_prevPrevDailyClose = null;
		_prevPrevDailyMa = null;
		_previousCandle = null;
		_entryPrice = 0m;
	}

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

		// Ensure the base strategy volume reflects the configured parameter.
		base.Volume = OrderVolume;

		// Subscribe to the working timeframe candles with an EMA for entry decisions.
		var ema = new ExponentialMovingAverage { Length = MaLength };
		var candleSubscription = SubscribeCandles(CandleType);
		candleSubscription
			.Bind(ema, ProcessWorkingCandle)
			.Start();

		// Subscribe to daily candles with another EMA for exit filtering.
		var dailyEma = new ExponentialMovingAverage { Length = DailyMaLength };
		var dailySubscription = SubscribeCandles(_dailyCandleType);
		dailySubscription
			.Bind(dailyEma, ProcessDailyCandle)
			.Start();

		// Draw candles, indicator, and trades on the chart if charting is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, candleSubscription);
			DrawOwnTrades(area);
			DrawIndicator(area, ema);
		}
	}

	private void ProcessDailyCandle(ICandleMessage candle, decimal emaValue)
	{
		// Process only finished daily candles.
		if (candle.State != CandleStates.Finished)
			return;

		_prevPrevDailyClose = _prevDailyClose;
		_prevPrevDailyMa = _prevDailyMa;
		_prevDailyClose = candle.ClosePrice;
		_prevDailyMa = emaValue;
	}

	private void ProcessWorkingCandle(ICandleMessage candle, decimal emaValue)
	{
		// Use only completed candles for decision making.
		if (candle.State != CandleStates.Finished)
			return;

		// Store EMA values so we can apply the configured shift.
		UpdateMaBuffer(emaValue);
		var shiftedMa = GetShiftedMaValue();
		if (shiftedMa == null)
		{
			_previousCandle = candle;
			return;
		}

		// Require at least one previous candle to evaluate entry conditions.
		if (_previousCandle == null)
		{
			_previousCandle = candle;
			return;
		}

		// Close positions when the daily EMA filter signals a crossover against us.
		if (TryCloseByDailyFilter())
		{
			_previousCandle = candle;
			return;
		}

		var pipSize = GetPipSize();

		// Check for stop-loss or take-profit triggers before adjusting trailing stops.
		if (CheckStopsAndTargets(candle))
		{
			_previousCandle = candle;
			return;
		}

		// Update trailing stop levels if the position has moved far enough.
		ApplyTrailingStop(candle, pipSize);

		// Evaluate long entry condition: previous low above the EMA.
		if (Position <= 0 && _previousCandle.LowPrice > shiftedMa.Value)
		{
			var volume = OrderVolume + (Position < 0 ? -Position : 0m);
			if (volume > 0)
			{
				BuyMarket(volume);
				SetRiskLevels(candle.ClosePrice, pipSize, true);
			}
			_previousCandle = candle;
			return;
		}

		// Evaluate short entry condition: previous high below the EMA.
		if (Position >= 0 && _previousCandle.HighPrice < shiftedMa.Value)
		{
			var volume = OrderVolume + (Position > 0 ? Position : 0m);
			if (volume > 0)
			{
				SellMarket(volume);
				SetRiskLevels(candle.ClosePrice, pipSize, false);
			}
			_previousCandle = candle;
			return;
		}

		// Store the current candle for the next iteration.
		_previousCandle = candle;
	}

	private bool TryCloseByDailyFilter()
	{
		if (_prevDailyClose == null || _prevDailyMa == null || _prevPrevDailyClose == null || _prevPrevDailyMa == null)
			return false;

		var prevClose = _prevDailyClose.Value;
		var prevMa = _prevDailyMa.Value;
		var prev2Close = _prevPrevDailyClose.Value;
		var prev2Ma = _prevPrevDailyMa.Value;

		// Bearish crossover: EMA moved above the daily close -> exit long positions.
		if (Position > 0 && prevMa > prevClose && prev2Ma < prev2Close)
		{
			SellMarket(Position);
			ResetRiskLevels();
			return true;
		}

		// Bullish crossover: EMA moved below the daily close -> exit short positions.
		if (Position < 0 && prevMa < prevClose && prev2Ma > prev2Close)
		{
			BuyMarket(-Position);
			ResetRiskLevels();
			return true;
		}

		return false;
	}

	private bool CheckStopsAndTargets(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(Position);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(-Position);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(-Position);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			// Clear stored levels when the position is flat.
			ResetRiskLevels();
		}

		return false;
	}

	private void ApplyTrailingStop(ICandleMessage candle, decimal pipSize)
	{
		if (TrailingStopPips <= 0m)
			return;

		var entryPrice = _entryPrice;
		var threshold = (TrailingStopPips + TrailingStepPips) * pipSize;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > threshold)
			{
				var minStop = candle.ClosePrice - threshold;
				var candidate = candle.ClosePrice - TrailingStopPips * pipSize;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < minStop)
					_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > threshold)
			{
				var maxStop = candle.ClosePrice + threshold;
				var candidate = candle.ClosePrice + TrailingStopPips * pipSize;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > maxStop)
					_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, decimal pipSize, bool isLong)
	{
		_entryPrice = executionPrice;
		if (StopLossPips > 0m && pipSize > 0m)
			_stopLossPrice = isLong
				? executionPrice - StopLossPips * pipSize
				: executionPrice + StopLossPips * pipSize;
		else
			_stopLossPrice = null;

		if (TakeProfitPips > 0m && pipSize > 0m)
			_takeProfitPrice = isLong
				? executionPrice + TakeProfitPips * pipSize
				: executionPrice - TakeProfitPips * pipSize;
		else
			_takeProfitPrice = null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
			return 0.0001m;

		var step = security.PriceStep ?? 0.0001m;
		var decimals = security.Decimals;

		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private void UpdateMaBuffer(decimal emaValue)
	{
		_maValues.Enqueue(emaValue);

		var maxCount = Math.Max(MaShift + 1, 1);
		while (_maValues.Count > maxCount)
			_maValues.Dequeue();
	}

	private decimal? GetShiftedMaValue()
	{
		var count = _maValues.Count;
		var targetIndex = count - MaShift - 1;
		if (targetIndex < 0)
			return null;

		var index = 0;
		foreach (var value in _maValues)
		{
			if (index == targetIndex)
				return value;
			index++;
		}

		return null;
	}
}