v1.6.0 - Node-Failure Strike Ledger, Grid Regen, TV Order Overlay, Balance Heal, Live Config Pickup, Reserve Ladder, Boundary Recovery, Candle Cache Unification, Chart Upgrades, Shelf/Startup Hardening
[1.6.0] - 2026-09-12 - Node-Failure Strike Ledger, Grid Regen, TV Order Overlay, Balance Heal, Live Config Pickup, Reserve Ladder, Boundary Recovery, Candle Cache Unification, Chart Upgrades, Shelf/Startup Hardening
2026-09-12
- Fix(startup): guard startup excess cancels against shelf orders and regress the reserve trigger (issue #27) — hoisting the matched-excess selection made it reachable on every startup, but without the slot-N gate the cheapest-first order wiped fork-kept shelf orders on the next boot;
matchedExcessis now filtered toparseSlotIndex(id) !== nullin both planOnly and execute branches. Tests:getTargetedSyncReasonblock (surplus with empty floor reserves firesbuy reserves 0/2, filled/disabled/empty-budget configs stay silent) and shelf block (exact rail cancel set, plan/execute parity, floor reserves + closest window + shelf survive) (modules/order/grid_reconcile_internal.ts,tests/test_reserve_orders.ts). - Fix(funds): skip shelf orders in geometric size recalc (issue #27 follow-up) — the divergence recalc distributed the side budget over every slot carrying the side's type and emitted on-chain UPDATEs for anything off-ideal, so fork-kept shelf orders with manual sizes took curve ideals on the first post-startup divergence pass;
_recalculateGridOrderSizesFromBlockchainskips non-slot-N slots in the per-slot loop (sameparseSlotIndexgate as reserve classification; shelf stays in the denominator so budget math is unchanged, only the mutation is skipped) (modules/order/grid.ts,tests/test_cow_divergence_correction.tsTest 7).
2026-09-11
-
Feat(node): node-failure strike ledger and broadcast-deferred fill rebalancing — a live bot run showed two compounding reliability gaps: a broadcast region outliving the fill lock's acquisition timeout cascaded into repeated "Lock acquisition timeout" consumer failures, and flapping nodes escaped blacklisting because a single successful health probe erased live-transport strikes while the transport kept re-selecting them on reconnect. New
node_connect_policy.tsmanages a failure ledger withLIVE_FAILURE_HEALTH_SUCCESS_STREAK(2 consecutive probe successes to reset, transport-sourced strikes clear on first success);bitshares-native/transport.tsfeeds strikes viaonNodeFailureobserver (abnormal closes as"connection"strikes, keep-alive trips as"keep-alive"with follow-up suppression, preferred-candidate pass intryConnect);node_manager.tsapplies strikes and resets;dexbot_cow_runtime.ts/dexbot_fill_runtime.ts/subscriptions.tshandle broadcast deferrals and fill rebalancing;docs/COW_INVARIANTS.mdupdated (docs/COW_INVARIANTS.md,modules/bitshares-native/*,modules/node_connect_policy.ts,modules/node_manager.ts,modules/bitshares_client.ts,modules/constants.ts,modules/dexbot_class.ts,modules/dexbot_cow_runtime.ts,modules/dexbot_fill_runtime.ts,tests/test_node_connect_policy.ts,tests/test_node_failover.ts,tests/test_fill_pipeline_robustness.ts,tests/test_native_transport.ts). -
Feat(trust-chain): guarded trust-chain free-balance heal and deferred-drain fill tolerance — a fill/broadcast chaos window left a persistent one-sided tracked-free drift that recovery could never repair, and deferred fill drains repeatedly tripped the fund invariant on already-processed ledger deltas. New
consumeDeferredDrainMarker()helper marks drain residue; the cycle that runs parked fills stampsmanager._orphanFillsCreditedAtto widen fund-invariant tolerance (x5) for that cycle only. New_fundDriftLedgerrecords one-sided drift;_tryTrustChainFreeHeal()seeds tracked free balance from chain total minus committed grid sizes, gated byFUND_INVARIANT_HEAL_ON_RECOVERY_FAIL(modules/order/accounting.ts,modules/dexbot_fill_runtime.ts,modules/dexbot_class.ts,modules/dexbot_maintenance_runtime.ts,modules/constants.ts,tests/test_fund_drift_heal.ts,tests/test_fill_pipeline_robustness.ts). -
Feat(tv): TradingView order overlay on the rewritten chart exporter — restores the on-chart order overlay lost in the exporter rewrite and folds in interaction, label, and data-source fixes iterated while validating. Re-adds order rendering from the orders-file (no-orders handling, MKT/DEEP liquidity panel), gates on order data with base-unit volume, derives canonical gridLo, restores drag-pan and X-range sync, uses plain-decimal price labels, removes last-price dashed line, turns off uPlot gridlines, wires the update marker end-to-end, moves timeframes onto the AMA row, and uses English BUYS/SELLS labels and en-US dates (
analysis/tradingview/tradingview_uplot_chart_generator.ts,analysis/tradingview/analyze_tradingview.ts,analysis/tradingview/README.md,scripts/README.md). -
Fix(credit): full offer id and hide Curr. CR pairs without live funds — Curr. CR lines print the full offer object id (e.g.
1.21.681) instead of the short numeric segment, and skip the line entirely when there is no live offer or the available balance is null/zero sono funds (...)rows no longer print; Avar. CR still covers own positions (scripts/analyze-credit.ts). -
Fix(reserve): reserve-aware targeted sync trigger plus live-config docs note (issue #27) — a live-applied
reserveOrdersincrease never placed orders because the targeted-sync shortfall compared live window+reserves against target window+reserves, so a pre-existing window surplus masked the reserve deficit until fills or the 4h fetch; newcountLiveReserveOrders(canonicalreserveEdgeIdSet+ live-anchor classification over the full master grid, intersected with live ACTIVE/PARTIAL orderIds, fail-closed without a full grid) feeds an independent"<side> reserves live/required"reason ingetTargetedSyncReason, budget-gated like the window check. Docs: new "Editing a running bot" subsection indocs/BITSHARES_ONBOARDING.mdmirroringBOT_LIVE_CONFIG_KEYS(live keys apply in ~1min, geometry needsdexbot reset, identity needs restart) (modules/dexbot_maintenance_runtime.ts,docs/BITSHARES_ONBOARDING.md,tests/test_reserve_orders.ts). -
Feat(tv): monthly candles, reordered market badge, stat badges — new
1Mtimeframe with true UTC-calendar-month bucketing inaggregateCandlesand Mon-YYYY axis labels; market panel row order changed to SELL/Market/BUY; new bottom-right range panel (visible-window High red / Low green, mirroring SELL/BUY badge colors) and bottom-right volume badge (visible-window max volume only, hides with the volume chart), both refreshed on rerender and glued to the visible window via throttled setScale hooks on both charts (analysis/tradingview/tradingview_uplot_chart_generator.ts,analysis/tradingview/README.md). -
Feat(tv): rigid plot pan, axis-gutter scaling, and Shift+wheel price zoom in the uPlot chart — plot drags pan time + price with locked span (sets a manual price range) instead of squashing the visible span mid-drag; Y scaling lives on the price-axis gutter (wheel/drag) and Shift+wheel (cursor-anchored, price pane only); new time-axis gutter drag scales the timeframe around its center, synced across panes; rAF-throttled auto-Y refit to the visible window on every x setScale respects a user-locked manual range, double-click on the price axis returns to autofit; zoom-out band widened (floor min/2, ceil max*2) (
analysis/tradingview/tradingview_uplot_chart_generator.ts). -
Fix(cache): trust only genuine query coverage when reusing cached LP candle windows — the immutable-gap pruner treated everything before the first locally cached bucket as proven-empty history, so stray boundary candles from a neighboring window's chunk file certified 700+ unqueried hours as empty and a whole month rendered flat; missing ranges are now pruned only when a chunk file's recorded
queriedRangesgenuinely cover them (one genuine full fetch still caches truly quiet spans permanently; previously skipped windows self-heal on the next run) (market_adapter/inputs/window_cache.ts,tests/test_window_cache.ts,tests/test_fetch_lp_data_logic.ts). -
Feat(tv): clickable base/quote volume toggle with currency suffix — toolbar unit button plus clickable legend Vol value and V-max badge switch base/quote units, persisted per chart in localStorage (
volumeMode) alongside the AMA settings; legend, hover tooltip, and V-max badge always render the currency suffix (e.g.1.2M BTS), axis ticks stay numeric; quote derived client-side as base x display close per candle; feed charts (volume = publish count) show thefeedssuffix and disable the switch via thevolumeIsCountpayload flag (analysis/tradingview/tradingview_uplot_chart_generator.ts,analysis/tradingview/README.md). -
Fix(tv): honest volume affordance on feed charts — legend Vol value and max badge no longer show a pointer cursor with a click-to-switch hint on feed charts where the toggle is dead by design (default cursor + feed title instead); count unit renamed from
pubstofeeds; badge label shortened fromV maxtomax(analysis/tradingview/tradingview_uplot_chart_generator.ts). -
Feat(cache): unify Kibana candle caching on
runCachedWindows, harden fetch robustness — pool, book, and feed fetches share one cache entry point (LP migrates off its bespoke manifest loop; sidecar*.fetch_manifest.jsonno longer written, legacy files still read); newfetchRangeWithRetry(per-range attempts + linear backoff + abort-signal timeout; LP keeps its 4-attempt budget, book/feed keep single-shot default); partial windows merge into output but are never persisted;kibanaSearchretries transient errors (3 attempts) for one-shot queries,kibanaMaxPages(500) runaway guard on paged fetchers, bidirectional fetch tolerates one-direction failure; singleisTransientNetworkError/sleepMs(modules/utils/errors.ts) and singleslugPart(market_adapter/interval_utils.ts); deadconsolidateByTimestampkey removed (newtests/test_fetch_book_data.ts,tests/test_kibana_candles.tscases;market_adapter/inputs/*,market_adapter/core/*). -
Fix(reserve): exclude non-slot-N shelf ids from reserve classification and placement (issue #27 follow-up) — fork-kept shelf orders (live non-slot-N ids below the rail, e.g.
deep-*) were classified as the reserve edge in every anchor outcome, so the targeted-sync reserve deficit could never fire while the shelf was live, and the Tier-2 live-anchor scan let the cheapest shelf drag the anchor to itself (isSlotInRailis fail-open for unparseable ids);reserveEdgeIdSet,resolveLiveReserveEdgeAnchorPrice,_pickEdgeReserveSlots, andpickEdgeReservesnow gate toparseSlotIndex(id) !== nullso classification and placement agree (no-op on grids that only mint slot-N ids) (modules/order/utils/order.ts,modules/order/grid_reconcile_internal.ts,modules/order/manager.ts,tests/test_reserve_orders.ts). -
Fix(reserve): exclude window members from the live-reserve count (issue #27 follow-up) —
countLiveReserveOrdersclassified reserves without excluding window members while every placement picker passes window ids asexcludeIds, so when the active window reached the grid edge the edge pick landed on live window orders and the count read N/N with zero dedicated reserves; newliveWindowIdSethelper derives window member ids from master rail geometry (sliced toactiveOrders, fail-open on unknown boundary) andreserveEdgeIdSettakes an optionalexcludeIdsforward (modules/order/utils/order.ts,modules/dexbot_maintenance_runtime.ts,tests/test_reserve_orders.ts). -
Fix(startup): startup excess plans matched-surplus cancels (issue #27 follow-up) — startup reconcile always runs
planOnly, but the planOnly branch recorded only unmatched-orphan cancels while the matched-surplus leg (cancelCount = chainCount - targetCount, reserve edge last) lived only in the execute branch, so on a fully-placed grid the surplus was silently dropped every restart with zero cancel ops; the matched-excess selection is hoisted above the planOnly/execute split so both share one ordering (orphans first, matched after, reserve edge last), and planOnly omitsreleaseUntrackedFundsfor matched slots (funds tracked on the grid slot) (modules/order/grid_reconcile_internal.ts,tests/test_reserve_orders.ts; full suite 276 passed). -
Fix(feed): align feed volume and AMA timeframes — cross-feed publication counts are averaged across both feed legs instead of summed, comparable feed-rate values are preserved on weekly/monthly candles, and the first sampled AMA value anchors to its warmup average so higher-timeframe charts show no initialization gap (
market_adapter/inputs/kibana_feed_source.ts,market_adapter/candle_utils.ts,tests/test_kibana_feed_source.ts).
2026-09-10
- Feat(live-config): apply
bots.jsonedits to the running bot without restart (issue #27) — the 1min bots-config poll only fingerprinted the market-adapter price feed (name:gridPrice), so window-count, reserve, fund, and weight edits sat unapplied until restart with nothing saying so, and monolithic (unlock) bots skipped the poll entirely via the wrapper-owned early return. NewcheckAndApplyBotConfigChangesruns at the top of the periodic adapter-sync path on every tick (startup, 1min poll, 4h chain fetch, wrapper-owned included) with a single shared snapshot read: it fingerprints the full normalized bot entry (key order/comments/whitespace-insensitive) and live-merges the safe allowlistBOT_LIVE_CONFIG_KEYS(activeOrders,reserveOrders,botFunds,weightDistribution,min_BTS_value,debtPolicy) intobot.config+manager.config, each converging through its existing consumer (targeted drift reconcile,recalculateFunds, dynamic-weight refresh, fee/acquisition reads). Anything outside the allowlist logs a one-time hint namingdexbot reset <name>for grid geometry vs restart for market/account identity and tuning; corrupt/unreadable snapshots preserve the stored fingerprint and never throw. The poll-disabled log now covers the adapter + live-config fallback, and the editor shows the live key list at save (modules/dexbot_maintenance_runtime.ts,modules/runtime_settings.ts,modules/dexbot_class.tsfingerprint fields,modules/account_bots.ts,tests/test_dexbot_maintenance_runtime_market_adapter_watchdog.ts). - Feat(live-config): live-apply
debtPolicybehind a structural shape gate — the credit runtime reads policy through a live getter, so threshold/toggle/item edits take effect on the next credit maintenance/watchdog cycle with no grid impact; a malformed policy is diverted to the reset/restart hint instead of merged so a bad edit can never poison the running credit cycle, and a successful merge reconciles the runtime (_setupCreditRuntime, no-oploadStatewhen already loaded) plus the watchdog interval (enable-from-zero creates/loads state and starts it, removal clears the policy and stops it). Drive-by fix: the full-resync path replacesbot.configwith a new object, which leftCreditRuntime.configpointing at the stale copy soreset-reloaded policies were silently ignored — the shared replace helper now re-points it (same files as above). - Feat(claw): agent settings patches touching only live-pickup keys no longer force a full grid-resync trigger — the default auto-trigger fired on
activeOrders/botFunds/weightDistribution/debtPolicy, so an agent window-count tweak caused a full cancel/replace wave for an edit the running bot absorbs incrementally within a minute; patches touching any non-live trigger key still trigger, explicittrigger: true/falsestill overrides, andpreviewBotSettingsUpdate.triggerRequiredfollows the same rule (claw/modules/dexbot_profiles.ts,claw/tests/test_dexbot_profiles.ts). - Refactor(live-config): single-source live-config plumbing, stale-code removal —
BOT_LIVE_CONFIG_KEYSlives inmodules/runtime_settings.ts(consumed by the maintenance runtime, the editor hint, and the Claw trigger gate); onecloneJsonValue, onediffBotConfigEntries(replacingdiffLiveBotConfig+detectNonLiveBotConfigChanges), one sharedreplaceBotConfigFromEntryPreservingRuntimefor the resync path; dead_appliedBotLiveSnapshotfield removed and the stale wrapper-owned poll test renamed to match the new behavior (same files as above). - Fix(boundary): anchor null-boundary recovery from live fill prices instead of fabricating a rail-top boundary — after GRID-LOAD rejected a poisoned persisted boundary with no safe re-derivation, the committed boundary stayed null while fills were the only remaining boundary mover; the first fills hit recovery with
config.startPrice="pool"(an unresolved mode string), where everyprice >= referencecomparison is false so the split fell through to the rail top (base at the rail edge, ceiling-clamped, then shifted by same-batch crawls), the active window ran off the rail, and ordinal pairing planned hundred-slot "rotations" that guards had to refuse.deriveTargetBoundarynow anchors from position signals, weakest last: live fill prices (gap-side extreme, midpoint when both sides filled) → numeric config center → forwarded genesis center → bounded rail-center fallback; fill-anchored recovery batches skip the crawl (the anchor already contains the fill info, crawling would double-count; genesis/rail-center anchors still crawl);calculateIdealBoundaryfails toward rail-center on non-numeric references instead of the rail top (modules/order/utils/order.ts,modules/order/strategy.tsforwardsgenesisStartPrice,tests/test_boundary_anchor_recovery.tsANCHOR-001..009 including a replay of both incident batches). - Fix(recovery): erase the poisoned persisted boundary on unrecoverable GRID-LOAD rejection —
storeMasterGridnever persists a null boundary, so the rejected value survived every flush and re-armed the identical rejection on every restart; new explicit-onlyAccountOrders.clearPersistedBoundary()runs best-effort when re-derivation fails, so the next boot loads boundary-less and the first fill batch re-anchors live (modules/account_orders.ts,modules/order/grid.ts). - Fix(cow): rail-edge truncation telemetry and structural-resync plumbing for refused plans — warn-only log when the planned window runs off the rail (sell-start past the last slot; no geometry refused, cross-guard/fund-validation/boundary-hold still judge), and the batch executor now honors
needsResyncfrom refused plans (unrecoverable boundary) by requesting a structural grid resync where bot context exists (modules/order/manager.ts,modules/dexbot_class.ts). - Fix(guard): freeze the last-fill-guard pivot once per batch — per-action refreshes mutated the pivot mid-batch so early actions were judged against a different pivot than later ones; all three guard sites pass through a frozen batch pivot (
skipRefresh), per-action skip lines demote from warn to debug, batch summary unchanged (modules/dexbot_cow_runtime.ts). - Fix(boundary): persist uncommitted fill crawls across refused broadcasts and restarts — a processed fill whose derivation never commits (refused broadcast, aborted plan, pre-restart loss) lost its crawl permanently, so startup reconcile refilled the holes same-side instead of rotating (4 consumed buys re-bought at the filled prices after restart). Strategy now records every shift-eligible fill as a pending crawl (slot-level dedupe on push); derivations incorporate owed entries (deduped against the current batch, reserve slots excluded); any accepted non-null commit clears the record; startup applies owed crawls onto the restored boundary (validated placed-order-aware, reserve-aware, clears mark dirty on all paths) before reconcile, and drops them under a null boundary where the absolute fill anchor subsumes all history (
modules/order/strategy.ts,modules/order/utils/order.tsincl.consumePendingFillCrawls,modules/order/manager.ts,modules/order/utils/system.ts,modules/account_orders.tssnapshot field + loader,modules/dexbot_startup_runtime.ts,tests/test_pending_fill_crawls.tsPEND-001..010). - Fix(tests): two pre-existing suite failures — restored the
[COW] No actions neededdebug log in the empty-action guard (dropped when the structural-resync block was added, breaking COW-COMMIT-003) and added the nine reserve/crossing exports (resolveReserveCount,selectReserveEdgeSlots,resolveReserveFloorIds/CeilIds,geometryTypeForSlotIndex,isShiftEligibleFill,buildCrossingCheckCandidates,isCrossingCheckCandidate,chainOrderMatchesSlotWithTolerance,consumePendingFillCrawls) to the dynamic-weights ESM mock name list, whose stub predated the reserve API (modules/dexbot_class.ts,tests/test_dexbot_maintenance_runtime_dynamic_weights.ts). Full suite: 274/274. - Feat(tv): opt-in MPA price-feed charts and explicit source selection — market candles and settlement-feed history answer different questions, so the chart shortcut no longer always charts pool/order-book fills:
--feedcharts settlement-price history for MPA pairs (single MPA or MPA/MPA cross via both legs),--poolforces LP candles,--bookforces order-book fills, the default stays pool-first with order-book fallback, and prediction markets are rejected; an explicit--feedwarns on stale/unknown feed age (flat-chart risk) but still charts, while auto mode never routes to feed. Newmarket_adapter/inputs/kibana_feed_source.tsbuckets settlement-price publications into OHLC candles in backing-per-MPA units, matching the live feed-price convention (cross pairs forward-fill both legs into one quote series). Chart defaults:--scaledropped fromtvand the exporter (the chart already ships a persisted Log/Linear toggle and always opens on log), range highlight now off by default with--rangere-enabling it (a stored browser choice still wins) (scripts/tv.ts,analysis/tradingview/*,market_adapter/inputs/kibana_feed_source.ts, READMEs,tests/test_tv_feed_routing.ts,tests/test_kibana_feed_source.ts). - Feat(tv): range-aware candle bucket cache shared by the pool and feed chart paths — repeated chart fetches re-queried every window because each run anchors its range at floored-now, shifting all windows and invalidating the exact time-range cache match, and the price-feed path had no disk cache at all; new
market_adapter/inputs/window_cache.tsloads sibling chunks once and keeps in-range buckets, queries only missing buckets plus a bounded 48h tail refresh for late-indexed records, prunes leading no-trade gaps and already-queried ranges out of immutable history, extends sub-range queries by one bucket (an inclusive range would otherwise truncate the final bucket into a fake zero-volume candle; output is clamped to the window and fresh data wins by volume), deletes orphan chunks after complete runs (failures never delete), and opts forward-filled cross-rate data out of sub-range fetches (exact reuse still applies). Chunk metas record the ranges actually queried (meta.queriedRanges), so gap pruning consults real coverage instead of the file's overalltimeRange, which over-claimed after sub-range rewrites (legacy files fall back to theirtimeRangeclaim), and same-filename overwrites keep prior in-window coverage viapriorQueriedInWindow. The pool fetcher delegates to the shared planner/cache (manifest, retry and merge behavior unchanged, helper export names preserved), the feed fetcher gains a cached sequential path, and both modes print identical per-window progress through one formatter with per-page Kibana chatter and staging/render timing lines removed (market_adapter/inputs/fetch_lp_data.ts,market_adapter/inputs/kibana_feed_source.ts,scripts/tv.ts,tests/test_window_cache.ts,tests/test_fetch_lp_data_logic.ts). - Feat(reserve): anchor the reserve ladder at resolved min/maxPrice bounds (issue #25) — reserve BUYs ranked by raw price alone, so keep-low windows pushed them just above the active window while the minPrice floor zone stayed empty, the opposite of the static dip insurance the ladder is for; both edges now anchor toward their resolved config bound (floor toward minPrice, ceiling toward maxPrice). New
resolveReserveEdgeAnchorPrice(config, side)resolves numeric and"Nx"relative bounds viaresolveConfiguredPriceBoundand returns null when unresolvable so callers keep the previous rank behavior, while newcompareReserveEdgeis the single-source comparator for every reserve pick (finite anchor: in-bound slots first, nearest the anchor first, floor ascending / ceiling descending, stale out-of-bound slots last; null anchor: plain rank);resolveReserveFloorIds/resolveReserveCeilIds/selectReserveEdgeSlotstake an optional anchor, and all selection sites (target grid, initial activation, startup reconcile, edge pick, divergence corrections) resolve and pass the per-side anchor. Unresolvable bound, garbage input, or an unresolved"pool"/"book"startPrice degrades to the rank behavior; the documented limit at the time was that the anchor is the statically resolved config bound rather than the live grid's own rail bound (modules/order/utils/order.ts,modules/order/strategy.ts,modules/order/manager.ts,modules/order/grid_reconcile_internal.ts,modules/order/utils/system.ts,tests/test_reserve_orders.ts). - Feat(reserve): live-grid reserve edge anchors, closing the gap the config-bound anchor left open — a config-resolved anchor misses the rail the bot actually trades, because mode strings like
"pool"are unresolvable, relative multipliers need a reference price, and a reload can flip raw/resolved bounds, which left the floor zone empty unless a manual shelf was placed; newresolveLiveReserveEdgeAnchorPrice(manager, side)resolves in explicit tiers (the genesis ladder extreme the loaded grid was built from, then the live in-rail extreme viaresolveGapBand+isSlotInRail, then the config bound, then null for legacy rank).deriveTargetBoundaryandreserveEdgeIdSettake the anchor explicitly, and the strategy resolves both sides once and shares them between placement and the no-crawl fill classification, so the two can never disagree about which slots are reserves. The startup pending-crawl recovery path resolves the same anchors, so a restart cannot rank a stale below-rail slot as a reserve and silently drop a crawl the live run recorded as ordinary market movement (regression-pinned by a restart test whose crawl is provably applied with the live anchor and dropped with the config fallback). All runtime sites switched off the config-bound anchor: target grid, initial activation, startup reconcile, edge pick, divergence corrections, and pending-crawl recovery (modules/order/utils/order.ts,modules/order/strategy.ts,modules/order/manager.ts,modules/order/grid_reconcile_internal.ts,modules/order/utils/system.ts,tests/test_reserve_orders.ts,tests/test_pending_fill_crawls.ts). - Refactor(reserve): single-source reserve edge ordering —
resolveReserveFloorIds/resolveReserveCeilIdshand-rolled the ordering thatcompareReserveEdgedefines, so which slots count as reserves had two spellings and any drift would misclassify fills as no-crawl reserves; both helpers are deleted andreserveEdgeIdSetnow filters and sorts throughselectReserveEdgeSlots/compareReserveEdge, also keying on the canonical side type so a stale SPREAD placeholder is never ranked as an edge order (parity fuzz over both former resolvers, 864k cases, showed zero price-level divergences). The dynamic-weights ESM mock name list drops the deleted twins and picks upreserveEdgeIdSet,resolveLiveReserveEdgeAnchorPrice, andcompareReserveEdge(modules/order/utils/order.ts,modules/order/grid_reconcile_internal.ts,tests/test_reserve_orders.ts,tests/test_dexbot_maintenance_runtime_dynamic_weights.ts). - Fix(reserve): activate reserves only with their stored size, and hold back the missing reserve share at startup — the reserve picker re-derived per-slot sizes from a different slot list than the strategy (its own SPREAD-inclusive, in-rail derivation), so a startup-placed reserve could be sized by one rule and corrected by another; activation now requires the slot's own stored size to satisfy the minimum, re-types the pick to the activation side, and never derives a size locally. Startup reconcile holds back only the reserve share still missing on-chain (
reserveCountminus reserves already placed) from its window plan, so an edge pick that is not activatable yet leaves its slot unplanned for the target-grid sizing pipeline instead of the plan parking a middle window slot there that the next cycle would have to rotate out; a live reserve is already part of matched-on-grid and never shrinks the window plan (fresh-grid deficit 5 → 3 window placements with 2 deferred; steady state unchanged at 5; two live reserves with an empty window → the full 3-slot window plan) (modules/order/grid_reconcile_internal.ts,modules/order/grid.ts,tests/test_reserve_orders.ts). - Fix(boundary): keep owed fill crawls hold-aware and reload-safe — the pending-crawl ledger records every shift-eligible fill as a relative boundary delta, consumed by a commit that uses the plan's boundary and replayed when a refused broadcast, aborted plan or restart leaves the crawl owed; auditing that contract against the committed-boundary writers found five leaks and one doc drift. A refill hold pins the committed boundary over the plan's target, so the shift those records encode was never applied — the commit clear now requires
boundaryHeld !== trueand routes through_clearPendingFillCrawls(), which logs the drop and marks the grid dirty so it reaches disk, with the hold flag hoisted in the COW runtime and passed to both commit sites so the uncertain-broadcast poll path cannot keep a pinned boundary while dropping its records. Reserve ladder orders live outside the boundary contract, so a skipped reserve CREATE must not pin geometry —collectRefillSlotIds()is now the single producer of the refill wire, shared by the COW plan path and the divergence fold, and fails open when reserve classification is unavailable. The rotation size-validation skip now records its slot ids like its five sibling skip sites, so the skip set and boundary hold see it.applyPersistedPendingCrawls()is shared by startup and recovery, and recovery applies stored crawls before itspersistGrid, which would otherwise write the empty in-memory array over them and erase the owed movement without applying it. A grid rebuild, a rejected snapshot andclearGridre-anchor the boundary absolutely, so relative deltas from the previous generation are dropped in memory and on disk. Docs: fund-driven sizing with a fill-driven boundary recorded inmodules/README.mdanddocs/COW_INVARIANTS.md(INV-COW-006/007/008 cover boundary ownership, the refill hold with reserve exclusion, and the owed-crawl lifecycle). Crawls are only retained on paths that provably derived nothing, so a drop cannot strand a slot the plan already moved past; reload paths apply deltas onto the boundary they were recorded against (modules/order/manager.ts,modules/dexbot_cow_runtime.ts,modules/order/utils/order.ts,modules/order/utils/system.ts,modules/dexbot_startup_runtime.ts,modules/dexbot_state_recovery.ts,modules/order/grid.ts,modules/account_orders.ts,tests/test_cow_boundary_hold.tsHOLD-009,tests/test_pending_fill_crawls.tsPEND-012..018 incl. the replay-exactly-once generation invariant,tests/test_reserve_orders.tsrefill-wire cases). - Fix(boundary): rail-gate startup placement, fold owed crawls into static rebuild centers, classify all deferred holds as non-blocking — startup placement could diverge from runtime activation, a static rebuild center discarded owed fill-crawl direction, a stale numeric recovery center could pin the boundary to a rail edge, and deferred chain-order holds were classified by exact reason string, letting a new defer reason re-freeze the pipeline.
getInitialOrdersToActivatenow filters window and reserve candidates through the sharedisSlotInRailgeometry (same predicate as_pickVirtualSlotsToActivate/_pickEdgeReserveSlots, fail-open for unknown geometry), so a stale stored rail can never be placed on the wrong side of the boundary. On a rebuild centered on a static config value (startPrice numeric, or AMA-driven whose live snapshot offsets the center — the gridPrice bounds reference does not make the ladder center fresh), owed fill crawls are folded into the rebuild center (oneincrementPercentstep per net crawl, reserve fills excluded, re-clamped to the post-guard bounds) before_clearPendingFillCrawls('grid rebuild'); a live-derived startPrice drops them because the derived price already contains the movement.deriveTargetBoundaryrejects a Tier-2/Tier-3 reference that falls outside the live rail and falls through to the bounded Tier-4 rail center instead of pinning an edge (Tier-1 fill anchors stay exempt — live market wins). The pending-crawl ledger records nothing underdryRunand caps after push at exactly 500, matching the persistedslice(-500)cap. A new sharedisNonBlockingUnmatchedOrderclassifies any*-deferredreason as a deliberate hold, used at all three blocking sites (validateCreateTargetSlots, the COW pre-broadcast gate, snapshot recovery) —boundary-unknown-deferredis now correctly non-blocking (transient and re-evaluated), so a new defer reason cannot silently regress into a permanent blocker; hold counts (unmatchedChainOrders/heldChainOrders/blockingChainOrders) surface ingetMetrics, the shutdown summary, and a deduped periodic[HOLD]warning. Newtests/test_hold_and_center_guards.ts(HOLD-001, CENTER-001..003, PEND-CAP-001, REBUILD-FOLD-001..004 including the static-center fold and the mixed-mode gridPrice cases, RAIL-GATE-001),tests/test_sync_out_of_grid_defer.tsextended with OUT-OF-GRID-007 (modules/order/manager.ts,modules/order/grid.ts,modules/order/strategy.ts,modules/order/utils/order.ts,modules/order/utils/validate.ts,modules/dexbot_cow_runtime.ts,modules/dexbot_state_recovery.ts,modules/dexbot_maintenance_runtime.ts,modules/dexbot_class.ts). - Feat(grid): bidirectional grid-regeneration trigger (grow + shrink on fund removal) — the 3% available-funds trigger only fired upward, so after an external fund removal the grid stayed over-allocated until the next fill forced a resize. The divergence check now also flags a side when its grid-tracked size exceeds the botFunds-capped allocation by
GRID_REGENERATION_PERCENTAGE, reusing the existing COW resize path (modules/order/grid.ts,modules/constants.ts,modules/dexbot_maintenance_runtime.ts,tests/test_grid_logic.ts,docs/FUND_MOVEMENT_AND_ACCOUNTING.md,docs/GRID_RECALCULATION.md). - Fix(credit): show short offer id behind avail funds in Curr. CR line — Curr. CR lines now append the live offer's short numeric id in grey (e.g.
| 53.76K BTS (123)), falling back to the offer object id when the ranked id is missing; no suffix when no offer is found (scripts/analyze-credit.ts). - Fix(tv): namespace TradingView chart prefs per pool/pair — all generated TradingView charts shared one localStorage key, so opening a chart for one pair applied another pair's saved timeframe, indicators, scale, and pair orientation. The prefs key is now v3 namespaced per chart (pool, asset ids, base interval), computed at export time; the uPlot price/volume cursor-sync key is split into its own constant so namespacing prefs cannot break pane sync; asset nodes without id/symbol no longer collapse distinct charts onto
[object Object](analysis/resolve_source.ts,analysis/tradingview/*,tests/test_tradingview_chart_storage_key.ts).
2026-09-09
-
Fix(tradingview): range band ignores span slider on grid-less charts — pair/pool charts render with
grid: null, which hid the span slider (display:none) and dropped the band into the uncontrollable ±2% width envelope instead of the 1.25x–2.0x slider span; slider now always renders inline (retagged grid → span), the no-grid fallback builds a symmetric AMA/span-AMA×span base (grid config supplies only tilt/guard params, never a price), and per-bar scaling embedscomputeAmaSlopeClipThresholdverbatim so the band clips raw AMA slope at the adaptive 90th-percentile threshold beforeapplyAsymmetricBounds+applyNarrowingSideGuard— grid and pool charts share one path, matching the live grid pipeline (analysis/tradingview/tradingview_uplot_chart_generator.ts). -
Tune(range): widen orange range zone to 1.40x, lower TradingView slider floor —
RANGE_QUALITYORANGE_MIN / RED_MAX 1.45 → 1.40 (suizidal starts below 1.40x, orange tight zone widens 0.10 → 0.15) with the range legend now built fromRANGE_QUALITYas single source of truth; TradingView range-span slider floor 1.25x → 1.2x (slider min, tooltip, all server- and client-side clamps, README flag row) (modules/constants.ts,modules/account_bots.ts,analysis/tradingview/*). -
Feat(reserve): per-side reserve ladder, edge-pinned dip/spike insurance (issue #25) — no way to rest live BUY/SELL orders far outside the active window for crash wicks and fat fingers (widening minPrice relocates the window, activeOrders counts from the rail); new
reserveOrders: { buy, sell }(default{0,0}, 0 disables per side; editor menu 5 Funding prompts both counts, non-negative-integer validation, legacy numeric form migrates to{ buy: n, sell: 0 }). Shared helpers inmodules/order/utils/order.ts(single source):resolveReserveCount/resolveReserveOrders,resolveReserveFloorIds/resolveReserveCeilIds(price-rank edge sets, boundary-independent),selectReserveEdgeSlots(floor-first / ceiling-last, skips windowed ids);deriveTargetBoundaryfilters reserve-edge fills so reserves never crawl; placement is window + edge union (middle stays VIRTUAL) acrossstrategy.ts,utils/system.ts,manager.ts, andgrid_reconcile_internal.ts(startup desired split + edge-cancel-last for unmatched orphans and matched excess); fee/count maintenance counts reserves once (grid.ts,grid_reconcile.ts,accounting.ts,dexbot_maintenance_runtime.ts,export.ts). Newtests/test_reserve_orders.ts(per-side clamp, floor/ceiling anchors, both-edges no-crawl, window+edge union, off-means-window-only). -
Fix(tradingview): invert range band colors to red-above, green-below — the range envelope around AMA painted the upper segment green and the lower segment red; swaps
UP_FILL/DOWN_FILL(plus boundary strokes) to the correct convention (chart cosmetics only,analysis/tradingview/*). -
Fix(sync): defer out-of-grid orphans instead of clamping onto edge slots (issue #24) —
slotIndexForPriceclamps below/above-grid prices onto slot-0/slot-(N-1), so the genesis Pass-2 path mis-adopted the first sub-grid orphan into the rail slot (overwriting the live orderId and poisoning slot bookkeeping) and queued every further same-zone orphan as a duplicatecancelOnly, wrongfully cancelling live correctly-priced orders; newisChainPriceOutOfGridguard defers out-of-range chain orders with reasonout-of-grid-deferred(no adopt, no cancel) while exact in-rail orphans still adopt (modules/order/sync_engine.ts,modules/order/utils/math.ts,tests/test_sync_out_of_grid_defer.tsOUT-OF-GRID-001..005). -
Fix(sync): keep out-of-grid holds from freezing creates and refills — holds are permanent by design, so three paths keyed on "any unmatched order" froze around them:
validateCreateTargetSlotsflaggedchain_orphan_collisionon the hold's clamped candidate slot (permanently blocking that rail refill), the COW pre-broadcast gate rejected every CREATE batch (UNMATCHED_CHAIN_ORDERS), and snapshot recovery rejected the persisted grid (full grid reset required) on every restart while a hold existed; all three now filterreason !== 'out-of-grid-deferred'(holds stay visible to crossing guards and sync but block nothing), plus auto-cancel idle wording corrected and live order ids replaced with synthetic1.7.91xxxxin tests (modules/order/utils/validate.ts,modules/dexbot_cow_runtime.ts,modules/dexbot_state_recovery.ts, OUT-OF-GRID-006,tests/test_uncertain_broadcast.tsUNC-016f which fails pre-fix withgrid inconsistent after reload: 1 unmatched remain). -
Docs(reserve): reserve ladder references across user docs and removal of the stale boundary-sync section — the per-side
reserveOrdersfeature had no user-facing documentation outside code comments and the changelog, and the architecture doc still described the fund-driven boundary sync deleted in 1.5.3; the README Bot Options Reference gains areserveOrdersrow (floor/ceiling, default{buy: 0, sell: 0}, editor menu 5 Funding) with the S/B display notation aligned, and GRID_RECONCILE, COW_INVARIANTS, the developer guide glossary (new Reserve term), FUND_MOVEMENT_AND_ACCOUNTING (the active-order count includes reserves for the BTS fee budget), MPA_CREDIT_USAGE, and DEXBOT2_VS_POWER_LAW_CURVE each gain a code-grounded line (counts re-verified againstgrid_reconcile.ts,getActiveOrdersTotal,accounting.ts, and the maintenance runtime);docs/architecture.mddrops the obsolete Fund-Driven Boundary Sync section (58 lines) with no remaining references in the README, architecture, reconcile, developer guide, or workflow docs (README.md,docs/*).