在 GitHub 上查看

FiftyFiveMedianSlopeStrategy

来源

  • 由 MetaTrader 4 智能交易系统 55_MA_med_FIN.mq4 转换而来。
  • 以 55 周期中位数移动平均线的斜率为核心,基于蜡烛的中位价进行计算。

交易逻辑

  • 订阅所选的K线序列(默认 1 小时时间框)并只处理已经完成的蜡烛。
  • 在每根蜡烛上计算指定方法(SMA、EMA、SMMA 或 LWMA)的移动平均值,输入价格为中位价 ((High + Low) / 2)。
  • 使用环形缓冲区保存最近的移动平均值,以便比较一根柱前的数值与 MaShift 根柱前的数值。
  • 当一根柱前的数值大于 MaShift 根柱前的数值时:
    • 首先平掉所有空头仓位。
    • 若未超过 MaxOrders 限制,则按基础手数开多单。
  • 当一根柱前的数值小于 MaShift 根柱前的数值时,流程镜像用于开空。
  • 通过内部标志交替信号:只有在出现反向信号后才会再次在同一方向建仓,防止重复触发。
  • 仅当蜡烛开盘时间满足 StartHour < hour < EndHour 时才允许交易(端点为严格不等号,以匹配原版 EA)。

仓位与风控

  • FixedVolume 指定每次市价单的基础手数。当其为 0 时,策略按 RiskPercentage 所占资金比例计算下单量。
  • MaxOrders 限制同方向累加的次数,0 表示无限制。
  • 可选的 StopLossPointsTakeProfitPoints 通过 StartProtection 以价格步长设置止损/止盈距离,再现 MT4 行为。

参数说明

  • FixedVolume – 固定下单手数;为 0 时启用资金占比模式。
  • RiskPercentage – 当固定手数为 0 时,按账户资金比例确定下单量。
  • TakeProfitPoints / StopLossPoints – 以价格步长表示的止盈、止损距离。
  • MaPeriod – 中位数移动平均的周期长度(默认 55)。
  • MaShift – 比较所用的柱数间隔(默认 13)。
  • MaMethod – 移动平均算法:简单、指数、平滑或线性加权。
  • StartHour / EndHour – 交易时间窗口(0–23 小时,端点不包含)。
  • MaxOrders – 同方向最多允许的建仓次数。
  • CandleType – 参与信号计算的时间框。

使用提示

  • 确认标的证券提供有效的 PriceStep 与成交量梯度,以保证手数对齐符合交易所规则。
  • 资金占比模式使用组合当前权益与最新收盘价;若任一数值缺失,则不会下单。
  • 策略在开仓前总是先平掉反向持仓,从而与原 MT4 脚本的行为保持一致。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Fifty Five Median Slope: EMA slope direction with ATR stops.
/// </summary>
public class FiftyFiveMedianSlopeStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<int> _slopeShift;

	private decimal _entryPrice;
	private decimal _prevEma;
	private int _barCount;
	private readonly decimal[] _emaHistory = new decimal[20];

	public FiftyFiveMedianSlopeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_emaLength = Param(nameof(EmaLength), 55)
			.SetDisplay("EMA Length", "Moving average period.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");

		_slopeShift = Param(nameof(SlopeShift), 13)
			.SetDisplay("Slope Shift", "Bars between slope comparison.", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	public int SlopeShift
	{
		get => _slopeShift.Value;
		set => _slopeShift.Value = value;
	}

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

		_entryPrice = 0;
		_prevEma = 0;
		_barCount = 0;
		Array.Clear(_emaHistory, 0, _emaHistory.Length);
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_entryPrice = 0;
		_prevEma = 0;
		_barCount = 0;

		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ema, atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}

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

		var len = Math.Min(SlopeShift + 1, _emaHistory.Length);
		var idx = _barCount % len;
		_emaHistory[idx] = emaVal;
		_barCount++;

		if (_barCount < len || atrVal <= 0)
			return;

		var shiftIdx = (_barCount - SlopeShift) % len;
		if (shiftIdx < 0) shiftIdx += len;
		var shiftedEma = _emaHistory[shiftIdx];

		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 3m || close <= _entryPrice - atrVal * 1.5m || emaVal < shiftedEma)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 3m || close >= _entryPrice + atrVal * 1.5m || emaVal > shiftedEma)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (emaVal > shiftedEma && _prevEma <= shiftedEma)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (emaVal < shiftedEma && _prevEma >= shiftedEma)
			{
				_entryPrice = close;
				SellMarket();
			}
		}

		_prevEma = emaVal;
	}
}