Возвращение блудного программиста (ч. 4)

от автора

Эта часть про то, как я пилю бэкенд, учусь на этом и получаю эмоциональные качели.

Содержание

Для начала, напомню о себе: «у меня свой бизнес, а в IT, я так, для души». Шутка. В общем, после 12 лет отсутствия в сфере я решил вернуться к своему базовому образованию – инженер-программист. Что-то приходится «вспоминать с нуля», но я не люблю начинать изучение полностью с теории, я больше экспериментатор. Во-первых, так можно учиться до бесконечности, оттачивая свои знания, но никогда их не применить. Во-вторых, банально, но ты не узнаешь, что такое горячо пока не потрогаешь. Есть еще, в-третьих, теперь есть ИИ – интерактивная энциклопедия и помощник в одном лице (хотя тут столько своих НО).

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

Важное отступление: у меня самый дешевый виртуальный хостинг, не VPS. Почему? Потому что «уже был», под мелкие эксперименты

Так вот. Спустя почти 2 недели, попутно изучая что и зачем, я закончил бэкенд. Насколько я знаю, здесь не очень принято обсуждать опыт использования ИИ, но я вас спрашиваю, как это работает: «я никогда не сталкивался с IT, по образованию я преподаватель литературы, но решил всё изменить, и за один вечер с ИИ написал супер-пупер мега сервис». Как, Карл?

Вы видели, что он выдаёт? О, это вполне работоспособный код, но это «бомба замедленного действия». В первую очередь, меня напрягла куча фреймворков, кажется, буквально на всё. Ладно, может это сейчас так и делается. Потом, выдуманные параметры (например, для ендпоитнов ISS MOEX), или наоборот, заверения, что ничего не надо. Самое прикольное – это когда ИИ выдает полный листинг файла, а потом текстом примечание, типа «я тут использовал устаревший/неэффективный/еще какой-то подход, если хотите, чтоб всё работало как часы – надо сделать то-то и то-то». Блин, а когда ловишь его на ошибке/неточности «да, вы правы. Сейчас исправлю. И, кажется, всё равно кривой код попадает к контекст».

Отдельные перлы

Ты прав по обоим пунктам.

По первому — current_price * quantity абсолютно верно. Рыночная цена облигации уже отражает амортизацию — ISS её считает с учётом остаточного номинала. Нам не нужно ничего пересчитывать.

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

И это после того, как он мне это вставил в код, я столкнулся с «кривыми» данными, нашел почему и… да, я прав.

ISS хранит только 2 дня внутридневных данных — значит таймфреймы 1м и 5м будут доступны только за текущий и предыдущий торговый день.

Это совершенно не так, ISS отдаёт свечи, чуть ли не c 2008-10 годов по 7 разным интервалам, но там нет 5 минут. Я долго пытался понять почему я получаю пустой массив.

Именно, ты правильно мыслишь. Раз данные в MySQL обновляются раз в минуту — WebSocket вообще не даёт никакого преимущества. Смысла в нём ноль.

А тут он мне на shared-хостинге (да он знает мои ограничения) советует мне заморочиться с WebSocket (это вообще реально?) для данных, которые на сервере обновляются раз в минуту, а на клиенте, если чаще минуты вообще берутся из кэша (на фронте TanStack Query, при чем сам ИИ предложил).

Фуух, всё. В защиту скажу, что для изучения, в чём-то это даже хорошо. Как это работает для меня на конкретном примере. Я понимаю, что нужно сделать. Надо раз в сутки делать запрос к ISS, получать список бумаг и заливать их в MySQL на сервере в несколько таблиц. Затем, ежеминутно запрашивать с ISS котировки и тоже заливать в MySQL. Так сделано, чтобы не долбить ISS запросами от каждого клиента. Я лучше один раз заберу к себе, а потом раздам клиентам, в том числе из кэша. Но я не знаю всех прелестей python (мой стек был Delphi+ADO+MS SQL. Да, я баловался и PHP, и Python, даже Perl чуток, но это не в счёт). Я прошу ИИ написать код и изучаю его. Всё здорово, всё понятно и работает.

Я рад, но не долго. Начинаю делать ручные тесты (пока не в теме QA Automation). Вот тут и начинается обучение: я вижу, что скрипт работает не так как запланировано, разбираюсь с этим (были неправильные ендпоитны, не учитывал пагинацию, уверяя что её нет, брал вообще не те поля, брал названия полей не из модели данных и так далее). Начинаю фиксить сам, потом делаем это вместе с ИИ. Снова тесты. Снова сам, снова ИИ. И на одной из итераций победа! Локально. Всё работает, всё правильно берёт, правильно раскладывает. Деплой.

