Безопасность (часть 2): как спасти депо от робота

23.06.2022 (c) Vytas Ramanchauskas, www.SpyTlt.com

Лонгрид больше про программирование и философию, чем про трейдинг.

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

Философское вступление

Вначале немного философии о том, откуда берутся ошибки в программах (а торговый робот – это всего лишь программа) и чем они отличаются от ошибок человеческих. Отличие принципиальное: человеческие ошибки идут в основном от невнимательности, забывчивости, раздолбайства, эмоциональных решений, иногда от саботажа и прямого вредительства; ошибки программ идут от пропущенной или неправильно закодированной логики, a отсутствиe понимания у программы приводит к тому, что робот может совершать заведомо вредоносные действия (например, зациклившись, отправлять на биржу безумное количество ордеров) и не понимать, что что-то идет не так.

Начну с анекдота: медсестра будит спящего больного: "больной, проснитесь, вам надо выпить снотворное". В этом анекдоте медсестра действует как компьютер, который четко и неукоснительно выполняет программу, не думая о последствиях и разумности действия. Врач, который сделал предписание, не предусмотрев такой вариант, подобен программисту, который что-то упустил из виду, а пациент – это пользователь программы, на которого сваливаются все последствия.

Сегодня в программировании вообще и в искусственном интеллекте идет грандиозный прорыв, рискующий перевернуть нашу жизнь, однако есть одна проблема, которая радикально мешает быть компьютерам умными в человеческом смысле – проблема понимания. Еще недавно была и вторая проблема – полная неспособность распознавать образы, но сейчас ее в существенной степени удалось решить, пусть частично и для отдельных случаев.

Как подступиться к пониманию в общем-то не очень понятно. Собственно, понимание это способность отвечать на простые (и очевидные для человека) вопросы наподобие: "Джон работает в IBM, Стив работает вместе с Джоном, где работает Стив?". Абсолютно банальный вопрос, который ставит в тупик компьютерные системы (если только их создатели заранее не предусмотрели вопрос такого типа). Человек проявляет понимание гораздо чаще, чем можно было бы подумать. Посмотрите, например, на такую фразу: "женщина смотрела на бриллиант сквозь стекло витрины и страстно хотела его". Все однозначно и понятно? На самом деле нет. Вернее, понятно человеку, а компьютерный переводчик вполне может сесть в лужу, ведь по нормам русского языка, в этом предложении "его" может относиться как к бриллианту, так и к стеклу витрины. Исключительно из самого предложения получить информацию, что именно хотела женщина невозможно, нужно некое внешнее знание, что женщины – это такие существа, которые бриллианты хотят, а стекла витрин – как-то не очень. Кстати, стекло витрины запросто может быть дороже бриллианта.

Поначалу, казалось, что можно мобилизовать армию студентов и энтузиастов, пусть они формализуют все знания человека о внешнем мире, включая "что хочет женщина". Однако очень быстро выяснилось, что во-первых этих правил слишком много и их очень трудно формально точно описать, во-вторых, когда их становится много, они начинают внезапно конфликтовать друг с другом и, что самое ужасное, оказывается, что не все сводится к предустановленным правилам. Человек может генерировать эти правила "на ходу". Например, отличный вопрос для Тьюринг теста: "левая нога мистера Смита находится в Лондоне, где находится его правая нога?". Для человека, даже для ребенка, вопрос очевидный, хотя какой-нибудь занудный программист (о них и о причинах их занудства ниже), обязательно отметит, что надо рассмотреть случай одноногого инвалида Смита, ноги которого могут пребывать в разных местах.

В этом вопросе наиболее примечателен следующий аспект: вам, разумеется, очевиден естественный ответ на этот вопрос. Но при этом, скорее всего, вас ни родители в детстве, ни учителя в школе, ни профессора в ВУЗе не учили в явном виде правилу, что правая нога всегда рядом с левой. Это каким-то образом ясно само собой. Пока вы об этом не думали, этого знания не было в вашей голове, но при необходимости вы легко можете сформулировать соответствующее правило и даже обобщить его, что, зная положение части целого вы знаете и положение остальных частей и всего целого (занудный программист снова скажет, что надо учитывать размеры этого целого, которые ограничивают точность оценки координат).

