The Price Radio Strategy
This strategy implements John Ehlers' Price Radio indicator. It enters long when the price derivative exceeds both amplitude and frequency thresholds, and enters short when it falls below their negative values.
Details
- Entry Criteria:
- Long: derivative greater than amplitude and frequency.
- Short: derivative less than negative amplitude and negative frequency.
- Long/Short: Both sides.
- Exit Criteria: Opposite signal.
- Stops: No.
- Default Values:
Length= 14.CandleType= TimeSpan.FromMinutes(1).TimeFrame().
- Filters:
- Category: Oscillator
- Direction: Both
- Indicators: Custom
- Stops: No
- Complexity: Basic
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
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>
/// John Ehlers' Price Radio strategy.
/// Uses derivative-based amplitude and frequency thresholds to trade.
/// </summary>
public class ThePriceRadioStrategy : Strategy
{
private readonly StrategyParam<int> _length;
private readonly StrategyParam<int> _maxEntries;
private readonly StrategyParam<int> _holdBars;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private Highest _envelope = null!;
private SimpleMovingAverage _amSma = null!;
private Highest _derivHigh = null!;
private Lowest _derivLow = null!;
private SimpleMovingAverage _fmSma = null!;
private decimal _prevClose;
private int _entriesExecuted;
private int _barsInPosition;
private int _barsSinceSignal;
/// <summary>
/// Lookback period.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// Maximum number of entries per run.
/// </summary>
public int MaxEntries
{
get => _maxEntries.Value;
set => _maxEntries.Value = value;
}
/// <summary>
/// Forced position holding period in finished bars.
/// </summary>
public int HoldBars
{
get => _holdBars.Value;
set => _holdBars.Value = value;
}
/// <summary>
/// Minimum finished bars between entries.
/// </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 a new instance of the <see cref="ThePriceRadioStrategy"/> class.
/// </summary>
public ThePriceRadioStrategy()
{
_length = Param(nameof(Length), 14)
.SetGreaterThanZero()
.SetDisplay("Length", "Lookback period", "General")
;
_maxEntries = Param(nameof(MaxEntries), 45)
.SetGreaterThanZero()
.SetDisplay("Max Entries", "Maximum entries per run", "Risk");
_holdBars = Param(nameof(HoldBars), 180)
.SetGreaterThanZero()
.SetDisplay("Hold Bars", "Bars to hold position before forced exit", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 240)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Minimum bars between entries", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_envelope = null!;
_amSma = null!;
_derivHigh = null!;
_derivLow = null!;
_fmSma = null!;
_prevClose = 0;
_entriesExecuted = 0;
_barsInPosition = 0;
_barsSinceSignal = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_envelope = new Highest { Length = 4 };
_amSma = new SMA { Length = Length };
_derivHigh = new Highest { Length = Length };
_derivLow = new Lowest { Length = Length };
_fmSma = new SMA { Length = Length };
_entriesExecuted = 0;
_barsInPosition = 0;
_barsSinceSignal = CooldownBars;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _amSma);
DrawIndicator(area, _fmSma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_prevClose == 0)
{
_prevClose = candle.ClosePrice;
return;
}
var deriv = candle.ClosePrice - _prevClose;
_prevClose = candle.ClosePrice;
var envelope = _envelope.Process(new DecimalIndicatorValue(_envelope, Math.Abs(deriv), candle.OpenTime)).ToDecimal();
var am = _amSma.Process(new DecimalIndicatorValue(_amSma, envelope, candle.OpenTime)).ToDecimal();
var high = _derivHigh.Process(new DecimalIndicatorValue(_derivHigh, deriv, candle.OpenTime)).ToDecimal();
var low = _derivLow.Process(new DecimalIndicatorValue(_derivLow, deriv, candle.OpenTime)).ToDecimal();
var clamped = Math.Min(Math.Max(10m * deriv, low), high);
var fm = _fmSma.Process(new DecimalIndicatorValue(_fmSma, clamped, candle.OpenTime)).ToDecimal();
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position != 0)
{
_barsInPosition++;
if (_barsInPosition >= HoldBars)
{
if (Position > 0)
SellMarket(Math.Abs(Position));
else
BuyMarket(Math.Abs(Position));
_barsInPosition = 0;
_barsSinceSignal = 0;
}
return;
}
_barsInPosition = 0;
_barsSinceSignal++;
if (_entriesExecuted >= MaxEntries || _barsSinceSignal < CooldownBars)
return;
if (deriv > am && deriv > fm)
{
BuyMarket();
_entriesExecuted++;
_barsSinceSignal = 0;
}
else if (deriv < -am && deriv < -fm)
{
SellMarket();
_entriesExecuted++;
_barsSinceSignal = 0;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import Highest, Lowest, SimpleMovingAverage, IndicatorHelper
from StockSharp.Algo.Strategies import Strategy
class the_price_radio_strategy(Strategy):
"""Ehlers Price Radio: derivative-based amplitude/frequency with hold bars and cooldown."""
def __init__(self):
super(the_price_radio_strategy, self).__init__()
self._length = self.Param("Length", 14).SetGreaterThanZero().SetDisplay("Length", "Lookback period", "General")
self._max_entries = self.Param("MaxEntries", 45).SetGreaterThanZero().SetDisplay("Max Entries", "Maximum entries per run", "Risk")
self._hold_bars = self.Param("HoldBars", 180).SetGreaterThanZero().SetDisplay("Hold Bars", "Bars to hold position", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 240).SetGreaterThanZero().SetDisplay("Cooldown Bars", "Minimum bars between entries", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(the_price_radio_strategy, self).OnReseted()
self._prev_close = 0
self._entries_executed = 0
self._bars_in_pos = 0
self._bars_since_signal = 0
def OnStarted2(self, time):
super(the_price_radio_strategy, self).OnStarted2(time)
self._prev_close = 0
self._entries_executed = 0
self._bars_in_pos = 0
self._bars_since_signal = self._cooldown_bars.Value
length = self._length.Value
self._envelope = Highest()
self._envelope.Length = 4
self._am_sma = SimpleMovingAverage()
self._am_sma.Length = length
self._deriv_high = Highest()
self._deriv_high.Length = length
self._deriv_low = Lowest()
self._deriv_low.Length = length
self._fm_sma = SimpleMovingAverage()
self._fm_sma.Length = length
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if self._prev_close == 0:
self._prev_close = close
return
deriv = close - self._prev_close
self._prev_close = close
t = candle.OpenTime
abs_deriv = abs(deriv)
envelope = float(IndicatorHelper.ToDecimal(IndicatorHelper.Process(self._envelope, Decimal(abs_deriv), t, True)))
am = float(IndicatorHelper.ToDecimal(IndicatorHelper.Process(self._am_sma, Decimal(envelope), t, True)))
high = float(IndicatorHelper.ToDecimal(IndicatorHelper.Process(self._deriv_high, Decimal(deriv), t, True)))
low = float(IndicatorHelper.ToDecimal(IndicatorHelper.Process(self._deriv_low, Decimal(deriv), t, True)))
clamped = min(max(10 * deriv, low), high)
fm = float(IndicatorHelper.ToDecimal(IndicatorHelper.Process(self._fm_sma, Decimal(clamped), t, True)))
if self.Position != 0:
self._bars_in_pos += 1
if self._bars_in_pos >= self._hold_bars.Value:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
self._bars_in_pos = 0
self._bars_since_signal = 0
return
self._bars_in_pos = 0
self._bars_since_signal += 1
if self._entries_executed >= self._max_entries.Value or self._bars_since_signal < self._cooldown_bars.Value:
return
if deriv > am and deriv > fm:
self.BuyMarket()
self._entries_executed += 1
self._bars_since_signal = 0
elif deriv < -am and deriv < -fm:
self.SellMarket()
self._entries_executed += 1
self._bars_since_signal = 0
def CreateClone(self):
return the_price_radio_strategy()