К обычному разбору
Тренировка по собеседованиюТехническое собеседованиеBHFT2026-04-22

BHFT: Техническое собеседование

Идите сверху вниз: сначала попробуйте сами, затем откройте разбор. Если шаг с кодом, пишите решение прямо здесь и запускайте проверки на странице.

Шагов
14
Вопросов
11
Задач
3
1Вопрос10 мин

Проблемы item-to-item рекомендаций одежды

Какие особенности и риски есть у item-to-item рекомендаций в fashion каталоге?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

В fashion важны визуальная похожесть, размер/пол/стиль, сезонность, availability и cold-start; pure co-click similarity часто ловит popularity bias.

Подробный разбор

Item-to-item в одежде нельзя сводить только к "что часто покупают вместе". Нужно разделять заменители и complements, учитывать размеры, gender/fit, бренд, цвет, стиль, цену, сезонность и наличие на складе.

Классические риски: popularity bias, устаревшие товары, нехватка истории для новых SKU, leakage из будущих заказов, плохая персонализация и отсутствие diversity. Хорошая схема часто смешивает collaborative signals, content/image embeddings и business constraints, а offline метрики дополняет online CTR/conversion и user satisfaction.

2Вопрос10 мин

Почему time-series модель может развалиться после хорошего offline

Модель на временном ряде показывает хороший offline score, но в реальности не работает. Какие причины проверить первыми?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Первое - leakage, random split, неверный prediction horizon, признаки из будущего и train/test режим, который не имитирует реальный момент решения.

Подробный разбор

Для временных рядов offline должен имитировать будущее: train на прошлом, validation на будущем. Нужно проверить rolling features, normalization, joins, target shift и любые признаки, которые используют данные после момента решения.

Хороший random split почти ничего не доказывает: соседние точки временного ряда похожи, окна могут пересекаться, а scaler или агрегаты могли быть fit на full dataset. Отдельно проверяют drift, costs/latency и gap между моментом наблюдения признаков и моментом предсказания.

3Вопрос10 мин

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

Модель дает prediction для trading/time-series задачи. Как оценить confidence и использовать его в решении?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Confidence можно брать из calibrated probability, uncertainty estimate или empirical reliability по бинам score; score модели сам по себе не всегда является уверенностью.

Подробный разбор

Важно отделить score модели от настоящей уверенности. Для классификации проверяют calibration curve, Brier/log loss, reliability bins и expected calibration error. Для регрессии можно строить prediction intervals, quantile models или ансамбли.

В trading confidence обычно связывают с threshold и размером позиции, но это нужно проверять out-of-time: бины с большим confidence должны иметь лучший realized outcome. Иначе модель может быть уверена только на train distribution.

4Кейс15 мин

HFT/time-series задача: постановка и признаки

Даны временные рыночные данные для HFT-задачи. Как подойти к постановке target, построению признаков, валидации и первому baseline?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Нужно сначала зафиксировать target и горизонт прогноза, затем строить признаки только из прошлого, валидироваться по времени и отдельно проверять leakage, latency и устойчивость на новых периодах.

Подробный разбор

Для HFT/time-series задачи нельзя начинать с модели. Сначала нужно понять, что именно предсказываем: направление цены, доходность, вероятность движения, spread или другой target. Потом фиксируем горизонт прогноза и момент времени, в который модель должна принять решение.

Дальше признаки: лаги цены/объема, rolling statistics, imbalance, spread, volatility, агрегаты по окнам и признаки микроструктуры рынка. Все они должны считаться только из информации, доступной до момента предсказания.

Валидация должна быть временной: train на прошлом, validation на будущем, без random split. Для финансовых рядов особенно важно проверить leakage через нормализацию, rolling features, future joins и неявное использование будущих строк.

Baseline: простой rule-based или линейная модель/GBDT на лаговых признаках. Только после него имеет смысл обсуждать sequence-модели, если есть достаточно данных и понятная latency/cost граница.

