在 GitHub 上查看

数独界面策略

概述

该策略是 MetaTrader 5 脚本 SudokuUI.mq5 的 StockSharp 移植版。原始 MQL 程序提供了一个图形化的数独谜题界面,可控制谜题生成、洗牌以及自动更新。由于 StockSharp 聚焦于自动化交易而非交互式图表小部件,本移植版把这些概念重构为基于数独统计的网格型均值回归策略。

系统把数独棋盘视为 9x9 的数字矩阵。各列的平均值会转换成围绕简单移动平均线(SMA)的对称偏离阈值。当价格相对于 SMA 的偏离超过这些阈值时,策略会建立反向仓位,寻求价格回归均值。当价格回到中性区间时仓位被平掉,对应原工具中重置棋盘的能力。

交易逻辑

  1. 准备数独

    • 策略可以从文件或原始字符串加载 81 位的数独定义。非数字字符会被忽略,数字 0 也会被跳过,与数独的规则一致。
    • 如果没有提供有效棋盘,会根据洗牌种子生成伪随机的数字矩阵。逻辑同时支持“洗牌种子”和“组合种子”,以便交易者复现指定的布局。
    • 允许在计算统计之前排除某个数字,模拟原 GUI 中隐藏标签的功能,同时可以缩小有效网格。
  2. 构建阈值

    • 对每一列在排除指定数字后计算平均值,再归一化到 [-1, 1] 区间,并乘以 ThresholdRange,从而得到相对于 SMA 的百分比偏离阈值。
    • 如果所有列只产生单侧的阈值,会补充缺失的正/负阈值,保证多空信号都存在。
  3. 生成信号

    • 策略订阅指定类型的 K 线,并与 SMA 指标绑定。只处理状态为 Finished 的 K 线,以符合 StockSharp 的最佳实践。
    • 当收盘价与 SMA 的百分比差距跌破最负阈值时,在清空空头后建立多头。相反,当差距突破最正阈值时,在清空多头后建立空头。
    • NeutralBand 定义的中性区间用于平仓。这一机制对应原脚本的自动助手,负责在条件满足时重置布局。
  4. 自动更新

    • EnableAutoUpdate 设为 true 时,每个交易日开始都会重新生成数独网格。洗牌种子、排除的数字以及洗牌次数都会影响新的阈值,使其既可预测又具有变化性。

参数

参数 说明
PuzzleDefinition 用于计算阈值的数独定义,可为文件路径或 81 位数字字符串。
ShufflingRandomSeed 主要洗牌种子。-1 表示根据当日日期自动计算。
CompositionRandomSeed 额外的种子,用于扰动洗牌流程并生成不同布局。
ShufflingCycles 对数字池进行额外洗牌的次数。数值越大,布局越随机。
EliminateLabel 在计算前剔除的数字(1-9)。0 表示保留全部数字。
EnableAutoUpdate 交易日变化时重新构建阈值。
SmaPeriod 作为回归锚点的 SMA 长度。
ThresholdRange 数独生成的最大绝对偏离,按价格比例表示。
NeutralBand 当价格回到该区间时会平仓的偏离范围。
Volume 市价委托的成交量。
CandleType 用于驱动指标的 K 线订阅类型。

使用建议

  • 策略只处理收盘完成的 K 线,并忽略价格为零的样本,确保在不同数据源上都稳定运行。
  • 若想完全复现原脚本的棋盘,可提供不含 0 的 81 位数字串,或包含这些数字的文本文件。
  • 如需固定网格,请关闭 EnableAutoUpdate 并设置明确的种子。打开该选项则模拟 MQL 中的“自动助手”,在每日开始时刷新布局。
  • 阈值源自列统计。如果棋盘表现出强烈的单侧偏差,可以剔除占主导地位的数字,以保持买卖信号的平衡。

与原脚本的差异

  • 所有用户界面元素(对话框、按钮、图表事件)已移除,对应功能通过策略参数提供。
  • 棋盘不再用于手动解谜,而是决定算法交易的阈值。同样的随机控制项决定策略的激进或保守程度。
  • StockSharp 版本完全自动运行。自动更新基于交易日切换而非按钮点击,仓位管理通过标准的 BuyMarketSellMarketClosePosition 方法完成。
using System;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Sudoku UI strategy: SMA mean reversion.
/// Buys when close < SMA - offset. Sells when close > SMA + offset.
/// </summary>
public class SudokuUiStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaPeriod;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int SmaPeriod
	{
		get => _smaPeriod.Value;
		set => _smaPeriod.Value = value;
	}

	public SudokuUiStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");

		_smaPeriod = Param(nameof(SmaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("SMA Period", "SMA period", "Indicators");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var sma = new SimpleMovingAverage { Length = SmaPeriod };

		decimal? prevClose = null;
		decimal? prevSma = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(sma, (candle, smaVal) =>
			{
				if (candle.State != CandleStates.Finished)
					return;

				if (!IsFormedAndOnlineAndAllowTrading())
					return;

				var close = candle.ClosePrice;

				if (prevClose.HasValue && prevSma.HasValue)
				{
					var crossBelow = prevClose.Value >= prevSma.Value && close < smaVal;
					var crossAbove = prevClose.Value <= prevSma.Value && close > smaVal;

					if (crossBelow && Position <= 0)
						BuyMarket();
					else if (crossAbove && Position >= 0)
						SellMarket();
				}

				prevClose = close;
				prevSma = smaVal;
			})
			.Start();

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