Стратегия Viva Las Vegas
Обзор
Viva Las Vegas — это экспериментальный советник по управлению капиталом, который на каждой новой попытке случайным образом открывает длинную или короткую позицию и передает размер следующей сделки одному из пяти классических прогрессирующих алгоритмов. Порт на StockSharp полностью повторяет логику MetaTrader:
- Направление выбирается через псевдослучайный «подброс монеты».
- Сразу же выставляются симметричные стоп-лосс и тейк-профит в пунктах.
- После закрытия позиции последовательность money-management обновляется и моментально открывается новая сделка.
В результате стратегия всегда находится либо в позиции, либо мгновенно готова открыть следующую — это удобный пример того, как ведут себя агрессивные системы мартингейла в инфраструктуре StockSharp.
Модули управления капиталом
Параметр MoneyManagement задает один из следующих вариантов, где BaseVolume служит базовым лотом:
- Martingale — удваивает объем после каждого убытка и возвращается к базовому объему после прибыли.
- Negative Pyramid — удваивает объем после убытка и делит его пополам после прибыли (но не опускается ниже базового значения).
- Labouchere — поддерживает числовую последовательность (по умолчанию
1-2-3), ставит сумму первого и последнего элементов, удаляет их после выигрыша и добавляет их сумму после проигрыша.
- Oscar’s Grind — увеличивает ставку на базовый лот после каждой прибыли, пока суммарный результат не достигнет одного базового лота, затем обнуляет прогрессию; убытки уменьшают накопленный результат.
- 31 System — циклически проходит по серии
1,1,1,2,2,4,4,8,8: первая прибыль удваивает текущий элемент, вторая подряд прибыль сбрасывает серию в начало.
Все схемы реализованы в точности как в MQL-версии, включая трактовку нулевой прибыли как поражения.
Последовательность работы
- При старте инициализируется генератор случайных чисел (если
Seed = 0, используется текущее время) и активируется встроенный модуль защиты StartProtection с заданным расстоянием до стопа и тейка.
- Когда нет позиции и нет активного ордера, стратегия запрашивает у выбранного модуля следующий объем, нормализует его по
VolumeStep инструмента и случайно выбирает BuyMarket или SellMarket.
- Как только позиция открыта, защитный модуль сопровождает её до срабатывания стопа или тейка.
- После возврата позиции к нулю фиксируется изменение
PnL:
- Прибыль > 0 — модуль получает сигнал win.
- Прибыль ≤ 0 — модуль получает сигнал loss.
- Цикл сразу повторяется, поэтому стратегия постоянно находится в рынке, управляя только одной сделкой за раз.
Благодаря этому поведение полностью совпадает с «одиночным тикетом» оригинального эксперта и легко отслеживается на графике.
Параметры
| Имя |
Тип |
Значение по умолчанию |
Описание |
StopTakePips |
int |
50 |
Расстояние в пунктах до стоп-лосса и тейк-профита, передается в StartProtection. |
BaseVolume |
decimal |
1 |
Базовый лот, от которого отталкиваются все прогрессии управления капиталом. |
MoneyManagement |
MoneyManagementMode |
Martingale |
Алгоритм, определяющий объем следующей сделки. |
Seed |
int |
0 |
Зерно генератора случайных чисел; ноль означает использование текущего времени. |
Особенности реализации
- Объемы приводятся к допустимому шагу
VolumeStep и проверяются на соответствие MinVolume / MaxVolume, что предотвращает отклоненные заявки.
- Пункты пересчитываются в шаги цены по классическому правилу MetaTrader: если количество знаков
Digits равно 3 или 5, один пункт содержит десять тиков.
- Фиксированный результат сделки вычисляется через свойство
PnL, поэтому любые закрытия (в том числе защитными ордерами) корректно влияют на прогрессию.
- В коде добавлены английские комментарии, поясняющие ключевые решения и облегчающие адаптацию стратегии для учебных целей.
Рекомендации по применению
- Используйте демо-счет или режим тестирования: мартингейл несет повышенный риск и предназначен в первую очередь для экспериментов.
- Перед запуском убедитесь, что
BaseVolume соответствует минимальному размеру лота выбранного инструмента.
- Добавьте стратегию на график StockSharp, чтобы видеть, как разные схемы управления капиталом наращивают или снижают объем.
namespace StockSharp.Samples.Strategies;
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
public class VivaLasVegasStrategy : Strategy
{
private readonly StrategyParam<int> _stopTakePips;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<MoneyManagementModes> _moneyManagementMode;
private readonly StrategyParam<int> _seed;
private int _activeSeed;
private IMoneyManagement _management;
private decimal _previousPosition;
private decimal _lastRealizedPnL;
private bool _orderInFlight;
public enum MoneyManagementModes
{
Martingale,
NegativePyramid,
Labouchere,
OscarsGrind,
System31,
}
public VivaLasVegasStrategy()
{
_stopTakePips = Param(nameof(StopTakePips), 50)
.SetGreaterThanZero()
.SetDisplay("Stop/take distance", "Protective distance expressed in pips for both stop-loss and take-profit.", "Risk")
.SetOptimize(10, 200, 10);
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Base volume", "Initial lot size used as the anchor for all money management progressions.", "Risk")
.SetOptimize(0.1m, 5m, 0.1m);
_moneyManagementMode = Param(nameof(MoneyManagement), MoneyManagementModes.Martingale)
.SetDisplay("Money management", "Progression model that decides the next order volume.", "General");
_seed = Param(nameof(Seed), 0)
.SetDisplay("Random seed", "Seed for the pseudo-random trade direction. Zero switches to time-based seeding.", "General");
}
public int StopTakePips
{
get => _stopTakePips.Value;
set => _stopTakePips.Value = value;
}
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
public MoneyManagementModes MoneyManagement
{
get => _moneyManagementMode.Value;
set
{
_moneyManagementMode.Value = value;
InitializeMoneyManagement();
}
}
public int Seed
{
get => _seed.Value;
set => _seed.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_activeSeed = 0;
_management = null;
_previousPosition = 0m;
_lastRealizedPnL = 0m;
_orderInFlight = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = BaseVolume;
InitializeMoneyManagement();
_activeSeed = Seed == 0 ? System.Environment.TickCount : Seed;
var steps = StopTakePips * GetPipMultiplier();
if (StopTakePips > 0 && steps > 0m)
{
var unit = new Unit(steps, UnitTypes.Absolute);
StartProtection(unit, unit);
}
else
{
StartProtection(null, null);
}
// Use candle subscription to pace trades
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position == 0m && !_orderInFlight)
TryOpenPosition();
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (Position != 0m)
{
// A fill confirmed our open position, so further market orders must wait.
_orderInFlight = false;
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (_previousPosition == 0m && Position != 0m)
{
// A new position was just established; capture the realized PnL baseline.
_lastRealizedPnL = PnL;
_orderInFlight = false;
}
else if (_previousPosition != 0m && Position == 0m)
{
var tradePnL = PnL - _lastRealizedPnL;
_lastRealizedPnL = PnL;
var closedVolume = Math.Abs(_previousPosition);
if (closedVolume > 0m && _management != null)
{
var result = tradePnL > 0m ? TradeResults.Win : TradeResults.Loss;
_management.Update(result, closedVolume, BaseVolume);
}
_orderInFlight = false;
}
_previousPosition = Position;
}
private void TryOpenPosition()
{
if (ProcessState != ProcessStates.Started)
return;
if (Position != 0m || _orderInFlight)
return;
var volume = _management?.GetVolume(BaseVolume) ?? BaseVolume;
volume = AdjustVolume(volume);
if (volume <= 0m)
return;
_activeSeed = _activeSeed * 1103515245 + 12345;
var isBuy = ((_activeSeed >> 16) & 1) == 0;
_orderInFlight = true;
if (isBuy)
{
// Coin toss favoured the bullish side.
BuyMarket(volume);
}
else
{
// Bearish outcome - sell into the market.
SellMarket(volume);
}
}
private decimal AdjustVolume(decimal volume)
{
var security = Security;
if (security == null)
return volume;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
var steps = Math.Max(1m, Math.Round(volume / step, MidpointRounding.AwayFromZero));
volume = steps * step;
}
var minVolume = security.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
var maxVolume = security.MaxVolume ?? 0m;
if (maxVolume > 0m && volume > maxVolume)
volume = maxVolume;
return volume;
}
private decimal GetPipMultiplier()
{
var security = Security;
if (security == null)
return 1m;
return security.Decimals is 3 or 5 ? 10m : 1m;
}
private void InitializeMoneyManagement()
{
_management = CreateMoneyManagement(MoneyManagement);
_management.Reset(BaseVolume);
}
private IMoneyManagement CreateMoneyManagement(MoneyManagementModes mode)
{
return mode switch
{
MoneyManagementModes.Martingale => new MartingaleManagement(),
MoneyManagementModes.NegativePyramid => new NegativePyramidManagement(),
MoneyManagementModes.Labouchere => new LabouchereManagement(),
MoneyManagementModes.OscarsGrind => new OscarsGrindManagement(),
MoneyManagementModes.System31 => new System31Management(),
_ => new MartingaleManagement(),
};
}
private enum TradeResults
{
Win,
Loss,
}
private interface IMoneyManagement
{
decimal GetVolume(decimal baseVolume);
void Update(TradeResults result, decimal closedVolume, decimal baseVolume);
void Reset(decimal baseVolume);
}
private sealed class MartingaleManagement : IMoneyManagement
{
private decimal _nextVolume;
public decimal GetVolume(decimal baseVolume)
{
if (_nextVolume <= 0m)
_nextVolume = baseVolume;
return _nextVolume;
}
public void Update(TradeResults result, decimal closedVolume, decimal baseVolume)
{
_nextVolume = result == TradeResults.Win ? baseVolume : _nextVolume * 2m;
}
public void Reset(decimal baseVolume)
{
_nextVolume = 0m;
}
}
private sealed class NegativePyramidManagement : IMoneyManagement
{
private decimal _nextVolume;
public decimal GetVolume(decimal baseVolume)
{
if (_nextVolume <= 0m)
_nextVolume = baseVolume;
return _nextVolume;
}
public void Update(TradeResults result, decimal closedVolume, decimal baseVolume)
{
if (result == TradeResults.Win)
{
_nextVolume /= 2m;
if (_nextVolume < baseVolume)
_nextVolume = baseVolume;
}
else
{
_nextVolume *= 2m;
}
}
public void Reset(decimal baseVolume)
{
_nextVolume = 0m;
}
}
private sealed class LabouchereManagement : IMoneyManagement
{
private static readonly int[] _baseSeries = { 1, 2, 3 };
private readonly List<decimal> _series = new();
public decimal GetVolume(decimal baseVolume)
{
if (_series.Count == 0)
Reset(baseVolume);
if (_series.Count > 1)
return (_series[0] + _series[^1]) * baseVolume;
return _series[0] * baseVolume;
}
public void Update(TradeResults result, decimal closedVolume, decimal baseVolume)
{
if (_series.Count == 0)
Reset(baseVolume);
if (result == TradeResults.Win)
{
if (_series.Count > 2)
{
_series.RemoveAt(_series.Count - 1);
_series.RemoveAt(0);
}
else
{
Reset(baseVolume);
}
}
else
{
var first = _series[0];
var last = _series[^1];
_series.Add(first + last);
}
}
public void Reset(decimal baseVolume)
{
_series.Clear();
foreach (var value in _baseSeries)
_series.Add(value);
}
}
private sealed class OscarsGrindManagement : IMoneyManagement
{
private decimal _nextVolume;
private decimal _currentResult;
public decimal GetVolume(decimal baseVolume)
{
if (_nextVolume <= 0m)
_nextVolume = baseVolume;
return _nextVolume;
}
public void Update(TradeResults result, decimal closedVolume, decimal baseVolume)
{
if (result == TradeResults.Win)
{
_currentResult += closedVolume;
if (_currentResult >= baseVolume)
{
_nextVolume = baseVolume;
_currentResult = 0m;
return;
}
_nextVolume += baseVolume;
var cap = baseVolume + Math.Abs(_currentResult);
if (_nextVolume > cap)
_nextVolume = cap;
}
else
{
_currentResult -= closedVolume;
}
}
public void Reset(decimal baseVolume)
{
_nextVolume = 0m;
_currentResult = 0m;
}
}
private sealed class System31Management : IMoneyManagement
{
private static readonly int[] _series = { 1, 1, 1, 2, 2, 4, 4, 8, 8 };
private int _index;
private bool _doubleUp;
public decimal GetVolume(decimal baseVolume)
{
var multiplier = _series[_index];
if (_doubleUp)
multiplier *= 2;
return multiplier * baseVolume;
}
public void Update(TradeResults result, decimal closedVolume, decimal baseVolume)
{
if (result == TradeResults.Win)
{
if (!_doubleUp)
{
_doubleUp = true;
}
else
{
_doubleUp = false;
_index = 0;
}
}
else
{
if (!_doubleUp)
{
_index = (_index + 1) % _series.Length;
}
else
{
_doubleUp = false;
}
}
}
public void Reset(decimal baseVolume)
{
_index = 0;
_doubleUp = false;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class viva_las_vegas_strategy(Strategy):
def __init__(self):
super(viva_las_vegas_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle type", "Timeframe for pacing trades.", "General")
self._stop_take_pips = self.Param("StopTakePips", 50) \
.SetDisplay("Stop/take distance", "Protective distance in pips.", "Risk")
self._base_volume = self.Param("BaseVolume", 1.0) \
.SetDisplay("Base volume", "Initial lot size.", "Risk")
self._seed = self.Param("Seed", 0) \
.SetDisplay("Random seed", "Seed for pseudo-random direction. Zero = time-based.", "General")
self._active_seed = 0
self._prev_position = 0.0
self._last_pnl = 0.0
self._next_volume = 0.0
self._entry_price = 0.0
self._best_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def StopTakePips(self):
return self._stop_take_pips.Value
@property
def BaseVolume(self):
return float(self._base_volume.Value)
@property
def SeedValue(self):
return self._seed.Value
def OnStarted2(self, time):
super(viva_las_vegas_strategy, self).OnStarted2(time)
import time as _time
self._active_seed = int(_time.time() * 1000) if self.SeedValue == 0 else self.SeedValue
self._prev_position = 0.0
self._last_pnl = 0.0
self._next_volume = self.BaseVolume
self._entry_price = 0.0
self._best_price = 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def _get_pip_size(self):
sec = self.Security
if sec is not None:
ps = sec.PriceStep
if ps is not None and float(ps) > 0:
return float(ps)
return 0.0001
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
pip = self._get_pip_size()
stop_dist = self.StopTakePips * pip
# manage position exit
if self.Position > 0:
if close > self._best_price:
self._best_price = close
if stop_dist > 0 and (close <= self._entry_price - stop_dist or close >= self._entry_price + stop_dist):
won = close >= self._entry_price + stop_dist
self.SellMarket()
self._update_volume(won)
self._entry_price = 0.0
self._best_price = 0.0
return
elif self.Position < 0:
if close < self._best_price:
self._best_price = close
if stop_dist > 0 and (close >= self._entry_price + stop_dist or close <= self._entry_price - stop_dist):
won = close <= self._entry_price - stop_dist
self.BuyMarket()
self._update_volume(won)
self._entry_price = 0.0
self._best_price = 0.0
return
if self.Position == 0:
self._active_seed = (self._active_seed * 1103515245 + 12345) & 0x7FFFFFFF
is_buy = ((self._active_seed >> 16) & 1) == 0
self._entry_price = close
self._best_price = close
if is_buy:
self.BuyMarket()
else:
self.SellMarket()
def _update_volume(self, won):
if won:
self._next_volume = self.BaseVolume
else:
self._next_volume = self._next_volume * 2.0
def OnReseted(self):
super(viva_las_vegas_strategy, self).OnReseted()
self._active_seed = 0
self._prev_position = 0.0
self._last_pnl = 0.0
self._next_volume = 0.0
self._entry_price = 0.0
self._best_price = 0.0
def CreateClone(self):
return viva_las_vegas_strategy()