Использование диаграмм Ренко позволяет фильтровать шум на графике цены, при этом он не учитывает время и объем. График сильно зависит от величины блока, который будет использован при построении.
Мы предложим альтернативный подход к определению оптимального размера блока, сравним с известным подходом при помощи статистических методов.
Перевод статьи Renko brick size optimization.
Привет всем!
Я провожу исследования временных финансовых рядов в Quantroom. Где мы с коллегами работаем над задачами алгоритмических стратегий на фондовом и крипто рынках. Сегодня я расскажу о том, как уменьшить шум в финансовых рядах, используя диаграмму Ренко. Цель статьи — ответить на вопрос: «Есть ли подход, который лучше известных позволяет определить оптимальный размер блока?».
Что такое диаграмма Ренко?
Диаграмма Ренко — это такой тип диаграммы, который отображает только движения цены, исключая время и объем торгов. Ренко не показывает движение цены в единицу времени. Перед тем как ее строить, Вы должны ответить на следующие вопросы:
- Каким должен быть размер блока на диаграмме, который представляет собой величину изменения цены? Если размер блока мал, диаграмма имеет больше движений, вместе с этим больше шума (примечание переводчика — brick — дословно переводится как «кирпич», однако здесь используется более релевантное слово — «блок») .
- Какой параметр цены будет использован для построения диаграммы (например, Open, Close и т.д.)? Также необходимо выбрать таймфрейм, это могут быть дни, часы, минуты или тики. Тиковые данные более точны, потому что содержат все ценовые колебания.

Основные принципы построения диаграммы Ренко:
- Новый блок строится только в том случае, если движение цены превысило заданный пороговый уровень.
- Размер блока всегда один и тот же. Например, если блок равен 10 пунктам, а цена увеличилась на 20, тогда будет нарисовано 2 блока. Если цена выросла, блок будет зелёным (светлым), если цена уменьшилась — красным (тёмным).
- Следующее значение выбранного варианта цены сравнивается с максимумом или минимумом предыдущего блока. Новый блок всегда отображается справа от предыдущего.
- Если цена превысила максимум предыдущего блока на установленный размер или больше, рисуется соответствующее количество зелёных (светлых) блоков так, чтобы нижняя граница нового блока соответствовала верхней границе предыдущего.
- Если цена снизилась ниже минимума предыдущего блока на установленный размер или еще ниже, рисуется соответствующее количество красных (тёмных) блоков так, чтобы верхняя граница нового блока соответствовала нижней границе предыдущего.
В этой статье вы найдете введение в диаграммы Ренко.
Анализ будет проводиться с использованием моего собственного модуля pyrenko, доступного на GitHub.
Существующие подходы к определению размера блока
- Традиционный. Используется фиксированное значение для размера блока.
- ATR. Используются значения, генерируемые индикатором Average True Range (ATR). ATR используется для измерения волатильности финансового инструмента. Метод ATR «автоматически» определяет оптимальный размер блока. Он вычисляет значение ATR на обычном графике, а затем присваивает это значение размеру блока.
Первое значение ATR рассчитывается по среднеарифметической формуле:
Оценка качества и score function
Было бы неплохо измерить качество графика Ренко. Давайте представим простую стратегию, которая использует эти правила следования тренду: ваша позиция должна быть Long, когда текущий блок зеленый (светлый). Переход в Short позицию осуществляется, когда меняется цвет очередного блока. Используя эту логику, мы имеем следующие параметры:
- balance — это сумма положительных и отрицательных сделок. Если текущий блок имеет то же направление, что и предыдущий, значение balance увеличивается на +1. Если направление было изменено, значение баланса уменьшается на -2. Положительное значение balance — хорошо. Чем больше значение, тем лучше.
- sign_changes — здесь учитываем количество изменений направления тренда. Чем меньше это значение, тем лучше.
- price_ratio — соотношение количества исходных ценовых баров к количеству блоков Ренко. Значение, превышающее 1, является хорошим. Чем больше значение, тем лучше.
Score function пытается интегрировать эти параметры в одно значение. Положительное значение — хорошо. Чем больше значение, тем лучше.
Если sign_changes равно 0, перед вычислением, ему надо присвоить значение 1.
Попробуем имитировать и визуализировать эту функцию оценки в трехмерном пространстве. Размер сферы соответствует значению оценки. Красные сферы имеют показатель равный -1.
Импортируем модули и пакеты:
[code python]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import numpy as np import pandas as pd import scipy.stats as st import scipy from scipy.stats import normaltest import scipy.optimize as opt from sklearn.utils import resample import seaborn as sns import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import datetime as dt from dateutil.relativedelta import relativedelta import pyrenko import quandl import talib |
[/code]
Генерируем, вычисляем и визуализируем результат score function:
[code python]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
sf_vals = pd.DataFrame() sf_vals['balance'] = range(-30, 51, 1) sf_vals['sign_changes'] = np.round(np.random.uniform(1, 35, 81)) sf_vals['price_ratio'] = np.random.uniform(0.1, 20, 81) sf_vals['score'] = [ np.log(x.balance / x.sign_changes + 1) * np.log(x.price_ratio) if x.balance >= 0 and x.price_ratio >= 1 else -1 for i, x in sf_vals.iterrows() ] sf_vals['color'] = ['cornflowerblue' if x.balance >= 0 else 'tomato' for i, x in sf_vals.iterrows()] sns.set_style("whitegrid") fig = plt.figure() ax = Axes3D(fig) for i, x in sf_vals.iterrows(): ax.scatter(xs = x['balance'], ys = x['sign_changes'], zs = x['price_ratio'], s = x['score'] * 10 if x['balance'] >=0 else x['score'] * (-7), c = x['color']) ax.set_xlabel('Balance') ax.set_ylabel('Sign changes') ax.set_zlabel('Price ratio') ax.set_title('Score function 3D space') plt.show() |
[/code]
Полученные значения умножаются на константы, чтобы получить более выраженный размер сфер.

