一貫したモメンタム戦略
Consistent Momentum 戦略は、2つの時間ウィンドウにわたって強いモメンタムを示す銘柄を選択し、ポートフォリオを毎月リバランスします。各トランシェを固定の月数保持し、ロングとショートのバスケットに均等に資本を配分します。
詳細
- エントリー条件: 毎月の最初の取引日に、両方のモメンタム指標の上位十分位の銘柄をロング、下位十分位をショート。
- ロング/ショート: 両方向。
- エグジット条件: 保有期間終了後またはリバランス時にポジションを閉じる。
- ストップ: 明示的なストップロジックなし。ポジションサイズはドル配分に基づく。
- デフォルト値:
LookbackDays = 7 * 21HoldingMonths = 6MinTradeUsd = 50CandleType = TimeSpan.FromMinutes(5).TimeFrame()
- フィルター:
- カテゴリ: モメンタム
- 方向: 両方
- インジケーター: Price momentum
- ストップ: いいえ
- 複雑さ: 上級
- 時間軸: 日足
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Configuration;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Consistent momentum strategy that trades the primary instrument when both medium-term and long-term momentum are aligned versus a benchmark.
/// </summary>
public class ConsistentMomentumStrategy : Strategy
{
private readonly StrategyParam<string> _security2Id;
private readonly StrategyParam<int> _mediumMomentumLength;
private readonly StrategyParam<int> _longMomentumLength;
private readonly StrategyParam<decimal> _entryMargin;
private readonly StrategyParam<decimal> _exitMargin;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private Security _benchmark = null!;
private RateOfChange _primaryMediumMomentum = null!;
private RateOfChange _primaryLongMomentum = null!;
private RateOfChange _benchmarkMediumMomentum = null!;
private RateOfChange _benchmarkLongMomentum = null!;
private bool _primaryUpdated;
private bool _benchmarkUpdated;
private decimal _primaryMediumValue;
private decimal _primaryLongValue;
private decimal _benchmarkMediumValue;
private decimal _benchmarkLongValue;
private int _cooldownRemaining;
/// <summary>
/// Benchmark security identifier.
/// </summary>
public string Security2Id
{
get => _security2Id.Value;
set => _security2Id.Value = value;
}
/// <summary>
/// Medium-term momentum length.
/// </summary>
public int MediumMomentumLength
{
get => _mediumMomentumLength.Value;
set => _mediumMomentumLength.Value = value;
}
/// <summary>
/// Long-term momentum length.
/// </summary>
public int LongMomentumLength
{
get => _longMomentumLength.Value;
set => _longMomentumLength.Value = value;
}
/// <summary>
/// Minimum relative edge required to open a position.
/// </summary>
public decimal EntryMargin
{
get => _entryMargin.Value;
set => _entryMargin.Value = value;
}
/// <summary>
/// Relative edge threshold used to close a position.
/// </summary>
public decimal ExitMargin
{
get => _exitMargin.Value;
set => _exitMargin.Value = value;
}
/// <summary>
/// Closed candles to wait before another position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Candle type used for both instruments.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public ConsistentMomentumStrategy()
{
_security2Id = Param(nameof(Security2Id), Paths.HistoryDefaultSecurity2)
.SetDisplay("Benchmark Security Id", "Identifier of the benchmark security", "General");
_mediumMomentumLength = Param(nameof(MediumMomentumLength), 18)
.SetRange(5, 80)
.SetDisplay("Medium Momentum Length", "Medium-term momentum length", "Indicators");
_longMomentumLength = Param(nameof(LongMomentumLength), 60)
.SetRange(20, 200)
.SetDisplay("Long Momentum Length", "Long-term momentum length", "Indicators");
_entryMargin = Param(nameof(EntryMargin), 1.5m)
.SetRange(0.1m, 20m)
.SetDisplay("Entry Margin", "Minimum relative edge required to open a position", "Signals");
_exitMargin = Param(nameof(ExitMargin), 0.4m)
.SetRange(0m, 10m)
.SetDisplay("Exit Margin", "Relative edge threshold used to close a position", "Signals");
_cooldownBars = Param(nameof(CooldownBars), 8)
.SetRange(0, 100)
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk");
_stopLoss = Param(nameof(StopLoss), 2.5m)
.SetRange(0.5m, 10m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle series for both instruments", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
if (!Security2Id.IsEmpty())
yield return (new Security { Id = Security2Id }, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_benchmark = null!;
_primaryMediumMomentum = null!;
_primaryLongMomentum = null!;
_benchmarkMediumMomentum = null!;
_benchmarkLongMomentum = null!;
_primaryUpdated = false;
_benchmarkUpdated = false;
_primaryMediumValue = 0m;
_primaryLongValue = 0m;
_benchmarkMediumValue = 0m;
_benchmarkLongValue = 0m;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary security is not specified.");
if (Security2Id.IsEmpty())
throw new InvalidOperationException("Benchmark security identifier is not specified.");
_benchmark = this.LookupById(Security2Id) ?? new Security { Id = Security2Id };
_primaryMediumMomentum = new RateOfChange { Length = MediumMomentumLength };
_primaryLongMomentum = new RateOfChange { Length = LongMomentumLength };
_benchmarkMediumMomentum = new RateOfChange { Length = MediumMomentumLength };
_benchmarkLongMomentum = new RateOfChange { Length = LongMomentumLength };
var primarySubscription = SubscribeCandles(CandleType, security: Security);
var benchmarkSubscription = SubscribeCandles(CandleType, security: _benchmark);
primarySubscription
.Bind(ProcessPrimaryCandle)
.Start();
benchmarkSubscription
.Bind(ProcessBenchmarkCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, primarySubscription);
DrawCandles(area, benchmarkSubscription);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent));
}
private void ProcessPrimaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var mediumValue = _primaryMediumMomentum.Process(candle);
var longValue = _primaryLongMomentum.Process(candle);
if (!mediumValue.IsEmpty && !longValue.IsEmpty && _primaryMediumMomentum.IsFormed && _primaryLongMomentum.IsFormed)
{
_primaryMediumValue = mediumValue.ToDecimal();
_primaryLongValue = longValue.ToDecimal();
_primaryUpdated = true;
TryProcessSignal();
}
}
private void ProcessBenchmarkCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var mediumValue = _benchmarkMediumMomentum.Process(candle);
var longValue = _benchmarkLongMomentum.Process(candle);
if (!mediumValue.IsEmpty && !longValue.IsEmpty && _benchmarkMediumMomentum.IsFormed && _benchmarkLongMomentum.IsFormed)
{
_benchmarkMediumValue = mediumValue.ToDecimal();
_benchmarkLongValue = longValue.ToDecimal();
_benchmarkUpdated = true;
TryProcessSignal();
}
}
private void TryProcessSignal()
{
if (!_primaryUpdated || !_benchmarkUpdated)
return;
_primaryUpdated = false;
_benchmarkUpdated = false;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var mediumEdge = _primaryMediumValue - _benchmarkMediumValue;
var longEdge = _primaryLongValue - _benchmarkLongValue;
var bullishConsistent = mediumEdge >= EntryMargin && longEdge >= EntryMargin;
var bearishConsistent = mediumEdge <= -EntryMargin && longEdge <= -EntryMargin;
var bullishExit = mediumEdge <= ExitMargin || longEdge <= ExitMargin;
var bearishExit = mediumEdge >= -ExitMargin || longEdge >= -ExitMargin;
if (_cooldownRemaining == 0 && Position == 0)
{
if (bullishConsistent)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (bearishConsistent)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
}
else if (Position > 0 && bullishExit)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && bearishExit)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import RateOfChange, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
class consistent_momentum_strategy(Strategy):
"""Consistent momentum strategy using dual securities with medium and long-term ROC."""
def __init__(self):
super(consistent_momentum_strategy, self).__init__()
self._security2_id = self.Param("Security2Id", "TONUSDT@BNBFT") \
.SetDisplay("Benchmark Security Id", "Identifier of the benchmark security", "General")
self._medium_momentum_length = self.Param("MediumMomentumLength", 18) \
.SetRange(5, 80) \
.SetDisplay("Medium Momentum Length", "Medium-term momentum length", "Indicators")
self._long_momentum_length = self.Param("LongMomentumLength", 60) \
.SetRange(20, 200) \
.SetDisplay("Long Momentum Length", "Long-term momentum length", "Indicators")
self._entry_margin = self.Param("EntryMargin", 1.5) \
.SetRange(0.1, 20.0) \
.SetDisplay("Entry Margin", "Minimum relative edge required to open a position", "Signals")
self._exit_margin = self.Param("ExitMargin", 0.4) \
.SetRange(0.0, 10.0) \
.SetDisplay("Exit Margin", "Relative edge threshold used to close a position", "Signals")
self._cooldown_bars = self.Param("CooldownBars", 8) \
.SetRange(0, 100) \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk")
self._stop_loss = self.Param("StopLoss", 2.5) \
.SetRange(0.5, 10.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle series for both instruments", "General")
self._benchmark = None
self._primary_medium_mom = None
self._primary_long_mom = None
self._benchmark_medium_mom = None
self._benchmark_long_mom = None
self._primary_updated = False
self._benchmark_updated = False
self._primary_medium_value = 0.0
self._primary_long_value = 0.0
self._benchmark_medium_value = 0.0
self._benchmark_long_value = 0.0
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
result = []
if self.Security is not None:
result.append((self.Security, self.candle_type))
sec2_id = str(self._security2_id.Value)
if sec2_id:
s = Security()
s.Id = sec2_id
result.append((s, self.candle_type))
return result
def OnReseted(self):
super(consistent_momentum_strategy, self).OnReseted()
self._benchmark = None
self._primary_medium_mom = None
self._primary_long_mom = None
self._benchmark_medium_mom = None
self._benchmark_long_mom = None
self._primary_updated = False
self._benchmark_updated = False
self._primary_medium_value = 0.0
self._primary_long_value = 0.0
self._benchmark_medium_value = 0.0
self._benchmark_long_value = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(consistent_momentum_strategy, self).OnStarted2(time)
sec2_id = str(self._security2_id.Value)
if not sec2_id:
raise Exception("Benchmark security identifier is not specified.")
s = Security()
s.Id = sec2_id
self._benchmark = s
med_len = int(self._medium_momentum_length.Value)
long_len = int(self._long_momentum_length.Value)
self._primary_medium_mom = RateOfChange()
self._primary_medium_mom.Length = med_len
self._primary_long_mom = RateOfChange()
self._primary_long_mom.Length = long_len
self._benchmark_medium_mom = RateOfChange()
self._benchmark_medium_mom.Length = med_len
self._benchmark_long_mom = RateOfChange()
self._benchmark_long_mom.Length = long_len
primary_subscription = self.SubscribeCandles(self.candle_type, True, self.Security)
benchmark_subscription = self.SubscribeCandles(self.candle_type, True, self._benchmark)
primary_subscription.Bind(self.ProcessPrimaryCandle).Start()
benchmark_subscription.Bind(self.ProcessBenchmarkCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, primary_subscription)
self.DrawCandles(area, benchmark_subscription)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessPrimaryCandle(self, candle):
if candle.State != CandleStates.Finished:
return
civ_med = CandleIndicatorValue(self._primary_medium_mom, candle)
civ_med.IsFinal = True
med_result = self._primary_medium_mom.Process(civ_med)
civ_long = CandleIndicatorValue(self._primary_long_mom, candle)
civ_long.IsFinal = True
long_result = self._primary_long_mom.Process(civ_long)
if not med_result.IsEmpty and not long_result.IsEmpty and self._primary_medium_mom.IsFormed and self._primary_long_mom.IsFormed:
self._primary_medium_value = float(med_result)
self._primary_long_value = float(long_result)
self._primary_updated = True
self.TryProcessSignal()
def ProcessBenchmarkCandle(self, candle):
if candle.State != CandleStates.Finished:
return
civ_med = CandleIndicatorValue(self._benchmark_medium_mom, candle)
civ_med.IsFinal = True
med_result = self._benchmark_medium_mom.Process(civ_med)
civ_long = CandleIndicatorValue(self._benchmark_long_mom, candle)
civ_long.IsFinal = True
long_result = self._benchmark_long_mom.Process(civ_long)
if not med_result.IsEmpty and not long_result.IsEmpty and self._benchmark_medium_mom.IsFormed and self._benchmark_long_mom.IsFormed:
self._benchmark_medium_value = float(med_result)
self._benchmark_long_value = float(long_result)
self._benchmark_updated = True
self.TryProcessSignal()
def TryProcessSignal(self):
if not self._primary_updated or not self._benchmark_updated:
return
self._primary_updated = False
self._benchmark_updated = False
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
entry_margin = float(self._entry_margin.Value)
exit_margin = float(self._exit_margin.Value)
cooldown = int(self._cooldown_bars.Value)
medium_edge = self._primary_medium_value - self._benchmark_medium_value
long_edge = self._primary_long_value - self._benchmark_long_value
bullish_consistent = medium_edge >= entry_margin and long_edge >= entry_margin
bearish_consistent = medium_edge <= -entry_margin and long_edge <= -entry_margin
bullish_exit = medium_edge <= exit_margin or long_edge <= exit_margin
bearish_exit = medium_edge >= -exit_margin or long_edge >= -exit_margin
if self._cooldown_remaining == 0 and self.Position == 0:
if bullish_consistent:
self.BuyMarket()
self._cooldown_remaining = cooldown
elif bearish_consistent:
self.SellMarket()
self._cooldown_remaining = cooldown
elif self.Position > 0 and bullish_exit:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and bearish_exit:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
def CreateClone(self):
return consistent_momentum_strategy()