在 GitHub 上查看

CGOscillator X2 策略

概述

CGOscillator X2 策略是一套基于重心振荡指标(Center of Gravity Oscillator)的多周期顺势系统。策略先在更高周期上评估振荡器的斜率来确认主导趋势,再等待较低周期出现回调钩形信号后顺势入场。策略提供以绝对价格单位表示的止损与止盈参数,允许在开仓后即时设置风险控制。

交易逻辑

  1. 趋势识别(高周期)
    • 在趋势周期上使用参数 TrendLength 计算 CG 振荡器。
    • 当前 CG 值高于信号线(上一周期的 CG 值)时判定为多头趋势;低于信号线时判定为空头趋势。
  2. 信号生成(低周期)
    • 在信号周期上使用独立长度的 CG 振荡器。
    • 策略比较最近两个已完成 K 线:若当前 CG ≥ 信号且上一根 CG < 上一根信号,则视为下跌趋势中的多头钩形;若当前 CG ≤ 信号且上一根 CG > 上一根信号,则视为上涨趋势中的空头钩形。
  3. 开仓与平仓
    • 仅当高周期显示上升趋势且低周期出现空头钩形(回调到超卖区)时才允许做多;做空逻辑为镜像条件。
    • 依据布尔参数可选择在高周期趋势翻转时平仓,或在低周期钩形反向时立即平仓。
  4. 风险控制
    • 每次市价开仓后都会根据参数设置绝对距离的止损和止盈。当价格在当前 K 线内触发止损或止盈时,策略会先行平仓再处理新的信号。

参数说明

名称 说明
TrendCandleType 用于趋势分析的蜡烛类型(时间框架)。
SignalCandleType 用于信号分析的蜡烛类型。
TrendLength 趋势周期 CG 振荡器的长度。
SignalLength 信号周期 CG 振荡器的长度。
BuyOpen 是否允许在上升趋势内开多仓。
SellOpen 是否允许在下降趋势内开空仓。
BuyClose 高周期趋势转为空头时是否平掉多单。
SellClose 高周期趋势转为多头时是否平掉空单。
BuyCloseSignal 低周期出现空头钩形时是否平掉多单。
SellCloseSignal 低周期出现多头钩形时是否平掉空单。
StopLoss 以绝对价格表示的止损距离,0 表示不启用。
TakeProfit 以绝对价格表示的止盈距离,0 表示不启用。

指标细节

自定义的 CenterOfGravityOscillatorIndicator 完全复刻 MT5 版本的 CG 振荡器:

  • 使用 (最高价 + 最低价) / 2 的中位价作为输入。
  • 对最近 Length 根中位价进行加权求和得到 CG 值。
  • 信号线是上一周期的 CG 值,用于识别钩形拐点。

