Skip to content

fix(chart): prevent live mark ticks from mutating non-oracle series - #2442

Open
Bayyan16 wants to merge 2 commits into
dcccrypto:playgroundfrom
Bayyan16:fix/chart-live-mark-source-integrity
Open

fix(chart): prevent live mark ticks from mutating non-oracle series#2442
Bayyan16 wants to merge 2 commits into
dcccrypto:playgroundfrom
Bayyan16:fix/chart-live-mark-source-integrity

Conversation

@Bayyan16

Copy link
Copy Markdown
Contributor

Summary

Fixes #2441.

This PR restores chart source integrity by preventing live protocol mark-price
ticks from mutating a visible chart series backed by a different data source.

Before this change, TradingChart correctly selected one of four possible
series sources:

Percolator internal trades
  -> Pyth benchmark candles
  -> GeckoTerminal / DEX candles
  -> Oracle fallback

However, the live slab-price subscriber did not preserve that source boundary.

Every finite mark-price tick correctly updated the separate Mark overlay, but
the same tick was also unconditionally merged into the current visible OHLC bar
or line/area point through series.update().

As a result, a mark observation could overwrite:

  • a GeckoTerminal/DEX candle;
  • a Pyth benchmark candle;
  • a Percolator internal-trade candle;
  • a DEX/Pyth/Percolator line or area point.

This produced synthetic chart movement that never occurred in the source
feeding the chart.

On the affected Playground market, DEX candles were approximately
$0.010–$0.022, while the protocol mark was approximately
$0.000189–$0.0002. Applying the mark to the last DEX candle created an
artificial downward candle greater than 98%.


Root Cause

TradingChart stores the latest rendered value in generic refs:

lastBarRef.current
lastPointRef.current

These refs retain the current data shape, but the live tick handler previously
did not know which source produced the data stored in them.

The old OHLC update path was effectively:

const merged = {
  ...lastBarRef.current,
  high: Math.max(lastBarRef.current.high, markPriceUsd),
  low: Math.min(lastBarRef.current.low, markPriceUsd),
  close: markPriceUsd,
};

lastBarRef.current = merged;
series.update(merged);

The line/area path similarly replaced the last visible value:

const merged = {
  time: lastPointRef.current.time,
  value: markPriceUsd,
};

lastPointRef.current = merged;
series.update(merged);

Those operations were executed for every active series, even when the series
was populated from DEX, Pyth, or actual Percolator trades.

This violated the following invariant:

A rendered price series must only be mutated by observations belonging to
the same source domain.

The correct ownership model is:

DEX series
  <- GeckoTerminal / DEX observations only

Pyth series
  <- Pyth benchmark observations only

Percolator series
  <- actual trades:<slab> events only

Oracle fallback series
  <- mark/oracle observations

Mark overlay
  <- live protocol mark price

What Changed

1. Added an explicit chart-source model

A new ChartDataSource type identifies the source currently backing the
visible price series:

type ChartDataSource =
  | "percolator"
  | "pyth"
  | "dex"
  | "oracle";

The source is resolved using the same production-priority rules already used by
TradingChart:

resolveChartDataSource(
  hasPercolatorData,
  hasPythData,
  hasExternalData,
);

The priority remains unchanged:

Percolator -> Pyth -> DEX -> Oracle

This PR does not alter source selection. It only makes the selected source
explicit and available to the live-tick path.


2. Added source-aware live-mark merge helpers

The new pure helpers:

mergeMarkPriceIntoBar(...)
mergeMarkPriceIntoPoint(...)

apply a mark tick only when the active source is:

oracle

For non-oracle sources:

percolator
pyth
dex

the helpers return the original bar or point unchanged.

This preserves object identity intentionally, allowing TradingChart to skip
series.update() completely when the live mark does not own the active series.


3. Preserved live updates for the Oracle fallback

This fix does not disable live chart updates globally.

When the visible series is the Oracle fallback, the live mark continues to
update the current bar:

high  = max(previous high, mark)
low   = min(previous low, mark)
close = mark

The same behavior is retained for Oracle-backed line and area series.

Therefore:

Oracle fallback:
  remains live

DEX / Pyth / Percolator:
  remain owned by their original sources

4. Kept the Mark overlay live for every source

The separate Mark price line still updates on every valid live mark tick,
regardless of the source backing the chart.

This preserves the intended trading interface:

Historical or trade-derived series:
  source-specific market context

Mark overlay:
  current protocol mark and risk context

The fix separates those responsibilities instead of suppressing the mark
display.


5. Prevented a source-transition mismatch window

The active-source ref is committed only after the structural chart effect has
rebuilt the visible series.

It is not updated prematurely during render.

This matters during transitions such as:

DEX -> Oracle
Pyth -> DEX
Percolator -> Pyth

Updating the ref during render would briefly create this inconsistent state:

new source identity
+ previous visible series
+ incoming live tick

A live tick arriving in that window could still mutate the previous source
under the new source identity.

The ref is now synchronized with the series after the structural rebuild, so
the live subscriber observes the source corresponding to the series actually
mounted in seriesRef.current.


