Стратегия «Нефть предсказывает акции»
Стратегия использует связь между доходностью нефти и акций. Если доходность нефти за последний месяц положительна, капитал направляется в фонд акций. В противном случае деньги переводятся в защитный фонд (кэш/облигации), чтобы не держать акции при слабой нефти.
Алгоритм отслеживает дневные свечи и проверяет сигнал в первый торговый день каждого месяца. Заявки отправляются по рыночной цене, при этом учитывается минимальный размер сделки, чтобы избежать слишком мелких операций.
Детали
- Вселенная: один ETF на акции, инструмент на нефть и ETF на кэш/облигации.
- Сигнал: переход в акционный ETF, когда доходность нефти за период
Lookback> 0; иначе держимCashEtf. - Перебалансировка: ежемесячно, в начале месяца.
- Позиционирование: либо акции, либо кэш, но не одновременно.
- Параметры:
Equity– ETF, в который инвестируем.Oil– инструмент на нефть для расчёта сигнала.CashEtf– защитный актив при отрицательной доходности нефти.Lookback– число свечей для расчёта доходности нефти.CandleType– таймфрейм свечей (по умолчанию дневной).
- Примечание: Пример иллюстрирует структуру и не учитывает комиссии и проскальзывание.
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>
/// Strategy that holds the primary equity instrument when the benchmark crude-oil proxy shows positive momentum and exits when the signal weakens.
/// </summary>
public class CrudeOilPredictsEquityStrategy : Strategy
{
private readonly StrategyParam<string> _oilSecurityId;
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<int> _trendLength;
private readonly StrategyParam<decimal> _oilThreshold;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private Security _oilSecurity = null!;
private RateOfChange _oilMomentum = null!;
private SimpleMovingAverage _equityTrend = null!;
private decimal _latestEquityPrice;
private decimal _latestEquityTrend;
private decimal _latestOilMomentum;
private bool _equityUpdated;
private bool _oilUpdated;
private int _cooldownRemaining;
/// <summary>
/// Crude oil benchmark identifier.
/// </summary>
public string OilSecurityId
{
get => _oilSecurityId.Value;
set => _oilSecurityId.Value = value;
}
/// <summary>
/// Number of candles used to compute oil momentum.
/// </summary>
public int Lookback
{
get => _lookback.Value;
set => _lookback.Value = value;
}
/// <summary>
/// Equity trend filter length.
/// </summary>
public int TrendLength
{
get => _trendLength.Value;
set => _trendLength.Value = value;
}
/// <summary>
/// Minimum oil momentum required to hold equity exposure.
/// </summary>
public decimal OilThreshold
{
get => _oilThreshold.Value;
set => _oilThreshold.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 CrudeOilPredictsEquityStrategy()
{
_oilSecurityId = Param(nameof(OilSecurityId), Paths.HistoryDefaultSecurity2)
.SetDisplay("Oil Security Id", "Identifier of the crude-oil benchmark security", "General");
_lookback = Param(nameof(Lookback), 20)
.SetRange(5, 120)
.SetDisplay("Lookback", "Number of candles used to compute oil momentum", "Indicators");
_trendLength = Param(nameof(TrendLength), 20)
.SetRange(5, 120)
.SetDisplay("Trend Length", "Equity trend filter length", "Indicators");
_oilThreshold = Param(nameof(OilThreshold), 0m)
.SetRange(-20m, 20m)
.SetDisplay("Oil Threshold", "Minimum oil momentum required to hold equity exposure", "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 (!OilSecurityId.IsEmpty())
yield return (new Security { Id = OilSecurityId }, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_oilSecurity = null!;
_oilMomentum = null!;
_equityTrend = null!;
_latestEquityPrice = 0m;
_latestEquityTrend = 0m;
_latestOilMomentum = 0m;
_equityUpdated = false;
_oilUpdated = false;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary equity security is not specified.");
if (OilSecurityId.IsEmpty())
throw new InvalidOperationException("Oil security identifier is not specified.");
_oilSecurity = this.LookupById(OilSecurityId) ?? new Security { Id = OilSecurityId };
_oilMomentum = new RateOfChange { Length = Lookback };
_equityTrend = new SimpleMovingAverage { Length = TrendLength };
var equitySubscription = SubscribeCandles(CandleType, security: Security);
var oilSubscription = SubscribeCandles(CandleType, security: _oilSecurity);
equitySubscription
.Bind(ProcessEquityCandle)
.Start();
oilSubscription
.Bind(ProcessOilCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, equitySubscription);
DrawCandles(area, oilSubscription);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent));
}
private void ProcessEquityCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestEquityPrice = candle.ClosePrice;
_latestEquityTrend = _equityTrend.Process(candle).ToDecimal();
_equityUpdated = _equityTrend.IsFormed;
TryProcessSignal();
}
private void ProcessOilCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var oilValue = _oilMomentum.Process(candle);
if (!oilValue.IsEmpty && _oilMomentum.IsFormed)
{
_latestOilMomentum = oilValue.ToDecimal();
_oilUpdated = true;
TryProcessSignal();
}
}
private void TryProcessSignal()
{
if (!_equityUpdated || !_oilUpdated)
return;
_equityUpdated = false;
_oilUpdated = false;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var bullishSignal = _latestOilMomentum > OilThreshold && _latestEquityPrice >= _latestEquityTrend;
var exitSignal = _latestOilMomentum <= OilThreshold || _latestEquityPrice < _latestEquityTrend;
if (_cooldownRemaining == 0 && Position == 0 && bullishSignal)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (Position > 0 && exitSignal)
{
SellMarket(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, SimpleMovingAverage, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
class crude_oil_predicts_equity_strategy(Strategy):
"""Strategy that holds equity when oil momentum is positive and equity is above trend."""
def __init__(self):
super(crude_oil_predicts_equity_strategy, self).__init__()
self._oil_security_id = self.Param("OilSecurityId", "TONUSDT@BNBFT") \
.SetDisplay("Oil Security Id", "Identifier of the crude-oil benchmark security", "General")
self._lookback = self.Param("Lookback", 20) \
.SetRange(5, 120) \
.SetDisplay("Lookback", "Number of candles used to compute oil momentum", "Indicators")
self._trend_length = self.Param("TrendLength", 20) \
.SetRange(5, 120) \
.SetDisplay("Trend Length", "Equity trend filter length", "Indicators")
self._oil_threshold = self.Param("OilThreshold", 0.0) \
.SetRange(-20.0, 20.0) \
.SetDisplay("Oil Threshold", "Minimum oil momentum required to hold equity exposure", "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._oil_security = None
self._oil_momentum = None
self._equity_trend = None
self._latest_equity_price = 0.0
self._latest_equity_trend = 0.0
self._latest_oil_momentum = 0.0
self._equity_updated = False
self._oil_updated = False
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._oil_security_id.Value)
if sec2_id:
s = Security()
s.Id = sec2_id
result.append((s, self.candle_type))
return result
def OnReseted(self):
super(crude_oil_predicts_equity_strategy, self).OnReseted()
self._oil_security = None
self._oil_momentum = None
self._equity_trend = None
self._latest_equity_price = 0.0
self._latest_equity_trend = 0.0
self._latest_oil_momentum = 0.0
self._equity_updated = False
self._oil_updated = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(crude_oil_predicts_equity_strategy, self).OnStarted2(time)
sec2_id = str(self._oil_security_id.Value)
if not sec2_id:
raise Exception("Oil security identifier is not specified.")
s = Security()
s.Id = sec2_id
self._oil_security = s
self._oil_momentum = RateOfChange()
self._oil_momentum.Length = int(self._lookback.Value)
self._equity_trend = SimpleMovingAverage()
self._equity_trend.Length = int(self._trend_length.Value)
equity_subscription = self.SubscribeCandles(self.candle_type, True, self.Security)
oil_subscription = self.SubscribeCandles(self.candle_type, True, self._oil_security)
equity_subscription.Bind(self.ProcessEquityCandle).Start()
oil_subscription.Bind(self.ProcessOilCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, equity_subscription)
self.DrawCandles(area, oil_subscription)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessEquityCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_equity_price = float(candle.ClosePrice)
civ = CandleIndicatorValue(self._equity_trend, candle)
civ.IsFinal = True
trend_result = self._equity_trend.Process(civ)
self._latest_equity_trend = float(trend_result)
self._equity_updated = self._equity_trend.IsFormed
self.TryProcessSignal()
def ProcessOilCandle(self, candle):
if candle.State != CandleStates.Finished:
return
civ = CandleIndicatorValue(self._oil_momentum, candle)
civ.IsFinal = True
oil_result = self._oil_momentum.Process(civ)
if not oil_result.IsEmpty and self._oil_momentum.IsFormed:
self._latest_oil_momentum = float(oil_result)
self._oil_updated = True
self.TryProcessSignal()
def TryProcessSignal(self):
if not self._equity_updated or not self._oil_updated:
return
self._equity_updated = False
self._oil_updated = False
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
oil_threshold = float(self._oil_threshold.Value)
cooldown = int(self._cooldown_bars.Value)
bullish_signal = self._latest_oil_momentum > oil_threshold and self._latest_equity_price >= self._latest_equity_trend
exit_signal = self._latest_oil_momentum <= oil_threshold or self._latest_equity_price < self._latest_equity_trend
if self._cooldown_remaining == 0 and self.Position == 0 and bullish_signal:
self.BuyMarket()
self._cooldown_remaining = cooldown
elif self.Position > 0 and exit_signal:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
def CreateClone(self):
return crude_oil_predicts_equity_strategy()