VIDYA N Bars Borders Martingale
概述
原始的 MetaTrader 策略将 “VIDYA N Bars Borders” 通道与马丁格尔资金管理结合。StockSharp 版本保留核心思想:当价格跌破自适应下轨时买入,当价格突破上轨时卖出。通道中线由自适应移动平均生成,宽度由平均真实波幅(ATR)定义。马丁格尔模块在出现亏损后放大下一笔交易,同时检查单笔和总持仓上限。
交易逻辑
- 订阅所选周期的蜡烛图数据。
- 计算
KaufmanAdaptiveMovingAverage(作为 VIDYA 的替代)并构建 ATR 通道。 - 若收盘价跌破下轨,则开多或反手做多;若启用
Reverse参数,则执行相反方向。 - 若收盘价突破上轨,则开空或反手做空;
Reverse为真时转为做多。 - 约束相邻两次入场之间的最小价格间距,避免在同一区域重复进场。
- 当浮动收益达到指定的金额目标时,立即平掉所有仓位。
- 每次平仓后,如果上一笔交易亏损,则将下一笔基础手数乘以马丁格尔系数;若盈利则恢复到基础手数。最终手数会按照交易品种的步长和限额自动调整。
参数
| 名称 | 说明 |
|---|---|
Candle Type |
交易所使用的蜡烛数据类型。 |
CMO Period |
自适应均线效率比窗口。 |
EMA Period |
自适应均线的平滑周期。 |
ATR Period |
ATR 通道的计算周期。 |
Profit Target |
达到该金额时立即平仓。 |
Increase Ratio |
亏损后下一笔手数的放大倍数。 |
Max Position Volume |
单笔头寸体量上限。 |
Max Total Volume |
策略允许的总敞口上限。 |
Max Positions |
同时持仓数量上限(此移植版本只维护一个净头寸)。 |
Minimum Step |
连续两次入场的最小点数间隔。 |
Base Volume |
未放大之前的基础手数。 |
Reverse Signals |
反向执行买卖信号。 |
实现说明
- StockSharp 暂无原生 VIDYA 指标,因此使用
KaufmanAdaptiveMovingAverage近似其自适应特性,可通过参数调整响应速度。 - 策略仅维护一个净头寸。MQL 版本可以排队多个挂单;在 StockSharp 中,每次信号要么开新仓,要么反向平仓。马丁格尔调整作用于下一次入场的手数。
- 最小入场间距和手数调整依赖品种元数据(
PriceStep、VolumeStep、MinVolume、MaxVolume),请在连接交易所/历史数据时提供这些信息。 - 盈亏跟踪基于策略
PnL和最新收盘价,适用于回测。实盘运行时请连接会实时更新收益的投资组合。
文件
CS/VidyaNBarsBordersMartingaleStrategy.cs— 策略的 C# 实现。
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>
/// Simplified from "VIDYA N Bars Borders Martingale" MetaTrader expert.
/// Uses EMA as adaptive MA proxy and a range-based channel from recent N bars.
/// Buys when price closes below lower band, sells when above upper band.
/// Includes simple martingale volume increase on losing trades.
/// </summary>
public class VidyaNBarsBordersMartingaleStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _rangePeriod;
private readonly StrategyParam<decimal> _martingaleMultiplier;
private ExponentialMovingAverage _ema;
private readonly Queue<decimal> _highHistory = new();
private readonly Queue<decimal> _lowHistory = new();
private decimal _currentVolume;
private decimal _entryPrice;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
public int RangePeriod
{
get => _rangePeriod.Value;
set => _rangePeriod.Value = value;
}
public decimal MartingaleMultiplier
{
get => _martingaleMultiplier.Value;
set => _martingaleMultiplier.Value = value;
}
public VidyaNBarsBordersMartingaleStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Trading candle type", "General");
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Smoothing period for adaptive MA proxy", "Indicators");
_rangePeriod = Param(nameof(RangePeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Range Period", "Number of bars for high/low range channel", "Indicators");
_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 1.25m)
.SetDisplay("Martingale Multiplier", "Volume multiplier after losing trade", "Risk");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new ExponentialMovingAverage { Length = EmaPeriod };
_highHistory.Clear();
_lowHistory.Clear();
_currentVolume = Volume > 0 ? Volume : 1;
_entryPrice = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Track high/low for range calculation
_highHistory.Enqueue(candle.HighPrice);
_lowHistory.Enqueue(candle.LowPrice);
if (_highHistory.Count > RangePeriod)
{
_highHistory.Dequeue();
_lowHistory.Dequeue();
}
if (!_ema.IsFormed || _highHistory.Count < RangePeriod)
return;
// Compute range from recent bars
decimal highest = decimal.MinValue;
decimal lowest = decimal.MaxValue;
var highs = _highHistory.ToArray();
var lows = _lowHistory.ToArray();
foreach (var h in highs)
if (h > highest) highest = h;
foreach (var l in lows)
if (l < lowest) lowest = l;
var range = (highest - lowest) * 0.75m;
if (range <= 0)
return;
var upper = emaValue + range;
var lower = emaValue - range;
var close = candle.ClosePrice;
var baseVolume = Volume > 0 ? Volume : 1;
var maxVolume = baseVolume * 8;
var nextVolume = _currentVolume;
if (close < lower)
{
// Price below lower band -> buy signal
if (Position < 0)
{
var wasLoss = close > _entryPrice;
nextVolume = wasLoss
? Math.Min(_currentVolume * MartingaleMultiplier, maxVolume)
: baseVolume;
}
if (Position <= 0)
{
BuyMarket(Position < 0 ? Math.Abs(Position) + nextVolume : nextVolume);
_currentVolume = nextVolume;
_entryPrice = close;
}
}
else if (close > upper)
{
// Price above upper band -> sell signal
if (Position > 0)
{
var wasLoss = close < _entryPrice;
nextVolume = wasLoss
? Math.Min(_currentVolume * MartingaleMultiplier, maxVolume)
: baseVolume;
}
if (Position >= 0)
{
SellMarket(Position > 0 ? Math.Abs(Position) + nextVolume : nextVolume);
_currentVolume = nextVolume;
_entryPrice = close;
}
}
}
/// <inheritdoc />
protected override void OnReseted()
{
_ema = null;
_highHistory.Clear();
_lowHistory.Clear();
_currentVolume = 0;
_entryPrice = 0;
base.OnReseted();
}
}
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 vidya_n_bars_borders_martingale_strategy(Strategy):
def __init__(self):
super(vidya_n_bars_borders_martingale_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._ema_period = self.Param("EmaPeriod", 20)
self._range_period = self.Param("RangePeriod", 10)
self._high_history = []
self._low_history = []
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def RangePeriod(self):
return self._range_period.Value
@RangePeriod.setter
def RangePeriod(self, value):
self._range_period.Value = value
def OnReseted(self):
super(vidya_n_bars_borders_martingale_strategy, self).OnReseted()
self._high_history = []
self._low_history = []
self._entry_price = 0.0
def OnStarted2(self, time):
super(vidya_n_bars_borders_martingale_strategy, self).OnStarted2(time)
self._high_history = []
self._low_history = []
self._entry_price = 0.0
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._process_candle).Start()
def _process_candle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
rng_period = self.RangePeriod
self._high_history.append(high)
self._low_history.append(low)
if len(self._high_history) > rng_period:
self._high_history.pop(0)
self._low_history.pop(0)
if len(self._high_history) < rng_period:
return
highest = max(self._high_history)
lowest = min(self._low_history)
rng = (highest - lowest) * 0.75
if rng <= 0:
return
upper = ema_val + rng
lower = ema_val - rng
if close < lower and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif close > upper and self.Position >= 0:
self.SellMarket()
self._entry_price = close
def CreateClone(self):
return vidya_n_bars_borders_martingale_strategy()