Типичные ошибки

  • Использовать random split для временного ряда.
  • Считать rolling features или normalization по всему датасету.
  • Начать с Transformer без target, горизонта и baseline.

Как сказать на собеседовании

  • Сначала спроси target, горизонт и момент принятия решения.
  • Отдельно проговори, какие признаки доступны online, а какие являются leakage.
5Кейс10 мин

Trades и order book как источники признаков

Какие сигналы обычно извлекают из trades и order book при HFT-задаче прогноза цены?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Из стакана берут bid/ask, spread, mid-price, depth/imbalance; из trades - signed volume, intensity, VWAP и агрегации по коротким окнам.

Подробный разбор

Order book дает состояние ликвидности: best bid/ask, mid-price, spread, объемы на уровнях, imbalance между bid/ask, slope/depth и изменения этих величин. Trades дают фактическую агрессию рынка: price, size, side/sign, trade count, volume, VWAP и burst intensity.

Так как события нерегулярные, признаки часто выравнивают на регулярную временную сетку: forward-fill для состояния стакана, rolling aggregations для сделок и lag features. Критично фиксировать момент доступности каждого признака, иначе легко заглянуть в будущее.

6Кейс10 мин

Как выбрать target для HFT mid-price prediction

Даны trades и order book. Как сформулировать target для предсказания будущего движения mid-price?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Нужно зафиксировать decision time, horizon и форму target: future return, direction, thresholded move или no-trade/up/down с учетом spread и costs.

Подробный разбор

Target должен соответствовать действию модели. Например, можно предсказывать relative change mid-price через 100 ms или 1 s, знак движения после учета spread/cost, или вероятность движения выше threshold.

Нельзя строить target так, чтобы признаки случайно видели будущее окно. Для шумных HFT-данных часто вводят no-trade class или threshold, чтобы не заставлять модель предсказывать микрошум, который не покрывает транзакционные издержки.

7Кейс10 мин

Time grid и признаки из нерегулярных HFT событий

Как построить feature matrix на регулярной 100 ms сетке из нерегулярных trades и order book events?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Сетка задает decision timestamps; state features forward-fill-ятся из последнего стакана, trades агрегируются только из прошлого окна.

Подробный разбор

Сначала выбирается регулярная сетка timestamp-ов, например каждые 100 ms. Для order book state на каждую точку берется последний известный snapshot не позже timestamp: best bid/ask, mid, spread, imbalance и depth. Для trades считаются rolling или bucket aggregations за прошлые окна: count, volume, signed volume, VWAP, max/min price.

Нельзя использовать события после decision timestamp. Target должен быть смещен в будущее, например future mid-price return через заданный horizon. Для train/test split нужен temporal split с gap, иначе rolling windows и forward-fill могут протащить leakage между выборками.

8ЗадачаHard

Feature builder для Coinbase/Binance time grid

Условие

Нужно собрать признаки для HFT lead-lag датасета.

Даны:

  • регулярная сетка времени grid;
  • нерегулярные обновления L1 order book для Binance и Coinbase;
  • нерегулярные сделки с обеих бирж.

Для каждого grid_ts нужно построить строку признаков, используя только события с timestamp <= grid_ts.

Для каждого стакана сделайте forward fill последнего известного bid/ask и верните:

  • bn_mid, bn_spread, bn_bid_size, bn_ask_size;
  • cb_mid, cb_spread, cb_bid_size, cb_ask_size.

Для сделок посчитайте суммарный notional price * amount в окне (previous_grid_ts, grid_ts] отдельно по бирже и стороне:

  • binance_buy_notional, binance_sell_notional;
  • coinbase_buy_notional, coinbase_sell_notional.

Если для стакана еще нет значения, верните None. Если сделок в окне нет, верните 0.0.

Сигнатура

