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);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class renko_level_ea_strategy(Strategy):
def __init__(self):
super(renko_level_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles for calculations", "Data")
self._brick_size = self.Param("BrickSize", 3000) \
.SetDisplay("Brick Size", "Renko block size in price steps", "Renko Levels")
self._reverse_signals = self.Param("ReverseSignals", False) \
.SetDisplay("Reverse Signals", "Invert long and short actions", "Trading")
self._upper_level = 0.0
self._lower_level = 0.0
self._previous_upper = None
self._levels_initialized = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def BrickSize(self):
return self._brick_size.Value
@property
def ReverseSignals(self):
return self._reverse_signals.Value
def OnReseted(self):
super(renko_level_ea_strategy, self).OnReseted()
self._upper_level = 0.0
self._lower_level = 0.0
self._previous_upper = None
self._levels_initialized = False
def OnStarted2(self, time):
super(renko_level_ea_strategy, self).OnStarted2(time)
self._upper_level = 0.0
self._lower_level = 0.0
self._previous_upper = None
self._levels_initialized = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if price_step <= 0:
price_step = 1.0
close = float(candle.ClosePrice)
if not self._update_levels(close, price_step):
return
if self._previous_upper is None:
self._previous_upper = self._upper_level
return
tolerance = price_step / 2.0
if abs(self._previous_upper - self._upper_level) <= tolerance:
return
is_up_move = self._upper_level > self._previous_upper
if self.ReverseSignals:
is_up_move = not is_up_move
if is_up_move:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
else:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
self._previous_upper = self._upper_level
def _update_levels(self, close, price_step):
step_count = self.BrickSize
if step_count <= 0:
return False
if not self._levels_initialized:
_, rnd, floor = self._calculate_bounds(close, price_step, step_count)
self._upper_level = rnd
self._lower_level = floor
self._levels_initialized = True
return True
if self._lower_level <= close <= self._upper_level:
return False
new_ceil, new_round, new_floor = self._calculate_bounds(close, price_step, step_count)
tolerance = price_step / 2.0
if close < self._lower_level:
if abs(new_round - self._lower_level) <= tolerance:
return False
self._upper_level = new_ceil
self._lower_level = new_round
return True
if close > self._upper_level:
if abs(new_round - self._upper_level) <= tolerance:
return False
self._lower_level = new_floor
self._upper_level = new_round
return True
return False
def _calculate_bounds(self, price, price_step, step_count):
import math
normalized_step = float(step_count)
ratio = price / price_step / normalized_step
# Use away-from-zero rounding to match C# Math.Round(MidpointRounding.AwayFromZero)
if ratio >= 0:
rounded = math.floor(ratio + 0.5)
else:
rounded = math.ceil(ratio - 0.5)
price_round = rounded * normalized_step * price_step
ceil_ratio = (price_round + normalized_step / 2.0 * price_step) / price_step / normalized_step
ceil_count = math.ceil(ceil_ratio)
price_ceil = ceil_count * normalized_step * price_step
floor_ratio = (price_round - normalized_step / 2.0 * price_step) / price_step / normalized_step
floor_count = math.floor(floor_ratio)
price_floor = floor_count * normalized_step * price_step
return (price_ceil, price_round, price_floor)
def CreateClone(self):
return renko_level_ea_strategy()