OBVious MA 戦略
OBVがロング・エントリー移動平均線を上抜けたときにロングポジションを建て、OBVがロング・エグジット平均線を下抜けたときに決済します。OBVがショート・エントリー平均線を下抜けたときにショートポジションを建て、ショート・エグジット平均線を上抜けたときに決済します。方向フィルターにより、ロングのみまたはショートのみの取引を有効にできます。
詳細
- エントリー条件:
- ロング: OBVがロング・エントリーMAを上抜け、方向がショートでない。
- ショート: OBVがショート・エントリーMAを下抜け、方向がロングでない。
- ロング/ショート: 両方
- エグジット条件:
- ロング: OBVがロング・エグジットMAを下抜け。
- ショート: OBVがショート・エグジットMAを上抜け。
- ストップ: なし。
- デフォルト値:
LongEntryLength= 190LongExitLength= 202ShortEntryLength= 395ShortExitLength= 300TradeDirection= "Long"
- フィルター:
- カテゴリ: トレンドフォロー
- 方向: 両方
- インジケーター: OBV, SMA
- ストップ: いいえ
- 複雑さ: 基本
- 時間軸: 任意
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 低
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>
/// OBVious MA strategy.
/// On-balance volume is compared with four simple moving averages of itself. A long opens when OBV crosses above the long entry
/// average and closes when it crosses below the long exit average. A short opens when OBV crosses below the short entry average
/// and closes when it crosses above the short exit average. TradeDirection ("Long", "Short" or "Both") limits the entries.
/// </summary>
public class ObviousMaStrategy : Strategy
{
private readonly StrategyParam<int> _longEntryLength;
private readonly StrategyParam<int> _longExitLength;
private readonly StrategyParam<int> _shortEntryLength;
private readonly StrategyParam<int> _shortExitLength;
private readonly StrategyParam<string> _tradeDirection;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _longEntryMa;
private SimpleMovingAverage _longExitMa;
private SimpleMovingAverage _shortEntryMa;
private SimpleMovingAverage _shortExitMa;
private decimal? _prevObv;
private decimal? _prevLongEntry;
private decimal? _prevLongExit;
private decimal? _prevShortEntry;
private decimal? _prevShortExit;
/// <summary>
/// Length of the OBV average for long entries.
/// </summary>
public int LongEntryLength
{
get => _longEntryLength.Value;
set => _longEntryLength.Value = value;
}
/// <summary>
/// Length of the OBV average for long exits.
/// </summary>
public int LongExitLength
{
get => _longExitLength.Value;
set => _longExitLength.Value = value;
}
/// <summary>
/// Length of the OBV average for short entries.
/// </summary>
public int ShortEntryLength
{
get => _shortEntryLength.Value;
set => _shortEntryLength.Value = value;
}
/// <summary>
/// Length of the OBV average for short exits.
/// </summary>
public int ShortExitLength
{
get => _shortExitLength.Value;
set => _shortExitLength.Value = value;
}
/// <summary>
/// Allowed entries: "Long", "Short" or "Both".
/// </summary>
public string TradeDirection
{
get => _tradeDirection.Value;
set => _tradeDirection.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public ObviousMaStrategy()
{
_longEntryLength = Param(nameof(LongEntryLength), 190)
.SetGreaterThanZero()
.SetDisplay("Long Entry Length", "Length of the OBV average for long entries", "Indicators");
_longExitLength = Param(nameof(LongExitLength), 202)
.SetGreaterThanZero()
.SetDisplay("Long Exit Length", "Length of the OBV average for long exits", "Indicators");
_shortEntryLength = Param(nameof(ShortEntryLength), 395)
.SetGreaterThanZero()
.SetDisplay("Short Entry Length", "Length of the OBV average for short entries", "Indicators");
_shortExitLength = Param(nameof(ShortExitLength), 300)
.SetGreaterThanZero()
.SetDisplay("Short Exit Length", "Length of the OBV average for short exits", "Indicators");
_tradeDirection = Param(nameof(TradeDirection), "Long")
.SetDisplay("Trade Direction", "Allowed entries: Long, Short or Both", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).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();
ResetState();
}
private void ResetState()
{
_prevObv = null;
_prevLongEntry = null;
_prevLongExit = null;
_prevShortEntry = null;
_prevShortExit = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var obv = new OnBalanceVolume();
_longEntryMa = new SimpleMovingAverage { Length = LongEntryLength };
_longExitMa = new SimpleMovingAverage { Length = LongExitLength };
_shortEntryMa = new SimpleMovingAverage { Length = ShortEntryLength };
_shortExitMa = new SimpleMovingAverage { Length = ShortExitLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(obv, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
{
DrawIndicator(oscillators, obv);
DrawIndicator(oscillators, _longEntryMa);
DrawIndicator(oscillators, _longExitMa);
DrawIndicator(oscillators, _shortEntryMa);
DrawIndicator(oscillators, _shortExitMa);
}
}
}
private static decimal? ProcessAverage(SimpleMovingAverage ma, decimal obv, DateTime time)
{
var value = ma.Process(new DecimalIndicatorValue(ma, obv, time) { IsFinal = true });
return ma.IsFormed ? value.GetValue<decimal>() : null;
}
private void ProcessCandle(ICandleMessage candle, decimal obv)
{
if (candle.State != CandleStates.Finished)
return;
// The averages are built on OBV itself, not on price.
var longEntry = ProcessAverage(_longEntryMa, obv, candle.OpenTime);
var longExit = ProcessAverage(_longExitMa, obv, candle.OpenTime);
var shortEntry = ProcessAverage(_shortEntryMa, obv, candle.OpenTime);
var shortExit = ProcessAverage(_shortExitMa, obv, candle.OpenTime);
var prevObv = _prevObv;
var prevLongEntry = _prevLongEntry;
var prevLongExit = _prevLongExit;
var prevShortEntry = _prevShortEntry;
var prevShortExit = _prevShortExit;
_prevObv = obv;
_prevLongEntry = longEntry;
_prevLongExit = longExit;
_prevShortEntry = shortEntry;
_prevShortExit = shortExit;
if (prevObv is not decimal prev || !IsFormedAndOnlineAndAllowTrading())
return;
var allowLong = !TradeDirection.EqualsIgnoreCase("Short");
var allowShort = !TradeDirection.EqualsIgnoreCase("Long");
var crossUpLongEntry = longEntry is decimal le && prevLongEntry is decimal ple && prev <= ple && obv > le;
var crossDownLongExit = longExit is decimal lx && prevLongExit is decimal plx && prev >= plx && obv < lx;
var crossDownShortEntry = shortEntry is decimal se && prevShortEntry is decimal pse && prev >= pse && obv < se;
var crossUpShortExit = shortExit is decimal sx && prevShortExit is decimal psx && prev <= psx && obv > sx;
if (crossUpLongEntry && allowLong && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (crossDownShortEntry && allowShort && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
else if (Position > 0 && crossDownLongExit)
SellMarket(Position);
else if (Position < 0 && crossUpShortExit)
BuyMarket(-Position);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import OnBalanceVolume, SimpleMovingAverage, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class obvious_ma_strategy(Strategy):
"""
OBVious MA strategy.
On-balance volume is compared with four simple moving averages of itself. A long opens when OBV crosses above the long entry
average and closes when it crosses below the long exit average. A short opens when OBV crosses below the short entry average
and closes when it crosses above the short exit average. TradeDirection ("Long", "Short" or "Both") limits the entries.
"""
def __init__(self):
super(obvious_ma_strategy, self).__init__()
self._long_entry_length = self.Param("LongEntryLength", 190).SetGreaterThanZero().SetDisplay("Long Entry Length", "Length of the OBV average for long entries", "Indicators")
self._long_exit_length = self.Param("LongExitLength", 202).SetGreaterThanZero().SetDisplay("Long Exit Length", "Length of the OBV average for long exits", "Indicators")
self._short_entry_length = self.Param("ShortEntryLength", 395).SetGreaterThanZero().SetDisplay("Short Entry Length", "Length of the OBV average for short entries", "Indicators")
self._short_exit_length = self.Param("ShortExitLength", 300).SetGreaterThanZero().SetDisplay("Short Exit Length", "Length of the OBV average for short exits", "Indicators")
self._trade_direction = self.Param("TradeDirection", "Long").SetDisplay("Trade Direction", "Allowed entries: Long, Short or Both", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_obv = None
self._prev_long_entry = None
self._prev_long_exit = None
self._prev_short_entry = None
self._prev_short_exit = None
def OnReseted(self):
super(obvious_ma_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(obvious_ma_strategy, self).OnStarted2(time)
self._reset_state()
obv = OnBalanceVolume()
self._long_entry_ma = SimpleMovingAverage()
self._long_entry_ma.Length = self._long_entry_length.Value
self._long_exit_ma = SimpleMovingAverage()
self._long_exit_ma.Length = self._long_exit_length.Value
self._short_entry_ma = SimpleMovingAverage()
self._short_entry_ma.Length = self._short_entry_length.Value
self._short_exit_ma = SimpleMovingAverage()
self._short_exit_ma.Length = self._short_exit_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(obv, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, obv)
self.DrawIndicator(oscillators, self._long_entry_ma)
self.DrawIndicator(oscillators, self._long_exit_ma)
self.DrawIndicator(oscillators, self._short_entry_ma)
self.DrawIndicator(oscillators, self._short_exit_ma)
def _process_average(self, ma, obv, time):
value = DecimalIndicatorValue(ma, obv, time)
value.IsFinal = True
result = ma.Process(value)
if not ma.IsFormed:
return None
return result.GetValue[Decimal](None)
def _process_candle(self, candle, obv):
if candle.State != CandleStates.Finished:
return
# The averages are built on OBV itself, not on price.
long_entry = self._process_average(self._long_entry_ma, obv, candle.OpenTime)
long_exit = self._process_average(self._long_exit_ma, obv, candle.OpenTime)
short_entry = self._process_average(self._short_entry_ma, obv, candle.OpenTime)
short_exit = self._process_average(self._short_exit_ma, obv, candle.OpenTime)
prev = self._prev_obv
prev_long_entry = self._prev_long_entry
prev_long_exit = self._prev_long_exit
prev_short_entry = self._prev_short_entry
prev_short_exit = self._prev_short_exit
self._prev_obv = obv
self._prev_long_entry = long_entry
self._prev_long_exit = long_exit
self._prev_short_entry = short_entry
self._prev_short_exit = short_exit
if prev is None or not self.IsFormedAndOnlineAndAllowTrading():
return
direction = str(self._trade_direction.Value).lower()
allow_long = direction != "short"
allow_short = direction != "long"
cross_up_long_entry = long_entry is not None and prev_long_entry is not None and prev <= prev_long_entry and obv > long_entry
cross_down_long_exit = long_exit is not None and prev_long_exit is not None and prev >= prev_long_exit and obv < long_exit
cross_down_short_entry = short_entry is not None and prev_short_entry is not None and prev >= prev_short_entry and obv < short_entry
cross_up_short_exit = short_exit is not None and prev_short_exit is not None and prev <= prev_short_exit and obv > short_exit
if cross_up_long_entry and allow_long and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif cross_down_short_entry and allow_short and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and cross_down_long_exit:
self.SellMarket(self.Position)
elif self.Position < 0 and cross_up_short_exit:
self.BuyMarket(-self.Position)
def CreateClone(self):
return obvious_ma_strategy()