GitHub で見る

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;
        }
    }
}