弹球机策略
概述
弹球机策略 是将 MetaTrader 5 专家顾问“Pinball machine (barabashkakvn's edition)”移植到 StockSharp 平台的版本。策略不分析市场结构,而是模拟弹球机的随机抽签:每根完成的 K 线都会触发一组随机数,如果有两次抽签结果相同,就会产生交易信号。C# 版本在保持原策略风格的同时,使用高阶 API 重写了资金管理和执行逻辑。
交易逻辑
- 触发器 – 策略按照
Candle Type参数指定的周期运行,每当一根 K 线收盘,就执行一次随机流程。 - 随机抽签 – 生成四个 0–100 之间的整数。如果第一对数字相同,产生做多信号;如果第二对数字相同,产生做空信号。两组抽签彼此独立,因此在同一根 K 线上同时出现两个信号的概率虽低但存在。
- 建仓条件 – 只有在当前没有持仓时才会开仓,这与原版可以进行双向套保的做法不同,保持了单一净头寸。
- 止损与止盈距离 – 每次准备下单时,再生成两个位于
Min Offset Points与Max Offset Points范围内的整数,分别转换为价格步长距离,用于设置止损和止盈的偏移。 - 仓位规模 –
Risk Percent参数限制每笔交易的最大亏损。策略会读取账户价值(优先使用CurrentValue,其次是CurrentBalance,最后是BeginValue),再将允许的风险金额除以入场价与止损价之间的距离。如果无法计算或结果为零,则回退到策略的Volume设置(默认为 1 手)。 - 下单方式 – 通过
BuyMarket/SellMarket发送市价单。由于烛形订阅中没有即时买卖盘报价,使用 K 线收盘价作为入场参考。 - 仓位管理 – 在每根收盘 K 线上检查止损与止盈价格。如果价格突破其中任意水平,就通过市价平仓,从而模拟 MetaTrader 中的保护性订单行为。
参数
- Risk Percent – 止损被触发时允许亏损的账户百分比。大于零的数值会启用风险比例仓位管理。
- Min Offset Points / Max Offset Points – 随机选择止损与止盈距离时所使用的价格步长上下限(含端点)。两个参数都必须为正值;如果最小值大于最大值,代码会自动交换它们。
- Candle Type – 驱动随机流程的数据序列。默认使用 1 分钟 K 线,也可以选择任何可用于
SubscribeCandles的DataType。
与 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);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class pinball_machine_strategy(Strategy):
def __init__(self):
super(pinball_machine_strategy, self).__init__()
self._risk_percent = self.Param("RiskPercent", 1.0)
self._min_offset_points = self.Param("MinOffsetPoints", 10)
self._max_offset_points = self.Param("MaxOffsetPoints", 100)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._entry_price = 0.0
self._seed = 0
@property
def RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.Value = value
@property
def MinOffsetPoints(self):
return self._min_offset_points.Value
@MinOffsetPoints.setter
def MinOffsetPoints(self, value):
self._min_offset_points.Value = value
@property
def MaxOffsetPoints(self):
return self._max_offset_points.Value
@MaxOffsetPoints.setter
def MaxOffsetPoints(self, value):
self._max_offset_points.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _next_inclusive(self, min_val, max_val):
low = min(min_val, max_val)
high = max(min_val, max_val)
self._seed = (self._seed * 1103515245 + 12345) & 0x7fffffff
return low + self._seed % (high - low + 1)
def _normalize_point_range(self):
min_p = min(int(self.MinOffsetPoints), int(self.MaxOffsetPoints))
max_p = max(int(self.MinOffsetPoints), int(self.MaxOffsetPoints))
if min_p <= 0:
min_p = 1
if max_p < min_p:
max_p = min_p
return (min_p, max_p)
def OnStarted2(self, time):
super(pinball_machine_strategy, self).OnStarted2(time)
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._entry_price = 0.0
self._seed = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_open_position(candle)
if self.Position != 0:
return
v1 = self._next_inclusive(0, 100)
v2 = self._next_inclusive(0, 100)
v3 = self._next_inclusive(0, 100)
v4 = self._next_inclusive(0, 100)
if v1 == v2:
if self._try_open_long(candle):
return
if v3 == v4:
self._try_open_short(candle)
def _manage_open_position(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
if self._stop_loss_price > 0.0 and low <= self._stop_loss_price:
self.SellMarket()
self._reset_targets()
return
if self._take_profit_price > 0.0 and high >= self._take_profit_price:
self.SellMarket()
self._reset_targets()
elif self.Position < 0:
if self._stop_loss_price > 0.0 and high >= self._stop_loss_price:
self.BuyMarket()
self._reset_targets()
return
if self._take_profit_price > 0.0 and low <= self._take_profit_price:
self.BuyMarket()
self._reset_targets()
def _try_open_long(self, candle):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
min_p, max_p = self._normalize_point_range()
stop_points = self._next_inclusive(min_p, max_p)
take_points = self._next_inclusive(min_p, max_p)
entry = float(candle.ClosePrice)
stop = entry - stop_points * step
take = entry + take_points * step
self.BuyMarket()
self._entry_price = entry
self._stop_loss_price = stop
self._take_profit_price = take
return True
def _try_open_short(self, candle):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
min_p, max_p = self._normalize_point_range()
stop_points = self._next_inclusive(min_p, max_p)
take_points = self._next_inclusive(min_p, max_p)
entry = float(candle.ClosePrice)
stop = entry + stop_points * step
take = entry - take_points * step
self.SellMarket()
self._entry_price = entry
self._stop_loss_price = stop
self._take_profit_price = take
return True
def _reset_targets(self):
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._entry_price = 0.0
def OnReseted(self):
super(pinball_machine_strategy, self).OnReseted()
self._reset_targets()
self._seed = 0
def CreateClone(self):
return pinball_machine_strategy()