在 GitHub 上查看

重心 OSMA 策略

该策略利用 重心 OSMA 振荡器来识别潜在的趋势反转。 振荡器将简单和加权移动平均相乘,并进行两次平滑处理以跟踪方向变化。 当指标在下跌后向上转折时,策略平掉空头并可选择开多; 当指标在上涨后向下转折时,策略平掉多头并可选择开空。

工作原理

  1. 使用收盘价作为自定义指标输入。
  2. 指标计算过程:
    • 计算周期为 Period 的简单移动平均 (SMA);
    • 计算相同周期的加权移动平均 (WMA);
    • 将两个平均值相乘;
    • 使用 SmoothPeriod1 与 SmoothPeriod2 进行两次额外平滑。
  3. 交易规则:
    • 若前一个值低于其前值且当前值高于前一个值,说明振荡器向上转折,关闭空头并可开多;
    • 若前一个值高于其前值且当前值低于前一个值,说明振荡器向下转折,关闭多头并可开空;
    • StopLoss 与 TakeProfit 以价格单位定义止损和止盈。

参数

  • Period – SMA 与 WMA 的基础周期;
  • SmoothPeriod1 – 第一次平滑的周期;
  • SmoothPeriod2 – 第二次平滑的周期;
  • StopLoss – 止损距离,0 表示禁用;
  • TakeProfit – 止盈距离,0 表示禁用;
  • BuyPosOpen – 是否允许开多;
  • SellPosOpen – 是否允许开空;
  • BuyPosClose – 是否在卖出信号时关闭多头;
  • SellPosClose – 是否在买入信号时关闭空头;
  • CandleType – 用于计算的K线类型(时间框)。

说明

  • 同时提供 C# 和 Python 实现。
  • 修改代码时请使用制表符缩进。
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Center-of-Gravity OSMA: product of a simple and weighted mean,
/// followed by two smoothing stages. Trades on local extrema of the oscillator.
/// </summary>
public class CenterOfGravityOsmaStrategy : Strategy
{
	private readonly StrategyParam<int> _period;
	private readonly StrategyParam<int> _smoothPeriod1;
	private readonly StrategyParam<int> _smoothPeriod2;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _closes = new();
	private readonly Queue<decimal> _products = new();
	private readonly Queue<decimal> _stage1 = new();
	private decimal? _prior;
	private decimal? _priorPrior;
	private Order _pendingOrder;

	public int Period { get => _period.Value; set => _period.Value = value; }
	public int SmoothPeriod1 { get => _smoothPeriod1.Value; set => _smoothPeriod1.Value = value; }
	public int SmoothPeriod2 { get => _smoothPeriod2.Value; set => _smoothPeriod2.Value = value; }
	public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
	public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
	public bool BuyPosOpen { get => _buyPosOpen.Value; set => _buyPosOpen.Value = value; }
	public bool SellPosOpen { get => _sellPosOpen.Value; set => _sellPosOpen.Value = value; }
	public bool BuyPosClose { get => _buyPosClose.Value; set => _buyPosClose.Value = value; }
	public bool SellPosClose { get => _sellPosClose.Value; set => _sellPosClose.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public CenterOfGravityOsmaStrategy()
	{
		_period = Param(nameof(Period), 10).SetGreaterThanZero()
			.SetDisplay("Period", "Base SMA and WMA period", "Indicator");
		_smoothPeriod1 = Param(nameof(SmoothPeriod1), 3).SetGreaterThanZero()
			.SetDisplay("Smooth Period 1", "First smoothing length", "Indicator");
		_smoothPeriod2 = Param(nameof(SmoothPeriod2), 3).SetGreaterThanZero()
			.SetDisplay("Smooth Period 2", "Second smoothing length", "Indicator");
		_stopLoss = Param(nameof(StopLoss), 0m).SetNotNegative()
			.SetDisplay("Stop Loss", "Optional price-unit loss limit", "Risk");
		_takeProfit = Param(nameof(TakeProfit), 0m).SetNotNegative()
			.SetDisplay("Take Profit", "Optional price-unit profit target", "Risk");
		_buyPosOpen = Param(nameof(BuyPosOpen), true).SetDisplay("Buy Pos Open", "Allow long entry", "Trading");
		_sellPosOpen = Param(nameof(SellPosOpen), true).SetDisplay("Sell Pos Open", "Allow short entry", "Trading");
		_buyPosClose = Param(nameof(BuyPosClose), true).SetDisplay("Buy Pos Close", "Allow long exit", "Trading");
		_sellPosClose = Param(nameof(SellPosClose), true).SetDisplay("Sell Pos Close", "Allow short exit", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();
		ResetHistory();
	}

	private void ResetHistory()
	{
		_closes.Clear();
		_products.Clear();
		_stage1.Clear();
		_prior = _priorPrior = null;
		_pendingOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ResetHistory();
		StartProtection(
			takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
			stopLoss: new Unit(StopLoss, UnitTypes.Absolute));
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private static void Append(Queue<decimal> history, decimal price, int period)
	{
		history.Enqueue(price);
		if (history.Count > period)
			history.Dequeue();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;
		Append(_closes, candle.ClosePrice, Period);
		if (_closes.Count < Period)
			return;

		var weighted = 0m;
		var weight = 0;
		foreach (var price in _closes)
		{
			weight++;
			weighted += weight * price;
		}

		var sma = _closes.Average();
		var wma = weighted / (Period * (Period + 1m) / 2m);
		Append(_products, sma * wma, SmoothPeriod1);
		if (_products.Count < SmoothPeriod1)
			return;

		Append(_stage1, _products.Average(), SmoothPeriod2);
		if (_stage1.Count < SmoothPeriod2)
			return;

		var value = _stage1.Average();
		var turnUp = _priorPrior is decimal older && _prior is decimal previous
			&& previous < older && value > previous;
		var turnDown = _priorPrior is decimal olderDown && _prior is decimal previousDown
			&& previousDown > olderDown && value < previousDown;
		_priorPrior = _prior;
		_prior = value;
		if (!IsFormedAndOnlineAndAllowTrading() ||
			(_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed)))
			return;

		if (turnUp)
		{
			if (Position < 0m && SellPosClose)
				_pendingOrder = BuyMarket(-Position + (BuyPosOpen ? Volume : 0m));
			else if (Position == 0m && BuyPosOpen)
				_pendingOrder = BuyMarket(Volume);
		}
		else if (turnDown)
		{
			if (Position > 0m && BuyPosClose)
				_pendingOrder = SellMarket(Position + (SellPosOpen ? Volume : 0m));
			else if (Position == 0m && SellPosOpen)
				_pendingOrder = SellMarket(Volume);
		}
	}
}