DCA 均值回归布林带策略
价格跌破下轨或每月第一天买入固定美元金额,在设定日期平掉所有仓位。
C# / Python 的精确交易规则
默认:InvestmentAmount=200 计价货币单位、OpenDate=DateTime.MinValue、CloseDate=DateTime.MaxValue、StrategyMode=Combined、BollingerPeriod=20、BollingerMultiplier=2、CandleType=5分钟。在已完成K线上,布林下轨为 SMA20 减去两个标准差。Bollinger 买入条件:上一根 Close >= 当时下轨,当前 Close < 当前下轨。Monthly 条件为每月最多买一次,选择指标预热后的首个有效已完成K线。模式 Bollinger 仅突破买入、Monthly 每月买入、Combined 任一条件买入(同根只下单一次)。在 OpenDate <= 日期 < CloseDate 内,数量 floor((InvestmentAmount / Close) / VolumeStep) × VolumeStep,遵守最小/最大下单量。仅累积多仓,不做空。到 CloseDate 的首个有效交易K线卖出全部实际剩余仓位,之后不再买入。无止损、空头或 EMA 交叉信号,滑点可能影响成交金额。原README未给出数值默认值,以上是明确采用的实现配置。
参数
InvestmentAmount- 每次投入金额OpenDate- 开始买入日期CloseDate- 平仓日期StrategyMode- BB 均值回归、每月 DCA 或两者结合BollingerPeriod- 布林带周期BollingerMultiplier- 标准差倍数CandleType- 布林带计算的时间框
指标
- 布林带
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>
/// Long-only fixed-quote DCA on lower Bollinger band crossings, calendar
/// month changes, or both. Liquidates accumulated holdings on CloseDate.
/// </summary>
public class DcaMeanReversionBollingerBandStrategy : Strategy
{
private readonly StrategyParam<decimal> _investmentAmount;
private readonly StrategyParam<DateTime> _openDate;
private readonly StrategyParam<DateTime> _closeDate;
private readonly StrategyParam<string> _strategyMode;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerMultiplier;
private readonly StrategyParam<DataType> _candleType;
private BollingerBands _bands;
private decimal? _previousClose;
private decimal? _previousLower;
private int _lastInvestmentMonth;
private int _lastInvestmentYear;
private bool _liquidated;
public decimal InvestmentAmount { get => _investmentAmount.Value; set => _investmentAmount.Value = value; }
public DateTime OpenDate { get => _openDate.Value; set => _openDate.Value = value; }
public DateTime CloseDate { get => _closeDate.Value; set => _closeDate.Value = value; }
public string StrategyMode { get => _strategyMode.Value; set => _strategyMode.Value = value; }
public int BollingerPeriod { get => _bollingerPeriod.Value; set => _bollingerPeriod.Value = value; }
public decimal BollingerMultiplier { get => _bollingerMultiplier.Value; set => _bollingerMultiplier.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DcaMeanReversionBollingerBandStrategy()
{
_investmentAmount = Param(nameof(InvestmentAmount), 200m)
.SetGreaterThanZero()
.SetDisplay("Investment Amount", "Fixed quoted-currency amount per buy", "Trading");
_openDate = Param(nameof(OpenDate), DateTime.MinValue)
.SetDisplay("Open Date", "Inclusive first buy date", "Calendar");
_closeDate = Param(nameof(CloseDate), DateTime.MaxValue)
.SetDisplay("Close Date", "Exclusive end date; sell all on its first available candle", "Calendar");
_strategyMode = Param(nameof(StrategyMode), "Combined")
.SetDisplay("Strategy Mode", "Bollinger, Monthly or Combined", "Signals");
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Bollinger Bands averaging period", "Indicators");
_bollingerMultiplier = Param(nameof(BollingerMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Multiplier", "Standard deviations for the lower band", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Completed-candle interval", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_bands = null;
_previousClose = null;
_previousLower = null;
_lastInvestmentMonth = 0;
_lastInvestmentYear = 0;
_liquidated = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (OpenDate.Date > CloseDate.Date)
throw new ArgumentException("OpenDate must not be after CloseDate.");
if (StrategyMode != "Bollinger" && StrategyMode != "Monthly" && StrategyMode != "Combined")
throw new ArgumentException("StrategyMode must be Bollinger, Monthly or Combined.");
_bands = new BollingerBands { Length = BollingerPeriod, Width = BollingerMultiplier };
_previousClose = null;
_previousLower = null;
_lastInvestmentMonth = 0;
_lastInvestmentYear = 0;
_liquidated = false;
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(_bands, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _bands);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue value)
{
if (candle.State != CandleStates.Finished)
return;
var date = candle.OpenTime.Date;
if (date < OpenDate.Date)
return;
// Liquidation does not depend on a new Bollinger crossing.
if (date >= CloseDate.Date)
{
if (!_liquidated && Position > 0m && IsFormedAndOnlineAndAllowTrading())
{
SellMarket(Position);
_liquidated = true;
}
return;
}
if (value is not BollingerBandsValue bandsValue ||
bandsValue.LowBand is not decimal lowerBand)
return;
var close = candle.ClosePrice;
var crossBelow = _previousClose is decimal oldClose &&
_previousLower is decimal oldLower &&
oldClose >= oldLower && close < lowerBand;
_previousClose = close;
_previousLower = lowerBand;
if (!_bands.IsFormed || !IsFormedAndOnlineAndAllowTrading())
return;
var monthly = StrategyMode != "Bollinger" &&
(_lastInvestmentMonth != date.Month || _lastInvestmentYear != date.Year);
var bollinger = StrategyMode != "Monthly" && crossBelow;
if (!monthly && !bollinger)
return;
var step = Security?.VolumeStep ?? 0m;
if (step <= 0m)
step = 0.001m;
var price = candle.ClosePrice;
if (price <= 0m)
return;
var maxVolume = Security.MaxVolume is > 0m ? Security.MaxVolume.Value : decimal.MaxValue;
var volume = Math.Floor(Math.Min(InvestmentAmount / price, maxVolume) / step) * step;
if (volume <= 0m || (Security.MinVolume > 0m && volume < Security.MinVolume))
return;
BuyMarket(volume);
if (monthly)
{
_lastInvestmentMonth = date.Month;
_lastInvestmentYear = date.Year;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateTime, TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import BollingerBands
from StockSharp.Algo.Strategies import Strategy
class dca_mean_reversion_bollinger_band_strategy(Strategy):
"""Invest a fixed quote amount on Bollinger down-cross or month change."""
def __init__(self):
super(dca_mean_reversion_bollinger_band_strategy, self).__init__()
self._investment_amount = self.Param("InvestmentAmount", 200.0) \
.SetGreaterThanZero() \
.SetDisplay("Investment Amount", "Quoted-currency amount for each buy", "Trading")
self._open_date = self.Param("OpenDate", DateTime.MinValue) \
.SetDisplay("Open Date", "Inclusive first buying date", "Calendar")
self._close_date = self.Param("CloseDate", DateTime.MaxValue) \
.SetDisplay("Close Date", "Exclusive end date for full liquidation", "Calendar")
self._strategy_mode = self.Param("StrategyMode", "Combined") \
.SetDisplay("Strategy Mode", "Bollinger, Monthly or Combined", "Signals")
self._bollinger_period = self.Param("BollingerPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Bollinger period", "Indicators")
self._bollinger_multiplier = self.Param("BollingerMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Multiplier", "Standard deviations for the lower band", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Completed-candle interval", "General")
self._bands = None
self._previous_close = None
self._previous_lower = None
self._last_monthly_year = 0
self._last_monthly_month = 0
self._liquidated = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(dca_mean_reversion_bollinger_band_strategy, self).OnReseted()
self._bands = None
self._previous_close = None
self._previous_lower = None
self._last_monthly_year = 0
self._last_monthly_month = 0
self._liquidated = False
def OnStarted2(self, time):
super(dca_mean_reversion_bollinger_band_strategy, self).OnStarted2(time)
if self._open_date.Value.Date > self._close_date.Value.Date:
raise ValueError("OpenDate must not be after CloseDate")
if str(self._strategy_mode.Value) not in ("Bollinger", "Monthly", "Combined"):
raise ValueError("StrategyMode must be Bollinger, Monthly or Combined")
self._bands = BollingerBands()
self._bands.Length = int(self._bollinger_period.Value)
self._bands.Width = Decimal(self._bollinger_multiplier.Value)
self._previous_close = None
self._previous_lower = None
self._last_monthly_year = 0
self._last_monthly_month = 0
self._liquidated = False
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._bands, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._bands)
self.DrawOwnTrades(area)
def _process_candle(self, candle, bands_value):
if candle.State != CandleStates.Finished:
return
date = candle.OpenTime.Date
if date < self._open_date.Value.Date:
return
if date >= self._close_date.Value.Date:
if not self._liquidated and self.Position > 0 and self.IsFormedAndOnlineAndAllowTrading():
self.SellMarket(self.Position)
self._liquidated = True
return
lower = bands_value.LowBand
if lower is None:
return
close = float(candle.ClosePrice)
low = float(lower)
cross_below = (self._previous_close is not None and
self._previous_lower is not None and
self._previous_close >= self._previous_lower and close < low)
self._previous_close = close
self._previous_lower = low
if not self._bands.IsFormed or not self.IsFormedAndOnlineAndAllowTrading():
return
mode = str(self._strategy_mode.Value)
monthly = mode != "Bollinger" and (
self._last_monthly_year != date.Year or self._last_monthly_month != date.Month
)
bollinger = mode != "Monthly" and cross_below
if not monthly and not bollinger:
return
price = float(candle.ClosePrice)
if price <= 0 or self.Security is None:
return
step = float(self.Security.VolumeStep)
if step <= 0:
step = 0.001
max_volume = float(self.Security.MaxVolume)
if max_volume <= 0:
max_volume = float("inf")
volume = math.floor(min(float(self._investment_amount.Value) / price, max_volume) / step) * step
min_volume = float(self.Security.MinVolume)
if volume <= 0 or (min_volume > 0 and volume < min_volume):
return
self.BuyMarket(Decimal.Parse(str(volume)))
if monthly:
self._last_monthly_year = date.Year
self._last_monthly_month = date.Month
def CreateClone(self):
return dca_mean_reversion_bollinger_band_strategy()