Основная идея этого исследования — проверить гипотезу о том, что мы, используя оптимизацию score function, можем получить размер блока лучший, чем дают существующий подход ATR. Данный результат будет проверен по значимости статистическими методами.
Эксперимент
Эксперимент состоит из трех частей:

Подготовка данных (Data preparation)
- Получение списка акций (индекс S&P500) из Википедии.
- Получение цен (High, Low, Close) каждого дня за последние 3 года.
- Удаление активов, у которых есть пропуски в данных.
Код подготовки:
[code python]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
years_ago = 3 quandl_api_key = "YOUR_QUANDL_API_KEY_HERE" # Getting symbols from Wikipedia symbols_table = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_500_companies", header=0)[0] symbols = list(symbols_table.loc[:, "Ticker symbol"]) # Price dataframes stocks_high = pd.DataFrame() stocks_low = pd.DataFrame() stocks_close = pd.DataFrame() # Grabbing data from Quandl for s in symbols: data = quandl.get("WIKI/{}".format(s.replace(".", "_")), api_key = quandl_api_key, trim_start = dt.datetime.now() - relativedelta(years = years_ago), trim_end = dt.datetime.now) stocks_high[s] = data['High'] stocks_close[s] = data['Close'] stocks_low[s] = data['Low'] # Dropping columns with NaN drop_columns = list(stocks_close.columns[stocks_close.isna().sum() > 0]) + list(stocks_high.columns[stocks_high.isna().sum() > 0]) + list(stocks_low.columns[stocks_low.isna().sum() > 0]) drop_columns = np.unique(drop_columns) stocks_high = stocks_high.drop(drop_columns, axis=1) stocks_low = stocks_low.drop(drop_columns, axis=1) stocks_close = stocks_close.drop(drop_columns, axis=1) |
[/code]
В результате собрано 470 акций.
Оптимизация размера блока и оценка диаграммы Ренко (Brick size optimization and Renko chart evaluation)
Эта часть отвечает за оценку диаграммы Ренко. Оптимальный размер блока определяется на проверках, которые содержат 70% дней. score function выполняется на остальных 30% дней.

Обе операции (оптимизация и оценка) выполняются для двух подходов (оптимизация ATR и оптимизации score function).
В случае ATR, его последнее значение считается оптимальным размером блока.
В случае оптимизации score function оптимальный размер блока дает максимальное значение этой функции.

