Zs1 Forex Instruments 策略
该策略复现了 MetaTrader 专家顾问 Zs1_www_forex-instruments_info 的对冲网格逻辑。算法会同时开出买入和卖出头寸,跟踪价格相对起始点的移动,并根据五个离散区域执行原始 EA 的分支逻辑。保留下来的锚定方向会按马丁格尔系数加仓,而整个仓位在达到盈亏平衡时会整体平仓。
核心行为
- 按配置的基础手数同时市价买入和卖出,建立初始对冲。
- 只要其中一条腿获得盈利就立即平仓,保留另一条作为锚定方向。
- 使用
Orders Space (pips)参数计算价格区间。当价格跨越新区间时,执行与原版 EA 相同的处理:- 区域 −2:若浮动盈利为正则立即平仓,否则沿趋势反向加仓。
- 区域 −1:按锚定方向的相反方向加仓。
- 区域 0:沿锚定方向加仓。
- 区域 +1:若浮动盈利为正则平仓,否则开出与上一单相反的方向。
- 当持仓数量不少于三笔时,只要浮动盈亏不为负就会立即平仓。
- 所有仓位平掉后,会自动开始下一轮循环。
参数
| 名称 | 说明 |
|---|---|
Orders Space (pips) |
相邻网格层之间的点差距离。 |
Zone Offset (pips) |
确认区间突破时需要额外突破的缓冲。 |
Initial Volume |
初始对冲及后续马丁加仓所使用的基础手数。 |
说明
- 马丁格尔系数遵循原策略的隧道序列(1, 3, 6, 12, ...)。
- 在发送订单前会根据标的物的最小/最大手数以及步进限制对下单数量做校验。
- 所有决策基于 Level1 的买一/卖一报价更新,从而贴近 MQL 版本的逐笔逻辑。
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()