def build_time_grid_features(
    grid: list[int],
    l1_binance: list[dict],
    l1_coinbase: list[dict],
    trades: list[dict],
) -> list[dict]:

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Forward fill

    Для каждого grid_ts нужен последний L1 update с ts <= grid_ts.

  • Окна сделок

    Используйте (previous_grid_ts, grid_ts], чтобы сделка на границе не попала в два окна.

Идея решения

Сортируем сетку, стаканы и сделки по времени. Дальше идем sweep-line по grid.

Для каждой биржи держим последний L1 update с ts <= grid_ts; это и есть forward fill. Mid-price считается как (bid + ask) / 2, spread как ask - bid.

Сделки агрегируем указателем по полуоткрытому окну (previous_grid_ts, grid_ts]. Это важно, чтобы сделка на границе попала ровно в одно окно.

Эталонный код

def build_time_grid_features(
    grid: list[int],
    l1_binance: list[dict],
    l1_coinbase: list[dict],
    trades: list[dict],
) -> list[dict]:
    grid = sorted(grid)
    books = {
        'bn': sorted(l1_binance, key=lambda row: row['ts']),
        'cb': sorted(l1_coinbase, key=lambda row: row['ts']),
    }
    pointers = {'bn': 0, 'cb': 0}
    last = {'bn': None, 'cb': None}
    trades = sorted(trades, key=lambda row: row['ts'])
    trade_index = 0

    def add_book_features(row: dict, prefix: str, book) -> None:
        if book is None:
            row[f'{prefix}_mid'] = None
            row[f'{prefix}_spread'] = None
            row[f'{prefix}_bid_size'] = None
            row[f'{prefix}_ask_size'] = None
            return
        row[f'{prefix}_mid'] = (book['bid_price'] + book['ask_price']) / 2
        row[f'{prefix}_spread'] = book['ask_price'] - book['bid_price']
        row[f'{prefix}_bid_size'] = book['bid_size']
        row[f'{prefix}_ask_size'] = book['ask_size']

    result: list[dict] = []
    previous_grid_ts = None

    for grid_ts in grid:
        for prefix, updates in books.items():
            while pointers[prefix] < len(updates) and updates[pointers[prefix]]['ts'] <= grid_ts:
                last[prefix] = updates[pointers[prefix]]
                pointers[prefix] += 1

        row = {'ts': grid_ts}
        add_book_features(row, 'bn', last['bn'])
        add_book_features(row, 'cb', last['cb'])

        buckets = {
            'binance_buy_notional': 0.0,
            'binance_sell_notional': 0.0,
            'coinbase_buy_notional': 0.0,
            'coinbase_sell_notional': 0.0,
        }
        lower_bound = float('-inf') if previous_grid_ts is None else previous_grid_ts

        while trade_index < len(trades) and trades[trade_index]['ts'] <= grid_ts:
            trade = trades[trade_index]
            if trade['ts'] > lower_bound:
                exchange = trade['exchange'].lower()
                side = trade['side'].lower()
                buckets[f'{exchange}_{side}_notional'] += trade['price'] * trade['amount']
            trade_index += 1

        row.update(buckets)
        result.append(row)
        previous_grid_ts = grid_ts

    return result
Сложность
Время: O((G + B + C + T) log T). Память: O(G).
Сетка, стаканы и сделки сортируются/сканируются. После сортировки sweep проходит по каждому списку один раз.
9Вопрос10 мин

Почему CatBoost, а не линейная модель

В time-series табличной задаче кандидат выбирает CatBoost. Как объяснить выбор и какой baseline нужен?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

CatBoost ловит нелинейности и interactions, но его нужно сравнить с naive, linear/ridge и простыми rules на temporal validation.

Подробный разбор

Сильный ответ не продает CatBoost как магию. Сначала baseline: константа, last value, линейная модель на лагах и простые rules. Потом GBDT, если есть нелинейные зависимости, разные масштабы признаков и interactions.

Улучшение должно быть на temporal validation, без leakage. Для HFT/time-series также важно проверить стабильность по периодам, sensitivity к drift и связь offline metric с trading proxy или PnL after costs.

