在 GitHub 上查看
Urdala Trol 对冲网格策略
概述
Urdala Trol Hedging Grid Strategy 是 MetaTrader 5 智能交易系统 Urdala_Trol.mq5 的 StockSharp 高阶 API 版本。策略始终保持双向持仓,并在触发止损后以网格+马丁方式逐步加仓。策略仅依赖 Level1 最优买卖价数据,不使用任何技术指标。
交易逻辑
- 初始对冲(步骤 0):当没有持仓时,立即按 Base Volume 参数同时买入和卖出一手,建立基础对冲。
- 亏损方向加仓(步骤 1.2):如果仅剩单方向持仓,并且该方向最亏损的仓位距离当前价格至少
Grid Step 个点,则按同方向再开一单。新单手数 = 最亏仓位手数 + Min Lots Multiplier * minVolumeStep,其中 minVolumeStep 来源于品种的 VolumeStep 或 MinVolume。
- 止损后的处理(步骤 1.1):当仓位被止损(包括跟踪止损)并产生亏损时,如果当前没有距离止损价小于
Min Nearest 点的同向仓位,则立即按相同方向重新入场。
- 盈利止损后的处理(步骤 2.1):止损盈利退出时,立刻以放大后的手数在相反方向开仓。
- 跟踪止损:价格在入场价上方(或下方)运行超过
Trailing Stop + Trailing Step 点后,将止损位上调(或下调),保持 Trailing Stop 点的距离。仅当两个参数都大于零时才启用跟踪。
所有以点(pip)表示的距离都会根据标的的 PriceStep 转换为绝对价格差。对于三位或五位报价,会额外乘以十,以复现原始 MQL “adjusted point” 的处理方式。
参数说明
| 参数 |
默认值 |
说明 |
BaseVolume |
0.1 |
打开初始对冲仓位时使用的基础手数。 |
MinLotsMultiplier |
3 |
加仓时附加的最小手数倍数。 |
StopLossPips |
50 |
止损距离(点)。设为 0 可关闭止损和跟踪功能。 |
TrailingStopPips |
5 |
跟踪止损距离(点),设为 0 则不启用跟踪。 |
TrailingStepPips |
5 |
跟踪止损向前移动前需要额外行进的点数;启用跟踪时必须为正。 |
GridStepPips |
50 |
亏损仓位与当前价格之间的最小距离(点),达到后才加仓。 |
MinNearestPips |
3 |
如果当前已有仓位距离最近止损价小于该值,则跳过再次入场。 |
实现细节
- 通过
SubscribeLevel1() 订阅行情,使用最佳买卖价驱动全部决策。
- 使用
RegisterOrder 注册委托,并在 OnOwnTradeReceived 中精确跟踪成交与仓位。
- 策略内部维护独立的仓位列表,以模拟对冲模式(StockSharp 默认按净头寸管理)。
- 止损和跟踪逻辑由策略自身触发市价单实现,没有单独登记止损委托。
使用建议
- 选择流动性良好的标的,并确保
PriceStep、VolumeStep、MinVolume、MaxVolume 等属性设置正确,以便正确换算点值与手数。
- 启动策略后会立即建立对冲仓位,随后按原版 MQL 逻辑响应止损事件。
- 根据标的波动调整各项点值参数:增大
Grid Step 可降低加仓频率,提高 Min Lots Multiplier 会加速手数增长。
- 谨慎控制风险:马丁结构在连续止损时会迅速放大头寸。
本目录按照要求暂不提供 Python 版本。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Urdala Trol strategy (simplified). Uses EMA with trailing stop logic
/// for grid-style entries based on trend direction.
/// </summary>
public class UrdalaTrolStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<decimal> _trailingPercent;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
public decimal TrailingPercent
{
get => _trailingPercent.Value;
set => _trailingPercent.Value = value;
}
public UrdalaTrolStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles", "General");
_emaLength = Param(nameof(EmaLength), 14)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA period", "Indicators");
_trailingPercent = Param(nameof(TrailingPercent), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Trailing %", "Trailing stop percent", "Risk");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaLength };
decimal highSinceEntry = 0;
decimal lowSinceEntry = decimal.MaxValue;
decimal prevClose = 0;
decimal prevEma = 0;
var hasPrev = false;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, (ICandleMessage candle, decimal emaVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!hasPrev)
{
prevClose = candle.ClosePrice;
prevEma = emaVal;
hasPrev = true;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
prevClose = candle.ClosePrice;
prevEma = emaVal;
return;
}
var close = candle.ClosePrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
// Track trailing stop levels
if (Position > 0)
{
if (high > highSinceEntry)
highSinceEntry = high;
var trailStop = highSinceEntry * (1m - TrailingPercent / 100m);
if (close < trailStop)
{
SellMarket();
highSinceEntry = 0;
lowSinceEntry = decimal.MaxValue;
return;
}
}
else if (Position < 0)
{
if (low < lowSinceEntry)
lowSinceEntry = low;
var trailStop = lowSinceEntry * (1m + TrailingPercent / 100m);
if (close > trailStop)
{
BuyMarket();
highSinceEntry = 0;
lowSinceEntry = decimal.MaxValue;
return;
}
}
// Entry signals based on EMA
var bullishCross = prevClose <= prevEma && close > emaVal;
var bearishCross = prevClose >= prevEma && close < emaVal;
if (bullishCross && Position <= 0)
{
BuyMarket();
highSinceEntry = high;
lowSinceEntry = decimal.MaxValue;
}
else if (bearishCross && Position >= 0)
{
SellMarket();
lowSinceEntry = low;
highSinceEntry = 0;
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
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.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class urdala_trol_strategy(Strategy):
def __init__(self):
super(urdala_trol_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles", "General")
self._ema_length = self.Param("EmaLength", 14) \
.SetDisplay("EMA Length", "EMA period", "Indicators")
self._trailing_percent = self.Param("TrailingPercent", 1.5) \
.SetDisplay("Trailing %", "Trailing stop percent", "Risk")
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def TrailingPercent(self):
return self._trailing_percent.Value
def OnReseted(self):
super(urdala_trol_strategy, self).OnReseted()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(urdala_trol_strategy, self).OnStarted2(time)
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
ema = ExponentialMovingAverage()
ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _on_process(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
ev = float(ema_value)
if not self._has_prev:
self._prev_close = close
self._prev_ema = ev
self._has_prev = True
return
if self.Position > 0:
if high > self._high_since_entry:
self._high_since_entry = high
trail_stop = self._high_since_entry * (1.0 - self.TrailingPercent / 100.0)
if close < trail_stop:
self.SellMarket()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = close
self._prev_ema = ev
return
elif self.Position < 0:
if low < self._low_since_entry:
self._low_since_entry = low
trail_stop = self._low_since_entry * (1.0 + self.TrailingPercent / 100.0)
if close > trail_stop:
self.BuyMarket()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = close
self._prev_ema = ev
return
bullish_cross = self._prev_close <= self._prev_ema and close > ev
bearish_cross = self._prev_close >= self._prev_ema and close < ev
if bullish_cross and self.Position <= 0:
self.BuyMarket()
self._high_since_entry = high
self._low_since_entry = 1e18
elif bearish_cross and self.Position >= 0:
self.SellMarket()
self._low_since_entry = low
self._high_since_entry = 0.0
self._prev_close = close
self._prev_ema = ev
def CreateClone(self):
return urdala_trol_strategy()