Payday Anomaly Strategy
This strategy exploits the "payday" effect by holding a broad market ETF around typical salary payment dates. The ETF is owned from two trading days before month-end through the third trading day of the new month, capturing inflows from paycheck contributions.
The rest of the month the portfolio is in cash. Daily candles determine the window and market orders adjust the position.
Details
- Instrument: broad market ETF.
- Window: from two days before month end to third trading day of next month.
- Positioning: long during window, flat otherwise.
- Data: daily candles.
- Risk control: trade skipped if order value below
MinTradeUsd.
// PaydayAnomalyStrategy.cs
// -----------------------------------------------------------------------------
// Holds market ETF only during days -2..+3 around typical U.S. payday
// (assume salary hits 1st business day of month). Long ETF from two trading
// days before month‑end through third trading day of new month.
// Trigger: daily candle close.
// -----------------------------------------------------------------------------
// Date: 2 Aug 2025
// -----------------------------------------------------------------------------
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Payday anomaly strategy.
/// Holds market ETF during the payday window.
/// </summary>
public class PaydayAnomalyStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
/// <summary>
/// The type of candles to use for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
private readonly Dictionary<Security, decimal> _latestPrices = [];
private DateTime _last = DateTime.MinValue;
private int _enteredMonthKey;
private int _exitedMonthKey;
public PaydayAnomalyStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
public override IEnumerable<(Security, DataType)> GetWorkingSecurities()
{
if (Security == null)
throw new InvalidOperationException("Security not set");
yield return (Security, CandleType);
}
protected override void OnReseted()
{
base.OnReseted();
_latestPrices.Clear();
_last = default;
_enteredMonthKey = 0;
_exitedMonthKey = 0;
}
protected override void OnStarted2(DateTime time)
{
if (Security == null)
throw new InvalidOperationException("Security not set");
base.OnStarted2(time);
SubscribeCandles(CandleType, true, Security).Bind(c => ProcessCandle(c, Security)).Start();
}
private void ProcessCandle(ICandleMessage candle, Security security)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Store the latest closing price for this security
_latestPrices[security] = candle.ClosePrice;
OnDaily(candle.OpenTime.Date);
}
private void OnDaily(DateTime d)
{
if (d == _last)
return;
_last = d;
var monthKey = (d.Year * 100) + d.Month;
int tdMonthEnd = TradingDaysLeftInMonth(d);
int tdMonthStart = TradingDayNumber(d);
bool inWindow = tdMonthEnd <= 2 || tdMonthStart <= 3;
if (inWindow && Position == 0 && _enteredMonthKey != monthKey)
{
BuyMarket();
_enteredMonthKey = monthKey;
_exitedMonthKey = 0;
}
else if (!inWindow && Position > 0 && _enteredMonthKey == monthKey && _exitedMonthKey != monthKey)
{
SellMarket(Position);
_exitedMonthKey = monthKey;
}
}
private decimal GetLatestPrice(Security security)
{
return _latestPrices.TryGetValue(security, out var price) ? price : 0m;
}
private int TradingDaysLeftInMonth(DateTime d)
{
int cnt = 0;
var cur = d;
while (cur.Month == d.Month)
{
// Simple approximation: assume weekdays are trading days
if (cur.DayOfWeek != DayOfWeek.Saturday && cur.DayOfWeek != DayOfWeek.Sunday)
cnt++;
cur = cur.AddDays(1);
}
return cnt - 1;
}
private int TradingDayNumber(DateTime d)
{
int num = 0;
var cur = new DateTime(d.Year, d.Month, 1);
while (cur <= d)
{
// Simple approximation: assume weekdays are trading days
if (cur.DayOfWeek != DayOfWeek.Saturday && cur.DayOfWeek != DayOfWeek.Sunday)
num++;
cur = cur.AddDays(1);
}
return num;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, DayOfWeek, DateTime
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class payday_anomaly_strategy(Strategy):
"""Holds market ETF during days around typical payday window."""
def __init__(self):
super(payday_anomaly_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._last = None
self._entered_month_key = 0
self._exited_month_key = 0
@property
def CandleType(self):
return self._candle_type.Value
def OnReseted(self):
super(payday_anomaly_strategy, self).OnReseted()
self._last = None
self._entered_month_key = 0
self._exited_month_key = 0
def OnStarted2(self, time):
super(payday_anomaly_strategy, self).OnStarted2(time)
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(self.ProcessCandle) \
.Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
d = candle.OpenTime.Date
if d == self._last:
return
self._last = d
month_key = d.Year * 100 + d.Month
td_end = self._trading_days_left(d)
td_start = self._trading_day_number(d)
in_window = td_end <= 2 or td_start <= 3
if in_window and self.Position == 0 and self._entered_month_key != month_key:
self.BuyMarket()
self._entered_month_key = month_key
self._exited_month_key = 0
elif not in_window and self.Position > 0 and self._entered_month_key == month_key and self._exited_month_key != month_key:
self.SellMarket(self.Position)
self._exited_month_key = month_key
def _trading_days_left(self, d):
cnt = 0
cur = d
while cur.Month == d.Month:
if cur.DayOfWeek != DayOfWeek.Saturday and cur.DayOfWeek != DayOfWeek.Sunday:
cnt += 1
cur = cur.AddDays(1)
return cnt - 1
def _trading_day_number(self, d):
num = 0
cur = DateTime(d.Year, d.Month, 1)
while cur <= d:
if cur.DayOfWeek != DayOfWeek.Saturday and cur.DayOfWeek != DayOfWeek.Sunday:
num += 1
cur = cur.AddDays(1)
return num
def CreateClone(self):
return payday_anomaly_strategy()