fix: Price (USD) candles must match Last headline (invertUsd, not 1/x; sub-cent scale) #543

Closed
opened 2026-08-17 03:51:33 +00:00 by PlasticDigits · 20 comments
PlasticDigits commented 2026-08-17 03:51:33 +00:00 (Migrated from gitlab.com)

Summary

On /trade and /charts, the Last headline (tape USD of the displayed base) and the Price (USD) candlestick series disagree after #524.

Observed on columbus-5:

Pair (pill) Last (above chart) Chart last / Y-axis After switch-side
USTR/UST1 (UST1 = factory asset_0, default invert) 0.0122588 (correct USD of 1 USTR) ~1.06 (UST1-ish / 1 / price_usd) Chart ~$0.95 (UST1) — headline and candles then agree
cUSTC/UST1 (same layout) 0.0049 (correct USD of 1 cUSTC) ~0.93 (1 / ~1.07) Both ~$1.07 for UST1
cLUNC/UST1 (cLUNC = factory asset_0, no default invert) 0.000047 (correct USD of 1 cLUNC) 0.00 (default lightweight-charts 2-dp) Last 1.0 (UST1) but chart ~21260 (1 / 0.000047)

1 USTR ≈ $0.012258. The chart must never show ~$1 (or 1/usd) as if that were USTR, nor 21260 as if that were USD of 1 UST1.

This is one product surface: display USD of the displayed base on both the headline and the candle pane, including Y-axis precision. Do not split invert-math vs scale into separate issues.

Related: #524 (UI invert; T524-4 one orientation), #522 (factory price_usd = USD of 1 asset_0), #515 (USTC/LUNC oracles), #151 (non-negative USD scale), #226 (finite candles), #149 (Last prefers tape).

Current codebase

Factory pairs keep a fixed asset_0 / asset_1. Indexer math is factory-oriented; #524 inverts only in the dApp.

Layer Behavior today
Indexer #466 / #522 swap_events.price = human quote per base. price_usd = USD of 1 human asset_0 = price × USD(1 human quote). Quote catalog: UST1=$1, cUSTC=#515 USTC, USTR=2.5× USTC, cLUNC=#515 LUNC.
Candle storage rebuild_candles_from_swaps writes OHLC as COALESCE(se.price_usd, se.price) into a single candles.open/high/low/close. There is no parallel human OHLC on GET /candles. If price_usd is NULL, the USD-labeled chart plots the human ratio.
Headline (correct for tape) resolveDisplayTapeLastPriceUsd → when inverted, invertUsd(factoryUsd, humanPrice) = price_usd / human_quote_per_base (USD of 1 display base). Else factory price_usd. resolveTradeChartHeadlineUsd prefers that tape string over last candle close.
Candle invert (wrong for USD) PriceChart maps indexer rows with indexerCandlesToChartPoints then applyChartDisplayInvert → invertOhlc = naive 1/x + high/low swap. That is correct for a human quote-per-base series, not for a USD-of-asset_0 series. 1 / $1.06 ≈ $0.94, not $0.012 USTR. 1 / $0.000047 ≈ 21260, not $1 UST1.
Y-axis PriceChartLightweightCanvas sets candlestick colors + #151 autoscale clamp. It does not set priceFormat.precision / minMove. TradingView lightweight-charts defaults to 2 decimal places, so $0.000047 prints as 0.00.
Orientation defaultDisplayInverted = UST1 is asset_0 and the other leg is not. UST1/USTR and UST1/cUSTC default to USTR/UST1 and cUSTC/UST1. cLUNC/UST1 (cLUNC = asset_0) stays factory-oriented until the pill is clicked.
Sync Invert rewrite of historical OHLC at the same times must setData (not series.update() from the oldest bar) — #336 / T524-6.

Why the three reports are the same bug

  • UST1-as-base + default invert (USTR, cUSTC): Last uses invertUsd (other-token USD). Candles use 1 / price_usd (still ~$1). Switch-side turns invert off, so Last and candles both show factory UST1 USD and appear to “fix” the chart.
  • Other-as-base (cLUNC/UST1): Last shows cLUNC USD (0.000047); candles are the same magnitude but the axis shows 0.00. Switch-side inverts: Last uses invertUsd → ~$1 UST1; candles use 1/x → ~21260 (human cLUNC per UST1), not dollars.

P522-2: price_usd / price is USD of 1 human quote. That is the invert the headline already implements. Candles must use the same identity, not 1 / price_usd.

Why this is needed

Traders read Last and the candle pane as one Price (USD) control (T524-4). Today they disagree by 1–2 orders of magnitude (USTR/cUSTC) or five (cLUNC). That is not a cosmetic invert: it is a false market.

  1. Default USTR/cUSTC view looks like a ~$1 UST1 chart while the headline correctly prints the floating token.
  2. cLUNC looks “worthless” (0.00) until invert, then looks like a 20k print — both wrong as USD of the displayed base.
  3. Existing #524 tests lock in invertOhlc as 1/x (priceChartCandles.test.ts “reciprocates…”). That helper is valid for human book/limit prices, not for the USD candle series.

