在 GitHub 上查看

Martingale Bone Crusher 策略

概述

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

交易逻辑

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

参数

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

说明

  • 下单手数会按照交易品种的步长、最小手数与最大手数进行对齐。
  • 浮动盈亏的金额计算依赖品种的 PriceStepStepPrice。若二者为 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
	}
}