分时段成交量策略
本策略源自 TradingView 指标 "Volume by Session" 的简化版本。将交易日划分为四个时段,每个时段都有自己的平均成交量。当某个时段的当前成交量偏离其平均值时,策略据此开仓或平仓。
详情
- 入场:当前时段的成交量高于或低于其平均值。
- 出场:出现反向信号时平仓。
- 多空:双向。
- 指标:SMA。
- 时间框架:日内。
该版本仅用于教学目的,省略了原脚本的可视化和高级选项。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volume by session strategy.
/// Tracks average volume and trades on volume deviations with price confirmation.
/// Buys when volume spikes above average and price is rising, sells when opposite.
/// </summary>
public class VolumeBySessionStrategy : Strategy
{
private readonly StrategyParam<int> _volAvgLength;
private readonly StrategyParam<decimal> _volMult;
private readonly StrategyParam<decimal> _stopPct;
private readonly StrategyParam<decimal> _tpPct;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _signalCooldownBars;
private readonly List<decimal> _volumes = new();
private decimal _volumeSum;
private decimal _entryPrice;
private decimal _stopDist;
private decimal? _prevHigh;
private decimal? _prevLow;
private int _cooldownRemaining;
public int VolAvgLength { get => _volAvgLength.Value; set => _volAvgLength.Value = value; }
public decimal VolMult { get => _volMult.Value; set => _volMult.Value = value; }
public decimal StopPct { get => _stopPct.Value; set => _stopPct.Value = value; }
public decimal TpPct { get => _tpPct.Value; set => _tpPct.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }
public VolumeBySessionStrategy()
{
_volAvgLength = Param(nameof(VolAvgLength), 20)
.SetGreaterThanZero()
.SetDisplay("Vol Avg Length", "Volume average period", "Parameters");
_volMult = Param(nameof(VolMult), 2.25m)
.SetGreaterThanZero()
.SetDisplay("Vol Multiplier", "Volume spike multiplier", "Parameters");
_stopPct = Param(nameof(StopPct), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Stop %", "Stop loss percent", "Risk");
_tpPct = Param(nameof(TpPct), 1m)
.SetGreaterThanZero()
.SetDisplay("TP %", "Take profit percent", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between new entries", "Trading");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_volumes.Clear();
_volumeSum = 0m;
_entryPrice = 0;
_stopDist = 0;
_prevHigh = null;
_prevLow = null;
_cooldownRemaining = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_volumes.Clear();
_volumeSum = 0m;
_entryPrice = 0;
_stopDist = 0;
_prevHigh = null;
_prevLow = null;
_cooldownRemaining = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var vol = candle.TotalVolume;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
// TP/SL
if (Position > 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice <= _entryPrice - _stopDist || candle.ClosePrice >= _entryPrice + _stopDist * (TpPct / StopPct))
{
SellMarket(Position);
_entryPrice = 0;
_stopDist = 0;
_cooldownRemaining = SignalCooldownBars;
}
}
else if (Position < 0 && _entryPrice > 0 && _stopDist > 0)
{
if (candle.ClosePrice >= _entryPrice + _stopDist || candle.ClosePrice <= _entryPrice - _stopDist * (TpPct / StopPct))
{
BuyMarket(Math.Abs(Position));
_entryPrice = 0;
_stopDist = 0;
_cooldownRemaining = SignalCooldownBars;
}
}
if (_volumes.Count >= VolAvgLength && _prevHigh.HasValue && _prevLow.HasValue && _cooldownRemaining == 0 && Position == 0)
{
var avgVol = _volumeSum / _volumes.Count;
var highVol = vol >= avgVol * VolMult;
var bullishBreakout = candle.ClosePrice > _prevHigh.Value && candle.ClosePrice > candle.OpenPrice;
var bearishBreakout = candle.ClosePrice < _prevLow.Value && candle.ClosePrice < candle.OpenPrice;
var minBody = Math.Max(candle.ClosePrice * 0.001m, Security?.PriceStep ?? 0m);
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
if (highVol && bullishBreakout && body >= minBody)
{
BuyMarket(Volume);
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
else if (highVol && bearishBreakout && body >= minBody)
{
SellMarket(Volume);
_entryPrice = candle.ClosePrice;
_stopDist = candle.ClosePrice * StopPct / 100m;
_cooldownRemaining = SignalCooldownBars;
}
}
_volumes.Add(vol);
_volumeSum += vol;
if (_volumes.Count > VolAvgLength)
{
_volumeSum -= _volumes[0];
_volumes.RemoveAt(0);
}
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
}
}
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.Strategies import Strategy
class volume_by_session_strategy(Strategy):
def __init__(self):
super(volume_by_session_strategy, self).__init__()
self._vol_avg_length = self.Param("VolAvgLength", 20) \
.SetDisplay("Vol Avg Length", "Volume average period", "Parameters")
self._vol_mult = self.Param("VolMult", 2.25) \
.SetDisplay("Vol Multiplier", "Volume spike multiplier", "Parameters")
self._stop_pct = self.Param("StopPct", 0.5) \
.SetDisplay("Stop %", "Stop loss percent", "Risk")
self._tp_pct = self.Param("TpPct", 1.0) \
.SetDisplay("TP %", "Take profit percent", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 12) \
.SetDisplay("Signal Cooldown", "Bars to wait between new entries", "Trading")
self._volumes = []
self._volume_sum = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._prev_high = None
self._prev_low = None
self._cooldown_remaining = 0
@property
def vol_avg_length(self):
return self._vol_avg_length.Value
@property
def vol_mult(self):
return self._vol_mult.Value
@property
def stop_pct(self):
return self._stop_pct.Value
@property
def tp_pct(self):
return self._tp_pct.Value
@property
def candle_type(self):
return self._candle_type.Value
@property
def signal_cooldown_bars(self):
return self._signal_cooldown_bars.Value
def OnReseted(self):
super(volume_by_session_strategy, self).OnReseted()
self._volumes = []
self._volume_sum = 0.0
self._entry_price = 0.0
self._stop_dist = 0.0
self._prev_high = None
self._prev_low = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(volume_by_session_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def on_process(self, candle):
if candle.State != CandleStates.Finished:
return
vol = float(candle.TotalVolume)
close = float(candle.ClosePrice)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
# TP/SL
tp_pct = float(self.tp_pct)
stop_pct = float(self.stop_pct)
if self.Position > 0 and self._entry_price > 0 and self._stop_dist > 0:
if close <= self._entry_price - self._stop_dist or close >= self._entry_price + self._stop_dist * (tp_pct / stop_pct):
self.SellMarket()
self._entry_price = 0
self._stop_dist = 0
self._cooldown_remaining = self.signal_cooldown_bars
elif self.Position < 0 and self._entry_price > 0 and self._stop_dist > 0:
if close >= self._entry_price + self._stop_dist or close <= self._entry_price - self._stop_dist * (tp_pct / stop_pct):
self.BuyMarket()
self._entry_price = 0
self._stop_dist = 0
self._cooldown_remaining = self.signal_cooldown_bars
if len(self._volumes) >= self.vol_avg_length and self._prev_high is not None and self._prev_low is not None and self._cooldown_remaining == 0 and self.Position == 0:
avg_vol = self._volume_sum / len(self._volumes)
high_vol = vol >= avg_vol * float(self.vol_mult)
bullish_breakout = close > self._prev_high and close > float(candle.OpenPrice)
bearish_breakout = close < self._prev_low and close < float(candle.OpenPrice)
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
min_body = max(float(candle.ClosePrice) * 0.001, price_step)
body = abs(float(candle.ClosePrice) - float(candle.OpenPrice))
if high_vol and bullish_breakout and body >= min_body:
self.BuyMarket()
self._entry_price = close
self._stop_dist = close * stop_pct / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
elif high_vol and bearish_breakout and body >= min_body:
self.SellMarket()
self._entry_price = close
self._stop_dist = close * stop_pct / 100.0
self._cooldown_remaining = self.signal_cooldown_bars
self._volumes.append(vol)
self._volume_sum += vol
if len(self._volumes) > self.vol_avg_length:
self._volume_sum -= self._volumes[0]
self._volumes.pop(0)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
def CreateClone(self):
return volume_by_session_strategy()