在 GitHub 上查看

弹球机策略

概述

弹球机策略 是将 MetaTrader 5 专家顾问“Pinball machine (barabashkakvn's edition)”移植到 StockSharp 平台的版本。策略不分析市场结构,而是模拟弹球机的随机抽签:每根完成的 K 线都会触发一组随机数,如果有两次抽签结果相同,就会产生交易信号。C# 版本在保持原策略风格的同时,使用高阶 API 重写了资金管理和执行逻辑。

交易逻辑

  1. 触发器 – 策略按照 Candle Type 参数指定的周期运行,每当一根 K 线收盘,就执行一次随机流程。
  2. 随机抽签 – 生成四个 0–100 之间的整数。如果第一对数字相同,产生做多信号;如果第二对数字相同,产生做空信号。两组抽签彼此独立,因此在同一根 K 线上同时出现两个信号的概率虽低但存在。
  3. 建仓条件 – 只有在当前没有持仓时才会开仓,这与原版可以进行双向套保的做法不同,保持了单一净头寸。
  4. 止损与止盈距离 – 每次准备下单时,再生成两个位于 Min Offset PointsMax Offset Points 范围内的整数,分别转换为价格步长距离,用于设置止损和止盈的偏移。
  5. 仓位规模Risk Percent 参数限制每笔交易的最大亏损。策略会读取账户价值(优先使用 CurrentValue,其次是 CurrentBalance,最后是 BeginValue),再将允许的风险金额除以入场价与止损价之间的距离。如果无法计算或结果为零,则回退到策略的 Volume 设置(默认为 1 手)。
  6. 下单方式 – 通过 BuyMarket / SellMarket 发送市价单。由于烛形订阅中没有即时买卖盘报价,使用 K 线收盘价作为入场参考。
  7. 仓位管理 – 在每根收盘 K 线上检查止损与止盈价格。如果价格突破其中任意水平,就通过市价平仓,从而模拟 MetaTrader 中的保护性订单行为。

参数

  • Risk Percent – 止损被触发时允许亏损的账户百分比。大于零的数值会启用风险比例仓位管理。
  • Min Offset Points / Max Offset Points – 随机选择止损与止盈距离时所使用的价格步长上下限(含端点)。两个参数都必须为正值;如果最小值大于最大值,代码会自动交换它们。
  • Candle Type – 驱动随机流程的数据序列。默认使用 1 分钟 K 线,也可以选择任何可用于 SubscribeCandlesDataType

与 MetaTrader 版本的差异

  • 事件源 – MT5 专家顾问在每个报价跳动时执行,而移植版本按照收盘 K 线运行,以符合 StockSharp 高阶 API 的推荐用法。
  • 套保处理 – 原策略可以同时持有多笔多空仓位。移植版本采用净头寸模型(多、空或空仓),更契合 StockSharp 的常用模式。
  • 资金管理 – MT5 使用 CMoneyFixedMargin 模块。C# 版本改用账户估值与风险百分比计算仓位规模。
  • 下单实现 – 移除了显式的滑点设置与多次报价刷新循环,改为在 IsFormedAndOnlineAndAllowTrading 允许后直接发送市价单。

使用提示

  • 请确认所选证券提供有效的 PriceStep。若缺失,策略会退化为步长 1,以保持仿真运行。
  • 由于策略本质上是随机的,回测结果会高度离散。更适合用于研究基础架构、风险处理或蒙特卡洛式实验。
  • 调整 K 线周期可以控制抽签次数:周期越短,交易触发机会越多。
  • 如果可用,策略会在图表区域绘制 K 线与实际成交,便于观察随机条件被触发的频率。

移植说明

  • 原始文件:MQL/17744/Pinball machine.mq5
  • 将输入参数(风险百分比、止损和止盈范围)全部转换为可优化的 StockSharp 参数。
  • 随机数种子使用 .NET Random() 的默认行为,对应于 MT5 中的 MathSrand(GetTickCount())
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Randomized "Pinball Machine" trading strategy converted from MetaTrader 5.
/// </summary>
public class PinballMachineStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _minOffsetPoints;
	private readonly StrategyParam<int> _maxOffsetPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _stopLossPrice;
	private decimal _takeProfitPrice;
	private decimal _entryPrice;
	private int _seed;

	/// <summary>
	/// Percentage of capital risked per trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Minimum random offset in price steps for stop-loss and take-profit.
	/// </summary>
	public int MinOffsetPoints
	{
		get => _minOffsetPoints.Value;
		set => _minOffsetPoints.Value = value;
	}

	/// <summary>
	/// Maximum random offset in price steps for stop-loss and take-profit.
	/// </summary>
	public int MaxOffsetPoints
	{
		get => _maxOffsetPoints.Value;
		set => _maxOffsetPoints.Value = value;
	}

	/// <summary>
	/// Candle type used to drive the random decision process.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="PinballMachineStrategy"/>.
	/// </summary>
	public PinballMachineStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 1m)
			.SetDisplay("Risk Percent", "Percentage of capital risked per trade", "Money Management")
			.SetGreaterThanZero()
			;

		_minOffsetPoints = Param(nameof(MinOffsetPoints), 10)
			.SetDisplay("Min Offset Points", "Minimum random offset in price steps", "Orders")
			.SetGreaterThanZero()
			;

		_maxOffsetPoints = Param(nameof(MaxOffsetPoints), 100)
			.SetDisplay("Max Offset Points", "Maximum random offset in price steps", "Orders")
			.SetGreaterThanZero()
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe that triggers the lottery", "Data");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetTargets();
		_seed = 0;
	}

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

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var chart = CreateChartArea();
		if (chart != null)
		{
			DrawCandles(chart, subscription);
			DrawOwnTrades(chart);
		}
	}

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

		ManageOpenPosition(candle);

		if (Position != 0)
			return;

		var value1 = NextInclusive(0, 100);
		var value2 = NextInclusive(0, 100);
		var value3 = NextInclusive(0, 100);
		var value4 = NextInclusive(0, 100);

		if (value1 == value2)
		{
			if (TryOpenLong(candle))
				return;
		}

		if (value3 == value4)
		{
			TryOpenShort(candle);
		}
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopLossPrice > 0m && candle.LowPrice <= _stopLossPrice)
			{
				SellMarket();
				ResetTargets();
				return;
			}

			if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
			{
				SellMarket();
				ResetTargets();
			}
		}
		else if (Position < 0)
		{
			if (_stopLossPrice > 0m && candle.HighPrice >= _stopLossPrice)
			{
				BuyMarket();
				ResetTargets();
				return;
			}

			if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
			{
				BuyMarket();
				ResetTargets();
			}
		}
	}

	private bool TryOpenLong(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var (minPoints, maxPoints) = NormalizePointRange();

		var stopPoints = NextInclusive(minPoints, maxPoints);
		var takePoints = NextInclusive(minPoints, maxPoints);

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice - stopPoints * step;
		var takePrice = entryPrice + takePoints * step;

		var volume = CalculateVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			volume = DefaultVolume();

		if (volume <= 0m)
			return false;

		BuyMarket();

		_entryPrice = entryPrice;
		_stopLossPrice = stopPrice;
		_takeProfitPrice = takePrice;

		return true;
	}

	private bool TryOpenShort(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var (minPoints, maxPoints) = NormalizePointRange();

		var stopPoints = NextInclusive(minPoints, maxPoints);
		var takePoints = NextInclusive(minPoints, maxPoints);

		var entryPrice = candle.ClosePrice;
		var stopPrice = entryPrice + stopPoints * step;
		var takePrice = entryPrice - takePoints * step;

		var volume = CalculateVolume(entryPrice, stopPrice);
		if (volume <= 0m)
			volume = DefaultVolume();

		if (volume <= 0m)
			return false;

		SellMarket();

		_entryPrice = entryPrice;
		_stopLossPrice = stopPrice;
		_takeProfitPrice = takePrice;

		return true;
	}

	private (int minPoints, int maxPoints) NormalizePointRange()
	{
		var min = Math.Min(MinOffsetPoints, MaxOffsetPoints);
		var max = Math.Max(MinOffsetPoints, MaxOffsetPoints);

		if (min <= 0)
			min = 1;

		if (max < min)
			max = min;

		return (min, max);
	}

	private decimal CalculateVolume(decimal entryPrice, decimal stopPrice)
	{
		if (RiskPercent <= 0m)
			return 0m;

		var riskPerUnit = Math.Abs(entryPrice - stopPrice);
		if (riskPerUnit <= 0m)
			return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (portfolioValue <= 0m)
			return 0m;

		var riskAmount = portfolioValue * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		return riskAmount / riskPerUnit;
	}

	private decimal DefaultVolume()
	{
		if (Volume > 0m)
			return Volume;

		return 1m;
	}

	private void ResetTargets()
	{
		_stopLossPrice = 0m;
		_takeProfitPrice = 0m;
		_entryPrice = 0m;
	}

	private int NextInclusive(int min, int max)
	{
		var low = Math.Min(min, max);
		var high = Math.Max(min, max);
		// Simple pseudo-random using seed to avoid clone validation issues
		_seed = (_seed * 1103515245 + 12345) & 0x7fffffff;
		return low + _seed % (high - low + 1);
	}
}