Индикатор проще развивать, понимая происхождение значения. Какие данные входят в расчёт? Что сохраняется на следующую свечу? Что изменится с другой средней или источником цен? Ответы даёт и исходный код.

Библиотека ta PyneCore написана на Pyne code. Её Persistent, вызовы и условия используют знакомое поведение. Все исходники доступны: готовый индикатор можно проследить так же, как свой.

Эта продвинутая глава учит чтению исходников и экспериментам. Не нужно сразу понимать всю библиотеку. Начните с короткой функции и проследите вызовы, на которых она основана.

Откройте настоящую реализацию

  1. Откройте прежний moving_averages.py в PyneIDE.
  2. В ta.ema(close, 10) удерживайте Command в macOS или Ctrl в Windows и Linux и нажмите ema. Подойдёт и Go to Definition правой кнопкой.
  3. Откроется установленный ta.py PyneCore у определения ema.
  4. Прочитайте описание и тело. Из вызова другой функции можно перейти к её определению.
Редактор PyneIDE показывает определение ema в библиотечном ta.py и описание начального значения.
Переход к определению открывает EMA установленного ta.py. Ваша программа остаётся в другой вкладке.

Параметры и определения вы уже открывали в главе о помощи редактора. То же действие открывает RSI, MACD и остальные функции. Полный справочник ta помогает находить функции и понимать параметры.

SMA: сумма за формулой

Две строки ta.sma:

length = int(length)
return lib_math.sum(source, length) / length

Среднее равно сумме окна, делённой на размер. После преобразования длины в целое суммой занимается lib_math.sum. Состояние находится не в теле sma, а в вызванном суммировании. Проследив через math.py, вы дойдёте до _math_stateful.py.

При чтении помните: короткая функция тоже может иметь состояние. Её вызовы входят в поведение расчёта. Среднее вручную складывало то же окно своим циклом.

EMA и RMA: обновление сохранённого среднего

После прогрева EMA выполняет:

last_val = last_val + alpha * (source - last_val)
return last_val

last_val является прошлым средним, source текущим значением. Старое среднее меняется на долю разности. В EMA alpha равно 2 / (length + 1). При длине десять прибавляется около 18,18 процента разницы новой цены и старого среднего.

В исходнике alpha и last_val помечены Persistent. Вначале last_val отсутствует, поэтому функция пытается получить SMA той же длины:

if not (last_val == last_val):
    last_val = sma(source, length)
    return last_val

Пока данных для SMA мало, результат отсутствует. Получив начальное среднее, функция обновляет прошлое. not (last_val == last_val) проверяет отсутствие: отсутствующее значение не равно себе. В своих примерах вы использовали na(value).

RMA, сглаживание Уайлдера, начинается похоже, но обновляется иначе:

last_val = (last_val * (length - 1) + source) / length
return last_val

При длине десять вес нового значения одна десятая. В EMA при той же длине вес больше, поэтому реакция на один ценовой ряд различается.

Полный код также показывает обработку отсутствующего входа и упрощение для длины один. Даже одинаковая формула может дать разницу из-за начального значения, пропусков или разных свечей вызова. Порядок операций с плавающей точкой тоже важен: сохраняйте его для точного сравнения.

RSI: два отдельных сглаженных изменения

RSI разделяет рост и падение источника. Основные строки PyneCore:

rma_u = rma(builtins.max(source - prev_src, 0.0), length)
rma_d = rma(builtins.max(prev_src - source, 0.0), length)
prev_src = source

prev_src хранит прежний допустимый источник через Persistent. При росте 100 до 103 изменение вверх 3, вниз 0. При падении 103 до 101 это 0 и 2. RMA сглаживает ряды отдельно, каждому месту вызова принадлежит своё состояние.

prev_src обновляется после вычисления разностей. Тот же порядок был в дополнительном задании с сохранением прошлого значения.

RSI получается из соотношения сглаженных изменений:

if rma_d <= 1e-10:
    return 100.0
if rma_u <= 1e-10:
    return 0.0

return 100 - 100 / (1 + rma_u / rma_d)

Отсутствие падения даёт 100, отсутствие роста 0. Исходник учитывает очень малые значения и сначала проверяет нисходящую часть: если обе ниже порога, результат 100. Одной формулы недостаточно, чтобы узнать такие детали.

Первая допустимая цена ещё не даёт изменения. RSI сохраняет её и начинает сглаживания только со следующим значением. Прогрев определяется числом изменений. RSI не является вероятностью: 70 не означает 70-процентный шанс роста.

MACD: новый индикатор из средних

MACD берёт разность двух EMA и сглаживает её ещё одной EMA. Основные шаги без проверок отсутствия:

fast = ema(source, fastlen)
slow = ema(source, slowlen)
macd_val = fast - slow
signal = ema(macd_val, siglen)

Три возвращаемых значения:

return macd_val, signal, macd_val - signal

MACD-линия является разностью быстрой и медленной средних. Сигнальная линия сглаживает её. Гистограмма равна разности линий и нулю при их равенстве.

Один вызов даёт три результата для трёх переменных:

macd_line, signal_line, histogram = ta.macd(close, 12, 26, 9)

