Мартин для небольших депозитов
Обзор
Стратегия переносит советник «Martin for small deposits» в StockSharp. Она обрабатывает только завершившиеся свечи, подписывается на 15 последних закрытий и открывает сетку, когда текущее закрытие ниже (для покупок) или выше (для продаж), чем закрытие 14 свечей назад. Все заявки выставляются по рынку через высокоуровневый API стратегий.
Логика входа
- Поддерживается скользящее окно из 15 закрытий последних свечей.
- При отсутствии открытых позиций или ожидающих заявок текущее закрытие сравнивается со значением 14 свечей назад.
- Более низкое закрытие запускает сетку покупок, более высокое — сетку продаж.
- Объём первой сделки равен Initial Volume. Каждая следующая сделка по той же стороне умножается на Increase Factor и затем приводится к допустимому шагу объёма инструмента.
Управление позицией
- Пока позиция открыта, стратегия ждёт минимум Bars To Skip завершённых свечей перед добавлением новой сделки.
- Дополнительные заявки отправляются только если цена прошла против позиции не менее Step (pips), пересчитанного в цену через размер пункта.
- После каждой сделки обновляются агрегированный объём, средняя цена входа, минимальная/максимальная цена входа и цена последней сделки.
- Совокупный объём не превышает Max Volume и биржевые ограничения. Если приведённый объём меньше минимально допустимого, сделка пропускается.
Условия выхода
- Когда нереализованная прибыль (разница между текущей ценой закрытия и средней ценой входа, умноженная на объём) превышает Min Profit, стратегия закрывает всю сетку.
- Если Take Profit (pips) больше нуля и цена прошла это расстояние от последней сделки в нужном направлении, позиция полностью закрывается.
- Пока выполняются заявки на закрытие, новые сделки не отправляются. После возврата к нулевой позиции внутренние счётчики сбрасываются, и следующая сетка начинается "с нуля".
Параметры
| Имя | Значение по умолчанию | Описание |
|---|---|---|
| Initial Volume | 0.01 | Базовый объём первой сделки. |
| Take Profit (pips) | 65 | Расстояние в пунктах от последней сделки для полного закрытия. 0 — выключить. |
| Step (pips) | 15 | Движение против позиции в пунктах, необходимое для добавления новой сделки. |
| Bars To Skip | 45 | Минимальное количество завершённых свечей между усреднениями. |
| Increase Factor | 1.7 | Множитель объёма для каждой новой сделки в сетке. |
| Max Volume | 6 | Максимально допустимый совокупный объём (до нормализации по шагу). |
| Min Profit | 10 | Порог прибыли для закрытия всей сетки. |
| Candle Type | 1 час | Таймфрейм подписки на свечи и расчёта сигналов. |
Особенности реализации
- Размер пункта определяется через
Security.PriceStepи количество знаков после запятой. Для инструментов с 3 или 5 знаками шаг цены умножается на 10, как в MQL. - Нереализованная прибыль рассчитывается по разнице цен и объёму, без учёта свопов и комиссий, присутствовавших в оригинале.
- Пока есть активные заявки на закрытие, новая усредняющая сделка не отправляется, что сохраняет последовательность действий оригинального эксперта.
- При Step (pips) = 0 усреднение не выполняется; при Take Profit (pips) = 0 закрытие сетки происходит только по условию Min Profit.
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Martingale averaging strategy for small deposits.
/// </summary>
public class MartinForSmallDepositsStrategy : Strategy
{
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stepPips;
private readonly StrategyParam<int> _barsToSkip;
private readonly StrategyParam<decimal> _increaseFactor;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _minProfit;
private readonly StrategyParam<DataType> _candleType;
private decimal _positionVolume;
private decimal _avgPrice;
private decimal _extremePrice;
private decimal _lastEntryPrice;
private int _currentTradeCount;
private int _currentDirection;
private int _barsSinceLastEntry;
private decimal _pendingOpenVolume;
private int _pendingOpenDirection;
private decimal _pendingCloseVolume;
private int _pendingCloseDirection;
private decimal _pipSize;
private readonly decimal[] _closeHistory = new decimal[15];
private int _closeHistoryCount;
private int _latestIndex = -1;
/// <summary>
/// Initializes a new instance of the <see cref="MartinForSmallDepositsStrategy"/> class.
/// </summary>
public MartinForSmallDepositsStrategy()
{
_initialVolume = Param(nameof(InitialVolume), 0.01m)
.SetDisplay("Initial Volume", "Base lot size for the first order", "Position Sizing")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 200)
.SetDisplay("Take Profit (pips)", "Take profit distance from the latest entry", "Risk")
;
_stepPips = Param(nameof(StepPips), 100)
.SetDisplay("Step (pips)", "Adverse price move required to add a new trade", "Position Sizing")
;
_barsToSkip = Param(nameof(BarsToSkip), 100)
.SetDisplay("Bars To Skip", "Number of finished candles to wait before averaging", "Timing")
;
_increaseFactor = Param(nameof(IncreaseFactor), 1.7m)
.SetDisplay("Increase Factor", "Multiplier applied to the volume of each new order", "Position Sizing")
;
_maxVolume = Param(nameof(MaxVolume), 6m)
.SetDisplay("Max Volume", "Maximum allowed aggregated volume", "Risk")
;
_minProfit = Param(nameof(MinProfit), 10m)
.SetDisplay("Min Profit", "Net profit threshold to close all positions", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for signal generation", "General");
}
/// <summary>
/// Base lot size for the first trade in the sequence.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Price move in pips that triggers an averaging order.
/// </summary>
public int StepPips
{
get => _stepPips.Value;
set => _stepPips.Value = value;
}
/// <summary>
/// Number of candles to wait between additional averaging trades.
/// </summary>
public int BarsToSkip
{
get => _barsToSkip.Value;
set => _barsToSkip.Value = value;
}
/// <summary>
/// Multiplier for the martingale position sizing.
/// </summary>
public decimal IncreaseFactor
{
get => _increaseFactor.Value;
set => _increaseFactor.Value = value;
}
/// <summary>
/// Maximum allowed aggregated volume across all open trades.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Profit target that closes the whole grid.
/// </summary>
public decimal MinProfit
{
get => _minProfit.Value;
set => _minProfit.Value = value;
}
/// <summary>
/// Candle type used to build signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_positionVolume = 0m;
_avgPrice = 0m;
_extremePrice = 0m;
_lastEntryPrice = 0m;
_currentTradeCount = 0;
_currentDirection = 0;
_barsSinceLastEntry = 0;
_pendingOpenVolume = 0m;
_pendingOpenDirection = 0;
_pendingCloseVolume = 0m;
_pendingCloseDirection = 0;
_pipSize = 0m;
Array.Clear(_closeHistory, 0, _closeHistory.Length);
_closeHistoryCount = 0;
_latestIndex = -1;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// No bound indicators - skip formation check
UpdateCloseHistory(candle.ClosePrice);
var pipSize = EnsurePipSize();
if (pipSize <= 0m)
return;
var stepDistance = StepPips > 0 ? StepPips * pipSize : 0m;
var takeProfitDistance = TakeProfitPips > 0 ? TakeProfitPips * pipSize : 0m;
var hasPosition = _positionVolume > 0m || Position != 0m || _pendingOpenDirection != 0 || _pendingCloseDirection != 0;
if (!hasPosition)
{
if (!IsHistoryReady())
return;
var referenceClose = GetReferenceClose();
if (candle.ClosePrice < referenceClose)
{
TryOpenBuy(candle.ClosePrice);
}
else if (candle.ClosePrice > referenceClose)
{
TryOpenSell(candle.ClosePrice);
}
return;
}
if (_pendingCloseDirection != 0)
return;
if (_positionVolume <= 0m || _currentDirection == 0)
return;
_barsSinceLastEntry++;
var price = candle.ClosePrice;
var openPnL = CalculateOpenProfit(price);
if (openPnL > MinProfit)
{
CloseAllPositions();
return;
}
if (_currentDirection > 0)
{
if (takeProfitDistance > 0m && price >= _lastEntryPrice + takeProfitDistance)
{
CloseAllPositions();
return;
}
if (_barsSinceLastEntry <= BarsToSkip)
return;
if (stepDistance > 0m && _extremePrice - price > stepDistance)
TryOpenBuy(price);
}
else if (_currentDirection < 0)
{
if (takeProfitDistance > 0m && price <= _lastEntryPrice - takeProfitDistance)
{
CloseAllPositions();
return;
}
if (_barsSinceLastEntry <= BarsToSkip)
return;
if (stepDistance > 0m && price - _extremePrice > stepDistance)
TryOpenSell(price);
}
}
private void TryOpenBuy(decimal price)
{
if (_pendingOpenDirection != 0 && _pendingOpenDirection != 1)
return;
var volume = GetNextVolume(1);
if (volume <= 0m)
return;
BuyMarket(volume);
_pendingOpenDirection = 1;
_pendingOpenVolume += volume;
}
private void TryOpenSell(decimal price)
{
if (_pendingOpenDirection != 0 && _pendingOpenDirection != -1)
return;
var volume = GetNextVolume(-1);
if (volume <= 0m)
return;
SellMarket(volume);
_pendingOpenDirection = -1;
_pendingOpenVolume += volume;
}
private void CloseAllPositions()
{
if (_pendingCloseDirection != 0)
return;
var volume = Position;
if (volume > 0m)
{
SellMarket(volume);
_pendingCloseDirection = -1;
_pendingCloseVolume += volume;
}
else if (volume < 0m)
{
var closeVolume = -volume;
BuyMarket(closeVolume);
_pendingCloseDirection = 1;
_pendingCloseVolume += closeVolume;
}
else if (_positionVolume > 0m)
{
if (_currentDirection > 0)
{
SellMarket(_positionVolume);
_pendingCloseDirection = -1;
_pendingCloseVolume += _positionVolume;
}
else if (_currentDirection < 0)
{
BuyMarket(_positionVolume);
_pendingCloseDirection = 1;
_pendingCloseVolume += _positionVolume;
}
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
if (trade.Order == null)
return;
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
if (trade.Order.Side == Sides.Buy)
{
if (_pendingCloseDirection == 1)
{
ApplyClose(volume);
_pendingCloseVolume -= volume;
if (_pendingCloseVolume <= 0m)
_pendingCloseDirection = 0;
return;
}
if (_pendingOpenDirection == 1)
{
ApplyLongOpen(price, volume);
_pendingOpenVolume -= volume;
if (_pendingOpenVolume <= 0m)
_pendingOpenDirection = 0;
return;
}
if (_currentDirection < 0)
ApplyClose(volume);
}
else if (trade.Order.Side == Sides.Sell)
{
if (_pendingCloseDirection == -1)
{
ApplyClose(volume);
_pendingCloseVolume -= volume;
if (_pendingCloseVolume <= 0m)
_pendingCloseDirection = 0;
return;
}
if (_pendingOpenDirection == -1)
{
ApplyShortOpen(price, volume);
_pendingOpenVolume -= volume;
if (_pendingOpenVolume <= 0m)
_pendingOpenDirection = 0;
return;
}
if (_currentDirection > 0)
ApplyClose(volume);
}
}
private void ApplyLongOpen(decimal price, decimal volume)
{
var previousVolume = _positionVolume;
_positionVolume += volume;
_avgPrice = previousVolume == 0m ? price : ((_avgPrice * previousVolume) + (price * volume)) / _positionVolume;
_extremePrice = previousVolume == 0m ? price : Math.Min(_extremePrice, price);
_lastEntryPrice = price;
_currentDirection = 1;
_currentTradeCount++;
_barsSinceLastEntry = 0;
}
private void ApplyShortOpen(decimal price, decimal volume)
{
var previousVolume = _positionVolume;
_positionVolume += volume;
_avgPrice = previousVolume == 0m ? price : ((_avgPrice * previousVolume) + (price * volume)) / _positionVolume;
_extremePrice = previousVolume == 0m ? price : Math.Max(_extremePrice, price);
_lastEntryPrice = price;
_currentDirection = -1;
_currentTradeCount++;
_barsSinceLastEntry = 0;
}
private void ApplyClose(decimal volume)
{
_positionVolume -= volume;
if (_positionVolume <= 0m)
{
ResetPositionState();
}
}
private void ResetPositionState()
{
_positionVolume = 0m;
_avgPrice = 0m;
_extremePrice = 0m;
_lastEntryPrice = 0m;
_currentTradeCount = 0;
_currentDirection = 0;
_barsSinceLastEntry = 0;
_pendingOpenDirection = 0;
_pendingOpenVolume = 0m;
_pendingCloseDirection = 0;
_pendingCloseVolume = 0m;
}
private decimal CalculateOpenProfit(decimal price)
{
if (_currentDirection > 0)
return (price - _avgPrice) * _positionVolume;
if (_currentDirection < 0)
return (_avgPrice - price) * _positionVolume;
return 0m;
}
private decimal GetNextVolume(int direction)
{
var baseVolume = InitialVolume;
if (baseVolume <= 0m)
return 0m;
var depth = _currentDirection == direction ? _currentTradeCount : 0;
decimal factor;
if (IncreaseFactor <= 0m || depth == 0)
{
factor = 1m;
}
else
{
var raw = Math.Pow((double)IncreaseFactor, depth);
if (double.IsInfinity(raw) || double.IsNaN(raw) || raw > (double)decimal.MaxValue)
return 0m;
factor = (decimal)raw;
}
var volume = baseVolume * factor;
if (MaxVolume > 0m && volume > MaxVolume)
volume = MaxVolume;
volume = NormalizeVolume(volume);
return volume;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var security = Security;
if (security == null)
return 0m;
if (security.VolumeStep is decimal step && step > 0m)
{
var steps = decimal.Truncate(volume / step);
volume = steps * step;
}
if (security.MinVolume is decimal min && volume < min)
return 0m;
if (security.MaxVolume is decimal max && volume > max)
volume = max;
return volume;
}
private decimal EnsurePipSize()
{
if (_pipSize > 0m)
return _pipSize;
var security = Security;
if (security == null)
return 0m;
var step = security.PriceStep ?? 0m;
if (step == 0m)
{
var decimals = security.Decimals;
if (decimals != null)
{
step = (decimal)Math.Pow(10, -decimals.Value);
}
}
if (step == 0m)
step = 0.01m;
var decimalsCount = security.Decimals ?? 0;
_pipSize = (decimalsCount == 3 || decimalsCount == 5) ? step * 10m : step;
if (_pipSize == 0m)
_pipSize = step > 0m ? step : 0.01m;
return _pipSize;
}
private void UpdateCloseHistory(decimal closePrice)
{
if (_closeHistory.Length == 0)
return;
_latestIndex = (_latestIndex + 1) % _closeHistory.Length;
_closeHistory[_latestIndex] = closePrice;
if (_closeHistoryCount < _closeHistory.Length)
_closeHistoryCount++;
}
private bool IsHistoryReady()
{
return _closeHistoryCount >= _closeHistory.Length;
}
private decimal GetReferenceClose()
{
var index = (_latestIndex + 1) % _closeHistory.Length;
return _closeHistory[index];
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class martin_for_small_deposits_strategy(Strategy):
def __init__(self):
super(martin_for_small_deposits_strategy, self).__init__()
self._initial_volume = self.Param("InitialVolume", 0.01)
self._take_profit_pips = self.Param("TakeProfitPips", 200)
self._step_pips = self.Param("StepPips", 100)
self._bars_to_skip = self.Param("BarsToSkip", 100)
self._increase_factor = self.Param("IncreaseFactor", 1.7)
self._max_volume = self.Param("MaxVolume", 6.0)
self._min_profit = self.Param("MinProfit", 10.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._position_volume = 0.0
self._avg_price = 0.0
self._extreme_price = 0.0
self._last_entry_price = 0.0
self._current_trade_count = 0
self._current_direction = 0
self._bars_since_last_entry = 0
self._pip_size = 0.0
self._close_history = [0.0] * 15
self._close_history_count = 0
self._latest_index = -1
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def InitialVolume(self):
return self._initial_volume.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StepPips(self):
return self._step_pips.Value
@property
def BarsToSkip(self):
return self._bars_to_skip.Value
@property
def IncreaseFactor(self):
return self._increase_factor.Value
@property
def MaxVolume(self):
return self._max_volume.Value
@property
def MinProfit(self):
return self._min_profit.Value
def OnStarted2(self, time):
super(martin_for_small_deposits_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_close_history(float(candle.ClosePrice))
pip_size = self._ensure_pip_size()
if pip_size <= 0:
return
step_dist = self.StepPips * pip_size if self.StepPips > 0 else 0.0
tp_dist = self.TakeProfitPips * pip_size if self.TakeProfitPips > 0 else 0.0
has_position = (self._position_volume > 0 or self.Position != 0 or
self._current_direction != 0)
if not has_position:
if not self._is_history_ready():
return
ref = self._get_reference_close()
price = float(candle.ClosePrice)
if price < ref:
self._try_open_buy(price)
elif price > ref:
self._try_open_sell(price)
return
if self._position_volume <= 0 or self._current_direction == 0:
return
self._bars_since_last_entry += 1
price = float(candle.ClosePrice)
pnl = self._calculate_open_profit(price)
if pnl > self.MinProfit:
self._close_all()
return
if self._current_direction > 0:
if tp_dist > 0 and price >= self._last_entry_price + tp_dist:
self._close_all()
return
if self._bars_since_last_entry <= self.BarsToSkip:
return
if step_dist > 0 and self._extreme_price - price > step_dist:
self._try_open_buy(price)
elif self._current_direction < 0:
if tp_dist > 0 and price <= self._last_entry_price - tp_dist:
self._close_all()
return
if self._bars_since_last_entry <= self.BarsToSkip:
return
if step_dist > 0 and price - self._extreme_price > step_dist:
self._try_open_sell(price)
def _try_open_buy(self, price):
vol = self._get_next_volume(1)
if vol <= 0:
return
self.BuyMarket()
self._apply_long_open(price, vol)
def _try_open_sell(self, price):
vol = self._get_next_volume(-1)
if vol <= 0:
return
self.SellMarket()
self._apply_short_open(price, vol)
def _close_all(self):
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._reset_position_state()
def _apply_long_open(self, price, volume):
prev = self._position_volume
self._position_volume += volume
if prev == 0:
self._avg_price = price
self._extreme_price = price
else:
self._avg_price = (self._avg_price * prev + price * volume) / self._position_volume
self._extreme_price = min(self._extreme_price, price)
self._last_entry_price = price
self._current_direction = 1
self._current_trade_count += 1
self._bars_since_last_entry = 0
def _apply_short_open(self, price, volume):
prev = self._position_volume
self._position_volume += volume
if prev == 0:
self._avg_price = price
self._extreme_price = price
else:
self._avg_price = (self._avg_price * prev + price * volume) / self._position_volume
self._extreme_price = max(self._extreme_price, price)
self._last_entry_price = price
self._current_direction = -1
self._current_trade_count += 1
self._bars_since_last_entry = 0
def _calculate_open_profit(self, price):
if self._current_direction > 0:
return (price - self._avg_price) * self._position_volume
elif self._current_direction < 0:
return (self._avg_price - price) * self._position_volume
return 0.0
def _get_next_volume(self, direction):
base = self.InitialVolume
if base <= 0:
return 0.0
depth = self._current_trade_count if self._current_direction == direction else 0
if self.IncreaseFactor <= 0 or depth == 0:
factor = 1.0
else:
raw = math.pow(self.IncreaseFactor, depth)
if math.isinf(raw) or math.isnan(raw):
return 0.0
factor = raw
vol = base * factor
if self.MaxVolume > 0 and vol > self.MaxVolume:
vol = self.MaxVolume
return vol
def _reset_position_state(self):
self._position_volume = 0.0
self._avg_price = 0.0
self._extreme_price = 0.0
self._last_entry_price = 0.0
self._current_trade_count = 0
self._current_direction = 0
self._bars_since_last_entry = 0
def _ensure_pip_size(self):
if self._pip_size > 0:
return self._pip_size
sec = self.Security
if sec is None:
return 0.0
step = float(sec.PriceStep) if sec.PriceStep is not None else 0.0
if step == 0:
d = sec.Decimals if sec.Decimals is not None else 0
if d > 0:
step = math.pow(10, -d)
if step == 0:
step = 0.01
d_count = sec.Decimals if sec is not None and sec.Decimals is not None else 0
self._pip_size = step * 10.0 if (d_count == 3 or d_count == 5) else step
if self._pip_size == 0:
self._pip_size = step if step > 0 else 0.01
return self._pip_size
def _update_close_history(self, close_price):
length = len(self._close_history)
if length == 0:
return
self._latest_index = (self._latest_index + 1) % length
self._close_history[self._latest_index] = close_price
if self._close_history_count < length:
self._close_history_count += 1
def _is_history_ready(self):
return self._close_history_count >= len(self._close_history)
def _get_reference_close(self):
index = (self._latest_index + 1) % len(self._close_history)
return self._close_history[index]
def OnReseted(self):
super(martin_for_small_deposits_strategy, self).OnReseted()
self._position_volume = 0.0
self._avg_price = 0.0
self._extreme_price = 0.0
self._last_entry_price = 0.0
self._current_trade_count = 0
self._current_direction = 0
self._bars_since_last_entry = 0
self._pip_size = 0.0
self._close_history = [0.0] * 15
self._close_history_count = 0
self._latest_index = -1
def CreateClone(self):
return martin_for_small_deposits_strategy()