
Как программиста, меня всегда раздражали классические описания торговых паттернов:
«Левое плечо должно быть примерно на одном уровне с правым».
«Линия шеи может иметь небольшой наклон».
«Между вершинами должно пройти некоторое время».
«Тело свечи маленькое, а нижняя тень достаточно длинная».
Для человека это звучит нормально. Трейдер смотрит на график и действительно видит голову и плечи, двойную вершину, флаг, повешенного или какую-нибудь другую фигуру с красивым названием. Для алгоритма все эти описания почти бесполезны. Машина не понимает, что такое «примерно», «небольшой» и «достаточно длинный». Ей нужны числа, интервалы и конкретные условия. Как только начинаешь переводить классическое описание паттерна в код, появляется зоопарк из коэффициентов:
высота второго пика должна отличаться от первого не более чем на 12%;
глубина впадины должна составлять от 20% до 60% высоты движения;
наклон линии не должен превышать определённого значения;
объём должен вести себя ещё каким-то образом.
Потом находится график, который человек уверенно называет «головой и плечами», но алгоритм его отбрасывает. Ослабляешь одно условие — алгоритм начинает находить огромное количество мусора. Добавляешь новое условие. Потом ещё одно. В итоге получается не торговая система, а слой из if, else и параметров, смысл которых через полгода уже никто не помнит. Я пошёл другим путём. Вместо того чтобы объяснять роботу, как должна выглядеть конкретная фигура, я поставил более простой вопрос:
Были ли в истории участки графика, похожие на тот, который наблюдается сейчас, и что происходило после них?
Меня не особенно интересует, называется текущая формация «головой и плечами», «чашкой с ручкой», «кривым флагом» или вообще никак не называется. Если текущий участок рынка похож на несколько исторических участков, а после них цена вела себя определённым образом, это уже можно исследовать алгоритмически.
Любой участок графика можно представить в виде обычного вектора. Предположим, есть последние N баров:
X=(x1,x2,x3,…,xN)
Это может быть последовательность цен закрытия, относительных изменений, нормализованных цен или другого выбранного признака. Далее берётся исторический участок той же длины:
Y=(y1,y2,y3,…,yN)
После этого остаётся измерить, насколько две последовательности похожи. Для этого существует много методов:
евклидово расстояние;
косинусное сходство;
корреляция;
Dynamic Time Warping;
Matrix Profile;
кластеризация;
нейросетевые представления временных рядов.
Я начал с обычной корреляции Пирсона. Не потому, что это самый современный и модный алгоритм. Как раз наоборот: он старый, простой, быстрый и понятный. Это хороший базовый инструмент, поведение которого можно проверить и объяснить. Коэффициент корреляции Пирсона рассчитывается так:

