В двух постах здесь market-analyzer служил лишь поводом, и оба раза я говорил, что само приложение тут не главное. Этот пост — про приложение.

Десктопный просмотрщик: дневные свечи MSFT, нарисованная агентом 20-периодная EMA, подписанный уровень сопротивления R1 и подсвеченные свечные паттерны — молот, поглощение и звезда

Это рабочее место для анализа рынка, которым вы управляете, разговаривая с агентом. Локальный сайдкар на Python превращает рыночные данные в индикаторы, паттерны, бэктесты, прогнозы и ончейн-чтения и отдаёт этот единственный набор возможностей дважды на одном loopback-порту: как 59 MCP-инструментов на /mcp для агента и как 29 HTTP-маршрутов плюс поток событий для окна. Управляющая поверхность — Claude Code. Electron-просмотрщик рисует то, что попросил агент, и больше не делает ничего.

Всё считается на вашей машине. Ордеров приложение не выставляет.

Разворот

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

Более раннее решение описывало MCP как «второй протокол сайдкара рядом с HTTP для просмотрщика». Основным клиентом был просмотрщик, а агент — дополнительной поверхностью, которая могла запрашивать данные и писать аннотации на график, которым управлял пользователь. Модель взаимодействия была общепринятой — той же, что почти у всякой AI-функции, выпущенной за последние два года: вы кликаете в приложении, а агент помогает сбоку.

Этому решению был ровно один день, когда его переформулировали в противоположное:

  • Пользователь пишет промпты агенту.
  • Агент ведёт анализ, бэктесты, скрины и визуализации графиков, вызывая MCP-инструменты.
  • Electron-приложение существует, чтобы показывать то, что отрисовал агент.
  • Пользователь не вводит тикеры в форму и не нажимает кнопку «запустить бэктест». Роль просмотрщика как управляющей поверхности сжимается почти до нуля; роль «окна» расширяется.

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

Различие, которое проводит эта фраза, стоит сформулировать точно, потому что большинство «приложений с AI» стоит по другую его сторону. Чат, прикрученный к GUI, оставляет главным GUI: кнопки никуда не делись, они работают и по-прежнему определяют, что вообще возможно, а ассистент — слой удобства, который со временем начинает нажимать их за вас. Поверхность возможностей — это то, что интерфейс и так уже показывал.

Переверните — и поверхностью продукта становятся инструменты. Что система умеет, определяется набором инструментов; работа окна — разборчиво нарисовать результат. Заполнять форму не нужно, потому что формы нет.

ADR не делает вид, что это далось бесплатно. Цена названа прямо: переформулировка «обесценивает заметный объём только что выпущенной работы над UI», — и дальше сказано, что именно про эту работу решают:

Мы решаем сохранить эту работу и изменить то, что она делает для пользователя, а не выбросить её.

Формулировка верная и встречается реже, чем кажется. Форма, которая больше не нужна, и график, который очень даже нужен, вышли на одной неделе; разворот убивает первое и повышает второе. Записанное так решение потом не вспоминают как «мы выкинули месяц работы над UI» — а именно эта версия делает всех оборонительными по отношению к следующему развороту.

К чему разворот принудил

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

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

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

Опроса перестало хватать. Опрос аннотаций раз в секунду вполне годится для «агент поставил метку на вчерашней свече». Он не годится для «нарисуй этот график, с этими наложениями, вот на этом диапазоне, — а теперь поменяй, теперь приблизь, теперь добавь двухсотдневную». Разговорное взаимодействие даёт очереди мелких правок, и секунда задержки на каждой превращает разговор в ожидание.

Поэтому просмотрщик подписан на поток server-sent events, и команды агента на отрисовку, уведомления о завершённых прогонах и срабатывания алертов доходят до него почти в реальном времени.

flowchart LR
user["человек за клавиатурой"]
agent["Claude Code — MCP-клиент<br/>управляющая поверхность"]