Итак, человек способен столкнувшись с новой, абсолютно незнакомой ему ситуацией, разобраться в ней. Разобравшись, он может формализовать, перевести в логику, а значит и в компьютерную программу это знание. Но логика (и компьютерная программа) вторичны по отношению к человеческому знанию, к пониманию. Логика – продукт человеческого мышления, причем как именно она порождается в общем случае совершенно не понятно.

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

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

У человека слишком много всего подразумеваемого. Если один человек просит другого пойти сделать чай, ему не надо делать оговорки наподобие "если дверь на кухню закрыта – открой ее", "если на кухне темно – включи свет" – человек и так это все понимает и способен решить множество мелких попутных проблем. В крайнем случае, столкнувшись с некоторой неразрешимой проблемой человек чаще всего понимает, что случилась проблема, может попросить помощи или хотя бы перестать усугублять ситуацию дальнейшими неверными действиями.

В компьютерную программу должно все быть заложено изначально. Если вы образно говоря по аналогии с предыдущим случаем не предусмотрели что дверь при необходимости надо открыть, то робот будет долбиться в закрытую дверь пока его не выключат.

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

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

  • Робот в конце дня что-то там считает с учетом текущей цены и цен за предыдущие дни, на основании отчетов открывает/закрывает позицию или же ничего не делает. Если исторические данные забыли или не смогли обновить, робот просто не будет иметь информации за вчерашний день или несколько последних дней и считая последними данными те что есть, принять неверное решение. Проверка актуальности исторических данных решает проблему.
  • Нулевые цены. Бывает нередко, даже у хороших и достойных провайдеров данных, возвращается нулевая цена (кто-то где-то некорректно обработал ошибку).
  • Нереальные цены – если сегодня цена вдвое ниже/выше чем вчера, это может свести с ума любой алгоритм и привести к необоснованным действиям. А такое регулярно случается в случае сплита или обратного сплита акций.
  • Зависший стрим – допустим, робот получает стриминговые данные о ценах и на основе них открывает/закрывает позиции. Если вдруг данные перестают идти (из-за проблем с интернетом, проблем с провайдером данных или по какой еще причине) традиционно написанный робот может "биться головой в закрытую дверь", тупо ожидая новых данных, а открытая позиция может тем временем лететь в ад.

Все эти случаи можно диагностировать в процессе, если программист подумал о них заранее: валидность исторических данных, что в них нет пропусков и присутствуют данные за предыдущий торговый день, можно проверить на старте, нулевые цены тоже легко детектировать, нереальное изменение цены – тут возможны сложности, потому как у отдельной акции цена может измениться радикально сама по себе. Однако если изменение происходит ночным гэпом да еще и примерно в кратное число раз – то это скорее всего неучтенный сплит, а не настоящее изменение цены. Зависший стрим тоже можно легко детектировать. Проблема в том, что программисты обычно пишут программы так, что ничего этого не делают. А делать надо. И что самое ужасное, этот список по определению не полный. Даже если устроить мозговой штурм и "подстелить соломку" буквально повсюду, в реальности может случиться совершенно новая, неожиданная и непредусмотренная проблема.

Практические советы

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

Начинать лучше не с полностью автоматических роботов, а с торговых советников, которые не открывают позиции сами, а всего лишь говорят, что следует делать, возможно даже подготавливая, но не отсылая, ордера. У IB в TWS есть прекрасная возможность подготовить ордера, они появятся на экране TWS, но чтобы они стали активными/исполнились, надо нажать кнопку "Transmit" (есть возможность одновременной отсылки всех подготовленных ордеров одним нажатием кнопки). У трейдера есть возможность оценить по-человечески совершаемое действие. И хотя этот подход может породить новые, уже человеческие, ошибки, вы можете быть спокойны, что "сошедший с ума робот" не начнет в цикле делать многочисленные безумные сделки.

