View on GitHub

Month of Year Effect Strategy

The Month of Year Effect captures performance differences observed in various months. For example, equities often rally in November and December but can be weak during September.

Testing indicates an average annual return of about 88%. It performs best in the stocks market.

The system goes long or short at the beginning of each month based on those historical averages, exiting by month-end.

Stops are used to protect capital if the usual seasonal behavior fails to appear.

Details

  • Entry Criteria: calendar effect triggers
  • Long/Short: Both
  • Exit Criteria: stop-loss or opposite signal
  • Stops: Yes, percent based
  • Default Values:
    • CandleType = 15 minute
    • StopLossPercent = 2 Days are UTC days, since the market trades around the clock; entries happen at the close of the day's first candle. November through April are bought and May through October sold short, the classic seasonal split with strong winter months and a weak September; the position opens on the month's first candle and closes at its last.
  • Filters:
    • Category: Seasonality
    • Direction: Both
    • Indicators: Seasonality
    • Stops: Yes
    • Complexity: Intermediate
    • Timeframe: Intraday
    • Seasonality: Yes
    • Neural networks: No
    • Divergence: No
    • Risk level: Medium
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>
/// Month of Year strategy.
/// Days are UTC days of a market that trades around the clock.
/// On the first candle of each month it buys from November through April and sells short from May through October,
/// the classic seasonal split with strong winter months and a weak September; the position closes at the month's last candle,
/// and a percent stop limits the loss.
/// </summary>
public class MonthOfYearStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private DateTime? _day;

	/// <summary>
	/// Stop-loss percentage.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

	/// <summary>
	/// Candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public MonthOfYearStrategy()
	{
		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetNotNegative()
			.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_day = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_day = null;
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);

		// The stop has to see prices between candles, not only at their close.
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
		}

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ObserveProtectionQuote(Level1ChangeMessage quote)
	{
		// The high-level handler activates native protection before this callback, also between signal bars.
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var day = candle.OpenTime.Date;
		var firstOfDay = _day != day;

		if (firstOfDay)
		{
			_day = day;
		}

		// The candle is the day's last when the next one would open on another day.
		var frame = CandleType.Arg is TimeSpan tf ? tf : TimeSpan.Zero;
		var lastOfDay = (candle.OpenTime + frame).Date != day;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position != 0)
		{
			if (lastOfDay && (candle.OpenTime + frame).Month != day.Month)
			{
				if (Position > 0)
					SellMarket(Position);
				else
					BuyMarket(-Position);
			}

			return;
		}

		if (!(firstOfDay && day.Day == 1))
			return;

		if ((day.Month is >= 5 and <= 10 ? -1 : 1) > 0)
			BuyMarket(Volume);
		else
			SellMarket(Volume);
	}
}