subgraph side["сайдкар на Python — отдельный процесс"]
direction TB
mcp["/mcp · 59 инструментов<br/>Streamable HTTP"]
routes["29 HTTP-маршрутов<br/>+ /events (SSE)"]
logic["анализ · стратегии · бэктест<br/>прогноз · советчик · алерты · defi"]
db[("SQLite · 12 таблиц")]
mcp --> logic
routes --> logic
logic --> db
end

subgraph win["Electron-просмотрщик — опционален"]
rend["рендерер · графики · 14 вкладок<br/>подписчик SSE"]
end

user --> agent --> mcp
routes --> rend
logic -. события .-> routes

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

Разделение именно закреплено, а не рекомендовано. Агент и просмотрщик аутентифицируются разными bearer-токенами и обращаются к разным транспортам на одном порту: у агента токен долгоживущий, в файле, на который его можно направить, у просмотрщика — выпускаемый на каждый запуск сайдкара. И просмотрщик вообще не выходит в сеть, кроме как к собственному сайдкару: каждый внешний запрос, каждый вызов RPC, каждый сторонний API с лимитами живёт на стороне Python. Процесс-рендерер, который не может дотянуться до интернета, — это процесс, который не может ничего туда утечь.

Та же форма, дважды, на разных языках

Если эта архитектура кажется знакомой по посту про Ritmolux — так и есть, и сходство не совпадение, на которое можно помахать рукой. Это одно и то же решение, принятое дважды.

Там ядро на Rust принимает PCM-кадры и ничего не знает об их происхождении; отдельное приложение кормит его loopback-захватом системного вывода, а компонент foobar2000 — собственным потоком визуализации плеера. Здесь сайдкар на Python принимает вызовы инструментов и ничего не знает о том, кто их сделал; агент зовёт по MCP, окно — по HTTP.

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

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

Различие между двумя случаями — в том, с какой стороны пришло ограничение. Разделение в Ritmolux было вынужденным: компонент foobar2000 — это C++ DLL, поэтому всё общее с отдельным Rust-приложением должно было выражаться через границу C, и эта граница сделала абстракцию непереговорной. Разделение в market-analyzer было выбрано — причём дважды: когда MCP добавили вторым протоколом и когда на следующий день он стал первым.

Что оно умеет

Поверхность, сгруппированная по тому, о чём вы стали бы просить, а не по модулям.

Графики. Свечи по одному инструменту на таймфреймах от пятнадцати минут до месяца, с маршрутизацией по символу на Yahoo Finance, Binance или Coinbase — биржевые пары на спотовые площадки, акции на Yahoo. Первый запрос добирает историю, дальше всё отдаётся из локального кэша SQLite с ленивой подгрузкой при прокрутке назад.

Техническая картина. Запаздывающие индикаторы — запаздывающие в смысле отсутствия заглядывания вперёд: решение на баре видит только бары до него включительно, — японские свечные паттерны, классические графические фигуры с жизненным циклом «формируется → подтверждена», Ишимоку, объёмно-взвешенные уровни поддержки и сопротивления, уровни Фибоначчи и пивоты, дивергенции моментума, чтение структуры рынка и снимок состояния одним вызовом, собирающий всё сразу.

График дневных баров NVDA с обведёнными подтверждёнными классическими фигурами — восходящий клин, перевёрнутая голова и плечи, двойная вершина, симметричный и восходящий треугольники, — у каждой отмечена расчётная цель движения

Стратегии и бэктесты. Девять стратегий, каждая — чистый модуль generate_signals(bars, params), выдающий «вне позиции», «лонг» или «шорт», с типизированной моделью параметров. Движок даёт кривую капитала, журнал сделок, расширенные метрики — Шарп, Сортино, Кальмар, профит-фактор и прочие — и скользящую walk-forward-проверку.

Результат бэктеста: строка метрик, кривая капитала, зелёная в прибыли и красная в просадке, и журнал сделок под ней

