在 GitHub 上查看

IFS Fractals

概述

IFS Fractals 是 MetaTrader 5 脚本 IFS_Fractals 的移植版本。原始脚本通过对点云连续应用 28 组仿射变换来绘制“分形单词”的位图。StockSharp 版本沿用了相同的迭代函数系统 (IFS),但将其转换为交易信号:生成点的 X 坐标按原始比例缩放、再用指数移动平均线 (EMA) 平滑,并作为动量指标来驱动多空交易。

策略逻辑

迭代函数系统

  • 仿射变换 – 每根完成的 K 线都会触发一批迭代(可配置数量)。在每次迭代中,依据原脚本提供的概率权重(全部为 35)随机选择 28 个变换中的一个,并使用与 MQL5 代码一致的系数更新当前点 (x, y)
  • 概率表 – 启动时预先计算概率累积表,可通过一次随机抽样在总概率质量内快速定位下一个变换。

信号构建

  • 归一化 – X 坐标除以原脚本绘制位图时使用的缩放系数(默认 50),从而保持信号幅度与标的价格无关。
  • EMA 平滑 – 归一化后的序列送入可配置周期的 EMA。该指标相当于低通滤波器,用于提取混沌迭代的主导趋势。
  • 入场规则 – 当 EMA 上穿正向入场阈值时,策略开多或反手做多;当 EMA 下穿负向阈值时,则开空或反手做空。
  • 出场规则 – 多单在 EMA 回落到出场阈值或更低时平仓;空单在 EMA 回升到负向出场阈值以上时平仓。这样形成的滞回区可以减少信号在零轴附近的频繁翻转。

风险控制

  • 仓位保护 – 通过 StartProtection 可设置绝对止盈/止损距离,参数为 0 表示关闭,对应原脚本默认不使用保护单的行为。
  • 仓位规模 – 开仓始终使用固定的市场成交量;在建立新仓前会先平掉相反方向的持仓,确保策略始终只有一个方向的敞口。

参数

  • Volume – 新开仓时使用的市场成交量。
  • Candle Type – 触发 IFS 迭代的 K 线类型(默认 5 分钟)。
  • Iterations – 每根完成 K 线后执行的迭代次数。
  • Scale – 在送入 EMA 之前对 X 坐标应用的缩放因子。
  • Entry Threshold – 开仓所需的 EMA 绝对值(正值对应多单,负值镜像应用于空单)。
  • Exit Threshold – EMA 回到该阈值时触发平仓。
  • EMA Period – 应用于分形信号的指数移动平均周期。
  • Take Profit – 绝对止盈距离;填 0 关闭。
  • Stop Loss – 绝对止损距离;填 0 关闭。

其他说明

  • 若不在代码中设定固定随机种子,每次运行都会得到不同的交易序列,这与原脚本绘制位图时的随机性一致。
  • 策略无需依赖市场指标,所有信号完全由 IFS 系数生成,订阅的 K 线只负责提供节奏。
  • 本包仅包含 C# 实现(位于 CS/ 目录),暂未提供 Python 版本。
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// IFS Fractals strategy: Williams %R oscillator crossover.
/// Buys when WPR crosses above oversold, sells when crosses below overbought.
/// </summary>
public class IfsFractalsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _wprPeriod;
	private readonly StrategyParam<decimal> _oversold;
	private readonly StrategyParam<decimal> _overbought;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private decimal _prevWpr;
	private int _candlesSinceTrade;
	private bool _hasPrev;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int WprPeriod { get => _wprPeriod.Value; set => _wprPeriod.Value = value; }
	public decimal Oversold { get => _oversold.Value; set => _oversold.Value = value; }
	public decimal Overbought { get => _overbought.Value; set => _overbought.Value = value; }
	public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }

	public IfsFractalsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_wprPeriod = Param(nameof(WprPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("WPR Period", "Williams %R period", "Indicators");
		_oversold = Param(nameof(Oversold), -85m)
			.SetDisplay("Oversold", "WPR oversold level", "Signals");
		_overbought = Param(nameof(Overbought), -15m)
			.SetDisplay("Overbought", "WPR overbought level", "Signals");
		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevWpr = 0;
		_candlesSinceTrade = SignalCooldownCandles;
		_hasPrev = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevWpr = 0;
		_candlesSinceTrade = SignalCooldownCandles;
		_hasPrev = false;
		var wpr = new WilliamsR { Length = WprPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(wpr, ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal wprValue)
	{
		if (candle.State != CandleStates.Finished) return;

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		if (_hasPrev)
		{
			if (_prevWpr < Oversold && wprValue >= Oversold && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				BuyMarket();
				_candlesSinceTrade = 0;
			}
			else if (_prevWpr > Overbought && wprValue <= Overbought && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				SellMarket();
				_candlesSinceTrade = 0;
			}
		}

		_prevWpr = wprValue;
		_hasPrev = true;
	}
}