在 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();
}
}
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, CommodityChannelIndex
from StockSharp.Algo.Strategies import Strategy
class cci_macd_scalper_strategy(Strategy):
def __init__(self):
super(cci_macd_scalper_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._ema_period = self.Param("EmaPeriod", 21)
self._cci_period = self.Param("CciPeriod", 14)
self._prev_cci = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def CciPeriod(self):
return self._cci_period.Value
@CciPeriod.setter
def CciPeriod(self, value):
self._cci_period.Value = value
def OnReseted(self):
super(cci_macd_scalper_strategy, self).OnReseted()
self._prev_cci = None
def OnStarted2(self, time):
super(cci_macd_scalper_strategy, self).OnStarted2(time)
self._prev_cci = None
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
cci = CommodityChannelIndex()
cci.Length = self.CciPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, cci, self._process_candle).Start()
def _process_candle(self, candle, ema_value, cci_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
cci_val = float(cci_value)
close = float(candle.ClosePrice)
if self._prev_cci is None:
self._prev_cci = cci_val
return
# CCI crosses back above oversold zone with trend confirmation -> buy
cci_cross_up = self._prev_cci <= -50.0 and cci_val > -50.0
# CCI crosses back below overbought zone with trend confirmation -> sell
cci_cross_down = self._prev_cci >= 50.0 and cci_val < 50.0
if cci_cross_up and close > ema_val:
if self.Position <= 0:
self.BuyMarket()
elif cci_cross_down and close < ema_val:
if self.Position >= 0:
self.SellMarket()
self._prev_cci = cci_val
def CreateClone(self):
return cci_macd_scalper_strategy()