首页
/
策略示例
在 GitHub 上查看
MO Bidir 对冲策略
概述
MO Bidir 对冲策略 是 MetaTrader 4 智能交易程序 mo_bidir_v0_1 的 StockSharp 移植版本。原始 EA 在 5 分钟图上运行,每根新 K 线都会同时开多、开空并附带固定的止损/止盈距离,以维持一个完全对冲的仓位结构。移植版本在遵循项目“仅处理收盘 K 线”的约束下,使用高层 API 和点值参数重新实现这一思路。
交易逻辑
订阅配置的 K 线类型(默认 5 分钟)并仅在 K 线收盘后处理数据。
每根 K 线结束时检查内部的对冲腿列表。如果仍有腿处于持仓状态,策略保持等待,直到保护条件触发后才会重新开仓。
当没有任何腿处于激活状态时,同时提交等量的市价买单与市价卖单。每个成交的订单都成为一个独立的“对冲腿”。
在订单成交后,依据设定的点数参数与标的的 PriceStep(若不可用则使用最小价格增量)计算止损与止盈价格。
在随后的每根收盘 K 线中,策略利用该 K 线的最高价与最低价判断保护条件:
多头腿若最低价触及止损则通过市价卖出平仓;若未触及止损而最高价达到目标,则市价卖出获利了结。
空头腿若最高价触及止损则市价买入平仓;若未触及止损而最低价达到目标,则市价买入获利了结。
如果同一根 K 线同时包含止损价与止盈价,则优先判定为止损触发,以匹配 MT4 中价格先触及止损即被立即平仓的逻辑。
当全部腿都被止损或止盈平仓后,策略在下一根收盘 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
class mo_bidir_strategy(Strategy):
def __init__(self):
super(mo_bidir_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._fast_ema_length = self.Param("FastEmaLength", 10) \
.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators")
self._slow_ema_length = self.Param("SlowEmaLength", 25) \
.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def FastEmaLength(self):
return self._fast_ema_length.Value
@property
def SlowEmaLength(self):
return self._slow_ema_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(mo_bidir_strategy, self).OnStarted2(time)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._fast_ema = ExponentialMovingAverage()
self._fast_ema.Length = self.FastEmaLength
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowEmaLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._fast_ema, self._slow_ema, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, fast_val, slow_val, atr_val):
if candle.State != CandleStates.Finished:
return
fv = float(fast_val)
sv = float(slow_val)
av = float(atr_val)
if self._prev_fast == 0 or self._prev_slow == 0 or av <= 0:
self._prev_fast = fv
self._prev_slow = sv
return
close = float(candle.ClosePrice)
if self.Position > 0:
if (fv < sv and self._prev_fast >= self._prev_slow) or close <= self._entry_price - av * 2.0:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if (fv > sv and self._prev_fast <= self._prev_slow) or close >= self._entry_price + av * 2.0:
self.BuyMarket()
self._entry_price = 0.0
if self.Position == 0:
if fv > sv and self._prev_fast <= self._prev_slow:
self._entry_price = close
self.BuyMarket()
elif fv < sv and self._prev_fast >= self._prev_slow:
self._entry_price = close
self.SellMarket()
self._prev_fast = fv
self._prev_slow = sv
def OnReseted(self):
super(mo_bidir_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
def CreateClone(self):
return mo_bidir_strategy()