GetInstrumentBy отдаёт общую запись, в которой поля sector нет вовсе — из-за этого
сектор не был заполнен ни у одного инструмента, и аллокация по нему показывала один
бакет unknown на все двадцать. Сектор живёт только в Shares/Bonds/Etfs, поэтому
резолв добивает недостающие поля из потипового справочника: по uid, затем figi, затем
ticker+board. Логика разрешения uid не тронута — одна бумага приезжает под несколькими
instrument_uid, и InstrumentAlias с _match_by_identity остаются единственным местом,
которое это разбирает.
Справочник тянется лениво и один раз на экземпляр клиента, и только для встреченных
типов: это 2000-5000 записей на тип и около 14 секунд на все три.
_backfill_sectors нужен отдельно от резолва: у уже известных бумаг uid не попадает в
missing, и без отдельного прохода они никогда бы не переразрешились.
Пустой сектор остаётся NULL, строка "unknown" не пишется никогда — это литерал самой
аллокации, и данные, притворяющиеся ответом, отличить от отсутствия ответа нельзя.
Из 80 инструментов сектор заполнен у 73. Остальные семь честно пустые: валюта, пять
фондов, у которых поле пусто у самого T-Invest, и внебиржевой структурный продукт.
ISS без ключа: история по доске, текущие котировки, метаданные бумаг. Облигации
приходят в процентах от номинала, поэтому price_daily хранит и price_pct как
опубликовано, и close как денежную величину, плюс НКД рядом.
SDK t_tech.invest импортируется ровно в client.py: остальной код видит обычные
Decimal и dataclass'ы. Операции читаются по курсору GetOperationsByCursor,
снапшоты GetPortfolio/GetPositions складываются в position_snapshot и
cash_snapshot — только для сверки, аналитика их не читает.
mapper.py — явный словарь OperationType -> EventKind на все значения enum SDK,
покрытый тестом: новый тип должен ломать тест, а не молча уезжать в other.
Покупка с привязанной карты помечается meta.card_funded: деньги пришли снаружи,
баланс счёта их не видел, и для доходности это внешний поток, а не внутреннее
движение.
Source.sync(ctx) -> SyncResult пишет только raw_* и возвращает курсор; локи,
журнал, ошибки и продвижение курсора берёт на себя worker/runner.
ZenMoney читается единственным доступным способом — POST /v8/diff/ по
serverTimestamp; токен живёт сутки, поэтому worker ротирует refresh_token через
source_credential. Маппер всегда пересобирает core из полных raw_*, так что
удаление в ZenMoney исчезает и у нас.
ЦБ ходит мимо прокси (trust_env=False) и отдаёт cp1251 с делением на Nominal.
Курсы только по рабочим дням — протяжку по календарю делает аналитика.
Планировщик — APScheduler в отдельном процессе, на источник advisory-лок
sync:<name>, чтобы ручной запуск не пересёкся с плановым.