Pinball Machine 策略
概览
本策略是 MetaTrader 4 专家顾问 Pinball_machine.mq4 的 StockSharp 翻译版本。原始 EA 会在每个到来的报价上生成随机整数,只要其中一对数值相同就立刻开仓。移植后的 StockSharp 版本保留了这种类似“弹球机”的随机玩法:在所选时间框架的每根已完成K线上执行两组随机抽签,只要对应的一对数字相等,就分别开出做多或做空的市价单。止损和止盈距离同样在每次评估时重新随机化,从而还原原程序那种不可预测的弹跳效果。
交易逻辑
- 按
CandleType参数订阅K线,并等待每根K线收盘。 - 对于每根完成的K线,在区间
[0, RandomMaxValue]内生成四个均匀分布的整数。第一对用于潜在的多单,第二对用于潜在的空单。 - 再额外生成两组整数,分别位于
MinStopLossPoints/MaxStopLossPoints和MinTakeProfitPoints/MaxTakeProfitPoints之间,用于决定保护性止损与止盈的距离(以价格步长为单位),该距离在多空方向上共用。 - 如果第一、第二个随机数相同,就按
TradeVolume的手数提交一张市价买单;如果第三、第四个随机数相同,就提交同样手数的市价卖单。两个条件可以在同一根K线上同时触发,与原始 MQL 程序中买卖互不干扰的行为完全一致。 - 若本次抽取的保护距离大于零,立即调用
SetStopLoss与SetTakeProfit挂出止损和止盈。距离会按交易品种的PriceStep进行换算,对应 MetaTrader 中以Point为单位的写法。
订单与风控
- 启动时调用
StartProtection(),让 StockSharp 自动管理附加的保护性订单。 - 每次进场都会先计算下单后的净持仓量(
Position ± TradeVolume),再将该值传给SetStopLoss与SetTakeProfit,从而在同向多笔交易并存时依旧能合并管理止损与止盈。 - 如果止损或止盈的最小/最大距离设为 0 或负值,本次评估会跳过对应的保护单。
参数说明
| 参数 | 说明 |
|---|---|
TradeVolume |
每次随机进场的下单数量(手数或合约数)。 |
CandleType |
触发随机抽签的K线时间框架;时间越短越贴近原始的逐笔行情执行方式。 |
RandomMaxValue |
随机整数的上限(含)。数值越大,命中相同数字的概率越低,进场频率越少。 |
MinStopLossPoints |
随机生成的止损距离下限(按价格步长计)。 |
MaxStopLossPoints |
随机生成的止损距离上限。 |
MinTakeProfitPoints |
随机生成的止盈距离下限。 |
MaxTakeProfitPoints |
随机生成的止盈距离上限。 |
RandomSeed |
伪随机数发生器的种子。为 0 时根据当前时间播种,其它数值则产生可复现的序列。 |
实现要点
- 原 EA 以逐笔报价驱动;StockSharp 版本改为在K线收盘时运行,因为高阶 API 以时间序列事件为核心。可将
CandleType设置为极短周期(如 1 秒或交易所报价K线)以还原原版的快速节奏。 - 止损和止盈距离在每次评估时只生成一次,并同时用于多单和空单,完全遵循原始脚本的写法。
- 请确保交易品种已设置正确的
PriceStep,否则以点数表示的保护距离需要手动调整。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Lottery-style strategy that makes two independent random entry decisions per candle.
/// Each entry receives random stop-loss and take-profit distances measured in price steps.
/// </summary>
public class PinballMachineRandomDrawStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _randomMaxValue;
private readonly StrategyParam<int> _minStopLossPoints;
private readonly StrategyParam<int> _maxStopLossPoints;
private readonly StrategyParam<int> _minTakeProfitPoints;
private readonly StrategyParam<int> _maxTakeProfitPoints;
private readonly StrategyParam<int> _randomSeed;
private Random _random;
private decimal _protectedPosition;
private ProtectedPosition _protection;
public PinballMachineRandomDrawStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Volume submitted for every matching pair.", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candles that trigger random draws.", "General");
_randomMaxValue = Param(nameof(RandomMaxValue), 99)
.SetRange(0, 1_000_000)
.SetDisplay("Random Maximum", "Inclusive upper bound for entry draws.", "Random");
_minStopLossPoints = Param(nameof(MinStopLossPoints), 10)
.SetDisplay("Minimum Stop Loss", "Minimum random stop-loss distance in price steps.", "Protection");
_maxStopLossPoints = Param(nameof(MaxStopLossPoints), 50)
.SetDisplay("Maximum Stop Loss", "Maximum random stop-loss distance in price steps.", "Protection");
_minTakeProfitPoints = Param(nameof(MinTakeProfitPoints), 10)
.SetDisplay("Minimum Take Profit", "Minimum random take-profit distance in price steps.", "Protection");
_maxTakeProfitPoints = Param(nameof(MaxTakeProfitPoints), 100)
.SetDisplay("Maximum Take Profit", "Maximum random take-profit distance in price steps.", "Protection");
_randomSeed = Param(nameof(RandomSeed), 0)
.SetDisplay("Random Seed", "Zero uses a time-based sequence; any other value is reproducible.", "Random");
}
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int RandomMaxValue
{
get => _randomMaxValue.Value;
set => _randomMaxValue.Value = value;
}
public int MinStopLossPoints
{
get => _minStopLossPoints.Value;
set => _minStopLossPoints.Value = value;
}
public int MaxStopLossPoints
{
get => _maxStopLossPoints.Value;
set => _maxStopLossPoints.Value = value;
}
public int MinTakeProfitPoints
{
get => _minTakeProfitPoints.Value;
set => _minTakeProfitPoints.Value = value;
}
public int MaxTakeProfitPoints
{
get => _maxTakeProfitPoints.Value;
set => _maxTakeProfitPoints.Value = value;
}
public int RandomSeed
{
get => _randomSeed.Value;
set => _randomSeed.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_random = null;
_protectedPosition = 0m;
_protection = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_random = RandomSeed == 0 ? new Random() : new Random(RandomSeed);
StartProtectionBook();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ProcessProtection(candle);
var draw = DrawEvaluation(
_random,
RandomMaxValue,
MinStopLossPoints,
MaxStopLossPoints,
MinTakeProfitPoints,
MaxTakeProfitPoints);
if (draw.EnterLong)
SubmitEntry(Sides.Buy, candle, draw.StopLossPoints, draw.TakeProfitPoints);
if (draw.EnterShort)
SubmitEntry(Sides.Sell, candle, draw.StopLossPoints, draw.TakeProfitPoints);
}
private void SubmitEntry(Sides side, ICandleMessage candle, int stopLossPoints, int takeProfitPoints)
{
if (TradeVolume <= 0m)
return;
SubmitMarket(side, TradeVolume, "Pinball entry");
_protectedPosition += side == Sides.Buy ? TradeVolume : -TradeVolume;
if (_protectedPosition == 0m)
{
_protection = null;
return;
}
var priceStep = Security?.PriceStep;
if (priceStep is not decimal step || step <= 0m || (stopLossPoints <= 0 && takeProfitPoints <= 0))
{
_protection = null;
return;
}
var protection = new ProtectedPosition(
_protectedPosition > 0m ? Sides.Buy : Sides.Sell,
Math.Abs(_protectedPosition),
candle.CloseTime);
SetStopLoss(protection, candle.ClosePrice, stopLossPoints, step);
SetTakeProfit(protection, candle.ClosePrice, takeProfitPoints, step);
_protection = protection;
}
private void ProcessProtection(ICandleMessage candle)
{
var protection = _protection;
if (protection is null || candle.CloseTime <= protection.ActiveAfter)
return;
var stopHit = protection.StopLossPrice is decimal stop &&
(protection.Side == Sides.Buy ? candle.LowPrice <= stop : candle.HighPrice >= stop);
var takeHit = !stopHit && protection.TakeProfitPrice is decimal take &&
(protection.Side == Sides.Buy ? candle.HighPrice >= take : candle.LowPrice <= take);
if (!stopHit && !takeHit)
return;
var exitSide = protection.Side == Sides.Buy ? Sides.Sell : Sides.Buy;
SubmitMarket(exitSide, protection.Volume, "Pinball protection exit");
_protectedPosition = 0m;
_protection = null;
}
private void SubmitMarket(Sides side, decimal volume, string comment)
{
var order = CreateOrder(side, 0m, volume);
order.Comment = comment;
RegisterOrder(order);
}
private void StartProtectionBook()
{
_protectedPosition = Position;
_protection = null;
}
private static void SetStopLoss(ProtectedPosition protection, decimal entryPrice, int points, decimal priceStep)
{
if (points <= 0)
return;
var distance = points * priceStep;
protection.StopLossPrice = protection.Side == Sides.Buy
? entryPrice - distance
: entryPrice + distance;
}
private static void SetTakeProfit(ProtectedPosition protection, decimal entryPrice, int points, decimal priceStep)
{
if (points <= 0)
return;
var distance = points * priceStep;
protection.TakeProfitPrice = protection.Side == Sides.Buy
? entryPrice + distance
: entryPrice - distance;
}
internal static Evaluation DrawEvaluation(
Random random,
int randomMaxValue,
int minStopLossPoints,
int maxStopLossPoints,
int minTakeProfitPoints,
int maxTakeProfitPoints)
{
ArgumentNullException.ThrowIfNull(random);
if (randomMaxValue < 0)
throw new ArgumentOutOfRangeException(nameof(randomMaxValue));
int drawEntry() => randomMaxValue == int.MaxValue
? (int)random.NextInt64((long)int.MaxValue + 1L)
: random.Next(randomMaxValue + 1);
return new(
drawEntry(),
drawEntry(),
drawEntry(),
drawEntry(),
DrawDistance(random, minStopLossPoints, maxStopLossPoints),
DrawDistance(random, minTakeProfitPoints, maxTakeProfitPoints));
}
private static int DrawDistance(Random random, int minimum, int maximum)
{
if (minimum <= 0 || maximum <= 0 || minimum > maximum)
return 0;
return maximum == int.MaxValue
? (int)random.NextInt64(minimum, (long)maximum + 1L)
: random.Next(minimum, maximum + 1);
}
internal readonly record struct Evaluation(
int LongFirst,
int LongSecond,
int ShortFirst,
int ShortSecond,
int StopLossPoints,
int TakeProfitPoints)
{
public bool EnterLong => LongFirst == LongSecond;
public bool EnterShort => ShortFirst == ShortSecond;
}
private sealed class ProtectedPosition(Sides side, decimal volume, DateTime activeAfter)
{
public Sides Side { get; } = side;
public decimal Volume { get; } = volume;
public DateTime ActiveAfter { get; } = activeAfter;
public decimal? StopLossPrice { get; set; }
public decimal? TakeProfitPrice { get; set; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Random
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class pinball_machine_random_draw_strategy(Strategy):
def __init__(self):
super(pinball_machine_random_draw_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Volume submitted for every matching pair", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candles that trigger random draws", "General")
self._random_max_value = self.Param("RandomMaxValue", 99) \
.SetDisplay("Random Maximum", "Inclusive upper bound for entry draws", "Random")
self._min_stop_loss_points = self.Param("MinStopLossPoints", 10) \
.SetDisplay("Minimum Stop Loss", "Minimum random stop-loss distance in price steps", "Protection")
self._max_stop_loss_points = self.Param("MaxStopLossPoints", 50) \
.SetDisplay("Maximum Stop Loss", "Maximum random stop-loss distance in price steps", "Protection")
self._min_take_profit_points = self.Param("MinTakeProfitPoints", 10) \
.SetDisplay("Minimum Take Profit", "Minimum random take-profit distance in price steps", "Protection")
self._max_take_profit_points = self.Param("MaxTakeProfitPoints", 100) \
.SetDisplay("Maximum Take Profit", "Maximum random take-profit distance in price steps", "Protection")
self._random_seed = self.Param("RandomSeed", 0) \
.SetDisplay("Random Seed", "Zero uses a time-based sequence; any other value is reproducible", "Random")
self._random = None
self._protected_position = 0.0
self._protection = None
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RandomMaxValue(self):
return self._random_max_value.Value
@RandomMaxValue.setter
def RandomMaxValue(self, value):
self._random_max_value.Value = value
@property
def MinStopLossPoints(self):
return self._min_stop_loss_points.Value
@MinStopLossPoints.setter
def MinStopLossPoints(self, value):
self._min_stop_loss_points.Value = value
@property
def MaxStopLossPoints(self):
return self._max_stop_loss_points.Value
@MaxStopLossPoints.setter
def MaxStopLossPoints(self, value):
self._max_stop_loss_points.Value = value
@property
def MinTakeProfitPoints(self):
return self._min_take_profit_points.Value
@MinTakeProfitPoints.setter
def MinTakeProfitPoints(self, value):
self._min_take_profit_points.Value = value
@property
def MaxTakeProfitPoints(self):
return self._max_take_profit_points.Value
@MaxTakeProfitPoints.setter
def MaxTakeProfitPoints(self, value):
self._max_take_profit_points.Value = value
@property
def RandomSeed(self):
return self._random_seed.Value
@RandomSeed.setter
def RandomSeed(self, value):
self._random_seed.Value = value
def OnReseted(self):
super(pinball_machine_random_draw_strategy, self).OnReseted()
self._random = None
self._protected_position = 0.0
self._protection = None
def OnStarted2(self, time):
super(pinball_machine_random_draw_strategy, self).OnStarted2(time)
seed = int(self.RandomSeed)
self._random = Random() if seed == 0 else Random(seed)
self._start_protection_book()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._process_protection(candle)
draw = self._draw_evaluation()
if draw[0] == draw[1]:
self._submit_entry(Sides.Buy, candle, draw[4], draw[5])
if draw[2] == draw[3]:
self._submit_entry(Sides.Sell, candle, draw[4], draw[5])
def _draw_evaluation(self):
maximum = int(self.RandomMaxValue)
if maximum < 0:
raise ValueError("RandomMaxValue must not be negative")
values = [self._random.Next(maximum + 1) for _ in range(4)]
values.append(self._draw_distance(int(self.MinStopLossPoints), int(self.MaxStopLossPoints)))
values.append(self._draw_distance(int(self.MinTakeProfitPoints), int(self.MaxTakeProfitPoints)))
return values
def _draw_distance(self, minimum, maximum):
if minimum <= 0 or maximum <= 0 or minimum > maximum:
return 0
return self._random.Next(minimum, maximum + 1)
def _submit_entry(self, side, candle, stop_loss_points, take_profit_points):
volume = self.TradeVolume
if volume <= 0:
return
self._submit_market(side, volume, "Pinball entry")
self._protected_position += volume if side == Sides.Buy else -volume
if self._protected_position == 0:
self._protection = None
return
price_step = self.Security.PriceStep if self.Security is not None else None
if price_step is None or price_step <= 0 or (stop_loss_points <= 0 and take_profit_points <= 0):
self._protection = None
return
protection = {
"side": Sides.Buy if self._protected_position > 0 else Sides.Sell,
"volume": abs(self._protected_position),
"active_after": candle.CloseTime,
"stop": None,
"take": None,
}
self._set_stop_loss(protection, candle.ClosePrice, stop_loss_points, price_step)
self._set_take_profit(protection, candle.ClosePrice, take_profit_points, price_step)
self._protection = protection
def _process_protection(self, candle):
protection = self._protection
if protection is None or candle.CloseTime <= protection["active_after"]:
return
is_long = protection["side"] == Sides.Buy
stop = protection["stop"]
take = protection["take"]
stop_hit = stop is not None and (candle.LowPrice <= stop if is_long else candle.HighPrice >= stop)
take_hit = not stop_hit and take is not None and (candle.HighPrice >= take if is_long else candle.LowPrice <= take)
if not stop_hit and not take_hit:
return
exit_side = Sides.Sell if is_long else Sides.Buy
self._submit_market(exit_side, protection["volume"], "Pinball protection exit")
self._protected_position = 0.0
self._protection = None
def _submit_market(self, side, volume, comment):
order = self.CreateOrder(side, 0, volume)
order.Comment = comment
self.RegisterOrder(order)
def _start_protection_book(self):
self._protected_position = self.Position
self._protection = None
@staticmethod
def _set_stop_loss(protection, entry_price, points, price_step):
if points <= 0:
return
distance = points * price_step
protection["stop"] = entry_price - distance if protection["side"] == Sides.Buy else entry_price + distance
@staticmethod
def _set_take_profit(protection, entry_price, points, price_step):
if points <= 0:
return
distance = points * price_step
protection["take"] = entry_price + distance if protection["side"] == Sides.Buy else entry_price - distance
def CreateClone(self):
return pinball_machine_random_draw_strategy()