CMO 零轴交叉策略
该策略基于 Chande 动量振荡器(CMO)的零轴交叉进行交易。 当振荡器从上向下穿越零轴时开多头,当从下向上穿越零轴时开空头。 可选的止损和止盈(以点数计)用于保护仓位,可分别启用或禁用 多头和空头的开仓及平仓操作。
参数
Volume– 下单数量。CmoPeriod– CMO 指标周期。StopLoss– 止损点数。TakeProfit– 止盈点数。AllowLongEntry– 是否允许开多。AllowShortEntry– 是否允许开空。AllowLongExit– 是否允许在反向信号下平多。AllowShortExit– 是否允许在反向信号下平空。CandleType– 计算所用的时间框架。
交易逻辑
- 订阅指定时间框架的 K 线并计算 CMO。
- 当 CMO 从正值跌破零轴:
- 若允许,则平掉空头仓位;
- 若允许,则开多头仓位。
- 当 CMO 从负值升破零轴:
- 若允许,则平掉多头仓位;
- 若允许,则开空头仓位。
- 止损和止盈以点数形式通过保护性订单实现。
备注
- 仅在 K 线收盘后作出交易决策。
- 策略使用 StockSharp 的高级 API,并通过
Bind连接指标。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that enters on Chande Momentum Oscillator zero cross.
/// </summary>
public class CmoZeroCrossStrategy : Strategy
{
private readonly StrategyParam<int> _cmoPeriod;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<bool> _allowLongEntry;
private readonly StrategyParam<bool> _allowShortEntry;
private readonly StrategyParam<bool> _allowLongExit;
private readonly StrategyParam<bool> _allowShortExit;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _minAbsCmo;
private readonly StrategyParam<int> _cooldownBars;
private ChandeMomentumOscillator _cmo = null!;
private decimal? _prevCmo;
private int _cooldownRemaining;
public int CmoPeriod
{
get => _cmoPeriod.Value;
set => _cmoPeriod.Value = value;
}
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
public bool AllowLongEntry
{
get => _allowLongEntry.Value;
set => _allowLongEntry.Value = value;
}
public bool AllowShortEntry
{
get => _allowShortEntry.Value;
set => _allowShortEntry.Value = value;
}
public bool AllowLongExit
{
get => _allowLongExit.Value;
set => _allowLongExit.Value = value;
}
public bool AllowShortExit
{
get => _allowShortExit.Value;
set => _allowShortExit.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal MinAbsCmo
{
get => _minAbsCmo.Value;
set => _minAbsCmo.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
public CmoZeroCrossStrategy()
{
_cmoPeriod = Param(nameof(CmoPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CMO Period", "Period for Chande Momentum Oscillator", "Indicators");
_stopLoss = Param(nameof(StopLoss), 1000m)
.SetNotNegative()
.SetDisplay("Stop Loss (pt)", "Stop loss in points", "Risk Management");
_takeProfit = Param(nameof(TakeProfit), 2000m)
.SetNotNegative()
.SetDisplay("Take Profit (pt)", "Take profit in points", "Risk Management");
_allowLongEntry = Param(nameof(AllowLongEntry), true)
.SetDisplay("Allow Long Entry", "Permission to open long positions", "Strategy");
_allowShortEntry = Param(nameof(AllowShortEntry), true)
.SetDisplay("Allow Short Entry", "Permission to open short positions", "Strategy");
_allowLongExit = Param(nameof(AllowLongExit), true)
.SetDisplay("Allow Long Exit", "Permission to close long positions", "Strategy");
_allowShortExit = Param(nameof(AllowShortExit), true)
.SetDisplay("Allow Short Exit", "Permission to close short positions", "Strategy");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for calculations", "General");
_minAbsCmo = Param(nameof(MinAbsCmo), 5m)
.SetDisplay("Minimum CMO", "Minimum absolute CMO value required after a zero cross", "Filters");
_cooldownBars = Param(nameof(CooldownBars), 4)
.SetDisplay("Cooldown Bars", "Completed candles to wait after a position change", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_cmo = null!;
_prevCmo = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_cmo = new ChandeMomentumOscillator { Length = CmoPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_cmo, ProcessCandle).Start();
StartProtection(
takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
stopLoss: new Unit(StopLoss, UnitTypes.Absolute));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _cmo);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal cmoValue)
{
if (candle.State != CandleStates.Finished || _cmo == null || !_cmo.IsFormed)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var prev = _prevCmo;
_prevCmo = cmoValue;
if (prev == null || _cooldownRemaining > 0)
return;
var crossUp = prev < 0m && cmoValue > 0m && Math.Abs(cmoValue) >= MinAbsCmo;
var crossDown = prev > 0m && cmoValue < 0m && Math.Abs(cmoValue) >= MinAbsCmo;
if (crossUp)
{
if (AllowShortExit && Position < 0)
BuyMarket();
if (AllowLongEntry && Position <= 0)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
}
else if (crossDown)
{
if (AllowLongExit && Position > 0)
SellMarket();
if (AllowShortEntry && Position >= 0)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
}
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import ChandeMomentumOscillator
from StockSharp.Algo.Strategies import Strategy
class cmo_zero_cross_strategy(Strategy):
def __init__(self):
super(cmo_zero_cross_strategy, self).__init__()
self._cmo_period = self.Param("CmoPeriod", 14) \
.SetDisplay("CMO Period", "Period for Chande Momentum Oscillator", "Indicators")
self._stop_loss = self.Param("StopLoss", 1000.0) \
.SetDisplay("Stop Loss (pt)", "Stop loss in points", "Risk Management")
self._take_profit = self.Param("TakeProfit", 2000.0) \
.SetDisplay("Take Profit (pt)", "Take profit in points", "Risk Management")
self._allow_long_entry = self.Param("AllowLongEntry", True) \
.SetDisplay("Allow Long Entry", "Permission to open long positions", "Strategy")
self._allow_short_entry = self.Param("AllowShortEntry", True) \
.SetDisplay("Allow Short Entry", "Permission to open short positions", "Strategy")
self._allow_long_exit = self.Param("AllowLongExit", True) \
.SetDisplay("Allow Long Exit", "Permission to close long positions", "Strategy")
self._allow_short_exit = self.Param("AllowShortExit", True) \
.SetDisplay("Allow Short Exit", "Permission to close short positions", "Strategy")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe for calculations", "General")
self._min_abs_cmo = self.Param("MinAbsCmo", 5.0) \
.SetDisplay("Minimum CMO", "Minimum absolute CMO value required after a zero cross", "Filters")
self._cooldown_bars = self.Param("CooldownBars", 4) \
.SetDisplay("Cooldown Bars", "Completed candles to wait after a position change", "Trading")
self._prev_cmo = None
self._cooldown_remaining = 0
@property
def cmo_period(self):
return self._cmo_period.Value
@property
def stop_loss(self):
return self._stop_loss.Value
@property
def take_profit(self):
return self._take_profit.Value
@property
def allow_long_entry(self):
return self._allow_long_entry.Value
@property
def allow_short_entry(self):
return self._allow_short_entry.Value
@property
def allow_long_exit(self):
return self._allow_long_exit.Value
@property
def allow_short_exit(self):
return self._allow_short_exit.Value
@property
def candle_type(self):
return self._candle_type.Value
@property
def min_abs_cmo(self):
return self._min_abs_cmo.Value
@property
def cooldown_bars(self):
return self._cooldown_bars.Value
def OnReseted(self):
super(cmo_zero_cross_strategy, self).OnReseted()
self._prev_cmo = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(cmo_zero_cross_strategy, self).OnStarted2(time)
cmo = ChandeMomentumOscillator()
cmo.Length = self.cmo_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(cmo, self.process_candle).Start()
self.StartProtection(
Unit(float(self.take_profit), UnitTypes.Absolute),
Unit(float(self.stop_loss), UnitTypes.Absolute))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, cmo)
self.DrawOwnTrades(area)
def process_candle(self, candle, cmo_value):
if candle.State != CandleStates.Finished:
return
cmo_value = float(cmo_value)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
prev = self._prev_cmo
self._prev_cmo = cmo_value
if prev is None or self._cooldown_remaining > 0:
return
min_cmo = float(self.min_abs_cmo)
cross_up = prev < 0 and cmo_value > 0 and abs(cmo_value) >= min_cmo
cross_down = prev > 0 and cmo_value < 0 and abs(cmo_value) >= min_cmo
if cross_up:
if self.allow_short_exit and self.Position < 0:
self.BuyMarket()
if self.allow_long_entry and self.Position <= 0:
self.BuyMarket()
self._cooldown_remaining = self.cooldown_bars
elif cross_down:
if self.allow_long_exit and self.Position > 0:
self.SellMarket()
if self.allow_short_entry and self.Position >= 0:
self.SellMarket()
self._cooldown_remaining = self.cooldown_bars
def CreateClone(self):
return cmo_zero_cross_strategy()