MARE5.1 移位均线策略
概述
MARE5.1 移位均线策略 是 MetaTrader 5 专家顾问 “MARE5.1” 的 StockSharp 高级 API 版本。策略默认使用 1 分钟 K 线(可配置),对比具有相同前移偏移量的两条简单移动平均线(SMA),并利用历史 SMA 关系以及上一根蜡烛的方向来确认信号。
交易逻辑
- 策略同时计算快线和慢线两条 SMA,二者都应用相同的前移偏移量,从而复现原始 EA 的行为。
- 做空 条件:
- 当前蜡烛上慢线至少比快线高一个最小价位。
- 两根蜡烛之前快线至少比慢线高一个最小价位。
- 五根蜡烛之前快线至少比慢线高一个最小价位。
- 最新完成的蜡烛(上一根)为阴线。
- 做多 条件为上述条件的镜像:
- 当前蜡烛上快线至少比慢线高一个最小价位。
- 两根蜡烛之前慢线至少比快线高一个最小价位。
- 五根蜡烛之前慢线至少比快线高一个最小价位。
- 最新完成的蜡烛(上一根)为阳线。
- 同一时间只允许持有一笔仓位,默认下单数量由
TradeVolume参数控制。 - 只有在设定的交易时间窗口(包含起止小时)内才会开仓,完全还原原始 EA 中按小时过滤交易的逻辑。
风险控制
策略保留了原始版本的固定止盈与止损距离。TakeProfitPips 与 StopLossPips 以“点”(对三位和五位小数报价会自动放大 10 倍)表示,并在启动时转换为绝对价格距离。通过 StartProtection 使用市价单方式管理保护性平仓。
指标与数据
- 快线 SMA – 周期由
FastPeriod决定。 - 慢线 SMA – 周期由
SlowPeriod决定。 - 数据源 – 默认使用 1 分钟蜡烛,可通过
CandleType参数选择任意 StockSharp 支持的蜡烛类型。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
TradeVolume |
0.01 | 每次进场的下单数量。 |
TakeProfitPips |
35 | 止盈距离(调整后的点数)。设为 0 可禁用。 |
StopLossPips |
55 | 止损距离(调整后的点数)。设为 0 可禁用。 |
FastPeriod |
14 | 快线 SMA 的周期。 |
SlowPeriod |
79 | 慢线 SMA 的周期。 |
MovingAverageShift |
4 | 应用于两条 SMA 的前移位移(单位:根数)。 |
SessionOpenHour |
2 | 允许交易的起始小时(0–23)。 |
SessionCloseHour |
3 | 允许交易的结束小时(0–23),必须大于 SessionOpenHour。 |
CandleType |
1 分钟蜡烛 | 策略使用的蜡烛数据类型。 |
备注
- 策略仅在蜡烛收盘后评估信号。内部缓冲 SMA 历史值,以复现原始 MQL 代码中按索引读取指标的方式。
- 使用标的的最小价格跳动(PriceStep)来比较两条 SMA 的差距,确保满足至少一个最小价位的要求。
- 止盈止损基于最小价格跳动计算,对三位与五位报价自动将点值放大 10 倍,与 MetaTrader 的处理方式一致。
- 策略不会加仓或网格化,必须等待当前仓位全部平掉后才会寻找下一次入场机会。
- 本目录仅包含 C# 实现版本,本策略暂未提供 Python 版本。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MARE5.1 strategy that trades shifted SMA crossovers with time filtering.
/// </summary>
public class Mare51Strategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _movingAverageShift;
private readonly StrategyParam<int> _sessionOpenHour;
private readonly StrategyParam<int> _sessionCloseHour;
private readonly StrategyParam<DataType> _candleType;
private SMA _fastSma;
private SMA _slowSma;
private decimal?[] _fastBuffer;
private decimal?[] _slowBuffer;
private ICandleMessage _previousCandle;
private decimal _pipSize;
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
public int MovingAverageShift
{
get => _movingAverageShift.Value;
set => _movingAverageShift.Value = value;
}
public int SessionOpenHour
{
get => _sessionOpenHour.Value;
set => _sessionOpenHour.Value = value;
}
public int SessionCloseHour
{
get => _sessionCloseHour.Value;
set => _sessionCloseHour.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public Mare51Strategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.01m)
.SetDisplay("Volume", "Default order volume", "Trading")
.SetGreaterThanZero();
_takeProfitPips = Param(nameof(TakeProfitPips), 35m)
.SetDisplay("Take Profit (pips)", "Take profit distance in adjusted pips", "Risk")
.SetNotNegative();
_stopLossPips = Param(nameof(StopLossPips), 55m)
.SetDisplay("Stop Loss (pips)", "Stop loss distance in adjusted pips", "Risk")
.SetNotNegative();
_fastPeriod = Param(nameof(FastPeriod), 14)
.SetDisplay("Fast Period", "Fast SMA period", "Indicators")
.SetGreaterThanZero();
_slowPeriod = Param(nameof(SlowPeriod), 20)
.SetDisplay("Slow Period", "Slow SMA period", "Indicators")
.SetGreaterThanZero();
_movingAverageShift = Param(nameof(MovingAverageShift), 1)
.SetDisplay("MA Shift", "Forward shift applied to both SMAs", "Indicators")
.SetNotNegative();
_sessionOpenHour = Param(nameof(SessionOpenHour), 0)
.SetDisplay("Session Open Hour", "Inclusive start hour for trading", "Session")
.SetRange(0, 23);
_sessionCloseHour = Param(nameof(SessionCloseHour), 23)
.SetDisplay("Session Close Hour", "Inclusive end hour for trading", "Session")
.SetRange(0, 23);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candle data type", "Data");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_fastSma = null;
_slowSma = null;
_fastBuffer = null;
_slowBuffer = null;
_previousCandle = null;
_pipSize = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (SessionOpenHour >= SessionCloseHour)
throw new InvalidOperationException("SessionOpenHour must be less than SessionCloseHour.");
Volume = TradeVolume;
_fastSma = new SMA { Length = FastPeriod };
_slowSma = new SMA { Length = SlowPeriod };
_fastBuffer = new decimal?[MovingAverageShift + 6];
_slowBuffer = new decimal?[MovingAverageShift + 6];
_pipSize = CalculatePipSize();
var takeProfitUnit = TakeProfitPips > 0m
? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute)
: new Unit(0m);
var stopLossUnit = StopLossPips > 0m
? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute)
: new Unit(0m);
StartProtection(stopLossUnit, takeProfitUnit);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastSma, _slowSma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastSma);
DrawIndicator(area, _slowSma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (_fastBuffer == null || _slowBuffer == null)
return;
// Shift raw SMA values so we can later access shifted indexes.
for (var i = _fastBuffer.Length - 1; i > 0; i--)
{
_fastBuffer[i] = _fastBuffer[i - 1];
_slowBuffer[i] = _slowBuffer[i - 1];
}
_fastBuffer[0] = fastValue;
_slowBuffer[0] = slowValue;
var previousCandle = _previousCandle;
_previousCandle = candle;
//if (!IsFormedAndOnlineAndAllowTrading())
// return;
if (previousCandle == null)
return;
if (_fastSma == null || _slowSma == null)
return;
if (!_fastSma.IsFormed || !_slowSma.IsFormed)
return;
var fast0 = GetShiftedValue(_fastBuffer, 0);
var fast2 = GetShiftedValue(_fastBuffer, 2);
var fast5 = GetShiftedValue(_fastBuffer, 5);
var slow0 = GetShiftedValue(_slowBuffer, 0);
var slow2 = GetShiftedValue(_slowBuffer, 2);
var slow5 = GetShiftedValue(_slowBuffer, 5);
if (fast0 is not decimal f0 || fast2 is not decimal f2 || fast5 is not decimal f5 ||
slow0 is not decimal s0 || slow2 is not decimal s2 || slow5 is not decimal s5)
{
return;
}
if (!IsWithinSession(candle.OpenTime))
return;
var bearishPrevious = previousCandle.ClosePrice < previousCandle.OpenPrice;
var bullishPrevious = previousCandle.ClosePrice > previousCandle.OpenPrice;
var sellSignal = f5 >= s5 && f2 < s2 && f0 < s0 && bearishPrevious;
var buySignal = f5 <= s5 && f2 > s2 && f0 > s0 && bullishPrevious;
if (Position != 0)
return;
if (sellSignal)
{
// Enter short when slow SMA overtakes the fast SMA and previous bars confirm the reversal.
SellMarket();
}
else if (buySignal)
{
// Enter long when fast SMA overtakes the slow SMA and previous bars confirm the reversal.
BuyMarket();
}
}
private decimal? GetShiftedValue(decimal?[] buffer, int index)
{
var targetIndex = index + MovingAverageShift;
if (buffer == null)
return null;
if (targetIndex < 0 || targetIndex >= buffer.Length)
return null;
return buffer[targetIndex];
}
private bool IsWithinSession(DateTimeOffset time)
{
var hour = time.Hour;
return hour >= SessionOpenHour && hour <= SessionCloseHour;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
var scale = GetDecimalScale(step);
return (scale == 3 || scale == 5) ? step * 10m : step;
}
private static int GetDecimalScale(decimal value)
{
var bits = decimal.GetBits(value);
return (bits[3] >> 16) & 0xFF;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class mare51_strategy(Strategy):
def __init__(self):
super(mare51_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.01)
self._take_profit_pips = self.Param("TakeProfitPips", 35.0)
self._stop_loss_pips = self.Param("StopLossPips", 55.0)
self._fast_period = self.Param("FastPeriod", 14)
self._slow_period = self.Param("SlowPeriod", 20)
self._ma_shift = self.Param("MovingAverageShift", 1)
self._session_open_hour = self.Param("SessionOpenHour", 0)
self._session_close_hour = self.Param("SessionCloseHour", 23)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._fast_buffer = None
self._slow_buffer = None
self._previous_candle_close = None
self._previous_candle_open = None
self._buffer_size = 0
@property
def FastPeriod(self):
return self._fast_period.Value
@FastPeriod.setter
def FastPeriod(self, value):
self._fast_period.Value = value
@property
def SlowPeriod(self):
return self._slow_period.Value
@SlowPeriod.setter
def SlowPeriod(self, value):
self._slow_period.Value = value
@property
def MovingAverageShift(self):
return self._ma_shift.Value
@MovingAverageShift.setter
def MovingAverageShift(self, value):
self._ma_shift.Value = value
@property
def SessionOpenHour(self):
return self._session_open_hour.Value
@SessionOpenHour.setter
def SessionOpenHour(self, value):
self._session_open_hour.Value = value
@property
def SessionCloseHour(self):
return self._session_close_hour.Value
@SessionCloseHour.setter
def SessionCloseHour(self, value):
self._session_close_hour.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(mare51_strategy, self).OnStarted2(time)
self._buffer_size = int(self.MovingAverageShift) + 6
self._fast_buffer = [None] * self._buffer_size
self._slow_buffer = [None] * self._buffer_size
self._previous_candle_close = None
self._previous_candle_open = None
fast_sma = SimpleMovingAverage()
fast_sma.Length = self.FastPeriod
slow_sma = SimpleMovingAverage()
slow_sma.Length = self.SlowPeriod
self._fast_sma = fast_sma
self._slow_sma = slow_sma
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_sma, slow_sma, self.ProcessCandle).Start()
self.Volume = self._trade_volume.Value
pip_size = self._calculate_pip_size()
tp = float(self._take_profit_pips.Value)
sl = float(self._stop_loss_pips.Value)
take_unit = Unit(tp * pip_size, UnitTypes.Absolute) if tp > 0 else Unit(0)
stop_unit = Unit(sl * pip_size, UnitTypes.Absolute) if sl > 0 else Unit(0)
self.StartProtection(
stopLoss=stop_unit,
takeProfit=take_unit)
def ProcessCandle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
for i in range(self._buffer_size - 1, 0, -1):
self._fast_buffer[i] = self._fast_buffer[i - 1]
self._slow_buffer[i] = self._slow_buffer[i - 1]
self._fast_buffer[0] = fast_val
self._slow_buffer[0] = slow_val
prev_close = self._previous_candle_close
prev_open = self._previous_candle_open
self._previous_candle_close = float(candle.ClosePrice)
self._previous_candle_open = float(candle.OpenPrice)
if prev_close is None or prev_open is None:
return
if not self._fast_sma.IsFormed or not self._slow_sma.IsFormed:
return
shift = int(self.MovingAverageShift)
f0 = self._get_shifted(self._fast_buffer, 0, shift)
f2 = self._get_shifted(self._fast_buffer, 2, shift)
f5 = self._get_shifted(self._fast_buffer, 5, shift)
s0 = self._get_shifted(self._slow_buffer, 0, shift)
s2 = self._get_shifted(self._slow_buffer, 2, shift)
s5 = self._get_shifted(self._slow_buffer, 5, shift)
if f0 is None or f2 is None or f5 is None or s0 is None or s2 is None or s5 is None:
return
hour = candle.OpenTime.Hour
if hour < int(self.SessionOpenHour) or hour > int(self.SessionCloseHour):
return
bearish_prev = prev_close < prev_open
bullish_prev = prev_close > prev_open
sell_signal = f5 >= s5 and f2 < s2 and f0 < s0 and bearish_prev
buy_signal = f5 <= s5 and f2 > s2 and f0 > s0 and bullish_prev
if self.Position != 0:
return
if sell_signal:
self.SellMarket()
elif buy_signal:
self.BuyMarket()
def _get_shifted(self, buffer, index, shift):
target = index + shift
if target < 0 or target >= self._buffer_size:
return None
return buffer[target]
def _calculate_pip_size(self):
sec = self.Security
if sec is None:
return 1.0
ps = sec.PriceStep
if ps is None:
return 1.0
step = float(ps)
if step <= 0:
return 1.0
# count decimal places
import System
bits = System.Decimal.GetBits(ps)
scale = (bits[3] >> 16) & 0xFF
if scale == 3 or scale == 5:
return step * 10.0
return step
def OnReseted(self):
super(mare51_strategy, self).OnReseted()
self._fast_buffer = None
self._slow_buffer = None
self._previous_candle_close = None
self._previous_candle_open = None
def CreateClone(self):
return mare51_strategy()