在 GitHub 上查看

FX-CHAOS 剥头皮策略

概述

FX-CHAOS 剥头皮策略复制了 MT5 专家顾问的逻辑,将 Awesome Oscillator 与基于分形的 ZigZag 水平相结合。移植到 StockSharp 后,策略订阅小时级别的 K 线用于交易执行,同时订阅日线用于高周期过滤。内置的跟踪器通过检测五根 K 线的分形形态来重建 “ZigZag on Fractals” 指标,并把识别出的波峰和波谷连接成交替的摆动点。

流程

  1. 数据采集
    • 小时 K 线驱动入场判定和风控。
    • 日线提供更高周期的 ZigZag 水平过滤。
    • 在小时数据上计算 Awesome Oscillator(5,34)。
  2. 分形 ZigZag 跟踪
    • 每根收盘的 K 线都会推入一个包含 5 根 K 线的滑动窗口。
    • 当窗口中部的 K 线满足上/下分形条件时,更新当前的 ZigZag 摆动点;同方向出现更极端的分形时,会替换之前的极值。
  3. 信号判定(按小时收盘)
    • 做多信号要求:当前 K 线开盘价低于上一根高点、收盘价突破该高点、收盘价位于最近的小时 ZigZag 摆动之下、站在最新的日线 ZigZag 之上,同时 Awesome Oscillator 为负值。
    • 做空信号为镜像条件:使用上一根低点,且 Awesome Oscillator 为正值。
  4. 下单与持仓管理
    • 若存在反向持仓,先平仓再根据信号开新仓,使用配置的交易量。
    • 记录入场价格,以便计算止损与止盈。

参数

名称 说明
Volume 每次下单的交易量(手数)。
Stop Loss (pts) 止损距离(点)。将与标的的最小报价单位相乘;填 0 表示禁用。
Take Profit (pts) 止盈距离(点)。计算方式与止损相同;填 0 表示禁用。
Trading Candle 执行信号的主要周期(默认 1 小时)。
Daily Candle 用于 ZigZag 过滤的高周期(默认 1 天)。

风险控制

  • 每根小时 K 线收盘时都会检查价格是否触及根据入场价计算出的止损或止盈水平。
  • 一旦触发保护水平,会立即平掉当前仓位并清除入场标记,避免同一根 K 线内再次进场。
  • 出现反向信号时,会先平掉当前仓位,再根据新信号重新建仓。

实现细节

  • ZigZag 跟踪器仅使用最少的本地状态,并基于蜡烛订阅执行,不直接访问指标缓冲区。
  • 在 ZigZag 形成之前(至少需要前后各两根 K 线)策略不会交易。
  • Awesome Oscillator 通过 BindEx 绑定,策略只在指标输出为最终值时才评估信号。
  • 止损/止盈距离会乘以 Security.PriceStep。当标的没有定义步长时,会退回到 1 点的倍数。

文件列表

  • CS/FxChaosScalpStrategy.cs —— 策略实现,包含 ZigZag 跟踪、Awesome Oscillator 过滤与下单逻辑。
  • README.md —— 英文文档。
  • README_ru.md —— 俄文文档。
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; }
		}
	}
}