Skip to content

fix(ui): tick replay, analytics catch-up, Connections widths, faster Report reads - #409

Merged
kirillDevPro merged 6 commits into
mainfrom
feat/tick-replay-and-fixes
Sep 3, 2026
Merged

fix(ui): tick replay, analytics catch-up, Connections widths, faster Report reads#409
kirillDevPro merged 6 commits into
mainfrom
feat/tick-replay-and-fixes

Conversation

@kirillDevPro

Copy link
Copy Markdown
Collaborator

What & why

Six reviewed changes, each checked on a live debug build:

  • trade-replay: the trade window draws a tick chart where the venue publishes trades
    (Binance, Gate, Bitget, OKX routes), and a spent tick budget, a deadline or a venue refusal
    now serves the ticks already fetched around the entry and exit instead of abandoning the
    whole stage and falling back to 1-minute candles. Every remaining candle fallback names its
    reason in the caption.
  • analytics: the window keeps its body on screen while trades keep landing. The first fix
    stopped a Busy failure from being published over settled data; the second closes the three
    sibling paths that still swapped or blanked the body (a valuation-coverage gap, the fourth
    retry under contention, a machine-driven report-axis re-observation).
  • connections: all three text columns in Settings › Connections grow under one width
    policy, with Name uncapped, Key capped at 260 px and Group at 140 px.
  • db: the Report row pass and distinct_strategies are index-usable (4.8 s → 2.8 ms and
    −63 % on the measured replica). The DESC arm appends the primary key as a tie-break so which
    tied row survives a LIMIT is defined rather than planner-chosen. No schema change.

How to verify

cargo test --workspace and cargo fmt --all -- --check are green on the integrated tree.
On a live build: a Binance USD-M trade opens with a tick trace around entry and exit; Analytics
«Summary» over 30 days does not blank while trades close; the Connections table at 860 px and
1791 px shows Name as the widest column.

With Analytics open, every closed trade made the whole window blink:
the numbers vanished behind a failure note and came back a second
later.

A trade landing is exactly when the report writer is committing, so
the writer-driven catch-up read often returns Busy under SQLite
contention. Five surfaces (summary, calendar, and the filters, time
and coins tuner axes) published that Busy failure over settled data;
only the strategy base already guarded against it. The bounded retry
then restored the numbers, which is what turned an error into a
flash. The open chart popup was also closed on every landed trade,
and the measured core-column width cache was dropped on every
published base.

One rule now covers all twelve call sites: a catch-up result replaces
what is on screen only when it carries data, or when it is a
non-Busy failure, or when Busy has exhausted its retry allowance. The
popup and the width cache survive a landed trade. The debounce gate
is untouched. Contract tests pin the preserve rule and the hover
asymmetry, and a civil-date rollover is caught explicitly.
…enue publishes trades

The trade-history window opened from the Report always replayed one-minute
candles, even on venues whose public REST publishes time-ranged trades, so
the detail view was coarser than the main chart's tick view.

Add a trade-replay route beside the kline route: fetch public trades
(Binance spot/USD-M/COIN-M, Gate spot/futures, Bitget spot/mix, OKX) in a
two-stage worker, sort the pages globally before aggregation, claim the
per-host send permit for the tick stage and release it on every exit path,
abandon a partial page rather than shipping a hole as a whole window, and
let the window accept a tick upgrade over an already-drawn candle replay.
Bybit and Hyperliquid publish no such endpoint, so they keep candles.
In Settings > Connections the Name and Group columns both grew with
the window while the Key column was pinned at 200px. Flexbox splits
free space evenly between equal grow factors, so on a wide window
the one-word group name took as much room as the core name, which
truncated beside it, and the masked key stayed cramped behind its
own two icons.

All three text columns now grow under one TextScaled width policy
(a design value scaled on the Font delta, no floor): Name is uncapped,
Key grows to a 260px cap and Group to a 140px cap. A cap rather than
a rigid width, because the default 860px window is already a shrink
regime where anything flex_shrink_0 is width the text columns never
get back. The cap is a new ConnCol.max resolved by the same policy
as the basis and applied by the table's cell as max_w.
Two read paths in report_read.rs paid for a full sort or a full table
read on every call. The Report row pass on the default DESC sort
carried a redundant leading `(expr) IS NULL` term that kept SQLite
off the index and forced a SORT over the whole source; the captured
default-sort statement took 4.8 s and now takes 2.8 ms, -51% per call
over a whole replayed run. distinct_strategies scanned the report
once per arm; split into a normal-strategy arm and a liquidation arm
over one WHERE it is -63% (1.81 s to 0.67 s).

LIMIT is applied per source before the Rust merge, and SQL defines no
order among rows tied on every ORDER BY term, so which tied trade
survived the cut was whatever plan SQLite picked. The DESC arm now
appends the source's primary key as a tie-break, which makes that
order total: measured 0 of 25 limits differing with the tie-break
against 20 of 25 without it.

The db_read_timing example gains an EXPLAIN QUERY PLAN mode that
registers stub scalar functions for the crate's own SQL functions, so
plans resolve for every statement; gen_klines and tools/gen_replica.py
build a realistic replica for the timing runs. No schema change: the
candidate index was measured and deferred because its write cost
could not be pinned down on this machine.
The previous fix kept the Analytics body on screen only when a
refresh failed with Busy. Three sibling paths still swapped or
blanked it while trades kept landing: a valuation-coverage gap
returned Ok(Split) and replaced the whole body with the quote-split
note; the Busy preserve gave up after three retries and published
the error on the fourth under sustained contention; and a
machine-driven report-axis re-observation escalated into a manual
reload that blanked the body and armed the dimming overlay.

One rule now covers all of them. CatchUpOutcome classifies every
read, preserve_on_catch_up keeps settled data through a superseded
result as well as a busy one, and the axis observer retires the
in-flight identities and asks the writer for a catch-up instead of
reloading. Calendar and the tuner's filter, time and coins surfaces
route through the same predicate.
…ing the stage

The tick stage walked the replay window in ascending time order and
returned OverTickBudget the moment the running total passed 40 000
rows, discarding every page already paid for. The window pads a
trade with up to an hour of lead context, so on a liquid Binance
USD-M coin the budget was spent on context before the trade itself
was reached, and the window silently fell back to 1-minute candles.

The window now carries the trade's own span and the tick plan tiles
it focus-first, so any prefix of the fetch is a contiguous interval
around the entry and exit. A spent budget, a deadline or a venue
refusal serves what was fetched: the exchange klines stay the bar
layer for the whole window while tick points cover the fetched span,
and an over-budget vector thins to real ticks rather than dropping
to candles. Every remaining candle fallback names its reason in the
caption (no trades route, retention, error, budget) in ru, en and es.
@kirillDevPro
kirillDevPro merged commit 4ec00f0 into main Sep 3, 2026
6 checks passed
@kirillDevPro
kirillDevPro deleted the feat/tick-replay-and-fixes branch September 3, 2026 21:15
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.

1 participant