快慢均线交叉策略
概述
快慢均线交叉策略 复刻了 MetaTrader 4 专家顾问 _HPCS_FastSlowMACrosssover_MT4_EA_V01_WE 的核心思想。策略订阅所选的K线类型,并对两条指数移动平均线 (EMA) 进行比较:当较快均线在允许的交易时间窗口内上穿或下穿较慢均线时,策略开仓交易。止盈和止损距离以点值(pip)定义,从而与依赖报价小数位的原始 MQL 逻辑保持一致。
交易逻辑
- 订阅配置的K线类型(默认:1分钟K线)。
- 计算两条EMA:
- 快速EMA周期(默认 14)。
- 慢速EMA周期(默认 21)。
- 在每根已完成的K线上执行以下检查:
- 判断K线收盘时间是否位于允许的交易时间窗口内。
- 当快速EMA上穿慢速EMA时识别出 看涨交叉。
- 当快速EMA下穿慢速EMA时识别出 看跌交叉。
- 执行交易:
- 如果存在反向仓位,先行平仓。
- 按照 Trade Volume 参数规定的数量发送市价单。
- 使用K线收盘价作为入场价格,用于计算风险控制水平。
- 使用K线的最高价和最低价管理持仓:
- 多头仓位下穿入场价 Stop Loss (pips) 距离时立即平仓。
- 多头仓位上穿入场价 Take Profit (pips) 距离时立即平仓。
- 空头仓位按照对称逻辑处理(止损在入场价上方,止盈在入场价下方)。
参数
| 参数 | 说明 |
|---|---|
| Fast MA Period | 快速EMA的周期长度,用于判定交叉。 |
| Slow MA Period | 慢速EMA的周期长度。 |
| Take Profit (pips) | 以点值表示的止盈距离,用于计算多空目标价。 |
| Stop Loss (pips) | 以点值表示的止损距离。 |
| Start Time | 每日交易窗口的起始时间(包含)。 |
| Stop Time | 每日交易窗口的结束时间(包含)。 |
| Candle Type | 用于计算指标的K线类型。 |
| Trade Volume | 每次信号使用的下单数量。 |
说明
- 点值根据标的的最小报价步长和小数位数计算。如果品种带有5位或3位小数,策略会将步长乘以 10,以匹配MetaTrader中的点值定义。
- 时间过滤器支持跨越午夜的会话。当 Start Time 晚于 Stop Time 时,策略会从开始时间交易到午夜,并在午夜之后继续运行直到结束时间。
- 每根K线只允许触发一次信号,与原始EA在同一根K线上仅提交一次订单的保护机制保持一致。
- 止盈和止损由策略逻辑主动执行,而不是在交易所挂出等待成交的委托,这与原始EA在提交订单时直接设置止盈止损的方式相呼应。
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>
/// Fast and slow moving average crossover strategy with intraday time filter and pip-based risk controls.
/// </summary>
public class FastSlowMaCrossoverStrategy : Strategy
{
private readonly StrategyParam<int> _fastMaPeriod;
private readonly StrategyParam<int> _slowMaPeriod;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<TimeSpan> _startTime;
private readonly StrategyParam<TimeSpan> _stopTime;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _tradeVolume;
private decimal _pipSize;
private decimal? _previousFast;
private decimal? _previousSlow;
private DateTimeOffset? _lastSignalTime;
private bool _hasActivePosition;
private bool _isLongPosition;
private decimal _entryPrice;
private decimal _stopPrice;
private decimal _targetPrice;
/// <summary>
/// Initializes a new instance of the <see cref="FastSlowMaCrossoverStrategy"/> class.
/// </summary>
public FastSlowMaCrossoverStrategy()
{
_fastMaPeriod = Param(nameof(FastMaPeriod), 30)
.SetGreaterThanZero()
.SetDisplay("Fast MA Period", "Length of the fast moving average", "Parameters")
;
_slowMaPeriod = Param(nameof(SlowMaPeriod), 80)
.SetGreaterThanZero()
.SetDisplay("Slow MA Period", "Length of the slow moving average", "Parameters")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 80)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Distance in pips for profit taking", "Risk Management")
;
_stopLossPips = Param(nameof(StopLossPips), 80)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Distance in pips for protective stop", "Risk Management")
;
_startTime = Param(nameof(StartTime), new TimeSpan(8, 0, 0))
.SetDisplay("Start Time", "Start of the allowed trading window", "Schedule");
_stopTime = Param(nameof(StopTime), new TimeSpan(18, 0, 0))
.SetDisplay("Stop Time", "End of the allowed trading window", "Schedule");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(120).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Volume of each market order", "Trading")
;
}
/// <summary>
/// Period of the fast moving average.
/// </summary>
public int FastMaPeriod
{
get => _fastMaPeriod.Value;
set => _fastMaPeriod.Value = value;
}
/// <summary>
/// Period of the slow moving average.
/// </summary>
public int SlowMaPeriod
{
get => _slowMaPeriod.Value;
set => _slowMaPeriod.Value = value;
}
/// <summary>
/// Profit target distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Start time of the allowed trading window.
/// </summary>
public TimeSpan StartTime
{
get => _startTime.Value;
set => _startTime.Value = value;
}
/// <summary>
/// Stop time of the allowed trading window.
/// </summary>
public TimeSpan StopTime
{
get => _stopTime.Value;
set => _stopTime.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Volume used for each market order.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set
{
_tradeVolume.Value = value;
Volume = value;
}
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_previousFast = null;
_previousSlow = null;
_lastSignalTime = null;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
_pipSize = CalculatePipSize();
var fastMa = new EMA { Length = FastMaPeriod };
var slowMa = new EMA { Length = SlowMaPeriod };
SubscribeCandles(CandleType)
.Bind(fastMa, slowMa, (candle, fastValue, slowValue) => ProcessCandle(candle, fastValue, slowValue, fastMa.IsFormed && slowMa.IsFormed))
.Start();
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
return 0.0001m;
var decimals = Security?.Decimals ?? 0;
var factor = (decimals == 3 || decimals == 5) ? 10m : 1m;
return priceStep * factor;
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, bool indicatorsFormed)
{
if (candle.State != CandleStates.Finished)
return;
var timeOfDay = candle.CloseTime.TimeOfDay;
if (!IsWithinTradingWindow(timeOfDay))
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
ManageExistingPosition(candle);
if (!indicatorsFormed)
{
_previousFast = fastValue;
_previousSlow = slowValue;
return;
}
if (_previousFast is null || _previousSlow is null)
{
_previousFast = fastValue;
_previousSlow = slowValue;
return;
}
var crossUp = _previousFast <= _previousSlow && fastValue > slowValue;
var crossDown = _previousFast >= _previousSlow && fastValue < slowValue;
if (_pipSize <= 0m)
{
_previousFast = fastValue;
_previousSlow = slowValue;
return;
}
var currentCandleTime = candle.CloseTime;
if (crossUp && Position <= 0m && _lastSignalTime != currentCandleTime)
{
var volume = TradeVolume;
if (Position < 0m)
volume += -Position;
if (volume > 0m)
{
BuyMarket(volume);
RecordEntryState(candle.ClosePrice, true);
_lastSignalTime = currentCandleTime;
}
}
else if (crossDown && Position >= 0m && _lastSignalTime != currentCandleTime)
{
var volume = TradeVolume;
if (Position > 0m)
volume += Position;
if (volume > 0m)
{
SellMarket(volume);
RecordEntryState(candle.ClosePrice, false);
_lastSignalTime = currentCandleTime;
}
}
_previousFast = fastValue;
_previousSlow = slowValue;
}
private void ManageExistingPosition(ICandleMessage candle)
{
if (!_hasActivePosition || _pipSize <= 0m)
return;
var high = candle.HighPrice;
var low = candle.LowPrice;
if (_isLongPosition)
{
if (StopLossPips > 0 && low <= _stopPrice)
{
SellMarket(Position);
ResetPositionState();
return;
}
if (TakeProfitPips > 0 && high >= _targetPrice)
{
SellMarket(Position);
ResetPositionState();
}
}
else
{
if (StopLossPips > 0 && high >= _stopPrice)
{
BuyMarket(-Position);
ResetPositionState();
return;
}
if (TakeProfitPips > 0 && low <= _targetPrice)
{
BuyMarket(-Position);
ResetPositionState();
}
}
}
private void RecordEntryState(decimal closePrice, bool isLong)
{
_hasActivePosition = true;
_isLongPosition = isLong;
_entryPrice = closePrice;
var takeOffset = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
var stopOffset = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
if (isLong)
{
_targetPrice = takeOffset > 0m ? (_entryPrice + takeOffset) : 0m;
_stopPrice = stopOffset > 0m ? (_entryPrice - stopOffset) : 0m;
}
else
{
_targetPrice = takeOffset > 0m ? (_entryPrice - takeOffset) : 0m;
_stopPrice = stopOffset > 0m ? (_entryPrice + stopOffset) : 0m;
}
}
private bool IsWithinTradingWindow(TimeSpan current)
{
var start = StartTime;
var stop = StopTime;
if (start == stop)
return true;
if (start < stop)
return current >= start && current <= stop;
return current >= start || current <= stop;
}
private void ResetPositionState()
{
_hasActivePosition = false;
_isLongPosition = false;
_entryPrice = 0m;
_stopPrice = 0m;
_targetPrice = 0m;
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0m)
{
ResetPositionState();
}
else
{
_hasActivePosition = true;
_isLongPosition = Position > 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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class fast_slow_ma_crossover_strategy(Strategy):
def __init__(self):
super(fast_slow_ma_crossover_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(120)))
self._fast_ma_period = self.Param("FastMaPeriod", 30)
self._slow_ma_period = self.Param("SlowMaPeriod", 80)
self._stop_loss_pct = self.Param("StopLossPct", 2.0)
self._take_profit_pct = self.Param("TakeProfitPct", 3.0)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FastMaPeriod(self):
return self._fast_ma_period.Value
@FastMaPeriod.setter
def FastMaPeriod(self, value):
self._fast_ma_period.Value = value
@property
def SlowMaPeriod(self):
return self._slow_ma_period.Value
@SlowMaPeriod.setter
def SlowMaPeriod(self, value):
self._slow_ma_period.Value = value
@property
def StopLossPct(self):
return self._stop_loss_pct.Value
@StopLossPct.setter
def StopLossPct(self, value):
self._stop_loss_pct.Value = value
@property
def TakeProfitPct(self):
return self._take_profit_pct.Value
@TakeProfitPct.setter
def TakeProfitPct(self, value):
self._take_profit_pct.Value = value
def OnReseted(self):
super(fast_slow_ma_crossover_strategy, self).OnReseted()
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._entry_price = 0.0
def OnStarted2(self, time):
super(fast_slow_ma_crossover_strategy, self).OnStarted2(time)
self._prev_fast = 0.0
self._prev_slow = 0.0
self._has_prev = False
self._entry_price = 0.0
fast_ma = ExponentialMovingAverage()
fast_ma.Length = self.FastMaPeriod
slow_ma = ExponentialMovingAverage()
slow_ma.Length = self.SlowMaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ma, slow_ma, self._process_candle).Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
close = float(candle.ClosePrice)
# Check SL/TP on existing position
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl_pct = (close - self._entry_price) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.SellMarket()
self._entry_price = 0.0
self._prev_fast = fast_val
self._prev_slow = slow_val
self._has_prev = True
return
elif self.Position < 0:
pnl_pct = (self._entry_price - close) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.BuyMarket()
self._entry_price = 0.0
self._prev_fast = fast_val
self._prev_slow = slow_val
self._has_prev = True
return
if self._has_prev:
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.BuyMarket()
self._entry_price = close
elif cross_down and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._prev_fast = fast_val
self._prev_slow = slow_val
self._has_prev = True
def CreateClone(self):
return fast_slow_ma_crossover_strategy()