季節性調整モメンタム戦略
モメンタムと過去の月次リターンを組み合わせます。確定5分足で M = Momentum(14) と M の A = SMA(14) を計算します。建玉なしで S >= SeasonalityThreshold かつ M > A なら買い、S <= -SeasonalityThreshold かつ M < A なら売りです。既定の SeasonalityThreshold = 0.5 の単位はパーセントポイントです。
ロングは季節条件を満たさなくなるか M < A、ショートは条件不成立か M > A で決済します。各シグナル注文後、CooldownBars = 120 本の確定足の間、次のシグナル判断を待ちます。約定済み建玉には実際の約定価格から StopLossPercent = 2 のローカル標準保護が別途働き、bid/ask 監視で成行決済します。保護は待機期間に依存しません。
SeasonalityHistoryFile が必須です。月次始値・終値を含む UTF-8 CSV で、ヘッダーは SecurityId,Month,Open,Close、Month は yyyy-MM、小数点はピリオドです。価格は正、同一銘柄の月は重複不可です。他の証券 ID は無視します。未指定、不正な行、対象銘柄のデータがない場合は起動できません。
各シグナル足の S は、過去 SeasonalityLookbackYears = 10 年の同じ暦月における 100 × (Close / Open − 1) の平均です。MinimumSeasonalSamples = 2 件以上が必要です。ファイルにあっても当月や将来月は使いません。履歴不足時は新規建玉を作りません。実際の銘柄の月次価格を入力し、固定の月別スコア表は使用しません。
using System;
using System.Globalization;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Momentum strategy that allows longs or shorts only when the current month historically supports that seasonal bias.
/// </summary>
public class SeasonalityAdjustedMomentumStrategy : Strategy
{
private readonly StrategyParam<int> _momentumPeriod;
private readonly StrategyParam<decimal> _seasonalityThreshold;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<string> _seasonalityHistoryFile;
private readonly StrategyParam<int> _minimumSeasonalSamples;
private readonly StrategyParam<int> _seasonalityLookbackYears;
private readonly List<(DateTime month, decimal value)> _seasonalHistory = new();
private DateTime? _seasonalMonth;
private decimal? _seasonalValue;
private Momentum _momentum;
private SimpleMovingAverage _momentumAverage;
private int _cooldown;
/// <summary>CSV monthly opening/closing prices: SecurityId,Month,Open,Close.</summary>
public string SeasonalityHistoryFile { get => _seasonalityHistoryFile.Value; set => _seasonalityHistoryFile.Value = value; }
/// <summary>Required completed observations of the same calendar month.</summary>
public int MinimumSeasonalSamples { get => _minimumSeasonalSamples.Value; set => _minimumSeasonalSamples.Value = value; }
/// <summary>Maximum number of prior years used for each seasonal estimate.</summary>
public int SeasonalityLookbackYears { get => _seasonalityLookbackYears.Value; set => _seasonalityLookbackYears.Value = value; }
/// <summary>
/// Period for the momentum indicator.
/// </summary>
public int MomentumPeriod
{
get => _momentumPeriod.Value;
set => _momentumPeriod.Value = value;
}
/// <summary>
/// Minimum absolute seasonality strength required to allow directional entries.
/// </summary>
public decimal SeasonalityThreshold
{
get => _seasonalityThreshold.Value;
set => _seasonalityThreshold.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Bars to wait after each order.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public SeasonalityAdjustedMomentumStrategy()
{
_seasonalityHistoryFile = Param<string>(nameof(SeasonalityHistoryFile)).SetRequired()
.SetDisplay("Seasonality History File", "CSV monthly prices for the execution instrument", "Seasonality");
_minimumSeasonalSamples = Param(nameof(MinimumSeasonalSamples), 2).SetGreaterThanZero()
.SetDisplay("Minimum Seasonal Samples", "Completed observations required for the same month", "Seasonality");
_seasonalityLookbackYears = Param(nameof(SeasonalityLookbackYears), 10).SetRange(1, 100)
.SetDisplay("Seasonality Lookback Years", "Historical years used by the seasonal estimate", "Seasonality");
_momentumPeriod = Param(nameof(MomentumPeriod), 14)
.SetRange(3, 100)
.SetDisplay("Momentum Period", "Period for the momentum indicator", "Indicators");
_seasonalityThreshold = Param(nameof(SeasonalityThreshold), 0.5m)
.SetRange(0m, 1m)
.SetDisplay("Seasonality Threshold", "Minimum absolute seasonality strength required for entries", "Signals");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetRange(0.5m, 10m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 120)
.SetRange(1, 500)
.SetDisplay("Cooldown Bars", "Bars to wait after each order", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles for the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_seasonalHistory.Clear();
_seasonalMonth = null;
_seasonalValue = null;
_momentum = null;
_momentumAverage = null;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
LoadSeasonalityHistory();
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Security is not specified.");
_momentum = new Momentum { Length = MomentumPeriod };
_momentumAverage = new SimpleMovingAverage { Length = MomentumPeriod };
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_momentum, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _momentum);
DrawIndicator(area, _momentumAverage);
DrawOwnTrades(area);
}
StartProtection(new Unit(0, UnitTypes.Absolute), new Unit(StopLossPercent, 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();
}
}
private void ProcessCandle(ICandleMessage candle, decimal momentumValue)
{
if (candle.State != CandleStates.Finished)
return;
var momentumAvgValue = _momentumAverage.Process(momentumValue, candle.OpenTime, true).ToDecimal();
if (!_momentum.IsFormed || !_momentumAverage.IsFormed)
return;
if (ProcessState != ProcessStates.Started)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var seasonalStrength = GetSeasonalStrength(candle.OpenTime);
var allowLong = seasonalStrength >= SeasonalityThreshold;
var allowShort = seasonalStrength <= -SeasonalityThreshold;
var bullishMomentum = momentumValue > momentumAvgValue;
var bearishMomentum = momentumValue < momentumAvgValue;
if (Position > 0)
{
if (!allowLong || bearishMomentum)
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
return;
}
if (Position < 0)
{
if (!allowShort || bullishMomentum)
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
return;
}
if (allowLong && bullishMomentum)
{
BuyMarket();
_cooldown = CooldownBars;
}
else if (allowShort && bearishMomentum)
{
SellMarket();
_cooldown = CooldownBars;
}
}
private void LoadSeasonalityHistory()
{
if (string.IsNullOrWhiteSpace(SeasonalityHistoryFile))
throw new InvalidOperationException("SeasonalityHistoryFile is required.");
_seasonalHistory.Clear();
_seasonalMonth = null;
_seasonalValue = null;
var lines = File.ReadLines(SeasonalityHistoryFile).GetEnumerator();
using (lines)
{
if (!lines.MoveNext() || lines.Current.Trim() != "SecurityId,Month,Open,Close")
throw new InvalidOperationException("Seasonality history must begin with SecurityId,Month,Open,Close.");
var months = new HashSet<DateTime>();
while (lines.MoveNext())
{
if (string.IsNullOrWhiteSpace(lines.Current))
continue;
var columns = lines.Current.Split(',');
if (columns.Length != 4)
throw new InvalidOperationException("Every seasonal observation must contain four CSV fields.");
if (!string.Equals(columns[0].Trim(), Security.Id, StringComparison.OrdinalIgnoreCase))
continue;
var month = DateTime.ParseExact(columns[1].Trim(), "yyyy-MM", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
var open = decimal.Parse(columns[2], CultureInfo.InvariantCulture);
var close = decimal.Parse(columns[3], CultureInfo.InvariantCulture);
if (open <= 0m || close <= 0m || !months.Add(month))
throw new InvalidOperationException("Seasonality prices must be positive and months must be unique.");
_seasonalHistory.Add((month, 100m * (close / open - 1m)));
}
}
if (_seasonalHistory.Count == 0)
throw new InvalidOperationException("Seasonality history contains no observations for the execution instrument.");
}
private decimal? GetSeasonalStrength(DateTime time)
{
var month = new DateTime(time.Year, time.Month, 1, 0, 0, 0, DateTimeKind.Utc);
if (_seasonalMonth != month)
{
var earliest = month.AddYears(-SeasonalityLookbackYears);
var history = _seasonalHistory.Where(h => h.month.Month == month.Month
&& h.month < month && h.month >= earliest).Select(h => h.value).ToArray();
_seasonalValue = history.Length >= MinimumSeasonalSamples ? history.Average() : null;
_seasonalMonth = month;
}
return _seasonalValue;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateTime, InvalidOperationException
from System.Globalization import CultureInfo
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import Momentum, SimpleMovingAverage
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import Level1Fields
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class seasonality_adjusted_momentum_strategy(Strategy):
"""
Momentum strategy that allows longs or shorts only when the current month
historically supports that seasonal bias.
"""
def __init__(self):
super(seasonality_adjusted_momentum_strategy, self).__init__()
self._seasonality_history_file = self.Param[str]("SeasonalityHistoryFile", None).SetRequired()
self._minimum_seasonal_samples = self.Param("MinimumSeasonalSamples", 2).SetGreaterThanZero()
self._seasonality_lookback_years = self.Param("SeasonalityLookbackYears", 10).SetRange(1, 100)
self._seasonal_history = []
self._seasonal_month = None
self._seasonal_value = None
self._momentum_period = self.Param("MomentumPeriod", 14) \
.SetDisplay("Momentum Period", "Period for the momentum indicator", "Indicators")
self._seasonality_threshold = self.Param("SeasonalityThreshold", 0.5) \
.SetDisplay("Seasonality Threshold", "Minimum absolute seasonality strength required for entries", "Signals")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 120) \
.SetDisplay("Cooldown Bars", "Bars to wait after each order", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles for the strategy", "General")
self._seasonal_history = []
self._momentum = None
self._momentum_average = None
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(seasonality_adjusted_momentum_strategy, self).OnReseted()
self._seasonal_history = []
self._seasonal_month = None
self._seasonal_value = None
self._momentum = None
self._momentum_average = None
self._cooldown = 0
self._seasonal_history.clear()
def OnStarted2(self, time):
self._load_seasonality_history()
super(seasonality_adjusted_momentum_strategy, self).OnStarted2(time)
mp = int(self._momentum_period.Value)
self._momentum = Momentum()
self._momentum.Length = mp
self._momentum_average = SimpleMovingAverage()
self._momentum_average.Length = mp
self._cooldown = 0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._momentum, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._momentum)
self.DrawIndicator(area, self._momentum_average)
self.DrawOwnTrades(area)
self.StartProtection(Unit(0, UnitTypes.Absolute), Unit(self._stop_loss_percent.Value, UnitTypes.Percent), useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Start()
def _process_candle(self, candle, momentum_value):
if candle.State != CandleStates.Finished:
return
mv = float(momentum_value)
momentum_avg_val = float(process_float(self._momentum_average, Decimal(mv), candle.OpenTime, True))
if not self._momentum.IsFormed or not self._momentum_average.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown > 0:
self._cooldown -= 1
return
seasonal_strength = self._get_seasonal_strength(candle.OpenTime)
st = float(self._seasonality_threshold.Value)
allow_long = seasonal_strength is not None and seasonal_strength >= st
allow_short = seasonal_strength is not None and seasonal_strength <= -st
bullish_momentum = mv > momentum_avg_val
bearish_momentum = mv < momentum_avg_val
cd = int(self._cooldown_bars.Value)
if self.Position > 0:
if not allow_long or bearish_momentum:
self.SellMarket(Math.Abs(self.Position))
self._cooldown = cd
return
if self.Position < 0:
if not allow_short or bullish_momentum:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = cd
return
if allow_long and bullish_momentum:
self.BuyMarket()
self._cooldown = cd
elif allow_short and bearish_momentum:
self.SellMarket()
self._cooldown = cd
def _load_seasonality_history(self):
if self.Security is None:
raise InvalidOperationException("Security is required for seasonal history")
path = self._seasonality_history_file.Value
if not path or not str(path).strip():
raise InvalidOperationException("SeasonalityHistoryFile is required")
self._seasonal_history = []
self._seasonal_month = None
self._seasonal_value = None
months = set()
with open(str(path), "r", encoding="utf-8-sig") as stream:
if stream.readline().strip() != "SecurityId,Month,Open,Close":
raise InvalidOperationException("Seasonality history must begin with SecurityId,Month,Open,Close")
for line in stream:
if not line.strip():
continue
fields = line.strip().split(",")
if len(fields) != 4:
raise InvalidOperationException("Every seasonal observation must contain four CSV fields")
if fields[0].strip().lower() != self.Security.Id.lower():
continue
month = DateTime.ParseExact(fields[1].strip(), "yyyy-MM", CultureInfo.InvariantCulture)
opening = Decimal.Parse(fields[2], CultureInfo.InvariantCulture)
closing = Decimal.Parse(fields[3], CultureInfo.InvariantCulture)
if opening <= 0 or closing <= 0 or month in months:
raise InvalidOperationException("Seasonality prices must be positive and months must be unique")
months.add(month)
self._seasonal_history.append((month, Decimal.Multiply(Decimal(100), Decimal.Subtract(Decimal.Divide(closing, opening), Decimal.One))))
if not self._seasonal_history:
raise InvalidOperationException("Seasonality history contains no observations for the execution instrument")
def _get_seasonal_strength(self, time):
month = DateTime(time.Year, time.Month, 1)
if self._seasonal_month != month:
earliest = month.AddYears(-int(self._seasonality_lookback_years.Value))
values = [value for observed, value in self._seasonal_history
if observed.Month == month.Month and earliest <= observed < month]
minimum = int(self._minimum_seasonal_samples.Value)
self._seasonal_value = None
if len(values) >= minimum:
total = Decimal.Zero
for value in values:
total = Decimal.Add(total, value)
self._seasonal_value = float(Decimal.Divide(total, Decimal(len(values))))
self._seasonal_month = month
return self._seasonal_value
def CreateClone(self):
return seasonality_adjusted_momentum_strategy()