平滑均线突破策略
概述
平滑均线突破策略再现了原始 MQL5 专家顾问 Smoothing Average (barabashkakvn's edition) 的逻辑。策略将可配置的移动平均线与按点数衡量的距离过滤器结合使用。当价格偏离均线达到设定的点数时,系统会顺势开仓(若启用反向模式,则反向开仓)。当价格穿越扩大后的均线通道时,仓位被平仓。
交易逻辑
标准模式(ReverseSignals = false)
- 做多开仓: 收盘价高于
MA - Entry Delta (pips)。 - 做空开仓: 收盘价低于
MA + Entry Delta (pips)。 - 做空平仓: 收盘价高于
MA + Entry Delta (pips) × Close Delta Coefficient。 - 做多平仓: 收盘价低于
MA - Entry Delta (pips) × Close Delta Coefficient。
反向模式(ReverseSignals = true)
- 做多开仓: 收盘价低于
MA + Entry Delta (pips)。 - 做空开仓: 收盘价高于
MA - Entry Delta (pips)。 - 做多平仓: 收盘价低于
MA - Entry Delta (pips) × Close Delta Coefficient。 - 做空平仓: 收盘价高于
MA + Entry Delta (pips) × Close Delta Coefficient。
移动平均线可以向前平移若干根 K 线。策略通过保存最近的指标值并取 MaShift 根之前的数值来模拟这一效果,与 MetaTrader 中指标绘制的平移线一致。
参数
Candle Type– 参与计算的 K 线类型。MA Length– 平滑均线的周期长度。MA Shift– 均线向前平移的 K 线数量。MA Type– 均线类型(简单、指数、平滑、线性加权)。Price Source– 输入到均线中的价格(默认使用典型价)。Entry Delta (pips)– 触发开仓所需的点数距离,按合约的最小变动价位转换为价格。Close Delta Coefficient– 计算平仓通道时对入场点数的倍数。Reverse Signals– 是否反转多空条件。Trade Volume– 每次下单的固定手数。
风险管理
- 所有订单均采用
Trade Volume指定的固定手数,不在持仓期间加仓或减仓。 - 平仓完全依赖规则,不会主动提交止损或止盈,但会调用
StartProtection()以启用平台的安全保护。 - 反向模式允许在不修改其他参数的情况下进行逆势交易。
实现细节
- 点值来自
Security.PriceStep。对于三位或五位报价的外汇品种,点值会按 MQL5 版本的方式乘以 10。 - 均线使用
Price Source参数,可匹配原始 EA 中对不同价格的选择。 - 条件判断使用 K 线收盘价,作为原始程序中 bid/ask 检查的稳定替代。
- C# 源码中的注释全部采用英文,以符合转换指引的要求。
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>
/// Smoothing Average strategy converted from MQL5.
/// Opens trades when price moves away from the moving average by a configurable delta.
/// Supports reversing the signals and shifting the moving average output.
/// </summary>
public class SmoothingAverageCrossoverStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maLength;
private readonly StrategyParam<int> _maShift;
private readonly StrategyParam<MovingAverageKinds> _maType;
private readonly StrategyParam<CandlePrices> _priceSource;
private readonly StrategyParam<decimal> _entryDeltaPips;
private readonly StrategyParam<decimal> _closeDeltaCoefficient;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly Queue<decimal> _maShiftBuffer = new();
private decimal _entryDelta;
private decimal _closeDelta;
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public SmoothingAverageCrossoverStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
_maLength = Param(nameof(MaLength), 60)
.SetGreaterThanZero()
.SetDisplay("MA Length", "Period of the smoothing average", "Moving Average");
_maShift = Param(nameof(MaShift), 3)
.SetNotNegative()
.SetDisplay("MA Shift", "Horizontal shift applied to the average", "Moving Average");
_maType = Param(nameof(MaType), MovingAverageKinds.Simple)
.SetDisplay("MA Type", "Type of smoothing applied", "Moving Average");
_priceSource = Param(nameof(PriceSource), CandlePrices.Typical)
.SetDisplay("Price Source", "Price used for the moving average", "Moving Average");
_entryDeltaPips = Param(nameof(EntryDeltaPips), 60m)
.SetNotNegative()
.SetDisplay("Entry Delta (pips)", "Distance from MA to trigger entries", "Trading Rules");
_closeDeltaCoefficient = Param(nameof(CloseDeltaCoefficient), 1.0m)
.SetGreaterThanZero()
.SetDisplay("Close Delta Coefficient", "Multiplier applied to entry delta for exits", "Trading Rules");
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Invert long and short logic", "Trading Rules");
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order volume for each entry", "Risk");
}
/// <summary>
/// Primary candle series used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Moving average period.
/// </summary>
public int MaLength
{
get => _maLength.Value;
set => _maLength.Value = value;
}
/// <summary>
/// Number of candles used to shift the moving average output.
/// </summary>
public int MaShift
{
get => _maShift.Value;
set => _maShift.Value = value;
}
/// <summary>
/// Moving average type.
/// </summary>
public MovingAverageKinds MaType
{
get => _maType.Value;
set => _maType.Value = value;
}
/// <summary>
/// Candle price source for the moving average.
/// </summary>
public CandlePrices PriceSource
{
get => _priceSource.Value;
set => _priceSource.Value = value;
}
/// <summary>
/// Delta in pip units used to open new positions.
/// </summary>
public decimal EntryDeltaPips
{
get => _entryDeltaPips.Value;
set => _entryDeltaPips.Value = value;
}
/// <summary>
/// Multiplier applied to the entry delta when evaluating exits.
/// </summary>
public decimal CloseDeltaCoefficient
{
get => _closeDeltaCoefficient.Value;
set => _closeDeltaCoefficient.Value = value;
}
/// <summary>
/// If true, swaps long and short signals.
/// </summary>
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
/// <summary>
/// Volume sent with market orders.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_maShiftBuffer.Clear();
_entryDelta = 0m;
_closeDelta = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Sync the base strategy volume with the parameter value.
Volume = TradeVolume;
// Calculate pip-based offsets once at the start to avoid repeated computations.
_entryDelta = CalculateEntryDelta();
_closeDelta = _entryDelta * CloseDeltaCoefficient;
var movingAverage = CreateMovingAverage(MaType, MaLength);
//movingAverage.CandlePrice = PriceSource;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(movingAverage, ProcessCandle)
.Start();
// Enable built-in protection helpers (no additional parameters required).
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, decimal maValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var shiftedMa = ApplyShift(maValue);
// Use candle close as a proxy for bid/ask checks from the original Expert Advisor.
var askPrice = candle.ClosePrice;
var bidPrice = candle.ClosePrice;
var entryUpper = shiftedMa + _entryDelta;
var entryLower = shiftedMa - _entryDelta;
var closeUpper = shiftedMa + _closeDelta;
var closeLower = shiftedMa - _closeDelta;
if (Position == 0m)
{
if (!ReverseSignals)
{
if (askPrice > entryUpper)
{
OpenLong();
return;
}
if (bidPrice < entryLower)
{
OpenShort();
return;
}
}
else
{
if (askPrice > entryUpper)
{
OpenShort();
return;
}
if (bidPrice < entryLower)
{
OpenLong();
return;
}
}
}
else
{
if (!ReverseSignals)
{
if (Position < 0m && bidPrice > closeUpper)
CloseShort();
if (Position > 0m && askPrice < closeLower)
CloseLong();
}
else
{
if (Position > 0m && askPrice < closeLower)
CloseLong();
if (Position < 0m && bidPrice > closeUpper)
CloseShort();
}
}
}
private decimal ApplyShift(decimal currentValue)
{
if (MaShift <= 0)
return currentValue;
var shifted = _maShiftBuffer.Count < MaShift ? currentValue : _maShiftBuffer.Peek();
_maShiftBuffer.Enqueue(currentValue);
if (_maShiftBuffer.Count > MaShift)
_maShiftBuffer.Dequeue();
return shifted;
}
private decimal CalculateEntryDelta()
{
var pip = CalculatePipSize();
return pip * EntryDeltaPips;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 0.0001m;
var digits = (int)Math.Round(Math.Log10((double)(1m / step)));
return digits == 3 || digits == 5 ? step * 10m : step;
}
private static DecimalLengthIndicator CreateMovingAverage(MovingAverageKinds type, int length)
{
return type switch
{
MovingAverageKinds.Simple => new SMA { Length = length },
MovingAverageKinds.Exponential => new EMA { Length = length },
MovingAverageKinds.Smoothed => new SmoothedMovingAverage { Length = length },
MovingAverageKinds.LinearWeighted => new WeightedMovingAverage { Length = length },
_ => new SMA { Length = length }
};
}
private void OpenLong()
{
var volume = TradeVolume + Math.Max(0m, -Position);
if (volume <= 0m)
return;
BuyMarket(volume);
}
private void OpenShort()
{
var volume = TradeVolume + Math.Max(0m, Position);
if (volume <= 0m)
return;
SellMarket(volume);
}
private void CloseLong()
{
if (Position <= 0m)
return;
SellMarket(Position);
}
private void CloseShort()
{
if (Position >= 0m)
return;
BuyMarket(Math.Abs(Position));
}
/// <summary>
/// Supported moving average types replicating the MQL5 enumeration.
/// </summary>
public enum MovingAverageKinds
{
Simple,
Exponential,
Smoothed,
LinearWeighted
}
public enum CandlePrices
{
Open,
Close,
High,
Low,
Median,
Typical,
Weighted
}
}
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
from StockSharp.Algo.Indicators import (
SimpleMovingAverage,
ExponentialMovingAverage,
SmoothedMovingAverage,
WeightedMovingAverage,
)
from StockSharp.Algo.Strategies import Strategy
from collections import deque
class smoothing_average_crossover_strategy(Strategy):
def __init__(self):
super(smoothing_average_crossover_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Timeframe used for calculations", "General")
self._ma_length = self.Param("MaLength", 60) \
.SetDisplay("MA Length", "Period of the smoothing average", "Moving Average")
self._ma_shift = self.Param("MaShift", 3) \
.SetDisplay("MA Shift", "Horizontal shift applied to the average", "Moving Average")
self._entry_delta_pips = self.Param("EntryDeltaPips", 60.0) \
.SetDisplay("Entry Delta pips", "Distance from MA to trigger entries", "Trading Rules")
self._close_delta_coefficient = self.Param("CloseDeltaCoefficient", 1.0) \
.SetDisplay("Close Delta Coefficient", "Multiplier applied to entry delta for exits", "Trading Rules")
self._reverse_signals = self.Param("ReverseSignals", False) \
.SetDisplay("Reverse Signals", "Invert long and short logic", "Trading Rules")
self._trade_volume = self.Param("TradeVolume", 1.0) \
.SetDisplay("Trade Volume", "Order volume for each entry", "Risk")
self._ma_shift_buffer = deque()
self._entry_delta = 0.0
self._close_delta = 0.0
@property
def candle_type(self):
return self._candle_type.Value
@property
def ma_length(self):
return self._ma_length.Value
@property
def ma_shift(self):
return self._ma_shift.Value
@property
def entry_delta_pips(self):
return self._entry_delta_pips.Value
@property
def close_delta_coefficient(self):
return self._close_delta_coefficient.Value
@property
def reverse_signals(self):
return self._reverse_signals.Value
@property
def trade_volume(self):
return self._trade_volume.Value
def OnReseted(self):
super(smoothing_average_crossover_strategy, self).OnReseted()
self._ma_shift_buffer = deque()
self._entry_delta = 0.0
self._close_delta = 0.0
def OnStarted2(self, time):
super(smoothing_average_crossover_strategy, self).OnStarted2(time)
self.Volume = self.trade_volume
pip = self._calculate_pip_size()
self._entry_delta = pip * float(self.entry_delta_pips)
self._close_delta = self._entry_delta * float(self.close_delta_coefficient)
ma = SimpleMovingAverage()
ma.Length = self.ma_length
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ma, self._process_candle).Start()
self.StartProtection(None, None)
def _process_candle(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
shifted_ma = self._apply_shift(float(ma_value))
close = float(candle.ClosePrice)
entry_upper = shifted_ma + self._entry_delta
entry_lower = shifted_ma - self._entry_delta
close_upper = shifted_ma + self._close_delta
close_lower = shifted_ma - self._close_delta
tv = float(self.trade_volume)
if self.Position == 0:
if not self.reverse_signals:
if close > entry_upper:
vol = tv + max(0.0, -self.Position)
if vol > 0:
self.BuyMarket(vol)
return
if close < entry_lower:
vol = tv + max(0.0, self.Position)
if vol > 0:
self.SellMarket(vol)
return
else:
if close > entry_upper:
vol = tv + max(0.0, self.Position)
if vol > 0:
self.SellMarket(vol)
return
if close < entry_lower:
vol = tv + max(0.0, -self.Position)
if vol > 0:
self.BuyMarket(vol)
return
else:
if not self.reverse_signals:
if self.Position < 0 and close > close_upper:
self.BuyMarket(abs(self.Position))
if self.Position > 0 and close < close_lower:
self.SellMarket(self.Position)
else:
if self.Position > 0 and close < close_lower:
self.SellMarket(self.Position)
if self.Position < 0 and close > close_upper:
self.BuyMarket(abs(self.Position))
def _apply_shift(self, current_value):
shift = self.ma_shift
if shift <= 0:
return current_value
if len(self._ma_shift_buffer) < shift:
shifted = current_value
else:
shifted = self._ma_shift_buffer[0]
self._ma_shift_buffer.append(current_value)
if len(self._ma_shift_buffer) > shift:
self._ma_shift_buffer.popleft()
return shifted
def _calculate_pip_size(self):
sec = self.Security
if sec is None:
return 0.0001
step = sec.PriceStep
if step is None or float(step) <= 0:
return 0.0001
step_val = float(step)
digits = int(round(Math.Log10(1.0 / step_val)))
if digits == 3 or digits == 5:
return step_val * 10.0
return step_val
def CreateClone(self):
return smoothing_average_crossover_strategy()