feat: performance attribution & benchmark-relative return reporting - #331
Merged
robertocarlous merged 2 commits intoAug 19, 2026
Merged
Conversation
Adds Brinson-style, benchmark-relative decomposition of a portfolio's (and a published strategy's) return into allocation and selection effects, linked across a 30/90-day window with Cariño smoothing. - src/analytics/attribution.ts: pure, zero-I/O Brinson attribution core (equal-weighted v1 benchmark, interaction folded into selection, Cariño logarithmic linking, null/unattributed degenerate handling — never Infinity/NaN), reusing buildDailyRateSeries's forward-fill policy for the benchmark side and the strategyMetrics value-series convention for the portfolio side. - src/jobs/attribution.ts + prisma models PortfolioAttribution / StrategyAttribution: precomputed, persisted rows (one query per run, not per subject), scheduled job wired into src/index.ts with graceful shutdown. - scripts/backfill-attribution.ts: on-demand recompute for deploy/backfill. - GET /api/v1/analytics/attribution: owner-scoped, reads the persisted row. - Strategy marketplace entries gain `vsBenchmark`, merged from StrategyAttribution without disturbing the existing SQL sort. - docs/PERFORMANCE_ATTRIBUTION.md, docs/openapi.yaml, and docs/STRATEGY_MARKETPLACE.md updated; anti-divergence test asserts attribution's value series agrees with strategyMetrics.bucketByInstant. Closes Neurowealth#320
getMarketplace now queries StrategyAttribution for vsBenchmark; the integration test's mocked db needed the same addition already made to the unit test's mock, or every marketplace request threw on the undefined model.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #320: a Brinson-style, benchmark-relative decomposition of a portfolio's (and a published strategy's) return into allocation and selection effects, precomputed and persisted like the rest of the analytics stack.
src/analytics/attribution.ts— pure, zero-I/O core: equal-weighted v1 benchmark (accepts an exogenous series so a real index can be swapped in later), Brinson allocation/selection with the interaction term folded into selection (documented, tested identity), Cariño logarithmic linking across daily periods, and null/unattributeddegenerate handling (neverInfinity/NaN). ReusesbuildDailyRateSeriesfor the benchmark's forward-fill and follows the same portfolio-value-series convention asstrategyMetrics.ts, verified by an anti-divergence test.src/jobs/attribution.ts+PortfolioAttribution/StrategyAttributionPrisma models — one bulk query per run (not per subject), scheduled job wired intosrc/index.tswith graceful shutdown, migration + hand-writtenrollback.sql.scripts/backfill-attribution.ts— on-demand recompute for deploy/backfill.GET /api/v1/analytics/attribution?window=30d|90d— owner-scoped, reads the persisted row;{ computed: false }(still 200) when nothing has been computed yet.vsBenchmark, merged fromStrategyAttributiononto the existingPublishedStrategyMetricpage without disturbing the SQL sort/DoS-prevention design.docs/PERFORMANCE_ATTRIBUTION.md(new),docs/openapi.yaml,docs/STRATEGY_MARKETPLACE.mdupdated.Test plan
tests/unit/analytics/attribution.test.ts— 20 fixture tests covering the Brinson identity, Cariño linking/reconciliation, degenerate cases (wipeouts, empty windows, missing benchmark data), and an anti-divergence check againststrategyMetrics.bucketByInstanttests/unit/strategy/service.test.ts—vsBenchmarkmerge behavior ingetMarketplacetests/integration/strategies.integration.test.ts— updated mock for the newStrategyAttributionquerynpm test(924/924),npm run lint,npm run format:check,npx tsc --noEmitrollback.sqlapplied and rolled back against a real local Postgres instancereconciliationGap~1e-17,reconciled: true) and correctly report a never-held benchmark sector's pure allocation effectdocs/openapi.yamlvalidated withredocly lint/bundle(same warning count asmain, all pre-existing)