В этом подходе используется минимизация одномерной функции, основанной на методе Брента. Score function должна использоваться со знаком минус, потому что это обратная задача.
Этот код содержит функцию, которая должна быть оптимизирована, а также процесс подсчета / оптимизации:
[code python]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# Max index of train data train_ind = round(stocks_close.shape[0] * 0.7) # Function for optimization def evaluate_renko(brick, history, column_name): renko_obj = pyrenko.renko() renko_obj.set_brick_size(brick_size = brick, auto = False) renko_obj.build_history(prices = history) return renko_obj.evaluate()[column_name] def scoring_stocks(method = 'optimization'): # Create result dataframe stock_result = pd.DataFrame(columns = ['brick_size', 'balance', 'price_ratio', 'score'], index = stocks_close.columns) # Optimization and evaluation of each stock for s in stock_result.index: # Getting bounds by ATR atr = talib.ATR(high = np.double(stocks_high[s][:train_ind]), low = np.double(stocks_low[s][:train_ind]), close = np.double(stocks_close[s][:train_ind]), timeperiod = 14) atr = atr[np.isnan(atr) == False] # Brick size maximization opt_bs = 0.0 if method == 'optimization': opt_bs = opt.fminbound(lambda x: -evaluate_renko(brick = x, history = stocks_close[s][:train_ind], column_name = 'score'), np.min(atr), np.max(atr), disp=0) # Test data data = stocks_close[s][train_ind:] # Build renko chart and evaluation renko_obj = pyrenko.renko() if method == 'optimization': renko_obj.set_brick_size(brick_size = opt_bs, auto = False) else: renko_obj.set_brick_size(brick_size = atr[-1], auto = False) renko_obj.build_history(prices = data) renko_res = renko_obj.evaluate() # Save result to dataframe if method == 'optimization': stock_result.loc[s]['brick_size'] = opt_bs else: stock_result.loc[s]['brick_size'] = atr[-1] stock_result.loc[s]['balance', 'price_ratio', 'score'] = renko_res return stock_result atr_result = scoring_stocks(method = 'atr') opt_result = scoring_stocks(method = 'optimization') |
[/code]
Метод Брента — это алгоритм корневого поиска, объединяющий метод деления пополам, секущий метод и обратную квадратичную интерполяцию. Он обладает надежностью деления пополам, при этом он может быть столь же быстрым, как некоторые из менее надежных методов. Алгоритм, по возможности, пробует использовать потенциально быстрый конвергентный секущий метод или обратную квадратичную интерполяцию, но при необходимости он возвращается к более надежному методу деления пополам. Граничными точками могут быть заданы максимумы и минимумы ATR для набора проверок. Это хорошо в качестве начального приближения. Подробнее о методе Брента можно прочесть здесь.


Давайте получим список активов по значению score для двух подходов. Значения баллов рассчитываются на тестовом прогоне.
Теперь нарисуем диаграммы Ренко тех акций, которые имеют лучшие результаты ($AVY для ATR, $AFL для оптимизации score function).

Блок кода для визуализации результата:
[code python]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# Score histogram for two approaches sns.distplot(atr_result["score"], kde=False, color="cornflowerblue", label="ATR approach") sns.distplot(opt_result["score"], kde=False, color="tomato", label="Score function optimization approach") sns.plt.legend() plt.title('Score distribution') plt.show() # Ordered stocks by score print(atr_result.sort_values(['score'], ascending = False).head(10)) print(opt_result.sort_values(['score'], ascending = False).head(10)) # Best Renko charts renko_obj = pyrenko.renko() renko_obj.set_brick_size(brick_size = opt_result.loc['AFL']['brick_size'], auto = False) renko_obj.build_history(prices = stocks_close['AFL'][train_ind:]) renko_obj.plot_renko() renko_obj.evaluate() renko_obj = pyrenko.renko() renko_obj.set_brick_size(brick_size = atr_result.loc['AVY']['brick_size'], auto = False) renko_obj.build_history(prices = stocks_close['AVY'][train_ind:]) renko_obj.plot_renko() renko_obj.evaluate() |
[/code]
Анализ результатов (Analysis of result)
График рассеивания демонстрирует, как price_ratio соответствует balance:
[code python]
1 2 3 4 5 6 7 |
with sns.axes_style("white"): sns.jointplot(x = atr_result.balance, y = atr_result.price_ratio, kind = "hex", color = "cornflowerblue") plt.show() with sns.axes_style("white"): sns.jointplot(x = opt_result.balance, y = opt_result.price_ratio, kind = "hex", color = "cornflowerblue") plt.show() |
[/code]

