锁仓网格(Locker)
基于网格的对冲策略,通过交替建立买入和卖出市价单来锁定浮亏,并在账户余额上获取一小部分百分比收益。
交易逻辑
- 第一个完成的K线收盘后,立即按照起始手数开出第一笔多单来启动网格。
- 记录之后的每一笔交易,在策略内部维护多空腿列表,以估算总体的未实现盈亏与已实现盈亏。
- 当活跃腿数达到八笔时,优先关闭最早的一对买卖腿,在继续处理本根K线的其他逻辑之前先降低敞口。
- 当综合利润高于账户价值目标百分比时,平掉所有剩余仓位并重置内部状态。
- 当综合利润跌破目标的负值时,比较最近开仓价与当前市场价:若价格上行超过设定步长,补充一笔空单;若价格下行超过同样的距离,则加仓多单。
- 平仓时总是发送与记录腿方向相反的市价单,从而立即解除锁仓。
参数
- Profit % – 达到该百分比(基于当前账户价值)即锁定利润并全部平仓。
- Start Volume – 启动网格时首笔多单使用的手数。
- Step Volume – 触发亏损阈值后,每笔对冲单使用的手数。
- Step Points – 网格层级之间的价格步数,会与品种的最小报价步长相乘得到实际价格距离。
- Enable Automation – 全局开关,关闭后暂停所有交易逻辑。
- Candle Type – 用于驱动策略的K线类型,只有在K线收盘后才会做出决策。
该移植版本保留了原始 MetaTrader 专家的思路,同时利用 StockSharp 的高级 API 下单,并在策略内部保存详细的交易状态,从而保持盈亏计算与 MQL 程序一致。
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;
public class LockerStrategy : Strategy
{
private readonly struct PositionEntry
{
public PositionEntry(Sides side, decimal price, decimal volume)
{
Side = side;
Price = price;
Volume = volume;
}
public Sides Side { get; }
public decimal Price { get; }
public decimal Volume { get; }
}
private readonly StrategyParam<decimal> _profitTargetPercent;
private readonly StrategyParam<decimal> _startVolume;
private readonly StrategyParam<decimal> _stepVolume;
private readonly StrategyParam<decimal> _stepPoints;
private readonly StrategyParam<bool> _enableAutomation;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxOpenPositions;
private readonly List<PositionEntry> _entries = new();
private decimal _realizedPnL;
private decimal _lastEntryPrice;
private Sides? _lastEntrySide;
private int _cooldown;
public decimal ProfitTargetPercent { get => _profitTargetPercent.Value; set => _profitTargetPercent.Value = value; }
public decimal StartVolume { get => _startVolume.Value; set => _startVolume.Value = value; }
public decimal StepVolume { get => _stepVolume.Value; set => _stepVolume.Value = value; }
public decimal StepPoints { get => _stepPoints.Value; set => _stepPoints.Value = value; }
public bool EnableAutomation { get => _enableAutomation.Value; set => _enableAutomation.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int MaxOpenPositions { get => _maxOpenPositions.Value; set => _maxOpenPositions.Value = value; }
public LockerStrategy()
{
_profitTargetPercent = Param(nameof(ProfitTargetPercent), 0.001m)
.SetGreaterThanZero()
.SetDisplay("Profit %", "Target profit percent of balance", "General")
;
_startVolume = Param(nameof(StartVolume), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Start Volume", "Initial trade volume", "General")
;
_stepVolume = Param(nameof(StepVolume), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Step Volume", "Volume for subsequent trades", "General")
;
_stepPoints = Param(nameof(StepPoints), 15000m)
.SetGreaterThanZero()
.SetDisplay("Step Points", "Number of price steps between new trades", "General")
;
_enableAutomation = Param(nameof(EnableAutomation), true)
.SetDisplay("Enable Automation", "Allow the strategy to place trades", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles for processing", "Data");
_maxOpenPositions = Param(nameof(MaxOpenPositions), 2)
.SetGreaterThanZero()
.SetDisplay("Max Open Positions", "Maximum number of hedged legs allowed", "Risk")
;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_entries.Clear();
_realizedPnL = 0m;
_lastEntryPrice = 0m;
_lastEntrySide = null;
_cooldown = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType).Bind(Process).Start();
}
private void Process(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!EnableAutomation)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var closePrice = candle.ClosePrice;
// Use the candle close as a proxy for bid/ask because we operate on finished bars.
var bid = closePrice;
var ask = closePrice;
var currentProfit = _realizedPnL + CalculateUnrealizedProfit(bid, ask);
var openCount = _entries.Count;
if (openCount == 0)
{
// Start the grid with an initial buy order.
OpenPosition(Sides.Buy, StartVolume, ask);
return;
}
if (openCount >= MaxOpenPositions && TryClosePair(bid, ask))
{
// Reduce exposure when too many hedged orders are active.
return;
}
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
if (portfolioValue <= 0m)
portfolioValue = 1000000m;
var targetProfit = portfolioValue * ProfitTargetPercent;
if (targetProfit > 0m && currentProfit >= targetProfit)
{
// Target reached, flatten the book.
CloseAllPositions(bid, ask);
_cooldown = 20;
return;
}
if (targetProfit <= 0m)
return;
if (currentProfit <= -targetProfit)
{
var lastPrice = _lastEntryPrice;
if (lastPrice == 0m)
return;
var stepDistance = GetStepDistance();
if (stepDistance <= 0m)
return;
// Add a hedging order whenever price travels far enough from the latest entry.
if (ask > lastPrice + stepDistance)
OpenPosition(Sides.Sell, StepVolume, ask);
else if (bid < lastPrice - stepDistance)
OpenPosition(Sides.Buy, StepVolume, bid);
}
}
private decimal CalculateUnrealizedProfit(decimal bid, decimal ask)
{
var profit = 0m;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
var exitPrice = entry.Side == Sides.Buy ? bid : ask;
var direction = entry.Side == Sides.Buy ? 1m : -1m;
profit += (exitPrice - entry.Price) * direction * entry.Volume;
}
return profit;
}
private bool TryClosePair(decimal bid, decimal ask)
{
var buyIndex = -1;
var sellIndex = -1;
for (var i = 0; i < _entries.Count; i++)
{
var entry = _entries[i];
if (entry.Side == Sides.Buy && buyIndex == -1)
buyIndex = i;
else if (entry.Side == Sides.Sell && sellIndex == -1)
sellIndex = i;
if (buyIndex != -1 && sellIndex != -1)
break;
}
if (buyIndex == -1 || sellIndex == -1)
return false;
if (buyIndex > sellIndex)
{
CloseEntry(buyIndex, bid, ask);
CloseEntry(sellIndex, bid, ask);
}
else
{
CloseEntry(sellIndex, bid, ask);
CloseEntry(buyIndex, bid, ask);
}
UpdateLastEntry();
return true;
}
private void CloseAllPositions(decimal bid, decimal ask)
{
while (_entries.Count > 0)
{
CloseEntry(_entries.Count - 1, bid, ask);
}
UpdateLastEntry();
}
private void CloseEntry(int index, decimal bid, decimal ask)
{
if (index < 0 || index >= _entries.Count)
return;
var entry = _entries[index];
var exitPrice = entry.Side == Sides.Buy ? bid : ask;
var direction = entry.Side == Sides.Buy ? Sides.Sell : Sides.Buy;
// Send the offsetting market order to neutralize the entry.
if (direction == Sides.Sell)
SellMarket();
else
BuyMarket();
var pnl = (exitPrice - entry.Price) * (entry.Side == Sides.Buy ? 1m : -1m) * entry.Volume;
_realizedPnL += pnl;
try { _entries.RemoveAt(index); } catch { }
}
private void OpenPosition(Sides side, decimal volume, decimal price)
{
if (volume <= 0m)
return;
if (side == Sides.Buy)
BuyMarket();
else
SellMarket();
_entries.Add(new PositionEntry(side, price, volume));
_lastEntryPrice = price;
_lastEntrySide = side;
}
private decimal GetStepDistance()
{
var priceStep = Security?.PriceStep ?? 0m;
return priceStep > 0m ? StepPoints * priceStep : StepPoints;
}
private void UpdateLastEntry()
{
if (_entries.Count == 0)
{
_lastEntryPrice = 0m;
_lastEntrySide = null;
return;
}
var entry = _entries[_entries.Count - 1];
_lastEntryPrice = entry.Price;
_lastEntrySide = entry.Side;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class locker_strategy(Strategy):
"""Hedging grid locker: opens initial position, hedges on drawdown, closes at profit target."""
def __init__(self):
super(locker_strategy, self).__init__()
self._profit_target_percent = self.Param("ProfitTargetPercent", 0.001) \
.SetGreaterThanZero() \
.SetDisplay("Profit %", "Target profit percent of balance", "General")
self._start_volume = self.Param("StartVolume", 0.5) \
.SetGreaterThanZero() \
.SetDisplay("Start Volume", "Initial trade volume", "General")
self._step_volume = self.Param("StepVolume", 0.2) \
.SetGreaterThanZero() \
.SetDisplay("Step Volume", "Volume for subsequent trades", "General")
self._step_points = self.Param("StepPoints", 15000.0) \
.SetGreaterThanZero() \
.SetDisplay("Step Points", "Number of price steps between new trades", "General")
self._enable_automation = self.Param("EnableAutomation", True) \
.SetDisplay("Enable Automation", "Allow the strategy to place trades", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles for processing", "Data")
self._max_open_positions = self.Param("MaxOpenPositions", 2) \
.SetGreaterThanZero() \
.SetDisplay("Max Open Positions", "Maximum number of hedged legs allowed", "Risk")
# entries: list of (side, price, volume) tuples; side='buy' or 'sell'
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
@property
def ProfitTargetPercent(self):
return float(self._profit_target_percent.Value)
@property
def StartVolume(self):
return float(self._start_volume.Value)
@property
def StepVolume(self):
return float(self._step_volume.Value)
@property
def StepPoints(self):
return float(self._step_points.Value)
@property
def EnableAutomation(self):
return self._enable_automation.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def MaxOpenPositions(self):
return int(self._max_open_positions.Value)
def OnStarted2(self, time):
super(locker_strategy, self).OnStarted2(time)
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.EnableAutomation:
return
if self._cooldown > 0:
self._cooldown -= 1
return
close_price = float(candle.ClosePrice)
bid = close_price
ask = close_price
current_profit = self._realized_pnl + self._calc_unrealized(bid, ask)
open_count = len(self._entries)
if open_count == 0:
self._open_position('buy', self.StartVolume, ask)
return
if open_count >= self.MaxOpenPositions and self._try_close_pair(bid, ask):
return
portfolio_value = 1000000.0
if self.Portfolio is not None and self.Portfolio.CurrentValue is not None:
pv = float(self.Portfolio.CurrentValue)
if pv > 0:
portfolio_value = pv
target_profit = portfolio_value * self.ProfitTargetPercent
if target_profit > 0 and current_profit >= target_profit:
self._close_all(bid, ask)
self._cooldown = 20
return
if target_profit <= 0:
return
if current_profit <= -target_profit:
last_price = self._last_entry_price
if last_price == 0:
return
step_distance = self._get_step_distance()
if step_distance <= 0:
return
if ask > last_price + step_distance:
self._open_position('sell', self.StepVolume, ask)
elif bid < last_price - step_distance:
self._open_position('buy', self.StepVolume, bid)
def _calc_unrealized(self, bid, ask):
profit = 0.0
for side, price, volume in self._entries:
exit_price = bid if side == 'buy' else ask
direction = 1.0 if side == 'buy' else -1.0
profit += (exit_price - price) * direction * volume
return profit
def _try_close_pair(self, bid, ask):
buy_index = -1
sell_index = -1
for i in range(len(self._entries)):
side = self._entries[i][0]
if side == 'buy' and buy_index == -1:
buy_index = i
elif side == 'sell' and sell_index == -1:
sell_index = i
if buy_index != -1 and sell_index != -1:
break
if buy_index == -1 or sell_index == -1:
return False
if buy_index > sell_index:
self._close_entry(buy_index, bid, ask)
self._close_entry(sell_index, bid, ask)
else:
self._close_entry(sell_index, bid, ask)
self._close_entry(buy_index, bid, ask)
self._update_last_entry()
return True
def _close_all(self, bid, ask):
while len(self._entries) > 0:
self._close_entry(len(self._entries) - 1, bid, ask)
self._update_last_entry()
def _close_entry(self, index, bid, ask):
if index < 0 or index >= len(self._entries):
return
side, price, volume = self._entries[index]
exit_price = bid if side == 'buy' else ask
if side == 'buy':
self.SellMarket()
else:
self.BuyMarket()
direction = 1.0 if side == 'buy' else -1.0
pnl = (exit_price - price) * direction * volume
self._realized_pnl += pnl
self._entries.pop(index)
def _open_position(self, side, volume, price):
if volume <= 0:
return
if side == 'buy':
self.BuyMarket()
else:
self.SellMarket()
self._entries.append((side, price, volume))
self._last_entry_price = price
self._last_entry_side = side
def _get_step_distance(self):
sec = self.Security
price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0
if price_step > 0:
return self.StepPoints * price_step
return self.StepPoints
def _update_last_entry(self):
if len(self._entries) == 0:
self._last_entry_price = 0.0
self._last_entry_side = None
return
side, price, volume = self._entries[-1]
self._last_entry_price = price
self._last_entry_side = side
def OnReseted(self):
super(locker_strategy, self).OnReseted()
self._entries = []
self._realized_pnl = 0.0
self._last_entry_price = 0.0
self._last_entry_side = None
self._cooldown = 0
def CreateClone(self):
return locker_strategy()