Hebbian-фактор был мёртв 8 версий: как я нашёл баг в своём индикаторе
Восемь версий. Ни одного предупреждения компилятора. Индикатор исправно рисовал на графике, советник открывал сделки - а один из факторов внутри всё это время не работал. Тихо. Я даже гордился - такой чистый код. Фактор, судя по всему, тоже был доволен.
Содержание
Это история про самый коварный класс багов в разработке торгового индикатора: тот, что не роняет программу, не пишет «ошибку» в лог и не ломает компиляцию. Он просто молча отдаёт неправильное число. А ты об этом не подозреваешь - потому что всё остальное «работает».
Рассказываю, как мёртвый фактор прожил в AlgoSynergy GOLD от версии 1.01 до 1.19, почему я не видел этого восемь релизов и как один диагностический Print() вскрыл проблему за минуту.
Что такое Hebbian-акселератор и зачем он нужен
В индикаторе AlgoSynergy GOLD решение по каждому таймфрейму собирается из нескольких компонентов: тренд (EMA), VWAP, триггер-линия и так называемый Hebbian-акселератор. Идея последнего простая: измерять силу отклонения цены от своей средней. Не «выше или ниже» (это и так дают EMA), а насколько далеко и резко цена ушла от равновесия.
Чем сильнее отклонение - тем ближе значение фактора к краям диапазона 0…1. Около 0.5 - цена у средней, скучно. Выше 0.70 - сильный бычий импульс, ниже 0.30 - сильный медвежий. Эти пороги и поднимали сигнал из «обычного» в «сильный».
Так было задумано. На практике фактор не выходил за 0.500-0.505 никогда.
Как баг выглядел в коде
Нормировка отклонения была написана так (упрощённо):
diff = (close - mean) / mean;
Логика на первый взгляд разумная: отклонение в долях от среднего. На акции по $50 или паре EURUSD это дало бы вменяемые числа.
Но GOLD торгует золото. Цена - около 3300-4000. Типичное отклонение закрытия от скользящей средней на M5 - несколько пунктов, ну десятки. Делим, скажем, 8 пунктов на 3700:
8 / 3700 ≈ 0.00216 // две десятых процента
После прогона через сигмоиду такое микроскопическое отклонение давало на выходе 0.500…0.505. Всегда. Пороги 0.70 и 0.30 были недостижимы в принципе - как требовать прыжок на три метра от того, кто привязан к полу. «Сильный сигнал» от Hebbian не возникал ни разу за восемь версий.
Почему я не замечал это 8 версий
Вот что делает такие баги опасными: ничто не сигналит о поломке.
- Компилятор молчал - синтаксически код идеален.
- Индикатор рисовал панель, стрелки, уровни - всё на месте.
- Советник открывал сделки - решение собирается из нескольких компонентов, и трёх оставшихся (EMA, VWAP, триггер) хватало, чтобы система жила.
- Бэктесты шли, сделки закрывались в плюс и в минус - статистика выглядела «нормальной».
Фактор не отсутствовал - он присутствовал и отдавал 0.5. А 0.5 - это «нейтрально», валидное значение. Никакого NaN, никакого нуля, который бросился бы в глаза. Просто тихая константа, маскирующаяся под работу. Я смотрел на график, видел, что «всё считается», и шёл дальше - восемь раз подряд.
Это ровно тот случай, когда доверять ощущениям нельзя. «Выглядит рабочим» и «работает» - разные вещи, и разделяет их только измерение.
Как нашёл: один Print() вскрыл всё
Я не угадал баг. Я его измерил.
Готовя версию 1.20, я добавил диагностический отчёт - прогнать фактор по последним 2000 барам каждого таймфрейма и вывести распределение: минимум, перцентили, максимум. Один Print() с десятком чисел.
p10=0.000 p50=0.500 p90=0.505 max=0.505
И всё стало очевидно мгновенно. Восемь версий, сотни часов - и один Print() объяснил мне, что я делал вид, что считаю, уже очень давно. Девяностый перцентиль - 0.505. То есть 90% значений лежат между 0.5 и 0.505. Фактор не просто слабый - он раздавлен в точку. Порог 0.70 от него не видел никто и никогда.
Минута на чтение отчёта - и восьмиверсионная загадка перестала быть загадкой. Данные сказали то, что глаз не ловил.
Как чинил: две калибровки и решение для H4
Диагноз ясен: нормировать отклонение нужно не на цену, а на волатильность. Сколько пунктов - это «много», зависит не от того, $50 актив или $4000, а от того, насколько он обычно ходит. Естественная мера этого - ATR.
diff = (close - mean) / (atr * K);
Теперь 8 пунктов сравниваются не с 3700, а с текущим ATR - и сразу превращаются в осмысленную величину. Осталось подобрать K.
- Прогон 1, K = 1.0 → p90 = 1.000. Перелёт: я починил мёртвый фактор и немедленно создал противоположную проблему. Теперь фактор бьёт в потолок, «сильным» становится почти всё - так же бесполезно, как раньше, только наоборот.
- Прогон 2, K = 3.0 → p90 = 0.693. Попали: верхний край распределения сел ровно у порога 0.70, «сильный сигнал» снова стал редким и значимым.
Дальше всплыла деталь. На H4 распределение оказалось смещено вверх - за период золото росло, и старший таймфрейм был устойчиво бычьим. С общим K=3 H4 перегревался. Поэтому в финале v1.20 - пер-TF коэффициенты: K=3.0 для M3-H1 и более мягкий K=5.0 для H4. Каждый таймфрейм нормируется под свою динамику.
Вывод: тихие баги опаснее громких
Громкий баг - это подарок. Он падает, орёт в лог, ломает сборку - ты его чинишь в тот же день.
Тихий баг живёт годами. Индикатор может рисовать и при этом молчать о том, что одна из деталей внутри сломана. Он не обязан вам признаваться. Единственная защита - не верить, что «раз компилируется и рисует, значит работает», а периодически измерять каждый компонент отдельно: распределение, перцентили, частоту срабатывания.
Один диагностический отчёт нашёл то, что восемь релизов и куча визуальных проверок пропустили. С тех пор у меня правило: новый фактор не считается рабочим, пока я не увидел его распределение на реальных данных. Не «выглядит правильно» - а доказано числами.
Именно так я и строю инструменты AlgoSynergy: не на ощущениях, что «вроде работает», а на данных - этот принцип измеримости лежит в основе всей методологии. А ещё я теперь гораздо осторожнее говорю «у меня это работает». Потому что знаю: иногда это значит просто «у меня это рисует».
Кстати, исправленный фактор теперь работает в проде - увидеть текущие показания консенсуса по золоту и биткойну можно на барометре в реальном времени.
Частые вопросы
Что такое Hebbian-акселератор в индикаторе GOLD?
Это компонент, который измеряет силу отклонения цены от своей средней - не «выше/ниже», а насколько далеко и резко цена ушла от равновесия. После фикса отклонение нормируется на волатильность (ATR), а не на абсолютную цену.
Почему «тихий» баг не замечали восемь версий?
Он ничего не ломал: компилятор молчал, индикатор рисовал, советник торговал, а фактор отдавал валидное «нейтральное» 0.5. Ни ошибки, ни NaN. Такую поломку не видит глаз - её ловит только измерение распределения значений.
Как защититься от подобных тихих багов?
Не доверять принципу «компилируется и рисует - значит работает». Каждый компонент периодически измеряют отдельно: распределение, перцентили, частоту срабатывания на реальных данных. Фактор не считается рабочим, пока его поведение не доказано числами.
Хотите посмотреть, как устроены индикаторы AlgoSynergy для XAUUSD и что у них «под капотом», - пишите в Telegram @AlgoSynergy. Покажу, расскажу, отвечу лично.
Материал носит образовательный характер и не является инвестиционной рекомендацией.