Skip to content

feat: expose estimated grossAPR/grossAPY and explicit scope marker - #465

Open
matheus1lva wants to merge 8 commits into
mainfrom
matheus1lva/apr-refactor
Open

feat: expose estimated grossAPR/grossAPY and explicit scope marker#465
matheus1lva wants to merge 8 commits into
mainfrom
matheus1lva/apr-refactor

Conversation

@matheus1lva

@matheus1lva matheus1lva commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Estimated-APR publishers compute gross, net it, and publish only netAPR/netAPY; Kong had
nowhere to put gross. This adds optional grossAPR/grossAPY to performance.estimated
(schema, promotion, REST list, GraphQL) and replaces the debtRatio vault-vs-strategy
sentinel with a publisher-emitted isStrategy marker, keeping debtRatio as the legacy
fallback. Additive only: apr/apy still come from netAPR/netAPY, never gross, and no
publisher changes are required. apr/apy values stay identical; the observable output changes are
listed under Risk / impact.

How to review

  • Start at packages/ingest/helpers/apy-apr.ts: promoteEstimatedApr is the single
    promotion list, now shared by the vault path and fetchStrategyPerformance
    (packages/ingest/abis/yearn/3/vault/snapshot/hook.ts), which previously hand-rolled its
    own copy.
  • Then packages/lib/estimated-apr.ts: the scope-resolution subquery. No publisher emits
    isStrategy yet, so it reduces to the old debtRatio filter today.
  • Read-side gates: packages/lib/types.ts, packages/web/app/api/rest/list/db.ts,
    packages/web/app/api/gql/typeDefs/vault.ts. Zod default-strip means a missed gate drops
    gross silently; these three are the only whitelists. REST snapshot serves the untyped
    merged blob and needs no change.
  • Contract + legacy component tables: docs/estimated-apr.md (new).
  • Side effect worth checking: strategy rows with a null netAPR no longer write
    apr: null, which failed EstimatedAprSchema and froze composition (same class as fix(ingest): default estimated apr components so composition parse survives netAPR/netAPY-only rows #443).
  • Observable change for one publisher: yvUSD's grossAPR moves from components to the
    top level. The GraphQL components.grossAPR field stays declared but @deprecated.

Test plan

  • Automated: bun --filter web test (29/29), touched ingest specs
    apy-apr.spec.ts + hook.spec.ts (27/27) — counts from before the review-loop specs.
    Full ingest/lib suites have pre-existing failures from missing RPC .env, untouched by this diff.
  • Added: hook.spec.ts covers the composition label fallback, the null-value guard,
    and vault-scoped label winning over a newer unscoped emission;
    apy-apr.spec.ts covers promoteEstimatedApr zeros, null isStrategy without debtRatio,
    isStrategy walk-back to an older vault-scoped emission, and null netAPR on the vault path;
    packages/web/app/api/gql/typeDefs/vault.spec.ts asserts the new GQL fields;
    REST list spec asserts gross is kept when present and optional when absent.
    Review-loop specs were checked by reading, not executed.
  • Not run: katana-estimated-apr.containers.spec.ts and yvusd-estimated-apr.containers.spec.ts
    (both need live RPC env); katana
    edited to seed gross on STRATEGY only, SIBLING stays net-only as the fix(ingest): default estimated apr components so composition parse survives netAPR/netAPY-only rows #443 freeze guard.
  • Manual: diff one yvUSD, one katana, one crv vault snapshot before/after —
    apr/apy identical, gross absent until publishers emit it.

Risk / impact

  • Hot-path SQL change in LATEST_ROWS_BY_ESTIMATED_APR_SQL. Prod-shaped Neon
    (output Timescale 2.13, 141 chunks, 2 in the 7d window): chunk exclusion holds
    139/141 for old EXISTS, correlated bool_or, and grouped HAVING. The correlated
    bool_or SubPlan was the regression — planner cost ~257k vs ~2.5k, BitmapAnd of
    output_series_time_idx (~360k rows/chunk), yvUSD 628ms vs old 86ms. Split-EXISTS
    also stayed a Filter SubPlan. Uncorrelated GROUP BY/HAVING restores the
    series_time index seek (yvUSD 2.5ms, dual-role 2.4ms, crv 5.4ms) and matches old
    row sets. getLatestEstimatedAprLabel is already that index seek (1.3ms).
  • Frozen snapshots caused by null-netAPR strategy rows start composing again. More generally any
    null-valued estimated component is now omitted from components instead of written as null.
  • Dual vault/strategy addresses (Strategy performance.estimated can show yvUSD-scoped data in non-yvUSD vaults #409) whose only emission is strategy-scoped now resolve a composition
    label via getLatestEstimatedAprLabel, so their composition strategies gain performance.estimated
    blocks they did not have before. Output change for existing data, not gated on publishers.
  • getLatestEstimatedAprLabel adds one indexed round-trip (idx_output_chain_address_label_series_time
    prefix on chain_id/address) to the v3 snapshot hook for every vault with no vault-scoped estimate.
  • yvUSD: GraphQL components.grossAPR now resolves null (value promoted to estimated.grossAPR).
    Audited yearn.fi, yearn-powerglove, ydaemon via GitHub code search: none read
    estimated.components.grossAPR (powerglove's grossApr comes from oracle.apr / timeseries points).
    yearn.fi estimatedAprSchema does not whitelist estimated.grossAPR/grossAPY; Zod default-strip
    drops them until they add the fields. They keep reading apr/apy and REST components.katRewardsAPR.
  • GraphQL EstimatedAprComponents now resolves compoundingPeriodsPerYear, katRewardsAPR,
    debtRatio, and isStrategy (the typed object dropped them before). REST list/snapshot already
    passed those through a z.record. yearn.fi reads katRewardsAPR from REST snapshot components, not GQL.
  • compare-rest-prod-fork diffs list estimated.apr/apy/grossAPR/grossAPY. yvUSD will report
    grossAPR/grossAPY as missing vs prod until prod is deployed; that is the promotion, not a
    price regression.
  • Legacy v2 read path (getLatestEstimatedApr, crv/velo/aero) applies no scope rule; a v2 publisher
    emitting isStrategy would be ignored there. Out of scope, documented in docs/estimated-apr.md.
  • No DB migration, no cache invalidation; gross appears per vault as snapshot hooks rerun.
    Rollback = revert, nothing persisted in a new shape.

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
kong Ready Ready Preview Sep 4, 2026 12:33pm UTC

Request Review

…posure, docs, coverage

- fall back to unscoped emission label for composition when the vault-scoped
  lookup is empty (isStrategy marker on dual vault/strategy addresses)
- expose katRewardsAPR/debtRatio/isStrategy in gql EstimatedAprComponents
- diff performance.estimated in the prod/fork comparator
- document the frozen legacy v2 read path and reserved output components
- test gross promotion end to end in the yvusd containers spec
Cover promotion zeros, null isStrategy without debtRatio, vault-path null netAPR,
vault-scoped label over newer unscoped emission, and optional REST list gross.
Unique hook.spec addresses and mock fetch. Document new GQL component fields.
Null isStrategy is documented as absent in outputs.md, same as the SQL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant