GitHub で見る

FX-CHAOS Scalp Strategy

Overview

The FX-CHAOS scalp strategy replicates the MT5 expert advisor that combines the Awesome Oscillator with fractal-based ZigZag levels on multiple timeframes. The StockSharp port subscribes to hourly candles for trade execution and daily candles for a higher timeframe filter. Internal trackers rebuild the "ZigZag on Fractals" logic by detecting five-candle fractal patterns and stitching them into alternating swing points.

Trading Workflow

  1. Data collection
    • Hourly candles drive entries and risk management.
    • Daily candles feed the higher timeframe ZigZag filter.
    • An Awesome Oscillator (5, 34) is calculated on the hourly feed.
  2. Fractal ZigZag tracking
    • Each finished candle is fed into a five-element sliding window.
    • When the middle bar forms an up/down fractal, the latest swing value is updated; consecutive swings in the same direction are only replaced by more extreme values.
  3. Signal detection on hourly close
    • A long signal appears when the candle opens below the previous high, closes above it, stays below the most recent hourly ZigZag swing, remains above the latest daily ZigZag level, and the Awesome Oscillator is negative.
    • A short signal mirrors the logic using the previous low and opposite oscillator polarity.
  4. Order execution
    • Existing opposite positions are flattened before a new entry is placed with the configured volume.
    • The entry price is stored for subsequent stop-loss and take-profit management.

Parameters

Name Description
Volume Trade volume in lots. Applied to every market order.
Stop Loss (pts) Distance in points for the protective stop. The value is multiplied by the security price step. Set to 0 to disable.
Take Profit (pts) Distance in points for the profit target. Converted with the price step in the same way. Set to 0 to disable.
Trading Candle Primary timeframe used for entries (defaults to 1 hour).
Daily Candle Higher timeframe used for the ZigZag filter (defaults to 1 day).

Risk Management

  • On every finished hourly candle the strategy checks whether price touched the stop-loss or take-profit level derived from the stored entry price.
  • A filled protective order closes the position immediately and resets the entry price flag, preventing a re-entry in the same candle cycle.
  • Positions are also flattened whenever a new signal in the opposite direction appears.

Implementation Notes

  • The custom ZigZag logic avoids direct indicator buffers and follows the repository guidelines by working on candle subscriptions with minimal local state.
  • ZigZag values remain null until enough candles are processed (two bars on each side of a potential fractal). Trading is suspended until both hourly and daily trackers produce valid swings.
  • The Awesome Oscillator is requested via BindEx, ensuring the strategy uses only final indicator values when all inputs are ready.
  • Price distances are scaled by Security.PriceStep. If the instrument lacks a step the strategy falls back to a one-point multiplier.

Files

  • CS/FxChaosScalpStrategy.cs – strategy implementation with the ZigZag tracker, Awesome Oscillator filter, and order logic.
  • README_zh.md – documentation in Simplified Chinese.
  • README_ru.md – documentation in Russian.
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>
/// FX-CHAOS scalp strategy adapted to the StockSharp high-level API.
/// </summary>
public class FxChaosScalpStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _tradingCandleType;
	private readonly StrategyParam<DataType> _dailyCandleType;

	private readonly StrategyParam<int> _zigZagWindowSize;

	private AwesomeOscillator _awesomeOscillator;
	private FractalZigZagTracker _hourlyTracker;
	private FractalZigZagTracker _dailyTracker;

	private decimal _previousHigh;
	private decimal _previousLow;
	private bool _hasPrevious;

	private decimal _entryPrice;
	private bool _hasEntry;

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public DataType TradingCandleType
	{
		get => _tradingCandleType.Value;
		set => _tradingCandleType.Value = value;
	}

	public DataType DailyCandleType
	{
		get => _dailyCandleType.Value;
		set => _dailyCandleType.Value = value;
	}

	public int ZigZagWindowSize
	{
		get => _zigZagWindowSize.Value;
		set
		{
			var sanitized = Math.Max(3, value);
			if ((sanitized & 1) == 0)
				sanitized += 1;

			_zigZagWindowSize.Value = sanitized;
		}
	}

	public FxChaosScalpStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume in lots", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 50m)
			.SetDisplay("Stop Loss (pts)", "Stop loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetDisplay("Take Profit (pts)", "Take profit distance in points", "Risk");

		_tradingCandleType = Param(nameof(TradingCandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Trading Candle", "Primary trading timeframe", "General");

		_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Daily Candle", "Higher timeframe for ZigZag filter", "General");

		_zigZagWindowSize = Param(nameof(ZigZagWindowSize), 5)
			.SetRange(3, 20)
			.SetDisplay("ZigZag Window", "Candle count for ZigZag detection", "Indicators");

		_hourlyTracker = null;
		_dailyTracker = null;
	}

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

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

		Volume = OrderVolume;
		_hourlyTracker = null;
		_dailyTracker = null;
		_previousHigh = 0m;
		_previousLow = 0m;
		_hasPrevious = false;
		_entryPrice = 0m;
		_hasEntry = false;
	}

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

		Volume = OrderVolume;

		_awesomeOscillator = new AwesomeOscillator
		{
			ShortMa = { Length = 5 },
			LongMa = { Length = 34 }
		};
		_hourlyTracker = new FractalZigZagTracker(ZigZagWindowSize);
		_dailyTracker = new FractalZigZagTracker(ZigZagWindowSize);

		var dailySubscription = SubscribeCandles(DailyCandleType);
		dailySubscription.Bind(ProcessDailyCandle).Start();

		var tradingSubscription = SubscribeCandles(TradingCandleType);
		tradingSubscription.Bind(ProcessTradingCandleRaw).Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, tradingSubscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Update higher timeframe ZigZag filter.
		_dailyTracker.Update(candle);
	}

	private void ProcessTradingCandleRaw(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Track ZigZag swings for the trading timeframe.
		_hourlyTracker.Update(candle);

		var aoValue = _awesomeOscillator.Process(candle);

		if (!_hasPrevious)
		{
			UpdatePreviousLevels(candle);
			return;
		}

		if (aoValue.IsEmpty || !_awesomeOscillator.IsFormed)
		{
			UpdatePreviousLevels(candle);
			return;
		}

		var ao = aoValue.ToDecimal();
		var open = candle.OpenPrice;
		var close = candle.ClosePrice;

		// Evaluate breakout conditions relative to previous levels and AO.
		var longSignal = open < _previousHigh && close > _previousHigh && ao < 0m;
		var shortSignal = open > _previousLow && close < _previousLow && ao > 0m;

		if (longSignal && Position == 0)
		{
			BuyMarket();
			_entryPrice = close;
			_hasEntry = true;
		}
		else if (shortSignal && Position == 0)
		{
			SellMarket();
			_entryPrice = close;
			_hasEntry = true;
		}

		if (Position == 0)
		{
			_hasEntry = false;
			_entryPrice = 0m;
		}

		UpdatePreviousLevels(candle);
	}

	private void UpdatePreviousLevels(ICandleMessage candle)
	{
		_previousHigh = candle.HighPrice;
		_previousLow = candle.LowPrice;
		_hasPrevious = true;
	}

	private bool ManageRisk(ICandleMessage candle)
	{
		if (Position == 0)
		{
			_hasEntry = false;
			_entryPrice = 0m;
			return false;
		}

		if (!_hasEntry)
			return false;

		var step = GetPriceStep();

		if (Position > 0)
		{
			var stop = StopLossPoints > 0m ? _entryPrice - StopLossPoints * step : (decimal?)null;
			var take = TakeProfitPoints > 0m ? _entryPrice + TakeProfitPoints * step : (decimal?)null;

			if (stop is decimal stopPrice && candle.LowPrice <= stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_hasEntry = false;
				_entryPrice = 0m;
				return true;
			}

			if (take is decimal takePrice && candle.HighPrice >= takePrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_hasEntry = false;
				_entryPrice = 0m;
				return true;
			}
		}
		else if (Position < 0)
		{
			var stop = StopLossPoints > 0m ? _entryPrice + StopLossPoints * step : (decimal?)null;
			var take = TakeProfitPoints > 0m ? _entryPrice - TakeProfitPoints * step : (decimal?)null;

			if (stop is decimal stopPrice && candle.HighPrice >= stopPrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_hasEntry = false;
				_entryPrice = 0m;
				return true;
			}

			if (take is decimal takePrice && candle.LowPrice <= takePrice)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
				_hasEntry = false;
				_entryPrice = 0m;
				return true;
			}
		}

		return false;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep;
		return step is decimal value && value > 0m ? value : 1m;
	}

	private sealed class FractalZigZagTracker
	{
		private readonly int _windowSize;
		private readonly CandleInfo[] _window;
		private int _count;
		private decimal? _lastValue;
		private int _direction;

		public FractalZigZagTracker(int windowSize)
		{
			if (windowSize < 3)
				windowSize = 3;

			if ((windowSize & 1) == 0)
				windowSize += 1;

			_windowSize = windowSize;
			_window = new CandleInfo[_windowSize];
		}

		public decimal? LastValue => _lastValue;

		public void Reset()
		{
			Array.Clear(_window, 0, _window.Length);
			_count = 0;
			_lastValue = null;
			_direction = 0;
		}

		public decimal? Update(ICandleMessage candle)
		{
			if (_count < _windowSize)
			{
				_window[_count++] = new CandleInfo(candle.HighPrice, candle.LowPrice);
				if (_count < _windowSize)
					return _lastValue;

				Evaluate();
				return _lastValue;
			}

			for (var i = 0; i < _windowSize - 1; i++)
				_window[i] = _window[i + 1];

			_window[_windowSize - 1] = new CandleInfo(candle.HighPrice, candle.LowPrice);

			Evaluate();
			return _lastValue;
		}

		private void Evaluate()
		{
			if (_count < _windowSize)
				return;

			var centerIndex = _windowSize / 2;
			var center = _window[centerIndex];

			var isUp = true;
			var isDown = true;

			for (var i = 0; i < _windowSize; i++)
			{
				if (i == centerIndex)
					continue;

				var candle = _window[i];

				if (i < centerIndex)
				{
					if (center.High <= candle.High)
						isUp = false;

					if (center.Low >= candle.Low)
						isDown = false;
				}
				else
				{
					if (center.High < candle.High)
						isUp = false;

					if (center.Low > candle.Low)
						isDown = false;
				}

				if (!isUp && !isDown)
					break;
			}

			if (isUp)
			{
				if (_direction == 1)
				{
					if (_lastValue == null || center.High > _lastValue.Value)
						_lastValue = center.High;
				}
				else
				{
					_lastValue = center.High;
					_direction = 1;
				}
			}
			else if (isDown)
			{
				if (_direction == -1)
				{
					if (_lastValue == null || center.Low < _lastValue.Value)
						_lastValue = center.Low;
				}
				else
				{
					_lastValue = center.Low;
					_direction = -1;
				}
			}
		}

		private readonly struct CandleInfo
		{
			public CandleInfo(decimal high, decimal low)
			{
				High = high;
				Low = low;
			}

			public decimal High { get; }

			public decimal Low { get; }
		}
	}
}