Estrategia de Anomalía del Día de Pago
Esta estrategia explota el efecto "payday" manteniendo un ETF de mercado amplio alrededor de las fechas típicas de pago de salarios. El ETF se posee desde dos días de negociación antes del fin de mes hasta el tercer día de negociación del nuevo mes, capturando las entradas de capital de las contribuciones salariales.
El resto del mes la cartera está en efectivo. Las velas diarias determinan la ventana y las órdenes de mercado ajustan la posición.
Detalles
- Instrumento: ETF de mercado amplio.
- Ventana: desde dos días antes del fin de mes hasta el tercer día de negociación del mes siguiente.
- Posicionamiento: largo durante la ventana, sin posición en otros momentos.
- Datos: velas diarias.
- Control de riesgo: operación omitida si el valor de la orden está por debajo de
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()