Стратегия Price Radio
Стратегия реализует индикатор Price Radio Джона Эллерса. Покупка происходит, когда производная цены превышает амплитуду и частоту, продажа — при обратном условии.
Детали
- Условия входа:
- Лонг: производная больше амплитуды и частоты.
- Шорт: производная меньше отрицательных значений амплитуды и частоты.
- Длинные/короткие: обе стороны.
- Условия выхода: противоположный сигнал.
- Стопы: нет.
- Значения по умолчанию:
Length= 14.CandleType= TimeSpan.FromMinutes(1).TimeFrame().
- Фильтры:
- 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()