MartingaleEA-5 Levels 策略(StockSharp)
MartingaleEA-5 Levels 策略 将 MetaTrader 5 专家顾问 “MartingaleEA-5 Levels” 迁移到 StockSharp 高级 API。系统会监控已有的多头或空头仓位,当价格向不利方向移动时构建最多五层的马丁格尔加仓网格。所有计算都在收盘后的 K 线完成,确保历史回测与实时交易具有一致的行为。
交易逻辑
- 监控现有仓位:策略假设已经存在初始的多头或空头仓位,可以手动下单或由其他模型开出第一笔交易。
- 判断不利波动:每根完成的 K 线都会计算当前价格距离最差开仓价(多头取最高价,空头取最低价)的幅度。
- 马丁格尔加仓:当该方向的浮动盈亏为负且不利波动超过累计距离阈值时,策略会按照
VolumeMultiplier倍数递增成交量,依次发送市价单加仓。参数最多提供五个层级,实际使用的数量由MaxAdditions控制。 - 利润/亏损触发关闭:在加仓组持有期间,策略持续累加该方向的未实现盈亏。一旦达到
TakeProfitCurrency或跌破StopLossCurrency,便使用市价单一次性平掉该方向的全部仓位,并重置马丁格尔计数。 - 数量归一化:所有下单量都会根据交易品种的
VolumeStep、MinVolume、MaxVolume进行调整,避免发送交易所无法接受的数量。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
EnableMartingale |
是否启用马丁格尔加仓与平仓逻辑。 | true |
VolumeMultiplier |
每次加仓相对于上一笔成交量的放大倍数。 | 2.0 |
MaxAdditions |
每个方向允许的最大加仓次数(最多五层)。 | 4 |
Level1DistancePips |
触发第二笔订单的不利波动距离(以点/点值表示)。 | 300 |
Level2DistancePips |
触发第三笔订单所需的附加距离。 | 400 |
Level3DistancePips |
触发第四笔订单所需的附加距离。 | 500 |
Level4DistancePips |
触发第五笔订单所需的附加距离。 | 600 |
Level5DistancePips |
触发第六笔订单所需的附加距离(若仍允许加仓)。 | 700 |
TakeProfitCurrency |
浮动盈利达到该货币金额时平掉整组仓位。 | 200 |
StopLossCurrency |
浮动亏损跌破该货币金额时强制平仓。 | -500 |
CandleType |
用于评估的时间框架(默认 1 分钟 K 线)。 | TimeFrame(1m) |
点值换算:所有距离会乘以品种的价格步长(
PriceStep或MinPriceStep)。若品种报价使用分数点,请相应调整参数。
使用建议
- 该实现与原始 EA 一致,假设同一时刻仅有一个方向的网格在运行。如同时持有多空仓位,系统会分别跟踪各自的浮动盈亏。
- 策略仅在 K 线收盘时做出反应,请选择与你所需响应速度匹配的时间框架。较低周期更接近逐笔行情的行为。
- 马丁格尔方法风险极高。投入真实资金前务必使用真实滑点与手续费进行充分回测,并设置合理的止损水平。
- 按需求仅提供 C# 高级 API 版本,目前未创建 Python 版本或对应目录。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Martingale averaging strategy converted from "MartingaleEA-5 Levels".
/// Opens initial position on simple momentum, then averages down with
/// increasing lot sizes up to 5 levels. Closes when floating profit
/// reaches target or stop threshold.
/// </summary>
public class MartingaleEa5LevelsStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<decimal> _volumeMultiplier;
private readonly StrategyParam<int> _maxAdditions;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private SimpleMovingAverage _sma;
private decimal? _prevClose;
private decimal? _prevMa;
private readonly List<(decimal price, decimal vol)> _entries = new();
private int _additions;
private decimal _lastVolume;
private Sides? _activeSide;
private int _candleCount;
private int _lastOrderCandle;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
public int MaxAdditions
{
get => _maxAdditions.Value;
set => _maxAdditions.Value = value;
}
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
public MartingaleEa5LevelsStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe", "General");
_maPeriod = Param(nameof(MaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("MA Period", "SMA period for entry signal", "Indicators");
_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volume Multiplier", "Multiplier for each martingale level", "Money Management");
_maxAdditions = Param(nameof(MaxAdditions), 4)
.SetDisplay("Max Additions", "Maximum martingale additions", "Money Management");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Take Profit %", "Floating profit % to close group", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Floating loss % to close group", "Risk");
}
protected override void OnReseted()
{
base.OnReseted();
_sma = default;
_prevClose = null;
_prevMa = null;
_entries.Clear();
_additions = 0;
_lastVolume = 0;
_activeSide = null;
_candleCount = 0;
_lastOrderCandle = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_sma = new SimpleMovingAverage { Length = MaPeriod };
_prevClose = null;
_prevMa = null;
_entries.Clear();
_additions = 0;
_lastVolume = 0;
_activeSide = null;
_candleCount = 0;
_lastOrderCandle = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_sma.IsFormed)
{
_prevClose = candle.ClosePrice;
_prevMa = smaValue;
return;
}
_candleCount++;
var close = candle.ClosePrice;
var volume = Volume;
if (volume <= 0)
volume = 1;
// Cooldown: allow at most one order per 100 candles
var cooldownPassed = (_candleCount - _lastOrderCandle) >= 100;
// Check martingale closure first
if (_entries.Count > 0)
{
var floatingPnl = CalculateFloatingPnl(close);
var totalCost = CalculateTotalCost();
if (totalCost > 0)
{
var pnlPercent = floatingPnl / totalCost * 100m;
if (cooldownPassed && (pnlPercent >= TakeProfitPercent || pnlPercent <= -StopLossPercent))
{
// Close entire position
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(Math.Abs(Position));
_lastOrderCandle = _candleCount;
_entries.Clear();
_additions = 0;
_lastVolume = 0;
_activeSide = null;
_prevClose = close;
_prevMa = smaValue;
return;
}
}
// Check for martingale additions
if (cooldownPassed && _additions < MaxAdditions)
{
var avgPrice = CalculateAvgPrice();
var adversePercent = _activeSide == Sides.Buy
? (avgPrice - close) / avgPrice * 100m
: (close - avgPrice) / avgPrice * 100m;
// Add at each 0.3% adverse move beyond previous level
var threshold = 0.3m * (_additions + 1);
if (adversePercent >= threshold)
{
var nextVol = _lastVolume * VolumeMultiplier;
if (nextVol < 1) nextVol = 1;
if (_activeSide == Sides.Buy)
{
BuyMarket(nextVol);
_entries.Add((close, nextVol));
}
else
{
SellMarket(nextVol);
_entries.Add((close, nextVol));
}
_lastVolume = nextVol;
_additions++;
_lastOrderCandle = _candleCount;
}
}
}
// Initial entry signal: MA crossover
if (cooldownPassed && _prevClose != null && _prevMa != null && _activeSide == null)
{
var buySignal = _prevClose.Value < _prevMa.Value && close > smaValue;
var sellSignal = _prevClose.Value > _prevMa.Value && close < smaValue;
if (buySignal)
{
BuyMarket(volume);
_entries.Clear();
_entries.Add((close, volume));
_additions = 0;
_lastVolume = volume;
_activeSide = Sides.Buy;
_lastOrderCandle = _candleCount;
}
else if (sellSignal)
{
SellMarket(volume);
_entries.Clear();
_entries.Add((close, volume));
_additions = 0;
_lastVolume = volume;
_activeSide = Sides.Sell;
_lastOrderCandle = _candleCount;
}
}
_prevClose = close;
_prevMa = smaValue;
}
private decimal CalculateFloatingPnl(decimal currentPrice)
{
var pnl = 0m;
foreach (var (price, vol) in _entries)
{
if (_activeSide == Sides.Buy)
pnl += (currentPrice - price) * vol;
else
pnl += (price - currentPrice) * vol;
}
return pnl;
}
private decimal CalculateTotalCost()
{
var cost = 0m;
foreach (var (price, vol) in _entries)
cost += price * vol;
return cost;
}
private decimal CalculateAvgPrice()
{
var totalVol = 0m;
var totalCost = 0m;
foreach (var (price, vol) in _entries)
{
totalVol += vol;
totalCost += price * vol;
}
return totalVol > 0 ? totalCost / totalVol : 0;
}
}
import clr
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.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class martingale_ea_5_levels_strategy(Strategy):
"""
Martingale EA 5 Levels: SMA crossover entry (simplified, no martingale averaging in Python).
"""
def __init__(self):
super(martingale_ea_5_levels_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 20).SetDisplay("MA Period", "SMA period", "Indicators")
self._take_profit_pct = self.Param("TakeProfitPercent", 0.5).SetDisplay("TP %", "Take profit percent", "Risk")
self._stop_loss_pct = self.Param("StopLossPercent", 2.0).SetDisplay("SL %", "Stop loss percent", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candles", "General")
self._prev_close = None
self._prev_ma = None
self._entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(martingale_ea_5_levels_strategy, self).OnReseted()
self._prev_close = None
self._prev_ma = None
self._entry_price = 0.0
def OnStarted2(self, time):
super(martingale_ea_5_levels_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ma = float(sma_val)
tp_pct = float(self._take_profit_pct.Value) / 100.0
sl_pct = float(self._stop_loss_pct.Value) / 100.0
if self.Position > 0 and self._entry_price > 0:
if close >= self._entry_price * (1.0 + tp_pct) or close <= self._entry_price * (1.0 - sl_pct):
self.SellMarket()
self._entry_price = 0.0
self._prev_close = close
self._prev_ma = ma
return
elif self.Position < 0 and self._entry_price > 0:
if close <= self._entry_price * (1.0 - tp_pct) or close >= self._entry_price * (1.0 + sl_pct):
self.BuyMarket()
self._entry_price = 0.0
self._prev_close = close
self._prev_ma = ma
return
if self._prev_close is not None and self._prev_ma is not None and self.Position == 0:
if self._prev_close < self._prev_ma and close > ma:
self.BuyMarket()
self._entry_price = close
elif self._prev_close > self._prev_ma and close < ma:
self.SellMarket()
self._entry_price = close
self._prev_close = close
self._prev_ma = ma
def CreateClone(self):
return martingale_ea_5_levels_strategy()