在 GitHub 上查看

Martingale Bone Crusher 策略

概述

Martingale Bone Crusher 策略 复刻了原始 MetaTrader 智能交易系统的核心逻辑。策略通过比较一快一慢两条简单移动平均线来确定做多或做空方向,并在出现亏损后采用马丁格尔资金管理模型放大下一笔订单的手数。策略同时提供了多种风险控制手段,包括固定金额止盈、百分比止盈、保本移动、以价格步长计量的传统止损/止盈以及以盈利金额为基础的移动止盈。

交易逻辑

  • 信号生成:在主图 K 线序列上计算两条简单移动平均线。当快线低于慢线时寻找做多信号;快线高于慢线时寻找做空信号;持仓未平仓前不会开出新的信号。
  • 马丁格尔序列:每当一笔交易结束,都会重新计算下一次下单手数。若上一笔交易亏损,则按照设置选择乘法放大或加法递增手数;若盈利,则手数恢复到初始值。
  • 模式选择:策略提供两个马丁格尔模式:
    • Martingale1:无论盈亏,下一笔始终跟随当前均线方向。
    • Martingale2:若上一笔亏损,则下一笔会反向开仓,复现原始 EA 的第二种逻辑。
  • 风险管理:持仓期间持续监控:
    • 以价格步长定义的固定止损与止盈;
    • 可选的价格追踪止损,使用固定步长追随极值;
    • 当价格向有利方向移动指定距离后自动将止损移至保本的功能;
    • 基于浮动盈亏的金额止盈与百分比止盈;
    • 当浮动盈利达到激活值后,按金额跟踪锁定收益的移动止盈。

参数

参数 说明
UseTakeProfitMoney 启用固定金额止盈。
TakeProfitMoney UseTakeProfitMoney 开启时触发平仓的金额。
UseTakeProfitPercent 启用以初始资金百分比衡量的止盈目标。
TakeProfitPercent UseTakeProfitPercent 开启时使用的百分比。
EnableTrailing 启用基于金额的移动止盈。
TrailingTakeProfitMoney 激活金额移动止盈所需的浮动收益。
TrailingStopMoney 金额移动止盈被激活后允许的利润回撤。
MartingaleModes 选择 Martingale1 或 Martingale2 的马丁格尔逻辑。
UseMoveToBreakeven 启用自动移至保本。
MoveToBreakevenTrigger 触发保本所需的价格步长数量。
BreakevenOffset 将止损移至保本时在入场价上添加的偏移。
Multiply DoubleLotSize 为 true 时,亏损后下一笔手数的倍增系数。
InitialVolume 首笔及获利后使用的基础下单手数。
DoubleLotSize 选择亏损后是使用乘法放大 (true) 还是加法递增 (false)。
LotSizeIncrement 当 DoubleLotSize 为 false 时,亏损后增加的手数。
TrailingStopSteps 价格追踪止损的步长。
StopLossSteps 传统止损的步长。
TakeProfitSteps 传统止盈的步长。
FastPeriod 快速简单移动平均的周期。
SlowPeriod 慢速简单移动平均的周期。
CandleType 指标计算所使用的 K 线类型。

说明

  • 下单手数会按照交易品种的步长、最小手数与最大手数进行对齐。
  • 浮动盈亏的金额计算依赖品种的 PriceStep 与 StepPrice。若二者为 0,则相关金额控制会自动跳过。
  • 同时提供 C# 和 Python 实现。
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 strategy that increases position size after a loss and manages risk using money targets and trailing stops.
/// </summary>
public class MartingaleBoneCrusherStrategy : Strategy
{
	private readonly StrategyParam<bool> _useTakeProfitMoney;
	private readonly StrategyParam<decimal> _takeProfitMoney;
	private readonly StrategyParam<bool> _useTakeProfitPercent;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<decimal> _trailingTakeProfitMoney;
	private readonly StrategyParam<decimal> _trailingStopMoney;
	private readonly StrategyParam<MartingaleModes> _martingaleMode;
	private readonly StrategyParam<bool> _useMoveToBreakeven;
	private readonly StrategyParam<decimal> _moveToBreakevenTrigger;
	private readonly StrategyParam<decimal> _breakevenOffset;
	private readonly StrategyParam<decimal> _multiply;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<bool> _doubleLotSize;
	private readonly StrategyParam<decimal> _lotSizeIncrement;
	private readonly StrategyParam<decimal> _trailingStopSteps;
	private readonly StrategyParam<decimal> _stopLossSteps;
	private readonly StrategyParam<decimal> _takeProfitSteps;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _fastMa;
	private SimpleMovingAverage _slowMa;
	private decimal _averagePrice;
	private decimal _positionVolume;
	private decimal _currentVolume;
	private decimal _lastOrderVolume;
	private decimal _lastTradeResult;
	private decimal _highestPrice;
	private decimal _lowestPrice;
	private decimal? _breakevenPrice;
	private decimal _maxFloatingProfit;
	private decimal _initialCapital;
	private Sides? _lastPositionSide;
	private Sides? _lastLosingSide;

