---
name: investpilot_tinvest_sdk
description: "КодИнвест автоторговля — T-Invest подключаем через прямой REST (aiohttp), не через pip-SDK; sandbox-first"
metadata: 
  node_type: memory
  type: project
  originSessionId: 14ce7ad8-d5d7-4361-b56a-279b01d5004f
---

Строим автоторговый слой для [[investpilot_overview]] (КодИнвест, @InvestCodoCeh_bot на mini, `~/Services/investpilot`).

**⚠️ 2026-07-07: код этой сессии слит в дев-дерево `~/Разработки/investpilot`** (сессия 6f344612): её rsync --delete трижды затирал патчи, наложенные прямо на прод; восстановлено из scratchpad+транскрипта, 70 тестов, e2e PHASE0_OK. Дев-дерево = единственный источник правды; патчить прод напрямую больше НЕЛЬЗЯ.

**SDK-решение (2026-07-06):** официальный pip-пакет `tinkoff-investments` УДАЛЁН с PyPI (404) после ребренда Tinkoff→Т-Банк; оставшиеся `tinkoff-invest`/`tinvest` — мёртвые обёртки 2021 под отключённый старый OpenAPI. Поэтому ходим в **актуальный REST-API напрямую через aiohttp** (`marketdata/tinvest_client.py`, паттерн как `iss_client.py`). Endpoint `POST https://invest-public-api.tinkoff.ru/rest/tinkoff.public.invest.api.contract.v1.<Service>/<Method>`, `Authorization: Bearer <token>`. С mini достижим напрямую (РФ IP, БЕЗ прокси; TG/YouTube — через xray, T-Invest — нет).

**ГРАБЛИ URL:** пакет и сервис в пути склеиваются ТОЧКОЙ (`contract.v1.SandboxService/...`); со слэшем (`contract.v1/SandboxService/...`) edge молча отдаёт 404 без тела — выглядит как IP-блок/WAF, съело сессию диагностики (ложные следы: «burst-блок», «TLS-фингерпринт», curl_cffi — всё опровергнуто, curl_cffi откатили). Никакого WAF у T-Invest не наблюдалось; троттлинг в клиенте всё равно оставлен (лимиты API).

**Спека (утверждена юзером):** sandbox-first; автономия = ГИБРИД (мелкие сделки в пределах лимита авто, крупные — подтверждение кнопкой в TG); риск = старт консервативный, цифры тюним после первых прогонов; универсум = весь sandbox T-Invest с фильтрами ликвидности (анализатор расширить под него). Боевой токен — только «Только просмотр»→«Торговля», НИКОГДА «переводы».

**Фундамент уже есть:** signals (analyzer/gates/scheduler) дают готовые entry/stop/target/horizon/direction/confidence (калибровка 2020-2026, PF 1.40); backtest/replay.py; feedback/learn.py + outcome_loop. Исполнителю ничего не «придумывать» — транслирует сигнал в ордер. Config-задел: `INVESTPILOT_TINKOFF_TOKEN` (env-префикс INVESTPILOT_).

**Статус:** Фазы 0 и 1 ЗАКРЫТЫ 2026-07-06. Фаза 1: `trading/executor.py` (LONG-only, бюджет 10% `sandbox_trade_budget_pct`, дубль-гард, flatten), таблица `trades`, кнопка «🤖 Исполнить в песочнице» на push-карточке (только при собранном executor), лог «sandbox-исполнитель: активен». ГРАБЛИ API: order_id обязан быть UUID (uuid5 от signal_id); `executedOrderPrice` = СУММА заявки, не цена за штуку. E2E-скрипты: `scripts/verify_tinvest_phase0.py`, `e2e_executor_sandbox.py`, `e2e_handler_button.py`. Микро-счёт: боевой старт будет с ~1000 ₽ → пол бюджета `trade_budget_floor_rub`=1000 (budget=min(free, max(10%·total, пол))); на 1к диверсификации нет by design, дорогие лоты честно отклоняются (LKOH лот 4239 ₽); e2e `e2e_micro_account.py`. Фаза 2 ЗАКРЫТА (та же ночь): `trading/risk.py` RiskGuard — лимит позиций (5), дневной стоп 2% от якоря дня (kv `risk:day_anchor`, MSK), /halt //resume (kv `risk:halt`), fail-closed, выходы не блокируются; pytest 70, живой e2e_risk_gates.py. Фаза 3 ЗАКРЫТА (та же ночь): гибрид-авто (`maybe_auto_execute`, порог `auto_trade_max_rub`=50к), outcome_loop продаёт позицию при закрытии сигнала (`_flatten_sandbox`, алерт при ошибке), reject-кнопка продаёт, вечерний бриф песочницы (`daily_brief`, фильтр по account_id); pytest 70, живой e2e_phase3.py. Контур полный: скан→сигнал→(research-обогащение досье)→панель→авто/кнопка→риск-гейты→сделка→мониторинг→продажа (с реальным PnL в outcomes.real_pct)→бриф. Фаза 4 + research ЗАКРЫТЫ: real_pct отдельно от модельного realized_pct (D7-паритет с бэктестом); CebersResearch /v1/search в `analysis/research.py` (bearer=OMNIROUTE_API_KEY, fail-open 12с). Стоп-заявки в sandbox НЕДОСТУПНЫ (StopOrdersService→401 с sandbox-токеном) — брокерские стопы только на бою. До боя: 4-8 недель прогона; решение владельца. Фаза 0: sandbox-токен и account_id в `.env` (`INVESTPILOT_TINKOFF_SANDBOX_TOKEN` / `_ACCOUNT_ID`, счёт 45962d71-… (сброшен 2026-07-08 из-за bug-скоса в 88% ОФЗ; старый 58b1df38 закрыт), 500к виртуальных ₽, 1976 акций в справочнике, котировки живые). Токен юзер прислал в чат — при желании перевыпустить. Далее: Фаза 1 исполнитель ордеров → риск-контур → автономный цикл + CebersResearch-обогащение (retrieval-слой перед панелью, решения — за панелью) → 4–8 недель sandbox → решение о бое по данным (юзеру объяснено: неделя статистики не даёт, горизонт сигналов 2–16 недель).

