Mean Reversion Donchian 策略
概述
该策略移植自 MetaTrader 专家顾问 MeanReversion.mq5。当价格在设定的回溯窗口内创出新的低点时,策略会开立多头仓位并将最近区间的中点作为回归目标;当价格刷新新高时,则按照相同思路开立空头仓位。头寸规模根据风险百分比和止损距离计算,尽可能还原原始 EA 的手数算法。
交易逻辑
- 使用所选 K 线类型和回溯周期构建唐奇安通道。上轨表示窗口内的最高价,下轨表示最低价,中线
(upper + lower) / 2作为均值回归目标。 - 若当前收盘完成的 K 线创出新低(
Low <= LowerBand)且无持仓,则按市价买入。止损价通过入场价对称反射,使中线成为止盈目标,对应原始 EA 的计算sl = 2 * Ask - tp。 - 若 K 线创出新高(
High >= UpperBand)且无持仓,则按市价卖出,止损放置在入场价上方,中线仍为止盈价位。 - 每根完成的 K 线都会检查止损与止盈。价格突破止损时立即平仓,触及中线则在目标位置离场。仓位回到空仓状态后内部状态会自动重置。
仓位管理
- 单笔风险为
Portfolio.CurrentValue * (RiskPercent / 100)。若账户权益不可用,策略会退而使用最小可交易手数。 - 合约风险按
|EntryPrice - StopPrice|计算,原始手数为RiskAmount / perUnitRisk,随后按照合约最小变动量归一化。当归一化结果小于最小可交易数量时,会自动采用最小手数,同时满足交易所的最大/最小限制。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
CandleType |
用于构建唐奇安通道的 K 线类型和周期。 | 15 分钟 |
LookbackPeriod |
计算最高价与最低价所用的 K 线数量。 | 200 |
RiskPercent |
每笔交易占用的账户权益百分比。 | 1% |
全部参数均支持优化。
其他说明
- 策略一次只持有一个方向的仓位,对应原版代码中的
PositionsTotal()>0限制。 - 止损和止盈在策略内部维护,而不是通过附加委托下达,以便保持与原 EA 一致并兼容高级 API。
- 当无法获取账户权益或合约手数信息时,策略仍会使用最小手数交易,以确保行为确定。
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>
/// Port of the MetaTrader strategy MeanReversion.mq5.
/// Buys when price sets a fresh lookback low and targets the mid-point of the recent range,
/// or sells at a new high aiming for the same reversion level.
/// Position size is determined from the percentage risk and the stop distance.
/// </summary>
public class MeanReversionDonchianStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _riskPercent;
private DonchianChannels _donchian = null!;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private Sides? _activeSide;
/// <summary>
/// Candle type and timeframe used for the Donchian channel calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Amount of candles included in the high/low range.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Percent of portfolio equity risked per trade.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MeanReversionDonchianStrategy"/>.
/// </summary>
public MeanReversionDonchianStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to analyze", "General");
_lookbackPeriod = Param(nameof(LookbackPeriod), 200)
.SetDisplay("Lookback", "Number of candles used for range detection", "Signals")
.SetRange(20, 500)
;
_riskPercent = Param(nameof(RiskPercent), 1m)
.SetDisplay("Risk %", "Percentage of equity risked per entry", "Money Management")
.SetRange(0.25m, 5m)
;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_stopPrice = null;
_takeProfitPrice = null;
_activeSide = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_donchian = new DonchianChannels { Length = LookbackPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_donchian, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _donchian);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue donchianValue)
{
if (candle.State != CandleStates.Finished)
return;
// indicators bound via BindEx
ManageOpenPosition(candle);
if (Position != 0)
return;
if (donchianValue is not IDonchianChannelsValue channel)
return;
if (channel.UpperBand is not decimal upperBand || channel.LowerBand is not decimal lowerBand || channel.Middle is not decimal midBand)
return;
GenerateSignals(candle, lowerBand, upperBand, midBand);
}
private void ManageOpenPosition(ICandleMessage candle)
{
if (Position > 0 && _activeSide == Sides.Buy)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Position);
ResetPositionState();
return;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
SellMarket(Position);
ResetPositionState();
}
}
else if (Position < 0 && _activeSide == Sides.Sell)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(-Position);
ResetPositionState();
return;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
BuyMarket(-Position);
ResetPositionState();
}
}
if (Position == 0 && _activeSide != null)
{
ResetPositionState();
}
}
private void GenerateSignals(ICandleMessage candle, decimal lowerBand, decimal upperBand, decimal midBand)
{
var closePrice = candle.ClosePrice;
if (candle.LowPrice <= lowerBand)
{
var stopPrice = 2m * closePrice - midBand;
var volume = CalculateRiskAdjustedVolume(closePrice, stopPrice);
if (volume > 0m && stopPrice < closePrice)
{
BuyMarket(volume);
_stopPrice = stopPrice;
_takeProfitPrice = midBand;
_activeSide = Sides.Buy;
}
}
else if (candle.HighPrice >= upperBand)
{
var stopPrice = 2m * closePrice - midBand;
var volume = CalculateRiskAdjustedVolume(closePrice, stopPrice);
if (volume > 0m && stopPrice > closePrice)
{
SellMarket(volume);
_stopPrice = stopPrice;
_takeProfitPrice = midBand;
_activeSide = Sides.Sell;
}
}
}
private decimal CalculateRiskAdjustedVolume(decimal entryPrice, decimal stopPrice)
{
var perUnitRisk = Math.Abs(entryPrice - stopPrice);
if (perUnitRisk <= 0m)
return 0m;
var portfolioValue = Portfolio?.CurrentValue ?? 0m;
var riskBudget = portfolioValue > 0m ? portfolioValue * (RiskPercent / 100m) : 0m;
if (riskBudget <= 0m)
{
return GetMinimalVolume();
}
var rawVolume = riskBudget / perUnitRisk;
var normalized = NormalizeVolume(rawVolume);
var minimal = GetMinimalVolume();
if (normalized < minimal)
normalized = minimal;
return normalized;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = Security?.VolumeStep ?? 0m;
if (step <= 0m)
return volume;
var normalized = Math.Floor(volume / step) * step;
var max = Security?.MaxVolume ?? 0m;
if (max > 0m && normalized > max)
normalized = max;
return normalized;
}
private decimal GetMinimalVolume()
{
var min = Security?.MinVolume ?? 0m;
if (min > 0m)
return min;
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
return step;
return Volume > 0m ? Volume : 1m;
}
private void ResetPositionState()
{
_stopPrice = null;
_takeProfitPrice = null;
_activeSide = null;
}
}
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, Sides
from StockSharp.Algo.Indicators import DonchianChannels
from StockSharp.Algo.Strategies import Strategy
class mean_reversion_donchian_strategy(Strategy):
"""Buys at Donchian low, sells at Donchian high, targeting the midpoint."""
def __init__(self):
super(mean_reversion_donchian_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles to analyze", "General")
self._lookback_period = self.Param("LookbackPeriod", 200) \
.SetDisplay("Lookback", "Number of candles used for range detection", "Signals")
self._risk_percent = self.Param("RiskPercent", 1.0) \
.SetDisplay("Risk %", "Percentage of equity risked per entry", "Money Management")
self._stop_price = None
self._take_profit_price = None
self._active_side = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@property
def RiskPercent(self):
return self._risk_percent.Value
def OnReseted(self):
super(mean_reversion_donchian_strategy, self).OnReseted()
self._stop_price = None
self._take_profit_price = None
self._active_side = None
def OnStarted2(self, time):
super(mean_reversion_donchian_strategy, self).OnStarted2(time)
donchian = DonchianChannels()
donchian.Length = self.LookbackPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(donchian, self._process_candle).Start()
def _process_candle(self, candle, donchian_value):
if candle.State != CandleStates.Finished:
return
self._manage_open_position(candle)
if self.Position != 0:
return
upper = donchian_value.UpperBand
lower = donchian_value.LowerBand
middle = donchian_value.Middle
if upper is None or lower is None or middle is None:
return
up = float(upper)
lo = float(lower)
mid = float(middle)
close = float(candle.ClosePrice)
if float(candle.LowPrice) <= lo:
stop_p = 2.0 * close - mid
if stop_p < close:
self.BuyMarket()
self._stop_price = stop_p
self._take_profit_price = mid
self._active_side = Sides.Buy
elif float(candle.HighPrice) >= up:
stop_p = 2.0 * close - mid
if stop_p > close:
self.SellMarket()
self._stop_price = stop_p
self._take_profit_price = mid
self._active_side = Sides.Sell
def _manage_open_position(self, candle):
if self.Position > 0 and self._active_side == Sides.Buy:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(self.Position)
self._reset_state()
return
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(self.Position)
self._reset_state()
elif self.Position < 0 and self._active_side == Sides.Sell:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_state()
return
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(abs(self.Position))
self._reset_state()
if self.Position == 0 and self._active_side is not None:
self._reset_state()
def _reset_state(self):
self._stop_price = None
self._take_profit_price = None
self._active_side = None
def CreateClone(self):
return mean_reversion_donchian_strategy()