feat(analytics): брокерские потоки по месяцам и порядок шагов пересчёта

cashflow_broker читает event заново по EXTERNAL_FLOW_KINDS, а не агрегирует готовый
external_flow_rub. Дневная серия неттит потоки по (счёт, валюта, день) ДО конвертации,
поэтому пополнение и вывод одного дня схлопываются, и разбивку из неё не восстановить:
на живых данных так спрятано 512 550 ₽ выводов, и все 40 месяцев выглядели бы как
«только пополнения». Правила чтения скопированы из valuation._load_deltas один в один,
поэтому net сходится с external_flow_rub по всем 40 месяцам до последнего знака.

Месяц без потоков строки не порождает: разрежённый ряд позволяет клиенту отличить
«ничего не было» от «вышло в ноль», а дорисовать нули он может сам.

Порядок шагов: fx → classify → matching → corpactions → lots → … → cashflow_broker →
networth → …. matching строго ПОСЛЕ classify, потому что classify пересчитывает
flow_type всех транзакций с нуля из правил и затёр бы internal_transfer, проставленный
линковкой; и строго ДО networth и cashflow, которые этот flow_type читают. corpactions
строго ДО lots: rebuild._split_ratios берёт коэффициенты из corporate_action.
This commit is contained in:
Dmitry
2026-09-18 15:06:19 +03:00
parent eb4eae5beb
commit 1ceeba2c48
3 changed files with 384 additions and 1 deletions
+11 -1
View File
@@ -2,7 +2,8 @@
The steps are registered on `metrics.refresh.STEPS` in the order they must run:
fx -> classify -> networth -> cashflow -> spending -> runway -> quality
fx -> classify -> matching -> corpactions -> lots -> valuation -> returns -> allocation
-> cashflow_broker -> networth -> cashflow -> spending -> runway -> quality
Registration happens lazily from `refresh_all` (see `register_steps`) so importing
`fintracker.metrics.refresh` stays free of the analytics import graph.
@@ -103,6 +104,7 @@ def register_steps() -> None:
from fintracker.analytics import (
allocation,
cashflow,
cashflow_broker,
classify,
networth,
quality,
@@ -111,17 +113,25 @@ def register_steps() -> None:
spending,
valuation,
)
from fintracker.ledger.corporate_actions import rebuild_corporate_actions
from fintracker.ledger.matching import rebuild_flow_links
from fintracker.ledger.rebuild import rebuild_lots
from fintracker.metrics.refresh import register_step
register_step("fx", _step_fx)
register_step("classify", classify.rebuild_classification)
# matching must follow classify, never precede it: classify recomputes every flow_type
# from the rules, so a link's `internal_transfer` written earlier would be overwritten
register_step("matching", rebuild_flow_links)
# splits and amortisations are what lots consume, so they have to exist first
register_step("corpactions", rebuild_corporate_actions)
# lots need FX (cost in RUB at the open date) and feed every later valuation step
register_step("lots", rebuild_lots)
# valuation prices the positions the lots describe; returns reads the series it writes
register_step("valuation", valuation.rebuild_valuation)
register_step("returns", returns.rebuild_returns)
register_step("allocation", allocation.rebuild_allocation)
register_step("cashflow_broker", cashflow_broker.rebuild_cash_flow_broker)
register_step("networth", networth.rebuild_net_worth_daily)
register_step("cashflow", cashflow.rebuild_cash_flow_monthly)
register_step("spending", spending.rebuild_spending_by_category)