Skip to content

fix: ReflectorService uses random price variation instead of static fallback when oracle is down #61

Description

@grantfox-oss

Problem

When the Reflector oracle is unavailable or returns stale data, ReflectorService in backend/src/services/reflector.ts falls back to random price variation instead of using the last known good price. This makes portfolio values jitter randomly whenever the oracle hiccups.

The fallback logic does something like:

// When Reflector is unavailable
const basePrice = lastKnownPrice || hardcodedPrice;
const variation = (Math.random() - 0.5) * 0.02; // ±1% random noise
return basePrice * (1 + variation);

This means:

  • Portfolio values fluctuate even when no trades happen
  • Rebalancing triggers can fire based on phantom price movements (the random variation might push a portfolio past its drift threshold)
  • Risk metrics (VaR, volatility) are computed on noise, not real data
  • The circuit breaker might trigger unnecessarily

Proposed Fix

1. Use last known price with no variation

When the oracle is down, return the last successfully fetched price with zero variation:

async getPrice(asset: string): Promise<PriceData> {
    try {
        const price = await this.fetchFromReflector(asset);
        this.cachePrice(asset, price); // store for fallback
        return price;
    } catch (error) {
        const cached = this.getLastKnownPrice(asset);
        if (cached) {
            logger.warn(`Reflector unavailable for ${asset}, using cached price from ${cached.timestamp}`);
            return { ...cached, source: 'cached', stale: true };
        }
        throw new Error(`No price data available for ${asset}`);
    }
}

2. Track staleness

Each cached price should have a timestamp. If the cached price is older than a configurable threshold (e.g., 1 hour), mark it as stale: true so downstream consumers can decide how to handle it:

  • Portfolio value calculations: use stale prices but show a "stale data" indicator
  • Rebalancing decisions: skip rebalancing if any asset price is stale (don't trade on bad data)
  • Risk metrics: pause VaR/volatility calculations if prices are stale

3. Cache prices in the database

Currently prices might only be cached in memory. Store them in PostgreSQL so they survive restarts:

CREATE TABLE IF NOT EXISTS price_cache (
    asset VARCHAR(32) PRIMARY KEY,
    price NUMERIC(20, 8) NOT NULL,
    source VARCHAR(32) NOT NULL,
    fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

The analyticsSnapshotWorker already runs periodically — have it also refresh the price cache.

4. Graceful degradation in the frontend

When the backend returns prices with stale: true, the frontend should:

  • Show a yellow warning banner: "Price data may be outdated"
  • Disable the "Rebalance Now" button
  • Still show portfolio value but with a lower opacity or asterisk

Files to modify

  • backend/src/services/reflector.ts — replace random variation with cached fallback, add staleness tracking
  • backend/src/db/ — add price_cache table and query methods
  • backend/src/services/rebalancing.ts — skip rebalancing when prices are stale
  • backend/src/services/riskManagements.ts — pause risk calculations when prices are stale
  • frontend/src/components/Dashboard.tsx — show stale data indicator
  • frontend/src/components/PriceTracker.tsx — show last-updated timestamp

Acceptance Criteria

  • ReflectorService returns cached price when oracle is down (no random variation)
  • Cached prices include a timestamp and staleness flag
  • Prices older than 1 hour are marked as stale: true
  • Rebalancing is skipped when any price is stale
  • Risk calculations are paused when prices are stale
  • Frontend shows a warning when using stale prices
  • Price cache persists across backend restarts (stored in PostgreSQL)
  • No phantom rebalancing triggers from random price noise

References

Affected Area

Backend, Frontend

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignbackendBackend relatedbugSomething isn't workinghelp wantedExtra attention is needed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions