EMA 6/12 均线交叉策略
该策略复刻了 MetaTrader 平台的 EMA_6_12 专家顾问:在默认的一小时 K 线中计算 EMA(6) 与 EMA(12),并在蜡烛收盘后确认均线交叉,再决定开仓或平仓。
交易逻辑
- 进场:
- 当 EMA(6) 上穿 EMA(12) 时触发做多信号,若当前没有持仓,则开多单。
- 当 EMA(6) 下穿 EMA(12) 时触发做空信号,若当前没有持仓,则开空单。
- 离场:
UseCloseSignals(默认启用)为真时,一旦出现相反方向的均线交叉,策略先行平掉当前持仓,并等待下一次交叉后再入场,保持与原始 EA 的节奏一致。- 可选的止盈与跟踪止损通过 StockSharp 自带的
StartProtection方法托管。
- 仓位管理:
- 下单数量由
OrderVolume参数控制,默认 1 手,在发单前会根据标的交易规则自动对齐。
- 下单数量由
风险控制
- 跟踪止损:将原 EA 的“点”参数转换为价格最小变动单位,只要参数大于零便会在持仓获利时自动跟随。
- 止盈:同样以价格步长表示,设置为 0 即可关闭。
- 策略不会加仓或网格交易,每个品种同时只持有一个方向的仓位。
参数说明
| 参数 | 说明 |
|---|---|
CandleType |
构建 K 线与均线所用的时间框架,默认 1 小时。 |
OrderVolume |
下单手数。 |
ShortEmaLength |
快速 EMA 的周期(默认 6)。 |
LongEmaLength |
慢速 EMA 的周期(默认 12)。 |
UseCloseSignals |
是否在出现反向交叉时平仓(默认开启)。 |
TrailingStopSteps |
以价格步长表示的跟踪止损距离,0 表示关闭。 |
TakeProfitSteps |
以价格步长表示的止盈距离,0 表示关闭。 |
其他说明
- 仅在蜡烛收盘后处理信号,避免盘中噪声。
- 平仓后保留上一根K线的 EMA 值,仅根据新的真实交叉交易;用于平仓的交叉不会在同一根K线上反向开仓。
- 代码注释为英文,并且缩进使用制表符,符合仓库要求。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// EMA(6)/EMA(12) crossover: enter while flat and close (never reverse on the
/// same crossover) when the close-signals switch is enabled.
/// </summary>
public class Ema612Strategy : Strategy
{
private readonly StrategyParam<int> _shortEmaLength;
private readonly StrategyParam<int> _longEmaLength;
private readonly StrategyParam<bool> _useCloseSignals;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _trailingStopSteps;
private readonly StrategyParam<decimal> _takeProfitSteps;
private readonly StrategyParam<DataType> _candleType;
private decimal? _previousFast;
private decimal? _previousSlow;
private Order _pendingOrder;
public int ShortEmaLength { get => _shortEmaLength.Value; set => _shortEmaLength.Value = value; }
public int LongEmaLength { get => _longEmaLength.Value; set => _longEmaLength.Value = value; }
public bool UseCloseSignals { get => _useCloseSignals.Value; set => _useCloseSignals.Value = value; }
public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
public decimal TrailingStopSteps { get => _trailingStopSteps.Value; set => _trailingStopSteps.Value = value; }
public decimal TakeProfitSteps { get => _takeProfitSteps.Value; set => _takeProfitSteps.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public Ema612Strategy()
{
_shortEmaLength = Param(nameof(ShortEmaLength), 6).SetGreaterThanZero()
.SetDisplay("Short EMA", "Period of the fast EMA", "Indicators");
_longEmaLength = Param(nameof(LongEmaLength), 12).SetGreaterThanZero()
.SetDisplay("Long EMA", "Period of the slow EMA", "Indicators");
_useCloseSignals = Param(nameof(UseCloseSignals), true)
.SetDisplay("Close Signals", "Close on an opposite cross instead of reversing", "Trading");
_orderVolume = Param(nameof(OrderVolume), 1m).SetGreaterThanZero()
.SetDisplay("Order Volume", "Entry size in lots", "Trading");
_trailingStopSteps = Param(nameof(TrailingStopSteps), 0m).SetNotNegative()
.SetDisplay("Trailing Stop Steps", "Zero disables trailing", "Protection");
_takeProfitSteps = Param(nameof(TakeProfitSteps), 0m).SetNotNegative()
.SetDisplay("Take Profit Steps", "Zero disables take profit", "Protection");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Input candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_previousFast = null;
_previousSlow = null;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_previousFast = null;
_previousSlow = null;
_pendingOrder = null;
Volume = NormalizeVolume(OrderVolume);
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
throw new InvalidOperationException("EMA6/12 requires a valid positive PriceStep.");
if (TakeProfitSteps > 0m || TrailingStopSteps > 0m)
StartProtection(
takeProfit: TakeProfitSteps > 0m ? new Unit(TakeProfitSteps * step, UnitTypes.Absolute) : new Unit(),
stopLoss: TrailingStopSteps > 0m ? new Unit(TrailingStopSteps * step, UnitTypes.Absolute) : new Unit(),
isStopTrailing: TrailingStopSteps > 0m,
useMarketOrders: true, isLocalStop: true);
var fast = new ExponentialMovingAverage { Length = ShortEmaLength };
var slow = new ExponentialMovingAverage { Length = LongEmaLength };
SubscribeCandles(CandleType).Bind(fast, slow, ProcessCandle).Start();
}
private decimal NormalizeVolume(decimal requested)
{
if (Security?.VolumeStep is decimal step && step > 0m)
requested = decimal.Floor(requested / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
requested = Math.Max(requested, min);
if (Security?.MaxVolume is decimal max && max > 0m)
requested = Math.Min(requested, max);
return requested;
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
var previousFast = _previousFast;
var previousSlow = _previousSlow;
_previousFast = fast;
_previousSlow = slow;
if (previousFast is null || previousSlow is null)
return;
var up = previousFast.Value <= previousSlow.Value && fast > slow;
var down = previousFast.Value >= previousSlow.Value && fast < slow;
if (!up && !down || _pendingOrder is not null &&
_pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
var side = up ? Sides.Buy : Sides.Sell;
if (Position == 0m)
{
var volume = NormalizeVolume(OrderVolume);
if (volume > 0m)
Submit(side, volume);
}
else if (UseCloseSignals && (side == Sides.Buy && Position < 0m ||
side == Sides.Sell && Position > 0m))
{
Submit(side, Math.Abs(Position));
}
}
private void Submit(Sides side, decimal volume)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
};
RegisterOrder(_pendingOrder);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import Decimal, TimeSpan
from StockSharp.Messages import DataType, CandleStates, OrderTypes, OrderStates, Sides, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Order
class ema612_strategy(Strategy):
"""EMA(6)/EMA(12) cross entries and opposite-cross close-only exits."""
def __init__(self):
super(ema612_strategy, self).__init__()
self._short_ema_length = self.Param("ShortEmaLength", 6).SetGreaterThanZero().SetDisplay(
"Short EMA", "Period of the fast EMA", "Indicators")
self._long_ema_length = self.Param("LongEmaLength", 12).SetGreaterThanZero().SetDisplay(
"Long EMA", "Period of the slow EMA", "Indicators")
self._use_close_signals = self.Param("UseCloseSignals", True).SetDisplay(
"Close Signals", "Close on an opposite cross instead of reversing", "Trading")
self._order_volume = self.Param("OrderVolume", 1.0).SetGreaterThanZero().SetDisplay(
"Order Volume", "Entry size in lots", "Trading")
self._trailing_stop_steps = self.Param("TrailingStopSteps", 0.0).SetNotNegative().SetDisplay(
"Trailing Stop Steps", "Zero disables trailing", "Protection")
self._take_profit_steps = self.Param("TakeProfitSteps", 0.0).SetNotNegative().SetDisplay(
"Take Profit Steps", "Zero disables take profit", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay(
"Candle Type", "Input candle timeframe", "General")
self._clear_state()
@property
def short_ema_length(self):
return self._short_ema_length.Value
@property
def long_ema_length(self):
return self._long_ema_length.Value
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def _clear_state(self):
self._previous_fast = None
self._previous_slow = None
self._pending_order = None
def OnReseted(self):
super(ema612_strategy, self).OnReseted()
self._clear_state()
def _normalized_volume(self, volume):
size = float(volume)
step = float(self.Security.VolumeStep or 0)
if step > 0:
size = math.floor(size / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
size = max(size, minimum)
if maximum > 0:
size = min(size, maximum)
return size
def OnStarted2(self, time):
super(ema612_strategy, self).OnStarted2(time)
self._clear_state()
self.Volume = Decimal(self._normalized_volume(self._order_volume.Value))
step = float(self.Security.PriceStep or 0)
if step <= 0:
raise ValueError("EMA6/12 requires a valid positive PriceStep.")
take = float(self._take_profit_steps.Value)
trail = float(self._trailing_stop_steps.Value)
if take > 0 or trail > 0:
self.StartProtection(
Unit(Decimal(take * step), UnitTypes.Absolute) if take > 0 else Unit(),
Unit(Decimal(trail * step), UnitTypes.Absolute) if trail > 0 else Unit(),
isStopTrailing=trail > 0,
useMarketOrders=True, isLocalStop=True)
fast = ExponentialMovingAverage()
fast.Length = self.short_ema_length
slow = ExponentialMovingAverage()
slow.Length = self.long_ema_length
self.SubscribeCandles(self.candle_type).Bind(fast, slow, self._process_candle).Start()
def _process_candle(self, candle, fast, slow):
if candle.State != CandleStates.Finished:
return
f, s = float(fast), float(slow)
pf, ps = self._previous_fast, self._previous_slow
self._previous_fast, self._previous_slow = f, s
if pf is None or ps is None:
return
up = pf <= ps and f > s
down = pf >= ps and f < s
if not (up or down):
return
if (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
side = Sides.Buy if up else Sides.Sell
position = float(self.Position)
if position == 0:
volume = self._normalized_volume(self._order_volume.Value)
if volume > 0:
self._submit(side, volume)
elif self._use_close_signals.Value and (
side == Sides.Buy and position < 0 or side == Sides.Sell and position > 0):
self._submit(side, abs(position))
def _submit(self, side, volume):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
self._pending_order = order
self.RegisterOrder(order)
def CreateClone(self):
return ema612_strategy()