6. Reused the resolved source for viewport identity

The structural chart effect now uses the resolved activeDataSource when
building the fit key:

`${chartDataKind(chartStyle)}:${timeframe}:${activeDataSource}`

This removes duplicate source-resolution logic and ensures that viewport
identity and live-series ownership use the same source-of-truth.

The existing pan/zoom behavior remains unchanged:

  • the viewport refits when the timeframe, data shape, or source changes;
  • ordinary source polling does not continuously reset user pan/zoom.

Files Changed

app/components/trade/TradingChart.tsx

  • resolves the active chart source explicitly;
  • tracks the source committed to the currently mounted series;
  • synchronizes source identity after structural series reconstruction;
  • keeps the Mark overlay live;
  • prevents mark ticks from mutating DEX, Pyth, or Percolator series;
  • preserves mark-driven updates for the Oracle fallback;
  • skips unnecessary series.update() calls for unchanged values;
  • uses the same resolved source in the viewport fit key.

app/lib/chart-live-tick.ts

Adds pure source-integrity primitives:

  • ChartDataSource;
  • resolveChartDataSource;
  • mergeMarkPriceIntoBar;
  • mergeMarkPriceIntoPoint;
  • shared OHLC and single-value interfaces.

app/__tests__/lib/chart-live-tick.test.ts

Adds deterministic regression coverage for:

  • source-priority resolution;
  • DEX OHLC protection;
  • Pyth OHLC protection;
  • Percolator OHLC protection;
  • DEX line/area protection;
  • Pyth line/area protection;
  • Percolator line/area protection;
  • Oracle OHLC live updates;
  • Oracle line/area live updates;
  • non-finite mark rejection;
  • the observed greater-than-98% synthetic-drop scenario.

Behavioral Matrix

Active chart source Mark overlay updates Visible OHLC/point updated by mark
Oracle fallback Yes Yes
GeckoTerminal / DEX Yes No
Pyth Yes No
Percolator internal trades Yes No

Percolator candles continue to receive their live OHLC updates through actual
trades:<slab> events rather than generic protocol mark notifications.


Regression Scenario

Representative values from the affected deployment:

DEX close: 0.0122
Mark:      0.0002

The previous implementation produced:

open:  0.012x
high:  0.012x
low:   0.0002
close: 0.0002

Synthetic drop:

((0.0122 - 0.0002) / 0.0122) * 100
≈ 98.36%

After this change:

DEX close remains: 0.0122
DEX low remains:   source-provided value
Mark overlay:      moves independently to 0.0002

No synthetic candle is generated.


Validation

Targeted and related regression tests

cd app

pnpm exec vitest run \
  __tests__/lib/chart-live-tick.test.ts \
  __tests__/lib/chart-style.test.ts \
  --reporter=verbose

Result:

Test Files  2 passed
Tests       52 passed

The new source-integrity suite contains:

11 passed

TypeScript

pnpm exec tsc --noEmit

Result:

Passed with no TypeScript errors.

Formatting

pnpm exec prettier --check \
  lib/chart-live-tick.ts \
  __tests__/lib/chart-live-tick.test.ts

Result:

All matched files use Prettier code style.

Production build

pnpm build

Result:

Compiled successfully.
TypeScript configuration validation completed.
Static pages generated successfully.
Page optimization completed.

Existing non-blocking warnings related to Next.js configuration,
middleware naming, Node deprecations, and optional BigInt native bindings remain
unchanged and are unrelated to this PR.


Git validation

git diff --cached --check

Result:

No whitespace errors.

The branch contains one focused commit and only the three files required for
this fix.


Runtime Validation Note

The original issue was reproduced on the deployed Playground market:

https://percolator-playground.vercel.app/trade/98Em84STzK8sL81TRt3ePnFQ9hG5HsVrQKydZDMYibso

The local environment could load the on-chain slab and position state, but it
could not load the equivalent DEX chart because the local
/api/markets/[slab] request did not resolve the deployment-specific metadata
required by GeckoTerminal.

Local result:

error: Failed to load market. Please try again later.
symbol: null
mainnet_ca: null
dex_pool_address: null
mark_price: null

Playground result:

error: null
symbol: Jimoothy
name: Jimoothy/USDC - meteora-dlmm
mainnet_ca: present
dex_pool_address: present
mark_price: 0.000189

Because the DEX chart mint is supplied through mainnet_ca, the affected DEX
series could not be recreated in the local UI.

The cross-source mutation itself is covered deterministically by the regression
tests.

Deployment-level post-fix validation should be completed against the Vercel PR
preview using the affected slab.

This PR does not claim that the fix has already been deployed to or verified on
the live Playground environment.


Scope

This PR fixes frontend chart source integrity only.

It does not change:

  • on-chain protocol state;
  • mark-price calculation;
  • liquidation-price calculation;
  • trade execution;
  • PnL accounting;
  • order validation;
  • collateral handling;
  • wallet or signer behavior;
  • RPC configuration;
  • DEX/Pyth/Percolator source-selection priority.

