在 GitHub 上查看

Farhad Crab 策略

概述

Farhad Crab 策略 是 MetaTrader 专家顾问 FarhadCrab1.mq4 的 StockSharp 高级 API 移植版。原始 EA 主要用于 M1 周期的 GBP/JPY、GBP/USD 与 EUR/USD,依靠快速的顺势突破与跟踪止损管理。本移植版本将其核心思想转换为 C#,结合短周期均线滤波、日线平滑均线安全阀以及自动化的止盈/移动止损流程。

策略在主交易周期同时计算两条均线:基于典型价的 9 周期 EMA 与基于开盘价的 9 周期 SMA。除此之外,还会在日线周期维护一条 55 周期的平滑移动平均线(SMMA)。当短周期条件显示价格强劲位于 EMA 之上且当前没有持仓时,策略建立多头;当最高价保持在开盘价 SMA 之下时,策略建立空头。日线 SMMA 用作风险控制:若其从下方上穿日线收盘价,则强制平掉所有多头;若从上方下穿,则平掉所有空头。

在离场管理方面,策略提供与原 EA 相同的可配置参数:以点数表示的多头/空头止盈距离,以及独立的移动止损触发。移动止损只有在价格领先一定距离后才会被激活,与 MetaTrader 实现保持一致。所有平仓动作均以市价指令完成,以适配 StockSharp 的事件驱动模型。

核心特性

  • 与原始 EA 相同的指标组合:典型价 9 周期 EMA、开盘价 9 周期 SMA、日线 55 周期 SMMA。
  • 多时间框架支持:同时订阅交易周期与日线,借助 StockSharp 自动计算指标,无需手动缓存数据。
  • 可配置离场:多/空止盈和移动止损都以点数设定,与原始外部输入一致,可用于参数优化。
  • 日线安全阀:完全复制 EA 的“日线均线穿越即平仓”规则,防止趋势反转时继续持仓。
  • 内置保护:在启动时调用 StartProtection(),遵循项目的风险管理要求。

参数

参数 描述 默认值
OrderVolume 新开仓使用的下单手数。 0.1
LongTakeProfitPips 多头止盈距离,单位为点。 10
ShortTakeProfitPips 空头止盈距离,单位为点。 10
LongTrailingStopPips 多头移动止损距离,设为 0 表示禁用。 8
ShortTrailingStopPips 空头移动止损距离,设为 0 表示禁用。 8
DailyMaPeriod 日线 SMMA 的周期长度。 55
CandleType 主交易周期,默认使用 1 分钟蜡烛。 1m

所有参数均通过 StrategyParam<T> 提供,并在需要时标记为可优化,方便在 StockSharp 优化器中批量测试。

交易规则

  1. 多头入场:当前蜡烛的最低价高于典型价 EMA,且当前无持仓时,开多。
  2. 空头入场:当前蜡烛的最高价低于开盘价 SMA,且当前无持仓时,开空。
  3. 多头保护性离场:当日线 SMMA 由下向上穿过日线收盘价(且之前在其下方)时,平掉所有多头。
  4. 空头保护性离场:当日线 SMMA 由上向下穿过日线收盘价(且之前在其上方)时,平掉所有空头。
  5. 止盈:价格达到设定的点数目标后立即平仓。
  6. 移动止损:价格向有利方向推进超过设定距离后开始跟踪,若回撤超过该距离则市价平仓。

实现细节

  • 代码完全依赖高层的 SubscribeCandles().Bind(...) 调用,不读取指标缓冲区,符合项目规范。
  • 点值根据证券的 PriceStep 计算,并对 3/5 位报价做 MetaTrader 风格的调整,以保证行为一致。
  • 止盈与移动止损通过监控价格并在触发时市价平仓完成,避免挂出额外的止损/止盈委托,更适合异步成交环境。
  • 在 OnReseted 中清空全部内部状态,确保多次启动或优化运行时不会遗留历史数据。