10Вопрос10 мин

Как понять, хороший ли MSE на тесте

Модель получила MSE на тестовом периоде. Как понять, хороший это результат или нет?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

MSE имеет смысл только относительно baseline, масштаба target, variance и бизнес-метрики; маленький MSE не гарантирует полезный trading signal.

Подробный разбор

Нужно сравнить MSE с константным прогнозом, last value, линейной моделью и variance target. Без baseline число MSE почти невозможно интерпретировать.

Для trading маленький MSE не гарантирует прибыль: важны направление, ranking, threshold, calibration и proxy-PnL после costs. Также стоит смотреть стабильность по периодам и slice metrics, потому что средняя ошибка может скрывать деградацию на волатильных режимах.

11ЗадачаMedium

PnL proxy по предсказаниям движения цены

Условие

Есть предсказания будущего изменения цены в basis points и фактически реализованные изменения.

Нужно построить простую proxy-метрику без полноценного бэктестера:

  • если prediction > threshold_bps, открываем long;
  • если prediction < -threshold_bps, открываем short;
  • иначе не торгуем.

PnL одной сделки в bps:

  • для long: realized_bps;
  • для short: -realized_bps.

Из каждой сделки вычитается fee_bps.

Сигнатура

def pnl_proxy(predicted_bps: list[int], realized_bps: list[int], threshold_bps: int = 0, fee_bps: int = 0) -> dict:

Верните словарь с ключами trades, hits, hit_rate, gross_bps, net_bps.

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Сигнал, а не magnitude

    В этой proxy-метрике prediction используется для выбора long/short/no-trade.

  • Комиссия за сделку

    Fee вычитается из net PnL один раз за каждую открытую позицию.

Идея решения

Сначала переводим prediction в торговый сигнал: 1 для long, -1 для short и 0 для no-trade.

Gross PnL сделки равен signal * realized_bps. Hit — это сделка с положительным gross PnL. Net PnL дополнительно вычитает комиссию за каждую сделку.

Эталонный код

def pnl_proxy(predicted_bps: list[int], realized_bps: list[int], threshold_bps: int = 0, fee_bps: int = 0) -> dict:
    if len(predicted_bps) != len(realized_bps):
        raise ValueError('predicted_bps and realized_bps must have the same length')

    trades = 0
    hits = 0
    gross_bps = 0

    for prediction, realized in zip(predicted_bps, realized_bps):
        if prediction > threshold_bps:
            signal = 1
        elif prediction < -threshold_bps:
            signal = -1
        else:
            signal = 0

        if signal == 0:
            continue

        trade_pnl = signal * realized
        trades += 1
        gross_bps += trade_pnl
        if trade_pnl > 0:
            hits += 1

    hit_rate = None if trades == 0 else hits / trades
    net_bps = gross_bps - trades * fee_bps

    return {
        'trades': trades,
        'hits': hits,
        'hit_rate': hit_rate,
        'gross_bps': gross_bps,
        'net_bps': net_bps,
    }
Сложность
Время: O(n). Память: O(1).
Один проход по предсказаниям и фактическим изменениям.
12Кейс14 мин

Ревью notebook: leakage и gap между train/test

На ревью notebook для временного ряда нужно найти leakage. Что проверять в feature generation и split?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Проверяем, что все признаки считаются только из прошлого, split идет по времени, а между train/test есть gap при пересекающихся окнах.

Подробный разбор

Типовые ошибки: rolling по всему датасету, scaler fit на full data, target-window пересекается с feature-window, random split, join будущих данных и использование признаков, которые в production станут доступны позже.

Gap нужен, если target или rolling features используют соседние интервалы и иначе train косвенно видит test. Хорошее ревью также проверяет reproducibility, backtest slices, feature freshness и связь offline score с proxy business или trading metric.

13ЗадачаMedium

Walk-forward splits с gap для временного ряда