The liquidation line observed in #2441 remains outside this fix because its
direction was consistent with a short position. Its compressed position on the
chart resulted from the large difference between the DEX price domain and the
position's Mark/Entry/Liquidation domain.


Acceptance Criteria

  • Mark overlay continues updating for all chart sources.
  • DEX OHLC candles are not mutated by mark ticks.
  • Pyth OHLC candles are not mutated by mark ticks.
  • Percolator OHLC candles are not mutated by generic mark ticks.
  • DEX line/area points are not overwritten by mark ticks.
  • Pyth line/area points are not overwritten by mark ticks.
  • Percolator line/area points are not overwritten by mark ticks.
  • Oracle-fallback OHLC bars continue receiving live mark updates.
  • Oracle-fallback line/area points continue receiving live mark updates.
  • Non-finite mark values remain rejected.
  • Source transitions do not expose a render-to-effect identity mismatch.
  • Related regression tests pass.
  • TypeScript validation passes.
  • Production build passes.
  • Verify the affected slab against the Vercel PR preview.

Related Work

This is distinct from previous chart fixes:

The values involved in this issue are valid finite numbers:

Number.isFinite(0.0122);   // true
Number.isFinite(0.000189); // true

The defect was not malformed data. It was a valid observation being applied to
a series owned by a different source.


Final Result

After this change:

Mark ticks update the Mark overlay.

Oracle-backed series continue updating from mark observations.

DEX-backed series remain owned by GeckoTerminal.

Pyth-backed series remain owned by Pyth.

Percolator-backed series remain owned by actual protocol trades.

This removes the synthetic giant-candle path while preserving the existing
source priority, live mark display, Oracle fallback behavior, and chart
interaction model.

@Bayyan16
Bayyan16 requested a review from dcccrypto as a code owner July 19, 2026 04:43
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

@Bayyan16 is attempting to deploy a commit to the Khubair Nasir's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc7e9ecc-6789-4214-a492-0156659a51cd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@dcccrypto

Copy link
Copy Markdown
Owner

Independent verification — not an approval (QA/Security own that), just evidence for whoever reviews.

I've been mutation-checking correctness PRs here because of a recurring failure mode in this repo: tests that pass without exercising the thing they're named for (11 confirmed instances, including one of my own). The helper logic in this PR is very well covered.

Method: removed only the source gate on the PR branch, restoring #2441 (mark ticks mutate every series):

if (source !== 'oracle' || !Number.isFinite(markPriceUsd)) return bar;
                        
if (!Number.isFinite(markPriceUsd)) return bar;

Result:

gate present → 11 passed
gate removed →  7 FAILED

and they're the right seven — all three non-oracle sources (percolator, pyth, dex) across both the OHLC-bar and line/area-point paths, plus the magnitude-documenting case. The four that stay green are the oracle-path and non-finite-guard cases, which correctly shouldn't move. That's a genuinely well-shaped test matrix.

One gap, same standard I applied to #2437: the coverage is entirely at the chart-live-tick.ts helper. TradingChart.tsx is also changed here — it's what calls resolveChartDataSource (line 455) and the two merge helpers (1123, 1134) — and no test touches TradingChart. So the mutation I ran can't distinguish "the component wires the guard correctly" from "the component stopped calling it".

That's milder than #2437's case because the helpers are pure and the wiring is small, so I'm not suggesting a full component test. But if the source resolution at line 455 were ever passed the wrong argument, every test here would stay green while the bug returned in the UI — which is exactly the shape of the original report.

Cheapest thing that would close it: one assertion that resolveChartDataSource is called with the same inputs the chart actually has, or a thin render test asserting a percolator-backed series is unchanged after a mark tick.

Not blocking — the fix is real and I verified it. Flagging for consistency with the standard I applied to the sibling PR.

@Bayyan16

Copy link
Copy Markdown
Contributor Author

Thank you for the mutation check and for distinguishing the helper coverage from the remaining component-wiring gap.

Agreed: the current suite proves the source gate itself, but it does not yet prove that TradingChart resolves and forwards the active source correctly to the live-tick merge path.

I’ll add the smallest focused component-level regression supported by the existing test harness, covering a non-oracle-backed series and verifying that a live mark tick does not update the visible series. I’ll also mutation-check the component wiring so the test fails if the active source is incorrectly treated as oracle.

I’ll keep the production logic unchanged and re-run the focused chart tests, TypeScript validation, build, formatting, and patch-integrity checks on the updated head.

@Bayyan16

Copy link
Copy Markdown
Contributor Author

Implemented the focused TradingChart component-wiring regression requested in the review.

The new test renders the actual component with Percolator data active and verifies that the final call to resolveChartDataSource receives:

true, false, false

I also mutation-checked the component wiring:

normal wiring:
1 passed

hasPercolatorData neutralized at the TradingChart call site:
1 failed
expected [true, false, false]
received [false, false, false]

source restored:
24 related tests passed

Additional validation on the updated head:

TypeScript: passed
Production build: passed
Prettier: passed
git diff --check: passed
Build & Fast Tests: passed
Merge Gate: passed

The production logic remains unchanged; the second commit is test-only.

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.

2 participants