docs: состояние проекта — расписание синков, порядок шагов метрик, отключённые счета, ручные события
This commit is contained in:
@@ -102,7 +102,7 @@ just revision "msg" # новая alembic-миграция из изме
|
||||
Импорт CSV на живую базу ещё не запускался — это на живых данных не проверено, только на
|
||||
фикстурах.
|
||||
|
||||
Фаза 3 завершена и прогнана на живых отчётах (399 backend-тестов, 27 flutter):
|
||||
Фаза 3 завершена и прогнана на живых отчётах:
|
||||
|
||||
- `sources/reports/` — протокол `ReportParser` + три парсера: Сбер (HTML), ВТБ (xlsx),
|
||||
универсальный CSV (экспорт Snowball). `registry.py` выбирает парсер по содержимому файла,
|
||||
@@ -149,9 +149,9 @@ just revision "msg" # новая alembic-миграция из изме
|
||||
source_id)`, так что строки обоих источников сосуществуют, и приоритет можно поменять без
|
||||
ресинка истории. Амортизация из MOEX идёт не в `corporate_action`, а в
|
||||
`bond_nominal_schedule` — этим типом безраздельно владеет `ledger/corporate_actions.py`;
|
||||
оба источника зарегистрированы (`tinvest_events`, `moex_payouts`), но не добавлены в
|
||||
`worker/jobs.default_schedule()` — как и сами `tinvest`/`moex`, они туда не входили и до
|
||||
этой фазы, расписание синков за её рамками;
|
||||
оба источника зарегистрированы (`tinvest_events`, `moex_payouts`) и стоят в
|
||||
`worker/jobs.default_schedule()` вместе с `tinvest`/`moex`; источники с `needs="tinvest_token"`
|
||||
не попадают в расписание, пока токен не задан;
|
||||
- `analytics/income.py` — `metric_income_monthly` (факт, только confirmed) и
|
||||
`metric_income_calendar` (прошлое и прогноз) с явным `basis` (`paid` / `announced` /
|
||||
`history`) на каждой строке — три источника числа никогда не смешиваются в одно;
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ sources/* ──sync──▶ raw_* (JSONB, идемпотентно) ──map
|
||||
worker (APScheduler, advisory locks) ▼
|
||||
ledger (лоты FIFO, дедуп, матчинг ZM↔брокер)
|
||||
│
|
||||
analytics (polars, pyxirr) ──▶ metric_* ──▶ api ──▶ Flutter
|
||||
analytics (pyxirr) ─────────▶ metric_* ──▶ api ──▶ Flutter
|
||||
```
|
||||
|
||||
- `backend/src/fintracker/sources/` — по одному пакету на источник, контракт в `base.py`.
|
||||
@@ -37,5 +37,5 @@ worker (APScheduler, advisory locks) ▼
|
||||
- [links.md](links.md) — внешние API и референсы.
|
||||
- [offline-cache.md](offline-cache.md) — офлайн-кэш Flutter-клиента: контракт `Cached<T>`,
|
||||
`CacheInterceptor`, баннер «данные на …» (фаза 5).
|
||||
- [design-system.md](design-system.md) — визуальный язык Flutter-клиента: токены темы,
|
||||
`SectionHeader`/`TileCarousel`, группированный `NavSidebar` (обкатано на Обзоре).
|
||||
- [design-system.md](design-system.md) — визуальный язык Flutter-клиента по образцу Snowball: токены
|
||||
темы, верхняя панель навигации, сетка карточек на Обзоре, таблица активов, вкладки Аналитики.
|
||||
|
||||
+31
-10
@@ -1,7 +1,7 @@
|
||||
# Архитектура
|
||||
|
||||
Полный проект с обоснованиями — `docs/ai/plan.md`. Здесь — то, что нужно держать в голове
|
||||
при изменении кода. Таблицы, которых ещё нет в `models/`, помечены *(план)*.
|
||||
при изменении кода. Всё описанное ниже реализовано; чего в коде нет, здесь не упоминается.
|
||||
|
||||
## Домен
|
||||
|
||||
@@ -10,15 +10,23 @@
|
||||
`source` + `source_id` уникальны. `mirror_of_account_id` помечает ZM-счёт, который лишь
|
||||
зеркалит брокерский (исключается из net worth). `primary_event_source` — чей леджер
|
||||
для этого счёта истина; события других источников становятся `shadow`.
|
||||
- `account.disabled` — переключатель пользователя («Активен» на экране Счета); синки его не пишут.
|
||||
Отключённый счёт выпадает из скоупов (`valuation.account_scopes`: all, account, portfolio), из
|
||||
капитала ZM-счетов и из подсказок импорта; события и транзакции остаются в леджере.
|
||||
- Ручные события: `POST /events` (`source = manual`, всегда `confirmed`, знаки выводятся из вида
|
||||
события) и `DELETE /events/{id}` — только для `manual`; брокерские события синк вернёт. Виды:
|
||||
buy, sell, transfer_in/out, dividend, coupon, interest, deposit, withdrawal, commission, tax,
|
||||
tax_refund. Комиссия входит в `amount`. После записи нужен `POST /metrics/refresh` (лоты).
|
||||
- `portfolio` ↔ `account` m:n через `portfolio_account`; составной портфель = все счета.
|
||||
- `account_link` — явная карта «ZM-счёт → брокерский счёт» для матчинга переводов.
|
||||
|
||||
### Инструменты (`models/instruments.py`)
|
||||
- `instrument` с частичными UNIQUE по `isin`, `figi`, `tinvest_uid`, `(ticker, board)`.
|
||||
Резолв: ISIN → FIGI → tinvest_uid → (ticker, board) → `instrument_alias`.
|
||||
- Нераспознанные из отчётов → `pending_instrument` *(план)*, подтверждает пользователь.
|
||||
- Нераспознанные из отчётов → `pending_instrument`, подтверждает пользователь
|
||||
(`/instruments/pending`).
|
||||
|
||||
### Леджер *(план, фаза 2)*
|
||||
### Леджер (`models/ledger.py`)
|
||||
- `event` — единая таблица событий всех брокерских источников: buy/sell/dividend/coupon/
|
||||
tax/commission/deposit/withdrawal/transfer_in/out/split/amortization/repayment/fx_exchange.
|
||||
`quantity` знаковый, `amount` — знаковый денежный эффект на счёт, `dedupe_key` UNIQUE,
|
||||
@@ -35,8 +43,12 @@
|
||||
|
||||
Реализованы: `zenmoney` (diff-курсор, два режима auth: статический токен или OAuth-ротация
|
||||
через `source_credential`; маппер всегда пересобирает core из полных `raw_*`), `cbr`
|
||||
(валюты берутся из счетов и транзакций, `trust_env=False` — мимо прокси). Расписание —
|
||||
`worker/jobs.py`.
|
||||
(валюты берутся из счетов и транзакций, `trust_env=False` — мимо прокси), `tinvest`
|
||||
(gRPC: счета, операции, инструменты, снапшоты для сверки) и `tinvest_events` (дивиденды и
|
||||
купоны), `moex` (котировки по бумагам из леджера) и `moex_payouts` (выплаты ISS). Расписание —
|
||||
`worker/jobs.py`. Отчёты брокеров (`sources/reports/`: Сбер HTML, ВТБ xlsx, универсальный
|
||||
CSV) — не синк, а загрузка через `/imports`; `ledger/report_import.py` показывает превью и
|
||||
пишет события только на commit.
|
||||
|
||||
Контракт `sources/base.py`: `Source.sync(SyncContext) -> SyncResult`. Источник пишет
|
||||
`raw_*` идемпотентно и возвращает новый курсор; всё остальное (lock, журнал, курсор,
|
||||
@@ -45,15 +57,24 @@
|
||||
## Worker (`worker/`)
|
||||
Отдельный процесс: APScheduler по расписанию из `worker/jobs.default_schedule()` +
|
||||
опрос `sync_job` каждые 5 с. На источник — advisory lock `sync:<name>`, поэтому ручной и
|
||||
плановый запуски не пересекаются. После синка, изменившего данные, — refresh метрик
|
||||
*(план)*.
|
||||
плановый запуски не пересекаются. После синка, изменившего данные, — `refresh_all`
|
||||
(ошибка пересчёта помечает сам синк как error). Ручной пересчёт метрик тоже идёт через
|
||||
`sync_job` (см. ниже).
|
||||
|
||||
## Аналитика (`analytics/`, `pricing/{fx,prices}.py`, `metrics/refresh.py`)
|
||||
Шаги регистрируются в `analytics/__init__.py` и выполняются `refresh_all` по порядку:
|
||||
`fx → classify → lots → valuation → returns → allocation → networth → cashflow → spending →
|
||||
runway → quality` (фаза 4 вставит income и rebalance после allocation). Запуск:
|
||||
`fx → classify → matching → corpactions → lots → valuation → returns → benchmarks →
|
||||
allocation → rebalance → cashflow_broker → income → tax → networth → cashflow → spending →
|
||||
runway → shadow_dedupe → report_reconcile → quality`. Порядок объяснён комментариями в
|
||||
`register_steps` — например, `matching` обязан идти после `classify`, а `quality` — последним,
|
||||
потому что забирает находки остальных шагов. Запуск:
|
||||
worker после синка с `changed=True`, `fintracker metrics refresh`, `POST /metrics/refresh`,
|
||||
`POST /rules/apply`. Каждая `metric_*` таблица пересобирается целиком. Net worth считается
|
||||
`POST /rules/apply`. `POST /metrics/refresh` только ставит задачу в `sync_job` (`source =
|
||||
METRICS_JOB`) и отвечает 202 — пересчёт делает worker; клиент опрашивает `GET /metrics/status`,
|
||||
пока `refreshing` не станет false. Шаги коммитятся по одному: при падении `metric_refresh_log`
|
||||
хранит `failed_step` и `step_timings`, а `/metrics/status` отдаёт `consistent: false`, пока
|
||||
следующий полный пересчёт не пройдёт. (`/rules/apply` и коммит импорта по-прежнему считают
|
||||
синхронно.) Каждая `metric_*` таблица пересобирается целиком. Net worth считается
|
||||
от текущего `account.balance` назад по транзакциям; конвертация — `FxTable` по дате
|
||||
операции, цены — `PriceTable` (протяжка вперёд, `STALE_AFTER_DAYS = 10`, назад не тянем).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user