Собственный индикатор
Для того, чтобы создать свой собственный индикатор, необходимо реализовать интерфейс IIndicator. В качестве примера можно взять исходные коды других индикаторов, которые находятся в репозитории GitHub/StockSharp. Вот как выглядит код реализации индикатора простой скользящей средней SimpleMovingAverage:
/// <summary>
/// Simple moving average.
/// </summary>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.SMAKey,
Description = LocalizedStrings.SimpleMovingAverageKey)]
[Doc("topics/api/indicators/list_of_indicators/sma.html")]
public class SimpleMovingAverage : DecimalLengthIndicator
{
/// <summary>
/// Initializes a new instance of the <see cref="SimpleMovingAverage"/>.
/// </summary>
public SimpleMovingAverage()
{
Length = 32;
Buffer.Stats = CircularBufferStats.Sum;
}
/// <inheritdoc />
protected override decimal? OnProcessDecimal(IIndicatorValue input)
{
var newValue = input.ToDecimal(Source);
if (input.IsFinal)
{
Buffer.PushBack(newValue);
return Buffer.Sum / Length;
}
return (Buffer.SumNoFirst + newValue) / Length;
}
}
SimpleMovingAverage наследуется от класса DecimalLengthIndicator, от которого необходимо наследовать все индикаторы, имеющие в качестве параметра длину периода и работающие с числовыми значениями типа decimal. Метод OnProcessDecimal возвращает decimal? - если вернуть null, будет создано пустое значение индикатора. Буфер Buffer является циклическим (CircularBuffer), а метод PushBack автоматически удаляет самый старый элемент при превышении вместимости.
Важные свойства и методы индикаторов
При создании собственного индикатора особое внимание следует уделить следующим свойствам и методам:
NumValuesToInitialize
Свойство NumValuesToInitialize указывает, сколько значений требуется индикатору для инициализации (формирования или "прогрева"). Это значение используется для определения, когда индикатор можно считать сформированным и готовым к использованию:
/// <inheritdoc />
public override int NumValuesToInitialize => Length;
Для более сложных индикаторов, состоящих из нескольких компонентов, это значение обычно определяется как максимальное из всех составных частей:
/// <inheritdoc />
public override int NumValuesToInitialize => ShortEma.NumValuesToInitialize.Max(LongEma.NumValuesToInitialize);
Measure
Свойство Measure определяет тип измерения и размерность, которую предоставляет индикатор:
/// <inheritdoc />
public override IndicatorMeasures Measure => IndicatorMeasures.Percent;
Доступные типы измерений:
IndicatorMeasures.Price- индикатор измеряет цену (например, скользящие средние)IndicatorMeasures.Percent- индикатор использует процентную шкалу от 0 до 100 (например, RSI)IndicatorMeasures.MinusOnePlusOne- индикатор использует шкалу от -1 до +1IndicatorMeasures.Volume- индикатор измеряет объем (например, OBV)
Это свойство критически важно для правильного отображения индикаторов на графике. Когда на одну и ту же панель накладываются несколько индикаторов с разными размерностями, для индикаторов с отличающимся типом Measure создаются отдельные оси Y. Это позволяет визуально отображать все индикаторы в их естественном масштабе, даже если один из них имеет значения в тысячах (например, цена), а другой измеряется в долях единицы (например, осциллятор).
Save и Load
Методы Save и Load необходимы для сохранения и загрузки настроек индикатора:
/// <inheritdoc />
public override void Save(SettingsStorage storage)
{
base.Save(storage);
storage.SetValue(nameof(ShortPeriod), ShortPeriod);
storage.SetValue(nameof(LongPeriod), LongPeriod);
}
/// <inheritdoc />
public override void Load(SettingsStorage storage)
{
base.Load(storage);
ShortPeriod = storage.GetValue<int>(nameof(ShortPeriod));
LongPeriod = storage.GetValue<int>(nameof(LongPeriod));
}
В этих методах необходимо сохранять и загружать все настраиваемые параметры индикатора. Это обеспечивает корректное сохранение и восстановление состояния индикатора при сохранении и загрузке стратегии.
Важно вызывать базовые методы base.Save() и base.Load() для обработки общих параметров индикатора, унаследованных от базового класса.
Составные индикаторы
Некоторые индикаторы являются составными, и используют в своих расчетах другие индикаторы. Поэтому индикаторы можно переиспользовать друг из друга, как показано в качестве примера реализация индикатора волатильности Чайкина ChaikinVolatility:
/// <summary>
/// Chaikin volatility.
/// </summary>
/// <remarks>
/// https://doc.stocksharp.com/topics/api/indicators/list_of_indicators/chv.html
/// </remarks>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.ChaikinVolatilityKey,
Description = LocalizedStrings.ChaikinVolatilityIndicatorKey)]
[IndicatorIn(typeof(CandleIndicatorValue))]
[Doc("topics/api/indicators/list_of_indicators/chv.html")]
public class ChaikinVolatility : BaseIndicator
{
/// <summary>
/// Initializes a new instance of the <see cref="ChaikinVolatility"/>.
/// </summary>
public ChaikinVolatility()
{
Ema = new();
Roc = new();
AddResetTracking(Ema);
AddResetTracking(Roc);
}
/// <summary>
/// Moving Average.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.MAKey,
Description = LocalizedStrings.MovingAverageKey,
GroupName = LocalizedStrings.GeneralKey)]
public ExponentialMovingAverage Ema { get; }
/// <summary>
/// Rate of change.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.ROCKey,
Description = LocalizedStrings.RateOfChangeKey,
GroupName = LocalizedStrings.GeneralKey)]
public RateOfChange Roc { get; }
/// <inheritdoc />
protected override bool CalcIsFormed() => Roc.IsFormed;
/// <inheritdoc />
public override int NumValuesToInitialize => Ema.NumValuesToInitialize + Roc.NumValuesToInitialize - 1;
/// <inheritdoc />
public override IndicatorMeasures Measure => IndicatorMeasures.Percent;
/// <inheritdoc />
protected override IIndicatorValue OnProcess(IIndicatorValue input)
{
var candle = input.ToCandle();
var emaValue = Ema.Process(input, candle.GetLength());
if (Ema.IsFormed)
{
var val = Roc.Process(emaValue);
return new DecimalIndicatorValue(this, val.ToDecimal(Source), input.Time);
}
return new DecimalIndicatorValue(this, input.Time);
}
/// <inheritdoc />
public override void Load(SettingsStorage storage)
{
base.Load(storage);
Ema.LoadIfNotNull(storage, nameof(Ema));
Roc.LoadIfNotNull(storage, nameof(Roc));
}
/// <inheritdoc />
public override void Save(SettingsStorage storage)
{
base.Save(storage);
storage.SetValue(nameof(Ema), Ema.Save());
storage.SetValue(nameof(Roc), Roc.Save());
}
}
Индикаторы с несколькими линиями
Последний вид индикаторов - это те, которые не просто состоят из других индикаторов, но так же графически отображаются несколькими состояниями одновременно (несколькими линиями). Например, AverageDirectionalIndex:
/// <summary>
/// Welles Wilder Average Directional Index.
/// </summary>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.AdxKey,
Description = LocalizedStrings.AverageDirectionalIndexKey)]
[Doc("topics/api/indicators/list_of_indicators/adx.html")]
[IndicatorOut(typeof(IAverageDirectionalIndexValue))]
public class AverageDirectionalIndex : BaseComplexIndicator<IAverageDirectionalIndexValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="AverageDirectionalIndex"/>.
/// </summary>
public AverageDirectionalIndex()
: this(new DirectionalIndex { Length = 14 }, new WilderMovingAverage { Length = 14 })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AverageDirectionalIndex"/>.
/// </summary>
/// <param name="dx">Welles Wilder Directional Movement Index.</param>
/// <param name="movingAverage">Moving Average.</param>
public AverageDirectionalIndex(DirectionalIndex dx, DecimalLengthIndicator movingAverage)
: base(dx, movingAverage)
{
Dx = dx;
MovingAverage = movingAverage;
Mode = ComplexIndicatorModes.Sequence;
}
/// <inheritdoc />
public override IndicatorMeasures Measure => IndicatorMeasures.Percent;
/// <summary>
/// Welles Wilder Directional Movement Index.
/// </summary>
[Browsable(false)]
public DirectionalIndex Dx { get; }
/// <summary>
/// Moving Average.
/// </summary>
[Browsable(false)]
public DecimalLengthIndicator MovingAverage { get; }
/// <summary>
/// Period length.
/// </summary>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.PeriodKey,
Description = LocalizedStrings.IndicatorPeriodKey,
GroupName = LocalizedStrings.GeneralKey)]
public int Length
{
get => MovingAverage.Length;
set
{
MovingAverage.Length = Dx.Length = value;
Reset();
}
}
/// <inheritdoc />
public override void Load(SettingsStorage storage)
{
base.Load(storage);
Length = storage.GetValue<int>(nameof(Length));
}
/// <inheritdoc />
public override void Save(SettingsStorage storage)
{
base.Save(storage);
storage.SetValue(nameof(Length), Length);
}
/// <inheritdoc />
protected override IAverageDirectionalIndexValue CreateValue(DateTime time)
=> new AverageDirectionalIndexValue(this, time);
}
/// <summary>
/// <see cref="AverageDirectionalIndex"/> indicator value.
/// </summary>
public interface IAverageDirectionalIndexValue : IComplexIndicatorValue
{
/// <summary>
/// Gets the <see cref="AverageDirectionalIndex.Dx"/> value.
/// </summary>
IDirectionalIndexValue Dx { get; }
/// <summary>
/// Gets the <see cref="AverageDirectionalIndex.MovingAverage"/> value.
/// </summary>
IIndicatorValue MovingAverageValue { get; }
/// <summary>
/// Gets the <see cref="AverageDirectionalIndex.MovingAverage"/> value.
/// </summary>
[Browsable(false)]
decimal? MovingAverage { get; }
}
/// <summary>
/// AverageDirectionalIndex indicator value implementation.
/// </summary>
public class AverageDirectionalIndexValue(AverageDirectionalIndex indicator, DateTime time)
: ComplexIndicatorValue<AverageDirectionalIndex>(indicator, time), IAverageDirectionalIndexValue
{
/// <inheritdoc />
public IDirectionalIndexValue Dx => (IDirectionalIndexValue)this[TypedIndicator.Dx];
/// <inheritdoc />
public IIndicatorValue MovingAverageValue => this[TypedIndicator.MovingAverage];
/// <inheritdoc />
public decimal? MovingAverage => MovingAverageValue.ToNullableDecimal(TypedIndicator.Source);
}
Такие индикаторы должны наследоваться от класса BaseComplexIndicator<TValue>, где TValue - интерфейс значения, реализующий IComplexIndicatorValue. Составные части индикатора передаются через конструктор в base(...). Каждый составной индикатор обязан реализовать собственный интерфейс значения и класс, производный от ComplexIndicatorValue<T>, а также переопределить метод CreateValue(DateTime). Атрибут [IndicatorOut] указывает на интерфейс значения. BaseComplexIndicator будет обрабатывать данные части один за другим. Если BaseComplexIndicator.Mode установлено в ComplexIndicatorModes.Sequence, то результирующее значение первого индикатора будет передано в качестве входного значения второму, и так далее до конца. Если же установлено значение ComplexIndicatorModes.Parallel, то результаты вложенных индикаторов игнорируются.
Пример комплексного индикатора с реализацией SaveLoad
Ниже приведен пример реализации индикатора Percentage Volume Oscillator (PVO), который демонстрирует реализацию свойств NumValuesToInitialize, Measure, а также методов Save и Load:
/// <summary>
/// Percentage Volume Oscillator (PVO).
/// </summary>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.PVOKey,
Description = LocalizedStrings.PercentageVolumeOscillatorKey)]
[IndicatorIn(typeof(CandleIndicatorValue))]
[Doc("topics/api/indicators/list_of_indicators/percentage_volume_oscillator.html")]
[IndicatorOut(typeof(IPercentageVolumeOscillatorValue))]
public class PercentageVolumeOscillator : BaseComplexIndicator<IPercentageVolumeOscillatorValue>
{
/// <summary>
/// Short EMA.
/// </summary>
[Browsable(false)]
public ExponentialMovingAverage ShortEma { get; }
/// <summary>
/// Long EMA.
/// </summary>
[Browsable(false)]
public ExponentialMovingAverage LongEma { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PercentageVolumeOscillator"/>.
/// </summary>
public PercentageVolumeOscillator()
: this(new(), new())
{
ShortPeriod = 12;
LongPeriod = 26;
}
/// <summary>
/// Initializes a new instance of the <see cref="PercentageVolumeOscillator"/>.
/// </summary>
/// <param name="shortEma">The short-term EMA.</param>
/// <param name="longEma">The long-term EMA.</param>
public PercentageVolumeOscillator(ExponentialMovingAverage shortEma, ExponentialMovingAverage longEma)
: base(shortEma, longEma)
{
ShortEma = shortEma;
LongEma = longEma;
}
/// <summary>
/// Short period.
/// </summary>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.ShortPeriodKey,
Description = LocalizedStrings.ShortMaDescKey,
GroupName = LocalizedStrings.GeneralKey)]
public int ShortPeriod
{
get => ShortEma.Length;
set => ShortEma.Length = value;
}
/// <summary>
/// Long period.
/// </summary>
[Display(
ResourceType = typeof(LocalizedStrings),
Name = LocalizedStrings.LongPeriodKey,
Description = LocalizedStrings.LongMaDescKey,
GroupName = LocalizedStrings.GeneralKey)]
public int LongPeriod
{
get => LongEma.Length;
set => LongEma.Length = value;
}
/// <inheritdoc />
public override IndicatorMeasures Measure => IndicatorMeasures.Volume;
/// <inheritdoc />
public override int NumValuesToInitialize => ShortEma.NumValuesToInitialize.Max(LongEma.NumValuesToInitialize);
/// <inheritdoc />
protected override bool CalcIsFormed() => ShortEma.IsFormed && LongEma.IsFormed;
/// <inheritdoc />
protected override IIndicatorValue OnProcess(IIndicatorValue input)
{
var volume = input.ToCandle().TotalVolume;
var result = new PercentageVolumeOscillatorValue(this, input.Time);
var shortValue = ShortEma.Process(input, volume);
var longValue = LongEma.Process(input, volume);
result.Add(ShortEma, shortValue);
result.Add(LongEma, longValue);
if (LongEma.IsFormed)
{
var den = longValue.ToDecimal(Source);
var pvo = den == 0 ? 0 : ((shortValue.ToDecimal(Source) - den) / den) * 100;
result.Add(this, new DecimalIndicatorValue(this, pvo, input.Time) { IsFinal = input.IsFinal });
}
return result;
}
/// <inheritdoc />
public override void Save(SettingsStorage storage)
{
base.Save(storage);
storage.SetValue(nameof(ShortPeriod), ShortPeriod);
storage.SetValue(nameof(LongPeriod), LongPeriod);
}
/// <inheritdoc />
public override void Load(SettingsStorage storage)
{
base.Load(storage);
ShortPeriod = storage.GetValue<int>(nameof(ShortPeriod));
LongPeriod = storage.GetValue<int>(nameof(LongPeriod));
}
/// <inheritdoc />
public override string ToString() => base.ToString() + $" S={ShortPeriod},L={LongPeriod}";
/// <inheritdoc />
protected override IPercentageVolumeOscillatorValue CreateValue(DateTime time)
=> new PercentageVolumeOscillatorValue(this, time);
}
/// <summary>
/// <see cref="PercentageVolumeOscillator"/> indicator value.
/// </summary>
public interface IPercentageVolumeOscillatorValue : IComplexIndicatorValue
{
/// <summary>
/// Gets the short EMA value.
/// </summary>
IIndicatorValue ShortEmaValue { get; }
/// <summary>
/// Gets the short EMA value.
/// </summary>
[Browsable(false)]
decimal? ShortEma { get; }
/// <summary>
/// Gets the long EMA value.
/// </summary>
IIndicatorValue LongEmaValue { get; }
/// <summary>
/// Gets the long EMA value.
/// </summary>
[Browsable(false)]
decimal? LongEma { get; }
}
/// <summary>
/// Percentage Volume Oscillator indicator value implementation.
/// </summary>
public class PercentageVolumeOscillatorValue(PercentageVolumeOscillator indicator, DateTime time)
: ComplexIndicatorValue<PercentageVolumeOscillator>(indicator, time), IPercentageVolumeOscillatorValue
{
/// <inheritdoc />
public IIndicatorValue ShortEmaValue => this[TypedIndicator.ShortEma];
/// <inheritdoc />
public decimal? ShortEma => ShortEmaValue.ToNullableDecimal(TypedIndicator.Source);
/// <inheritdoc />
public IIndicatorValue LongEmaValue => this[TypedIndicator.LongEma];
/// <inheritdoc />
public decimal? LongEma => LongEmaValue.ToNullableDecimal(TypedIndicator.Source);
}
Этот пример демонстрирует:
- Реализацию
NumValuesToInitializeдля комплексного индикатора - Указание типа измерения через свойство
Measure - Реализацию интерфейса значения
IPercentageVolumeOscillatorValueи классаPercentageVolumeOscillatorValueс primary constructor синтаксисом - Использование
[IndicatorOut(typeof(IPercentageVolumeOscillatorValue))]с интерфейсом (не конкретным классом) - Корректные реализации методов
SaveиLoadдля сохранения и загрузки параметров - Переопределение
CreateValue(DateTime)для создания экземпляра значения индикатора