在 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();
	}
}