在 GitHub 上查看
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 StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Pinball Machine: Pseudo-random entry with ATR-based risk management.
/// Uses candle hash to generate deterministic random signals.
/// </summary>
public class PinballMachineRandomDrawStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrLength;
private decimal _entryPrice;
private int _candleCount;
public PinballMachineRandomDrawStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period for stops.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_candleCount = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_candleCount = 0;
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0)
return;
_candleCount++;
var close = candle.ClosePrice;
// Exit management
if (Position > 0)
{
if (close >= _entryPrice + atrVal * 2m || close <= _entryPrice - atrVal * 1.5m)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (close <= _entryPrice - atrVal * 2m || close >= _entryPrice + atrVal * 1.5m)
{
BuyMarket();
_entryPrice = 0;
}
}
// Pseudo-random entry based on candle price hash
if (Position == 0)
{
var hash = (int)(close * 100m) ^ _candleCount;
var mod = Math.Abs(hash) % 10;
if (mod < 3)
{
_entryPrice = close;
BuyMarket();
}
else if (mod > 6)
{
_entryPrice = close;
SellMarket();
}
}
}
}
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
from StockSharp.Algo.Indicators import AverageTrueRange
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._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Timeframe", "General")
self._atr_length = self.Param("AtrLength", 14).SetDisplay("ATR Length", "ATR period for stops", "Indicators")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(pinball_machine_random_draw_strategy, self).OnReseted()
self._entry_price = 0
self._candle_count = 0
def OnStarted2(self, time):
super(pinball_machine_random_draw_strategy, self).OnStarted2(time)
self._entry_price = 0
self._candle_count = 0
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
if atr_val <= 0:
return
self._candle_count += 1
close = candle.ClosePrice
if self.Position > 0:
if close >= self._entry_price + atr_val * 2 or close <= self._entry_price - atr_val * 1.5:
self.SellMarket()
self._entry_price = 0
elif self.Position < 0:
if close <= self._entry_price - atr_val * 2 or close >= self._entry_price + atr_val * 1.5:
self.BuyMarket()
self._entry_price = 0
if self.Position == 0:
h = int(float(close) * 100) ^ self._candle_count
mod = abs(h) % 10
if mod < 3:
self._entry_price = close
self.BuyMarket()
elif mod > 6:
self._entry_price = close
self.SellMarket()
def CreateClone(self):
return pinball_machine_random_draw_strategy()