在 GitHub 上查看
Exp X2MA Candle MM Recovery 策略
概览
本策略是 MetaTrader 专家顾问 Exp_X2MACandle_MMRec 的 C# 版本。它通过原始 X2MA 指标得到的双重平滑蜡烛颜色来管理仓位,当颜色发生变化时开仓或平仓。同时,策略记录最近的交易结果,在连续亏损时自动降低下单手数,实现一个简化的资金管理机制。
算法只处理已经收盘的蜡烛。订阅用户设定的时间框架,对每根蜡烛的四个价格(开高低收)连续应用两种可配置的移动平均。随后按照平滑后的开收价计算蜡烛颜色:开盘价 < 收盘价 → 2(看多),开盘价 > 收盘价 → 0(看空),否则为 1(中性)。默认情况下信号会在向前两根完成蜡烛上评估,避免在当前蜡烛尚未完全形成时重复下单。
指标细节
- 蜡烛的四个价格序列都会经过两级移动平均;每一级的方式和周期可以单独设置。
- 平滑方式与 StockSharp 指标的映射关系:
Simple → SimpleMovingAverage
Exponential → ExponentialMovingAverage
Smoothed → SmoothedMovingAverage
Weighted → WeightedMovingAverage
Jurik → JurikMovingAverage(如果库暴露 Phase 属性,则会写入 MQL 中的相位参数)。
- 当平滑后的开收价绝对差值小于
GapPoints * StepPrice 时,开盘价会被替换为上一根蜡烛的收盘价,使得身体长度为零。
- 根据信号条数
SignalBar(默认 1)向后取颜色序列:如果两根前的颜色为 2 且前一根不为 2,策略认为出现做多信号;颜色为 0 时产生做空信号,同时也可以选择性地触发相反方向的平仓。
资金管理
- 原版顾问通过 MetaTrader 历史成交信息来判断是否需要减小手数。StockSharp 无法访问同样的接口,因此移植版在内部维护一条固定长度的队列,记录最近
HistoryDepth 笔已关闭交易的盈亏情况。
- 当队列中的亏损数量达到
LossTrigger 时,下一笔交易的数量切换为 ReducedVolume,否则使用 NormalVolume。
- 平仓结果通过触发信号时蜡烛的收盘价进行估算。原策略中的止损/止盈指令没有自动迁移,如需保护仓位,可使用 StockSharp 的
StartProtection 或其他风控组件。
参数说明
| 参数 |
说明 |
CandleType |
进行平滑和交易的蜡烛时间框架。 |
FirstMethod / FirstLength / FirstPhase |
第一层移动平均的方式、周期及 Jurik 相位。 |
SecondMethod / SecondLength / SecondPhase |
第二层移动平均的方式、周期及相位。 |
GapPoints |
以最小价格变动为单位的身体压扁阈值。 |
SignalBar |
读取颜色时向后偏移的蜡烛数量。 |
AllowLongEntry / AllowShortEntry |
是否允许开多/开空。 |
AllowLongExit / AllowShortExit |
是否允许平多/平空。 |
NormalVolume |
正常情况下的下单数量。 |
ReducedVolume |
达到亏损阈值后使用的下单数量。 |
HistoryDepth |
参与统计的最近交易数量,设为 0 可关闭该机制。 |
LossTrigger |
触发减仓的亏损笔数,设为 0 表示始终使用正常手数。 |
使用提示
- 策略只针对一个证券运行,并在每根已完成蜡烛时执行逻辑,避免在同一信号上多次下单。
- 如果希望保留历史统计但不降低手数,可把
ReducedVolume 设置为与 NormalVolume 相同,或者把 LossTrigger 设为 0。
- 由于盈亏判断基于蜡烛收盘价,实际平台上的滑点或部分成交可能导致结果与 MetaTrader 有轻微差异,请根据实盘情况调整参数。
- 需要止损或止盈时,请结合 StockSharp 的风控模块添加相应设置。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class ExpX2MaCandleMmRecStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private decimal? _prevFast, _prevSlow;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public ExpX2MaCandleMmRecStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame()).SetDisplay("Candle Type", "Timeframe", "General");
_fastPeriod = Param(nameof(FastPeriod), 8).SetGreaterThanZero().SetDisplay("Fast WMA", "Fast WMA period", "Indicators");
_slowPeriod = Param(nameof(SlowPeriod), 25).SetGreaterThanZero().SetDisplay("Slow WMA", "Slow WMA period", "Indicators");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevFast = null;
_prevSlow = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevFast = null; _prevSlow = null;
var fast = new WeightedMovingAverage { Length = FastPeriod };
var slow = new WeightedMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(fast, slow, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fast); DrawIndicator(area, slow); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished) return;
if (!IsFormedAndOnlineAndAllowTrading()) { _prevFast = fast; _prevSlow = slow; return; }
if (_prevFast == null || _prevSlow == null) { _prevFast = fast; _prevSlow = slow; return; }
var prevAbove = _prevFast.Value > _prevSlow.Value;
var currAbove = fast > slow;
_prevFast = fast; _prevSlow = slow;
if (!prevAbove && currAbove && Position <= 0) { if (Position < 0) BuyMarket(); BuyMarket(); }
else if (prevAbove && !currAbove && Position >= 0) { if (Position > 0) SellMarket(); SellMarket(); }
}
}
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.Indicators import WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
class exp_x2_ma_candle_mm_rec_strategy(Strategy):
def __init__(self):
super(exp_x2_ma_candle_mm_rec_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._fast_period = self.Param("FastPeriod", 8) \
.SetDisplay("Fast WMA", "Fast WMA period", "Indicators")
self._slow_period = self.Param("SlowPeriod", 25) \
.SetDisplay("Slow WMA", "Slow WMA period", "Indicators")
self._prev_fast = None
self._prev_slow = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def FastPeriod(self):
return self._fast_period.Value
@property
def SlowPeriod(self):
return self._slow_period.Value
def OnReseted(self):
super(exp_x2_ma_candle_mm_rec_strategy, self).OnReseted()
self._prev_fast = None
self._prev_slow = None
def OnStarted2(self, time):
super(exp_x2_ma_candle_mm_rec_strategy, self).OnStarted2(time)
self._prev_fast = None
self._prev_slow = None
fast = WeightedMovingAverage()
fast.Length = self.FastPeriod
slow = WeightedMovingAverage()
slow.Length = self.SlowPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast, slow, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, fast)
self.DrawIndicator(area, slow)
self.DrawOwnTrades(area)
def _on_process(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fv = float(fast_value)
sv = float(slow_value)
if self._prev_fast is None or self._prev_slow is None:
self._prev_fast = fv
self._prev_slow = sv
return
prev_above = self._prev_fast > self._prev_slow
curr_above = fv > sv
self._prev_fast = fv
self._prev_slow = sv
if not prev_above and curr_above and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif prev_above and not curr_above and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
def CreateClone(self):
return exp_x2_ma_candle_mm_rec_strategy()