DCA 平均回帰 Bollinger Band 戦略
価格がBollinger Bandの下限バンドを下抜けたとき、または毎月1日目に固定ドル額を購入します。指定した日付にすべてのポジションを決済します。
C# / Python の厳密な取引条件
既定値: InvestmentAmount=200 建値通貨単位、OpenDate=DateTime.MinValue、CloseDate=DateTime.MaxValue、StrategyMode=Combined、BollingerPeriod=20、BollingerMultiplier=2、CandleType=5分足。確定足のボリンジャー下限は SMA20 から標準偏差2倍を引いた値です。Bollinger 買いは、前回の Close >= 前回の下限 から現在の Close < 現在の下限 に抜けた時。Monthly は指標が形成された後、月に1回最初の有効な確定足で購入します。Bollinger は下抜けのみ、Monthly は月次のみ、Combined は両方(同時なら注文1回)。OpenDate <= 日付 < CloseDate の間、数量は floor((InvestmentAmount / Close) / VolumeStep) × VolumeStep に切り捨て、数量制限に従います。ロング積み増しのみでショートは行いません。CloseDate 到達後、最初の有効な足で実際の残高を全量売却し、その後の購入を停止します。価格ストップ、ショート、EMAクロスはなく、スリッページで約定金額が変わる場合があります。元のREADMEに数値の既定値がなかったため、この実装で定義しました。
パラメータ
InvestmentAmount- 毎回の投資金額OpenDate- 購入開始日CloseDate- 全ポジション決済日StrategyMode- BB平均回帰、月次DCA、または組み合わせBollingerPeriod- Bollinger Bandsの期間BollingerMultiplier- 標準偏差の乗数CandleType- Bollinger計算の時間軸
インジケーター
- Bollinger Bands
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()