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