在 GitHub 上查看

CCI MACD Scalper

概述

CCI MACD Scalper 将 MetaTrader 5 专家顾问 “CCI + MACD Scalper” 迁移到 StockSharp 的高级策略 API。原有的指标组合被完整保留:EMA 用于趋势过滤,CCI 用于零轴触发,MACD 用于确认动量;资金管理规则则按照 StockSharp 的约定重写。仓位规模依据账户权益自动计算,过小的止损会被拒绝,启用追踪止损后首次移动会部分锁定利润。为了模拟 MQL 中的 EventSetTimer,每次成交后都会暂停五根 K 线才允许再次开仓。

策略逻辑

指标与数据处理

  • K 线 – 所有计算都基于一个可配置的时间框架,只处理已完成的 K 线,避免信号重绘。
  • EMA(34) – 以收盘价计算的指数平均线提供趋势方向。做多要求当前收盘价高于前一根 EMA,做空要求低于前一根 EMA。
  • CCI(50) – 作为动量触发条件。必须在最近两根完成的 K 线上完成零轴交叉(当前 K 线只用于确认,不参与比较)。
  • MACD(12,26,9) – MACD 主线与信号线在前两根 K 线上需位于零轴同侧,并且信号线必须在这两根之间向有利方向穿越主线(向上交叉对应多单,向下交叉对应空单)。
  • 摆动缓冲区 – 最近五根完成 K 线的最高价与最低价构成止损参考,多单使用最低价,空单使用最高价,与原脚本中 iLowest/iHighest(偏移一根)完全一致。

入场规则

  • 交易时段 – 仅当 K 线收盘时间处于本地时区的 [MinHour, MaxHour] 区间内时才允许开仓。
  • 冷却机制 – 每次开仓后需等待五个所选时间框架的长度才能再次尝试入场,完全复刻原始 EA 的计时器行为。
  • 多头条件
    • 当前净仓位不为正(Position <= 0)。
    • 收盘价高于上一根 EMA 数值。
    • 最近两根 K 线的 CCI 从负值穿越到正值。
    • MACD 在同一时间段内出现位于零轴下方的信号线向上交叉。
    • 止损位于最近低点且满足最小距离约束。
  • 空头条件
    • 当前净仓位不为负(Position >= 0)。
    • 收盘价低于上一根 EMA 数值。
    • 最近两根 K 线的 CCI 从正值穿越到负值。
    • MACD 在零轴上方出现信号线向下交叉。
    • 止损位于最近高点并满足最小距离要求。

风险控制与仓位管理

  • 动态仓位 – 根据参数 RiskPercent 与账户权益计算下单手数。通过止损距离、价格步长及步长价值评估单份风险,结果按照交易所的最小变动手数取整,并限制在允许的最小/最大范围内。
  • 止损 / 止盈 – 止损取对应的摆动极值,如距离小于 MinimalStopLossPoints 会被拒绝。止盈按 entry ± RiskReward × stopDistance 计算,延续原版 EA 的盈亏比设定。
  • 追踪止损(可选) – 启用后,当收盘价超出当前止损 TrailingStopPoints 的距离时更新止损。第一次移动会自动平掉初始仓位的一半,以复刻 MetaTrader 中的部分平仓逻辑。
  • 保护性平仓 – 多单在价格跌破止损(K 线最低价)或触及止盈(最高价)时平仓;空单采取对称判断。

参数

名称 说明 默认值
CandleType 用于计算的 K 线时间框架。 15 分钟
RiskPercent 每笔交易风险占账户权益的百分比。 2%
RiskReward 止盈与止损的收益风险比。 1.5
EmaPeriod EMA 趋势过滤的周期。 34
CciPeriod CCI 指标的周期。 50
MinHour 允许开仓的起始小时(含)。 0
MaxHour 允许开仓的结束小时(含)。 24
MinimalStopLossPoints 入场到止损的最小允许距离(点)。 100
UseTrailingStop 是否启用追踪止损及部分平仓。 关闭
TrailingStopPoints 追踪止损的距离(点)。 100

其他说明

  • 点值转换依赖标的的 PriceStep。若缺少有效步长,则退化为 1 个价格单位。
  • 账户权益优先使用 Portfolio.CurrentValue,若不可用则退回 BeginValue。当两者都缺失时,策略回退到基础的 Volume 手数。
  • 本策略仅提供 C# 实现,未包含 Python 版本。
using System;
using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "CCI MACD Scalper" MetaTrader expert.
/// Uses CCI zero-line crossover with EMA trend filter for scalping entries.
/// </summary>
public class CciMacdScalperStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _cciPeriod;

	private ExponentialMovingAverage _ema;
	private CommodityChannelIndex _cci;
	private decimal? _prevCci;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	public CciMacdScalperStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for scalping", "General");

		_emaPeriod = Param(nameof(EmaPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA trend filter period", "Indicators");

		_cciPeriod = Param(nameof(CciPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "CCI period for zero-line crosses", "Indicators");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_prevCci = null;
		_ema = new ExponentialMovingAverage { Length = EmaPeriod };
		_cci = new CommodityChannelIndex { Length = CciPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_ema, _cci, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ema);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal emaValue, decimal cciValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_ema.IsFormed || !_cci.IsFormed)
		{
			_prevCci = cciValue;
			return;
		}

		if (_prevCci is null)
		{
			_prevCci = cciValue;
			return;
		}

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		var close = candle.ClosePrice;

		// CCI crosses back above the oversold zone with trend confirmation -> buy
		var cciCrossUp = _prevCci.Value <= -50m && cciValue > -50m;
		// CCI crosses back below the overbought zone with trend confirmation -> sell
		var cciCrossDown = _prevCci.Value >= 50m && cciValue < 50m;

		if (cciCrossUp && close > emaValue)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		else if (cciCrossDown && close < emaValue)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		_prevCci = cciValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_ema = null;
		_cci = null;
		_prevCci = null;

		base.OnReseted();
	}
}