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()