在 GitHub 上查看

MO Bidir 对冲策略

概述

MO Bidir 对冲策略 是 MetaTrader 4 智能交易程序 mo_bidir_v0_1 的 StockSharp 移植版本。原始 EA 在 5 分钟图上运行,每根新 K 线都会同时开多、开空并附带固定的止损/止盈距离,以维持一个完全对冲的仓位结构。移植版本在遵循项目“仅处理收盘 K 线”的约束下,使用高层 API 和点值参数重新实现这一思路。

交易逻辑

  1. 订阅配置的 K 线类型(默认 5 分钟)并仅在 K 线收盘后处理数据。
  2. 每根 K 线结束时检查内部的对冲腿列表。如果仍有腿处于持仓状态,策略保持等待,直到保护条件触发后才会重新开仓。
  3. 当没有任何腿处于激活状态时,同时提交等量的市价买单与市价卖单。每个成交的订单都成为一个独立的“对冲腿”。
  4. 在订单成交后,依据设定的点数参数与标的的 PriceStep(若不可用则使用最小价格增量)计算止损与止盈价格。
  5. 在随后的每根收盘 K 线中,策略利用该 K 线的最高价与最低价判断保护条件:
    • 多头腿若最低价触及止损则通过市价卖出平仓;若未触及止损而最高价达到目标,则市价卖出获利了结。
    • 空头腿若最高价触及止损则市价买入平仓;若未触及止损而最低价达到目标,则市价买入获利了结。
    • 如果同一根 K 线同时包含止损价与止盈价,则优先判定为止损触发,以匹配 MT4 中价格先触及止损即被立即平仓的逻辑。
  6. 当全部腿都被止损或止盈平仓后,策略在下一根收盘 K 线立刻准备新的对冲组合。

上述流程完全遵循原策略的思想,同时仅依赖 BuyMarket/SellMarket 等高层 API 来实现订单管理。

参数

参数 说明
TradeVolume 双边同时使用的下单数量,必须为正值。
StopLossPoints 距离开仓价的止损点数,单位为标的的最小价格变动。设为 0 可关闭止损。
TakeProfitPoints 距离开仓价的止盈点数。设为 0 可关闭止盈。
CandleType 用于检测新 K 线的时间框架,默认 5 分钟。

所有点数都会乘以标的的 PriceStep 转化为绝对价格;若没有可用的步长信息,则退回到 MinPriceStep,二者都缺失时保护价位保持未激活状态。

风险管理

  • 两个方向使用完全相同的固定手数,并依赖对称的止损/止盈保护。
  • 默认止损 80 点、止盈 750 点,与原始 EA 中“8 pips vs. 75 pips”的设定保持一致(以 5 位报价外汇品种为例)。
  • 保护条件触发时通过市价单立即平仓,从而释放保证金并让剩余的腿继续运行直到自己的保护价被触发。

实现细节

  • 策略仅在 K 线收盘后做出决策;若回测缺乏逐笔数据,则当同一根 K 线同时触及止损与止盈时,会按照“止损优先”假设处理。
  • 内部维护一个对冲腿列表,用以跟踪每个方向的成交均价和剩余持仓量,即使净仓位为零也能模拟 MT4 中多空同时持仓的行为。
  • 未额外添加移动止损、时间过滤或指标过滤,移植版本保持与原 EA 相同的极简结构。

使用建议

  • 根据交易品种调整 TradeVolume,并确认所使用的账户支持双向持仓(若交易所或经纪商要求)。
  • 若需要按照“点(pip)”维度设置参数,请先将目标 pips 换算为对应的点数后再填入策略参数。
  • 如需额外的资金或风险控制,可结合 StockSharp 的 StartProtection 或其他风险模块一起使用。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Mo Bidir: EMA crossover with ATR stops.
/// </summary>
public class MoBidirStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;

	public MoBidirStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_fastEmaLength = Param(nameof(FastEmaLength), 10)
			.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators");
		_slowEmaLength = Param(nameof(SlowEmaLength), 25)
			.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
	public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

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

		_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastEma, slowEma, atr, ProcessCandle).Start();
		var area = CreateChartArea();
		if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if ((fastVal < slowVal && _prevFast >= _prevSlow) || close <= _entryPrice - atrVal * 2m) { SellMarket(); _entryPrice = 0; }
		}
		else if (Position < 0)
		{
			if ((fastVal > slowVal && _prevFast <= _prevSlow) || close >= _entryPrice + atrVal * 2m) { BuyMarket(); _entryPrice = 0; }
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow) { _entryPrice = close; BuyMarket(); }
			else if (fastVal < slowVal && _prevFast >= _prevSlow) { _entryPrice = close; SellMarket(); }
		}
		_prevFast = fastVal; _prevSlow = slowVal;
	}
}