Skip to content

Use completed UTC buckets for position analytics#610

Open
antoncoding wants to merge 1 commit into
masterfrom
codex/completed-utc-position-periods
Open

Use completed UTC buckets for position analytics#610
antoncoding wants to merge 1 commit into
masterfrom
codex/completed-utc-position-periods

Conversation

@antoncoding

@antoncoding antoncoding commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep 24H rolling; calculate 7D, 30D, 3M, and 6M from completed UTC days.
  • Extend the existing PositionDailyFlow paginator to fetch sparse user flows and market-day states together, instead of keeping a second analytics pipeline.
  • Skip raw transaction-history downloads on archive-capable chains, reuse daily flows for completed-period charts, and resolve exact UTC boundary blocks.
  • Show the calculation range only on the selected period badge tooltip.

Cleanup

  • Deleted the separate daily-analytics data source, query builder, and calculation utility introduced in the first revision.
  • Removed tooltip/chart prop experiments that did not affect correctness.
  • Kept only the historical end snapshot handoff required to prevent completed-period charts from ending at the live balance.
  • Net result: 191 fewer lines than the first PR revision.

Verification

  • npx ultracite fix
  • npx ultracite check
  • pnpm check
  • pnpm build
  • Live indexer cursor test across two pages for both flow and market rows
  • High-activity 6M case (0xfe6509875528e7ea210127fad61800d1fd0d77bd, Optimism): 303 flow rows + 863 market-day rows, one analytics request, ~381 KB
  • Deterministic checks for deposit/withdraw earnings, time-weighted exposure, and negative earned values

@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
monarch Ready Ready Preview, Comment Jul 14, 2026 5:51pm

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@antoncoding, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a4a9f577-f4ab-42ff-b2e2-a6768f9be294

📥 Commits

Reviewing files that changed from the base of the PR and between 0a5a36b and 1241b0d.

