在 GitHub 上查看

Moving Average Shift 策略

概述

该策略是将 MetaTrader 4 自带的 Moving Average 专家顾问移植到 StockSharp 高层 API 的版本。系统通过对比完成收盘的 K 线与平移后的简单移动平均线(SMA)来识别趋势变化,所有交易都采用市价单,并且同一时刻只保留一笔持仓。

交易逻辑

  1. 订阅配置的蜡烛图时间框架(默认 5 分钟),并按设定周期计算 SMA。
  2. 将 SMA 向后平移指定数量的已完成蜡烛,以复现 iMA 函数的 shift 参数效果。
  3. 评估上一根完成蜡烛:
    • 看涨穿越(开盘价在平移后的 SMA 下方、收盘价在上方)且当前无持仓时,开多单。
    • 看跌穿越(开盘价在上方、收盘价在下方)且当前无持仓时,开空单。
  4. 按相同规则管理离场:
    • 最近蜡烛向下穿越 SMA 时平掉多头。
    • 最近蜡烛向上穿越 SMA 时平掉空头。
  5. 策略最多只维护一笔仓位,遵循原始 EA 轮换买卖的模式。

参数

名称 说明 默认值
CandleType 用于计算的蜡烛图数据类型,可选择任意时间框架。 5 分钟
MovingPeriod SMA 的周期长度(单位:根蜡烛)。 12
MovingShift SMA 的平移距离(单位:已完成蜡烛数),等效于 iMAshift 参数。 6
BaseVolume 开仓使用的基础手数,长短方向共用。 1

指标处理

  • OnStarted 中创建 SimpleMovingAverage 指标,并通过高层 Bind API 将其绑定到蜡烛订阅。
  • 利用固定长度的先进先出队列缓存 SMA 输出,从而得到 MovingShift 根蜡烛之前的值,无需自行重算指标。
  • 队列最多保存 MovingShift + 1 个值,即使设置较大的平移距离也不会增加内存压力。

下单与风控

  • 使用 BuyMarket / SellMarket 提交市价单,手数由 BaseVolume 控制;平仓时根据当前仓位数量一次性反向下单。
  • 原始 MetaTrader 版本会根据账户可用保证金和连续亏损自动调整手数。为保持稳定可移植性,此移植版本保留核心信号,并把仓位控制交给用户通过参数配置。

转换说明

  • 信号基于上一根完成蜡烛生成,对应 MT4 中 Volume[0] == 1 的检查(即等待新柱开始再处理上一柱)。
  • 仅处理 CandleStates.Finished 状态的蜡烛,避免在未收盘的情况下提前交易。
  • 如果图表区域可用,策略会绘制蜡烛、SMA 曲线以及成交标记,方便可视化验证。

使用步骤

  1. 在 StockSharp Designer、Shell 或 Runner 中编译该策略。
  2. 选择交易品种并绑定投资组合。
  3. 按需要调整参数(时间框架、周期、基础手数等)。
  4. 启动策略,系统会自动订阅蜡烛数据并依据 SMA 穿越信号进行交易。

拓展建议

  • 通过 StartProtection 添加止损和止盈,提高风险控制能力。
  • 将简单 SMA 替换为 EMA 等其他均线,或引入额外过滤指标,进一步优化信号质量。
  • GetEntryVolume 方法中实现动态加仓/减仓策略。
using System;

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

namespace StockSharp.Samples.Strategies;

public class MovingAverageShiftStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _cooldownCandles;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevClose;
	private decimal _prevEma;
	private bool _hasPrev;
	private int _cooldownRemaining;

	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
	public int CooldownCandles { get => _cooldownCandles.Value; set => _cooldownCandles.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public MovingAverageShiftStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 50).SetDisplay("EMA Period", "EMA lookback", "Indicators");
		_cooldownCandles = Param(nameof(CooldownCandles), 200).SetDisplay("Cooldown", "Candles between signals", "General");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame()).SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = default;
		_prevEma = default;
		_hasPrev = default;
		_cooldownRemaining = default;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevClose = 0;
		_prevEma = 0;
		_hasPrev = false;
		_cooldownRemaining = 0;

		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ema, ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal ema)
	{
		if (candle.State != CandleStates.Finished) return;
		var close = candle.ClosePrice;
		if (!_hasPrev) { _prevClose = close; _prevEma = ema; _hasPrev = true; return; }

		if (_cooldownRemaining > 0)
		{
			_cooldownRemaining--;
			_prevClose = close;
			_prevEma = ema;
			return;
		}

		if (_prevClose <= _prevEma && close > ema && Position <= 0)
		{
			if (Position < 0) BuyMarket();
			BuyMarket();
			_cooldownRemaining = CooldownCandles;
		}
		else if (_prevClose >= _prevEma && close < ema && Position >= 0)
		{
			if (Position > 0) SellMarket();
			SellMarket();
			_cooldownRemaining = CooldownCandles;
		}
		_prevClose = close;
		_prevEma = ema;
	}
}