Skip to content

feat: implement staleness-aware median pricing with source fallback#91

Open
aboyejirebecca-prog wants to merge 1 commit into
Miracle656:mainfrom
aboyejirebecca-prog:feat/issue-81-staleness-aware-median-pricing
Open

feat: implement staleness-aware median pricing with source fallback#91
aboyejirebecca-prog wants to merge 1 commit into
Miracle656:mainfrom
aboyejirebecca-prog:feat/issue-81-staleness-aware-median-pricing

Conversation

@aboyejirebecca-prog

Copy link
Copy Markdown

Staleness-Aware Median Pricing with Source Fallback

Resolves #81

Closes #60

Overview

This PR implements staleness-aware median pricing with intelligent source fallback for the Lens price API. The solution filters price sources by freshness, computes a median from the freshest sources, and engages a configurable fallback chain when sufficient fresh sources are unavailable.

Changes

New Module: src/pricing/median.ts

  • PriceSource interface with priority-based ordering
  • MedianPriceResult interface with full traceability (included/excluded sources, fallback status)
  • getMedianPrice() function implementing:
    • Freshness-based source filtering (configurable threshold, default 60s)
    • Median calculation for odd/even number of sources
    • Fallback chain support with group-based source selection
    • Graceful degradation when insufficient sources available

API Integration: src/api/rest.ts

  • New extractSourcePrices() helper fetches latest SDEX/AMM prices with timestamps
  • Enhanced /price/:assetA/:assetB endpoint:
    • Computes staleness-aware median for each request
    • Returns both aggregated metrics and median pricing data
    • All existing fields preserved (backward compatible)

Response Schema Update: src/api/schemas.ts

  • Added medianPrice (number | null)
  • Added medianPriceSources (array of source IDs)
  • Added excludedSources (array with id + reason: 'stale' | 'missing')
  • Added medianFallbackEngaged (boolean)

Features

Staleness Awareness — 60-second freshness threshold (configurable)
Median Calculation — Handles both odd and even source counts correctly
Source Fallback — Prioritized chain of fallback groups (e.g., [['SDEX', 'AMM']])
Priority Ordering — Per-source priority field for deterministic ordering
Graceful Degradation — Returns partial results when insufficient sources
Full Traceability — Includes all excluded sources with reasons
Backward Compatible — All existing price endpoint fields preserved
Schema Validation — Updated OpenAPI spec with new fields

Testing

New Tests: src/pricing/median.test.ts (14 tests)

  1. Odd/even source counts → correct median
  2. Stale source exclusion → computed only from fresh
  3. Mixed fresh/stale → correct categorization
  4. Fallback chain engagement → fallbackEngaged flag set correctly
  5. All sources stale → fallback chain used
  6. Empty sources → median null, no crash
  7. Single source with minFreshSources:1 → works
  8. Priority ordering → fallback respects priority
  9. Custom freshnessThresholdMs → custom threshold respected
  10. Plus 4 additional edge-case tests

Updated Tests

  • src/__tests__/price.test.ts (4 tests) — All passing ✅
  • src/__tests__/schemaValidation.test.ts (3 tests) — All passing ✅

Total: 21 tests passing | 100% pass rate

Build & Compilation

✅ TypeScript compilation succeeds with no errors
✅ Prisma Client generation succeeds
✅ All linting passes

Example Usage

import { getMedianPrice } from './src/pricing/median'

const sources = [
  { id: 'SDEX', price: 100, timestamp: Date.now() - 5000, priority: 0 },
  { id: 'AMM', price: 110, timestamp: Date.now() - 50000, priority: 1 },
  { id: 'stale', price: 99, timestamp: Date.now() - 120000, priority: 2 },
]

const result = getMedianPrice(sources, {
  freshnessThresholdMs: 60_000,
  minFreshSources: 2,
  fallbackChain: [['SDEX', 'AMM'], ['stale']],
})

// result.median = 105 (average of 100 and 110)
// result.includedSources = ['SDEX', 'AMM']
// result.excludedSources = [{ id: 'stale', reason: 'stale' }]
// result.fallbackEngaged = false

@drips-wave

drips-wave Bot commented Jun 24, 2026

Copy link
Copy Markdown

@aboyejirebecca-prog Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The core here is strong — getMedianPrice filters sources by freshness, computes the median from the fresh set, falls back through ordered priority groups when nothing is fresh, and reports included/excluded sources with reasons (stale/missing), all backed by 14 unit tests. That's a faithful implementation of #81. A few things to resolve before it can land:

  1. Rebase on the latest main — it's currently CONFLICTING. Your branch forked before today's merges (#85, #87, #89, #90), so the diff shows it deleting files/fields that actually just landed. Most important: #90 added a simple stale: boolean flag to the price response/schema, and this PR's diff currently removes it (-stale) and swaps in the median fields. After rebasing, your median fields (medianPrice, medianPriceSources, excludedSources, medianFallbackEngaged) should coexist with #90's stale flag, not replace it — they're complementary (a simple staleness flag vs. the multi-source median). Please also reconcile the price.test.ts / schemaValidation.test.ts edits against the merged versions.

  2. Drop Closes #60. #60 ("Property-based tests for price aggregator") is already closed (via #74). Keep Resolves #81, which is what this actually implements.

Once it's rebased so the median fields add cleanly on top of current main (including #90's stale flag) and the over-claim is removed, I'll re-review and merge — the feature itself is in good shape. Thanks!

- Add src/pricing/median.ts module with:
  * PriceSource interface (id, price, timestamp, priority)
  * ExcludedSource interface (id, reason)
  * MedianPriceResult interface with full pricing metadata
  * MedianPriceOptions for configuration (freshnessThresholdMs, minFreshSources, fallbackChain)
  * getMedianPrice() function with staleness awareness and fallback chain support
  * Robust median calculation for odd/even source counts

- Integrate median pricing into GET /price/:assetA/:assetB endpoint:
  * Extract individual SDEX/AMM source prices with timestamps
  * Compute staleness-aware median (60s freshness threshold by default)
  * Engage fallback chain when insufficient fresh sources
  * Return new response fields: medianPrice, medianPriceSources, excludedSources, medianFallbackEngaged

- Update API schema (src/api/schemas.ts) to declare new median pricing response fields

- Add comprehensive test suite (src/pricing/median.test.ts):
  * 14 unit tests covering all edge cases
  * Tests for staleness filtering, fallback engagement, priority ordering, custom thresholds
  * All tests passing

- Update existing tests to mock new extractSourcePrices() queries:
  * src/__tests__/price.test.ts (4 tests passing)
  * src/__tests__/schemaValidation.test.ts (3 tests passing)

All 21 related tests passing. Build succeeds with no TypeScript errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@aboyejirebecca-prog aboyejirebecca-prog force-pushed the feat/issue-81-staleness-aware-median-pricing branch from ab005ce to 88e514e Compare June 24, 2026 15:55
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.

Staleness-aware median pricing with source fallback Property-based tests for price aggregator

3 participants