Search by

rekryt / tbank-lib

Rekryt

Асинхронная библиотека для T-Invest API (T-Bank Investments) на AMPHP v3 и Revolt Event Loop

Package info

github.com/rekryt/tbank-lib

pkg:composer/rekryt/tbank-lib

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.2 2026-09-12 16:30 UTC

This package is auto-updated.

Last update: 2026-09-12 17:35:25 UTC


README

Асинхронная PHP-библиотека для T-Invest API на AMPHP v3 и Revolt Event Loop.

Весь публичный API брокера — 109 unary-методов и 7 стримов — доступен типизированными вызовами. Заявка, портфель и котировка приезжают не массивом, а объектом: PostOrderResponse, PortfolioResponse, Quotation. Деньги считаются целыми числами в нано-долях, без float.

$client = Client::create(new ClientConfig(token: $token));

$portfolio = $client->operations()->getPortfolio($accountId);

echo $portfolio->totalAmountPortfolio?->toString();   // 101405.12 RUB

Возможности

Unary-методы 9 сервисов, 109 методов с развёрнутыми параметрами и именованными аргументами
Стримы биржевой, заявки и портфель по WebSocket: переподключение, восстановление подписок, сторожевой таймер
Справочник InstrumentRegistry — поиск по uid, figi, тикеру и positionUid за O(1) с ленивой догрузкой
Лимиты иерархические token bucket-ы «метод → сервис → глобальные 50/с», без sleep
Повторы экспоненциальный backoff с джиттером; торговые поручения повторяются только с ключом идемпотентности
Ошибки каталог из 175 кодов T-Invest API, разложенный по дереву исключений
Деньги Quotation и MoneyValue с целочисленной нано-арифметикой и округлением до шага цены
Асинхронность всё работает в Event Loop: сотня подписок и десяток методов одновременно — один процесс

Требования

  • PHP 8.2 или новее
  • ext-json, ext-mbstring
  • расширения для gRPC не нужны: библиотека ходит по REST и WebSocket (см. ADR 0002)

Установка

composer require rekryt/tbank-lib

Токен

Токен выпускается в личном кабинете: Инвестиции → Настройки → Токены для API. Подробно — в документации брокера.

Для начала берите токен только на чтение: он не даст выставить заявку даже по ошибке. Токен — это доступ к деньгам, поэтому в репозитории ему не место: держите его в .env и в переменных окружения.

cp examples/.env.example examples/.env
# впишите API_TOKEN в examples/.env

Быстрый старт

<?php

declare(strict_types=1);

use OpenCCK\Client;
use OpenCCK\ClientConfig;

require __DIR__ . '/vendor/autoload.php';

$client = Client::create(new ClientConfig(token: OpenCCK\env('API_TOKEN') ?? ''));

$accounts = $client->users()->getAccounts();
$accountId = $accounts->accounts[0]->id;

$portfolio = $client->operations()->getPortfolio($accountId);

printf("Счёт %s: %s\n", $accountId, $portfolio->totalAmountPortfolio?->toString() ?? '');

foreach ($portfolio->positions as $position) {
    printf(
        "  %-12s %s шт., доходность %s\n",
        $position->figi,
        $position->quantity?->toString() ?? '0',
        $position->expectedYield?->toString() ?? '0'
    );
}

Вызов выглядит синхронным, но не блокирует процесс: под ним Event Loop, и пока ответ в пути, работают остальные волокна. Отдельного await не нужно — библиотека сама живёт в цикле.

Обратите внимание: getAccounts() вернёт пустой список, если у токена нет доступа ни к одному счёту, — проверяйте результат перед обращением по индексу.

Стрим

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

$stream = $client->marketDataStream();

$stream->subscribeCandles([$instrumentUid], SubscriptionInterval::OneMinute);
$stream->subscribeLastPrice([$instrumentUid]);