Условие

Для временных рядов нельзя делать random split: validation должен идти после train по времени.

Напишите функцию, которая строит n_folds walk-forward splits. Каждый split использует expanding train prefix, затем optional gap, затем validation window фиксированного размера.

Индексы полуоткрытые: [start, end).

Сигнатура

def walk_forward_splits(n_samples: int, n_folds: int, validation_size: int, gap: int = 0) -> list[dict]:

Каждый элемент результата:

{"train_start": 0, "train_end": ..., "valid_start": ..., "valid_end": ...}

Решение прямо на странице

Напишите код, запустите проверки и только потом открывайте разбор.

Проверка решения

Нажмите «Запустить проверки» или Ctrl+Enter.

Показать разбор

Подсказки

  • Validation окна в конце

    Сначала найдите start первого validation окна: n_samples - n_folds * validation_size.

  • Gap вычитается из train_end

    Validation все равно начинается в valid_start, а train должен закончиться раньше на gap.

Идея решения

Последние n_folds * validation_size наблюдений отводим под последовательные validation окна.

Для каждого окна valid_start:valid_end train идет от нуля до valid_start - gap. Gap нужен, когда признаки используют lag/rolling windows или есть задержка между данными и моментом предсказания.

Эталонный код

def walk_forward_splits(n_samples: int, n_folds: int, validation_size: int, gap: int = 0) -> list[dict]:
    if n_samples <= 0 or n_folds <= 0 or validation_size <= 0 or gap < 0:
        raise ValueError('invalid split parameters')

    first_valid_start = n_samples - n_folds * validation_size
    if first_valid_start <= gap:
        raise ValueError('not enough samples for the requested splits')

    splits = []
    for fold in range(n_folds):
        valid_start = first_valid_start + fold * validation_size
        valid_end = valid_start + validation_size
        train_end = valid_start - gap
        splits.append({
            'train_start': 0,
            'train_end': train_end,
            'valid_start': valid_start,
            'valid_end': valid_end,
        })

    return splits
Сложность
Время: O(n_folds). Память: O(n_folds).
Функция строит по одному словарю на fold.
14Кейс12 мин

Какую deep learning архитектуру выбрать для временного ряда

После градиентного бустинга: если смотреть в сторону deep learning, какую архитектуру предложить для последовательных данных или временного ряда и почему?

Ответьте без подсказки

Сначала проговорите ответ вслух или тезисами.

Запишите черновик

Формулы, план решения, риски и примеры.

Сравните с разбором

Откройте разбор только после своей попытки.

Показать разбор

Короткий ответ

Для последовательностей можно предложить TCN/LSTM/Transformer, но выбор зависит от длины истории, latency, объема данных и требований к интерпретируемости. Часто стартуют с агрегированных временных признаков и легкой sequence-модели.

Подробный разбор

Ответ должен идти от данных. Если есть временной ряд или последовательность событий, можно использовать LSTM/GRU, TCN или Transformer. Для длинных зависимостей и большого объема данных Transformer гибче, но дороже. Для latency-sensitive задач часто разумнее TCN или компактная recurrent модель.

Нужно объяснить вход: сырые события, агрегированные окна, признаки стакана/каталога/пользователя, временные embeddings, маски, normalization. Если данные очень частые, их обычно агрегируют до разумного resolution, иначе модель будет дорогой и шумной.

Сравнение с GBDT важно: deep learning имеет смысл при больших данных, сложных последовательных паттернах и достаточной инфраструктуре. Иначе бустинг на аккуратных rolling features может быть сильнее и проще в production.

Типичные ошибки

  • Автоматически выбирать Transformer без обсуждения latency и данных.
  • Не описать, какие именно последовательности подаются на вход.
  • Не сравнить с сильным baseline на бустинге.

Как сказать на собеседовании

  • Начни с baseline: GBDT на rolling features.
  • Потом предложи sequence model и явно назови, когда она должна выиграть.