Кстати, TWS блокирует большие пачки ордеров, отосланные за короткий интервал времени. Видимо преценденты были.

Обработка исключений

Стандартом де факто обработки ошибок в современном программировании стал механизм исключений: при возникновении некоторой ошибочной ситуации типа попытка прочитать несуществующий файл происходит генерация исключения, которое обрабатывается где-то выше в дереве вызовов. В традиционной программе возникает сообщение об ошибке, которое показывается пользователю, иногда спрашивая его что делать дальше. Считается что исключения ни в коем случае нельзя игнорировать, необработанное исключение – повод аварийного завершения программы. Это абсолютно разумно, если это обычная программа, за компьютером сидит пользователь, который реагирует на сообщение об ошибке.

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

В качестве примера, к чему приводит традиционный подход к реагированию на необработанные исключения, можно привести пример дорогостоящей катастрофы ракеты Ариан V ). Вкратце, в управляющем коде остался модуль от предыдущей версии ракеты (ленивые программисты любят повторно переиспользовать код, и это нормально). В реальном полете реализовалась ситуация, которая не могла иметь место в предыдущей версии ракеты, что через цепь нелепостей в конечном счете привело к необработанному исключению и аварийному останову компьютера. Резервный компьютер мог бы спасти ситуацию, если б имела место поломка основного компьютера, но не в этом случае. "Брат-близнец" в точности повторил поведение основного компьютера, ракета стала неуправляемой и автономная система самоуничтожения устроила эффектный, но очень дорогой фейерверк.

Аварийный режим

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

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

Соответственно первый и очень важный момент: робот должен иметь возможность оперативно позвать хозяина, подать сигнал тревоги. Если предполагается, что робот домашний/офисный и хозяин находится за компьютером или поблизости, достаточно будет всплывающие окна и звукового сигнала тревоги. Кстати, не помешает уровень громкости проверить – ситуация, когда робот вопил изо всех сил, а звук был просто выключен была в моей жизни. Теперь робот принудительно включает звук перед открытием рынка. Если же робот запущен на удаленном сервере и/или предполагается что хозяин может оставить компьютер без присмотра, то не обойтись без друих каналов связи, робот может, например, посылать сообщение в мессенджере или отправлять СМС.

Контроль за результатами действия

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

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

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

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

Однако программа обычно никак не контролирует что же получилось в результате того, что она сделала. Нет исключения – нет проблем. Предполагается, что если что-то не так, то будет выброшено исключение, и вот его надо как-то обрабатывать, обычно это как-то сводится к показу пользователю сообщения об ошибке.

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

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

Логгирование

Надо обязательно вести логи. Очень подробные, возможно избыточно подробные логи всего. Если случится нечто непонятное – лог поможет понять что именно случилось в программе. Нынешние компы быстры, диски огромны, вопрос быстродействия (если только это не хардкорный HFT) не стоит, ведение подробного лога совершенно некритично по ресурсам. Имеет смысл хранить логи достаточно долго, возможно очень долго – иногда возникает потребность вернуться к достаточно давнему прошлому. Логгирование можно считать сделанным правильно, если по логам можно восстановить все что происходило с роботом, почему робот принял или не принял то или иное решение. Логгируя исключения полезно приводить включать всю необходимую информацию. Например, в ранних версиях .NET исключение при попытке прочесть несуществующий файл, не включало в себя собственно имя файла. Отсутствие такой информации может сильно затруднить понимание произошедшего.

Единая точка входа для выставления ордеров и "цербер" на ней

