DCA Strategy
This strategy buys a fixed quote amount at regular intervals, either on a chosen weekday or after a set number of candles. It optionally closes all positions after a specified end date.
Executable schedule and sizing
Default values: CandleType=5m, CapitalToInvest=200 quote-currency units, StartDate=DateTime.MinValue, EndDate=DateTime.MaxValue, CloseAllOnLastCandle=false, UseDayOfWeek=false, DayOfWeek=Wednesday, EveryXCandles=72.
On a finished candle inside StartDate <= candle date < EndDate, in interval mode purchase after every 72 eligible finished candles. In weekday mode purchase on the first eligible finished candle each Wednesday, at most once per calendar date. Compute rawVolume = 200 / Close and floor to Security.VolumeStep, honoring instrument minimum and maximum volume. Each purchase is a market buy; subsequent purchases add to the same accumulated long (no EMA signal and no shorts). At or after EndDate, stop buying; if CloseAllOnLastCandle=true, send one sell for the actual remaining position on the first eligible candle on/after that date. There is no independent price stop. A market fill can differ in quote value due to slippage. These defaults are implementation choices because the original README did not specify numerical values.
Parameters
- Candle Type
- Capital to invest per trade
- Start date
- End date
- Close all on last candle
- Use day of week mode
- Day of week
- Every X candles
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Fixed quote-currency investment either at regular completed-candle
/// intervals or once per configured weekday. Never shorts.
/// </summary>
public class DcaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _capitalToInvest;
private readonly StrategyParam<DateTime> _startDate;
private readonly StrategyParam<DateTime> _endDate;
private readonly StrategyParam<bool> _closeAllOnLastCandle;
private readonly StrategyParam<bool> _useDayOfWeek;
private readonly StrategyParam<DayOfWeek> _dayOfWeek;
private readonly StrategyParam<int> _everyXCandles;
private int _eligibleBars;
private DateTime? _lastWeekdayPurchase;
private bool _endReached;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public decimal CapitalToInvest { get => _capitalToInvest.Value; set => _capitalToInvest.Value = value; }
public DateTime StartDate { get => _startDate.Value; set => _startDate.Value = value; }
public DateTime EndDate { get => _endDate.Value; set => _endDate.Value = value; }
public bool CloseAllOnLastCandle { get => _closeAllOnLastCandle.Value; set => _closeAllOnLastCandle.Value = value; }
public bool UseDayOfWeek { get => _useDayOfWeek.Value; set => _useDayOfWeek.Value = value; }
public DayOfWeek DayOfWeek { get => _dayOfWeek.Value; set => _dayOfWeek.Value = value; }
public int EveryXCandles { get => _everyXCandles.Value; set => _everyXCandles.Value = value; }
public DcaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Completed-candle interval for scheduling", "General");
_capitalToInvest = Param(nameof(CapitalToInvest), 200m)
.SetGreaterThanZero()
.SetDisplay("Capital To Invest", "Fixed quote-currency amount for each purchase", "Trading");
_startDate = Param(nameof(StartDate), DateTime.MinValue)
.SetDisplay("Start Date", "Inclusive start of the buying range", "Calendar");
_endDate = Param(nameof(EndDate), DateTime.MaxValue)
.SetDisplay("End Date", "Exclusive end of the buying range", "Calendar");
_closeAllOnLastCandle = Param(nameof(CloseAllOnLastCandle), false)
.SetDisplay("Close All On Last Candle", "Liquidate accumulated holdings when EndDate is reached", "Calendar");
_useDayOfWeek = Param(nameof(UseDayOfWeek), false)
.SetDisplay("Use Day Of Week", "Purchase once per selected weekday instead of every N bars", "Calendar");
_dayOfWeek = Param(nameof(DayOfWeek), System.DayOfWeek.Wednesday)
.SetDisplay("Day Of Week", "Weekday for scheduled purchases", "Calendar");
_everyXCandles = Param(nameof(EveryXCandles), 72)
.SetGreaterThanZero()
.SetDisplay("Every X Candles", "Completed candles per periodic purchase", "Calendar");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_eligibleBars = 0;
_lastWeekdayPurchase = null;
_endReached = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (StartDate.Date > EndDate.Date)
throw new ArgumentException("StartDate must not be after EndDate.");
_eligibleBars = 0;
_lastWeekdayPurchase = null;
_endReached = false;
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 date = candle.OpenTime.Date;
if (date < StartDate.Date)
return;
// Treat EndDate as the first day on which new investments cease;
// optionally exit once, using actual holdings rather than Volume.
if (date >= EndDate.Date)
{
if (!_endReached && CloseAllOnLastCandle &&
Position > 0m && IsFormedAndOnlineAndAllowTrading())
{
SellMarket(Position);
_endReached = true;
}
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (UseDayOfWeek)
{
if (candle.OpenTime.DayOfWeek != DayOfWeek ||
_lastWeekdayPurchase == date)
return;
}
else
{
_eligibleBars++;
if (_eligibleBars % EveryXCandles != 0)
return;
}
var price = candle.ClosePrice;
if (price <= 0m || Security is null)
return;
// The budget is expressed in quoted currency. Floor to the native
// volume step and never silently round up into over-investment.
var step = Security.VolumeStep is > 0m ? Security.VolumeStep.Value : 0.001m;
var maxVolume = Security.MaxVolume is > 0m ? Security.MaxVolume.Value : decimal.MaxValue;
var budgetVolume = Math.Min(CapitalToInvest / price, maxVolume);
var volume = Math.Floor(budgetVolume / step) * step;
if (volume <= 0m || (Security.MinVolume > 0m && volume < Security.MinVolume))
return;
BuyMarket(volume);
if (UseDayOfWeek)
_lastWeekdayPurchase = date;
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateTime, DayOfWeek, TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class dca_strategy(Strategy):
"""Buy a fixed quote-currency amount on a weekday or every N finished candles."""
def __init__(self):
super(dca_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Completed-candle schedule", "General")
self._capital_to_invest = self.Param("CapitalToInvest", 200.0) \
.SetGreaterThanZero() \
.SetDisplay("Capital To Invest", "Fixed quote-currency amount per purchase", "Trading")
self._start_date = self.Param("StartDate", DateTime.MinValue) \
.SetDisplay("Start Date", "Inclusive buying start", "Calendar")
self._end_date = self.Param("EndDate", DateTime.MaxValue) \
.SetDisplay("End Date", "Exclusive buying end", "Calendar")
self._close_all_on_last_candle = self.Param("CloseAllOnLastCandle", False) \
.SetDisplay("Close All On Last Candle", "Close holdings when EndDate is reached", "Calendar")
self._use_day_of_week = self.Param("UseDayOfWeek", False) \
.SetDisplay("Use Day Of Week", "Use weekday rather than N bars", "Calendar")
self._day_of_week = self.Param("DayOfWeek", DayOfWeek.Wednesday) \
.SetDisplay("Day Of Week", "Selected purchase weekday", "Calendar")
self._every_x_candles = self.Param("EveryXCandles", 72) \
.SetGreaterThanZero() \
.SetDisplay("Every X Candles", "Period of purchases in completed candles", "Calendar")
self._eligible_bars = 0
self._last_weekday_purchase = None
self._end_reached = False
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(dca_strategy, self).OnReseted()
self._eligible_bars = 0
self._last_weekday_purchase = None
self._end_reached = False
def OnStarted2(self, time):
super(dca_strategy, self).OnStarted2(time)
if self._start_date.Value.Date > self._end_date.Value.Date:
raise ValueError("StartDate must not be after EndDate")
self._eligible_bars = 0
self._last_weekday_purchase = None
self._end_reached = False
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
date = candle.OpenTime.Date
if date < self._start_date.Value.Date:
return
if date >= self._end_date.Value.Date:
if not self._end_reached and bool(self._close_all_on_last_candle.Value) and self.Position > 0 and self.IsFormedAndOnlineAndAllowTrading():
self.SellMarket(self.Position)
self._end_reached = True
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
weekday_mode = bool(self._use_day_of_week.Value)
if weekday_mode:
if candle.OpenTime.DayOfWeek != self._day_of_week.Value or self._last_weekday_purchase == date:
return
else:
self._eligible_bars += 1
if self._eligible_bars % int(self._every_x_candles.Value) != 0:
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")
# Never round a quote-valued investment UP above the budget.
volume = math.floor(min(float(self._capital_to_invest.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 weekday_mode:
self._last_weekday_purchase = date
def CreateClone(self):
return dca_strategy()