Если не углубляться в математику, результат находится в диапазоне от −1 до 1. Значение, близкое к 1, означает, что два участка менялись похожим образом. Значение около нуля говорит об отсутствии выраженного сходства. Значение около −1 соответствует почти зеркальному движению. Для поиска похожих паттернов в первую очередь интересны положительные значения, находящиеся выше выбранного порога. В упрощённом виде логика выглядит так:
<code class="python">current_window = get_current_window(length=N)<br /><br />candidates = []<br /><br />for historical_window in history:<br /> correlation = pearson(<br /> current_window,<br /> historical_window,<br /> )<br /><br /> if correlation >= minimum_correlation:<br /> candidates.append(historical_window)</code>
На этом этапе алгоритм получает список исторических участков, геометрия которых похожа на текущую. Но здесь начинается самое важное.
Корреляция — это ещё не прогнозВысокая корреляция говорит только о том, что два участка истории похожи друг на друга внутри рассматриваемого окна. Она ничего не говорит о том, что произойдёт дальше. Это принципиальный момент, который легко упустить. Особенно когда на экране видишь два почти одинаковых графика и мозг сам дорисовывает продолжение. Корреляция в такой системе — только первичный фильтр. Она отбирает потенциальных кандидатов. Торгового сигнала на этом этапе ещё нет. Более того, у коэффициента Пирсона есть свойства, которые одновременно являются его преимуществами и недостатками. Например, он практически не обращает внимания на абсолютный уровень цены. Участок графика на значении 15 000 может быть похож на участок на значении 20 000. Для поиска формы это удобно. Пирсон также почти не реагирует на линейное изменение масштаба. Движение на 10 пунктов и движение на 100 пунктов могут получить высокую корреляцию, если их форма совпадает. И вот здесь уже возникает проблема. Геометрия одинаковая, но рыночная ситуация совершенно разная. Один участок сформировался в спокойном рынке, другой — во время резкого увеличения волатильности. Для управления позицией, постановки стопа и оценки возможного продолжения это две разные реальности. Кроме того, два обычных растущих участка могут получить высокую корреляцию просто потому, что оба росли. Это ещё не значит, что внутри них действительно присутствовала одна и та же структура.
Есть и проблема времени. Корреляция сравнивает точки последовательно: первая точка с первой, вторая со второй и так далее. Если один и тот же паттерн в одном случае сформировался за 40 минут, а в другом за 70 минут, обычный Пирсон может оценить их сходство хуже, чем человек. Поэтому взять кандидатов только по коэффициенту корреляции и сразу открыть сделку — плохая идея. Так получится не торговый робот, а генератор красивых совпадений.
После первичного поиска похожих участков логично проверить, в каком состоянии рынок находился в каждом случае. Форму последних N баров можно дополнить набором контекстных характеристик:
S = (направление, волатильность, положение относительно уровня, расстояние до уровня, состояние рынка, время сессии,…)
Логика здесь следующая:
Если текущий рынок направлен вверх, исторический кандидат тоже должен был находиться в растущем рынке. Если текущий участок смотрит вниз, кандидат также должен формироваться в нисходящем движении. Если текущий паттерн находится выше выбранного контрольного уровня, кандидат тоже должен находиться выше него. Если ниже — ниже. Причём проверялся не только сам факт нахождения по одну сторону от уровня. Учитывалось и относительное расстояние. Участок, расположенный почти вплотную к уровню, и участок, находящийся далеко от него, формально могут иметь одинаковую форму, но рыночный контекст у них уже отличается. Также сравнивались внутренние состояния рынка, положение цены относительно дополнительных ориентиров, направление движения и волатильность. Идея здесь вполне здравая: недостаточно найти похожую картинку. Нужно найти похожую картинку, возникшую в похожих условиях. Разберём эти условия.
Если текущий рынок растущий, в первую очередь интересны исторические кандидаты, которые тоже формировались на растущем рынке. Сравнивать текущую восходящую структуру с внешне похожим участком внутри сильного падения обычно бессмысленно. Форма может совпадать, а причины её появления и распределение дальнейших результатов будут совершенно разными. То же самое относится к боковому рынку. Паттерн, сформированный внутри узкого диапазона, нельзя бездумно смешивать с таким же рисунком внутри направленного движения.
Допустим, в системе используется некоторый контрольный уровень. Если текущая цена находится выше него, а исторический кандидат сформировался ниже, это могут быть два разных рыночных состояния, даже при высокой корреляции между последовательностями цен. Также имеет значение расстояние до уровня. Условно можно представить два случая:
цена находится на 0,1% выше уровня;
цена находится на 2% выше уровня.
Технически оба участка находятся сверху. Но характер рынка, запас движения и вероятность возврата к уровню могут отличаться. Поэтому сравнивать желательно не абсолютное расстояние в пунктах, а нормализованное расстояние: например, относительно цены, волатильности или среднего диапазона бара.
Представим два участка:
в первом цена за рассматриваемое время прошла 20 пунктов;
во втором — 120 пунктов.
После нормализации графики могут выглядеть практически одинаково. Корреляция будет высокой. Но математическое ожидание движения, допустимый стоп, размер рыночного шума и вероятность случайного выбивания позиции отличаются в разы. Поэтому волатильность исторического кандидата должна быть сопоставима с текущей. Не обязательно совпадать до последнего знака. Это было бы бессмысленно. Но она должна была находиться в определённом диапазоне. Для такой проверки можно использовать:
стандартное отклонение изменений цены;
средний диапазон бара;
ATR;
реализованную волатильность;
амплитуду движения внутри окна;
отношение амплитуды к текущей цене.
Можно дополнительно разделять рынок на условные состояния:
устойчивый рост;
устойчивое падение;
боковое движение;
повышенная волатильность;
сжатие диапазона;
переходное состояние.
Тогда кандидат проходит дальше только в том случае, если его состояние совпадает с текущим или хотя бы считается совместимым. В упрощённом виде эта логика выглядит так:
<code class="python">current_window = get_current_window(length=N)<br />current_state = describe_market_state(current_window)<br /><br />candidates = []<br /><br />for historical_point in historical_points:<br /> historical_window = get_window(<br /> end=historical_point,<br /> length=N,<br /> )<br /><br /> correlation = pearson(<br /> current_window,<br /> historical_window,<br /> )<br /><br /> if correlation < minimum_correlation:<br /> continue<br /><br /> historical_state = describe_market_state(<br /> historical_window<br /> )<br /><br /> if not same_direction(<br /> current_state,<br /> historical_state,<br /> ):<br /> continue<br /><br /> if not same_side_of_reference_level(<br /> current_state,<br /> historical_state,<br /> ):<br /> continue<br /><br /> if not comparable_distance_to_level(<br /> current_state,<br /> historical_state,<br /> ):<br /> continue<br /><br /> if not comparable_volatility(<br /> current_state,<br /> historical_state,<br /> ):<br /> continue<br /><br /> if not compatible_market_state(<br /> current_state,<br /> historical_state,<br /> ):<br /> continue<br /><br /> candidates.append({<br /> "point": historical_point,<br /> "correlation": correlation,<br /> "state": historical_state,<br /> })</code>На бумаге всё выглядит логично. Чем больше совпадает параметров, тем более релевантным кажется кандидат.
Но у всех этих фильтров есть и обратная сторона: когда одновременно работает множество разных условий, становится сложно понять, что именно влияет на результат. Допустим, после включения всех фильтров робот стал прибыльнее. Почему?
Потому что действительно полезно сравнивать только одинаковую волатильность?
Потому что направление рынка было определено правильно?
Потому что фильтр положения относительно уровня случайно исключил несколько убыточных периодов?
Или потому что количество сделок сократилось настолько, что в тесте осталось несколько удачных случаев?
Ответить на это без отдельных экспериментов невозможно. Есть и вторая проблема: каждый дополнительный фильтр требует порога. Например:
насколько волатильность может отличаться;
какое расстояние до уровня считать сопоставимым;
какой наклон означает рост;
когда боковое движение превращается в тренд;
насколько состояния должны быть идентичными;
какой временной интервал использовать для их расчёта.
Чем больше таких порогов, тем больше возможностей подогнать систему под исторические данные. Получается довольно неприятная ситуация. Контекстные фильтры могут быть полезными, но именно они же могут превратить нормальную идею в переоптимизированный конструктор. Поэтому важно отдельно понять, что даёт сам поиск похожих участков по Пирсону без большого количества дополнительных ограничений. После этого каждый фильтр можно возвращать по одному и проверять его вклад:
Базовый вариант — только корреляция.
Добавляется направление рынка.
Отдельно проверяется фильтр волатильности.
Затем положение относительно уровня.
После этого расстояние до уровня.
Потом совместимость состояний рынка.
Результаты сравниваются на данных, которые не участвовали в настройке.
Так хотя бы можно понять, какой фильтр действительно улучшает результат, а какой просто красиво работает на прошлом периоде.
После первичного отбора остаётся некоторое количество кандидатов. Для каждого кандидата берётся последующий участок рынка определённой длиной.
Чтобы сравнивать движения между собой, будущие траектории приводятся к относительному виду. Например, точка окончания найденного паттерна принимается за ноль, а всё дальнейшее движение измеряется относительно неё. В результате алгоритм получает не одну линию будущего, а набор исторических сценариев. И это намного честнее, чем рисовать на графике единственную «прогнозную траекторию». Рынок не обязан повторить какой-то конкретный исторический участок. Даже очень похожие паттерны могут иметь разное продолжение. Поэтому нормальный результат алгоритма — не точная линия будущей цены, а распределение возможных исходов. Из этого распределения можно получить:
долю кандидатов, после которых цена выросла/снизилась;
медианное изменение цены;
среднее изменение;
диапазон наиболее типичных результатов;
максимальное благоприятное движение;
максимальное неблагоприятное движение;
вероятность достижения цели раньше стопа;
степень согласованности кандидатов между собой.
Особенно важен последний пункт. Допустим, алгоритм нашёл двадцать подходящих исторических участков. После десяти из них цена выросла, после других десяти упала примерно на такую же величину. Среднее продолжение может выглядеть как аккуратная горизонтальная линия. Только никакого прогноза здесь нет. Кандидаты просто не согласны между собой.
Другой случай: пятнадцать независимых кандидатов показали умеренный рост, четыре остались около нуля и только один завершился сильным падением. Это уже другое распределение. Здесь можно обсуждать вероятностное преимущество, возможный стоп и соотношение риска к ожидаемому результату.
Кандидатов также можно взвешивать. Участок с корреляцией 0,97 и почти идентичной волатильностью логично учитывать сильнее, чем участок, который едва прошёл минимальный порог. Но с весами легко переборщить. Сложная формула взвешивания часто создаёт красивый результат на истории и разваливается на новых данных. Поэтому чем проще и понятнее вес, тем лучше. В упрощённом виде полный алгоритм может выглядеть так:
<code class="python">current = get_current_window()<br />current_state = describe_market_state(current)<br /><br />matches = []<br /><br />for point in history_before_now:<br /> candidate = get_window(point)<br /><br /> similarity = pearson(<br /> current,<br /> candidate,<br /> )<br /><br /> if similarity < threshold:<br /> continue<br /><br /> candidate_state = describe_market_state(<br /> candidate<br /> )<br /><br /> if state_filters_enabled:<br /> if not states_are_compatible(<br /> current_state,<br /> candidate_state,<br /> ):<br /> continue<br /><br /> if overlaps_existing_candidate(<br /> point,<br /> matches,<br /> ):<br /> continue<br /><br /> future = get_future_path(<br /> after=point,<br /> horizon=forecast_horizon,<br /> )<br /><br /> matches.append({<br /> "similarity": similarity,<br /> "future": normalize(future),<br /> "state": candidate_state,<br /> })<br /><br />forecast = aggregate_future_paths(matches)<br /><br />if forecast.edge_after_costs > required_edge:<br /> send_signal(forecast)</code>На словах всё просто. Основные проблемы начинаются внутри каждой функции.
Допустим, рынок несколько часов двигался практически без изменения характера. При использовании скользящего окна алгоритм может найти десятки соседних кандидатов:
первый заканчивается в 14:30;
второй — в 14:31;
третий — в 14:32;
четвёртый — в 14:33;
и так далее.
Формально это разные участки. Фактически они относятся к одному и тому же рыночному эпизоду и почти полностью перекрывают друг друга. Если считать каждый такой участок независимым кандидатом, получится ложная уверенность. Алгоритм скажет: «У меня есть сто исторических примеров». На самом деле у него может быть три события, каждое из которых породило несколько десятков перекрывающихся окон. Поэтому кандидатов необходимо разрежать или объединять. Между ними должен существовать минимальный временной интервал. Другой вариант — группировать соседние совпадения в единый исторический эпизод и выбирать внутри группы лучшего кандидата. Например:
<code class="python">def remove_overlapping_candidates(<br /> candidates,<br /> minimum_distance,<br />):<br /> candidates = sorted(<br /> candidates,<br /> key=lambda item: item["similarity"],<br /> reverse=True,<br /> )<br /><br /> selected = []<br /><br /> for candidate in candidates:<br /> if all(<br /> abs(<br /> candidate["point"]<br /> - selected_item["point"]<br /> ) >= minimum_distance<br /> for selected_item in selected<br /> ):<br /> selected.append(candidate)<br /><br /> return selected</code>
Это очень важный момент. Количество найденных кандидатов само по себе почти ничего не говорит о качестве прогноза. Сначала нужно понять:
сколько среди них независимых рыночных эпизодов;
насколько сильно они перекрываются;
не относятся ли они к одному и тому же дню;
не возникли ли они в одной продолжительной трендовой фазе;
совпадают ли их дальнейшие движения.
Иногда текущий рынок похож на большое количество исторических участков. Иногда даже при относительно мягком пороге алгоритм почти ничего не находит. Это может означать, что текущая структура:
очень типична для выбранного инструмента;
слишком проста и в основном состоит из обычного тренда;
относится к редкому состоянию рынка;
возникла при необычной волатильности;
плохо описывается выбранной длиной окна.
Но большое количество кандидатов не означает автоматически высокую надёжность прогноза. Если все кандидаты являются копиями одного исторического движения, пользы от такой статистики немного.
Выбор длины анализируемого участка — один из самых чувствительных параметров всей системы. Короткое окно быстро реагирует на изменения, но часто сравнивает обычный рыночный шум. Кандидатов становится много, а их полезность снижается. Длинное окно описывает более крупную структуру и обычно даёт более специфичные совпадения. Но кандидатов становится меньше, а старые участки могут относиться к рынку, который уже изменился. Нет одной универсально правильной длины. Она зависит от:
инструмента;
таймфрейма;
прогнозного горизонта;
характера торговли;
среднего времени удержания позиции;
структуры ликвидности;
того, какую форму вообще предполагается находить.
При этом окно поиска и горизонт прогноза — разные параметры. То, что анализируются последние полтора часа, не означает, что прогноз тоже должен строиться на полтора часа вперёд. Можно анализировать 90 минут истории и смотреть продолжение на 15 минут. Можно взять 60 минут и изучать следующие два часа. Эти величины нужно проверять отдельно. Есть и более сложный вариант — использовать несколько масштабов одновременно. Например:
короткое окно описывает локальный паттерн;
среднее окно показывает текущий импульс;
длинное окно задаёт общий рыночный контекст.
Технически это выглядит интересно. Но здесь снова действует простое правило: сложность должна появляться только тогда, когда есть измеримая проблема, которую она решает. Добавлять ещё три окна просто потому, что система с тремя окнами выглядит умнее, бессмысленно.
У обычной корреляции есть ещё одно ограничение. Она предполагает, что похожие события развиваются примерно с одинаковой скоростью. Предположим, текущий паттерн сформировался за 60 баров. Исторически почти такая же структура возникла за 80 баров. Человек может растянуть график по горизонтали и увидеть практически полное совпадение. Пирсон при прямом сравнении точка к точке может оценить такие участки значительно хуже. Для решения этой проблемы существует Dynamic Time Warping — динамическое выравнивание временных рядов.
Этот метод позволяет условно растягивать и сжимать отдельные участки последовательности, подбирая более подходящее соответствие между точками. Но за гибкость приходится платить. Dynamic Time Warping:
работает медленнее;
сложнее в настройке;
может находить сходство там, где его уже слишком много;
требует ограничений на допустимую деформацию времени.
Если разрешить алгоритму слишком свободно растягивать последовательности, почти любой график можно сделать похожим на любой другой. Поэтому переход от Пирсона к более сложному методу не является автоматическим улучшением. Сначала нужно доказать, что проблема разной скорости паттернов действительно существенна и влияет на торговый результат.
Когда рассказываешь о поиске похожих участков, почти неизбежно возникает вопрос: почему не использовать нейросеть? Можно взять автоэнкодер, получить скрытое представление каждого участка графика, сложить эмбеддинги в векторную базу и искать ближайших соседей.
Можно обучить Transformer, свёрточную сеть или другую модель временных рядов. Технически это возможно. Но сложная модель не является гарантом правильного решения задачи.
Если в данных есть утечка будущего, нейросеть найдёт её быстрее корреляции.
Если исторические кандидаты неправильно разделены, модель запомнит перекрывающиеся участки.
Если не учтены комиссии и проскальзывание, она оптимизирует преимущество, которого в реальной торговле не существует.
Если в качестве цели выбрана неправильная метрика, модель будет великолепно решать неправильную задачу.
Более того, сложную модель значительно тяжелее отлаживать. Когда корреляция выдала кандидата, можно открыть оба участка и глазами увидеть, почему они похожи. Когда нейросеть решила, что два участка находятся рядом в скрытом пространстве размерностью 128, объяснить результат уже сложнее. Это не означает, что нейросети бесполезны. У них есть смысл, когда простой метод упирается в конкретное ограничение. Например:
корреляция плохо справляется с нелинейными искажениями;
форма паттерна зависит от нескольких признаков одновременно;
важна не только цена, но и объём, волатильность, скорость и микроструктура;
последовательный перебор истории становится слишком дорогим;
простой алгоритм перестал давать улучшения, а его ограничения уже понятны.
Тогда можно переходить к обучаемым представлениям. Но начинать стоит с модели, которую можно проверить. Иначе вместо торгового алгоритма получается сложная система, которая уверенно выдаёт непонятный результат.
Нейросеть не превращает слабую идею в сильную. Она просто делает слабую идею менее прозрачной.
По сути, описанный подход близок к методу ближайших соседей для временных рядов. Я не пытаюсь доказать, что на графике сформировалась фигура с определённым названием. Логика звучит иначе:
Текущая траектория цены похожа на набор исторических траекторий. Эти траектории могли возникать в сопоставимом состоянии рынка. После них наблюдалось определённое распределение результатов.
Это и есть алгоритмическое определение паттерна.
Не картинка из учебника.
Не субъективное впечатление.
Не две линии, нарисованные на графике после завершения движения.
дурачок?
кто это будет читать? тем боле, что отработка 50/50![]()
Судя по вашим комментариям, вы способны обсуждать только готовые сигналы и отдельные сделки. Как только речь заходит о более сложных вещах — алгоритмах, статистике и методиках проверки, вы начинаете демонстрировать удивительное непонимание. Разжёвывать статью до доступного вам уровня я не собираюсь. Поищите контент попроще — вероятно, на другой площадке. Здесь вам явно сложно.
А простым путем пойти не пробовали?
), 100 лет назад пошагово расписал, как искать мат. пропорции! ![]()
Для МТ4 более 15 лет в сети бесплатные индикаторы валяются, для поиска всех математических пропорций. Кто захотел, уже освоил и собрал свою базу моделей.
Есть координата Х, есть Y. Прошлое движение составило столько то пунктов и столько то баров.
Текущее движение скорректировалось к прошлому на столько то баров, столько то пунктов.
А если еще сможете эти пункты и бары привести к пропорциям кое какого эталона, получите одни и те же значения.
У. Д. Ганна читайте, он для таких как вы (хотя нет, такие как вы его не поймут
Когда читаю очередной такой перл, ржу не могу.