CCI MA v1.5 策略
该策略将 MetaTrader 中的 “CCI_MA v1.5” 智能交易系统迁移到 StockSharp 的高层 API。原始版本等待商品通道指数(CCI)与基于自身数值的简单移动平均线产生交叉,并借助第二个 CCI 监控 ±100 附近的超买/超卖回撤。移植后的实现保留了相同的信号顺序、可选的资金管理以及以点为单位的止损/止盈规则,并通过蜡烛订阅与指标绑定完成计算。
工作原理
- 数据来源 – 用户可以配置任意蜡烛序列(默认 15 分钟)。两个 CCI 都使用蜡烛的收盘价,复现 MetaTrader 中的
PRICE_CLOSE选项。 - 核心指标 – 主
CommodityChannelIndex(参数CciPeriod)衡量动量。长度为MaPeriod的SimpleMovingAverage作用于 CCI 序列,得到信号线。第二个 CCI(SignalCciPeriod)监控 ±100 区域的离场条件。 - 开仓逻辑 – 向上交叉后的下一根蜡烛触发买入:上一根已完成蜡烛的 CCI 必须高于其 SMA,而再往前一根蜡烛的 CCI 位于 SMA 之下。做空逻辑与之对称。若当前持有反向仓位,策略会在下单时加上其绝对数量,实现与 MQL 版本一致的反手行为。
- 平仓逻辑 – 多头在监控 CCI 从 +100 上方跌破 +100,或主 CCI 从上向下穿越其 SMA(同样基于前两根已完成蜡烛)时退出。空头条件相反。保护性止损与止盈按照 MetaTrader 的点数规则实现:策略根据交易品种的
PriceStep计算点值(对于三位或五位报价乘以 10),并在每根完成的蜡烛上比较最高/最低价是否触及入场价 ± 距离。 - 仓位规模 –
LotVolume指定基础下单量。当UseMoneyManagement为真时,策略将其乘以floor(balance / DepositPerLot),并受MaxMultiplier限制,完整复刻原程序的资金阶梯。提交订单前会将数量与交易所的VolumeStep、MinVolume和MaxVolume约束对齐。
参数
- Candle Type – 指定用于计算指标的蜡烛数据类型。
- CCI Period – 主 CCI 的周期长度。
- Exit CCI Period – 监控 ±100 阀值的辅助 CCI 周期。
- CCI MA Period – 作用于主 CCI 的简单移动平均线周期。
- Lot Volume – 资金管理前的基础下单量。
- Enable Money Management – 是否开启基于账户余额的仓位扩展。
- Deposit Per Lot – 每增加一手所需的余额增量(仅在启用资金管理时使用)。
- Max Multiplier – 资金管理可达到的最大倍数。
- Stop Loss (pips) – 以点为单位的止损距离(0 表示关闭)。
- Take Profit (pips) – 以点为单位的止盈距离(0 表示关闭)。
策略会在至少收集到两根完整蜡烛后才开始交易,以便完全复现 MQL 中基于前两根蜡烛的比较逻辑。止损与止盈检查在每根已完成蜡烛上执行,并利用其最高价/最低价来近似 MetaTrader 服务器端的保护性订单,同时保持在 StockSharp 高层 API 范围内运行。
namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// Commodity Channel Index strategy converted from the MetaTrader "CCI_MA v1.5" expert advisor.
/// Uses a primary CCI with a manually computed SMA of CCI values as a signal line.
/// A secondary CCI provides overbought/oversold exit confirmation.
/// </summary>
public class CciMaV15Strategy : Strategy
{
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<int> _signalCciPeriod;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private CommodityChannelIndex _cci;
private CommodityChannelIndex _signalCci;
private readonly List<decimal> _cciHistory = new();
private decimal? _prevCciMa;
private decimal? _prevCci;
private decimal? _prevSignalCci;
/// <summary>
/// Primary CCI period.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// Secondary CCI period for exit signals.
/// </summary>
public int SignalCciPeriod
{
get => _signalCciPeriod.Value;
set => _signalCciPeriod.Value = value;
}
/// <summary>
/// SMA period applied to the primary CCI values.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Stop loss distance in absolute points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance in absolute points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public CciMaV15Strategy()
{
_cciPeriod = Param(nameof(CciPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Length of the primary CCI", "CCI")
.SetOptimize(7, 35, 7);
_signalCciPeriod = Param(nameof(SignalCciPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Exit CCI Period", "Length of the secondary CCI", "CCI")
.SetOptimize(7, 35, 7);
_maPeriod = Param(nameof(MaPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("CCI MA Period", "SMA length applied to the CCI", "CCI")
.SetOptimize(3, 21, 3);
_stopLossPoints = Param(nameof(StopLossPoints), 500m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Protective stop distance in absolute points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
.SetNotNegative()
.SetDisplay("Take Profit", "Profit target distance in absolute points", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Market data series", "General");
Volume = 1;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_cci = null;
_signalCci = null;
_cciHistory.Clear();
_prevCciMa = null;
_prevCci = null;
_prevSignalCci = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_cci = new CommodityChannelIndex { Length = CciPeriod };
_signalCci = new CommodityChannelIndex { Length = SignalCciPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_cci, _signalCci, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var indArea = CreateChartArea();
if (indArea != null)
{
DrawIndicator(indArea, _cci);
DrawIndicator(indArea, _signalCci);
}
}
var tp = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
var sl = StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
if (tp != null || sl != null)
StartProtection(tp, sl);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal cciValue, decimal signalCciValue)
{
if (candle.State != CandleStates.Finished)
return;
// Maintain CCI history for manual SMA calculation
_cciHistory.Add(cciValue);
if (_cciHistory.Count > MaPeriod)
_cciHistory.RemoveAt(0);
// Compute SMA of CCI
decimal? cciMa = null;
if (_cciHistory.Count >= MaPeriod)
{
decimal sum = 0;
for (int i = 0; i < _cciHistory.Count; i++)
sum += _cciHistory[i];
cciMa = sum / _cciHistory.Count;
}
if (cciMa == null || _prevCci == null || _prevCciMa == null || _prevSignalCci == null)
{
_prevCci = cciValue;
_prevCciMa = cciMa;
_prevSignalCci = signalCciValue;
return;
}
// Exit logic: secondary CCI overbought/oversold reversal
if (Position > 0 && _prevSignalCci > 100 && signalCciValue <= 100)
{
SellMarket(Position);
}
else if (Position < 0 && _prevSignalCci < -100 && signalCciValue >= -100)
{
BuyMarket(Math.Abs(Position));
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_prevCci = cciValue;
_prevCciMa = cciMa;
_prevSignalCci = signalCciValue;
return;
}
// Entry: CCI crosses above its MA (buy) or below (sell)
if (_prevCci < _prevCciMa && cciValue > cciMa.Value && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
}
else if (_prevCci > _prevCciMa && cciValue < cciMa.Value && Position >= 0)
{
if (Position > 0)
SellMarket(Position);
SellMarket(Volume);
}
_prevCci = cciValue;
_prevCciMa = cciMa;
_prevSignalCci = signalCciValue;
}
}
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, UnitTypes, Unit
from StockSharp.Algo.Indicators import CommodityChannelIndex
from StockSharp.Algo.Strategies import Strategy
class cci_ma_v15_strategy(Strategy):
"""
CCI MA v1.5 strategy. Uses primary CCI with SMA signal line and secondary CCI for exits.
"""
def __init__(self):
super(cci_ma_v15_strategy, self).__init__()
self._cci_period = self.Param("CciPeriod", 14).SetDisplay("CCI Period", "Length of the primary CCI", "CCI")
self._signal_cci_period = self.Param("SignalCciPeriod", 14).SetDisplay("Exit CCI Period", "Length of the secondary CCI", "CCI")
self._ma_period = self.Param("MaPeriod", 9).SetDisplay("CCI MA Period", "SMA length applied to the CCI", "CCI")
self._stop_loss_points = self.Param("StopLossPoints", 500.0).SetDisplay("Stop Loss", "Protective stop distance in absolute points", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 500.0).SetDisplay("Take Profit", "Profit target distance in absolute points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Market data series", "General")
self._cci_history = []
self._prev_cci_ma = None
self._prev_cci = None
self._prev_signal_cci = None
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(cci_ma_v15_strategy, self).OnReseted()
self._cci_history = []
self._prev_cci_ma = None
self._prev_cci = None
self._prev_signal_cci = None
def OnStarted2(self, time):
super(cci_ma_v15_strategy, self).OnStarted2(time)
cci = CommodityChannelIndex()
cci.Length = self._cci_period.Value
signal_cci = CommodityChannelIndex()
signal_cci.Length = self._signal_cci_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(cci, signal_cci, self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
ind_area = self.CreateChartArea()
if ind_area is not None:
self.DrawIndicator(ind_area, cci)
self.DrawIndicator(ind_area, signal_cci)
tp = Unit(self._take_profit_points.Value, UnitTypes.Absolute) if self._take_profit_points.Value > 0 else None
sl = Unit(self._stop_loss_points.Value, UnitTypes.Absolute) if self._stop_loss_points.Value > 0 else None
if tp is not None or sl is not None:
self.StartProtection(tp, sl)
def on_process(self, candle, cci_value, signal_cci_value):
if candle.State != CandleStates.Finished:
return
self._cci_history.append(float(cci_value))
ma_period = self._ma_period.Value
if len(self._cci_history) > ma_period:
self._cci_history.pop(0)
cci_ma = None
if len(self._cci_history) >= ma_period:
cci_ma = sum(self._cci_history) / len(self._cci_history)
if cci_ma is None or self._prev_cci is None or self._prev_cci_ma is None or self._prev_signal_cci is None:
self._prev_cci = float(cci_value)
self._prev_cci_ma = cci_ma
self._prev_signal_cci = float(signal_cci_value)
return
# Exit logic
if self.Position > 0 and self._prev_signal_cci > 100 and float(signal_cci_value) <= 100:
self.SellMarket()
elif self.Position < 0 and self._prev_signal_cci < -100 and float(signal_cci_value) >= -100:
self.BuyMarket()
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_cci = float(cci_value)
self._prev_cci_ma = cci_ma
self._prev_signal_cci = float(signal_cci_value)
return
# Entry logic
if self._prev_cci < self._prev_cci_ma and float(cci_value) > cci_ma and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif self._prev_cci > self._prev_cci_ma and float(cci_value) < cci_ma and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_cci = float(cci_value)
self._prev_cci_ma = cci_ma
self._prev_signal_cci = float(signal_cci_value)
def CreateClone(self):
return cci_ma_v15_strategy()