Очень важный принцип: следует реализовать единственную точку входа, через которую ставятся абсолютно все ордера. Без исключений. На этой точке входа следует завести "Цербера", который проверяет все ордера. Критерии проверки могут меняться в зависимости от используемых систем, но самые очевидные и универсальные проверки следующие:

  • Размер ордера. Я использую два порога: при превышении первого, выдается предупреждение с возможностью разрешить исполнение, причем кнопка разрешения становиться доступной через пару секунд, чтобы не нажать ее машинально. При превышении второго порога ордер блокируется без возможности его разрешить. Пороги могут быть разными для разных систем и/или инструментов.
  • Количество ордеров. Один из самых страшных сценариев – робот зациклился и постоянно открывает/закрывает позиции. Имеет смысл установить ограничение на количество ордеров за день и за, скажем, минуту.
  • Слишком большая прибыль/убыток за день – роботу имеет смысл позвать хозяина, чтобы он обратил внимание на ситуацию и принял решение о дальнейших действиях.

Детектор аномалий

Имеет смысл Простейшие критерии аномалий: слишком большое количество сделок, слишком сильное падение баланса счета – это хорошие триггеры аварийного режима, когда робот зовет хозяина, блокирует открытие новых сделок, сохраняя логику закрытия уже открытых позиций. Впрочем, если сделок аномально много, а баланс растет, достаточно только на всякий случай позвать хозяина. Так же имеет смысл "звать на помощь" в случае аномальных движений на рынке: слишком сильных изменений индексов и цен отслеживаемых активов, всплески объёмов, волатильности. Помимо идеи привлечь внимание трейдера к аномальной ситуации на рынке у такого рода мониторинга есть еще одна цель: сильное изменение цены может быть вызвано не только ситуацией на рынке, но и ошибкой в данных, что чрезвычайно критично.

Недостоверные данные

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

  • нулевое значение цены. Понятное дело, что нулевая цена означает некую ошибку, такого быть не может. Однако, это отнюдь не редкая ситуация.
  • слишком большое изменение цены. Все, конечно, может быть, но слишком сильное изменение цены, особенно у индекса, может свидетельствовать о каких-либо проблемах с данными.
  • отдельным пунктом: изменение цены примерно в кратное количество раз от вчерашнего закрытия – это, скорее всего, сплит или обратный сплит, о котором робот (а возможно и сам трейдер) не в курсе. И может наломать дров.
  • неверные исторические данные, чаще всего необновлённые данные, пропуск данных за прошлую торговую сессию.
Сделать базовые проверки на заведомо неверные значение совсем несложно, а это может уберечь от очень неприятных последствий.

Мониторинг состояния системы

Полезно отслеживать загруженность CPU, памяти, места на диске. Очень многие программы, включая TWS, начинают себя вести феерически неадекватно, если на диске кончилось место. Чрезмерная загруженность CPU, вызванная или самим роботом или некой внешней программой тоже может привести к проблемам. Нехватка оперативной памяти тоже может вызвать проблемы.

Перепросмотр кода (code review)

Известный прием, когда код пересматривает другой программист. Глядя на код свежим взглядом часто получается увидеть пропущенные ошибки, качество и надежность от этого сильно повышаются. Однако, второй программист – удовольствие недешевое. Есть "вариант для бедных". Вы сами можете сделать code review, однако здесь есть одна хитрость: надо сделать достаточно долгую паузу. Дело в том, что, погрузившись в задачу у вас в голове формируется некий подход к ее решению, возможно содержащий ошибку. Если вы завтра снова будете смотреть этот же код, в голове всплывет тот же подход и вы скорее всего пропустите ошибку снова. Нужно выждать достаточно долгую паузу, хотя бы неделю, переключиться за это время на другую задачу. А потом заново посмотреть на свой код. Тем не менее, привлечение второго программиста работает лучше.

Watch dog

Хорошо известных прием, позволяющий оперативно обнаруживать глобальные аварийными ситуации, когда несмотря на все старания могут возникать. Идея проста: робот периодически несколько раз в минуту записывает в файл текущее время как отметку "я жив, все ок". В случае штатного завершения работы в файл записывается отметка и об этом. Watch dog – программа, которая следит за содержимым этого файла и поднимает тревогу, если робот завис. Watch dog не спасет в случае зависания/фатального сбоя всего компьютера. Может иметь смысл запускать Watch dog на другом, удаленном компьютере.