foreach ($stream->events() as $event) {
    if ($event instanceof PayloadEvent && $event->payload instanceof Candle) {
        $candle = $event->payload;
        printf("%s: закрытие %s\n", $candle->time?->toString() ?? '', $candle->close?->toString() ?? '');
    }

    // за время разрыва биржа торговала: пропуск добирают unary-методами
    if ($event instanceof StreamDisconnectedEvent) {
        printf("разрыв, попытка %d через %.1f с\n", $event->attempt, $event->retryIn);
    }
}

Подписок больше трёхсот на соединение библиотека не сложит — это лимит брокера; сверх него она сама открывает следующее соединение. StreamMode::Raw отдаёт разобранный JSON без гидрации в DTO: на потоках в сотни тысяч сообщений в секунду это заметно дешевле.

Разворачивать примеры целиком — в examples/streams/.

Справочник инструментов

$registry = $client->registry();

$registry->warmup();                       // весь список одним запросом
$sber = $registry->byTicker('SBER', 'TQBR');

echo $sber?->uid;

Промах догружается точечно, одновременные запросы одного инструмента схлопываются в один сетевой вызов, записи живут 12 часов. toArray() и fill() переживают перезапуск процесса.

Примеры

examples/README.md — таблица всех 116 методов API: на каждый метод запускаемый .php и описание .md с входными данными, кодом, настоящим ответом биржи и разбором возможных ошибок.

php examples/quickstart/first-request.php        # счета и портфель
php examples/market-data/get-candles.php         # один метод API
php examples/streams/market-data-stream.php      # стрим котировок

Рядом с примерами на каждый метод — рукописные сценарии: quickstart/ для первого знакомства и recipes/ для задач, которые одним методом не решаются: лимиты, повторы, снимок справочника, добор данных после разрыва стрима, несколько токенов, экспортер метрик для Prometheus.

Примеры, меняющие состояние счёта — выставление и отмена заявок, пополнение песочницы, перевод валюты, — без ключа --live останавливаются, не дойдя до сети.

Архитектура

src/
├── Domain/           деньги, идентификаторы, enum-ы, справочник — про предметную область
├── App/              DTO, сервисы, стримы, события — про API брокера
└── Infrastructure/   транспорт, лимиты, повторы, ошибки, часы — про то, как это доехало

Зависимости идут в одну сторону: Infrastructure → App → Domain. Доменное ядро ничего не знает ни про HTTP, ни про amphp — котировку можно считать в тесте без сети и без Event Loop.

DTO, enum-ы, сервисы и примеры генерируются из снимков OpenAPI в resources/openapi/ (composer generate). Сгенерированный код лежит в репозитории, а CI проверяет, что он не разошёлся со спецификацией. Правьте генератор или снимок, а не выходные файлы.

Решения, которые дороже всего переигрывать, записаны в adr/: REST и WebSocket вместо gRPC, целочисленные деньги, иерархические token bucket-ы, повторы только идемпотентных вызовов. Устройство целиком, границы и обоснования — в BRIEF.md.

Разработка

composer qa                # всё, что гоняет CI
composer test              # 1696 тестов
composer coverage          # покрытие src/ с порогом 85 %
composer bench             # бюджеты производительности
composer generate:check    # код не разошёлся со спецификацией
composer check:secrets     # в рабочем дереве нет токенов
composer check:links       # ссылки в документации и примерах живые
Гейт Требование
PHPStan level 9, без baseline, 0 ошибок
Psalm errorLevel=1, 0 ошибок
PHP-CS-Fixer PSR-12 + правила .editorconfig
PHPUnit покрытие src/ ≥ 85 % (сейчас 98.8 %)
Бенчмарки бюджеты §7 BRIEF.md

Интеграционный прогон по песочнице (composer test:integration) запускается только при заданном TBANK_SANDBOX_TOKEN — он открывает счёт, пополняет его, выставляет и отменяет заявку и закрывает счёт за собой.

Лицензия

MIT — см. LICENSE.

Библиотека не связана с ПАО «Т-Банк» и разрабатывается независимо. Торговля на бирже сопряжена с риском потери средств; автор не несёт ответственности за решения, принятые вашим кодом.