在 GitHub 上查看

Renko Level EA 策略

概述

  • 将 MetaTrader 专家顾问 Renko Level EA.mq5 转换到 StockSharp 框架。
  • 通过 BrickSize 参数计算虚拟的 Renko 上下水平线,并跟踪它们的移动。
  • 使用 CandleType 定义的收盘价(默认 1 分钟 K 线)驱动逻辑,只在 K 线完成后做出决策。
  • 没有固定止损或止盈,所有离场都依靠反向信号完成。

交易逻辑

  1. 第一根完成的 K 线会把收盘价四舍五入到 Renko 网格,初始化上下两个水平。
  2. 随后的每根 K 线:
    • 收盘价位于区间内时,水平保持不变。
    • 收盘价突破上轨,Renko 方块向上平移到下一个格子。
    • 收盘价跌破下轨,Renko 方块向下移动。
  3. 只要上轨发生变化,就视为方向切换:
    • 上轨上升 → 多头信号(若 ReverseSignalstrue 则反向)。
    • 上轨下降 → 空头信号。
  4. ReverseSignalsAllowIncrease 分别对应原 EA 的反向和加仓开关,可灵活复现不同行为。

仓位管理

  • 做多前会先平掉所有空头仓位,做空前会先平掉多头仓位。
  • AllowIncrease = false 时,只有在当前方向没有持仓时才会再次下单。
  • AllowIncrease = true 时,即使已经持有同向仓位,也会按 OrderVolume 继续加仓。
  • 不设止损/止盈,仓位在出现反向信号时被平仓或反转。
  • 调用 StartProtection() 以启用框架内置的安全保护。

参数

名称 说明 默认值 可优化
BrickSize Security.PriceStep 为单位的 Renko 方块高度,决定触发信号所需的价格位移。 30 是(10 → 100,步长 10)
OrderVolume 每次市价单的下单数量。 1
ReverseSignals 交换多空方向,对应原始 EA 的 Reverse 选项。 false
AllowIncrease 允许在已有仓位的情况下继续加仓,对应 EA 的 Increase 参数。 false
CandleType 用于计算的 K 线类型,默认 1 分钟时间框,可替换为任意受支持的序列。 TimeFrameCandleMessage(1m)

实践提示

  • BrickSize 会自动乘以交易品种的最小报价步长,因此可用于外汇、期货和加密货币等不同市场。
  • 决策只依赖收盘价,盘中波动只有在形成最终收盘时才被纳入计算。
  • 同时启用 ReverseSignalsAllowIncrease 可以探索反趋势或金字塔加仓等变体。
  • 适合想要跟随 Renko 突破、不依赖额外指标过滤的策略研究。

分类

  • 策略类型:趋势跟随(Renko 突破)。
  • 方向:多/空。
  • 复杂度:中等(自定义水平跟踪,参数少)。
  • 止损:无,依靠反向信号退出。
  • 时间框架:由 CandleType 决定。
  • 指标:自建 Renko 水平。
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>
/// Strategy that mirrors the Renko Level Expert Advisor from MetaTrader.
/// Tracks level changes generated by a Renko style grid and flips positions accordingly.
/// </summary>
public class RenkoLevelEaStrategy : Strategy
{
	private readonly StrategyParam<int> _brickSize;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<bool> _allowIncrease;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _upperLevel;
	private decimal _lowerLevel;
	private decimal? _previousUpperLevel;
	private bool _levelsInitialized;

	/// <summary>
	/// Renko brick size expressed in price steps.
	/// </summary>
	public int BrickSize
	{
		get => _brickSize.Value;
		set => _brickSize.Value = value;
	}

	/// <summary>
	/// Volume for each executed market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// When enabled, long and short signals are swapped.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Allows adding to an existing position instead of waiting for a flat position.
	/// </summary>
	public bool AllowIncrease
	{
		get => _allowIncrease.Value;
		set => _allowIncrease.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate price movement.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="RenkoLevelEaStrategy"/>.
	/// </summary>
	public RenkoLevelEaStrategy()
	{
		_brickSize = Param(nameof(BrickSize), 3000)
			.SetGreaterThanZero()
			.SetDisplay("Brick Size", "Renko block size in price steps", "Renko Levels")
			
			.SetOptimize(10, 100, 10);

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume for market orders", "Trading");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert long and short actions", "Trading");

		_allowIncrease = Param(nameof(AllowIncrease), false)
			.SetDisplay("Allow Increase", "Allow adding to existing positions", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for calculations", "Data");
	}

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

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

		// Reset previously calculated Renko levels.
		_upperLevel = 0m;
		_lowerLevel = 0m;
		_previousUpperLevel = null;
		_levelsInitialized = false;
	}

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

