在 GitHub 上查看

区间跟随策略

概览

区间跟随策略在 StockSharp 高级 API 上复现了 MetaTrader 5 专家顾问 “Range Follower”。策略利用日线的平均真实波幅 (ATR) 作为波动性基准,只要价格在当日内远离日内高点或低点达到指定阈值,便开立一次突破交易。ATR 被划分为触发段与剩余段,触发段用于判断是否入场,剩余段用于设置止盈距离,从而完整保留了原始 EA 的设计。

交易逻辑

  1. 日内波动基准
    • 在日线数据上计算 20 周期 ATR,得到当前交易日的参考区间。
    • TriggerPercent 参数把 ATR 拆分为两部分:触发距离(入场阈值)以及剩余距离(止盈目标)。
  2. 区间跟踪
    • 通过日线订阅实时更新当前交易日的最高价与最低价。
    • Level1 行情提供最新买一与卖一,用于计算报价相对于日内极值的距离。
  3. 每日仅一次入场
    • 若买一价高于当日最低价的距离超过触发距离,且当天尚未建仓,则以市价买入。
    • 若卖一价低于当日最高价的距离超过触发距离,且当天尚未建仓,则以市价卖出。
    • 每个交易日只允许一次交易,日切换时会重置该标志。
  4. 止损与止盈
    • 多头仓位的止损设置在入场价下方一个触发距离,止盈设置在入场价上方一个剩余距离。
    • 空头仓位的止损设置在入场价上方一个触发距离,止盈设置在入场价下方一个剩余距离。
    • 通过 Level1 价格与蜡烛收盘共同监控,一旦触及任一价格立即平仓。
  5. 日度重置
    • 当出现新交易日的第一根蜡烛时,策略会平掉所有持仓,清空内部状态并重新加载 ATR。
    • 如果在当日初始化时发现实际区间已经超过触发距离,则当天暂停交易,以复制原 EA 的安全检查。

参数

名称 默认值 说明
CandleType 15 分钟蜡烛 用于识别交易日边界的工作级别。
TriggerPercent 60 ATR 中用于触发的百分比,必须处于 10 到 90 之间。
Volume 0.1 多空方向的市价下单数量。

风险管理

  • 止损与止盈均来源于同一 ATR 基准,回报风险比始终为 (100 - TriggerPercent) : TriggerPercent
  • 策略最多仅持有一个仓位,止损或止盈被触发后立即平仓,避免叠加敞口。
  • 调用 StartProtection() 以启用 StockSharp 的保护机制,便于额外挂载移动止损或组合风控模块。

实现细节

  • 通过单独的日线订阅与 AverageTrueRange 指标获取 ATR 数值,全部使用高阶 Bind 接口完成。
  • Level1 数据用于还原 EA 以 tick 为驱动的入场与离场判断,买一和卖一价格直接驱动交易决策。
  • 交易日边界来自工作级别蜡烛,确保不同交易日历下都能一致地重置策略状态。
  • 转换过程中未使用自定义指标缓存或循环历史数据,而是通过字段保存状态以符合项目规范。
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Range Follower strategy: ATR-based range breakout.
/// Tracks high/low range and enters when price breaks out by ATR threshold.
/// </summary>
public class RangeFollowerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<int> _rangePeriod;

	private decimal _rangeHigh;
	private decimal _rangeLow;
	private int _barCount;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public int RangePeriod { get => _rangePeriod.Value; set => _rangePeriod.Value = value; }

	public RangeFollowerStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR period", "Indicators");
		_rangePeriod = Param(nameof(RangePeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Range Period", "Bars for range calculation", "Indicators");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rangeHigh = 0m;
		_rangeLow = decimal.MaxValue;
		_barCount = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_rangeHigh = 0;
		_rangeLow = decimal.MaxValue;
		_barCount = 0;
		var atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(atr, ProcessCandle).Start();
	}

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

		var close = candle.ClosePrice;
		_barCount++;

		if (candle.HighPrice > _rangeHigh) _rangeHigh = candle.HighPrice;
		if (candle.LowPrice < _rangeLow) _rangeLow = candle.LowPrice;

		if (_barCount < RangePeriod) return;

		var threshold = atrValue * 0.5m;

		if (close > _rangeHigh - threshold && Position <= 0)
		{
			BuyMarket();
			ResetRange();
		}
		else if (close < _rangeLow + threshold && Position >= 0)
		{
			SellMarket();
			ResetRange();
		}

		// Slide the range window
		if (_barCount > RangePeriod * 2)
			ResetRange();
	}

	private void ResetRange()
	{
		_rangeHigh = 0;
		_rangeLow = decimal.MaxValue;
		_barCount = 0;
	}
}