Мы можем сделать вывод, что метод оптимизации score function сжимает данные лучше, потому что price_ratio имеет тенденцию к увеличению.
Нарисуем гистограмму, показывающую, количество положительных / нейтральных / отрицательных результатов акций на тестовом наборе:
- положительный: оценка> 0;
- нейтральный: оценка = 0;
- отрицательный: оценка <0.
Код, который подсчитывает и визуализирует результат:
[code python]
1 2 3 4 5 6 7 8 |
opt_result['summary'] = ['negative' if s < 0 else 'positive' if s > 0 else 'neutral' for s in opt_result.score] opt_result['approach'] = 'Score function optimization' atr_result['summary'] = ['negative' if s < 0 else 'positive' if s > 0 else 'neutral' for s in atr_result.score] atr_result['approach'] = 'ATR' sns.countplot(x = "approach", hue = "summary", data = opt_result.append(atr_result), palette=["greenyellow", "cornflowerblue", "tomato"]) plt.title('Number of positive / negative / neutral stocks by method') plt.show() |
[/code]

Гистограммы имеют некоторые отличия, но формы распределений схожи. Давайте посмотрим на гистограмму оценки двух подходов и распределение их разницы:
[code python]
1 2 3 4 5 6 7 8 9 10 |
sns.distplot(atr_result["score"], kde=False, color="cornflowerblue", label="ATR approach") sns.distplot(opt_result["score"], kde=False, color="tomato", label="Score function optimization approach") sns.plt.legend() plt.title('Score distribution') plt.show() diff_score = opt_result.score - atr_result.score sns.distplot(diff_score, kde = False, rug = False) plt.title('Score difference distribution (CFO - ATR)') plt.show() |
[/code]


Будет полезно оценить доверительный интервал разницы в баллах. Это позволит понять, какой подход в среднем дает лучший результат.
Проверим нулевую гипотезу о том, что выборка получена из нормального распределения. Значение p = 1.085e-11, что очень мало. Это означает, что выборка исходит из ненормального распределения:
[code python]
1 |
print(normaltest(diff_score)) |
[/code]
Недопустимо вычислять доверительный интервал при ненормальном распределении.
Мы можем использовать bootstrapping, чтобы получить доверительный интервал. Bootstrapping — компьютерный метод для изучения распределения статистики на основе нескольких поколений выборок методом Монте-Карло на основе имеющейся выборки. Позволяет быстро и легко оценивать различные статистические данные (доверительные интервалы, дисперсию, корреляцию и т. д.) для сложных моделей.

Выборка bootstrapping показана ниже. Она распределяется в соответствии с нормальным распределением, p-значение = 0,9816.
Код реализации bootstrapping’a:
[code python]
1 2 3 4 5 6 7 8 9 10 11 12 13 |
num_bs_size = 1000 num_elements_mean = 100 bs = list() for i in range(0, num_bs_size): bs.append(np.mean(resample(diff_score, n_samples = num_elements_mean))) print(normaltest(bs)) print('99% confidence interval: ', st.t.interval(0.99, len(bs)-1, loc=np.mean(bs), scale=st.sem(bs))) print('t-test one sample: ', st.ttest_1samp(bs, popmean = 0.0)) sns.distplot(bs, kde = False, rug = False, color = 'chartreuse') plt.title('Bootstrapped sample of score difference distribution (CFO - ATR)') plt.show() |
[/code]

Доверительный интервал в 99% равен [0.3717, 0.3964], обе границы строго больше 0. p-значение в t-тесте равно 0,0 (нулевая гипотеза: score_diff = 0.0), мы отклоняем нулевую гипотезу. Эти расчеты подтверждают, что средняя оценка разницы между оптимизацией score function и подходом ATR является более высокой.
Конкретный пример разницы в подходах (акция UNH).

Заключение
- Формализовали score function, которая оценивает качество построенной диаграммы Ренко.
- Описали процесс оптимизации баллов.
- Предлагаемый подход получил лучшее качество, чем классический ATR. Это преимущество статистически значимо.
- pyrenko — это модуль, продемонстрированный в анализе, может быть использован любым исследователем.
- Продемонстрирован полный цикл процесса снижения шума с использованием Renko. Код приведен в статье.
- Теоретически параметр balance, умноженный на brick_size, можно интерпретировать как прибыль на тестовой выборке. Эта простая стратегия следования за трендом, является хорошей основой для дальнейших исследований. Транзакционные издержки должны быть включены для более точной оценки прибыли (комиссии брокера, спред, проскальзывание и т.д.).
- Разработанный подход может использоваться в стратегиях, таких как парный трейдинг, следование за трендом, поддержка и сопротивление и т.д. Очищенный от шума график становится понятным, а трендовые линии более четкими.