С первого взгляда всё отлично, но помните отступление про хостинг? А теперь эти скрипты «отжирают» 13% CPU, вместо разрешённых CPU. Разбираемся. Тут, конечно, я развёл руками. Отдался ИИ. Оказывается, мало того, что они долбят ISS каждый раз SSL церемониями на каждую бумагу, так еще и MySQL по несколько раз дергают, т.е. для 100 бумаг – это 100 соединений, и 200-300 обращений к базе через дополнительную прослойку (SQLAlchemy). Это только 1 скрипт, во втором всё еще хуже: на каждую бумагу несколько запросов к ISS (из-за пагинации ответов), и кратно записей в MySQL. Не удивительно, что каждый из них в момент работы занимал до 50-60% процессорного времени.

Починили. В начале скрипта открываем сессию к ISS и запросы шлём в рамках этой сессии, собираем по результатам массив и на native SQL делаем bulk insert. Для меня пока что всё логично, и время CPU сократилось до 2-4%, т.е. имеем ожидаемый результат.

Для тех, кто любит пинать ногами

Первоначальный код
def fetch_coupons(ticker):    while True:        url = f'{ISS_BASE}/statistics/engines/stock/markets/bonds/bondization/{ticker}.json'        try:            r = requests.get(url, params=params, timeout=15)            r.raise_for_status()            data = r.json()            # some more code        except Exception as e:            print(f' Ошибка при загрузке купонов {ticker} (start={start_coupons}): {e}')            break        while True:        url = f'{ISS_BASE}/statistics/engines/stock/markets/bonds/bondization/{ticker}.json'        try:            r = requests.get(url, params=params, timeout=15)            r.raise_for_status()            data = r.json()            # some more code        except Exception as e:            print(f' Ошибка при загрузке амортизаций {ticker} (start={start_amort}): {e}')            break        return sorted_coupons, sorted_amortswith app.app_context():    bonds = Security.query.filter_by(is_active=True, type='bond').all()    for i, sec in enumerate(bonds, 1):        rows, amort = fetch_coupons(sec.ticker)                for c in rows:            try:                db.session.execute(“INSERT …. ON DUPLICATE KEY UPDATE”)            except Exception as e:                print(f'  Ошибка записи купона {sec.ticker}: {e}')                continue        for a in amort:            try:                db.session.execute(“INSERT …. ON DUPLICATE KEY UPDATE”)            except Exception as e:                print(f'  Ошибка записи амортизаций {sec.ticker}: {e}')                continue                                       try:        db.session.commit()    except Exception as e:        db.session.rollback()        print(f'  Ошибка commit {sec.ticker}: {e}')
К чему пришли в итоге
def fetch_coupons(session, ticker):    while True:        url = f'{ISS_BASE}/statistics/engines/stock/markets/bonds/bondization/{ticker}.json'            try:            r = session.get(url, params=params, timeout=15)            r.raise_for_status()            data = r.json()            # some more code        except Exception as e:            print(f' Ошибка при загрузке купонов {ticker} (start={start_coupons}): {e}')            break        while True:        url = f'{ISS_BASE}/statistics/engines/stock/markets/bonds/bondization/{ticker}.json'        try:            r = session.get(url, params=params, timeout=15)            r.raise_for_status()            data = r.json()            # some more code        except Exception as e:            print(f' Ошибка при загрузке амортизаций {ticker} (start={start_amort}): {e}')            break     return sorted_coupons, sorted_amorts       with app.app_context():    with requests.Session() as session:        bonds = Security.query.filter_by(is_active=True, type='bond').all()                      for i, sec in enumerate(bonds, 1):            rows, amort = fetch_coupons(session, sec.ticker)                    for a in amort:                bulk_amortizations.append({})            for c in rows:                bulk_coupons.append({})           if bulk_amortizations:            try:                db.session.execute(                    db.text(),                    bulk_amortizations                )                db.session.commit()            except Exception as e:                db.session.rollback()                print(f'  Ошибка массовой записи амортизаций: {e}')                            if bulk_coupons:            try:                db.session.execute(                    db.text(),                    bulk_coupons                )                db.session.commit()            except Exception as e:                db.session.rollback()                print(f'  Ошибка массовой записи амортизаций: {e}')                             db.session.remove()

Чему я научился за это время короткое:

  • доверяй ИИ, но проверяй. Тут как раз становятся актуальны статьи (здесь же, на Habr), почему нельзя вайбкодить если ты не разбираешься в программировании;

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

  • изучил ряд новых для себя конструкций, как например, декораторы функций в python;

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

  • ну и понял множество нюансов, вроде handshake в начале каждой сессии.

Смогу ли я повторить бэкенд без ИИ теперь. Вполне, но в любом случае мне потребуется документация на большинство инструментов, которые я применил. Здесь я рассказал только про два cron скрипта, которые мне показались наиболее интересными именно с точки зрения моего подхода к обучению. Кроме описанного у меня используется кэширование (Flask-Caching), аутентификация JWT (Flask-JWT-Extended), естественно сам Flask, ORM (SQLAlchemy), alembic для миграций.

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

ссылка на оригинал статьи https://habr.com/ru/articles/1035800/