GitHub で見る

1月効果戦略

年越しの1月効果を売買します。12月26日以降の最初の営業日に、最初の利用可能な確定足で買います。1月15日以降の最初のセッション終了時に決済し、終了足がなければその後最初の適格な足で決済します。毎月の繰り返しやショートエントリーはありません。

確定15分足と、明示的に設定した Security.Board の取引日カレンダーを使用します。祝日、特別セッション、取引所のタイムゾーンを考慮し、カレンダーがない場合は起動できません。ロングのみで、年間シーズンごとのエントリー試行は最大1回です。損切り後も同じシーズンには再エントリーしません。未完了注文がある間は重複した判断を行いません。

StopLoss = 2 は実際のエントリー約定価格から2%のローカル標準保護です。銘柄の bid/ask 更新により、足の間でも成行決済を発動できます。利確は設定しません。

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;
using Ecng.ComponentModel;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>Long-only January effect: enter on or after December 26 and exit on or after January 15.</summary>
public class JanuaryEffectStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _stopLoss;
	private int _entryYear = -1;
	private Order _pendingOrder;

	/// <summary>Signal candle interval.</summary>
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	/// <summary>Loss limit as a percentage of the actual entry price.</summary>
	public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }

	/// <summary>Initializes the seasonal strategy.</summary>
	public JanuaryEffectStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Finished candles used for calendar decisions", "Data");
		_stopLoss = Param(nameof(StopLoss), 2m).SetGreaterThanZero()
			.SetDisplay("Stop Loss %", "Percentage loss limit from the executed entry", "Risk");
		OrderRegistering += order => _pendingOrder = order;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryYear = -1;
		_pendingOrder = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		if (Security?.Board is null)
			throw new InvalidOperationException("The instrument's trading board and calendar are required.");
		base.OnStarted2(time);
		_entryYear = -1;
		_pendingOrder = null;
		StartProtection(new Unit(), new Unit(StopLoss, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Start();
		}
		var candles = SubscribeCandles(CandleType);
		candles.Bind(ProcessCandle).Start();
		var area = CreateChartArea();
		if (area is not null)
		{
			DrawCandles(area, candles);
			DrawOwnTrades(area);
		}
	}

	private (DateTime entry, DateTime exit) GetSeason(int year)
	{
		var calendar = Security.Board.WorkingTime;
		var entry = new DateTime(year, 12, 26);
		while (!calendar.IsWorkingDate(entry))
			entry = entry.AddDays(1);
		var exit = new DateTime(year + 1, 1, 15);
		while (!calendar.IsWorkingDate(exit))
			exit = exit.AddDays(1);
		return (entry, exit);
	}

	private TimeSpan SessionEnd(DateTime date)
	{
		var calendar = Security.Board.WorkingTime;
		if (!calendar.IsEnabled)
			return TimeSpan.FromDays(1);
		var period = calendar.GetPeriod(date);
		IEnumerable<Range<TimeSpan>> sessions = period?.Times;
		if (calendar.SpecialDays.TryGetValue(date, out var special))
			sessions = special;
		else if (period is not null && period.SpecialDays.TryGetValue(date.DayOfWeek, out var weekday))
			sessions = weekday;
		return sessions is not null && sessions.Any() ? sessions.Max(r => r.Max) : TimeSpan.FromDays(1);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished || !IsFormedAndOnlineAndAllowTrading()
			|| _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
			return;

		var local = TimeZoneInfo.ConvertTimeFromUtc(candle.OpenTime, Security.Board.TimeZone);
		var date = local.Date;
		var year = local.Month == 12 ? local.Year : local.Year - 1;
		var (entry, exit) = GetSeason(year);

		if (Position > 0m)
		{
			var barEnd = TimeZoneInfo.ConvertTimeFromUtc(candle.OpenTime + (TimeSpan)CandleType.Arg, Security.Board.TimeZone);
			if (date > exit || date == exit && barEnd >= exit + SessionEnd(exit))
				SellMarket(Position);
		}
		else if (Position == 0m && local.Month == 12 && date >= entry && _entryYear != year
			&& Security.Board.WorkingTime.IsWorkingDate(date))
		{
			_entryYear = year;
			BuyMarket(Volume);
		}
	}
}