在 GitHub 上查看

N Trades Per Set Martingale 策略

概述

本策略完整复刻了 MetaTrader 智能交易系统 “N trades per set martingale + Close and reset on equity increase”。策略仅做多,但通过马丁加仓和基于权益的重置机制实现动态仓位管理。每次上一笔交易平仓后都会立即开立新的多单,因此始终保持在场内。

交易逻辑

  1. 顺序进场:只要当前没有持仓,就立刻以市价买入,并在成交后挂出止损与止盈。
  2. 胜负统计:持仓关闭后,将出场价与入场价比较。若为盈利则增加胜场计数,否则计入败场;打平被视为亏损,与原版 EA 完全一致。
  3. 结束一轮:同时跟踪本轮的交易数量。当计数达到 Trades Per Set 时,本轮结束,根据结果分为三种情况:
    • 全部盈利:使用当前权益除以 Equity Divisor 重新计算基础手数,然后清零各个计数器。
    • 全部亏损:将手数乘以 Scale Factor,随后清零计数器。
    • 混合结果:若胜负都有,则保持当前手数,仅清零计数器。
  4. 权益重置:当账户权益累计增长达到 Equity Increase 时触发全局重置:清零计数器,按照最新权益重新计算手数,并把权益目标向前平移同样的增量。

该流程与原始 EA 中 fxDreema 模块的连线完全一致。

参数说明

参数 说明
Trades Per Set 构成一轮马丁循环的交易数量。
Stop Loss (pips) 以价格跳动为单位的止损距离,填 0 表示关闭止损。
Take Profit (pips) 以价格跳动为单位的止盈距离,填 0 表示关闭止盈。
Scale Factor 全部亏损后用于放大下次交易手数的倍数,低于 1 的值会被限制为 1。
Equity Divisor 在盈利轮次或权益重置后,用于由权益计算基础手数的除数。
Equity Increase 触发全局重置所需的权益增量,填 0 则禁用权益重置。

资金管理

  • 手数会按照品种约束 (VolumeStepMinVolumeMaxVolume) 调整,与 MQL 中的 AlignLots 行为一致。
  • 当无法获得权益数据时,沿用上一笔的手数;如果是首笔交易,则退回到交易品种的最小步长。
  • 止损与止盈通过 PriceStep 转换成价格跳动数。如果品种未提供价格步长,则直接将输入值四舍五入到最近的整数。

使用提示

  • 策略只做多,与原版保持一致。若交易渠道允许做空,需要手动禁用空头交易。
  • 由于每次成交后都会重新设置保护单,部分成交能够自动继承相同的止损和止盈。
  • 权益重置在每次平仓后检查,请确保投资组合连接能提供实时权益,否则条件将无法触发。
namespace StockSharp.Samples.Strategies;

using System;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// N Trades Per Set Martingale: RSI-based entry with position tracking.
/// Buys when RSI oversold, sells when RSI overbought.
/// </summary>
public class NTradesPerSetMartingaleStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiPeriod;

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

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public NTradesPerSetMartingaleStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation period", "Indicators");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		decimal? prevRsi = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(rsi, (candle, rsiVal) =>
			{
				if (candle.State != CandleStates.Finished)
					return;

				if (!IsFormedAndOnlineAndAllowTrading())
					return;

				if (prevRsi.HasValue)
				{
					var crossBelowOversold = prevRsi.Value >= 30m && rsiVal < 30m;
					var crossAboveOverbought = prevRsi.Value <= 70m && rsiVal > 70m;

					if (crossBelowOversold && Position <= 0)
						BuyMarket();
					else if (crossAboveOverbought && Position >= 0)
						SellMarket();
				}

				prevRsi = rsiVal;
			})
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, rsi);
			DrawOwnTrades(area);
		}
	}
}