Zs1 Forex Instruments Strategy
This strategy reproduces the hedged grid logic of the MetaTrader expert Zs1_www_forex-instruments_info. The algorithm opens a simultaneous buy/sell pair, monitors how far price travels from the starting point and reacts to five discrete trading zones. The surviving leg of the hedge is averaged with martingale multipliers while the basket is protected by an equity-based exit.
Core behaviour
- Open an initial market hedge (one buy and one sell) with the configured base volume.
- Once either leg becomes profitable, close it and keep the losing side as the anchor order.
- Track price displacement using the
Orders Space (pips)parameter. When a new zone is reached, execute the same branching logic as the original expert:- Zone −2: close the basket on profit, otherwise average against the move.
- Zone −1: add a position opposite to the initial anchor.
- Zone 0: add a position in the direction of the anchor.
- Zone +1: close the basket on profit, otherwise open the opposite side.
- Whenever three or more trades are active, immediately exit if the floating profit is non-negative.
- After all positions are closed the cycle restarts automatically.
Parameters
| Name | Description |
|---|---|
Orders Space (pips) |
Distance in pips between adjacent grid levels. |
Zone Offset (pips) |
Extra buffer that must be breached before a new zone is confirmed. |
Initial Volume |
Base volume used for the opening hedge and for martingale scaling. |
Notes
- The martingale multipliers follow the original tunnel sequence (1, 3, 6, 12, ...).
- Volume validation respects the security's minimum, maximum and step constraints before sending any order.
- All decisions are driven by best bid/ask updates from Level1 data, matching the tick-based logic of the MQL version.
namespace StockSharp.Samples.Strategies;
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Hedged grid strategy converted from the MetaTrader expert "Zs1_www_forex-instruments_info".
/// The strategy opens an initial buy/sell pair, tracks price zones relative to the starting level
/// and adds or closes positions according to the original tunnel logic.
/// </summary>
public class Zs1ForexInstrumentsStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _ordersSpacePips;
private readonly StrategyParam<int> _pkPips;
private readonly List<Entry> _longEntries = new();
private readonly List<Entry> _shortEntries = new();
private decimal _pipValue;
private decimal _firstPrice;
private int _zone;
private int _lastZone;
private bool _zoneChanged;
private int _firstStage;
private Sides? _firstOrderDirection;
private Sides? _lastOrderDirection;
private bool _isClosingAll;
private decimal _currentPrice;
private bool _hasPriceData;
private sealed class Entry
{
public Entry(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; set; }
public decimal Volume { get; set; }
}
/// <summary>
/// Candle type used to drive the grid logic.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Distance in pips between consecutive price zones.
/// </summary>
public decimal OrdersSpacePips
{
get => _ordersSpacePips.Value;
set => _ordersSpacePips.Value = value;
}
/// <summary>
/// Additional pip offset used when detecting new zones.
/// </summary>
public int PkPips
{
get => _pkPips.Value;
set => _pkPips.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Zs1ForexInstrumentsStrategy"/> class.
/// </summary>
public Zs1ForexInstrumentsStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle type", "Candle timeframe for price sampling.", "General");
_ordersSpacePips = Param(nameof(OrdersSpacePips), 500m)
.SetGreaterThanZero()
.SetDisplay("Orders Space (pips)", "Distance between successive grid levels.", "Trading")
.SetOptimize(100m, 2000m, 100m);
_pkPips = Param(nameof(PkPips), 10)
.SetNotNegative()
.SetDisplay("Zone Offset (pips)", "Additional offset applied when checking zone boundaries.", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longEntries.Clear();
_shortEntries.Clear();
_pipValue = 0m;
_firstPrice = 0m;
_zone = 0;
_lastZone = 0;
_zoneChanged = false;
_firstStage = 0;
_firstOrderDirection = null;
_lastOrderDirection = null;
_isClosingAll = false;
_currentPrice = 0m;
_hasPriceData = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipValue = CalculatePipValue();
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_currentPrice = candle.ClosePrice;
_hasPriceData = true;
if (!_hasPriceData || _currentPrice <= 0m)
return;
if (_isClosingAll)
return;
var ordersTotal = GetOrdersTotal();
if (_firstStage != 0)
CheckZone();
if (_firstStage == 0 && ordersTotal == 0)
{
OpenFirst();
ordersTotal = GetOrdersTotal();
}
if (_zoneChanged)
{
ProcessZoneChange();
}
if (ordersTotal >= 3 && CalculateFloatingProfit() >= 0m)
{
CloseAllOrders();
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade == null)
return;
var volume = trade.Trade.Volume;
var price = trade.Trade.Price;
var side = trade.Order?.Side;
if (side == null)
return;
// Determine intent based on position context
if (side == Sides.Buy)
{
if (Position < 0 || _isClosingAll)
{
// Closing short
ReduceEntries(_shortEntries, volume);
}
else
{
// Opening long
_longEntries.Add(new Entry(price, volume));
_lastOrderDirection = Sides.Buy;
}
}
else
{
if (Position > 0 || _isClosingAll)
{
// Closing long
ReduceEntries(_longEntries, volume);
}
else
{
// Opening short
_shortEntries.Add(new Entry(price, volume));
_lastOrderDirection = Sides.Sell;
}
}
if (_firstStage == 1 && _firstPrice == 0m && _longEntries.Count > 0 && _shortEntries.Count > 0)
{
var longPrice = _longEntries[0].Price;
var shortPrice = _shortEntries[0].Price;
_firstPrice = (longPrice + shortPrice) / 2m;
}
if (!_longEntries.Any() && !_shortEntries.Any() && _isClosingAll)
{
ResetState();
_isClosingAll = false;
}
}
private void ProcessZoneChange()
{
switch (_firstStage)
{
case 1:
ZoneF1();
break;
case 2 when _firstOrderDirection == Sides.Buy:
switch (_zone)
{
case -2:
ZoneMinusTwo();
break;
case -1:
ZoneMinusOne();
break;
case 0:
ZoneZero();
break;
case 1:
case 2:
ZonePlusOne();
break;
}
break;
case 2 when _firstOrderDirection == Sides.Sell:
switch (_zone)
{
case 2:
ZoneMinusTwo();
break;
case 1:
ZoneMinusOne();
break;
case 0:
ZoneZero();
break;
case -1:
case -2:
ZonePlusOne();
break;
}
break;
}
}
private void ZoneF1()
{
_zoneChanged = false;
CloseFirstOrders();
}
private void ZoneMinusTwo()
{
_zoneChanged = false;
if (CalculateFloatingProfit() > 0m)
{
CloseAllOrders();
}
else
{
OpenAnother();
}
}
private void ZoneMinusOne()
{
_zoneChanged = false;
if (_firstOrderDirection == null)
return;
if (_firstOrderDirection == Sides.Buy)
{
OpenSellOrder();
}
else
{
OpenBuyOrder();
}
}
private void ZoneZero()
{
_zoneChanged = false;
if (_firstOrderDirection == null)
return;
if (_firstOrderDirection == Sides.Buy)
{
OpenBuyOrder();
}
else
{
OpenSellOrder();
}
}
private void ZonePlusOne()
{
_zoneChanged = false;
if (CalculateFloatingProfit() > 0m)
{
CloseAllOrders();
}
else
{
OpenAnother();
}
}
private void OpenFirst()
{
BuyMarket();
SellMarket();
_firstStage = 1;
_zone = 0;
_lastZone = 0;
_zoneChanged = false;
_firstPrice = _currentPrice;
_firstOrderDirection = null;
_lastOrderDirection = null;
}
private void CloseFirstOrders()
{
if (_longEntries.Count > 0 && _currentPrice > _longEntries[0].Price)
{
// Long is profitable, close it, keep short
SellMarket();
_firstStage = 2;
_firstOrderDirection = Sides.Sell;
_lastOrderDirection = Sides.Sell;
return;
}
if (_shortEntries.Count > 0 && _currentPrice < _shortEntries[0].Price)
{
// Short is profitable, close it, keep long
BuyMarket();
_firstStage = 2;
_firstOrderDirection = Sides.Buy;
_lastOrderDirection = Sides.Buy;
}
}
private void OpenBuyOrder()
{
BuyMarket();
}
private void OpenSellOrder()
{
SellMarket();
}
private void OpenAnother()
{
if (_lastOrderDirection == Sides.Buy)
{
OpenSellOrder();
}
else if (_lastOrderDirection == Sides.Sell)
{
OpenBuyOrder();
}
else if (_firstOrderDirection == Sides.Buy)
{
OpenSellOrder();
}
else if (_firstOrderDirection == Sides.Sell)
{
OpenBuyOrder();
}
}
private void CloseAllOrders()
{
if (_isClosingAll)
return;
_zoneChanged = false;
_isClosingAll = true;
// Close by selling longs and buying back shorts
if (_longEntries.Any())
{
var totalLong = _longEntries.Sum(e => e.Volume);
if (totalLong > 0m)
SellMarket(totalLong);
}
if (_shortEntries.Any())
{
var totalShort = _shortEntries.Sum(e => e.Volume);
if (totalShort > 0m)
BuyMarket(totalShort);
}
if (!_longEntries.Any() && !_shortEntries.Any())
{
ResetState();
_isClosingAll = false;
}
}
private void ResetState()
{
_longEntries.Clear();
_shortEntries.Clear();
_zone = 0;
_lastZone = 0;
_zoneChanged = false;
_firstStage = 0;
_firstOrderDirection = null;
_lastOrderDirection = null;
_firstPrice = 0m;
}
private void CheckZone()
{
var step = OrdersSpacePips * _pipValue;
if (step <= 0m || _firstPrice <= 0m)
return;
var offset = PkPips * _pipValue;
var price = _currentPrice + offset;
if (price >= _firstPrice + step * (_zone + 1))
{
_lastZone = _zone;
_zone++;
_zoneChanged = true;
}
else if (price <= _firstPrice - step * (1 - _zone))
{
_lastZone = _zone;
_zone--;
_zoneChanged = true;
}
if (_zoneChanged && _zone == _lastZone)
{
_zoneChanged = false;
}
}
private int GetOrdersTotal()
{
return _longEntries.Count + _shortEntries.Count;
}
private decimal CalculateFloatingProfit()
{
if (!_hasPriceData)
return 0m;
decimal profit = 0m;
foreach (var entry in _longEntries)
{
profit += (_currentPrice - entry.Price) * entry.Volume;
}
foreach (var entry in _shortEntries)
{
profit += (entry.Price - _currentPrice) * entry.Volume;
}
return profit;
}
private void ReduceEntries(List<Entry> entries, decimal volume)
{
var remaining = volume;
while (remaining > 0m && entries.Count > 0)
{
var current = entries[0];
if (current.Volume <= remaining + 0.0001m)
{
remaining -= current.Volume;
entries.RemoveAt(0);
}
else
{
current.Volume -= remaining;
remaining = 0m;
}
}
}
private decimal CalculatePipValue()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
return step;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class Entry(object):
def __init__(self, price, volume):
self.Price = price
self.Volume = volume
class zs1_forex_instruments_strategy(Strategy):
"""Hedged grid strategy converted from MetaTrader 'Zs1_www_forex-instruments_info'.
Opens an initial buy/sell pair, tracks price zones relative to the starting level,
and adds or closes positions according to tunnel logic."""
def __init__(self):
super(zs1_forex_instruments_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle type", "Candle timeframe for price sampling.", "General")
self._orders_space_pips = self.Param("OrdersSpacePips", 500.0) \
.SetGreaterThanZero() \
.SetDisplay("Orders Space (pips)", "Distance between successive grid levels.", "Trading")
self._pk_pips = self.Param("PkPips", 10) \
.SetDisplay("Zone Offset (pips)", "Additional offset applied when checking zone boundaries.", "Trading")
self._long_entries = []
self._short_entries = []
self._pip_value = 0.0
self._first_price = 0.0
self._zone = 0
self._last_zone = 0
self._zone_changed = False
self._first_stage = 0
self._first_order_direction = None
self._last_order_direction = None
self._is_closing_all = False
self._current_price = 0.0
self._has_price_data = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def OrdersSpacePips(self):
return self._orders_space_pips.Value
@OrdersSpacePips.setter
def OrdersSpacePips(self, value):
self._orders_space_pips.Value = value
@property
def PkPips(self):
return self._pk_pips.Value
@PkPips.setter
def PkPips(self, value):
self._pk_pips.Value = value
def OnReseted(self):
super(zs1_forex_instruments_strategy, self).OnReseted()
self._long_entries = []
self._short_entries = []
self._pip_value = 0.0
self._first_price = 0.0
self._zone = 0
self._last_zone = 0
self._zone_changed = False
self._first_stage = 0
self._first_order_direction = None
self._last_order_direction = None
self._is_closing_all = False
self._current_price = 0.0
self._has_price_data = False
def OnStarted2(self, time):
super(zs1_forex_instruments_strategy, self).OnStarted2(time)
self._pip_value = self._calculate_pip_value()
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._current_price = float(candle.ClosePrice)
self._has_price_data = True
if not self._has_price_data or self._current_price <= 0.0:
return
if self._is_closing_all:
return
orders_total = self._get_orders_total()
if self._first_stage != 0:
self._check_zone()
if self._first_stage == 0 and orders_total == 0:
self._open_first()
orders_total = self._get_orders_total()
if self._zone_changed:
self._process_zone_change()
if orders_total >= 3 and self._calculate_floating_profit() >= 0.0:
self._close_all_orders()
def OnOwnTradeReceived(self, trade):
super(zs1_forex_instruments_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Trade is None:
return
volume = float(trade.Trade.Volume)
price = float(trade.Trade.Price)
side = trade.Order.Side if trade.Order is not None else None
if side is None:
return
if side == Sides.Buy:
if self.Position < 0 or self._is_closing_all:
# Closing short
self._reduce_entries(self._short_entries, volume)
else:
# Opening long
self._long_entries.append(Entry(price, volume))
self._last_order_direction = Sides.Buy
else:
if self.Position > 0 or self._is_closing_all:
# Closing long
self._reduce_entries(self._long_entries, volume)
else:
# Opening short
self._short_entries.append(Entry(price, volume))
self._last_order_direction = Sides.Sell
if self._first_stage == 1 and self._first_price == 0.0 and len(self._long_entries) > 0 and len(self._short_entries) > 0:
long_price = self._long_entries[0].Price
short_price = self._short_entries[0].Price
self._first_price = (long_price + short_price) / 2.0
if len(self._long_entries) == 0 and len(self._short_entries) == 0 and self._is_closing_all:
self._reset_state()
self._is_closing_all = False
def _process_zone_change(self):
if self._first_stage == 1:
self._zone_f1()
elif self._first_stage == 2:
if self._first_order_direction == Sides.Buy:
if self._zone == -2:
self._zone_minus_two()
elif self._zone == -1:
self._zone_minus_one()
elif self._zone == 0:
self._zone_zero()
elif self._zone == 1 or self._zone == 2:
self._zone_plus_one()
elif self._first_order_direction == Sides.Sell:
if self._zone == 2:
self._zone_minus_two()
elif self._zone == 1:
self._zone_minus_one()
elif self._zone == 0:
self._zone_zero()
elif self._zone == -1 or self._zone == -2:
self._zone_plus_one()
def _zone_f1(self):
self._zone_changed = False
self._close_first_orders()
def _zone_minus_two(self):
self._zone_changed = False
if self._calculate_floating_profit() > 0.0:
self._close_all_orders()
else:
self._open_another()
def _zone_minus_one(self):
self._zone_changed = False
if self._first_order_direction is None:
return
if self._first_order_direction == Sides.Buy:
self._open_sell_order()
else:
self._open_buy_order()
def _zone_zero(self):
self._zone_changed = False
if self._first_order_direction is None:
return
if self._first_order_direction == Sides.Buy:
self._open_buy_order()
else:
self._open_sell_order()
def _zone_plus_one(self):
self._zone_changed = False
if self._calculate_floating_profit() > 0.0:
self._close_all_orders()
else:
self._open_another()
def _open_first(self):
self.BuyMarket()
self.SellMarket()
self._first_stage = 1
self._zone = 0
self._last_zone = 0
self._zone_changed = False
self._first_price = self._current_price
self._first_order_direction = None
self._last_order_direction = None
def _close_first_orders(self):
if len(self._long_entries) > 0 and self._current_price > self._long_entries[0].Price:
# Long is profitable, close it, keep short
self.SellMarket()
self._first_stage = 2
self._first_order_direction = Sides.Sell
self._last_order_direction = Sides.Sell
return
if len(self._short_entries) > 0 and self._current_price < self._short_entries[0].Price:
# Short is profitable, close it, keep long
self.BuyMarket()
self._first_stage = 2
self._first_order_direction = Sides.Buy
self._last_order_direction = Sides.Buy
def _open_buy_order(self):
self.BuyMarket()
def _open_sell_order(self):
self.SellMarket()
def _open_another(self):
if self._last_order_direction == Sides.Buy:
self._open_sell_order()
elif self._last_order_direction == Sides.Sell:
self._open_buy_order()
elif self._first_order_direction == Sides.Buy:
self._open_sell_order()
elif self._first_order_direction == Sides.Sell:
self._open_buy_order()
def _close_all_orders(self):
if self._is_closing_all:
return
self._zone_changed = False
self._is_closing_all = True
# Close by selling longs and buying back shorts
if len(self._long_entries) > 0:
total_long = 0.0
for e in self._long_entries:
total_long += e.Volume
if total_long > 0.0:
self.SellMarket(total_long)
if len(self._short_entries) > 0:
total_short = 0.0
for e in self._short_entries:
total_short += e.Volume
if total_short > 0.0:
self.BuyMarket(total_short)
if len(self._long_entries) == 0 and len(self._short_entries) == 0:
self._reset_state()
self._is_closing_all = False
def _reset_state(self):
self._long_entries = []
self._short_entries = []
self._zone = 0
self._last_zone = 0
self._zone_changed = False
self._first_stage = 0
self._first_order_direction = None
self._last_order_direction = None
self._first_price = 0.0
def _check_zone(self):
step = float(self.OrdersSpacePips) * self._pip_value
if step <= 0.0 or self._first_price <= 0.0:
return
offset = float(self.PkPips) * self._pip_value
price = self._current_price + offset
if price >= self._first_price + step * (self._zone + 1):
self._last_zone = self._zone
self._zone += 1
self._zone_changed = True
elif price <= self._first_price - step * (1 - self._zone):
self._last_zone = self._zone
self._zone -= 1
self._zone_changed = True
if self._zone_changed and self._zone == self._last_zone:
self._zone_changed = False
def _get_orders_total(self):
return len(self._long_entries) + len(self._short_entries)
def _calculate_floating_profit(self):
if not self._has_price_data:
return 0.0
profit = 0.0
for entry in self._long_entries:
profit += (self._current_price - entry.Price) * entry.Volume
for entry in self._short_entries:
profit += (entry.Price - self._current_price) * entry.Volume
return profit
def _reduce_entries(self, entries, volume):
remaining = volume
while remaining > 0.0 and len(entries) > 0:
current = entries[0]
if current.Volume <= remaining + 0.0001:
remaining -= current.Volume
entries.pop(0)
else:
current.Volume -= remaining
remaining = 0.0
def _calculate_pip_value(self):
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0.0:
return 1.0
return float(step)
def CreateClone(self):
return zs1_forex_instruments_strategy()