使用建议

  • 原策略针对高波动货币对的 M1 周期进行了优化。若希望保持类似表现,可使用默认周期并结合外汇主要货币对,当然也可以调整参数适配其它市场。
  • 策略始终保持单一持仓,便于回测与实盘监控,无需处理网格或加仓逻辑。
  • 若市场震荡幅度较大,可适当减小移动止损距离或仅依赖止盈离场,以避免频繁被“摇出”。
  • 日线 SMMA 是主要的风险控制工具,如希望更平滑的趋势过滤,可增加 DailyMaPeriod 的取值。

可测试公式: 每根完成的M1 K线计算 typical=(high+low+close)/3。EMA(9)以最初九个 typical 的平均值初始化,随后 EMA=(8*EMA+2*typical)/10;另一个 SMA(9) 使用 Open 价格。没有持仓或待执行订单时,low > EMA 则买入,否则 high < SMA(Open) 则卖出。每日 SMMA(55) 用55根完成日线初始化,随后 SMMA=(54*SMMA+close)/55,日收盘价穿越 SMMA 不利方向时平仓。止盈与追踪止损依据实际入场成交价,只按完成M1 K线评估;新的追踪价位从后续K线起生效。缩短 DailyMaPeriod 仅能测试交易机制,不能证明55天默认配置已验证。历史收益不保证未来表现。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Farhad Crab uses EMA(9) on typical M1 prices, SMA(9) of M1 opens,
/// and daily SMMA protection with direction-specific take/trailing exits.
/// </summary>
public class FarhadCrabStrategy : Strategy
{
	private readonly StrategyParam<int> _dailyMaPeriod;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _longTakeProfitPips;
	private readonly StrategyParam<decimal> _shortTakeProfitPips;
	private readonly StrategyParam<decimal> _longTrailingStopPips;
	private readonly StrategyParam<decimal> _shortTrailingStopPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _typicalSeed = new();
	private readonly Queue<decimal> _opens = new();
	private readonly Queue<decimal> _dailySeed = new();
	private decimal? _emaTypical;
	private decimal? _dailySmma;
	private decimal? _priorSmma;
	private decimal? _priorDailyClose;
	private decimal? _entryPrice;
	private decimal? _trailingLevel;
	private decimal _bestPrice;
	private Order _pendingOrder;
	private Order _entryOrder;

	public int DailyMaPeriod { get => _dailyMaPeriod.Value; set => _dailyMaPeriod.Value = value; }
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
	public decimal LongTakeProfitPips { get => _longTakeProfitPips.Value; set => _longTakeProfitPips.Value = value; }
	public decimal ShortTakeProfitPips { get => _shortTakeProfitPips.Value; set => _shortTakeProfitPips.Value = value; }
	public decimal LongTrailingStopPips { get => _longTrailingStopPips.Value; set => _longTrailingStopPips.Value = value; }
	public decimal ShortTrailingStopPips { get => _shortTrailingStopPips.Value; set => _shortTrailingStopPips.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public FarhadCrabStrategy()
	{
		_dailyMaPeriod = Param(nameof(DailyMaPeriod), 55).SetGreaterThanZero();
		_orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero();
		_longTakeProfitPips = Param(nameof(LongTakeProfitPips), 10m).SetNotNegative();
		_shortTakeProfitPips = Param(nameof(ShortTakeProfitPips), 10m).SetNotNegative();
		_longTrailingStopPips = Param(nameof(LongTrailingStopPips), 8m).SetNotNegative();
		_shortTrailingStopPips = Param(nameof(ShortTrailingStopPips), 8m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, TimeSpan.FromDays(1).TimeFrame())];

	protected override void OnReseted()
	{
		base.OnReseted();
		ClearState();
	}

	private void ClearState()
	{
		_typicalSeed.Clear();
		_opens.Clear();
		_dailySeed.Clear();
		_emaTypical = null;
		_dailySmma = null;
		_priorSmma = null;
		_priorDailyClose = null;
		_entryPrice = null;
		_trailingLevel = null;
		_bestPrice = 0m;
		_pendingOrder = null;
		_entryOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		if ((Security?.PriceStep ?? 0m) <= 0m)
			throw new InvalidOperationException("Farhad Crab requires a positive instrument PriceStep.");
		SubscribeCandles(TimeSpan.FromDays(1).TimeFrame()).Bind(ProcessDailyCandle).Start();
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private decimal PipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return Security?.Decimals is 3 or 5 ? step * 10m : step;
	}

	private bool Pending => _pendingOrder is not null &&
		_pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);

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

