在 GitHub 上查看

RangeEA 周期网格策略

概览

RangeEA 周期网格策略是一套从 MetaTrader 专家顾问移植的限价网格系统。它会计算最近两周的最高价和最低价,从而获得 当前的周区间,并在区间内部按均匀间距布置预设数量的限价挂单。每张挂单的止损和止盈距离都会根据 挂单价格与当前价格之间的差距进行动态缩放,同时确保不少于指定的最小点数。当账户权益达到设定的 收益百分比时,策略会立即平仓并撤销所有挂单,以锁定利润。

策略通过 StockSharp 的高级 API 实现:使用蜡烛图驱动逻辑、借助内置方法管理挂单,并将所有重要参数暴露 出来以便优化。

交易流程

  1. 订阅两组蜡烛数据:
    • 可配置的交易周期(默认 1 小时),用于维护网格。
    • 周期为 1 周的蜡烛图,用于估算交易区间。
  2. 每当新的周蜡烛收盘时,更新最近两周的最高价与最低价,二者的差值即为当前网格范围。
  3. 在每根交易蜡烛收盘时执行以下步骤:
    • 检查是否处于允许交易的时间窗口(StartTradeHourEndTradeHour)。
    • 如果启用了每日重置,则在新交易日开始时清除旧的挂单。
    • 当没有任何限价挂单时,在区间内重新分布 NumberOfOrders 张挂单。
    • 如果已有至少两张挂单成交,当网格规模缩小到 NumberOfOrders - 2 张时,会在倒数第二个成交价位重新放置 一张挂单。
    • 持续监控账户权益,一旦达到 TargetPercentage 指定的收益率,就全部平仓并撤单。
  4. 当交易时间结束且 CloseAllAtEndTrade 为真时,策略会取消所有挂单并平掉持仓。

参数说明

名称 说明 默认值
CandleType 触发策略逻辑的交易蜡烛周期。 1 小时蜡烛
WeeklyCandleType 计算区间所使用的蜡烛周期。 1 周蜡烛
StartTradeHour 可以开始下单的小时数。 0
EndTradeHour 停止交易的小时数。 24
CloseAllAtEndTrade 交易时间之外是否平仓并撤单。 true
MaxOpenOrders 同时允许存在的订单和持仓数量上限。 5
NumberOfOrders 网格中的限价挂单数量。 10
OrderVolume 每张挂单的成交量。 0.01
ResetOrdersDaily 是否在每天开始时重建网格。 true
StopLossPoints 止损距离的最小点数。 60
TakeProfitPoints 止盈距离的最小点数。 60
StopLossMultiplier 动态止损距离的乘数。 3
TakeProfitMultiplier 动态止盈距离的乘数。 1
TargetPercentage 触发全部平仓的收益百分比。 8

风险控制

  • MaxOpenOrders 保证挂单和持仓数量不会无限增加。
  • 止损与止盈距离至少为设定的最小点数,并可通过乘数参数进一步放大。
  • 每日重置可以防止过期的挂单延续到新交易日。
  • 权益目标机制能够在达到收益目标后及时锁定利润。

备注

  • 需要确保标的提供周蜡烛数据,否则无法计算交易区间。
  • 如果标的的最小价格跳动与默认点值不同,应相应调整点数相关参数。
  • 可以通过优化 NumberOfOrdersOrderVolume 以及止损/止盈乘数来适配不同波动水平的市场。
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>
/// Range based grid strategy that detects the trading range from recent price action
/// and places buy/sell limit orders at grid levels within the range.
/// Buys at lower grid levels, sells at upper grid levels.
/// </summary>
public class RangeWeeklyGridStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rangePeriod;
	private readonly StrategyParam<int> _gridLevels;

	private decimal _rangeHigh;
	private decimal _rangeLow;
	private bool _rangeSet;
	private decimal _entryPrice;
	private DateTimeOffset _lastTradeTime;

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

	public int RangePeriod
	{
		get => _rangePeriod.Value;
		set => _rangePeriod.Value = value;
	}

	public int GridLevels
	{
		get => _gridLevels.Value;
		set => _gridLevels.Value = value;
	}

	public RangeWeeklyGridStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle type", "General");

		_rangePeriod = Param(nameof(RangePeriod), 100)
			.SetDisplay("Range Period", "Number of candles to determine range", "Logic");

		_gridLevels = Param(nameof(GridLevels), 5)
			.SetDisplay("Grid Levels", "Number of grid levels within the range", "Logic");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_rangeHigh = 0;
		_rangeLow = 0;
		_rangeSet = false;
		_entryPrice = 0;
		_lastTradeTime = default;
	}

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

		var highest = new Highest { Length = RangePeriod };
		var lowest = new Lowest { Length = RangePeriod };

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

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

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

		if (highestValue <= 0 || lowestValue <= 0 || highestValue <= lowestValue)
			return;

		_rangeHigh = highestValue;
		_rangeLow = lowestValue;
		_rangeSet = true;

		if (!_rangeSet)
			return;

		var range = _rangeHigh - _rangeLow;
		if (range <= 0)
			return;

		// Cooldown: at least 1 day between trades
		if (_lastTradeTime != default && candle.CloseTime - _lastTradeTime < TimeSpan.FromDays(1))
			return;

		var gridStep = range / (GridLevels + 1);
		var close = candle.ClosePrice;
		var mid = (_rangeHigh + _rangeLow) / 2;

		// Buy when price is in lower portion of range
		if (close <= _rangeLow + gridStep && Position <= 0)
		{
			BuyMarket();
			_entryPrice = close;
			_lastTradeTime = candle.CloseTime;
		}
		// Sell when price is in upper portion of range
		else if (close >= _rangeHigh - gridStep && Position >= 0)
		{
			SellMarket();
			_entryPrice = close;
			_lastTradeTime = candle.CloseTime;
		}
		// Take profit at mid-range
		else if (Position > 0 && close >= mid)
		{
			SellMarket();
			_lastTradeTime = candle.CloseTime;
		}
		else if (Position < 0 && close <= mid)
		{
			BuyMarket();
			_lastTradeTime = candle.CloseTime;
		}
	}
}