Порядок важен: MACD, сигнал, гистограмма. До появления обеих ценовых средних разность отсутствует. Потом сигнальная линия проходит свой прогрев. Начальные пропуски сложного индикатора следуют и из внутренних расчётов.

ATR: ценовой скачок тоже входит в диапазон

Реализация короткая:

return rma(tr(True), length)

Знакомая RMA сглаживает True Range, истинный диапазон. Он равен максимуму трёх расстояний:

  • текущий максимум минус минимум;
  • расстояние максимума от прошлого закрытия;
  • расстояние минимума от прошлого закрытия.

При прошлом закрытии 100, текущем минимуме 108 и максимуме 110 диапазон свечи лишь 2. Истинный диапазон max(2, 10, 8), то есть 10: он учитывает скачок после закрытия.

На первой свече без прошлого закрытия tr(True) использует high - low. В исходнике прошлое закрытие читается из внутренних данных среды. ATR показывает величину движения в единицах цены, но не направление.

Полосы Боллинджера: среднее и стандартное отклонение

Центр ta.bb является SMA, границы отстоят на заданный множитель стандартного отклонения. Сначала считаются отклонение и среднее:

std_dev = stdev(series, length)
middle = sma(series, length)

Для допустимого центра возврат:

std_dev *= mult
return middle, middle + std_dev, middle - std_dev

Стандартное отклонение описывает разброс окна вокруг среднего. При центре 100, отклонении 3 и множителе 2 верхняя граница 106, нижняя 94. Снова три значения, но иной порядок, чем у MACD: центр, верх, низ.

Свой вариант: сначала повторите оригинал

Создайте bands_workshop.py. Собственная функция собирает полосы из простых частей библиотеки. Для сравнения также вычисляется верхняя граница ta.bb.

"""
@pyne edge
"""
from pynecore.lib import close, color, input, plot, script, ta


def custom_bands(source: float, length: int, multiplier: float) -> tuple[float, float, float]:
    middle = ta.sma(source, length)
    deviation = ta.stdev(source, length)
    upper = middle + multiplier * deviation
    lower = middle - multiplier * deviation
    return middle, upper, lower


@script.indicator("Bands Workshop", overlay=True)
def main(
    length: int = input.int(20, title="Length", minval=2, maxval=200),
    multiplier: float = input.float(2.0, title="Deviation multiplier", minval=0.1, maxval=5.0),
):
    builtin_middle, builtin_upper, builtin_lower = ta.bb(close, length, multiplier)
    own_middle, own_upper, own_lower = custom_bands(close, length, multiplier)
    plot(builtin_upper, "Built-in upper", color=color.blue, linewidth=3)
    plot(own_upper, "Custom upper", color=color.orange)
    plot(own_middle, "Custom middle", color=color.green)
    plot(own_lower, "Custom lower", color=color.gray)

tuple[float, float, float] задаёт тип результата из трёх чисел. return middle, upper, lower возвращает тройку, три переменные перед вызовом принимают значения отдельно. builtin_middle и builtin_lower сейчас не рисуются, но позже пригодятся для проверки.

Запустите на демонстрационных данных. Верхние границы совпадают, тонкая оранжевая лежит поверх толстой синей. Наведите указатель на свечу и сравните Built-in upper и Custom upper. При длине двадцать у первых девятнадцати свечей нет полного окна.

График рядом с bands_workshop.py показывает равные Built-in upper и Custom upper, а также собственные центр и нижнюю полосу.
Обе верхние полосы на выбранной свече показывают 216.85 в английском интерфейсе. Линии совпадают.

Для точной проверки откройте значения через Open plot output CSV. У своего варианта проверяйте первую допустимую точку и последующие. Внешне одинаковые линии могут слегка различаться.

Попробуйте самостоятельно: центр EMA

Измените только центр custom_bands: EMA вместо SMA. Отклонение и готовый ta.bb оставьте для сравнения.

Предскажите: будет ли верхняя граница совпадать всегда? Что произойдёт после резкого изменения цены?

Решение

Первая строка расчёта:

middle = ta.ema(source, length)

Границы теперь измеряются от EMA, расчёт ta.stdev прежний. Центр и границы обычно сдвигаются относительно оригинала. Разность верхних значений равна разности EMA и SMA, поскольку к ним добавляются одинаковые отклонение и множитель.

Это ваш экспериментальный вариант. Изменение линии само не говорит об улучшении торгового правила. Это проверяется отдельной стратегией на тех же данных и настройках.

Продолжайте изучать библиотеку

Выберите индикатор и опишите вход, сохраняемые данные и результат своими словами. Идите по внутренним вызовам до понимания расчёта. Для RSI цепочка rsi → rma → sma ведёт от разностей к сглаживанию и начальному значению.

Читайте ветви отсутствий, прогрева и граничных случаев. Меняйте сначала одну деталь и сравнивайте с оригиналом. Так намеренное изменение отделяется от ошибки.

Исходники остальных функций доступны тем же нажатием с Command или Ctrl. На длинный индикатор нужно больше времени, но подход прежний: входы, память, расчёт, результат.