feat(clickhouse): realtime point-in-time DEX fill state for richer swap filters - #242
Open
jxom wants to merge 3 commits into
Open
feat(clickhouse): realtime point-in-time DEX fill state for richer swap filters#242jxom wants to merge 3 commits into
jxom wants to merge 3 commits into
Conversation
…-in-time fill state Decode OrderPlaced+OrderFlipped into a positioned order-state stream (dex_order_events) and ASOF-join each fill to the latest preceding state in a refreshable MV (dex_fills_enriched), materializing token/isBid/tick, at_peg, price, quote amount and taker-oriented source/destination tokens. Lets Cadent's swaps feed restore at-peg and source/destination-token filters as SQL column reads instead of per-request raw-log replay. Amp-Thread-ID: https://ampcode.com/threads/T-019eb863-5051-7439-a545-3f40e9e65a55
Resolve the fill->order-state ASOF join at query time over the already decoded, sort-keyed dex_order_events / dex_fills tables instead of in a refreshable MV. A plain view is realtime (no refresh lag), correct for same-block flips and reorgs (it reads the complete live state), and still far cheaper than the app's per-request raw-logs replay. Validated against ClickHouse 26.5 incl. a same-block flip+fill that an insert-time MV would mis-resolve. Amp-Thread-ID: https://ampcode.com/threads/T-019eb863-5051-7439-a545-3f40e9e65a55
Drop the per-fill taker-oriented source_*/destination_* columns. A fill is natively one taker filling one resting maker order on one book, so the view exposes token/quote_token, isBid, tick, at_peg, price and the base/quote amounts (amountFilled/quote_amount). Taker source->destination is a swap-level (route) notion derived during getSwaps assembly, not a per-fill property; removing it also drops the redundant quote_amount/quote_token duplication. Amp-Thread-ID: https://ampcode.com/threads/T-019eb863-5051-7439-a545-3f40e9e65a55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Cadent's swaps feed currently re-derives each fill's order state in the app on every request: it replays the raw
logsorder-state stream, walks eachorderIdto find the latestOrderPlaced/OrderFlippedbefore the fill, then computes price/orientation/at-peg and assembles routes in memory. That made theatPeg,sourceToken, anddestinationTokenfilters slow and forced post-pagination filtering (they returned fewer thanlimitrows). Cadent had to remove them as a stopgap.The existing
dex_ordersobject can't fix this on its own — it decodesOrderPlacedonly, so it's stale for flip orders whose token/side/tick change mid-life viaOrderFlipped.What
Two new ClickHouse objects that resolve fill state server-side, point-in-time, flip-correct, and realtime:
dex_order_events(table + insert-time MV) — decodes the union ofOrderPlacedandOrderFlipped(DEX precompile0xdec0…0000) into one positioned state stream ordered by(orderId, block_num, log_idx). Block-scoped, backfillable, public. Decoding is row-local, so it's safe to do incrementally/realtime — same pattern asdex_fills/dex_orders.dex_fills_enriched(plain view) — ASOF-joins each fill to the latestdex_order_eventsrow strictly before its(block_num, log_idx), exactly mirroring the app's "latest state event before the fill" rule. Exposes each fill book-natively (one taker filling one resting maker order on one book):token/quote_token,isBid,tick,at_peg,price, and the base/quote amounts (amountFilled/quote_amount).Why book-native, not taker source→destination
A fill is natively book-oriented. Taker
source→destinationis a swap-level (route) notion, not a per-fill property: a swap groups fills by(tx, taker)and its source/destination are the route ends, which only come from assembling the group in order. So the API derives orientation/route/rate fromisBidduringgetSwapsassembly; the view stays honest to what a fill is, and avoids redundant columns.Why a plain view, not a materialized view
The join is resolved at query time over the already-decoded, sort-keyed source tables:
dex_fills_mv,dex_order_events_mv) off onelogsinsert have no guaranteed execution order, so a flip in the same block as a later fill could be joined before it exists.dex_order_events(byorderId), so per-query cost is an ASOF join over decoded rows, far below the app's current per-request raw-logsreplay. Callers scope cost with the usual time/token/pagination predicates, applied in SQL before LIMIT (which also fixes the original post-pagination-filtering bug).Validation
cargo test --lib— passing (incl.order_events_decode_placed_and_flipped_from_logs,enriched_fills_are_realtime_point_in_time_join, updateddecoded_tables_are_block_scoped_and_public)cargo fmt --check,cargo clippy --all-targets— cleanclickhouse-server 26.5):ensure_schema()applies all DDL cleanly (caught + fixed aFINAL-alias ordering bug)amount=1000000, isBid=1, tick=-3✓View; seeded fills appear in the view immediately, with no refresh ✓(300,5)correctly resolved to the flip at(300,1)rather than the earlier placed state — the exact case an insert-time MV mis-resolves ✓logs/ 1M fills / 240k order events): token filter ~1.8× faster than the raw-logssubquery (65 vs 119 ms);at_pegnow expressible in SQL (80 ms) instead of post-pagination JS; windowed unfiltered head page near parity (92 vs 78 ms, before counting OLD's separate state query).Follow-up
Cadent side will rewrite
getSwapsto read these objects, deriving swap source→destination/route/rate from the book-native fill columns during assembly, and restoreatPeg/sourceToken/destinationTokenwithout post-pagination filtering.