Web интерфейс

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

Резервный интернет канал

Довольно вероятная неприятность, которая может случиться – это проблемы с подключением к интернету. Если робот работает на выделенном сервере вероятность проблем такого рода резко снижается, а вот для "домашних" и офисных роботов она более чем актуальна. Кроме очевидного совета не экономить на провайдере и использовать максимально надежного, можно посоветовать использовать роутер с дополнительным модемом, который умеет переключатья на него переключаться при проблемах связи. Еще более радикальным решением может быть одновременное подключение к двум провайдерам, хорошие роутеры имеют два порта WAN и умеют автоматически переключаться с провайдера на провайдера при необходимости. Сам компьютер, разумеется, должен быть надежным и оборудованным хорошим UPS.

Альтернативный вариант – аренда выделенного сервера и запуск робота на нем.

Вирусы и зловредные программы

Отдельная и большая тема – безопасность компьютера в смысле вирусов и зловредных программ. Реалии сегодняшнего дня таковы, что помимо вирусов стоит опасаться еще и антивирусов. Тема безопасности – большая, сложная и в некотором смысле бесконечная, поскольку всегда есть возможность еще что-то улучшить, усилить, плюс надо реагировать на новые угрозы. Представляется разумным выделить отдельный ("чистый") компьютер (физический или виртуальную машину) для работы с биржей, банками и других критически важных дел. Там не должно быть "левых" программ, на нем не следует открывать никакие сайты, кроме сайтов брокеров и банков. Для серфинга и прочих дел лучше использовать другой ("грязный") компьютер.

Сравнение теории и практики

Обязательно надо сравнивать практические результаты робота с тем, что выдает бэктестер, особенно при запуске новой системы или внесении существенных изменений. Различия всегда проявляются и надо обязательно анализировать их причину: некоторые отличия неустранимы, например, бэктестер, опираясь на выкачанные позже исторические котировки мог в какой-то момент открыть сделку, а робот, получая стримингорвые данные в это же время имел чуть-чуть другую информацию о цене, а расхождение в цене даже в один цент может привести, что в одном случае сделка будет открыта, а в другом – нет. Но могут вылезти и ошибки реализации. Или могут обнаружиться неприятные расхождения реального мира и программистских абстракций. К примеру, попытки покупать на открытии или вскоре после открытия часто приносят удивительные открытия неискушенному трейдеру, особенно если они совершаются с не очень ликвидной бумагой, да еще и после отчета.

Начинать с малого

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

Практический опыт

Все вышеизложенное реализовано на практике. "Улов" пока такой:

  • несколько раз робот блокировал неверно введенные ручные ордера, в том числе серьезные ошибки из-за банальных опечаток.
  • пару раз поднимал тревогу и блокировал торговлю из-за сплитов, которые я не учел и которые могли бы привести к открытию позиций, которые не следовало открывать.
  • несколько раз заставлял при запуске до открытия обновить исторические данные, которые хотя и обновляются автоматически иногда остаются необновленными.
  • один раз робот вовремя предупредил что заканчивается место на системном диске.
  • самый фееричный случай: в давно используемую торговую систему было решено внести небольшие изменения. И это довольно неплохая ловушка внимательности – если изначально знаешь, что сделать надо пару мелочей, то невольно расслабляешься и делаешь их не так внимательно, как при каких-то серьезных изменениях. В результате из-за невнимательности была внесена очень серьезная ошибка, приводящая к тому, что робот пытался открыть огромную позицию, сильно бо́льшую чем следовало. "Цербер" заблокировал эту попытку – один этот случай, я полагаю, окупил все затраты на безопасность и "лишний" код для проверок.


Вопросы и обсуждение - в телеграм-группе SpyTlt.