在 GitHub 上查看
VQ EA
概述
- 将 MetaTrader 专家顾问“VQ_EA”迁移到 StockSharp 平台。
- 使用平滑的中位价近似原始的 Volatility Quality (VQ) 指标曲线,从而保持高阶 API 的实现方式。
- 当平滑曲线方向发生反转时开仓,可选地通过保护性订单管理仓位。
原始 MQL 行为
- 从自定义指标 VQ 的第 3、4 个缓冲区读取买入或卖出信号。
- 当出现新的信号且当前方向没有仓位时,以市价开立新仓。
- 出现相反信号时立即关闭对向仓位。
- 附带固定手数/分数手数、保本、移动止损、日志输出以及提醒/邮件等功能。
StockSharp 实现
- 使用中位价的简单移动平均线以及可选的第二次平滑来模拟 VQ 曲线的变色效果。
- 通过点值过滤避免微小波动触发信号。
- 继续使用市价单进出场,以贴近原专家顾问的执行方式。
信号生成步骤
- 订阅指定类型的蜡烛并计算每根已完成蜡烛的中位价。
- 应用基础平滑周期 (
Length),若 Smoothing 大于 1,则再次平滑。
- 将当前平滑值与前一值比较,若绝对变化超过
FilterPoints(转换为价格单位),判断方向上升或下降。
- 方向由下降转为上升时开多,反之开空;若持有反向仓位,则在开新仓时加上绝对仓位量以实现反向。
风险管理
StopLossPoints、TakeProfitPoints、TrailingStopPoints 会根据标的的最小价差 (PriceStep) 转换为绝对价格距离。
- 至少启用一项保护功能时,会调用
StartProtection 并使用市价调整,模拟原始 EA 的保护单逻辑。
- 只有在
UseTrailing 为 true 且拖尾距离大于 0 时才启用移动止损。
参数说明
Length – 中位价基础平滑周期,默认 5。
Smoothing – 第二次平滑周期,默认 1(不启用)。
FilterPoints – 方向变化所需的最小点数,默认 5。
StopLossPoints – 止损点数,默认 60,设为 0 关闭。
TakeProfitPoints – 止盈点数,默认 0(关闭)。
UseTrailing – 是否启用移动止损,默认 false。
TrailingStopPoints – 移动止损点数,默认 0(UseTrailing 为 false 时忽略)。
CandleType – 使用的时间框架,默认 1 小时蜡烛。
Volume – 继承自 Strategy.Volume,默认 1,每次开仓使用相同的数量。
与原始专家的差异
- 未直接移植 VQ 指标,而是用平滑的中位价近似,实现上存在差别。
- 未复现保本移动、提醒与邮件调度、分数手数等高级资金管理功能。
- 移动止损步长逻辑简化为 StockSharp 内置的拖尾管理器。
使用提示
- 仅在蜡烛收盘后生成信号,等价于原策略的“收盘交易”模式。
- 请确保标的设置了正确的
PriceStep;若不可用,将退化为 1.0 来换算点值。
- 策略主要用于演示,可按需求扩展更多资金管理规则。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class VqEaStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _momentumPeriod;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevMom; private bool _hasPrev;
private int _cooldown;
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int MomentumPeriod { get => _momentumPeriod.Value; set => _momentumPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public VqEaStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20).SetDisplay("EMA Period", "EMA filter", "Indicators");
_momentumPeriod = Param(nameof(MomentumPeriod), 14).SetDisplay("Momentum", "Momentum period", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame()).SetDisplay("Candle Type", "Candle timeframe", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevMom = default;
_hasPrev = default;
_cooldown = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var mom = new Momentum { Length = MomentumPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, mom, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal ema, decimal mom)
{
if (candle.State != CandleStates.Finished) return;
if (!IsFormedAndOnlineAndAllowTrading()) return;
var close = candle.ClosePrice;
if (!_hasPrev) { _prevMom = mom; _hasPrev = true; return; }
if (_cooldown > 0)
{
_cooldown--;
_prevMom = mom;
return;
}
if (close > ema && _prevMom <= 0 && mom > 0 && Position <= 0)
{
var volume = Volume + Math.Abs(Position);
BuyMarket(volume);
_cooldown = 2;
}
else if (close < ema && _prevMom >= 0 && mom < 0 && Position >= 0)
{
var volume = Volume + Math.Abs(Position);
SellMarket(volume);
_cooldown = 2;
}
_prevMom = mom;
}
}
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, Momentum
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class vq_ea_strategy(Strategy):
"""EMA trend filter with Momentum zero-cross for entries and cooldown."""
def __init__(self):
super(vq_ea_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetDisplay("EMA Period", "EMA filter", "Indicators")
self._mom_period = self.Param("MomentumPeriod", 14).SetDisplay("Momentum", "Momentum period", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(vq_ea_strategy, self).OnReseted()
self._prev_mom = 0
self._has_prev = False
self._cooldown = 0
def OnStarted2(self, time):
super(vq_ea_strategy, self).OnStarted2(time)
self._prev_mom = 0
self._has_prev = False
self._cooldown = 0
ema = ExponentialMovingAverage()
ema.Length = self._ema_period.Value
mom = Momentum()
mom.Length = self._mom_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(ema, mom, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def OnProcess(self, candle, ema_val, mom_val):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
ev = float(ema_val)
mv = float(mom_val)
close = float(candle.ClosePrice)
if not self._has_prev:
self._prev_mom = mv
self._has_prev = True
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_mom = mv
return
if close > ev and self._prev_mom <= 0 and mv > 0 and self.Position <= 0:
volume = self.Volume + abs(self.Position)
self.BuyMarket(volume)
self._cooldown = 2
elif close < ev and self._prev_mom >= 0 and mv < 0 and self.Position >= 0:
volume = self.Volume + abs(self.Position)
self.SellMarket(volume)
self._cooldown = 2
self._prev_mom = mv
def CreateClone(self):
return vq_ea_strategy()