Прогнозы. Один инструмент, три вида, все — read-only условия, а не советы. Волатильность предсказывает реализованную амплитуду на бар с полосой неопределённости, оценивается по QLIKE против экспоненциально взвешенной и персистентной базовых моделей. Режим предсказывает следующее состояние «тренд × волатильность» распределением, оценивается по Брайеру против персистентности. Направление сохранилось как пониженный в правах вид: калиброванная вероятность «вверх/вниз/вбок» на горизонт, причём каждый горизонт независимо проходит через шлюз «walk-forward должен обыграть базовую модель», и при непрохождении отдаётся null вместе с основанием этой проверки.

Вот на эту деталь я бы указал сильнее всего. Честное «преимущества нет» здесь нормальный ответ, а не баг. Прогнозирующая поверхность, которая всегда возвращает число, в дни, когда сказать ей нечего, возвращает шум, — а шум, оформленный вероятностью, неотличим от сигнала, оформленного вероятностью. Встроенное в контракт «ничего», с приложенным обоснованием этого «ничего», — вот что не даёт системе быть уверенно неправой ровно в те дни, когда ей следовало помолчать.

Ончейн-чтения. Разобранные позиции по нескольким EVM-сетям, дополненные состоянием пулов ликвидности, прочитанным напрямую через RPC. Себестоимость восстанавливается воспроизведением транзакций с ценами по времени блока, а не оценивается, и позиции, которые воспроизведение не может учесть полностью, помечаются неполными с названной причиной, а не с догадкой — тот же инстинкт, что и у прогнозного «ничего», в другой подсистеме.

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

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

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

Всё это доступно как 59 MCP-инструментов, закреплённых исчерпывающим тестом регистрации, причём несколько глаголов намеренно сведены под один различающий аргумент вместо развёртывания «по инструменту на чтение». Поверхность инструментов — это пространство имён, которое агенту приходится держать в голове, и шестьдесят хорошо названных инструментов удобнее двухсот узких.

Два ограничения, которые выдаёт сама область

Всё перечисленное сформировано двумя вещами, которыми нельзя поступиться, а README объясняет причину предложением, которое стоит позаимствовать целиком:

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

Это конкретный и неприятный режим отказа. Ничего не падает, ничего не тормозит. Вы получаете число, число неверное, и выглядит оно ровно как верное: та же форма кривой капитала, тот же правдоподобный Шарп. Ловить нечего и грепать нечего, а человек на приёме сейчас начнёт по этому действовать.

Поэтому оба ограничения записаны как инварианты MUST в спецификации, а не носятся как привычки.

Никакого заглядывания вперёд. Решение на баре i исполняется на баре i + 1, по цене его открытия. Движок не имеет права дать сигналу заполниться по любой цене с индексом ≤ i и обязан отбросить сигнал, у которого исполняемого бара не существует. Заглядывание вперёд — самый лёгкий баг этой области и самый трудный для замечания, потому что от него результаты становятся лучше, а стратегию, которая вдруг начала работать, никто не идёт расследовать.

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

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

Условия — факты, решения — ваши

Принцип, на котором всё построено: поверхность анализа сообщает условия и никогда не рекомендует. Найти фигуру, классифицировать режим, прогнать бэктест — и остановиться. Синтез остаётся за пользователем.

Пересекать эту черту разрешено ровно одному слою, и под собственную запись о решении: советчику, который сплавляет условия, живые сигналы стратегий, подтверждённое бэктестом преимущество и прогнозы в помеченную рекомендацию — или в честное «вне позиции». Каждую его рекомендацию связывают три правила. Она помечена как рекомендательная. Она несёт своё обоснование и свою проверенную основу с честной неопределённостью. И решает по-прежнему пользователь.

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

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

Граница на дальнем конце — та, которая важнее всего:

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

Каталог генерирует себя сам

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

Это не случайная аккуратность, и у неё есть история. Когда-то этот README утверждал, что MCP-инструментов 56, тогда как код отдавал 59, и заявлял версию 0.9.0, тогда как проект был на 0.26.0, — с этого расхождения начинается пост про проверки документации. Число инструментов, вписанное руками в манифест, ничто не перезапускает. Число, сгенерированное из теста регистрации, — перезапускает.

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