**2026-07-10 (поздний вечер): v0.11 — горизонты часы→неделя + фьюч-шорты + вселенная 75 (D27).** Свинг = режим по умолчанию push/paper (`default_signal_mode`, 7 дн/trail 1.0, PF 1.41) — длинные 2–16 нед остаются контекстом, не сделкой; интрадей-капы 6/18. Вселенная: IMOEX 46 + топ-29 ликвидных TQBR (`iss.board_turnovers()` один вызов, порог 100 млн ₽/день, вес 0 → индекс-ядро портфеля не трогают); LLM-кап `scan_llm_cap=12` на скан (`analyze(llm_ok=False)` после капа = только детерминированное ядро, paper живёт). ШОРТ исполняется: `executor._execute_short` → `broker.future_for_asset(basicAsset, ближайшая экспирация)` → SELL контрактов по бюджету//ГО_sell; `flatten_signal` двусторонний (нетто→продать/откупить), real_pct шорта к нотионалу. ГРАБЛЯ: executor оперирует FIGI, брокер искал по тикеру — `_meta` резолвит оба (поймано интеграционным тестом). 126 тестов.

**2026-07-10 (вечер): PaperBroker v2 — T-Invest read-only как первичный источник (v0.10).** Юзер выдал read-only токен ПРОДА (`INVESTPILOT_TINKOFF_TOKEN` в `.env`, только там). Котировки real-time (ISS даёт задержку до ~15 мин — фолбэк), каталог всей площадки (357 руб.акций+ETF / 1527 облигаций / 463 фьючерса), статус торгов вкл. вечёрку фьючей. **Фьючерсы смоделированы честно:** цена в ПУНКТАХ — руб/пункт = `minPriceIncrementAmount/minPriceIncrement` из `GetFuturesMargin` (грабля всех самодельных симуляторов: «цена×лот» даёт мусорный P/L); покупка резервирует ГО (initialMarginOnBuy/Sell), не номинал; позиция знаковая (шорт естественен); вармаржа/клиринги НЕ симулируются — mark-to-market при оценке + фиксация при закрытии (осознанное упрощение для часы–неделя). Живой прогон: MXH7 ГО 28 107, цикл купить→закрыть вернул ГО до копейки. ОСТАЛОСЬ (согласовано): горизонты часы→неделя (свинг основной), вселенная анализа с фильтрами ликвидности, сигналы+исполнение по фьючерсам (шорт через фьючи).

**⚠️ 2026-07-10: ОТВЯЗАНЫ ОТ T-Invest sandbox — свой брокер `PaperBroker` (D26).** Их sandbox лёг стороной на дни (gRPC-13 `70001` на ВСЕХ sandbox-эндпоинтах; MarketData тем же токеном жив → их подсистема, не наш токен/код — разведено прямым сравнением). `trading/paper_broker.py` — локальный брокер-песочница: бот набирает виртуальный портфель на РЕАЛЬНЫХ ценах MOEX (`iss.market_data` по доске TQBR/CETS/TQOB), состояние в НАШЕЙ БД (kv `paper:cash` старт 500к + таблица `paper_positions`). Drop-in по поверхности sandbox-клиента (те же `get_sandbox_portfolio`/`post_sandbox_order`/`last_prices`/`trading_status`/`shares`/`bonds`/`currencies` + формы `{units,nano}`) → executor и его тесты НЕ тронуты; в `main.py` инжектится `PaperBroker(iss,db,settings)` вместо `TinvestClient` (T-Invest creds больше не нужны). Облигации в % номинала (×1000/100). Статус сессии = реальный MOEX `TRADINGSTATUS='T'`. Живой прогон: с 500к сам набрал ОФЗ 48/акции 28/подушка 17 на настоящих котировках, 8 позиций. Тесты `test_paper_broker.py` (7), verify.sh проверяет «брокер активен + paper_positions≥1» (мёртвый PHASE0 убран). Обучение на рекомендациях (outcome-петля на ISS) от sandbox НЕ зависело — идёт как шло. T-Invest REST-клиент (`tinvest_client.py`) остаётся в дереве как задел под БОЕВОЙ контур (full-токен, отдельное решение), но в проде не инстанцируется.

**Осведомлённость о часах биржи (2026-07-08):** биржа НЕ круглосуточна и секции живут по-разному (валюта нет утренней сессии, акции/ОФЗ торгуются с ~07:00 утренней сессии MOEX). НЕ хардкодить часы — спрашивать `trading_status(figi)` (MarketDataService/GetTradingStatus) + `is_tradable_now` (NORMAL_TRADING+apiTradeAvailableFlag+marketOrderAvailableFlag). `buy_market` пропускает закрытую секцию через `ExecResult(session_closed=True)` — тихий ретрай, не ошибка. Боевой урок: авто-портфель стоял на ВЕЧЕРНЕМ скане (19:15) → валюта/акции уже закрыты (30079) → перенесён на УТРЕННИЙ (10:15). kv-гейт autoportfolio ставится только при успешной сборке.
