小资金马丁策略
概述
该策略在 StockSharp 中复刻了“Martin for small deposits”马丁网格专家。策略每次仅在一根完成的K线上执行逻辑,使用最近15根K线的收盘价:当最新收盘价低于14根之前的收盘价时开多网格,反之则开空网格。所有交易都通过高级策略 API 以市价提交。
入场逻辑
- 使用滑动窗口保存最近15根完成K线的收盘价。
- 当没有持仓或挂单时,将最新收盘价与14根之前的收盘价进行比较。
- 最新收盘价更低时启动多头网格;更高时启动空头网格。
- 首单手数等于 Initial Volume。同方向的后续加仓按照马丁系数放大,然后再按品种的最小交易步长归一化。
仓位管理
- 持仓期间,策略会等待 Bars To Skip 根完成K线后才考虑下一次加仓。
- 只有当价格朝着持仓不利方向移动至少 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 | 连续完成的K线数量,在此期间禁止再次加仓。 |
| Increase Factor | 1.7 | 同方向每次加仓前使用的马丁倍数。 |
| Max Volume | 6 | 网格允许的最大总手数(归一化前)。 |
| Min Profit | 10 | 净利润超过该值时平掉整个网格。 |
| Candle Type | 1小时 | 订阅K线与计算信号所使用的周期。 |
实现说明
- 点值根据
Security.PriceStep与小数位数推导;当报价精度为3或5位小数时,会将最小报价步长乘以10以匹配 MQL 中的“pip”概念。 - 未实现利润通过价格差与持仓量估算,不包含原始专家中的掉期或手续费调整。
- 在平仓订单未完全成交之前,策略不会发送新的加仓指令,从而保持原始 MQL 顺序执行的行为。
- 当 Step (pips) 为零时不会进行加仓;当 Take Profit (pips) 为零时,仅有 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()