Тот же инстинкт виден и в том, как проект себя фотографирует. Скриншоты в этом посте сняты с настоящего приложения в полном размере, управляемого через MCP посредством Playwright-обвязки: приложению велели отрисовать каждую вкладку тем же интерфейсом, которым пользуется агент, и затем сфотографировали. Скриншот, полученный так, не может показать состояние, которое поверхность инструментов не способна произвести, — а это на удивление частый способ для продуктовых скриншотов соврать.

Он быстро разворачивается — и записывает почему

Однодневный разворот не единичный случай. Так этот проект себя ведёт, и второй пример острее, потому что разворот случился в тот же день, что и отменяемое решение.

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

Его отменили в тот же день, и рассуждение — образец того, как переоткрывать решение без драмы. Изменились два факта.

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

А реально используемая вырезка оказалась крошечной. Три файла, около 250 строк, из которых ровно одна функция — примерно сорок строк HTTP и разбора JSON против публичного API графиков — была единственным, что адаптер когда-либо вызывал. Аргумент про «обкатанный код» на практике почти ничего не делал.

Дальше общее правило, и вот его стоит сохранить:

Дисциплина вендоринга окупается, когда апстрим жив, расхождение — реальный риск, а объём достаточно велик, чтобы копирование с дисциплиной выигрывало у переписывания с нуля. Здесь не выполняется ни одно из этих условий.

Это предложение переиспользуемо. Оно называет условия, при которых исходное решение было бы верным, проверяет их и не находит выполненным ни одного. Спорить о том, хорош ли вендоринг вообще, никому не приходится — приходится проверить три условия. И цена сохранения политики названа конкретно, а не отвлечённо: заголовок «Вендорено из…» в исходниках, lock-файл, неиспользуемый путь пакета и «постоянное когнитивное притяжение, заставляющее участников думать о репозитории, которого к моменту выпуска этого приложения не будет».

Заменяющее решение доводит и уборку. Апстрим разрешён как read-only источник идей и структуры, не копируется и не является зависимостью во время выполнения, и нигде далее не назван — ни в исходниках, ни в комментариях, ни в заголовках файлов, ни в планах, диаграммах, файлах навыков, ни в самом lock-файле. Отменённое решение, оставившее свой словарь разбросанным по дереву, — это решение, которому продолжают наполовину следовать.

Два правила про зависимости

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

Выдержка в 14 дней. Ни одна версия пакета моложе четырнадцати дней не может быть выбрана при разрешении зависимостей; это включено штатными механизмами обоих пакетных менеджеров. Модель угрозы конкретна: вредоносная версия легитимного пакета или свежеопубликованный тайпсквот попадает в реестр и живёт там от часов до дней, прежде чем его пометят и снимут. Историческое окно «обнаружить и удалить» — порядка одного-четырнадцати дней.

Lock-файлы уже защищают установки: CI ставит по замороженному локу, поэтому закоммиченный lock не может молча впитать новую версию сверху. Уязвимо время разрешения — момент, когда разработчик добавляет или обновляет пакет, ведь то, что резолвер выберет тогда, попадёт в lock-файл и разъедется по всем машинам и в CI. Выдержка закрывает это окно конфигурацией, а не самописным инструментом.

А дальше честность, которая делает это политикой, а не позой:

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

Автоматический обход для срочных патчей безопасности звучит ответственно и выпотрошил бы политику, потому что «срочный патч безопасности» — ровно то, чем притворяется атака на цепочку поставок. Принять задержку, записать, что вы её приняли, и требовать человека для обхода — вот версия, которая переживает встречу с настоящим инцидентом.

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

Где оно сейчас

Версия 0.26.0, до 1.0, в активной разработке. Работают обе половины: 59 MCP-инструментов, 29 REST-операций, 28 видов SSE-событий, двенадцать таблиц SQLite, три десятка адаптеров данных и четырнадцать вкладок в просмотрщике. Поверхность MCP и контракт REST между релизами ещё могут меняться; стабильность начинается с 1.0.0. Установщиков для конечного пользователя нет — репозиторий клонируют и запускают.

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