OBVious MA Strategy
The strategy enters a long position when OBV crosses above its long entry moving average and exits when OBV crosses below the long exit average. Short positions are entered when OBV crosses below its short entry average and exited when it crosses above the short exit average. A direction filter allows enabling only long or short trades.
Details
- Entry Criteria:
- Long: OBV crosses above long entry MA and direction is not Short.
- Short: OBV crosses below short entry MA and direction is not Long.
- Long/Short: Both
- Exit Criteria:
- Long: OBV crosses below long exit MA.
- Short: OBV crosses above short exit MA.
- Stops: None.
- Default Values:
LongEntryLength= 190LongExitLength= 202ShortEntryLength= 395ShortExitLength= 300TradeDirection= "Long"
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: OBV, SMA
- Stops: No
- Complexity: Basic
- Timeframe: Any
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Low
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()