5/8 均线交叉策略
概述
5/8 均线交叉策略是 MetaTrader 专家顾问「5_8 MACross」在 StockSharp 平台上的移植版本。策略比较基于收盘价的快速指数移动平均线(EMA)与基于开盘价的慢速 EMA,当两条均线发生交叉时采取操作,适用于所有提供标准时间周期 K 线的数据。
指标
- 快速 EMA —— 可配置周期(默认 5),使用 K 线收盘价计算。
- 慢速 EMA —— 可配置周期(默认 8),使用 K 线开盘价计算。
交易逻辑
- 策略仅处理已经收盘的 K 线,以避免未完成数据导致的重绘。
- 当上一根 K 线中快速 EMA 位于慢速 EMA 之下或相等,而当前 K 线快速 EMA 向上穿越慢速 EMA 时,生成做多信号。
- 当上一根 K 线中快速 EMA 位于慢速 EMA 之上或相等,而当前 K 线快速 EMA 向下跌破慢速 EMA 时,生成做空信号。
- 出现信号时策略会反向持仓:先平掉当前仓位,再以设定的
Volume数量在新方向上发送市价单。
风险管理
- 止盈 —— 以价格点数为单位的可选目标。点值根据标的的最小价格变动自动计算,对于三位和五位报价会额外乘以 10,以模拟 MetaTrader 对“点/点差”的处理方式。
- 止损 —— 可选的保护性止损,同样以距离开仓价的点数表示,0 表示不启用。
- 移动止损 —— 以点数表示的可选距离。开仓后策略会跟踪多头情况下的最高价或空头情况下的最低价,只在有利方向移动止损。如果未设置初始止损,移动止损也会在建仓后立即提供保护。
- 当收盘价触及止盈或(移动)止损水平时,策略以市价立即平仓。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
FastLength |
快速 EMA(收盘价)的周期。 | 5 |
SlowLength |
慢速 EMA(开盘价)的周期。 | 8 |
TakeProfitPoints |
止盈距离(点数)。 | 40 |
StopLossPoints |
止损距离(点数,0 表示关闭)。 | 0 |
TrailingStopPoints |
移动止损距离(点数,0 表示关闭)。 | 0 |
CandleType |
用于计算的 K 线类型/周期。 | 1 分钟 K 线 |
Volume |
来自基础 Strategy 类的下单手数。 |
0.1 |
与 MQL 版本的差异
- 省略了 MetaTrader 特有的对冲账户检查和账户信息调用,StockSharp 在持仓处理上采用不同机制。
- 在本移植中,信号基于完全收盘的 K 线计算,而原始脚本在新 K 线的首个报价上触发,这样做能提升事件驱动环境下的稳定性。
- 移动止损使用 K 线的最高价/最低价推进,而不是即时买卖价,从而在历史回测中保持确定性。
使用说明
- 在策略属性中设置
Volume,以匹配所需的下单手数。 - 如需账户层面的风险控制,可结合 StockSharp 的保护模块或额外的过滤策略。
- 策略仅使用市价单,不挂出任何挂单;所有进出场都由均线交叉和风控逻辑驱动。
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>
/// 5/8 exponential moving average crossover strategy converted from MetaTrader.
/// Uses a fast EMA on close prices and a slower EMA on open prices with manual stop, take profit, and trailing logic.
/// </summary>
public class FiveEightMaCrossStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _fastMa = null!;
private ExponentialMovingAverage _slowMa = null!;
private decimal _prevFast;
private decimal _prevSlow;
private bool _isInitialized;
private decimal _pointValue;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _trailDistance;
private decimal _maxPrice;
private decimal _minPrice;
/// <summary>
/// Fast EMA length.
/// </summary>
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
/// <summary>
/// Slow EMA length.
/// </summary>
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
/// <summary>
/// Take profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in price points.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="FiveEightMaCrossStrategy"/>.
/// </summary>
public FiveEightMaCrossStrategy()
{
_fastLength = Param(nameof(FastLength), 8)
.SetGreaterThanZero()
.SetDisplay("Fast EMA Length", "Length of the EMA calculated on closing prices", "Indicators")
.SetOptimize(3, 20, 1);
_slowLength = Param(nameof(SlowLength), 21)
.SetGreaterThanZero()
.SetDisplay("Slow EMA Length", "Length of the EMA calculated on opening prices", "Indicators")
.SetOptimize(5, 30, 1);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 40m)
.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
.SetNotNegative()
.SetOptimize(10m, 100m, 10m);
_stopLossPoints = Param(nameof(StopLossPoints), 0m)
.SetDisplay("Stop Loss (points)", "Stop loss distance expressed in price points", "Risk Management")
.SetNotNegative()
.SetOptimize(0m, 100m, 10m);
_trailingStopPoints = Param(nameof(TrailingStopPoints), 0m)
.SetDisplay("Trailing Stop (points)", "Trailing stop distance expressed in price points", "Risk Management")
.SetNotNegative()
.SetOptimize(0m, 100m, 10m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for calculations", "General");
Volume = 0.1m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMa = null!;
_slowMa = null!;
_prevFast = 0m;
_prevSlow = 0m;
_isInitialized = false;
_pointValue = 0m;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastMa = new EMA { Length = FastLength };
_slowMa = new EMA { Length = SlowLength };
_pointValue = CalculatePointValue();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private decimal CalculatePointValue()
{
var step = Security?.PriceStep;
if (step == null || step.Value <= 0m)
return 1m;
var stepValue = step.Value;
var stepDouble = (double)stepValue;
if (stepDouble <= 0d)
return stepValue;
var digitsDouble = Math.Log10(1d / stepDouble);
var digits = (int)Math.Round(digitsDouble, MidpointRounding.AwayFromZero);
var multiplier = (digits == 3 || digits == 5) ? 10m : 1m;
return stepValue * multiplier;
}
private void ProcessCandle(ICandleMessage candle)
{
// Process only finished candles to avoid repainting.
if (candle.State != CandleStates.Finished)
return;
// Feed indicators with the corresponding price source.
var fastValue = _fastMa.Process(new DecimalIndicatorValue(_fastMa, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
var slowValue = _slowMa.Process(new DecimalIndicatorValue(_slowMa, candle.OpenPrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
if (!_fastMa.IsFormed || !_slowMa.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
HandleRiskManagement(candle);
// indicators are processed manually
if (!_isInitialized)
{
_prevFast = fastValue;
_prevSlow = slowValue;
_isInitialized = true;
return;
}
var crossUp = _prevFast <= _prevSlow && fastValue > slowValue;
var crossDown = _prevFast >= _prevSlow && fastValue < slowValue;
if (crossUp && Position <= 0)
{
EnterLong(candle);
}
else if (crossDown && Position >= 0)
{
EnterShort(candle);
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
private void EnterLong(ICandleMessage candle)
{
var positionVolume = Position < 0 ? Math.Abs(Position) : 0m;
var volume = Volume + positionVolume;
if (volume <= 0m)
return;
ResetPositionState();
// Enter long position with volume including any short covering.
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_takePrice = TakeProfitPoints > 0m ? _entryPrice + TakeProfitPoints * _pointValue : null;
_stopPrice = StopLossPoints > 0m ? _entryPrice - StopLossPoints * _pointValue : null;
_trailDistance = TrailingStopPoints > 0m ? TrailingStopPoints * _pointValue : 0m;
_maxPrice = candle.HighPrice;
_minPrice = candle.LowPrice;
if (_trailDistance > 0m)
{
var trailStart = _entryPrice.Value - _trailDistance;
if (_stopPrice == null || trailStart > _stopPrice.Value)
_stopPrice = trailStart;
}
}
private void EnterShort(ICandleMessage candle)
{
var positionVolume = Position > 0 ? Position : 0m;
var volume = Volume + positionVolume;
if (volume <= 0m)
return;
ResetPositionState();
// Enter short position with volume including any long exit.
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_takePrice = TakeProfitPoints > 0m ? _entryPrice - TakeProfitPoints * _pointValue : null;
_stopPrice = StopLossPoints > 0m ? _entryPrice + StopLossPoints * _pointValue : null;
_trailDistance = TrailingStopPoints > 0m ? TrailingStopPoints * _pointValue : 0m;
_maxPrice = candle.HighPrice;
_minPrice = candle.LowPrice;
if (_trailDistance > 0m)
{
var trailStart = _entryPrice.Value + _trailDistance;
if (_stopPrice == null || trailStart < _stopPrice.Value)
_stopPrice = trailStart;
}
}
private void HandleRiskManagement(ICandleMessage candle)
{
if (Position > 0 && _entryPrice.HasValue)
{
// Update trailing stop using the highest price reached since entry.
_maxPrice = Math.Max(_maxPrice, candle.HighPrice);
if (_trailDistance > 0m)
{
var trailCandidate = _maxPrice - _trailDistance;
if (_stopPrice == null || trailCandidate > _stopPrice.Value)
_stopPrice = trailCandidate;
}
if (_takePrice.HasValue && candle.ClosePrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetPositionState();
return;
}
if (_stopPrice.HasValue && candle.ClosePrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetPositionState();
return;
}
}
else if (Position < 0 && _entryPrice.HasValue)
{
// Update trailing stop using the lowest price reached since entry.
_minPrice = Math.Min(_minPrice, candle.LowPrice);
if (_trailDistance > 0m)
{
var trailCandidate = _minPrice + _trailDistance;
if (_stopPrice == null || trailCandidate < _stopPrice.Value)
_stopPrice = trailCandidate;
}
if (_takePrice.HasValue && candle.ClosePrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
return;
}
if (_stopPrice.HasValue && candle.ClosePrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
}
}
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
_trailDistance = 0m;
_maxPrice = 0m;
_minPrice = 0m;
}
}
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 System import Decimal
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class five_eight_ma_cross_strategy(Strategy):
def __init__(self):
super(five_eight_ma_cross_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 8)
self._slow_length = self.Param("SlowLength", 21)
self._take_profit_points = self.Param("TakeProfitPoints", 40.0)
self._stop_loss_points = self.Param("StopLossPoints", 0.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 0.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._prev_fast = 0.0
self._prev_slow = 0.0
self._is_initialized = False
self._point_value = 1.0
self._entry_price = None
self._stop_price = None
self._take_price = None
self._trail_distance = 0.0
self._max_price = 0.0
self._min_price = 0.0
@property
def FastLength(self):
return self._fast_length.Value
@FastLength.setter
def FastLength(self, value):
self._fast_length.Value = value
@property
def SlowLength(self):
return self._slow_length.Value
@SlowLength.setter
def SlowLength(self, value):
self._slow_length.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 StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@TrailingStopPoints.setter
def TrailingStopPoints(self, value):
self._trailing_stop_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(five_eight_ma_cross_strategy, self).OnStarted2(time)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._is_initialized = False
self._point_value = self._calculate_point_value()
self._reset_position_state()
self._fast_ma = ExponentialMovingAverage()
self._fast_ma.Length = self.FastLength
self._slow_ma = ExponentialMovingAverage()
self._slow_ma.Length = self.SlowLength
self.Volume = 0.1
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def _calculate_point_value(self):
if self.Security is None or self.Security.PriceStep is None:
return 1.0
step = float(self.Security.PriceStep)
if step <= 0.0:
return 1.0
import math
digits = int(round(math.log10(1.0 / step)))
multiplier = 10.0 if (digits == 3 or digits == 5) else 1.0
return step * multiplier
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_price = float(candle.OpenPrice)
fast_result = process_float(self._fast_ma, candle.ClosePrice, candle.OpenTime, True)
slow_result = process_float(self._slow_ma, candle.OpenPrice, candle.OpenTime, True)
fast_val = fast_result.Value
slow_val = slow_result.Value
if not self._fast_ma.IsFormed or not self._slow_ma.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
self._handle_risk_management(candle)
if not self._is_initialized:
self._prev_fast = fast_val
self._prev_slow = slow_val
self._is_initialized = True
return
cross_up = self._prev_fast <= self._prev_slow and fast_val > slow_val
cross_down = self._prev_fast >= self._prev_slow and fast_val < slow_val
if cross_up and self.Position <= 0:
self._enter_long(candle)
elif cross_down and self.Position >= 0:
self._enter_short(candle)
self._prev_fast = fast_val
self._prev_slow = slow_val
def _enter_long(self, candle):
pos_vol = abs(float(self.Position)) if self.Position < 0 else 0.0
volume = float(self.Volume) + pos_vol
if volume <= 0:
return
self._reset_position_state()
self.BuyMarket(volume)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
tp_pts = float(self.TakeProfitPoints)
sl_pts = float(self.StopLossPoints)
trail_pts = float(self.TrailingStopPoints)
self._entry_price = close
self._take_price = close + tp_pts * self._point_value if tp_pts > 0.0 else None
self._stop_price = close - sl_pts * self._point_value if sl_pts > 0.0 else None
self._trail_distance = trail_pts * self._point_value if trail_pts > 0.0 else 0.0
self._max_price = high
self._min_price = low
if self._trail_distance > 0.0:
trail_start = close - self._trail_distance
if self._stop_price is None or trail_start > self._stop_price:
self._stop_price = trail_start
def _enter_short(self, candle):
pos_vol = float(self.Position) if self.Position > 0 else 0.0
volume = float(self.Volume) + pos_vol
if volume <= 0:
return
self._reset_position_state()
self.SellMarket(volume)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
tp_pts = float(self.TakeProfitPoints)
sl_pts = float(self.StopLossPoints)
trail_pts = float(self.TrailingStopPoints)
self._entry_price = close
self._take_price = close - tp_pts * self._point_value if tp_pts > 0.0 else None
self._stop_price = close + sl_pts * self._point_value if sl_pts > 0.0 else None
self._trail_distance = trail_pts * self._point_value if trail_pts > 0.0 else 0.0
self._max_price = high
self._min_price = low
if self._trail_distance > 0.0:
trail_start = close + self._trail_distance
if self._stop_price is None or trail_start < self._stop_price:
self._stop_price = trail_start
def _handle_risk_management(self, candle):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0 and self._entry_price is not None:
self._max_price = max(self._max_price, high)
if self._trail_distance > 0.0:
trail_candidate = self._max_price - self._trail_distance
if self._stop_price is None or trail_candidate > self._stop_price:
self._stop_price = trail_candidate
if self._take_price is not None and close >= self._take_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position_state()
return
if self._stop_price is not None and close <= self._stop_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position_state()
return
elif self.Position < 0 and self._entry_price is not None:
self._min_price = min(self._min_price, low)
if self._trail_distance > 0.0:
trail_candidate = self._min_price + self._trail_distance
if self._stop_price is None or trail_candidate < self._stop_price:
self._stop_price = trail_candidate
if self._take_price is not None and close <= self._take_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position_state()
return
if self._stop_price is not None and close >= self._stop_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position_state()
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
self._trail_distance = 0.0
self._max_price = 0.0
self._min_price = 0.0
def OnReseted(self):
super(five_eight_ma_cross_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._is_initialized = False
self._point_value = 1.0
self._reset_position_state()
def CreateClone(self):
return five_eight_ma_cross_strategy()