在 GitHub 上查看

OzFx Accelerator Stochastic 策略

概述

  • 将 MetaTrader 顾问 OzFx (barabashkakvn 版本) 移植到 StockSharp 的高级策略 API。
  • 通过加速/减速指标(AC)与随机指标阈值的组合,在趋势启动时分批加仓。
  • 适用于以手数下单、以点数设置止损止盈的外汇交易场景。

交易逻辑

  1. 计算 AC 指标:Awesome Oscillator 减去其 5 周期简单均线。
  2. 订阅带有可调 %K%D 以及减速参数的随机指标。
  3. 每根 K 线收盘后联合评估最近两个 AC 值与当前随机值:
    • 做多%K 上穿阈值,当前 AC 为正且大于上一值,而上一值为负。
    • 做空%K 下穿阈值,当前 AC 为负且小于上一值,而上一值为正。
  4. 条件成立时最多开出 5 笔等量市价单。第一笔保持与原 EA 一致,不设置初始止损/止盈;其余四笔继承统一的止损,并按照倍数递增的止盈目标挂出。
  5. 持仓管理复刻 MQL 中的 modok 逻辑:
    • TrailingStopPips 为 0 时,只在前一次仓位盈利离场后才将第一层止损提到开仓价,若 AC+随机组合翻转则一次性平掉全部层级。
    • 当设置了正的 TrailingStopPips 时,价格超过 TrailingStop + TrailingStep 后持续上移止损,同样的动量反转也会触发全部平仓。

分批与目标

  • 多头额外四层的止盈价分别为 entry + TakeProfit * i (i = 1..4),空头对称设置在下方。
  • 除首层外,每层都会带上初始止损,与 MT5 版本完全一致。
  • 任意一层触发止盈都会把内部 modok 标志设为 true,从而下一轮信号立即获得首层的保本保护。

风险控制

  • StopLossPipsTakeProfitPips 以点数配置,策略依据交易品种的最小报价步长和小数位自动换算为价格距离(对 5 位、3 位报价自动处理“迷你点”)。
  • TrailingStopPips = 0 表示只启用保本移动;设为正值则使用完整的跟踪止损算法。
  • 由于 StockSharp 使用市价指令平仓,当 K 线的高/低触及存储的止损或止盈水平时,会立即发出反向市价单,与原顾问在服务器端触发保护单的效果一致。

参数

名称 说明 默认值
OrderVolume 每一层的下单手数。 0.1
StopLossPips 止损距离(点)。 100
TakeProfitPips 相邻止盈之间的基础间距(点)。 50
TrailingStopPips 跟踪止损距离(点,0 表示关闭)。 50
TrailingStepPips 更新跟踪止损前所需的额外移动幅度。 5
KPeriod 随机指标 %K 周期。 5
DPeriod %D 平滑周期。 3
SmoothingPeriod %K 终极平滑周期。 3
StochasticLevel 多空分界阈值。 50
CandleType 计算所用的 K 线类型。 4 小时

实现细节

  • 所有信号、跟踪和保护动作均基于收盘价执行,保持与原 EA 在新 Bar 开启时决策的节奏一致。
  • AC 指标由 Awesome Oscillator 与其 5 周期 SMA 动态组合,无需直接访问指标缓冲区。
  • 点值换算自动适配 4/5 位外汇品种,在缺失交易所数据时会采用合理的保底步长。
  • 策略内部维护每一层的建仓信息,从而精确复现分批止盈、止损迁移以及 modok 状态切换。
  • 采用市场化平仓方式:当 K 线的高低价突破记录的止损/止盈水平时立即执行平仓,避免与服务器端挂单状态不一致。
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;
using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;



/// <summary>
/// OzFx strategy converted from MetaTrader 5 to the StockSharp high-level API.
/// Stacks multiple entries when the Acceleration/Deceleration oscillator and stochastic agree.
/// Implements layered targets and dynamic protection to mimic the expert advisor behaviour.
/// </summary>
public class OzFxAcceleratorStochasticStrategy : Strategy
{
	private readonly StrategyParam<int> _maxLayers;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _smoothingPeriod;
	private readonly StrategyParam<decimal> _stochasticLevel;
	private readonly StrategyParam<DataType> _candleType;

	private AwesomeOscillator _ao = null!;
	private SimpleMovingAverage _aoSma = null!;
	private StochasticOscillator _stochastic = null!;

	private decimal? _lastAc;
	private bool _lastExitWasTakeProfit;
	private decimal _pipSize;
	private bool _pipInitialized;