Constraints / guardrails

  1. T524-1 — indexer factory meaning unchanged. Do not change #466 / #522 swap_events.price / price_usd or CG/CMC last_price. Invert stays a display of factory series. Additive candle fields (human OHLC alongside USD) are allowed if required for per-bar invertUsd; do not replace factory USD with a reciprocal in storage.
  2. T524-2 — on-chain stays factory. Do not change place_limit_order / market submit convert-on-submit. This issue is chart + headline + axis only.
  3. Same formula as the headline. Display USD of 1 displayed base = factory price_usd when not inverted; when inverted = invertUsd(price_usd, human_quote_per_base) = price_usd / price. Never 1 / price_usd on a USD series. Never plot human quote-per-base on a control labeled Price (USD) (P522-5).
  4. Per-bar invert. Do not scale every historical bar by the latest tape human price (flattens / distorts history). Each bar needs that bar’s human quote-per-base (or an equivalent stored USD-of-quote).
  5. Missing price_usd. If a bar has no USD, drop it from the USD series (or compute human × quote_catalog the same way the indexer does). Do not fall back to raw/human price on the USD axis (cLUNC 21260 case).
  6. Finite / non-positive (T524-6, #226). Drop ≤ 0 / non-finite inputs. After invert, swap high/low so high ≥ low. Never pass NaN / Infinity into lightweight-charts.
  7. Non-negative USD scale (#151). Autoscale must not go below 0 or below the lowest visible inverted low.
  8. Y-axis precision. Adaptive priceFormat (precision / minMove) from the visible USD magnitude so $0.000047 and $0.012258 are readable. Do not keep a fixed 2-dp format on a USD pane. Compact formatNum T/K must not be used as a price-axis formatter (P522-5).
  9. One orientation state (T524-4). Headline Last, last candle close fallback, last-value label, MA/RSI computed from the same inverted USD points, pair pill, and ticket display stay on the same displayed base. Invert rewrite → setData (T524-6 / #336).
  10. Do not change default invert rules (T524-3). UST1-as-asset_0 still defaults to the other token; cLUNC/UST1 stays factory until the pill is used. Do not substring-match cUSTC.
  11. U1 (#508). Never describe invert as mint/redeem.
  12. /limits standalone stays out of scope (no silent invert).
  13. Indicators. SMA/RSI must run on the display USD series after invert, not on factory USD then invert the indicator (would be wrong). Volume histogram stays quote/base volume (not inverted as price).

Relevant files

Must touch

  • frontend-dapp/src/utils/tradePairDisplayOrientation.ts — keep invertUsd as the USD invert; do not reuse invertOhlc for USD candles
  • frontend-dapp/src/components/charts/priceChartCandles.ts — applyChartDisplayInvert must take factory USD and human OHLC (or precomputed display USD)
  • frontend-dapp/src/components/charts/PriceChart.tsx — wire display series; Last already gets resolveDisplayTapeLastPriceUsd
  • frontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsx — adaptive USD priceFormat; keep #151 clamp
  • frontend-dapp/src/components/charts/priceChartPriceScale.ts — if precision helpers live here
  • frontend-dapp/src/utils/pairPriceUsd.ts — resolveDisplayTapeLastPriceUsd is the reference formula
  • frontend-dapp/src/types/index.ts — IndexerCandle human and/or *_usd fields if the API grows
  • frontend-dapp/src/services/indexer/client.ts — getCandles mapping
  • indexer/src/db/queries/candles.rs — rebuild_candles_from_swaps / CandleRow if additive human (or usd) OHLC is required
  • indexer candle HTTP handler + migrations (only if additive columns)
  • docs/frontend.md § Trade pair display invert + price chart invariants
  • docs/indexer-invariants.md row Pair price human + USD #522 (API additive note only)
  • skills/AGENTS_FRONTEND_TRADE_PAIR_INVERT.md, skills/AGENTS_FRONTEND_PRICE_CHART.md, skills/AGENTS_INDEXER_PAIR_PRICE_USD.md
  • scripts/qa/verify-issue-524.sh and a new scripts/qa/verify-issue-<iid>.sh

Do not treat as invert-USD

  • invertOhlc / displayPriceToFactoryToken1PerToken0 for limit/book human prices (those stay 1/x of quote-per-base)
  • Aggregator last_price (stays human quote-per-base)

Regression tests to update (they currently encode 1/x on candle USD)

  • frontend-dapp/src/components/charts/__tests__/priceChartCandles.test.ts (applyChartDisplayInvert reciprocates…)
  • frontend-dapp/src/utils/__tests__/tradePairDisplayOrientation.test.ts (invertOhlc vs invertUsd — keep both; document which series each applies to)
  • frontend-dapp/src/components/charts/__tests__/PriceChart.test.tsx (#524 invert describe)
  • frontend-dapp/src/components/charts/__tests__/priceChartLightweightRealLibrary.charts.test.ts
  1. Treat USD invert as invertUsd, not invertOhlc. For each bar: display_usd = inverted ? factory_usd / human_quote_per_base : factory_usd. Apply independently to open/close; for high/low, invert each then swap so high ≥ low (same as today).
  2. Give the client both series. Preferred: additive candle JSON, e.g. keep open/high/low/close as factory USD (COALESCE(price_usd, NULL) — not human fallback) and add open_human / high_human / low_human / close_human from se.price. Rebuild affected pairs. Frontend maps with #226 finite guards on both.
  3. No human-on-USD-axis fallback. Bars with NULL USD are dropped (empty-state / gap), matching P522-5. Optional later: server-side human × quote_catalog so price_usd is populated (already the indexer identity).
  4. Adaptive USD axis. From visible display USD (e.g. minMove = 10^floor(log10(min_visible)) clamped to a sane range, precision = digits needed for that minMove). cLUNC (1e-5) and USTR (1e-2) must not share a 2-dp format. Update on invert and on pair/interval change via applyOptions (do not recreate the chart — #148).
  5. Last vs last candle. After the fix, last candle close (display USD) and tape Last must match within float/oracle freshness. If tape is missing, headline fallback is the display last close (already resolveTradeChartHeadlineUsd(tape, chartPoints) once chartPoints are display USD).
  6. Keep #524 ticket/book invert as-is. Only the USD candle path changes formula.

Acceptance criteria

  • A1. UST1/USTR default invert (pill USTR/UST1): Last and candle last-value / last close are USD of 1 USTR (same order as 0.012…, not ~1.0).
  • A2. Same pair after switch-side (pill UST1/USTR): Last and candles are USD of 1 UST1 (~$0.95–$1.10), not 1/0.012 ≈ 81.
  • A3. UST1/cUSTC default: Last and candles are USD of 1 cUSTC (~$0.004–0.006 class), not ~0.93. Switch-side: both UST1 USD (~$1), not 1/0.0049.
  • A4. cLUNC/UST1 factory orientation: Last and candles are USD of 1 cLUNC (~$0.000047 class). Y-axis / last-value must not render 0.00 as the only digits (at least 5–6 decimal places or equivalent minMove).
  • A5. cLUNC/UST1 after switch-side: Last and candles are USD of 1 UST1 (~$1), not ~21260.
  • A6. Non-UST1 pairs (e.g. cLUNC/cUSTC if present) are unchanged: no default invert; candles stay factory USD of asset_0.
  • A7. /trade and /charts share the same series math and pill state (sessionStorage key per pairAddr unchanged).
  • A8. SMA/RSI follow display USD. Volume pane unchanged. Invert still setData for historical rewrite.
  • A9. Empty / all-dropped USD bars show the existing empty state; no NaN / Infinity / negative axis.
  • A10. Docs/skills: #524 playbook states USD candles use invertUsd; invertOhlc is human quote-per-base only. make verify-issue-524 still passes. New make verify-issue-<iid> covers A1–A9 unit paths.

Test plan (all paths)

Unit (no chain)

  1. invertUsd vs invertOhlc fixtures: factory USD 1.06 + human 86.48 → display 0.012258…; 1/1.06 must fail the USD assertion. Inverse: factory USD 0.000047 + human 0.000047 → display 1.0; 1/0.000047 must fail.
  2. applyChartDisplayInvert (new signature): OHLC USD + human → display USD; high/low swap; drop ≤0 / non-finite on either series; times unchanged.
  3. Missing USD on a bar → bar dropped even if human is 21260.
  4. resolveDisplayTapeLastPriceUsd and last inverted candle close agree on the same print (shared fixture).
  5. Adaptive priceFormat: inputs 1.06, 0.012258, 0.000047 produce precision/minMove that distinguish those values (not all 0.00 / not all 1.06).
  6. PriceChart / PriceChartLightweightCanvas: invert toggle calls setData (not historical update); applyOptions updates priceFormat without createChart again.
  7. TradePage / ChartsPage: default invert still UST1-as-asset_0 only; cLUNC/UST1 starts factory-oriented.
  8. Real-library charts test (*.charts.test.ts): inverted USTR-scale series last close ≈ 0.012, not ~1; cLUNC-scale last-value visible ≠ 0.00.

Indexer (if additive candle fields)

  1. rebuild_candles_from_swaps writes USD from price_usd only (no silent human fallback into the USD columns). Human columns from se.price. Integration: UST1/USTR bar has USD ~1 and human ~80–90; cLUNC/UST1 bar has USD ~5e-5 and human ~5e-5 (UST1 per cLUNC).
  2. Existing #522 lib + swap_price_human_usd tests still pass (make verify-issue-522).

Manual / LocalTerra / mainnet smoke

  1. /trade + /charts for UST1/USTR, UST1/cUSTC, cLUNC/UST1: record Last vs chart last-value both orientations and all intervals (1m…1w).
  2. Pair switch A→B→A: no stale invert series (#180 / #226).
  3. Interval switch keeps canvas; invert then interval: still display USD.
  4. Ticket Buy {displayBase} / limit convert-on-submit unchanged (make verify-issue-524).

Test plan (attack, hack, and abuse)

These are display bugs; abuse is false price, XSS, and chart DoS — not custody. Still required.

ID Vector Expect
H1 Malicious indexer candle: open/price_usd = 0, -1, NaN, Infinity, 1e309, "" Row dropped; no NaN/Infinity into setData; no thrown Cannot update oldest data
H2 Human 0 or USD 0 on invert (n/0, 1/0) Bar dropped; no Infinity last-value; headline stays on tape or hides
H3 Extreme human (1e-18, 1e18) with tiny/huge USD Finite drop or clamp; axis stays non-negative; no tab lock from autoscale
H4 XSS in pair symbol / pill / aria-label (existing #524) Text nodes only; no innerHTML of indexer strings
H5 sessionStorage invert key forged (cl8y-dex-trade-pair-invert:<otherPair>) Keyed by pairAddr only; pair B never reads pair A; invalid values ignored
H6 Stale getCandles for pair A after switch to B Query key wins; A’s USD/human never plotted on B (#226)
H7 Candle JSON extra fields / prototype keys Parsed as data objects; no __proto__ pollution affecting invert
H8 Missing oracle (price_usd NULL on all swaps) USD series empty + existing empty state; must not draw human 21260 as dollars
H9 Indicator toggle + invert spam No chart recreate loop; no unbounded pane add; RSI/MA on display USD only
H10 Do not treat this chart as a price oracle for submit Limit/market still convert human factory token1/token0 (T524-2). A wrong USD candle must not change on-chain price.

Verification criteria

Done when:

  1. make verify-issue-<iid> exits 0 (new script: unit paths A1–A9 + H1–H3 + docs/skill grep).
  2. make verify-issue-524 and make verify-issue-522 still exit 0.
  3. bash scripts/with-node.sh --cwd frontend-dapp -- npm test -- --run for priceChartCandles, tradePairDisplayOrientation, pairPriceUsd, PriceChart, priceChartLightweightSeriesSync, and charts real-library invert cases.
  4. Manual or Playwright smoke on /trade and /charts for UST1/USTR, UST1/cUSTC, cLUNC/UST1: Last and chart last-value match in both pill orientations; cLUNC factory view is not 0.00; inverted cLUNC is not 21260.
  5. Docs: docs/frontend.md invert section states USD candles use invertUsd; playbooks cross-link. No claim that 1/x inverts USD-of-UST1 into USD-of-USTR.

Mode

  • chart / indexer display (not a new swap mode)
  • v2 pool-only
  • limit order (submit unchanged)
  • hybrid

Severity

  • blocks production
  • major UX / incorrect accounting display
  • minor
## Summary On `/trade` and `/charts`, the **Last** headline (tape USD of the **displayed** base) and the **Price (USD)** candlestick series disagree after [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524). Observed on columbus-5: | Pair (pill) | Last (above chart) | Chart last / Y-axis | After switch-side | |-------------|--------------------|---------------------|-------------------| | USTR/UST1 (UST1 = factory `asset_0`, default invert) | `0.0122588` (correct USD of 1 USTR) | `~1.06` (UST1-ish / `1 / price_usd`) | Chart `~$0.95` (UST1) — headline and candles then agree | | cUSTC/UST1 (same layout) | `0.0049` (correct USD of 1 cUSTC) | `~0.93` (`1 / ~1.07`) | Both `~$1.07` for UST1 | | cLUNC/UST1 (cLUNC = factory `asset_0`, no default invert) | `0.000047` (correct USD of 1 cLUNC) | `0.00` (default lightweight-charts 2-dp) | Last `1.0` (UST1) but chart `~21260` (`1 / 0.000047`) | `1 USTR ≈ $0.012258`. The chart must never show ~$1 (or `1/usd`) as if that were USTR, nor `21260` as if that were USD of 1 UST1. This is **one** product surface: **display USD of the displayed base on both the headline and the candle pane**, including Y-axis precision. Do not split invert-math vs scale into separate issues. Related: [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) (UI invert; **T524-4** one orientation), [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) (factory `price_usd` = USD of 1 `asset_0`), [#515](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/515) (USTC/LUNC oracles), [#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151) (non-negative USD scale), [#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226) (finite candles), [#149](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/149) (Last prefers tape). ## Current codebase Factory pairs keep a fixed `asset_0` / `asset_1`. Indexer math is factory-oriented; [#524](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/524) inverts **only in the dApp**. | Layer | Behavior today | |-------|----------------| | Indexer [#466](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/466) / [#522](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/522) | `swap_events.price` = human **quote per base**. `price_usd` = **USD of 1 human `asset_0`** = `price × USD(1 human quote)`. Quote catalog: UST1=`$1`, cUSTC=#515 USTC, USTR=`2.5×` USTC, cLUNC=#515 LUNC. | | Candle storage | `rebuild_candles_from_swaps` writes OHLC as `COALESCE(se.price_usd, se.price)` into a **single** `candles.open/high/low/close`. There is **no** parallel human OHLC on `GET /candles`. If `price_usd` is NULL, the USD-labeled chart plots the **human ratio**. | | Headline (correct for tape) | `resolveDisplayTapeLastPriceUsd` → when inverted, `invertUsd(factoryUsd, humanPrice)` = `price_usd / human_quote_per_base` (USD of 1 **display** base). Else factory `price_usd`. `resolveTradeChartHeadlineUsd` prefers that tape string over last candle close. | | Candle invert (wrong for USD) | `PriceChart` maps indexer rows with `indexerCandlesToChartPoints` then `applyChartDisplayInvert` → `invertOhlc` = **naive `1/x` + high/low swap**. That is correct for a **human quote-per-base** series, **not** for a **USD-of-asset_0** series. `1 / $1.06 ≈ $0.94`, not `$0.012` USTR. `1 / $0.000047 ≈ 21260`, not `$1` UST1. | | Y-axis | `PriceChartLightweightCanvas` sets candlestick colors + `#151` autoscale clamp. It does **not** set `priceFormat.precision` / `minMove`. TradingView lightweight-charts defaults to **2 decimal places**, so `$0.000047` prints as **`0.00`**. | | Orientation | `defaultDisplayInverted` = UST1 is `asset_0` and the other leg is not. UST1/USTR and UST1/cUSTC default to **USTR/UST1** and **cUSTC/UST1**. cLUNC/UST1 (cLUNC = `asset_0`) stays factory-oriented until the pill is clicked. | | Sync | Invert rewrite of historical OHLC at the same times must `setData` (not `series.update()` from the oldest bar) — [#336](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/336) / **T524-6**. | **Why the three reports are the same bug** - **UST1-as-base + default invert (USTR, cUSTC):** Last uses `invertUsd` (other-token USD). Candles use `1 / price_usd` (still ~$1). Switch-side turns invert **off**, so Last and candles both show factory UST1 USD and **appear** to “fix” the chart. - **Other-as-base (cLUNC/UST1):** Last shows cLUNC USD (`0.000047`); candles are the same magnitude but the axis shows `0.00`. Switch-side inverts: Last uses `invertUsd` → `~$1` UST1; candles use `1/x` → `~21260` (human cLUNC per UST1), not dollars. `P522-2`: `price_usd / price` **is** USD of 1 human quote. That is the invert the headline already implements. Candles must use the same identity, not `1 / price_usd`. ## Why this is needed Traders read **Last** and the **candle pane** as one Price (USD) control (**T524-4**). Today they disagree by 1–2 orders of magnitude (USTR/cUSTC) or five (cLUNC). That is not a cosmetic invert: it is a **false market**. 1. Default USTR/cUSTC view looks like a ~$1 UST1 chart while the headline correctly prints the floating token. 2. cLUNC looks “worthless” (`0.00`) until invert, then looks like a 20k print — both wrong as USD of the displayed base. 3. Existing `#524` tests lock in `invertOhlc` as `1/x` (`priceChartCandles.test.ts` “reciprocates…”). That helper is valid for **human** book/limit prices, not for the **USD candle** series. ## Constraints / guardrails 1. **T524-1 — indexer factory meaning unchanged.** Do not change `#466` / `#522` `swap_events.price` / `price_usd` or CG/CMC `last_price`. Invert stays a **display** of factory series. Additive candle fields (human OHLC alongside USD) are allowed if required for per-bar `invertUsd`; do not *replace* factory USD with a reciprocal in storage. 2. **T524-2 — on-chain stays factory.** Do not change `place_limit_order` / market submit convert-on-submit. This issue is chart + headline + axis only. 3. **Same formula as the headline.** Display USD of 1 displayed base = factory `price_usd` when not inverted; when inverted = `invertUsd(price_usd, human_quote_per_base)` = `price_usd / price`. **Never** `1 / price_usd` on a USD series. **Never** plot human quote-per-base on a control labeled **Price (USD)** (**P522-5**). 4. **Per-bar invert.** Do not scale every historical bar by the *latest* tape human price (flattens / distorts history). Each bar needs that bar’s human quote-per-base (or an equivalent stored USD-of-quote). 5. **Missing `price_usd`.** If a bar has no USD, **drop** it from the USD series (or compute `human × quote_catalog` the same way the indexer does). Do not fall back to raw/human `price` on the USD axis (cLUNC `21260` case). 6. **Finite / non-positive (**T524-6**, [#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226)).** Drop `≤ 0` / non-finite inputs. After invert, swap high/low so `high ≥ low`. Never pass `NaN` / `Infinity` into lightweight-charts. 7. **Non-negative USD scale ([#151](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/151)).** Autoscale must not go below 0 or below the lowest **visible** inverted low. 8. **Y-axis precision.** Adaptive `priceFormat` (precision / `minMove`) from the visible USD magnitude so `$0.000047` and `$0.012258` are readable. Do not keep a fixed 2-dp format on a USD pane. Compact `formatNum` `T`/`K` must not be used as a price-axis formatter (**P522-5**). 9. **One orientation state (**T524-4**).** Headline Last, last candle close fallback, last-value label, MA/RSI computed from the same inverted USD points, pair pill, and ticket display stay on the same displayed base. Invert rewrite → `setData` (**T524-6** / [#336](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/336)). 10. **Do not change default invert rules (**T524-3**).** UST1-as-`asset_0` still defaults to the other token; cLUNC/UST1 stays factory until the pill is used. Do not substring-match `cUSTC`. 11. **U1 ([#508](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/508)).** Never describe invert as mint/redeem. 12. **`/limits` standalone** stays out of scope (no silent invert). 13. **Indicators.** SMA/RSI must run on the **display USD** series after invert, not on factory USD then invert the indicator (would be wrong). Volume histogram stays quote/base volume (not inverted as price). ## Relevant files **Must touch** - `frontend-dapp/src/utils/tradePairDisplayOrientation.ts` — keep `invertUsd` as the USD invert; do not reuse `invertOhlc` for USD candles - `frontend-dapp/src/components/charts/priceChartCandles.ts` — `applyChartDisplayInvert` must take factory USD **and** human OHLC (or precomputed display USD) - `frontend-dapp/src/components/charts/PriceChart.tsx` — wire display series; Last already gets `resolveDisplayTapeLastPriceUsd` - `frontend-dapp/src/components/charts/PriceChartLightweightCanvas.tsx` — adaptive USD `priceFormat`; keep `#151` clamp - `frontend-dapp/src/components/charts/priceChartPriceScale.ts` — if precision helpers live here - `frontend-dapp/src/utils/pairPriceUsd.ts` — `resolveDisplayTapeLastPriceUsd` is the reference formula - `frontend-dapp/src/types/index.ts` — `IndexerCandle` human and/or `*_usd` fields if the API grows - `frontend-dapp/src/services/indexer/client.ts` — `getCandles` mapping - `indexer/src/db/queries/candles.rs` — `rebuild_candles_from_swaps` / `CandleRow` if additive human (or usd) OHLC is required - `indexer` candle HTTP handler + migrations (only if additive columns) - `docs/frontend.md` § Trade pair display invert + price chart invariants - `docs/indexer-invariants.md` row **Pair price human + USD #522** (API additive note only) - `skills/AGENTS_FRONTEND_TRADE_PAIR_INVERT.md`, `skills/AGENTS_FRONTEND_PRICE_CHART.md`, `skills/AGENTS_INDEXER_PAIR_PRICE_USD.md` - `scripts/qa/verify-issue-524.sh` and a new `scripts/qa/verify-issue-<iid>.sh` **Do not treat as invert-USD** - `invertOhlc` / `displayPriceToFactoryToken1PerToken0` for **limit/book human** prices (those stay `1/x` of quote-per-base) - Aggregator `last_price` (stays human quote-per-base) **Regression tests to update (they currently encode `1/x` on candle USD)** - `frontend-dapp/src/components/charts/__tests__/priceChartCandles.test.ts` (`applyChartDisplayInvert reciprocates…`) - `frontend-dapp/src/utils/__tests__/tradePairDisplayOrientation.test.ts` (`invertOhlc` vs `invertUsd` — keep both; document which series each applies to) - `frontend-dapp/src/components/charts/__tests__/PriceChart.test.tsx` (#524 invert describe) - `frontend-dapp/src/components/charts/__tests__/priceChartLightweightRealLibrary.charts.test.ts` ## Recommended direction 1. **Treat USD invert as `invertUsd`, not `invertOhlc`.** For each bar: `display_usd = inverted ? factory_usd / human_quote_per_base : factory_usd`. Apply independently to open/close; for high/low, invert each then swap so `high ≥ low` (same as today). 2. **Give the client both series.** Preferred: additive candle JSON, e.g. keep `open/high/low/close` as factory **USD** (`COALESCE(price_usd, NULL)` — **not** human fallback) and add `open_human` / `high_human` / `low_human` / `close_human` from `se.price`. Rebuild affected pairs. Frontend maps with `#226` finite guards on both. 3. **No human-on-USD-axis fallback.** Bars with NULL USD are dropped (empty-state / gap), matching **P522-5**. Optional later: server-side `human × quote_catalog` so `price_usd` is populated (already the indexer identity). 4. **Adaptive USD axis.** From visible display USD (e.g. `minMove` = `10^floor(log10(min_visible))` clamped to a sane range, precision = digits needed for that `minMove`). cLUNC (`1e-5`) and USTR (`1e-2`) must not share a 2-dp format. Update on invert and on pair/interval change via `applyOptions` (do not recreate the chart — [#148](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/148)). 5. **Last vs last candle.** After the fix, last candle close (display USD) and tape Last must match within float/oracle freshness. If tape is missing, headline fallback is the **display** last close (already `resolveTradeChartHeadlineUsd(tape, chartPoints)` once `chartPoints` are display USD). 6. **Keep `#524` ticket/book invert as-is.** Only the **USD candle path** changes formula. ## Acceptance criteria - [ ] **A1.** UST1/USTR default invert (pill `USTR/UST1`): Last **and** candle last-value / last close are USD of 1 USTR (same order as `0.012…`, not `~1.0`). - [ ] **A2.** Same pair after switch-side (pill `UST1/USTR`): Last **and** candles are USD of 1 UST1 (`~$0.95–$1.10`), not `1/0.012 ≈ 81`. - [ ] **A3.** UST1/cUSTC default: Last **and** candles are USD of 1 cUSTC (`~$0.004–0.006` class), not `~0.93`. Switch-side: both UST1 USD (`~$1`), not `1/0.0049`. - [ ] **A4.** cLUNC/UST1 factory orientation: Last **and** candles are USD of 1 cLUNC (`~$0.000047` class). Y-axis / last-value must **not** render `0.00` as the only digits (at least 5–6 decimal places or equivalent `minMove`). - [ ] **A5.** cLUNC/UST1 after switch-side: Last **and** candles are USD of 1 UST1 (`~$1`), **not** `~21260`. - [ ] **A6.** Non-UST1 pairs (e.g. cLUNC/cUSTC if present) are unchanged: no default invert; candles stay factory USD of `asset_0`. - [ ] **A7.** `/trade` and `/charts` share the same series math and pill state (sessionStorage key per `pairAddr` unchanged). - [ ] **A8.** SMA/RSI follow display USD. Volume pane unchanged. Invert still `setData` for historical rewrite. - [ ] **A9.** Empty / all-dropped USD bars show the existing empty state; no `NaN` / `Infinity` / negative axis. - [ ] **A10.** Docs/skills: `#524` playbook states USD candles use `invertUsd`; `invertOhlc` is human quote-per-base only. `make verify-issue-524` still passes. New `make verify-issue-<iid>` covers A1–A9 unit paths. ## Test plan (all paths) **Unit (no chain)** 1. `invertUsd` vs `invertOhlc` fixtures: factory USD `1.06` + human `86.48` → display `0.012258…`; `1/1.06` must **fail** the USD assertion. Inverse: factory USD `0.000047` + human `0.000047` → display `1.0`; `1/0.000047` must **fail**. 2. `applyChartDisplayInvert` (new signature): OHLC USD + human → display USD; high/low swap; drop `≤0` / non-finite on either series; times unchanged. 3. Missing USD on a bar → bar dropped even if human is `21260`. 4. `resolveDisplayTapeLastPriceUsd` and last inverted candle close agree on the same print (shared fixture). 5. Adaptive `priceFormat`: inputs `1.06`, `0.012258`, `0.000047` produce precision/`minMove` that distinguish those values (not all `0.00` / not all `1.06`). 6. `PriceChart` / `PriceChartLightweightCanvas`: invert toggle calls `setData` (not historical `update`); `applyOptions` updates `priceFormat` without `createChart` again. 7. TradePage / ChartsPage: default invert still UST1-as-`asset_0` only; cLUNC/UST1 starts factory-oriented. 8. Real-library charts test (`*.charts.test.ts`): inverted USTR-scale series last close ≈ `0.012`, not `~1`; cLUNC-scale last-value visible ≠ `0.00`. **Indexer (if additive candle fields)** 9. `rebuild_candles_from_swaps` writes USD from `price_usd` only (no silent human fallback into the USD columns). Human columns from `se.price`. Integration: UST1/USTR bar has USD ~1 and human ~80–90; cLUNC/UST1 bar has USD ~`5e-5` and human ~`5e-5` (UST1 per cLUNC). 10. Existing `#522` lib + `swap_price_human_usd` tests still pass (`make verify-issue-522`). **Manual / LocalTerra / mainnet smoke** 11. `/trade` + `/charts` for UST1/USTR, UST1/cUSTC, cLUNC/UST1: record Last vs chart last-value both orientations and all intervals (`1m`…`1w`). 12. Pair switch A→B→A: no stale invert series ([#180](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/180) / [#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226)). 13. Interval switch keeps canvas; invert then interval: still display USD. 14. Ticket Buy {displayBase} / limit convert-on-submit **unchanged** (`make verify-issue-524`). ## Test plan (attack, hack, and abuse) These are **display** bugs; abuse is false price, XSS, and chart DoS — not custody. Still required. | ID | Vector | Expect | |----|--------|--------| | **H1** | Malicious indexer candle: `open`/`price_usd` = `0`, `-1`, `NaN`, `Infinity`, `1e309`, `""` | Row dropped; no `NaN`/`Infinity` into `setData`; no thrown `Cannot update oldest data` | | **H2** | Human `0` or USD `0` on invert (`n/0`, `1/0`) | Bar dropped; no `Infinity` last-value; headline stays on tape or hides | | **H3** | Extreme human (`1e-18`, `1e18`) with tiny/huge USD | Finite drop or clamp; axis stays non-negative; no tab lock from autoscale | | **H4** | XSS in pair symbol / pill / `aria-label` (existing #524) | Text nodes only; no `innerHTML` of indexer strings | | **H5** | sessionStorage invert key forged (`cl8y-dex-trade-pair-invert:<otherPair>`) | Keyed by `pairAddr` only; pair B never reads pair A; invalid values ignored | | **H6** | Stale `getCandles` for pair A after switch to B | Query key wins; A’s USD/human never plotted on B ([#226](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/226)) | | **H7** | Candle JSON extra fields / prototype keys | Parsed as data objects; no `__proto__` pollution affecting invert | | **H8** | Missing oracle (`price_usd` NULL on all swaps) | USD series empty + existing empty state; **must not** draw human `21260` as dollars | | **H9** | Indicator toggle + invert spam | No chart recreate loop; no unbounded pane add; RSI/MA on display USD only | | **H10** | Do not treat this chart as a price oracle for submit | Limit/market still convert **human** factory `token1/token0` (**T524-2**). A wrong USD candle must not change on-chain price. | ## Verification criteria Done when: 1. `make verify-issue-<iid>` exits 0 (new script: unit paths A1–A9 + H1–H3 + docs/skill grep). 2. `make verify-issue-524` and `make verify-issue-522` still exit 0. 3. `bash scripts/with-node.sh --cwd frontend-dapp -- npm test -- --run` for `priceChartCandles`, `tradePairDisplayOrientation`, `pairPriceUsd`, `PriceChart`, `priceChartLightweightSeriesSync`, and charts real-library invert cases. 4. Manual or Playwright smoke on `/trade` and `/charts` for **UST1/USTR**, **UST1/cUSTC**, **cLUNC/UST1**: Last and chart last-value match in **both** pill orientations; cLUNC factory view is not `0.00`; inverted cLUNC is not `21260`. 5. Docs: `docs/frontend.md` invert section states USD candles use `invertUsd`; playbooks cross-link. No claim that `1/x` inverts USD-of-UST1 into USD-of-USTR. ## Mode - [x] chart / indexer display (not a new swap mode) - [ ] v2 pool-only - [ ] limit order (submit unchanged) - [ ] hybrid ## Severity - [ ] blocks production - [x] major UX / incorrect accounting display - [ ] minor
PlasticDigits commented 2026-08-17 03:51:35 +00:00 (Migrated from gitlab.com)

marked as related to #524

marked as related to #524
PlasticDigits commented 2026-08-17 03:51:36 +00:00 (Migrated from gitlab.com)

marked as related to #522

marked as related to #522
PlasticDigits commented 2026-08-17 03:51:36 +00:00 (Migrated from gitlab.com)

marked as related to #151

marked as related to #151
PlasticDigits commented 2026-08-17 03:51:37 +00:00 (Migrated from gitlab.com)

marked as related to #226

marked as related to #226
PlasticDigits commented 2026-08-17 04:13:14 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1069

mentioned in merge request !1069
PlasticDigits commented 2026-08-17 04:44:07 +00:00 (Migrated from gitlab.com)

mentioned in commit f53e3bf768

mentioned in commit f53e3bf768b325dbbbef4e494dac8784f754c4a2
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-17 04:44:07 +00:00
PlasticDigits commented 2026-08-17 04:45:59 +00:00 (Migrated from gitlab.com)

mentioned in commit 64c6dd9623

mentioned in commit 64c6dd962314440fa313af38bfb44d24ad97777b
PlasticDigits commented 2026-08-17 04:47:06 +00:00 (Migrated from gitlab.com)

mentioned in commit d7aa1c07e1

mentioned in commit d7aa1c07e1f21416a60c226a09bceb15b9b88db4
PlasticDigits commented 2026-08-17 04:53:32 +00:00 (Migrated from gitlab.com)

Merged via !1069 onto main (f53e3bf7). Sequential stack is now 7fdc3a44 (!1069 + !1070 + !1071).

Sanity check on main: make verify-issue-543 — 8/8 pass (unit A1–A9 / H1–H3 paths, docs/skill grep, indexer candle_human_usd + candle_skip_zero_price). USD candles use invertUsd / invertUsdNumber, not 1/x. No merge-conflict leftovers.

Conflicts: none on GitLab (first merge). Later !1070/!1071 needed Makefile/AGENTS.md target unions only.

Remaining (not a merge blocker): production/QA indexer must apply indexer/migrations/20260817000000_candle_human_ohlc.sql (adds *_human, TRUNCATEs candles, rebuilds from swap_events). Until then inverted charts can look empty (bars without *_human are dropped). Manual columbus-5 Last-vs-candle smoke (UST1/USTR, UST1/cUSTC, cLUNC/UST1, both pills) is still unchecked. Tracked in a post-merge follow-up.

Merged via !1069 onto `main` (`f53e3bf7`). Sequential stack is now `7fdc3a44` (!1069 + !1070 + !1071). **Sanity check on `main`:** `make verify-issue-543` — 8/8 pass (unit A1–A9 / H1–H3 paths, docs/skill grep, indexer `candle_human_usd` + `candle_skip_zero_price`). USD candles use `invertUsd` / `invertUsdNumber`, not `1/x`. No merge-conflict leftovers. **Conflicts:** none on GitLab (first merge). Later !1070/!1071 needed Makefile/`AGENTS.md` target unions only. **Remaining (not a merge blocker):** production/QA indexer must apply `indexer/migrations/20260817000000_candle_human_ohlc.sql` (adds `*_human`, **TRUNCATEs `candles`**, rebuilds from `swap_events`). Until then inverted charts can look empty (bars without `*_human` are dropped). Manual columbus-5 Last-vs-candle smoke (UST1/USTR, UST1/cUSTC, cLUNC/UST1, both pills) is still unchecked. Tracked in a post-merge follow-up.
PlasticDigits commented 2026-08-17 04:53:57 +00:00 (Migrated from gitlab.com)

mentioned in issue #545

mentioned in issue #545
PlasticDigits commented 2026-08-17 04:53:58 +00:00 (Migrated from gitlab.com)

marked as related to #545

marked as related to #545
PlasticDigits commented 2026-08-17 10:26:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #547

mentioned in issue #547
PlasticDigits commented 2026-08-18 00:28:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #556

mentioned in issue #556
PlasticDigits commented 2026-08-18 02:30:14 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1083

mentioned in merge request !1083
PlasticDigits commented 2026-08-18 12:12:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #564

mentioned in issue #564
PlasticDigits commented 2026-08-19 00:57:40 +00:00 (Migrated from gitlab.com)

mentioned in issue #568

mentioned in issue #568
PlasticDigits commented 2026-08-19 00:57:42 +00:00 (Migrated from gitlab.com)

marked as related to #568

marked as related to #568
PlasticDigits commented 2026-08-27 00:20:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #680

mentioned in issue #680
PlasticDigits commented 2026-08-29 14:16:59 +00:00 (Migrated from gitlab.com)

mentioned in issue #705

mentioned in issue #705
PlasticDigits commented 2026-09-01 08:14:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #717

mentioned in issue #717
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#543
No description provided.