Skip to content

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

Choose a tag to compare

@froooze froooze released this 12 Sep 07:31
· 112 commits to test since this release
6924afd

[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; matchedExcess is now filtered to parseSlotIndex(id) !== null in both planOnly and execute branches. Tests: getTargetedSyncReason block (surplus with empty floor reserves fires buy 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; _recalculateGridOrderSizesFromBlockchain skips non-slot-N slots in the per-slot loop (same parseSlotIndex gate 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.ts Test 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.ts manages a failure ledger with LIVE_FAILURE_HEALTH_SUCCESS_STREAK (2 consecutive probe successes to reset, transport-sourced strikes clear on first success); bitshares-native/transport.ts feeds strikes via onNodeFailure observer (abnormal closes as "connection" strikes, keep-alive trips as "keep-alive" with follow-up suppression, preferred-candidate pass in tryConnect); node_manager.ts applies strikes and resets; dexbot_cow_runtime.ts / dexbot_fill_runtime.ts / subscriptions.ts handle broadcast deferrals and fill rebalancing; docs/COW_INVARIANTS.md updated (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 stamps manager._orphanFillsCreditedAt to widen fund-invariant tolerance (x5) for that cycle only. New _fundDriftLedger records one-sided drift; _tryTrustChainFreeHeal() seeds tracked free balance from chain total minus committed grid sizes, gated by FUND_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 so no 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 reserveOrders increase 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; new countLiveReserveOrders (canonical reserveEdgeIdSet + 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 in getTargetedSyncReason, budget-gated like the window check. Docs: new "Editing a running bot" subsection in docs/BITSHARES_ONBOARDING.md mirroring BOT_LIVE_CONFIG_KEYS (live keys apply in ~1min, geometry needs dexbot 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 1M timeframe with true UTC-calendar-month bucketing in aggregateCandles and 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 queriedRanges genuinely 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 the feeds suffix and disable the switch via the volumeIsCount payload 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 pubs to feeds; badge label shortened from V max to max (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.json no longer written, legacy files still read); new fetchRangeWithRetry (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; kibanaSearch retries transient errors (3 attempts) for one-shot queries, kibanaMaxPages (500) runaway guard on paged fetchers, bidirectional fetch tolerates one-direction failure; single isTransientNetworkError/sleepMs (modules/utils/errors.ts) and single slugPart (market_adapter/interval_utils.ts); dead consolidateByTimestamp key removed (new tests/test_fetch_book_data.ts, tests/test_kibana_candles.ts cases; 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 (isSlotInRail is fail-open for unparseable ids); reserveEdgeIdSet, resolveLiveReserveEdgeAnchorPrice, _pickEdgeReserveSlots, and pickEdgeReserves now gate to parseSlotIndex(id) !== null so 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) — countLiveReserveOrders classified reserves without excluding window members while every placement picker passes window ids as excludeIds, 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; new liveWindowIdSet helper derives window member ids from master rail geometry (sliced to activeOrders, fail-open on unknown boundary) and reserveEdgeIdSet takes an optional excludeIds forward (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 omits releaseUntrackedFunds for 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.json edits 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. New checkAndApplyBotConfigChanges runs 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 allowlist BOT_LIVE_CONFIG_KEYS (activeOrders, reserveOrders, botFunds, weightDistribution, min_BTS_value, debtPolicy) into bot.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 naming dexbot 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.ts fingerprint fields, modules/account_bots.ts, tests/test_dexbot_maintenance_runtime_market_adapter_watchdog.ts).
  • Feat(live-config): live-apply debtPolicy behind 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-op loadState when 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 replaces bot.config with a new object, which left CreditRuntime.config pointing at the stale copy so reset-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, explicit trigger: true/false still overrides, and previewBotSettingsUpdate.triggerRequired follows 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_KEYS lives in modules/runtime_settings.ts (consumed by the maintenance runtime, the editor hint, and the Claw trigger gate); one cloneJsonValue, one diffBotConfigEntries (replacing diffLiveBotConfig + detectNonLiveBotConfigChanges), one shared replaceBotConfigFromEntryPreservingRuntime for the resync path; dead _appliedBotLiveSnapshot field 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 every price >= reference comparison 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. deriveTargetBoundary now 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); calculateIdealBoundary fails toward rail-center on non-numeric references instead of the rail top (modules/order/utils/order.ts, modules/order/strategy.ts forwards genesisStartPrice, tests/test_boundary_anchor_recovery.ts ANCHOR-001..009 including a replay of both incident batches).
  • Fix(recovery): erase the poisoned persisted boundary on unrecoverable GRID-LOAD rejection — storeMasterGrid never persists a null boundary, so the rejected value survived every flush and re-armed the identical rejection on every restart; new explicit-only AccountOrders.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 needsResync from 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.ts incl. consumePendingFillCrawls, modules/order/manager.ts, modules/order/utils/system.ts, modules/account_orders.ts snapshot field + loader, modules/dexbot_startup_runtime.ts, tests/test_pending_fill_crawls.ts PEND-001..010).
  • Fix(tests): two pre-existing suite failures — restored the [COW] No actions needed debug 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: --feed charts settlement-price history for MPA pairs (single MPA or MPA/MPA cross via both legs), --pool forces LP candles, --book forces order-book fills, the default stays pool-first with order-book fallback, and prediction markets are rejected; an explicit --feed warns on stale/unknown feed age (flat-chart risk) but still charts, while auto mode never routes to feed. New market_adapter/inputs/kibana_feed_source.ts buckets 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: --scale dropped from tv and the exporter (the chart already ships a persisted Log/Linear toggle and always opens on log), range highlight now off by default with --range re-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.ts loads 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 overall timeRange, which over-claimed after sub-range rewrites (legacy files fall back to their timeRange claim), and same-filename overwrites keep prior in-window coverage via priorQueriedInWindow. 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 via resolveConfiguredPriceBound and returns null when unresolvable so callers keep the previous rank behavior, while new compareReserveEdge is 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/selectReserveEdgeSlots take 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; new resolveLiveReserveEdgeAnchorPrice(manager, side) resolves in explicit tiers (the genesis ladder extreme the loaded grid was built from, then the live in-rail extreme via resolveGapBand + isSlotInRail, then the config bound, then null for legacy rank). deriveTargetBoundary and reserveEdgeIdSet take 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/resolveReserveCeilIds hand-rolled the ordering that compareReserveEdge defines, so which slots count as reserves had two spellings and any drift would misclassify fills as no-crawl reserves; both helpers are deleted and reserveEdgeIdSet now filters and sorts through selectReserveEdgeSlots/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 up reserveEdgeIdSet, resolveLiveReserveEdgeAnchorPrice, and compareReserveEdge (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 (reserveCount minus 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 !== true and 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 its persistGrid, 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 and clearGrid re-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 in modules/README.md and docs/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.ts HOLD-009, tests/test_pending_fill_crawls.ts PEND-012..018 incl. the replay-exactly-once generation invariant, tests/test_reserve_orders.ts refill-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. getInitialOrdersToActivate now filters window and reserve candidates through the shared isSlotInRail geometry (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 (one incrementPercent step 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. deriveTargetBoundary rejects 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 under dryRun and caps after push at exactly 500, matching the persisted slice(-500) cap. A new shared isNonBlockingUnmatchedOrder classifies any *-deferred reason as a deliberate hold, used at all three blocking sites (validateCreateTargetSlots, the COW pre-broadcast gate, snapshot recovery) — boundary-unknown-deferred is 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 in getMetrics, the shutdown summary, and a deduped periodic [HOLD] warning. New tests/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.ts extended 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 embeds computeAmaSlopeClipThreshold verbatim so the band clips raw AMA slope at the adaptive 90th-percentile threshold before applyAsymmetricBounds + 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_QUALITY ORANGE_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 from RANGE_QUALITY as 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 in modules/order/utils/order.ts (single source): resolveReserveCount/resolveReserveOrders, resolveReserveFloorIds/resolveReserveCeilIds (price-rank edge sets, boundary-independent), selectReserveEdgeSlots (floor-first / ceiling-last, skips windowed ids); deriveTargetBoundary filters reserve-edge fills so reserves never crawl; placement is window + edge union (middle stays VIRTUAL) across strategy.ts, utils/system.ts, manager.ts, and grid_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). New tests/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) — slotIndexForPrice clamps 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 duplicate cancelOnly, wrongfully cancelling live correctly-priced orders; new isChainPriceOutOfGrid guard defers out-of-range chain orders with reason out-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.ts OUT-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: validateCreateTargetSlots flagged chain_orphan_collision on 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 filter reason !== '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 synthetic 1.7.91xxxx in tests (modules/order/utils/validate.ts, modules/dexbot_cow_runtime.ts, modules/dexbot_state_recovery.ts, OUT-OF-GRID-006, tests/test_uncertain_broadcast.ts UNC-016f which fails pre-fix with grid 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 reserveOrders feature 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 a reserveOrders row (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 against grid_reconcile.ts, getActiveOrdersTotal, accounting.ts, and the maintenance runtime); docs/architecture.md drops 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/*).