在 GitHub 上查看
CHO Smoothed EA 策略
概述
该策略复刻原始的“CHO Smoothed EA”智能交易系统。在每根已完成的K线上计算Chaikin振荡指标,使用可配置的移动平均线对指标进行平滑处理。策略支持交易时段限制、方向控制以及零轴过滤等附加条件,信号满足后立即以市价单入场,并通过以点数表示的止损、止盈和移动止损来管理持仓风险。
交易逻辑
- Chaikin振荡指标在每根收盘的K线上计算,快速与慢速周期均可配置。
- 通过移动平均线对振荡指标进行平滑,形成信号线,移动平均的周期和类型都可以调整。
- 当振荡值向上穿越信号线时触发做多信号,反之向下穿越时触发做空信号,可以选择将信号反向交易。
- 启用零轴过滤后,做多时要求振荡值与信号值同时低于零轴,做空时则需要两者同时高于零轴。
- 策略可以在入场前自动平掉反向仓位,也可以等待仓位清空后才接受信号,还支持“仅保持单一仓位”模式。
- 可配置日内交易时间窗口,支持跨越午夜的区间。
- 入场后按照品种最小报价单位把点数参数转换为价格距离,持续监控K线是否触发止损、止盈或移动止损。
风险控制
- 止损与止盈均以入场价格为基准,根据点数×最小报价单位得到具体价位。
- 移动止损在价格按设定的最小前进幅度运行后启动,并始终以设定的点数距离跟随价格。
- 一旦触发任意保护条件,策略立即以市价单平仓,并重置所有风险控制水平。
参数说明
- Candle Type:用于计算指标的K线周期。
- Fast Period / Slow Period:Chaikin振荡指标的快速与慢速周期。
- Signal MA Period / Signal MA Type:对振荡指标进行平滑的移动平均设置。
- Use Zero Level:是否要求信号生成时位于零轴正确的一侧。
- Trade Mode:限制仅做多、仅做空或双向交易。
- Reverse Signals:是否反向执行多空信号。
- Close Opposite:入场前是否平掉已有的反向仓位。
- Only One Position:是否禁止在已有仓位的情况下再次入场。
- Use Time Control / Start Time / End Time:启用并配置每日交易时间窗口。
- Stop Loss (pts):以点数表示的止损距离。
- Take Profit (pts):以点数表示的止盈距离。
- Trailing Stop (pts):以点数表示的移动止损距离。
- Trailing Step (pts):移动止损更新前价格需要前进的最小点数。
其他说明
- 启动前请设置策略的
Volume 属性来控制下单手数。
- 策略使用市价单,实盘运行时请充分考虑滑点与流动性。
- 当交易窗口的开始时间与结束时间相同,策略会保持不交易,这与原始EA的行为一致。
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>
/// Trades CCI oscillator zero-line crossovers with signal MA smoothing.
/// Originally based on Chaikin Oscillator concept, adapted to use CCI.
/// </summary>
public class ChoSmoothedEaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<int> _maPeriod;
private CommodityChannelIndex _cci;
private readonly Queue<decimal> _cciHistory = new();
private decimal? _prevCci;
private decimal? _prevSignal;
/// <summary>
/// Initializes a new instance of the <see cref="ChoSmoothedEaStrategy"/> class.
/// </summary>
public ChoSmoothedEaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for signal calculations", "General");
_cciPeriod = Param(nameof(CciPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Period for CCI oscillator", "Indicator");
_maPeriod = Param(nameof(MaPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("Signal MA Period", "Period of smoothing moving average on CCI", "Indicator");
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// CCI oscillator period.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// Period of the smoothing moving average.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevCci = null;
_prevSignal = null;
_cciHistory.Clear();
_cci = new CommodityChannelIndex { Length = CciPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_cci, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal cciValue)
{
if (candle.State != CandleStates.Finished)
return;
_cciHistory.Enqueue(cciValue);
while (_cciHistory.Count > MaPeriod)
_cciHistory.Dequeue();
if (!_cci.IsFormed)
return;
if (_cciHistory.Count < MaPeriod)
{
_prevCci = cciValue;
return;
}
// Calculate signal line (SMA of CCI)
var sum = 0m;
var history = _cciHistory.ToArray();
foreach (var v in history)
sum += v;
var signalValue = sum / history.Length;
if (_prevCci is null || _prevSignal is null)
{
_prevCci = cciValue;
_prevSignal = signalValue;
return;
}
var crossUp = _prevCci <= _prevSignal && cciValue > signalValue;
var crossDown = _prevCci >= _prevSignal && cciValue < signalValue;
var volume = Volume;
if (volume <= 0)
volume = 1;
var minSpread = 25m;
if (crossUp && Math.Abs(cciValue - signalValue) >= minSpread)
{
if (Position <= 0)
BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
}
else if (crossDown && Math.Abs(cciValue - signalValue) >= minSpread)
{
if (Position >= 0)
SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
}
_prevCci = cciValue;
_prevSignal = signalValue;
}
/// <inheritdoc />
protected override void OnReseted()
{
_prevCci = null;
_prevSignal = null;
_cci = null;
_cciHistory.Clear();
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 CommodityChannelIndex
from StockSharp.Algo.Strategies import Strategy
class cho_smoothed_ea_strategy(Strategy):
def __init__(self):
super(cho_smoothed_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._cci_period = self.Param("CciPeriod", 20)
self._ma_period = self.Param("MaPeriod", 9)
self._cci_history = []
self._prev_cci = 0.0
self._prev_signal = 0.0
self._has_prev = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def CciPeriod(self):
return self._cci_period.Value
@CciPeriod.setter
def CciPeriod(self, value):
self._cci_period.Value = value
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
def OnReseted(self):
super(cho_smoothed_ea_strategy, self).OnReseted()
self._cci_history = []
self._prev_cci = 0.0
self._prev_signal = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(cho_smoothed_ea_strategy, self).OnStarted2(time)
self._cci_history = []
self._prev_cci = 0.0
self._prev_signal = 0.0
self._has_prev = False
cci = CommodityChannelIndex()
cci.Length = self.CciPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(cci, self._process_candle).Start()
def _process_candle(self, candle, cci_value):
if candle.State != CandleStates.Finished:
return
cci_val = float(cci_value)
ma_len = self.MaPeriod
self._cci_history.append(cci_val)
while len(self._cci_history) > ma_len:
self._cci_history.pop(0)
if len(self._cci_history) < ma_len:
self._prev_cci = cci_val
return
signal_val = sum(self._cci_history) / ma_len
if self._has_prev:
cross_up = self._prev_cci <= self._prev_signal and cci_val > signal_val
cross_down = self._prev_cci >= self._prev_signal and cci_val < signal_val
min_spread = 25.0
if cross_up and abs(cci_val - signal_val) >= min_spread and self.Position <= 0:
self.BuyMarket()
elif cross_down and abs(cci_val - signal_val) >= min_spread and self.Position >= 0:
self.SellMarket()
self._prev_cci = cci_val
self._prev_signal = signal_val
self._has_prev = True
def CreateClone(self):
return cho_smoothed_ea_strategy()