	private readonly List<EntryInfo> _longEntries = new();
	private readonly List<EntryInfo> _shortEntries = new();

	/// <summary>
	/// Defines exit origin to replicate modok flag logic.
	/// </summary>
	private enum ExitReasons
	{
		Manual,
		TakeProfit,
		StopLoss,
	}

	/// <summary>
	/// Stores layered entry metadata (volume, price, protective levels).
	/// </summary>
	private sealed class EntryInfo
	{
		public decimal Volume;
		public decimal EntryPrice;
		public decimal? StopPrice;
		public decimal? TakeProfitPrice;
		public int Layer;
	}

	/// <summary>
	/// Maximum number of layered positions.
	/// </summary>
	public int MaxLayers
	{
		get => _maxLayers.Value;
		set => _maxLayers.Value = value;
	}

	/// <summary>
	/// Order volume for each layer.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Base take profit distance per layer measured in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips. Zero disables trailing mode.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Additional distance in pips before the trailing stop is advanced.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Main stochastic lookback period.
	/// </summary>
	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	/// <summary>
	/// %D smoothing length.
	/// </summary>
	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	/// <summary>
	/// Final smoothing applied to %K.
	/// </summary>
	public int SmoothingPeriod
	{
		get => _smoothingPeriod.Value;
		set => _smoothingPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic threshold separating bullish and bearish regimes.
	/// </summary>
	public decimal StochasticLevel
	{
		get => _stochasticLevel.Value;
		set => _stochasticLevel.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="OzFxAcceleratorStochasticStrategy"/>.
	/// </summary>
	public OzFxAcceleratorStochasticStrategy()
	{
		_maxLayers = Param(nameof(MaxLayers), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Layers", "Maximum number of layered positions", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume for each layer", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 10m)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 5m)
			.SetDisplay("Take Profit (pips)", "Base take profit increment in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetDisplay("Trailing Step (pips)", "Extra move required before advancing the trailing stop", "Risk");

		_kPeriod = Param(nameof(KPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("%K Period", "Stochastic lookback window", "Stochastic");

		_dPeriod = Param(nameof(DPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("%D Period", "Smoothing length for %D", "Stochastic");

		_smoothingPeriod = Param(nameof(SmoothingPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Slowing", "Final smoothing for %K", "Stochastic");

		_stochasticLevel = Param(nameof(StochasticLevel), 50m)
			.SetDisplay("Stochastic Level", "Threshold used to trigger signals", "Stochastic");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

		_longEntries.Clear();
		_shortEntries.Clear();
		_lastAc = null;
		_lastExitWasTakeProfit = false;
		_pipInitialized = false;
		_pipSize = 0m;
	}

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

		_ao = new AwesomeOscillator
		{
			ShortMa = { Length = 5 },
			LongMa = { Length = 34 },
		};

		_aoSma = new SMA
		{
			Length = 5,
		};

		_stochastic = new StochasticOscillator
		{
			K = { Length = KPeriod },
			D = { Length = DPeriod },
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_ao, _stochastic, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ao);
			DrawIndicator(area, _stochastic);
			DrawOwnTrades(area);
		}
	}

	/// <summary>
	/// Processes finished candles, updates indicators, and manages entries/exits.
	/// </summary>
	private void ProcessCandle(ICandleMessage candle, IIndicatorValue aoValue, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!aoValue.IsFinal || !stochValue.IsFinal)
			return;

		var stoch = (StochasticOscillatorValue)stochValue;
		if (stoch.K is not decimal stochK)
			return;

		var ao = aoValue.GetValue<decimal>();
		var aoSmaValue = _aoSma.Process(new DecimalIndicatorValue(_aoSma, ao, candle.ServerTime) { IsFinal = true });
		if (!_aoSma.IsFormed)
			return;

		var ac = ao - aoSmaValue.GetValue<decimal>();
		var prevAcNullable = _lastAc;
		if (prevAcNullable is not decimal prevAc)
		{
			_lastAc = ac;
			return;
		}

		// indicators checked via BindEx
		if (!_ao.IsFormed || !_stochastic.IsFormed)
		{
			_lastAc = ac;
			return;
		}

		var pip = GetPipSize();
		var stopDistance = StopLossPips > 0m ? StopLossPips * pip : 0m;
		var takeDistance = TakeProfitPips > 0m ? TakeProfitPips * pip : 0m;
		var trailingStopDistance = TrailingStopPips > 0m ? TrailingStopPips * pip : 0m;
		var trailingStepDistance = TrailingStepPips > 0m ? TrailingStepPips * pip : 0m;
		var useTrailing = TrailingStopPips > 0m;

		TryEnterLong(candle, stochK, ac, prevAc, stopDistance, takeDistance);
		TryEnterShort(candle, stochK, ac, prevAc, stopDistance, takeDistance);

		ManageLongPositions(candle, stochK, ac, prevAc, trailingStopDistance, trailingStepDistance, useTrailing);
		ManageShortPositions(candle, stochK, ac, prevAc, trailingStopDistance, trailingStepDistance, useTrailing);

		_lastAc = ac;
	}

	/// <summary>
	/// Opens up to five long layers when momentum turns bullish.
	/// </summary>
	private void TryEnterLong(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal stopDistance, decimal takeDistance)
	{
		if (_longEntries.Count != 0 || _shortEntries.Count != 0)
			return;

		if (!(stochK > StochasticLevel && currentAc > previousAc))
			return;

		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		var entryPrice = candle.ClosePrice;

		// First layer mirrors the expert advisor: no stop or target until trailing engages.
		BuyMarket();
		_longEntries.Add(new EntryInfo
		{
			Volume = volume,
			EntryPrice = entryPrice,
			StopPrice = null,
			TakeProfitPrice = null,
			Layer = 0,
		});

		for (var i = 1; i < MaxLayers; i++)
		{
			BuyMarket();

			var stopPrice = stopDistance > 0m ? entryPrice - stopDistance : (decimal?)null;
			var takePrice = takeDistance > 0m ? entryPrice + takeDistance * i : (decimal?)null;

			_longEntries.Add(new EntryInfo
			{
				Volume = volume,
				EntryPrice = entryPrice,
				StopPrice = stopPrice,
				TakeProfitPrice = takePrice,
				Layer = i,
			});
		}
	}

	/// <summary>
	/// Opens up to five short layers when momentum turns bearish.
	/// </summary>
	private void TryEnterShort(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal stopDistance, decimal takeDistance)
	{
		if (_shortEntries.Count != 0 || _longEntries.Count != 0)
			return;

		if (!(stochK < StochasticLevel && currentAc < previousAc))
			return;

		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		var entryPrice = candle.ClosePrice;

		SellMarket();
		_shortEntries.Add(new EntryInfo
		{
			Volume = volume,
			EntryPrice = entryPrice,
			StopPrice = null,
			TakeProfitPrice = null,
			Layer = 0,
		});

		for (var i = 1; i < MaxLayers; i++)
		{
			SellMarket();

			var stopPrice = stopDistance > 0m ? entryPrice + stopDistance : (decimal?)null;
			var takePrice = takeDistance > 0m ? entryPrice - takeDistance * i : (decimal?)null;

			_shortEntries.Add(new EntryInfo
			{
				Volume = volume,
				EntryPrice = entryPrice,
				StopPrice = stopPrice,
				TakeProfitPrice = takePrice,
				Layer = i,
			});
		}
	}

	/// <summary>
	/// Manages open long layers including trailing logic and staged targets.
	/// </summary>
	private void ManageLongPositions(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal trailingStopDistance, decimal trailingStepDistance, bool useTrailing)
	{
		if (_longEntries.Count == 0)
			return;

		if (Position <= 0m)
		{
			_longEntries.Clear();
			return;
		}

		var closePrice = candle.ClosePrice;
		var highPrice = candle.HighPrice;
		var lowPrice = candle.LowPrice;

		var exitSignal = stochK < 50m && currentAc < previousAc;

		if (useTrailing)
		{
			if (exitSignal)
			{
				CloseAllLong(ExitReasons.Manual);
				return;
			}

			if (trailingStopDistance > 0m)
			{
				for (var i = 0; i < _longEntries.Count; i++)
				{
					var entry = _longEntries[i];
					var profit = closePrice - entry.EntryPrice;
					if (profit > trailingStopDistance + trailingStepDistance)
					{
						var newStop = closePrice - trailingStopDistance;
						if (entry.StopPrice is not decimal existing || newStop > existing)
							entry.StopPrice = newStop;
					}
				}
			}
		}
		else if (_lastExitWasTakeProfit)
		{
			if (exitSignal)
			{
				CloseAllLong(ExitReasons.Manual);
				return;
			}

			for (var i = 0; i < _longEntries.Count; i++)
			{
				var entry = _longEntries[i];
				if (entry.StopPrice is null && closePrice > entry.EntryPrice)
					entry.StopPrice = entry.EntryPrice;
			}
		}

		for (var i = 0; i < _longEntries.Count; i++)
		{
			var entry = _longEntries[i];
			if (entry.StopPrice is decimal stopPrice && lowPrice <= stopPrice)
			{
				CloseAllLong(ExitReasons.StopLoss);
				return;
			}
		}

		var anyTakeProfit = false;
		for (var i = _longEntries.Count - 1; i >= 0; i--)
		{
			var entry = _longEntries[i];
			if (entry.TakeProfitPrice is decimal takePrice && highPrice >= takePrice)
			{
				SellMarket();
				_longEntries.RemoveAt(i);
				anyTakeProfit = true;
			}
		}

		if (anyTakeProfit)
			_lastExitWasTakeProfit = true;
	}

	/// <summary>
	/// Manages open short layers including trailing logic and staged targets.
	/// </summary>
	private void ManageShortPositions(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal trailingStopDistance, decimal trailingStepDistance, bool useTrailing)
	{
		if (_shortEntries.Count == 0)
			return;

		if (Position >= 0m)
		{
			_shortEntries.Clear();
			return;
		}

		var closePrice = candle.ClosePrice;
		var highPrice = candle.HighPrice;
		var lowPrice = candle.LowPrice;

		var exitSignal = stochK > 50m && currentAc > previousAc;

		if (useTrailing)
		{
			if (exitSignal)
			{
				CloseAllShort(ExitReasons.Manual);
				return;
			}

			if (trailingStopDistance > 0m)
			{
				for (var i = 0; i < _shortEntries.Count; i++)
				{
					var entry = _shortEntries[i];
					var profit = entry.EntryPrice - closePrice;
					if (profit > trailingStopDistance + trailingStepDistance)
					{
						var newStop = closePrice + trailingStopDistance;
						if (entry.StopPrice is not decimal existing || newStop < existing)
							entry.StopPrice = newStop;
					}
				}
			}
		}
		else if (_lastExitWasTakeProfit)
		{
			if (exitSignal)
			{
				CloseAllShort(ExitReasons.Manual);
				return;
			}

			for (var i = 0; i < _shortEntries.Count; i++)
			{
				var entry = _shortEntries[i];
				if (entry.StopPrice is null && closePrice < entry.EntryPrice)
					entry.StopPrice = entry.EntryPrice;
			}
		}

		for (var i = 0; i < _shortEntries.Count; i++)
		{
			var entry = _shortEntries[i];
			if (entry.StopPrice is decimal stopPrice && highPrice >= stopPrice)
			{
				CloseAllShort(ExitReasons.StopLoss);
				return;
			}
		}

		var anyTakeProfit = false;
		for (var i = _shortEntries.Count - 1; i >= 0; i--)
		{
			var entry = _shortEntries[i];
			if (entry.TakeProfitPrice is decimal takePrice && lowPrice <= takePrice)
			{
				BuyMarket();
				_shortEntries.RemoveAt(i);
				anyTakeProfit = true;
			}
		}

		if (anyTakeProfit)
			_lastExitWasTakeProfit = true;
	}

	/// <summary>
	/// Closes all long layers and updates the modok-like flag.
	/// </summary>
	private void CloseAllLong(ExitReasons reason)
	{
		var volume = 0m;
		for (var i = 0; i < _longEntries.Count; i++)
			volume += _longEntries[i].Volume;

		if (volume > 0m && Position > 0m)
			SellMarket();

		_longEntries.Clear();

		if (reason == ExitReasons.TakeProfit)
			_lastExitWasTakeProfit = true;
		else if (reason == ExitReasons.StopLoss)
			_lastExitWasTakeProfit = false;
	}

	/// <summary>
	/// Closes all short layers and updates the modok-like flag.
	/// </summary>
	private void CloseAllShort(ExitReasons reason)
	{
		var volume = 0m;
		for (var i = 0; i < _shortEntries.Count; i++)
			volume += _shortEntries[i].Volume;

		if (volume > 0m && Position < 0m)
			BuyMarket();

		_shortEntries.Clear();

		if (reason == ExitReasons.TakeProfit)
			_lastExitWasTakeProfit = true;
		else if (reason == ExitReasons.StopLoss)
			_lastExitWasTakeProfit = false;
	}

	/// <summary>
	/// Calculates pip value based on the security tick size and decimal digits.
	/// </summary>
	private decimal GetPipSize()
	{
		if (_pipInitialized)
			return _pipSize;

		var security = Security;
		var step = security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 0.0001m;

		var decimals = security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;

		_pipSize = step * adjust;
		if (_pipSize <= 0m)
			_pipSize = step;

		if (_pipSize <= 0m)
			_pipSize = 0.0001m;

		_pipInitialized = true;
		return _pipSize;
	}
}