	/// <summary>
	/// Initializes a new instance of <see cref="MartingaleBoneCrusherStrategy"/>.
	/// </summary>
	public MartingaleBoneCrusherStrategy()
	{
		_useTakeProfitMoney = Param(nameof(UseTakeProfitMoney), false)
			.SetDisplay("Use Money TP", "Enable fixed money take profit", "Risk Management");

		_takeProfitMoney = Param(nameof(TakeProfitMoney), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Money TP", "Take profit in money", "Risk Management");

		_useTakeProfitPercent = Param(nameof(UseTakeProfitPercent), false)
			.SetDisplay("Use Percent TP", "Enable percentage take profit", "Risk Management");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Percent TP", "Take profit percentage", "Risk Management");

		_enableTrailing = Param(nameof(EnableTrailing), true)
			.SetDisplay("Trailing Enabled", "Use money trailing stop", "Risk Management");

		_trailingTakeProfitMoney = Param(nameof(TrailingTakeProfitMoney), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Start", "Profit to activate trailing", "Risk Management");

		_trailingStopMoney = Param(nameof(TrailingStopMoney), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step", "Allowed profit pullback", "Risk Management");

		_martingaleMode = Param(nameof(MartingaleMode), MartingaleModes.Martingale2)
			.SetDisplay("Mode", "Martingale logic variant", "General");

		_useMoveToBreakeven = Param(nameof(UseMoveToBreakeven), true)
			.SetDisplay("Use Breakeven", "Enable breakeven stop", "Risk Management");

		_moveToBreakevenTrigger = Param(nameof(MoveToBreakevenTrigger), 10m)
			.SetNotNegative()
			.SetDisplay("Breakeven Trigger", "Steps to move stop", "Risk Management");

		_breakevenOffset = Param(nameof(BreakevenOffset), 5m)
			.SetNotNegative()
			.SetDisplay("Breakeven Offset", "Offset from entry", "Risk Management");

		_multiply = Param(nameof(Multiply), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Multiply", "Lot multiplier after loss", "Position Sizing");

		_initialVolume = Param(nameof(InitialVolume), 0.01m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Base order volume", "Position Sizing");

		_doubleLotSize = Param(nameof(DoubleLotSize), false)
			.SetDisplay("Double Volume", "Multiply volume after loss", "Position Sizing");

		_lotSizeIncrement = Param(nameof(LotSizeIncrement), 0.01m)
			.SetNotNegative()
			.SetDisplay("Lot Increment", "Volume increment after loss", "Position Sizing");

		_trailingStopSteps = Param(nameof(TrailingStopSteps), 30m)
			.SetNotNegative()
			.SetDisplay("Trailing Steps", "Trailing distance in steps", "Price Targets");

		_stopLossSteps = Param(nameof(StopLossSteps), 5m)
			.SetNotNegative()
			.SetDisplay("Stop Steps", "Stop-loss distance in steps", "Price Targets");

		_takeProfitSteps = Param(nameof(TakeProfitSteps), 5m)
			.SetNotNegative()
			.SetDisplay("Take Profit Steps", "Take-profit distance in steps", "Price Targets");

		_fastPeriod = Param(nameof(FastPeriod), 2)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA", "Fast moving average length", "Signals");

		_slowPeriod = Param(nameof(SlowPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA", "Slow moving average length", "Signals");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series", "General");
	}

	/// <summary>
	/// Enable fixed take profit in money.
	/// </summary>
	public bool UseTakeProfitMoney
	{
		get => _useTakeProfitMoney.Value;
		set => _useTakeProfitMoney.Value = value;
	}

	/// <summary>
	/// Take profit amount in money.
	/// </summary>
	public decimal TakeProfitMoney
	{
		get => _takeProfitMoney.Value;
		set => _takeProfitMoney.Value = value;
	}

	/// <summary>
	/// Enable take profit measured in percent.
	/// </summary>
	public bool UseTakeProfitPercent
	{
		get => _useTakeProfitPercent.Value;
		set => _useTakeProfitPercent.Value = value;
	}

	/// <summary>
	/// Percentage profit target.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	/// <summary>
	/// Enable trailing stop in money.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Profit required to activate money trailing.
	/// </summary>
	public decimal TrailingTakeProfitMoney
	{
		get => _trailingTakeProfitMoney.Value;
		set => _trailingTakeProfitMoney.Value = value;
	}

	/// <summary>
	/// Allowed profit pullback while trailing.
	/// </summary>
	public decimal TrailingStopMoney
	{
		get => _trailingStopMoney.Value;
		set => _trailingStopMoney.Value = value;
	}

	/// <summary>
	/// Selected martingale mode.
	/// </summary>
	public MartingaleModes MartingaleMode
	{
		get => _martingaleMode.Value;
		set => _martingaleMode.Value = value;
	}

	/// <summary>
	/// Enable automatic move to breakeven.
	/// </summary>
	public bool UseMoveToBreakeven
	{
		get => _useMoveToBreakeven.Value;
		set => _useMoveToBreakeven.Value = value;
	}

	/// <summary>
	/// Distance in steps required to activate breakeven.
	/// </summary>
	public decimal MoveToBreakevenTrigger
	{
		get => _moveToBreakevenTrigger.Value;
		set => _moveToBreakevenTrigger.Value = value;
	}

	/// <summary>
	/// Offset added to entry price when moving stop to breakeven.
	/// </summary>
	public decimal BreakevenOffset
	{
		get => _breakevenOffset.Value;
		set => _breakevenOffset.Value = value;
	}

	/// <summary>
	/// Multiplier applied to volume after a loss.
	/// </summary>
	public decimal Multiply
	{
		get => _multiply.Value;
		set => _multiply.Value = value;
	}

	/// <summary>
	/// Base order volume.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Use multiplication instead of addition for martingale.
	/// </summary>
	public bool DoubleLotSize
	{
		get => _doubleLotSize.Value;
		set => _doubleLotSize.Value = value;
	}

	/// <summary>
	/// Additional volume added after a loss when doubling is disabled.
	/// </summary>
	public decimal LotSizeIncrement
	{
		get => _lotSizeIncrement.Value;
		set => _lotSizeIncrement.Value = value;
	}

	/// <summary>
	/// Trailing distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopSteps
	{
		get => _trailingStopSteps.Value;
		set => _trailingStopSteps.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossSteps
	{
		get => _stopLossSteps.Value;
		set => _stopLossSteps.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitSteps
	{
		get => _takeProfitSteps.Value;
		set => _takeProfitSteps.Value = value;
	}

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </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();

		_fastMa = null;
		_slowMa = null;
		_averagePrice = 0m;
		_positionVolume = 0m;
		_currentVolume = AlignVolume(InitialVolume);
		_lastOrderVolume = _currentVolume;
		_lastTradeResult = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
		_breakevenPrice = null;
		_maxFloatingProfit = 0m;
		_initialCapital = 0m;
		_lastPositionSide = null;
		_lastLosingSide = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = AlignVolume(InitialVolume);
		_currentVolume = Volume;
		_lastOrderVolume = Volume;

		_initialCapital = Portfolio?.BeginValue ?? Portfolio?.CurrentValue ?? 0m;

		_fastMa = new SimpleMovingAverage { Length = FastPeriod };
		_slowMa = new SimpleMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_fastMa, _slowMa, ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_fastMa.IsFormed || !_slowMa.IsFormed)
			return;

		if (Position != 0)
		{
			UpdateExtremes(candle);

			if (TryApplyStopAndTake(candle))
				return;

			if (TryApplyBreakeven(candle.ClosePrice))
				return;

			if (TryApplyMoneyTargets(candle.ClosePrice))
				return;

			TryActivateBreakeven(candle.ClosePrice);
			return;
		}

		var entrySide = DetermineEntrySide(fastValue, slowValue);
		if (entrySide is null)
			return;

		var volume = AlignVolume(_currentVolume);
		if (volume <= 0m)
			return;

		if (entrySide == Sides.Buy)
			BuyMarket(volume);
		else
			SellMarket(volume);

		_averagePrice = candle.ClosePrice;
		_positionVolume = volume;
		_lastOrderVolume = volume;
		_lastPositionSide = entrySide;
		_highestPrice = candle.ClosePrice;
		_lowestPrice = candle.ClosePrice;
		_breakevenPrice = null;
		_maxFloatingProfit = 0m;
	}

	private Sides? DetermineEntrySide(decimal fastValue, decimal slowValue)
	{
		Sides? signal = null;
		if (fastValue < slowValue)
			signal = Sides.Buy;
		else if (fastValue > slowValue)
			signal = Sides.Sell;

		if (_lastTradeResult < 0m)
		{
			if (MartingaleMode == MartingaleModes.Martingale2 && _lastLosingSide.HasValue)
				return _lastLosingSide == Sides.Buy ? Sides.Sell : Sides.Buy;

			return signal;
		}

		return signal;
	}

	private bool TryApplyStopAndTake(ICandleMessage candle)
	{
		if (_positionVolume <= 0m || !_lastPositionSide.HasValue)
			return false;

		var stopDistance = StepsToPrice(StopLossSteps);
		var takeDistance = StepsToPrice(TakeProfitSteps);
		var trailingDistance = StepsToPrice(TrailingStopSteps);
		var closePrice = candle.ClosePrice;

		if (_lastPositionSide == Sides.Buy)
		{
			if (stopDistance > 0m && candle.LowPrice <= _averagePrice - stopDistance)
			{
				ClosePosition(_averagePrice - stopDistance);
				return true;
			}

			if (takeDistance > 0m && candle.HighPrice >= _averagePrice + takeDistance)
			{
				ClosePosition(_averagePrice + takeDistance);
				return true;
			}

			if (TrailingStopSteps > 0m && trailingDistance > 0m && closePrice <= _highestPrice - trailingDistance)
			{
				ClosePosition(closePrice);
				return true;
			}
		}
		else
		{
			if (stopDistance > 0m && candle.HighPrice >= _averagePrice + stopDistance)
			{
				ClosePosition(_averagePrice + stopDistance);
				return true;
			}

			if (takeDistance > 0m && candle.LowPrice <= _averagePrice - takeDistance)
			{
				ClosePosition(_averagePrice - takeDistance);
				return true;
			}

			if (TrailingStopSteps > 0m && trailingDistance > 0m && closePrice >= _lowestPrice + trailingDistance)
			{
				ClosePosition(closePrice);
				return true;
			}
		}

		return false;
	}

	private bool TryApplyBreakeven(decimal closePrice)
	{
		if (!UseMoveToBreakeven || !_breakevenPrice.HasValue || !_lastPositionSide.HasValue)
			return false;

		if (_lastPositionSide == Sides.Buy && closePrice <= _breakevenPrice.Value)
		{
			ClosePosition(closePrice);
			return true;
		}

		if (_lastPositionSide == Sides.Sell && closePrice >= _breakevenPrice.Value)
		{
			ClosePosition(closePrice);
			return true;
		}

		return false;
	}

	private bool TryApplyMoneyTargets(decimal closePrice)
	{
		var profit = GetFloatingProfit(closePrice);

		if (UseTakeProfitMoney && profit >= TakeProfitMoney)
		{
			ClosePosition(closePrice);
			return true;
		}

		if (UseTakeProfitPercent && _initialCapital > 0m)
		{
			var target = _initialCapital * TakeProfitPercent / 100m;
			if (profit >= target)
			{
				ClosePosition(closePrice);
				return true;
			}
		}

		if (EnableTrailing && profit > 0m)
		{
			if (profit >= TrailingTakeProfitMoney)
				_maxFloatingProfit = Math.Max(_maxFloatingProfit, profit);

			if (_maxFloatingProfit > 0m && _maxFloatingProfit - profit >= TrailingStopMoney)
			{
				ClosePosition(closePrice);
				return true;
			}
		}

		return false;
	}

	private void TryActivateBreakeven(decimal closePrice)
	{
		if (!UseMoveToBreakeven || _breakevenPrice.HasValue || !_lastPositionSide.HasValue)
			return;

		var trigger = StepsToPrice(MoveToBreakevenTrigger);
		if (trigger <= 0m)
			return;

		var offset = StepsToPrice(BreakevenOffset);
		if (_lastPositionSide == Sides.Buy)
		{
			if (closePrice >= _averagePrice + trigger)
				_breakevenPrice = _averagePrice + offset;
		}
		else if (closePrice <= _averagePrice - trigger)
		{
			_breakevenPrice = _averagePrice - offset;
		}
	}

	private decimal GetFloatingProfit(decimal currentPrice)
	{
		if (_positionVolume <= 0m || !_lastPositionSide.HasValue)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m || stepPrice <= 0m)
			return 0m;

		var direction = _lastPositionSide == Sides.Buy ? 1m : -1m;
		var priceDiff = (currentPrice - _averagePrice) * direction;
		var steps = priceDiff / priceStep;
		return steps * stepPrice * _positionVolume;
	}

	private void ClosePosition(decimal exitPrice)
	{
		if (Position > 0m)
			SellMarket(Position);
		else if (Position < 0m)
			BuyMarket(-Position);

		ComputeTradeResult(exitPrice);
		ResetPositionState();
		UpdateNextVolume();
	}

	private void ComputeTradeResult(decimal exitPrice)
	{
		if (_positionVolume <= 0m || !_lastPositionSide.HasValue)
		{
			_lastTradeResult = 0m;
			_lastLosingSide = null;
			return;
		}

		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m || stepPrice <= 0m)
		{
			_lastTradeResult = 0m;
			_lastLosingSide = null;
			return;
		}

		var direction = _lastPositionSide == Sides.Buy ? 1m : -1m;
		var priceDiff = (exitPrice - _averagePrice) * direction;
		var steps = priceDiff / priceStep;
		var pnl = steps * stepPrice * _positionVolume;

		_lastTradeResult = pnl;
		_lastLosingSide = pnl < 0m ? _lastPositionSide : null;
	}

	private void ResetPositionState()
	{
		_averagePrice = 0m;
		_positionVolume = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
		_breakevenPrice = null;
		_maxFloatingProfit = 0m;
		_lastPositionSide = null;
	}

	private void UpdateExtremes(ICandleMessage candle)
	{
		if (!_lastPositionSide.HasValue)
			return;

		if (_lastPositionSide == Sides.Buy)
		{
			if (candle.HighPrice > _highestPrice)
				_highestPrice = candle.HighPrice;
		}
		else
		{
			if (_lowestPrice == 0m || candle.LowPrice < _lowestPrice)
				_lowestPrice = candle.LowPrice;
		}
	}

	private void UpdateNextVolume()
	{
		decimal nextVolume;
		if (_lastTradeResult < 0m)
			nextVolume = DoubleLotSize ? _lastOrderVolume * Multiply : _lastOrderVolume + LotSizeIncrement;
		else
			nextVolume = InitialVolume;

		_currentVolume = AlignVolume(nextVolume);
		_lastOrderVolume = _currentVolume;
	}

	private decimal StepsToPrice(decimal steps)
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			return 0m;

		return steps * priceStep;
	}

	private decimal AlignVolume(decimal volume)
	{
		if (Security is null)
			return volume;

		var step = Security.VolumeStep ?? 0m;

		if (step > 0m)
		{
			var ratio = Math.Round(volume / step, MidpointRounding.AwayFromZero);
			if (ratio == 0m && volume > 0m)
				ratio = 1m;
			volume = ratio * step;
		}

		if (volume <= 0m)
			volume = InitialVolume;

		return volume;
	}

	/// <summary>
	/// Supported martingale variants.
	/// </summary>
	public enum MartingaleModes
	{
		/// <summary>
		/// Follow moving average direction after every loss.
		/// </summary>
		Martingale1,

		/// <summary>
		/// Reverse direction after a loss while using martingale sizing.
		/// </summary>
		Martingale2
	}
}