Liquid Pulse Strategy
该策略检测由 MACD 和 ADX 确认的高成交量激增。ATR 定义止损和止盈,并限制每日交易次数。
细节
- 入场条件:
- 多头:成交量激增,MACD 上穿信号线,+DI > -DI,ADX ≥ 阈值
- 空头:成交量激增,MACD 下穿信号线,-DI > +DI,ADX ≥ 阈值
- 方向:双向
- 出场条件:基于 ATR 的止损或止盈
- 止损:ATR 倍数
- 默认值:
VolumeSensitivity= MediumMacdSpeed= MediumDailyTradeLimit= 20AtrPeriod= 9AdxTrendThreshold= 41
- 筛选:
- 类别:趋势跟随
- 方向:双向
- 指标:MACD、ADX、ATR、成交量
- 止损:是
- 复杂度:中等
- 周期:中期
- 季节性:否
- 神经网络:否
- 背离:否
- 风险等级:中等
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>
/// Liquid Pulse strategy.
/// Detects high volume spikes confirmed by MACD and ADX.
/// ATR defines stop and take profit; limits trades per day.
/// </summary>
public class LiquidPulseStrategy : Strategy
{
public enum VolumeSensitivityLevels { Low, Medium, High }
public enum MacdSpeedOptions { Fast, Medium, Slow }
private readonly StrategyParam<VolumeSensitivityLevels> _volumeSensitivity;
private readonly StrategyParam<MacdSpeedOptions> _macdSpeed;
private readonly StrategyParam<int> _dailyTradeLimit;
private readonly StrategyParam<int> _adxTrendThreshold;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergenceSignal _macd;
private AverageDirectionalIndex _adx;
private AverageTrueRange _atr;
private SimpleMovingAverage _volSma;
private decimal _prevMacd, _prevSignal, _entryPrice, _stop, _tp;
private DateTime _day;
private int _dailyTrades;
private int _volLookback;
private decimal _volThreshold;
public VolumeSensitivityLevels VolumeSensitivity { get => _volumeSensitivity.Value; set => _volumeSensitivity.Value = value; }
public MacdSpeedOptions MacdSpeed { get => _macdSpeed.Value; set => _macdSpeed.Value = value; }
public int DailyTradeLimit { get => _dailyTradeLimit.Value; set => _dailyTradeLimit.Value = value; }
public int AdxTrendThreshold { get => _adxTrendThreshold.Value; set => _adxTrendThreshold.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public LiquidPulseStrategy()
{
_volumeSensitivity = Param(nameof(VolumeSensitivity), VolumeSensitivityLevels.Medium)
.SetDisplay("Volume Sensitivity", "Volume sensitivity", "General");
_macdSpeed = Param(nameof(MacdSpeed), MacdSpeedOptions.Medium)
.SetDisplay("MACD Speed", "MACD speed", "General");
_dailyTradeLimit = Param(nameof(DailyTradeLimit), 20)
.SetDisplay("Daily Trade Limit", "Max trades per day", "Risk");
_adxTrendThreshold = Param(nameof(AdxTrendThreshold), 20)
.SetDisplay("ADX Trend Threshold", "Trend threshold", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 9)
.SetDisplay("ATR Period", "ATR period", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevMacd = 0m;
_prevSignal = 0m;
_entryPrice = 0m;
_stop = 0m;
_tp = 0m;
_day = default;
_dailyTrades = 0;
_volLookback = 0;
_volThreshold = 0m;
_macd = null;
_adx = null;
_atr = null;
_volSma = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevMacd = _prevSignal = 0m;
_entryPrice = _stop = _tp = 0m;
_day = default;
_dailyTrades = 0;
(_volLookback, _volThreshold) = VolumeSensitivity switch
{
VolumeSensitivityLevels.Low => (30, 1.5m),
VolumeSensitivityLevels.Medium => (20, 1.2m),
_ => (11, 1.0m)
};
_volSma = new SMA { Length = _volLookback };
var (fast, slow, signal) = MacdSpeed switch
{
MacdSpeedOptions.Fast => (2, 7, 5),
MacdSpeedOptions.Medium => (5, 13, 9),
_ => (12, 26, 9)
};
_macd = new()
{
Macd = { ShortMa = { Length = fast }, LongMa = { Length = slow } },
SignalMa = { Length = signal }
};
_adx = new() { Length = 14 };
_atr = new() { Length = AtrPeriod };
var sub = SubscribeCandles(CandleType);
sub.BindEx(_macd, _adx, _atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, sub);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdVal, IIndicatorValue adxVal, IIndicatorValue atrVal)
{
if (candle.State != CandleStates.Finished || !macdVal.IsFinal || !adxVal.IsFinal || !atrVal.IsFinal)
return;
var day = candle.OpenTime.Date;
if (day != _day)
{
_day = day;
_dailyTrades = 0;
}
// Volume spike detection
var volInput = new DecimalIndicatorValue(_volSma, candle.TotalVolume, candle.ServerTime) { IsFinal = true };
var avgVolResult = _volSma.Process(volInput);
var avgVol = avgVolResult.IsEmpty ? 0m : avgVolResult.ToDecimal();
var highVol = avgVol > 0m && candle.TotalVolume >= _volThreshold * avgVol;
if (macdVal is not IMovingAverageConvergenceDivergenceSignalValue macdTyped)
return;
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
return;
if (adxVal is not IAverageDirectionalIndexValue adxTyped)
return;
if (adxTyped.MovingAverage is not decimal adx)
return;
if (adxTyped.Dx is not IDirectionalIndexValue dxVal)
return;
if (dxVal.Plus is not decimal plusDi || dxVal.Minus is not decimal minusDi)
return;
var atr = atrVal.IsEmpty ? 0m : atrVal.ToDecimal();
var bull = _prevMacd <= _prevSignal && macd > signal && plusDi > minusDi && adx >= AdxTrendThreshold;
var bear = _prevMacd >= _prevSignal && macd < signal && minusDi > plusDi && adx >= AdxTrendThreshold;
// Check stops/TP for existing position
if (Position > 0 && _stop > 0 && (candle.LowPrice <= _stop || candle.HighPrice >= _tp))
{
SellMarket(Math.Abs(Position));
_entryPrice = _stop = _tp = 0m;
}
else if (Position < 0 && _stop > 0 && (candle.HighPrice >= _stop || candle.LowPrice <= _tp))
{
BuyMarket(Math.Abs(Position));
_entryPrice = _stop = _tp = 0m;
}
if (highVol && _dailyTrades < DailyTradeLimit && atr > 0)
{
if (bull && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_entryPrice = candle.ClosePrice;
_stop = _entryPrice - atr * 1.5m;
_tp = _entryPrice + atr * 2m;
_dailyTrades++;
}
else if (bear && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_entryPrice = candle.ClosePrice;
_stop = _entryPrice + atr * 1.5m;
_tp = _entryPrice - atr * 2m;
_dailyTrades++;
}
}
_prevMacd = macd;
_prevSignal = signal;
}
}
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
from StockSharp.Algo.Indicators import (
MovingAverageConvergenceDivergenceSignal,
AverageDirectionalIndex,
AverageTrueRange,
SimpleMovingAverage,
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class liquid_pulse_strategy(Strategy):
"""
Liquid Pulse strategy.
Detects high volume spikes confirmed by MACD and ADX.
ATR defines stop and take profit; limits trades per day.
"""
def __init__(self):
super(liquid_pulse_strategy, self).__init__()
# 0=Low, 1=Medium, 2=High
self._volume_sensitivity = self.Param("VolumeSensitivity", 1) \
.SetDisplay("Volume Sensitivity", "Volume sensitivity", "General")
# 0=Fast, 1=Medium, 2=Slow
self._macd_speed = self.Param("MacdSpeed", 1) \
.SetDisplay("MACD Speed", "MACD speed", "General")
self._daily_trade_limit = self.Param("DailyTradeLimit", 20) \
.SetDisplay("Daily Trade Limit", "Max trades per day", "Risk")
self._adx_trend_threshold = self.Param("AdxTrendThreshold", 20) \
.SetDisplay("ADX Trend Threshold", "Trend threshold", "Indicators")
self._atr_period = self.Param("AtrPeriod", 9) \
.SetDisplay("ATR Period", "ATR period", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._prev_macd = 0.0
self._prev_signal = 0.0
self._entry_price = 0.0
self._stop = 0.0
self._tp = 0.0
self._day = None
self._daily_trades = 0
self._vol_lookback = 0
self._vol_threshold = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(liquid_pulse_strategy, self).OnReseted()
self._prev_macd = 0.0
self._prev_signal = 0.0
self._entry_price = 0.0
self._stop = 0.0
self._tp = 0.0
self._day = None
self._daily_trades = 0
self._vol_lookback = 0
self._vol_threshold = 0.0
self._macd = None
self._adx = None
self._atr = None
self._vol_sma = None
def OnStarted2(self, time):
super(liquid_pulse_strategy, self).OnStarted2(time)
self._prev_macd = 0.0
self._prev_signal = 0.0
self._entry_price = 0.0
self._stop = 0.0
self._tp = 0.0
self._day = None
self._daily_trades = 0
vol_sens = self._volume_sensitivity.Value
if vol_sens == 0: # Low
self._vol_lookback = 30
self._vol_threshold = 1.5
elif vol_sens == 1: # Medium
self._vol_lookback = 20
self._vol_threshold = 1.2
else: # High
self._vol_lookback = 11
self._vol_threshold = 1.0
self._vol_sma = SimpleMovingAverage()
self._vol_sma.Length = self._vol_lookback
macd_spd = self._macd_speed.Value
if macd_spd == 0: # Fast
fast_len, slow_len, sig_len = 2, 7, 5
elif macd_spd == 1: # Medium
fast_len, slow_len, sig_len = 5, 13, 9
else: # Slow
fast_len, slow_len, sig_len = 12, 26, 9
self._macd = MovingAverageConvergenceDivergenceSignal()
self._macd.Macd.ShortMa.Length = fast_len
self._macd.Macd.LongMa.Length = slow_len
self._macd.SignalMa.Length = sig_len
self._adx = AverageDirectionalIndex()
self._adx.Length = 14
self._atr = AverageTrueRange()
self._atr.Length = self._atr_period.Value
sub = self.SubscribeCandles(self.candle_type)
sub.BindEx(self._macd, self._adx, self._atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def _process_candle(self, candle, macd_val, adx_val, atr_val):
if candle.State != CandleStates.Finished:
return
if not macd_val.IsFinal or not adx_val.IsFinal or not atr_val.IsFinal:
return
day = candle.OpenTime.Date
if self._day is None or self._day != day:
self._day = day
self._daily_trades = 0
# Volume spike detection
avg_vol_result = process_float(self._vol_sma, candle.TotalVolume, candle.ServerTime, True)
avg_vol = 0.0 if avg_vol_result.IsEmpty else float(avg_vol_result)
high_vol = avg_vol > 0 and float(candle.TotalVolume) >= self._vol_threshold * avg_vol
# MACD values
macd_typed = macd_val
macd_m = macd_typed.Macd
signal_s = macd_typed.Signal
if macd_m is None or signal_s is None:
return
macd_m = float(macd_m)
signal_s = float(signal_s)
# ADX values
adx_typed = adx_val
adx_ma = adx_typed.MovingAverage
if adx_ma is None:
return
adx_ma = float(adx_ma)
dx_val = adx_typed.Dx
if dx_val is None:
return
plus_di = dx_val.Plus
minus_di = dx_val.Minus
if plus_di is None or minus_di is None:
return
plus_di = float(plus_di)
minus_di = float(minus_di)
atr = 0.0 if atr_val.IsEmpty else float(atr_val)
adx_threshold = self._adx_trend_threshold.Value
bull = self._prev_macd <= self._prev_signal and macd_m > signal_s and plus_di > minus_di and adx_ma >= adx_threshold
bear = self._prev_macd >= self._prev_signal and macd_m < signal_s and minus_di > plus_di and adx_ma >= adx_threshold
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
# Check stops/TP for existing position
if self.Position > 0 and self._stop > 0 and (low <= self._stop or high >= self._tp):
self.SellMarket(abs(self.Position))
self._entry_price = 0.0
self._stop = 0.0
self._tp = 0.0
elif self.Position < 0 and self._stop > 0 and (high >= self._stop or low <= self._tp):
self.BuyMarket(abs(self.Position))
self._entry_price = 0.0
self._stop = 0.0
self._tp = 0.0
if high_vol and self._daily_trades < self._daily_trade_limit.Value and atr > 0:
if bull and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
self._entry_price = close
self._stop = self._entry_price - atr * 1.5
self._tp = self._entry_price + atr * 2.0
self._daily_trades += 1
elif bear and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
self._entry_price = close
self._stop = self._entry_price + atr * 1.5
self._tp = self._entry_price - atr * 2.0
self._daily_trades += 1
self._prev_macd = macd_m
self._prev_signal = signal_s
def CreateClone(self):
return liquid_pulse_strategy()