使用提示

  • 通过设置策略的 Volume 属性来确定基础下单数量。若需要反手,策略会自动在市价单中加入当前仓位的绝对值,确保新仓方向正确。
  • 策略仅在 K 线收盘后做出决策,能够减少盘中噪音干扰,但会在每根 K 线收盘时才反应。
  • 止损与止盈采用绝对价格单位,请根据标的品种的最小变动价位与波动性进行调整。
  • 只需配置合适的蜡烛类型,该策略即可用于 StockSharp 支持的任何交易品种。
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that trades pullbacks using the Center of Gravity oscillator on two timeframes.
/// </summary>
public class CgOscillatorX2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _trendCandleType;
	private readonly StrategyParam<DataType> _signalCandleType;
	private readonly StrategyParam<int> _trendLength;
	private readonly StrategyParam<int> _signalLength;
	private readonly StrategyParam<bool> _buyOpen;
	private readonly StrategyParam<bool> _sellOpen;
	private readonly StrategyParam<bool> _buyClose;
	private readonly StrategyParam<bool> _sellClose;
	private readonly StrategyParam<bool> _buyCloseSignal;
	private readonly StrategyParam<bool> _sellCloseSignal;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<int> _signalCooldownBars;

	private CenterOfGravityOscillator _trendIndicator;
	private CenterOfGravityOscillator _signalIndicator;

	private int _trendDirection;
	private decimal? _trendPrevCg;
	private decimal? _signalPrevCg;
	private decimal? _signalPrevPrevCg;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private int _cooldownRemaining;

	public DataType TrendCandleType { get => _trendCandleType.Value; set => _trendCandleType.Value = value; }
	public DataType SignalCandleType { get => _signalCandleType.Value; set => _signalCandleType.Value = value; }
	public int TrendLength { get => _trendLength.Value; set => _trendLength.Value = value; }
	public int SignalLength { get => _signalLength.Value; set => _signalLength.Value = value; }
	public bool BuyOpen { get => _buyOpen.Value; set => _buyOpen.Value = value; }
	public bool SellOpen { get => _sellOpen.Value; set => _sellOpen.Value = value; }
	public bool BuyClose { get => _buyClose.Value; set => _buyClose.Value = value; }
	public bool SellClose { get => _sellClose.Value; set => _sellClose.Value = value; }
	public bool BuyCloseSignal { get => _buyCloseSignal.Value; set => _buyCloseSignal.Value = value; }
	public bool SellCloseSignal { get => _sellCloseSignal.Value; set => _sellCloseSignal.Value = value; }
	public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
	public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
	public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }

	public CgOscillatorX2Strategy()
	{
		_trendCandleType = Param(nameof(TrendCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Trend Candle Type", "Higher timeframe for trend detection", "General");

		_signalCandleType = Param(nameof(SignalCandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Signal Candle Type", "Lower timeframe for trade execution", "General");

		_trendLength = Param(nameof(TrendLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Trend Length", "CG length on the trend timeframe", "Indicator");

		_signalLength = Param(nameof(SignalLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Signal Length", "CG length on the signal timeframe", "Indicator");

		_buyOpen = Param(nameof(BuyOpen), true)
			.SetDisplay("Allow Long Entries", "Enable long entries during uptrend", "Trading");

		_sellOpen = Param(nameof(SellOpen), true)
			.SetDisplay("Allow Short Entries", "Enable short entries during downtrend", "Trading");

		_buyClose = Param(nameof(BuyClose), true)
			.SetDisplay("Close Long On Trend Flip", "Exit long positions when higher trend turns bearish", "Trading");

		_sellClose = Param(nameof(SellClose), true)
			.SetDisplay("Close Short On Trend Flip", "Exit short positions when higher trend turns bullish", "Trading");

		_buyCloseSignal = Param(nameof(BuyCloseSignal), false)
			.SetDisplay("Close Long On Pullback", "Exit long positions when the oscillator confirms a bearish hook", "Trading");

		_sellCloseSignal = Param(nameof(SellCloseSignal), false)
			.SetDisplay("Close Short On Pullback", "Exit short positions when the oscillator confirms a bullish hook", "Trading");

		_stopLoss = Param(nameof(StopLoss), 0m)
			.SetNotNegative()
			.SetDisplay("Stop Loss Distance", "Absolute stop-loss distance in price units", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 0m)
			.SetNotNegative()
			.SetDisplay("Take Profit Distance", "Absolute take-profit distance in price units", "Risk");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 6)
			.SetNotNegative()
			.SetDisplay("Signal Cooldown Bars", "Closed signal candles to wait before a new entry", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TrendCandleType);

		if (!TrendCandleType.Equals(SignalCandleType))
			yield return (Security, SignalCandleType);
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_trendIndicator = new CenterOfGravityOscillator
		{
			Length = TrendLength
		};

		_signalIndicator = new CenterOfGravityOscillator
		{
			Length = SignalLength
		};

		SubscribeCandles(TrendCandleType)
			.BindEx(_trendIndicator, ProcessTrend)
			.Start();

		SubscribeCandles(SignalCandleType)
			.BindEx(_signalIndicator, ProcessSignal)
			.Start();
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_trendDirection = 0;
		_trendPrevCg = null;
		_signalPrevCg = null;
		_signalPrevPrevCg = null;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_cooldownRemaining = 0;
	}

	private void ProcessTrend(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_trendIndicator.IsFormed)
			return;

		var cgValue = value.GetValue<decimal>();
		var prevCg = _trendPrevCg;
		_trendPrevCg = cgValue;

		if (cgValue > 0)
			_trendDirection = 1;
		else if (cgValue < 0)
			_trendDirection = -1;
		else
			_trendDirection = 0;
	}

	private void ProcessSignal(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_signalIndicator.IsFormed)
			return;

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var cgValue = value.GetValue<decimal>();

		var prevCg = _signalPrevCg;
		var prevPrevCg = _signalPrevPrevCg;

		_signalPrevPrevCg = _signalPrevCg;
		_signalPrevCg = cgValue;

		if (prevCg is null)
			return;

		if (TryCloseByRisk(candle))
			return;

		var closeBuy = BuyCloseSignal && prevCg < 0;
		var closeSell = SellCloseSignal && prevCg > 0;
		var openBuy = false;
		var openSell = false;
		var bullishHook = prevPrevCg.HasValue && prevPrevCg.Value >= prevCg && cgValue > prevCg;
		var bearishHook = prevPrevCg.HasValue && prevPrevCg.Value <= prevCg && cgValue < prevCg;

		if (_trendDirection < 0)
		{
			if (BuyClose)
				closeBuy = true;

			if (_cooldownRemaining == 0 && SellOpen && bearishHook)
				openSell = true;
		}
		else if (_trendDirection > 0)
		{
			if (SellClose)
				closeSell = true;

			if (_cooldownRemaining == 0 && BuyOpen && bullishHook)
				openBuy = true;
		}

		if (closeBuy && Position > 0)
		{
			SellMarket(Position);
			ResetRiskTargets();
		}

		if (closeSell && Position < 0)
		{
			BuyMarket(-Position);
			ResetRiskTargets();
		}

		if (openBuy && Position <= 0)
		{
			var volume = Volume + (Position < 0 ? Math.Abs(Position) : 0m);
			BuyMarket(volume);
			SetRiskTargets(candle.ClosePrice, true);
			_cooldownRemaining = SignalCooldownBars;
		}
		else if (openSell && Position >= 0)
		{
			var volume = Volume + (Position > 0 ? Math.Abs(Position) : 0m);
			SellMarket(volume);
			SetRiskTargets(candle.ClosePrice, false);
			_cooldownRemaining = SignalCooldownBars;
		}
	}

	private bool TryCloseByRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetRiskTargets();
				return true;
			}

			if (_takePrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Position);
				ResetRiskTargets();
				return true;
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(-Position);
				ResetRiskTargets();
				return true;
			}

			if (_takePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(-Position);
				ResetRiskTargets();
				return true;
			}
		}

		return false;
	}

	private void SetRiskTargets(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;

		if (StopLoss > 0m)
			_stopPrice = isLong ? entryPrice - StopLoss : entryPrice + StopLoss;
		else
			_stopPrice = null;

		if (TakeProfit > 0m)
			_takePrice = isLong ? entryPrice + TakeProfit : entryPrice - TakeProfit;
		else
			_takePrice = null;
	}

	private void ResetRiskTargets()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}
}