		var priorSmma = _dailySmma;
		var priorClose = _priorDailyClose;
		if (_dailySmma is null)
		{
			_dailySeed.Enqueue(candle.ClosePrice);
			if (_dailySeed.Count == DailyMaPeriod)
				_dailySmma = _dailySeed.Average();
		}
		else
			_dailySmma = (_dailySmma.Value * (DailyMaPeriod - 1) + candle.ClosePrice) / DailyMaPeriod;

		_priorSmma = priorSmma;
		_priorDailyClose = candle.ClosePrice;

		if (Pending || Position == 0m || priorSmma is null || priorClose is null || _dailySmma is null)
			return;

		if (Position > 0m && priorSmma < priorClose && _dailySmma > candle.ClosePrice ||
			Position < 0m && priorSmma > priorClose && _dailySmma < candle.ClosePrice)
			Exit("Farhad Crab daily safety exit");
	}

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

		var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
		if (_emaTypical is null)
		{
			_typicalSeed.Enqueue(typical);
			if (_typicalSeed.Count == 9)
				_emaTypical = _typicalSeed.Average();
		}
		else
			_emaTypical = (_emaTypical.Value * 8m + typical * 2m) / 10m;

		_opens.Enqueue(candle.OpenPrice);
		while (_opens.Count > 9)
			_opens.Dequeue();

		if (Pending)
			return;
		if (Position != 0m)
		{
			HandleRisk(candle);
			return;
		}
		if (_emaTypical is null || _opens.Count < 9)
			return;

		if (candle.LowPrice > _emaTypical.Value)
			Enter(Sides.Buy);
		else if (candle.HighPrice < _opens.Average())
			Enter(Sides.Sell);
	}

	private void Enter(Sides side)
	{
		var volume = OrderVolume;
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(min, volume);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(max, volume);
		if (volume > 0m)
			Submit(side, volume, "Farhad Crab entry");
	}

	private void HandleRisk(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entry)
			return;
		var positive = Position > 0m;
		var pip = PipSize();
		var target = (positive ? LongTakeProfitPips : ShortTakeProfitPips) * pip;
		if (target > 0m && (positive ? candle.HighPrice >= entry + target :
			candle.LowPrice <= entry - target))
		{
			Exit("Farhad Crab take profit");
			return;
		}

		if (_trailingLevel is decimal level && (positive ? candle.LowPrice <= level :
			candle.HighPrice >= level))
		{
			Exit("Farhad Crab trailing stop");
			return;
		}

		var distance = (positive ? LongTrailingStopPips : ShortTrailingStopPips) * pip;
		if (distance <= 0m)
			return;

		_bestPrice = positive ? Math.Max(_bestPrice, candle.HighPrice) :
			Math.Min(_bestPrice, candle.LowPrice);
		var move = positive ? _bestPrice - entry : entry - _bestPrice;
		if (move < distance)
			return;
		var next = positive ? _bestPrice - distance : _bestPrice + distance;
		if (_trailingLevel is null || (positive ? next > _trailingLevel.Value : next < _trailingLevel.Value))
			_trailingLevel = next;
	}

	private void Exit(string reason)
	{
		if (!Pending && Position != 0m)
			Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), reason);
	}

	private void Submit(Sides side, decimal volume, string comment)
	{
		_pendingOrder = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		if (comment == "Farhad Crab entry")
			_entryOrder = _pendingOrder;
		RegisterOrder(_pendingOrder);
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Order == _entryOrder && trade.Trade is not null)
		{
			_entryPrice = trade.Trade.Price;
			_bestPrice = _entryPrice.Value;
			_trailingLevel = null;
		}
		else if (Position == 0m)
		{
			_entryPrice = null;
			_trailingLevel = null;
			_bestPrice = 0m;
		}
	}
}