在 GitHub 上查看

Precipice Martin 策略 (C#)

概述

Precipice Martin 是一种机械化的网格策略,在每根完成的K线收盘时都会开出一笔市价单。原始的 MetaTrader 5 程序会在每根新K线出现时同时建立多单和空单,并使用以点数表示的固定止损和止盈。只要出现亏损交易,下一个订单的手数就会按照指定的马丁系数放大;盈利交易则把手数恢复到最小值。

本 C# 版本基于 StockSharp 的高阶 API 实现相同的思路。对每根完结的K线,策略会:

  1. 检查当前持仓,如果K线的最高价或最低价触及了设置的止损/止盈,就立即平仓。
  2. 在空仓状态下(同时启用多空方向时)轮流开多或开空,以模拟原策略的双向进场,同时兼容 StockSharp 的净头寸模型。
  3. 可选地启用马丁加仓逻辑,使连续亏损时订单手数逐步放大。
  4. 根据标的的最小价格跳动,将用户输入的点数转换为绝对价格偏移,用于计算止损和止盈。

转换说明

  • 原版程序会在同一根K线上同时持有多头和空头。由于 StockSharp 默认采用净头寸模式,移植版在每次空仓时交替选择方向,从而避免立即互相对冲,同时长期来看仍然会在两个方向上交易。
  • 止损和止盈由策略内部管理。当检测到K线突破相应的价格水平时,使用市价单平仓,并把盈亏结果传递给马丁逻辑。
  • 手数检查复刻了 MQL5 中的 LotCheck 函数:将计算出的手数按交易所的最小变动单位进行取整,并限制在允许的最小/最大范围内;如果取整后变为 0,则放弃下单。
  • 马丁乘数与 CalculateLot 等价:任一非盈利结果都会把乘数乘以 MartingaleCoefficient,盈利则重置为 1。

参数

参数 说明
Use Buy 是否允许开多单。
Buy SL/TP (pips) 多单的止损和止盈距离(点)。为 0 时表示该方向不设置固定退出。
Use Sell 是否允许开空单。
Sell SL/TP (pips) 空单的止损和止盈距离(点)。
Use Martingale 是否启用马丁加仓。关闭时所有订单都使用最小手数。
Martingale Coefficient 每次亏损后乘以的马丁系数。
Candle Type 策略处理的K线类型(时间周期),默认使用 1 分钟,可根据需要调整。

交易逻辑

  1. 点值计算:根据标的的价格最小跳动计算一个标准点。对于五位小数报价,1 点等于 10 个最小跳动,与 MT5 保持一致。
  2. 方向选择:如果同时启用多空方向,在完全空仓后会交替开多或开空;若只启用了一个方向,则始终按照该方向下单。
  3. 目标价格:开仓后立即记录止损和止盈的绝对价格,距离等于点数乘以点值。为 0 时表示该方向没有固定退出。
  4. 退出条件:每根完成的K线都会检查最高价/最低价是否突破了记录的目标,若触发则以市价平仓,数量等于最近一次进场的手数。
  5. 马丁加仓:下一次的下单量 = 最小手数 × 当前马丁乘数。亏损(包括打平)会把乘数乘以 MartingaleCoefficient,盈利则把乘数重置为 1。下单前会先按交易所的手数步长取整。
  6. 风控校验:如果取整后手数低于最小手数,订单会被忽略,以避免出现资金不足等错误。

使用建议

  1. Candle Type 参数设置与原策略相同的时间周期。
  2. 根据标的波动调整止损和止盈的点数,注意最终应用的是绝对价格距离。
  3. 如需启用马丁加仓,请慎重设置系数,避免在连续亏损时手数过快膨胀。
  4. 请在有实时K线数据的标的上运行策略;算法只会在K线收盘后做出决策。
  5. 保持对保证金占用的监控。本移植版本同一时间只有一个净头寸,但马丁加仓仍可能迅速放大仓位。

与 MT5 版本的差异

  • 净头寸模式:交替进场取代了原本的双向同时持仓。若需要真正的对冲,可分别运行两个策略实例(一个只做多,另一个只做空)。
  • 订单管理:不在交易所挂出止损/止盈委托,而是通过内部逻辑判断是否触发,并用市价单离场。
  • 历史遍历:原脚本每次都会遍历历史成交记录以确定马丁乘数,C# 版本则在每次平仓后即时更新乘数,降低计算量。

风险提示

马丁策略在连续亏损时会快速放大仓位,可能造成账户风险急剧上升。请在模拟或历史数据上充分测试,并根据自身风险承受能力选择合适的点数和马丁系数。

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>
/// Grid style strategy that opens a position on every new bar with optional martingale sizing.
/// </summary>
public class PrecipiceMartinStrategy : Strategy
{
	private readonly StrategyParam<bool> _useBuy;
	private readonly StrategyParam<int> _buyStepPips;
	private readonly StrategyParam<bool> _useSell;
	private readonly StrategyParam<int> _sellStepPips;
	private readonly StrategyParam<bool> _useMartingale;
	private readonly StrategyParam<decimal> _martingaleCoefficient;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _martingaleMultiplier;
	private decimal? _longEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortEntryPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal _lastLongVolume;
	private decimal _lastShortVolume;
	private bool _preferLongEntry;

	public bool UseBuy
	{
		get => _useBuy.Value;
		set => _useBuy.Value = value;
	}

	public int BuyStepPips
	{
		get => _buyStepPips.Value;
		set => _buyStepPips.Value = value;
	}

	public bool UseSell
	{
		get => _useSell.Value;
		set => _useSell.Value = value;
	}

	public int SellStepPips
	{
		get => _sellStepPips.Value;
		set => _sellStepPips.Value = value;
	}

	public bool UseMartingale
	{
		get => _useMartingale.Value;
		set => _useMartingale.Value = value;
	}

	public decimal MartingaleCoefficient
	{
		get => _martingaleCoefficient.Value;
		set => _martingaleCoefficient.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PrecipiceMartinStrategy()
	{
		_useBuy = Param(nameof(UseBuy), true)
			.SetDisplay("Use Buy", "Enable opening long positions", "Trading");
		_buyStepPips = Param(nameof(BuyStepPips), 89)
			.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading");
		_useSell = Param(nameof(UseSell), true)
			.SetDisplay("Use Sell", "Enable opening short positions", "Trading");
		_sellStepPips = Param(nameof(SellStepPips), 89)
			.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading");
		_useMartingale = Param(nameof(UseMartingale), true)
			.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing");
		_martingaleCoefficient = Param(nameof(MartingaleCoefficient), 1.6m)
			.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
			.SetGreaterThanZero();
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_martingaleMultiplier = 1m;
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastLongVolume = 0m;
		_lastShortVolume = 0m;
		_preferLongEntry = true;
	}

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

		// Calculate the pip size based on the instrument tick size.
		_pipSize = (Security?.PriceStep ?? 1m) * 10m;
		if (_pipSize <= 0m)
			_pipSize = Security?.PriceStep ?? 1m;
		if (_pipSize <= 0m)
			_pipSize = 1m;

		_martingaleMultiplier = 1m;

		// Subscribe to candle data and process every completed bar.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ignore unfinished candles because the original strategy trades on bar close.
		if (candle.State != CandleStates.Finished)
			return;

		// Manage exits before looking for new entries.
		var closedLong = TryCloseLong(candle);
		var closedShort = TryCloseShort(candle);

		// Do not open new trades while any position is still active.
		if (Position != 0)
			return;

		// Avoid immediate re-entry for a direction that has just closed on this bar.
		if (closedLong)
			return;
		if (closedShort)
			return;

		if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
			return;

		if (UseBuy && UseSell)
		{
			if (_preferLongEntry)
			{
				if (TryEnterLong(candle))
				{
					_preferLongEntry = false;
					return;
				}

				if (TryEnterShort(candle))
				{
					_preferLongEntry = false;
				}
			}
			else
			{
				if (TryEnterShort(candle))
				{
					_preferLongEntry = true;
					return;
				}

				if (TryEnterLong(candle))
				{
					_preferLongEntry = true;
				}
			}
		}
		else
		{
			if (UseBuy)
			{
				TryEnterLong(candle);
			}

			if (UseSell)
			{
				TryEnterShort(candle);
			}
		}
	}

	private bool TryEnterLong(ICandleMessage candle)
	{
		// Prevent duplicate long entries.
		if (_longEntryPrice.HasValue)
			return false;

		// Ensure no net position exists before opening a new long.
		if (Position != 0)
			return false;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return false;

		var entryPrice = candle.ClosePrice;

		Volume = volume;
		BuyMarket();

		_longEntryPrice = entryPrice;
		_lastLongVolume = volume;

		if (BuyStepPips > 0)
		{
			var offset = BuyStepPips * _pipSize;
			_longStopPrice = entryPrice - offset;
			_longTakePrice = entryPrice + offset;
		}
		else
		{
			_longStopPrice = null;
			_longTakePrice = null;
		}

		return true;
	}

	private bool TryEnterShort(ICandleMessage candle)
	{
		// Prevent duplicate short entries.
		if (_shortEntryPrice.HasValue)
			return false;

		// Ensure no net position exists before opening a new short.
		if (Position != 0)
			return false;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return false;

		var entryPrice = candle.ClosePrice;

		Volume = volume;
		SellMarket();

		_shortEntryPrice = entryPrice;
		_lastShortVolume = volume;

		if (SellStepPips > 0)
		{
			var offset = SellStepPips * _pipSize;
			_shortStopPrice = entryPrice + offset;
			_shortTakePrice = entryPrice - offset;
		}
		else
		{
			_shortStopPrice = null;
			_shortTakePrice = null;
		}

		return true;
	}

	private bool TryCloseLong(ICandleMessage candle)
	{
		if (!_longEntryPrice.HasValue)
			return false;

		var volume = Position;
		if (volume <= 0m)
			volume = _lastLongVolume;

		if (volume <= 0m)
			return false;

		var stopHit = _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value;
		var takeHit = _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value;

		if (!stopHit && !takeHit)
			return false;

		var exitPrice = stopHit ? _longStopPrice!.Value : _longTakePrice!.Value;

		SellMarket();

		var pnl = (exitPrice - _longEntryPrice.Value) * volume;
		UpdateMartingale(pnl);

		ResetLongState();
		return true;
	}

	private bool TryCloseShort(ICandleMessage candle)
	{
		if (!_shortEntryPrice.HasValue)
			return false;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			volume = _lastShortVolume;

		if (volume <= 0m)
			return false;

		var stopHit = _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value;
		var takeHit = _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value;

		if (!stopHit && !takeHit)
			return false;

		var exitPrice = stopHit ? _shortStopPrice!.Value : _shortTakePrice!.Value;

		BuyMarket();

		var pnl = (_shortEntryPrice.Value - exitPrice) * volume;
		UpdateMartingale(pnl);

		ResetShortState();
		return true;
	}

	private decimal CalculateOrderVolume()
	{
		var minVolume = Security?.MinVolume ?? Volume;
		if (minVolume <= 0m)
			minVolume = 1m;

		var multiplier = UseMartingale ? _martingaleMultiplier : 1m;
		var volume = minVolume * multiplier;

		return AdjustVolume(volume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			var steps = Math.Truncate(volume / step.Value);
			volume = steps * step.Value;
		}

		var min = Security?.MinVolume;
		if (min.HasValue && min.Value > 0m && volume < min.Value)
			volume = 0m;

		var max = Security?.MaxVolume;
		if (max.HasValue && max.Value > 0m && volume > max.Value)
			volume = max.Value;

		return volume;
	}

	private void UpdateMartingale(decimal realizedPnl)
	{
		if (!UseMartingale)
		{
			_martingaleMultiplier = 1m;
			return;
		}

		// Reset the multiplier after profitable trades and scale up after losses.
		_martingaleMultiplier = realizedPnl > 0m
			? 1m
			: _martingaleMultiplier * MartingaleCoefficient;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_lastLongVolume = 0m;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastShortVolume = 0m;
	}
}