双均线趋势共振策略
概述
双均线趋势共振策略 复刻了原始的 MetaTrader 智能交易系统:通过一条慢速指数移动平均线(EMA)和一条快速线性加权移动平均线(LWMA)来确认趋势。策略会等待两条均线在同一方向上连续倾斜,并使用上一根 K 线的收盘价作为附加确认信号,只有在动量与趋势完全一致时才入场,旨在捕捉最有力量的趋势波段。
在 StockSharp 中的实现仅处理已完成的蜡烛线,保存最近三根 K 线的均线斜率,并通过 StartProtection 自动管理止损和止盈。策略与具体品种无关,只要证券提供蜡烛数据并定义了价格最小变动单位(point),就可以运行。
指标
- 慢速 EMA(默认 57):代表主趋势方向,要求连续两根 K 线的 EMA 值递增或递减。
- 快速 LWMA(默认 3):作为动量确认指标,只有当其斜率与慢速 EMA 一致时才允许入场。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
SlowMaLength |
57 | 慢速 EMA 趋势过滤器的周期。 |
FastMaLength |
3 | 快速 LWMA 动量过滤器的周期。 |
StopLossPoints |
100 | 止损距离,按品种的 point 表示,最终乘以 Security.PriceStep 转换为价格。 |
TakeProfitPoints |
100 | 止盈距离,按 point 表示,最终乘以 Security.PriceStep 转换为价格。 |
CandleType |
15 分钟 | 用于计算的蜡烛类型。 |
全部参数都暴露为 StrategyParam<T>,因此可以在运行时修改,也可以通过 StockSharp 的优化工具进行批量优化。
交易规则
做多条件
- 慢速 EMA 上升:当前值 > 前一根 > 前两根。
- 快速 LWMA 上升:当前值 > 前一根 > 前两根。
- 上一根 K 线的收盘价高于上一根慢速 EMA 值。
- 当前慢速 EMA 高于当前快速 LWMA。
- 当前仓位为空仓或净空。
- 满足全部条件时,发送市价买入订单,数量为
Volume + |Position|,以便在需要时翻多。
做空条件
- 慢速 EMA 下降:当前值 < 前一根 < 前两根。
- 快速 LWMA 下降:当前值 < 前一根 < 前两根。
- 上一根 K 线的收盘价低于上一根慢速 EMA 值。
- 当前慢速 EMA 低于当前快速 LWMA。
- 当前仓位为空仓或净多。
- 满足全部条件时,发送市价卖出订单,数量为
Volume + |Position|,实现快速翻空。
防护机制
StartProtection会将StopLossPoints与TakeProfitPoints乘以Security.PriceStep,从而得到绝对价格距离,然后以市价方式执行止损或止盈,确保在无挂单支持时也能退出。- 当出现反向信号时,策略会立即反向开仓,即使已有的止损/止盈单仍在等待。
实现细节
- 仅处理
CandleStates.Finished的蜡烛,等同于 MQL 版本中的“新柱”检测。 - 使用私有字段保存最近两根均线值和上一根收盘价,避免直接访问指标历史。
- 通过
IsFormedAndOnlineAndAllowTrading()确保数据完备且允许交易后才下单。 LogInfo输出多空入场的详细信息,便于调试与实时监控。- 如果图表可用,会同时绘制蜡烛与两条均线,方便视觉确认。
使用建议
- 根据标的物的合约规模设置
Volume。策略使用Volume + |Position|的市价单来完成反手。 - 如果证券未设置
PriceStep,代码会回退到 1。此时请根据真实最小跳动调整参数。 - 优化时可重点关注均线周期与止损/止盈距离,以适配不同市场的波动特征。
- 可以在此基础上增加波动率、交易时段等过滤器,代码结构已经为扩展做好准备。
建议的优化范围
SlowMaLength:20 – 120(步长 5–10)。FastMaLength:2 – 10(步长 1)。StopLossPoints/TakeProfitPoints:50 – 200(根据波动率调整)。
上述范围兼顾了原策略的默认设置,同时为其他品种提供足够的弹性。
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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Dual moving average trend confirmation strategy.
/// Uses a slow EMA and a fast LWMA to detect synchronized trends.
/// Enters long when both averages slope upward, price stays above the slow EMA, and the slow EMA is above the fast LWMA.
/// Enters short when both averages slope downward, price stays below the slow EMA, and the slow EMA is below the fast LWMA.
/// Built-in stop-loss and take-profit are defined in instrument points.
/// </summary>
public class DualMaTrendConfirmationStrategy : Strategy
{
private readonly StrategyParam<int> _slowMaLength;
private readonly StrategyParam<int> _fastMaLength;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal _previousClose;
private decimal _slowPrevious;
private decimal _slowPrevious2;
private decimal _fastPrevious;
private decimal _fastPrevious2;
private int _historyCount;
/// <summary>
/// Slow EMA period length.
/// </summary>
public int SlowMaLength
{
get => _slowMaLength.Value;
set => _slowMaLength.Value = value;
}
/// <summary>
/// Fast LWMA period length.
/// </summary>
public int FastMaLength
{
get => _fastMaLength.Value;
set => _fastMaLength.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in instrument points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in instrument points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="DualMaTrendConfirmationStrategy"/> class.
/// </summary>
public DualMaTrendConfirmationStrategy()
{
_slowMaLength = Param(nameof(SlowMaLength), 57)
.SetDisplay("Slow EMA Length", "Period for the slow EMA trend filter", "Moving Averages")
.SetRange(10, 200)
;
_fastMaLength = Param(nameof(FastMaLength), 3)
.SetDisplay("Fast LWMA Length", "Period for the fast LWMA confirmation filter", "Moving Averages")
.SetRange(1, 50)
;
_stopLossPoints = Param(nameof(StopLossPoints), 100m)
.SetDisplay("Stop Loss (points)", "Stop-loss distance measured in instrument points", "Risk Management")
.SetRange(10m, 500m)
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 100m)
.SetDisplay("Take Profit (points)", "Take-profit distance measured in instrument points", "Risk Management")
.SetRange(10m, 500m)
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for moving average calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Clear stored history so the next candle starts with a clean state.
_previousClose = 0m;
_slowPrevious = 0m;
_slowPrevious2 = 0m;
_fastPrevious = 0m;
_fastPrevious2 = 0m;
_historyCount = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var slowEma = new ExponentialMovingAverage
{
Length = SlowMaLength
};
var fastLwma = new WeightedMovingAverage
{
Length = FastMaLength
};
var subscription = SubscribeCandles(CandleType);
var step = Security.PriceStep ?? 1m;
// Enable automatic stop-loss and take-profit management based on point offsets.
StartProtection(
takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
stopLoss: new Unit(StopLossPoints * step, UnitTypes.Absolute),
useMarketOrders: true);
subscription
.Bind(slowEma, fastLwma, (candle, slowValue, fastValue) => ProcessCandle(candle, slowValue, fastValue, slowEma, fastLwma))
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, slowEma);
DrawIndicator(area, fastLwma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal slowValue, decimal fastValue, ExponentialMovingAverage slowEma, WeightedMovingAverage fastLwma)
{
// Work only with fully formed candles to avoid premature decisions.
if (candle.State != CandleStates.Finished)
return;
// Ensure both indicators produced reliable values before trading logic.
if (!slowEma.IsFormed || !fastLwma.IsFormed)
{
UpdateHistory(slowValue, fastValue, candle.ClosePrice);
return;
}
// Accumulate at least two previous candles for slope calculations.
if (_historyCount < 2)
{
UpdateHistory(slowValue, fastValue, candle.ClosePrice);
return;
}
var slowRising = slowValue > _slowPrevious && _slowPrevious > _slowPrevious2;
var fastRising = fastValue > _fastPrevious && _fastPrevious > _fastPrevious2;
var slowFalling = slowValue < _slowPrevious && _slowPrevious < _slowPrevious2;
var fastFalling = fastValue < _fastPrevious && _fastPrevious < _fastPrevious2;
var priceAboveSlow = _previousClose > _slowPrevious;
var priceBelowSlow = _previousClose < _slowPrevious;
var slowAboveFast = slowValue > fastValue;
var slowBelowFast = slowValue < fastValue;
if (slowRising && fastRising && priceAboveSlow && slowAboveFast && Position <= 0)
{
BuyMarket();
}
else if (slowFalling && fastFalling && priceBelowSlow && slowBelowFast && Position >= 0)
{
SellMarket();
}
UpdateHistory(slowValue, fastValue, candle.ClosePrice);
}
private void UpdateHistory(decimal slowValue, decimal fastValue, decimal closePrice)
{
// Shift previous values so the last two candles are always available.
_slowPrevious2 = _slowPrevious;
_slowPrevious = slowValue;
_fastPrevious2 = _fastPrevious;
_fastPrevious = fastValue;
_previousClose = closePrice;
if (_historyCount < 2)
_historyCount++;
}
}
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 ExponentialMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
class dual_ma_trend_confirmation_strategy(Strategy):
def __init__(self):
super(dual_ma_trend_confirmation_strategy, self).__init__()
self._slow_ma_length = self.Param("SlowMaLength", 57)
self._fast_ma_length = self.Param("FastMaLength", 3)
self._stop_loss_points = self.Param("StopLossPoints", 100.0)
self._take_profit_points = self.Param("TakeProfitPoints", 100.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
@property
def SlowMaLength(self):
return self._slow_ma_length.Value
@SlowMaLength.setter
def SlowMaLength(self, value):
self._slow_ma_length.Value = value
@property
def FastMaLength(self):
return self._fast_ma_length.Value
@FastMaLength.setter
def FastMaLength(self, value):
self._fast_ma_length.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.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(dual_ma_trend_confirmation_strategy, self).OnStarted2(time)
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowMaLength
self._fast_lwma = WeightedMovingAverage()
self._fast_lwma.Length = self.FastMaLength
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
self.StartProtection(
Unit(float(self.TakeProfitPoints) * step, UnitTypes.Absolute),
Unit(float(self.StopLossPoints) * step, UnitTypes.Absolute),
False, None, None, True)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._slow_ema, self._fast_lwma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, slow_value, fast_value):
if candle.State != CandleStates.Finished:
return
sv = float(slow_value)
fv = float(fast_value)
close = float(candle.ClosePrice)
if not self._slow_ema.IsFormed or not self._fast_lwma.IsFormed:
self._update_history(sv, fv, close)
return
if self._history_count < 2:
self._update_history(sv, fv, close)
return
slow_rising = sv > self._slow_previous and self._slow_previous > self._slow_previous2
fast_rising = fv > self._fast_previous and self._fast_previous > self._fast_previous2
slow_falling = sv < self._slow_previous and self._slow_previous < self._slow_previous2
fast_falling = fv < self._fast_previous and self._fast_previous < self._fast_previous2
price_above_slow = self._previous_close > self._slow_previous
price_below_slow = self._previous_close < self._slow_previous
slow_above_fast = sv > fv
slow_below_fast = sv < fv
if slow_rising and fast_rising and price_above_slow and slow_above_fast and self.Position <= 0:
self.BuyMarket()
elif slow_falling and fast_falling and price_below_slow and slow_below_fast and self.Position >= 0:
self.SellMarket()
self._update_history(sv, fv, close)
def _update_history(self, slow_value, fast_value, close):
self._slow_previous2 = self._slow_previous
self._slow_previous = slow_value
self._fast_previous2 = self._fast_previous
self._fast_previous = fast_value
self._previous_close = close
if self._history_count < 2:
self._history_count += 1
def OnReseted(self):
super(dual_ma_trend_confirmation_strategy, self).OnReseted()
self._previous_close = 0.0
self._slow_previous = 0.0
self._slow_previous2 = 0.0
self._fast_previous = 0.0
self._fast_previous2 = 0.0
self._history_count = 0
def CreateClone(self):
return dual_ma_trend_confirmation_strategy()