在 GitHub 上查看

RSI Martingale

概述

RSI Martingale 是 MetaTrader 5 智能交易系统 RSI&Martingale1.5 的移植版本。策略等待相对强弱指标(RSI)在可配置的历史窗口内创出极值,以捕捉动能反转。当出现新低或新高时,系统沿着预期的均值回归方向建仓,并在 RSI 穿越 50 中轴或触及固定止盈/止损时离场。可选的马丁格尔模块会在出现亏损后,以更大的手数在反向重新入场。每日盈亏限制以及按小时的时间过滤器,可以在高风险时段或达到收益目标后暂停交易。

策略逻辑

RSI 极值

  • 指标 – 单一 RSI,基于所选的蜡烛类型计算。在指标形成之前(历史数据不足时)不会发出信号。
  • 最低值 – 如果当前 RSI 小于或等于 Bars For Extremes 窗口内的所有 RSI 值,且低于 50,则开多单。
  • 最高值 – 如果当前 RSI 大于或等于窗口中的所有值,且高于 50,则开空单。

持仓管理

  • 离场规则 – 当 RSI 穿越 50 水平时平仓:多单在 RSI 上穿 50 时平仓,空单在 RSI 下破 50 时平仓。
  • 固定目标 – 止盈止损以点数表示。启用后,策略会比较最近一根 K 线的最高价/最低价与目标价位,若触及则立即平仓。
  • 手数对齐 – 每笔订单的手数在下单前都会按照交易品种的步长、最小和最大手数进行调整。

马丁格尔恢复

  • 触发条件 – 记录每次亏损平仓的方向和成交手数。
  • 重新入场 – 在下一根满足条件的 K 线上,如果当前没有持仓,可以立即在反向开仓。手数等于亏损单手数乘以 Martingale Multiplier,或在关闭马丁格尔时退回到 Initial Volume
  • 重置 – 提交马丁格尔订单后,亏损信息会被清除,以防重复触发。

日内资金控制

  • 基准 – 每个交易日开始时记录账户权益并清除暂停标志。
  • 监控窗口 – 只有在 Daily Control StartDaily Control End 之间才检查盈亏目标。
  • 暂停 – 当权益涨幅超过 Daily Profit % 或跌幅超过 Daily Loss % 时,策略会关闭所有持仓,并在当日剩余时间内停止开新仓。

交易时段过滤

  • 交易窗口 – 仅在当前小时位于 Trading StartTrading End 范围内时允许新开仓。
  • 避开时段 – 24 个布尔参数沿用原始 EA 的“避开新闻”设置,可在指定小时内禁止交易。

参数

  • Initial Volume – 默认开仓手数。
  • RSI Period – RSI 指标的周期长度。
  • Bars For Extremes – 用于检测 RSI 极值的完成 K 线数量。
  • Take Profit (pips) – 固定止盈距离,0 表示禁用。
  • Stop Loss (pips) – 固定止损距离,0 表示禁用。
  • Enable Martingale – 是否在亏损后启用马丁格尔加仓。
  • Martingale Multiplier – 马丁格尔模式下的手数乘数。
  • Daily Targets – 启用每日盈亏暂停逻辑。
  • Daily Profit % – 当日收益率超过该值后停止交易。
  • Daily Loss % – 当日回撤超过该值后停止交易。
  • Daily Control Start / Daily Control End – 计算日内盈亏目标的起止小时。
  • Trading Start / Trading End – 允许开仓的小时范围。
  • Avoid Hour 00 … Avoid Hour 23 – 在对应小时禁止交易。
  • Candle Type – 用于计算 RSI 的订阅蜡烛类型。

补充说明

  • 策略仅处理已经完成的 K 线,不会在单根 K 线内部响应。
  • 日内收益计算结合了策略的已实现盈亏和基于最新收盘价估算的浮动盈亏。
  • 本包仅提供 C# 版本,没有 Python 实现。
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// RSI extremes strategy with martingale recovery.
/// Buys when RSI is at a local minimum below 50, sells when RSI is at a local maximum above 50.
/// Closes on RSI crossing 50. Doubles volume after a losing trade.
/// </summary>
public class RSIMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _barsForCondition;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _recentRsi = new();
	private decimal _entryPrice;
	private int _direction; // 1=long, -1=short, 0=flat

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

	public int BarsForCondition
	{
		get => _barsForCondition.Value;
		set => _barsForCondition.Value = value;
	}

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

	public RSIMartingaleStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetDisplay("RSI Period", "RSI indicator period", "Indicator");

		_barsForCondition = Param(nameof(BarsForCondition), 10)
			.SetDisplay("Bars For Extremes", "Number of RSI values to check for extremes", "Indicator");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "Data");
	}

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

		var rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(rsi, ProcessCandle)
			.Start();

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

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

		_recentRsi.Add(rsiValue);
		if (_recentRsi.Count > BarsForCondition)
			_recentRsi.RemoveAt(0);

		if (_recentRsi.Count < BarsForCondition)
			return;

		// Check exit: close on RSI crossing 50
		if (_direction > 0 && rsiValue > 50)
		{
			SellMarket();
			_direction = 0;
			return;
		}
		else if (_direction < 0 && rsiValue < 50)
		{
			BuyMarket();
			_direction = 0;
			return;
		}

		if (Position != 0)
			return;

		// Check if current RSI is local minimum (oversold entry)
		if (IsLocalMinimum() && rsiValue < 50 && Position <= 0)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_direction = 1;
		}
		// Check if current RSI is local maximum (overbought entry)
		else if (IsLocalMaximum() && rsiValue > 50 && Position >= 0)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_direction = -1;
		}
	}

	private bool IsLocalMinimum()
	{
		if (_recentRsi.Count < 2)
			return false;

		var current = _recentRsi[_recentRsi.Count - 1];
		for (var i = 0; i < _recentRsi.Count - 1; i++)
		{
			if (current > _recentRsi[i])
				return false;
		}
		return true;
	}

	private bool IsLocalMaximum()
	{
		if (_recentRsi.Count < 2)
			return false;

		var current = _recentRsi[_recentRsi.Count - 1];
		for (var i = 0; i < _recentRsi.Count - 1; i++)
		{
			if (current < _recentRsi[i])
				return false;
		}
		return true;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_recentRsi.Clear();
		_entryPrice = 0;
		_direction = 0;

		base.OnReseted();
	}
}