📒 Files selected for processing (23)
  • docs/TECHNICAL_OVERVIEW.md
  • docs/VALIDATIONS.md
  • src/data-sources/monarch-api/index.ts
  • src/data-sources/monarch-api/position-daily-flows.ts
  • src/features/position-detail/components/history-tab.tsx
  • src/features/position-detail/position-view.tsx
  • src/features/positions/components/adapter-managed-exposure.tsx
  • src/features/positions/components/positions-period-settings.tsx
  • src/features/positions/components/supplied-markets-detail.tsx
  • src/features/positions/components/supplied-morpho-blue-grouped-table.tsx
  • src/features/positions/components/user-positions-chart.tsx
  • src/features/positions/positions-view.tsx
  • src/features/vault/components/vault-adapter-position-overview.tsx
  • src/features/vault/vault-view.tsx
  • src/graphql/envio-queries.ts
  • src/hooks/queries/useBlockTimestamps.ts
  • src/hooks/queries/usePositionDailyAnalyticsQuery.ts
  • src/hooks/usePositionsWithEarnings.ts
  • src/hooks/useUserPositionsSummaryData.ts
  • src/hooks/useVaultHistoricalApy.ts
  • src/utils/blockEstimation.ts
  • src/utils/earnings-period.ts
  • src/utils/interest.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/completed-utc-position-periods

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@antoncoding antoncoding force-pushed the codex/completed-utc-position-periods branch from 4bbedee to 32855b5 Compare July 14, 2026 16:47

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a daily analytics calculation mechanism for user positions, fetching daily flows and market snapshots via a new Envio GraphQL query to calculate earnings and APY without relying on heavy transaction history. The UI and hooks have been updated to support completed UTC days analytics periods. The review feedback highlights a potential bug in the daily analytics calculation where unaligned timestamps could break bucket lookups, and identifies type-safety issues where numeric chain IDs are implicitly converted to string keys via Object.fromEntries.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/utils/position-daily-analytics.ts Outdated
Comment on lines +94 to +120
for (let bucketStart = startTimestamp; bucketStart < endTimestamp; bucketStart += SECONDS_PER_DAY) {
const bucketEnd = bucketStart + SECONDS_PER_DAY;
const flow = flowByBucket.get(bucketStart);
const exposure = getCompletedFlowExposure(flow, currentShares, bucketEnd);
const endMarketSnapshot = marketSnapshotByBucket.get(bucketStart) ?? latestMarketSnapshot;
const startMarketSnapshot = latestMarketSnapshot ?? endMarketSnapshot;

if (exposure.weightedSharesSeconds > 0n) {
if (startMarketSnapshot && endMarketSnapshot) {
const startWeightedAssets = toWeightedAssetsSeconds(exposure.weightedSharesSeconds, startMarketSnapshot);
const endWeightedAssets = toWeightedAssetsSeconds(exposure.weightedSharesSeconds, endMarketSnapshot);
weightedSuppliedAssets += (startWeightedAssets + endWeightedAssets) / 2n;
} else {
weightedSuppliedAssets += getFallbackWeightedAssetsSeconds({
weightedSharesSeconds: exposure.weightedSharesSeconds,
startingBalance,
startingShares,
endingBalance,
endingShares,
});
}
}

effectiveTime += exposure.activeSeconds;
currentShares = exposure.closingShares;
latestMarketSnapshot = endMarketSnapshot;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The daily analytics calculation loops through days using bucketStart += SECONDS_PER_DAY starting from startTimestamp. If startTimestamp or endTimestamp are not perfectly aligned to UTC midnight (e.g., due to block timestamp estimation or clock drift), bucketStart will not match the keys in flowByBucket and marketSnapshotByBucket (which are strictly aligned to UTC midnight multiples of 86400). This will cause the lookups to return undefined and break the calculations.

Aligning startTimestamp and endTimestamp to UTC midnight boundaries inside the function makes the daily bucket lookups robust against unaligned inputs.

  const alignedStart = Math.floor(startTimestamp / SECONDS_PER_DAY) * SECONDS_PER_DAY;
  const alignedEnd = Math.floor(endTimestamp / SECONDS_PER_DAY) * SECONDS_PER_DAY;

  for (let bucketStart = alignedStart; bucketStart < alignedEnd; bucketStart += SECONDS_PER_DAY) {
    const bucketEnd = bucketStart + SECONDS_PER_DAY;
    const flow = flowByBucket.get(bucketStart);
    const exposure = getCompletedFlowExposure(flow, currentShares, bucketEnd);
    const endMarketSnapshot = marketSnapshotByBucket.get(bucketStart) ?? latestMarketSnapshot;
    const startMarketSnapshot = latestMarketSnapshot ?? endMarketSnapshot;

    if (exposure.weightedSharesSeconds > 0n) {
      if (startMarketSnapshot && endMarketSnapshot) {
        const startWeightedAssets = toWeightedAssetsSeconds(exposure.weightedSharesSeconds, startMarketSnapshot);
        const endWeightedAssets = toWeightedAssetsSeconds(exposure.weightedSharesSeconds, endMarketSnapshot);
        weightedSuppliedAssets += (startWeightedAssets + endWeightedAssets) / 2n;
      } else {
        weightedSuppliedAssets += getFallbackWeightedAssetsSeconds({
          weightedSharesSeconds: exposure.weightedSharesSeconds,
          startingBalance,
          startingShares,
          endingBalance,
          endingShares,
        });
      }
    }

    effectiveTime += exposure.activeSeconds;
    currentShares = exposure.closingShares;
    latestMarketSnapshot = endMarketSnapshot;
  }

Comment on lines +393 to +396
const endTimestampsByChain = useMemo(
() => Object.fromEntries(Object.entries(earningsRangesByChain).map(([chainId, range]) => [chainId, range.endTimestamp])),
[earningsRangesByChain],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Object.fromEntries on Object.entries converts the numeric chain IDs into string keys in the resulting object. This can lead to implicit type conversions or TypeScript type-checking issues when passing endTimestampsByChain to functions expecting Record<number, number>. Constructing the record with explicit numeric keys is safer and cleaner.

  const endTimestampsByChain = useMemo(() => {
    const result: Record<number, number> = {};
    for (const [chainId, range] of Object.entries(earningsRangesByChain)) {
      result[Number(chainId)] = range.endTimestamp;
    }
    return result;
  }, [earningsRangesByChain]);

Comment thread src/features/vault/vault-view.tsx Outdated
Comment on lines 141 to 144
const endTimestampsByChain = useMemo(
() => Object.fromEntries(Object.entries(earningsRangesByChain).map(([rangeChainId, range]) => [rangeChainId, range.endTimestamp])),
[earningsRangesByChain],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Object.fromEntries on Object.entries converts the numeric chain IDs into string keys in the resulting object. This can lead to implicit type conversions or TypeScript type-checking issues when passing endTimestampsByChain to functions expecting Record<number, number>. Constructing the record with explicit numeric keys is safer and cleaner.

  const endTimestampsByChain = useMemo(() => {
    const result: Record<number, number> = {};
    for (const [chainId, range] of Object.entries(earningsRangesByChain)) {
      result[Number(chainId)] = range.endTimestamp;
    }
    return result;
  }, [earningsRangesByChain]);

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