		// Subscribe to candle data that feeds the Renko level logic.
		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(ProcessCandle)
			.Start();

		// Draw prices and trades if a chart is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

		// Enable built-in protection features.
		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Trade only on completed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure the strategy is ready to place trades.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		if (priceStep <= 0)
			priceStep = 1m;

		// Update Renko bounds with the latest closing price.
		if (!UpdateLevels(candle.ClosePrice, priceStep))
			return;

		// Skip the very first signal to mirror indicator warm-up.
		if (_previousUpperLevel == null)
		{
			_previousUpperLevel = _upperLevel;
			return;
		}

		// Proceed only if the Renko level actually changed.
		if (AreEqual(_previousUpperLevel.Value, _upperLevel, priceStep))
			return;

		var isUpMove = _upperLevel > _previousUpperLevel.Value;

		if (ReverseSignals)
			isUpMove = !isUpMove;

		if (isUpMove)
			HandleLongSignal();
		else
			HandleShortSignal();

		_previousUpperLevel = _upperLevel;
	}

	private bool UpdateLevels(decimal closePrice, decimal priceStep)
	{
		var stepCount = BrickSize;
		if (stepCount <= 0)
			return false;

		if (!_levelsInitialized)
		{
			CalculateBounds(closePrice, priceStep, stepCount, out var ceil, out var round, out var floor);
			_upperLevel = round;
			_lowerLevel = floor;
			_levelsInitialized = true;
			return true;
		}

		if (closePrice >= _lowerLevel && closePrice <= _upperLevel)
			return false;

		CalculateBounds(closePrice, priceStep, stepCount, out var newCeil, out var newRound, out var newFloor);

		if (closePrice < _lowerLevel)
		{
			if (AreEqual(newRound, _lowerLevel, priceStep))
				return false;

			_upperLevel = newCeil;
			_lowerLevel = newRound;
			return true;
		}

		if (closePrice > _upperLevel)
		{
			if (AreEqual(newRound, _upperLevel, priceStep))
				return false;

			_lowerLevel = newFloor;
			_upperLevel = newRound;
			return true;
		}

		return false;
	}

	private void CalculateBounds(decimal price, decimal priceStep, int stepCount, out decimal priceCeil, out decimal priceRound, out decimal priceFloor)
	{
		var normalizedStep = (decimal)stepCount;

		var ratio = price / priceStep / normalizedStep;
		var rounded = Math.Round(ratio, MidpointRounding.AwayFromZero);

		priceRound = (decimal)rounded * normalizedStep * priceStep;

		var ceilRatio = (priceRound + normalizedStep / 2m * priceStep) / priceStep / normalizedStep;
		var ceilCount = Math.Ceiling((double)ceilRatio);
		priceCeil = (decimal)ceilCount * normalizedStep * priceStep;

		var floorRatio = (priceRound - normalizedStep / 2m * priceStep) / priceStep / normalizedStep;
		var floorCount = Math.Floor((double)floorRatio);
		priceFloor = (decimal)floorCount * normalizedStep * priceStep;
	}

	private bool AreEqual(decimal left, decimal right, decimal priceStep)
	{
		var tolerance = priceStep / 2m;
		return Math.Abs(left - right) <= tolerance;
	}

	private void HandleLongSignal()
	{
		// Close the short side before flipping to long.
		if (Position < 0)
			ClosePosition();

		// Respect the increase toggle to avoid stacking positions unintentionally.
		if (!AllowIncrease && Position > 0)
			return;

		BuyMarket(OrderVolume);
	}

	private void HandleShortSignal()
	{
		// Close the long side before flipping to short.
		if (Position > 0)
			ClosePosition();

		// Respect the increase toggle for short accumulation.
		if (!AllowIncrease && Position < 0)
			return;

		SellMarket(OrderVolume);
	}
}