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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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