dexbot 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/analysis/README.md +2 -2
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/grid_correction_check.ts +80 -7
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
- package/analysis/tradingview/h-bts_tradingview.html +1570 -0
- package/analysis/tradingview/t-bts_tradingview.html +1570 -0
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +19 -5
- package/analysis/trend_detection/package.json +1 -1
- package/claw/package.json +1 -1
- package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
- package/claw/runtimes/openclaw-plugin/package.json +1 -1
- package/claw/tests/test_claw_mcp_transport.ts +2 -2
- package/dist/analysis/grid_correction_check.d.ts +5 -1
- package/dist/analysis/grid_correction_check.d.ts.map +1 -1
- package/dist/analysis/grid_correction_check.js +81 -7
- package/dist/analysis/grid_correction_check.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +19 -5
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_book_data.js +2 -2
- package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts +22 -3
- package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
- package/dist/market_adapter/inputs/fetch_lp_data.js +15 -15
- package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
- package/dist/market_adapter/inputs/kibana_feed_source.js +2 -2
- package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
- package/dist/market_adapter/inputs/window_cache.d.ts +54 -8
- package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
- package/dist/market_adapter/inputs/window_cache.js +328 -87
- package/dist/market_adapter/inputs/window_cache.js.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts +1 -1
- package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
- package/dist/modules/bitshares-native/subscriptions.js +18 -4
- package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
- package/dist/modules/bitshares_client.d.ts.map +1 -1
- package/dist/modules/bitshares_client.js +10 -0
- package/dist/modules/bitshares_client.js.map +1 -1
- package/dist/modules/constants.d.ts +7 -3
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +44 -20
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credit_runtime.d.ts.map +1 -1
- package/dist/modules/credit_runtime.js +24 -6
- package/dist/modules/credit_runtime.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +32 -7
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +55 -11
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +137 -7
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +955 -45
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +28 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +116 -3
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +46 -0
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +193 -6
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +6 -1
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +16 -3
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/order/grid.d.ts +3 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +47 -6
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +39 -4
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +3 -1
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +200 -15
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +60 -14
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +87 -20
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +95 -7
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +277 -15
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/utils/errors.d.ts +22 -0
- package/dist/modules/utils/errors.d.ts.map +1 -1
- package/dist/modules/utils/errors.js +30 -0
- package/dist/modules/utils/errors.js.map +1 -1
- package/docs/COPY_ON_WRITE_MASTER_PLAN.md +1 -1
- package/docs/DEXBOT_COMPARISON.md +3 -7
- package/docs/EVOLUTION.md +9 -7
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -9
- package/docs/LIFECYCLE.md +2 -2
- package/docs/README.md +9 -1
- package/docs/architecture.md +7 -7
- package/docs/developer_guide.md +4 -4
- package/market_adapter/README.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.6.3] - 2026-09-14 - Correction-Queue Staleness Guard, Grid-Checker Price Epochs, Final Pivot Gate
|
|
6
|
+
|
|
7
|
+
### 2026-09-15
|
|
8
|
+
|
|
9
|
+
- **Fix(guard)**: final pre-broadcast pivot gate — a fill queued AFTER the batch-start pivot freeze but BEFORE broadcast passed every per-action LAST-FILL-GUARD check on a stale pivot (live incident on a market-pair bot: freeze at .745, sell fill queued at .765, batch of 4 broadcast at .910 against a buy pivot; the violating sell rotation filled 6s later, 0.6% below the true sell threshold, and tripped the fund invariant on drain). New `runFinalPivotGate()` re-checks BUILT ops against a re-refreshed pivot after the op-building loop and before the batch summary, fund validation, and single-flight claim: unchanged pivot is a pure no-op (arrays untouched, no extra probes); a moved/armed pivot re-runs the guard on each op's final price with the build loop's bypass rules (spread-correction CREATEs incl. batch-origin fallback, stamped gap-evacuations; unstamped evacuations guarded normally, same-slot UPDATEs resolve via source-id fallback) and drops violators into the existing skipped-slot restore paths (rotations restore source+dest from master, creates feed the refill-hold intersect, dropped CREATE pending entries removed by slot). Fail-open throughout: unresolvable price/type, cold pivot, and refresh throws all KEEP the op; cancels and size-updates are never gated. Pair-mode/chunk grouping is computed lazily at broadcast from the filtered opContexts (no stored indexes go stale); `cancelOpIndexByOrderId` (no live readers past op-building) is rebuilt from kept contexts as defence-in-depth. Pending-index hygiene: the gate's compaction REMAPS this batch's kept pending-broadcast entries' stored opIndex/ctxIndex to the compacted positions (lockstep keeps the two arrays aligned with each other but does not rewrite indexes stored inside pending entries — an unremapped ctxIndex would resolve to a shifted context after the first drop, letting the uncertain-broadcast reconcile adopt a matched chain order into the wrong slot); entries are identified by the fingerprints collected at record time (`recordPendingBroadcast` now returns its fingerprint; `entry.batchId` is always null in production and cannot discriminate), and entries whose referenced op was dropped are removed. Gate stats are kept separate from the build loop's counters (own `finalGateStats` object; reported as `gateChecked/gatePassed/gateSkipped/gateBypassed` fields on the batch summary, omitted when the gate did not re-check) so the summary's checked/passed/skipped totals stay the build loop's verdicts. The frozen pivot is captured AFTER the batch-start refresh (the refresh is part of the freeze) so batches whose freeze picked up a pre-freeze queued fill no longer trigger a spurious "pivot moved" warn + redundant re-check. Observability: always-on `Final gate: queue a->b pivot x->y` at debug, pivot-moved line at warn with freeze-queue depth + drop count, per-drop lines at warn. New `tests/test_final_pivot_gate.ts` (FG-1..FG-11, incl. incident replay dropping both violating rotations, the same-slot fallback, and pending-index remap identity checks); neighbor suites green (`modules/dexbot_cow_runtime.ts`).
|
|
10
|
+
|
|
11
|
+
### 2026-09-14
|
|
12
|
+
|
|
13
|
+
- **Fix(correction-queue)**: validate queued price corrections against the live slot before broadcast — corrections are queued as snapshots (expected price/size at detect time), but a geometry-changing resync re-slots/re-prices orders without invalidating the queue, so the maintenance pre-gate drain replayed stale snapshots and broadcast UPDATEs that reverted resync placements back onto occupied levels. Entries are now validated pre-broadcast against the live slot (ownership, price via slot predicates, size via integer quantum); stale entries are dropped and self-heal via the next sync re-queue. Zero-delta `updateOrder` returns `{success:true, skipped:true}` so routine no-ops no longer count as permanent failures; queue removal/dedupe is keyed on `(chainOrderId, isSurplus)` so sibling entries sharing a chain id survive; `queuedAt`/`queuedBy` provenance is stamped at queue time (sync detectors + gap-evacuation); drain summaries report `staleDropped` to keep failure WARN a true signal. New `tests/test_correction_queue_staleness.ts` (11 cases); 10 neighbor suites green (`modules/order/utils/order.ts`, `modules/order/sync_engine.ts`, `modules/dexbot_cow_runtime.ts`, `modules/dexbot_maintenance_runtime.ts`, `modules/order/manager.ts`).
|
|
14
|
+
|
|
15
|
+
- **Fix(checker)**: split grid-correction checker fills into price epochs across repriced order lifetimes — the checker combined fills from different lifetimes of one order id after native repricing (limit-order update) operations and reported a false monotonicity violation. It now queries limit-order update operations and splits fills into chronological price epochs, preserving weighted aggregation for partial fills within one unchanged epoch (`analysis/grid_correction_check.ts`, `analysis/README.md`; TypeScript build passes, historical checker run clean).
|
|
16
|
+
|
|
17
|
+
## [1.6.2] - 2026-09-14 - Grid-Price Invariant, Gap-Slot Batch Sizing, Candle Shard Cache, Recovery Tolerance, Fill Counter Hygiene, Node List Cleanup
|
|
18
|
+
|
|
19
|
+
### 2026-09-14
|
|
20
|
+
|
|
21
|
+
- **Fix(grid)**: enforce grid-price invariant at emission sites + escalate persistent corruption and stranded holds — the engine had two sources of truth for a slot's price — the genesis ladder (`priceForSlot(idx, genesis)`) and the mutable `slot.price` field — and every violation was the second winning over the first. Five mechanisms fed it: orphan adoption overwrote the slot's identity price (S1), a pre-broadcast "freshness" step re-substituted that corrupted price at debug level (S2), guards checked range membership but never grid membership (S3), the fill-guard pivot was written from unvalidated fill prices (S4), and guard bypasses skipped even that (S5). New `checkGridPriceInvariant` / `reportGridPriceInvariant` (`modules/order/utils/order.ts`) require the emitted price for a slot to equal its genesis level (fail-open on everything unjudgeable: no genesis, unparseable id, out-of-ladder index, non-finite price, checker error), enforced blocking at all six emission sites: CREATE, UPDATE (rotation), CREATE-FALLBACK (`modules/dexbot_cow_runtime.ts`); RECONCILE-CREATE, RECONCILE-UPDATE, STARTUP-CREATE (`modules/order/grid_reconcile_internal.ts`). Rotation UPDATE derives the emitted price from the destination slot's genesis level (`deriveRotationPrice`) instead of trusting the planner's carried `newPrice`; both pre-broadcast price substitutions removed (drift reported at warn, never adopted); legacy orphan adoption no longer writes `chainOrder.price` into `slot.price` and gains a rail guard; materialize path derives slot price (and side) from the ladder; `loadGrid` repairs a mismatched slot price from the genesis ladder in both validation modes; fill-guard pivot validated onto the ladder (`resolveOnGridPivot`: near-ladder pivots snap, far off-ladder fail open and are counted as `pivotOffGrid`). Persistent invariant rejection escalates after `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` consecutive rejecting batches per slot (bot-scoped streak, reset on clean check, 15m cooldown) to `requestStructuralGridResync('grid-price-invariant-violation')`. Out-of-bounds policy: hold and surface — grid geometry is not invalidated by the market leaving it (the removed 5% placement gate is documented as a design deadlock, not a tuning problem). New `docs/GRID_PRICE_INVARIANT.md` plus `docs/README.md` index entry and `CONSOLIDATED_ORPHAN_FIX_SUMMARY.md` status refresh. Tests: new `tests/test_grid_price_invariant_guard.ts` (GPI-001..015) and `tests/test_grid_price_invariant_wiring.ts` (GPI-WIRE-001..008) plus `tests/test_startup_and_guard_log_hygiene.ts` (GPI-LOG-001); every behavioral fix mutation-tested (11 escalation/guard mutations verified); audited over 1,153 slots across five geometries with zero rejections; full suite 282/282 pass (`modules/order/utils/order.ts`, `modules/dexbot_cow_runtime.ts`, `modules/order/grid_reconcile_internal.ts`, `modules/order/grid.ts`).
|
|
22
|
+
|
|
23
|
+
- **Fix(fill)**: retire deferred-retry counter on queue settle + pin COW finally drain — the fill-consumer deferral counter (`_deferredFillRetryWaits`) only retired when a pending retry timer fired against an already-empty queue, so a queue drained by any other path left the counter elevated and the deferral log cadence stuck at warn (every 12th defer) after the backlog cleared. The counter now retires in `consumeFillQueue` at both settle points (top-of-run empty early-return and tail-after-drain); `_deferredFillRetryTimer` declared/initialized on the bot class and cleared on shutdown so the unref'd timer cannot leak across lifecycle restarts. New RETRY-007 pins the counter reset on a clean drain (7 → 0 on settle); RETRY-006 pins the COW batch finally (injected `_ensureCredentialDaemonWritable` throw after `_batchInFlight++` / `_cowBroadcastInFlight` asserts the finally releases both and reschedules the fill consumer exactly once; the COW layer converts the throw into a handled `{executed:false}` abort via `_handleBatchHardAbort`) (`modules/dexbot_fill_runtime.ts`, `modules/dexbot_class.ts`, `tests/test_fill_defer_retry.ts`; neighbors green, `tsc --noEmit` clean).
|
|
24
|
+
|
|
25
|
+
- **Fix(recovery)**: tolerate transient in-band stranding in persisted-boundary gate — a mid-evacuation persisted snapshot (honest boundary one crawl stale, live order stranded in-band by SPREAD GUARD) failed the restore gate's `placed_order_in_band` check and forced `rms_structural_grid_resync`, wiping boundary progression, owed crawls, streaks, and the queued GAP-EVAC plan on every uncertain-broadcast recovery (observed: boundary regressed five slots 93 → 88, five owed fill crawls dropped, one live chain order left unmatched, three structural resyncs in twelve minutes). The gate's premise ("honest writers never strand") is disproven: SPREAD GUARD keeps live rail orders in-band across fill-driven boundary crawls by design, and GAP-EVAC heals them per-slot over 2–3 cycles — stranding alone is not a poison signature. `validateBoundaryCommit` now records in-band placements instead of early-returning, `crossed_book_geometry` takes precedence over a co-occurring strand, and the full scan no longer skips rail slots after the first strand; new `isTransientInBandRejection()` (pure stranding, any count, no structural signal) with shared constant `BOUNDARY_REJECT_PLACED_IN_BAND`. `recoverFromPersistedGrid` tolerates transient stranding with a warn and proceeds with load (true poison still refuses); `loadGrid` gains optional `options.tolerateTransientStranding` (default strict, existing callers unchanged). Startup strict path degrading a mid-evacuation strand to boundary-less is a known follow-up; runtime backstops (GAP-EVAC streaks + cancel-only teeth, commit-time gate, P4 fund-drift snapshot reject) own persistent cases (`modules/order/utils/math.ts`, `modules/dexbot_state_recovery.ts`, `modules/order/grid.ts`, `tests/test_boundary_restore_validation.ts` 9/9; gap-evac, persistence, bloat, crawl, uncertain-broadcast suites pass; `tsc --noEmit` clean).
|
|
26
|
+
|
|
27
|
+
- **Fix(nodes)**: prune dead default nodes and single-source the connection-trace node list — `NODE_MANAGEMENT.DEFAULT_NODES` drops three dead endpoints, leaving seven live defaults; `tests/test_connection_trace.ts` no longer carries its own hardcoded copy — it reads `NODE_MANAGEMENT.DEFAULT_NODES` with an optional `BITSHARES_TRACE_NODE` override for single-node tracing, and the transport login probe uses `NODES[0]` instead of a hardcoded URL, so future node-list changes propagate automatically (`modules/constants.ts`, `tests/test_connection_trace.ts`).
|
|
28
|
+
|
|
29
|
+
- **Test(seams)**: eliminate wall-clock sleeps and live-chain hangs from offline tests — offline unit tests slept on production pacing delays and opened real WebSocket connections (suite past 125s, several files over 5s each). New production test seams with defaults unchanged: `createSubscriptionManager` accepts `overrides.noticeCoalesceMs` (page-timeout watchdog unref'd), COW `options.pollIntervalMs` for the 1.5s missing-create poll (set/restored via try/finally across every batch exit path incl. re-plan recursion), `scheduleDeferredFillRetry`/`parkFillsForTotalsRetry` `retryDelayMs`, credit-split `settleDelayMs` plus overridable collateral-balance fetch, sync-engine targeted-refetch fns plus `_skipEmptyReadConfirmDelay`, and `disconnectClient` stopping node health monitoring on teardown. New `resolveSeamMs()`/`resolveSeamMsOrNull()` (`modules/utils/errors.ts`) consolidate seam resolution (explicit 0 disables the delay; null/undefined falls through to the default — fixes `retryDelayMs` `> 0` silently dropping explicit 0 and `Number(null) === 0` selecting 0); each caller records its resolved delay so tests assert resolution instead of wall-clock timing (write-only in production). New `tests/test_seam_resolve.ts` plus COW-COMMIT-012 (seam restore on the guard-refusal path, precedence explicit > bot > default, explicit 0 honored) and RETRY-003b / TOTALS-004 (0 resolves to 0 — the only discriminating input for `||`-vs-`??` regressions, verified by fault injection); two stale pre-eager-gap-recovery mock expectations corrected, dead CJS override removed. Suite 287 passed, 0 failed, 15 live skipped, ~85s (was ~126s); `tsc` main + tests clean (`modules/bitshares-native/subscriptions.ts`, `modules/bitshares_client.ts`, `modules/credit_runtime.ts`, `modules/dexbot_cow_runtime.ts`, `modules/dexbot_fill_runtime.ts`, `modules/dexbot_startup_runtime.ts`, `modules/order/sync_engine.ts`, `modules/utils/errors.ts`).
|
|
30
|
+
|
|
31
|
+
### 2026-09-13
|
|
32
|
+
|
|
33
|
+
- **Feat(batch)**: derive fill/broadcast batch sizing from gap-slot count — two independent fixed caps (`FILL_PROCESSING.MAX_FILL_BATCH_SIZE` and `COW_PERFORMANCE.MAX_OPS_PER_BROADCAST`, both 4) bounded fill batching and per-broadcast order operations with no relationship to actual grid geometry. New `_getGapSlotBatchSize()` resolves via `resolveGapSlots()` (`manager._gapSlots`, else `calculateGapSlots(config)`, ≥1 floor, falls back to 1 on failure); legacy `_getMaxFillBatchSize()` / `_getMaxOpsPerBroadcast()` kept as thin aliases; dead `config._gapSlots` fallback operand removed; both constants removed from `modules/constants.ts` (remaining COW_PERFORMANCE keys unchanged). `executeChunkedWithRetryOnUncertain` prefers the gap-slot size with its own ≥1 guard. Docs across `architecture.md`, `LIFECYCLE.md`, `FUND_MOVEMENT_AND_ACCOUNTING.md`, `developer_guide.md` use gap-slot batch sizing terminology (stale FILL_PROCESSING snippet removed); `COPY_ON_WRITE_MASTER_PLAN.md` marks `MAX_OPS_PER_BROADCAST` removed with a pointer. Completed/obsolete legacy docs removed (not describing anything still in the code): `BROWSER_COMPAT_PLAN.md` (superseded by the package.json browser field + AGENTS.md), `GRID_PRICE_SLOT_DETERMINISM_PLAN.md` (implemented), `PLAN_FILL_STALE_RESTORE_MAIN.md` (implemented), `PLAN_MIN_BTS_VALUE.md` (completed plan), `crash_report_jan_mar_2026.md` (historical incident). Batch size now scales with grid size by construction (larger-gap grids produce larger fill batches and larger single broadcast transactions than the old fixed cap of 4 — intended); removed keys simply drop out of the runtime-settings override merge, no remaining consumers (`modules/dexbot_class.ts`, `modules/constants.ts`, `modules/dexbot_cow_runtime.ts`; `tests/test_cow_ops_per_broadcast.ts` mirrors via `manager._gapSlots` with strict `calculateGapSlots` equality, chunking/death-spiral expectations updated, `tests/sim_batching.ts` uses the GAP_SLOTS constant; full `npm test` exit 0).
|
|
34
|
+
|
|
35
|
+
- **Fix(cache)**: store Kibana candle cache in stable calendar-month shards — run-relative chunk files (`chunk_<index>_<dates>`) shifted names on every run because windows anchor at floored-now, forcing a full rewrite plus an orphan-deletion pass each invocation; the deletion destroyed cached history (fully-disjoint files wiped by narrow runs, boundary-straddling files losing out-of-window buckets). Storage is now fixed UTC calendar-month shards (`<base>.shard_YYYY-MM.json`) decoupled from querying: runs load only overlapping shards, fetch only genuinely missing buckets, and rewrite solely shards that gained buckets (higher-volume-wins) or query coverage (monotonically unioned `queriedRanges`); pure-reuse runs do zero writes/deletes. Legacy `*.chunk_*` files are absorbed (buckets + clipped coverage folded into shards) and retired only when every bucket provably lives in a shard; disjoint legacy is never loaded or deleted. Removed `cleanupOrphanCacheChunks`, `chunkPathFor`, `siblingChunkFiles`, `priorQueriedInWindow`; preserved immutable-gap pruning, 48h tail refresh, partial-window withholding, fetch retry budget. Windows are now fetch-planning splits only (`{index, gte, lte}`); the LP orphan-cleanup wrapper/export is dropped. Preceded by the narrower prune-scope fix (run passes its active coverage; orphans whose `meta.timeRange` does not overlap it are kept) plus the chart fixes it was validated against (issues #29/#30): update-marker div reuse (`um-wrap` class) and log-scale vertical-pan/wheel-zoom bbox offset correction (`market_adapter/inputs/window_cache.ts`, `market_adapter/inputs/fetch_lp_data.ts`, `market_adapter/README.md`, `analysis/tradingview/tradingview_uplot_chart_generator.ts`; `tests/test_window_cache.ts` rewritten with shard mapping, coverage set ops, scoped-load, and end-to-end integrations; LP/book assertions target `.shard_` files; full suite exit 0; live-verified: repeated feed chart runs show pure reuse, a wider run refetched one genuinely-lost span and left 7 stable shards).
|
|
36
|
+
|
|
5
37
|
## [1.6.1] - 2026-09-13 - Never-Run-Stale Hardening, Whitelist Range-Scaling Opt-In, Live-Save Docs, Shelf-Count Hardening
|
|
6
38
|
|
|
7
39
|
### 2026-09-13
|
|
8
40
|
|
|
41
|
+
- **Fix(recovery)**: give the GRID-PRICE-INVARIANT guard and the deferred-hold policy a self-healing exit — both had the same shape (state the guard refuses to emit, but nothing repairs it) and both now escalate to the existing structural resync rather than running forever. A slot rejected as off-grid is skipped and warned, which is correct for a one-off, but the recurring planner carries the slot's price straight from `manager.orders`, so an in-process corruption is re-planned, re-rejected and re-warned every cycle with nothing able to heal it short of a restart — the slot is dead while the bot looks healthy, and the repeated warns train operators to ignore them. The guard now counts **consecutive** rejecting batches per slot (bot-scoped — the monolithic runtime (`dexbot.ts`, the `dexbot` bin) builds EVERY active bot in one process, so a module-level streak would let one bot's rejections push another to the threshold on its FIRST rejection and fire a spurious resync on a healthy bot, and the count also must not outlive a repair; GPI-WIRE-009 pins the cross-bot case) and at `TIMING.GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` (3) fires `requestStructuralGridResync('grid-price-invariant-violation', {slotId, expected, actual, site, streak})`; a clean check clears the streak so escalation means "rejected N consecutive batches", not "N times ever", and `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` (15m) bounds repeats. Healing in place at rejection time is deliberately NOT done — silently rewriting `slot.price` would erase the diagnostic signal distinguishing the four corruption sources. Separately, out-of-rail orphans hold locked funds and are never auto-cancelled per cycle (correct: cancelling on ambiguous evidence is irreversible), but "held indefinitely" had no exit; a hold whose signature is unchanged for `DEFERRED_HOLD_ESCALATE_MS` (24h) now escalates at `error` to `requestStructuralGridResync('deferred-hold-stale')`, which is safe because the full reset's reconcile is update-first (unmatched orders are price-updated onto rail slots, only true surplus cancelled), so funds are released without inventing a new cancellation policy. Hold age is tracked **per stranded order** (bot-scoped map keyed `id@price/size:reason`), because two simpler clocks were wrong: `manager._lastUnmatchedChainOrdersAt` is unconditionally refreshed on every sync observing any unmatched order, so it records "when we last looked" and an age gate on it could never fire (HOLD-007 pins this); and the whole-held-set signature clock looked correct but was **reset by unrelated churn** — the signature includes every entry's reason, so an unrelated hold flapping in and out restarted the clock every cycle and starved a genuinely stranded order of escalation forever (HOLD-010 pins this; a 6-hourly flap was simulated reporting a clock reset on every tick). A third defect sat in the same path: the signature-change branch `return`ed before the escalation call, so any churn skipped escalation entirely — escalation now runs on both branches, since a signature change is a reason to re-log, not to stop evaluating age. Escalation triggers only on genuinely **stranded** reasons via a narrow allow-list (`isStrandedHoldOrder`: `out-of-rail-deferred`, `out-of-grid-deferred`) rather than the broad `-deferred` non-blocking filter, because a resync cannot end a broadcast region or re-evaluate an uncommitted boundary; `broadcast-active-deferred`, `boundary-hold-trailing-market`, `boundary-unknown-deferred` and `held-plan-unchanged-deferred` are excluded (HOLD-011 pins this), and the allow-list fails closed so a future transient reason is excluded by default. Both escalations reuse the existing debounced, batch-in-flight-aware resync path; a second repair mechanism would duplicate tested machinery. New GPI-WIRE-006..009 (`tests/test_grid_price_invariant_wiring.ts`) and HOLD-006..011 (`tests/test_hold_and_center_guards.ts`); mutation-verified that removing the escalation call, removing the streak reset, disabling the cooldown, removing the hold escalation, re-gating escalation behind the signature early return, reading the hold age from the whole-set clock, and replacing the stranded allow-list with the broad `-deferred` filter are each caught (`modules/constants.ts`, `modules/dexbot_cow_runtime.ts`, `modules/dexbot_maintenance_runtime.ts`, `docs/GRID_PRICE_INVARIANT.md`, `docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md`).
|
|
42
|
+
|
|
9
43
|
- **Fix(shelf)**: exclude shelf orders from every grid count (issue #27 follow-up) — the four shelf follow-ups gated `reserveEdgeIdSet`, `matchedExcess`, and the geometric size recalc to `parseSlotIndex(id) !== null`, but three counters feeding the same decisions never got the gate, so a live fork-kept shelf (non-slot-N id, `ACTIVE` + `orderId`) still poisoned them via the fail-open `isSlotInRail` geometry. `_countActiveOnGrid` inflated `matchedOnGrid` (suppressing `neededSlots`/creates), `chainCount` in `_reconcileStartupSide` counted shelf-bound chain orders (fabricating a `chain - target` surplus that cancelled real window orders), `countLiveGridOrders` masked window shortfalls in targeted sync, and `_getOnChainOrders` plus the spread `buyCount`/`sellCount` masked `oneSideEmpty`. All now apply the same slot-N gate (no-op on grids that only mint slot-N ids); shelf stays in the geometric-recalc denominator by design (conservative sizing direction). `_pickVirtualSlotsToActivate` and the `getInitialOrdersToActivate` window picks are gated too so a `VIRTUAL` shelf can never consume window activation budget (`modules/order/grid_reconcile_internal.ts`, `modules/dexbot_maintenance_runtime.ts`, `modules/order/grid.ts`, `modules/order/manager.ts`; `tests/test_reserve_orders.ts` shelf expectations corrected to the fixed behavior — chain 12 grid vs target 8 plans 4 rail cancels, grid count converges to 8 with 3 shelf surviving alongside — plus a new block pinning `_countActiveOnGrid`, startup create non-suppression, targeted-sync `buy 5/8` firing through shelf, and slot-N-only activation picks; `tests/test_startup_decision.ts`, `tests/test_resync_balance_fix.ts`, `tests/test_resync_duplicate_race.ts` fixtures migrated from ad-hoc ids to `slot-N` ids per the production invariant; full suite exit 0, zero failures).
|
|
10
44
|
|
|
11
45
|
- **Fix(whitelist)**: disable range scaling in whitelist defaults — AMA whitelist generation is conservative by default so new entries enable AMA live writes without enabling dynamic weights or asymmetric range scaling; new explicit asymmetric-bounds opt-in flags for new and targeted entries, legacy array-form whitelist entries interpreted as AMA-only, CLI help and generation tests updated for the new defaults; README, onboarding, market-adapter, and grid-recalculation guidance aligned with the range-scaling opt-in behavior (`modules/market_adapter_whitelist.ts`, `scripts/generate_market_adapter_whitelist.ts`, `dexbot.ts`, `README.md`, `docs/BITSHARES_ONBOARDING.md`, `docs/GRID_RECALCULATION.md`, `market_adapter/README.md`, `scripts/README.md`, `tests/test_market_adapter_fixes.ts`).
|
package/analysis/README.md
CHANGED
|
@@ -225,7 +225,7 @@ node dist/analysis/trade_profitability.js 1.2.123456 \
|
|
|
225
225
|
|
|
226
226
|
Validates grid discipline from the same Kibana fill pipeline as `trade_profitability.ts`: two consecutive same-direction fills on a pair must be monotonic — sell prices rising, buy prices falling (equal is OK). An inversion means the bot placed an order below its own previous sell (or above its own previous buy), e.g. an orphaned order filling outside grid accounting. Used as the external regression gate for the orphan-fix plans in `docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md`.
|
|
227
227
|
|
|
228
|
-
**Pipeline:** Kibana `fill_order` query (paginated `search_after`) → on-chain asset precision resolution → buy/sell classification → chronological sort → per-order aggregation (
|
|
228
|
+
**Pipeline:** Kibana `fill_order` query (paginated `search_after`) → on-chain asset precision resolution → buy/sell classification → chronological sort → per-order/price-epoch aggregation (partial fills at one price collapsed to weighted-average; repriced order lifetimes kept separate) → consecutive same-direction pair comparison → violation report with daily histogram.
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
231
|
# Per-order aggregated check (default), last 7 days
|
|
@@ -263,7 +263,7 @@ Exit code `0` = pass, `2` = violations found, `1` = fatal error. Bot keys resolv
|
|
|
263
263
|
|
|
264
264
|
</details>
|
|
265
265
|
|
|
266
|
-
**Notes:** strict sat-level comparison is the ground truth (`--tolerance` only forgives small inversions);
|
|
266
|
+
**Notes:** strict sat-level comparison is the ground truth (`--tolerance` only forgives small inversions); partial fills at one price are collapsed to a weighted-average price in the default mode, but fills after a native order repricing are kept in separate price epochs so updated orders are not mixed together.
|
|
267
267
|
|
|
268
268
|
## Charts & Visualization
|
|
269
269
|
|
|
@@ -42,6 +42,7 @@ import { loadBotMeta, loadBotSettings, computeBotKey, persistBotAccountId } from
|
|
|
42
42
|
const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
|
|
43
43
|
|
|
44
44
|
const OP_FILL_ORDER = 4;
|
|
45
|
+
const OP_LIMIT_ORDER_UPDATE = 77;
|
|
45
46
|
const BTS_ID = '1.3.0';
|
|
46
47
|
|
|
47
48
|
interface AssetInfo { symbol: string; precision: number; }
|
|
@@ -113,6 +114,10 @@ interface FillRecord {
|
|
|
113
114
|
isMaker: boolean;
|
|
114
115
|
sort: any[];
|
|
115
116
|
}
|
|
117
|
+
interface OrderUpdate {
|
|
118
|
+
orderId: string;
|
|
119
|
+
sequence: number;
|
|
120
|
+
}
|
|
116
121
|
interface TradeFill {
|
|
117
122
|
time: string;
|
|
118
123
|
orderId: string;
|
|
@@ -485,6 +490,50 @@ function buildFillQuery(accountId: string, gte: string, lte: string, size: numbe
|
|
|
485
490
|
};
|
|
486
491
|
}
|
|
487
492
|
|
|
493
|
+
async function fetchAllOrderUpdates(config: any, accountId: string, gte: string, lte: string): Promise<OrderUpdate[]> {
|
|
494
|
+
const pageSize = 10000;
|
|
495
|
+
const updates: OrderUpdate[] = [];
|
|
496
|
+
let searchAfter: any[] | null = null;
|
|
497
|
+
const cfg = { ...BASE_CONFIG, timeout: 60000, ...config };
|
|
498
|
+
while (true) {
|
|
499
|
+
const query: any = {
|
|
500
|
+
size: pageSize,
|
|
501
|
+
track_total_hits: false,
|
|
502
|
+
_source: [
|
|
503
|
+
'block_data.block_num', 'operation_id_num',
|
|
504
|
+
'operation_history.op_object.order',
|
|
505
|
+
'operation_history.op_object.seller',
|
|
506
|
+
],
|
|
507
|
+
query: { bool: { filter: [
|
|
508
|
+
{ term: { operation_type: OP_LIMIT_ORDER_UPDATE } },
|
|
509
|
+
{ term: { 'operation_history.op_object.seller.keyword': accountId } },
|
|
510
|
+
{ range: { 'block_data.block_time': { gte, lte } } },
|
|
511
|
+
] } },
|
|
512
|
+
sort: [
|
|
513
|
+
{ 'block_data.block_num': { order: 'asc' } },
|
|
514
|
+
{ operation_id_num: { order: 'asc' } },
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
if (searchAfter) query.search_after = searchAfter;
|
|
518
|
+
const result: any = await kibanaSearch(cfg, query);
|
|
519
|
+
const hits = result?.hits?.hits ?? [];
|
|
520
|
+
if (!hits.length) break;
|
|
521
|
+
for (const hit of hits) {
|
|
522
|
+
const source = hit?._source || {};
|
|
523
|
+
const op = source.operation_history?.op_object || {};
|
|
524
|
+
const orderId = op.order;
|
|
525
|
+
if (!orderId) continue;
|
|
526
|
+
const blockNum = Number(source.block_data?.block_num ?? 0);
|
|
527
|
+
const opNum = Number(source.operation_id_num ?? 0);
|
|
528
|
+
updates.push({ orderId, sequence: blockNum * 1e6 + opNum });
|
|
529
|
+
}
|
|
530
|
+
const last = hits[hits.length - 1];
|
|
531
|
+
searchAfter = last?.sort;
|
|
532
|
+
if (!searchAfter || hits.length < pageSize) break;
|
|
533
|
+
}
|
|
534
|
+
return updates;
|
|
535
|
+
}
|
|
536
|
+
|
|
488
537
|
async function fetchAllFills(config: any, accountId: string, gte: string, lte: string): Promise<FillRecord[]> {
|
|
489
538
|
const pageSize = 10000;
|
|
490
539
|
const fills: FillRecord[] = [];
|
|
@@ -578,11 +627,31 @@ function classifyFills(fills: FillRecord[]): { trades: TradeFill[]; skipped: num
|
|
|
578
627
|
return { trades, skipped };
|
|
579
628
|
}
|
|
580
629
|
|
|
581
|
-
// ─── Per-order aggregation
|
|
582
|
-
|
|
630
|
+
// ─── Per-order/price-epoch aggregation ───────────────────────────────────────
|
|
631
|
+
// Collapses partial fills at one order lifetime, but never mixes fills from
|
|
632
|
+
// different native limit_order_update repricings of the same order ID.
|
|
633
|
+
function aggregateByOrder(trades: TradeFill[], updates: OrderUpdate[] = []): AggregatedOrder[] {
|
|
634
|
+
const updatesByOrder = new Map<string, number[]>();
|
|
635
|
+
for (const update of updates) {
|
|
636
|
+
const list = updatesByOrder.get(update.orderId) || [];
|
|
637
|
+
list.push(update.sequence);
|
|
638
|
+
updatesByOrder.set(update.orderId, list);
|
|
639
|
+
}
|
|
640
|
+
for (const list of updatesByOrder.values()) list.sort((a, b) => a - b);
|
|
641
|
+
|
|
583
642
|
const map = new Map<string, { trades: TradeFill[] }>();
|
|
584
643
|
for (const t of trades) {
|
|
585
|
-
|
|
644
|
+
// An update is ordered before the fills it can affect. The epoch is
|
|
645
|
+
// therefore the latest update sequence at or before this fill.
|
|
646
|
+
const orderUpdates = updatesByOrder.get(t.orderId) || [];
|
|
647
|
+
let epoch = 0;
|
|
648
|
+
for (const sequence of orderUpdates) {
|
|
649
|
+
if (sequence <= t.sequence) epoch = sequence;
|
|
650
|
+
else break;
|
|
651
|
+
}
|
|
652
|
+
const k = t.orderId
|
|
653
|
+
? `${t.orderId}:${t.direction}:${t.baseAsset}:${t.quoteAsset}:${epoch}`
|
|
654
|
+
: `__fill_${t.sequence}`;
|
|
586
655
|
if (!map.has(k)) map.set(k, { trades: [] });
|
|
587
656
|
map.get(k)!.trades.push(t);
|
|
588
657
|
}
|
|
@@ -870,9 +939,13 @@ async function main() {
|
|
|
870
939
|
const incrementPercent = resolveIncrementPercent(botMeta, opts.incrementPercent);
|
|
871
940
|
console.log(`Increment: ${incrementPercent}% (halfInc ${incrementPercent/2}%)${opts.incrementPercent == null && botMeta?.incrementPercent != null ? ' — from bot config' : opts.incrementPercent != null ? ' — from --increment' : ' — default'}`);
|
|
872
941
|
|
|
873
|
-
console.log(`\nFetching fills from Kibana...`);
|
|
874
|
-
const fills = await
|
|
942
|
+
console.log(`\nFetching fills and order updates from Kibana...`);
|
|
943
|
+
const [fills, orderUpdates] = await Promise.all([
|
|
944
|
+
fetchAllFills({}, accountId, gte, lte),
|
|
945
|
+
fetchAllOrderUpdates({}, accountId, gte, lte),
|
|
946
|
+
]);
|
|
875
947
|
console.log(` Fetched ${fills.length} fill_order operation(s)`);
|
|
948
|
+
console.log(` Fetched ${orderUpdates.length} limit_order_update operation(s)`);
|
|
876
949
|
|
|
877
950
|
if (fills.length === 0) {
|
|
878
951
|
console.log('\nNo fills in range — nothing to check.');
|
|
@@ -905,8 +978,8 @@ async function main() {
|
|
|
905
978
|
if (opts.perFill) {
|
|
906
979
|
items = trades;
|
|
907
980
|
} else {
|
|
908
|
-
const orders = aggregateByOrder(trades);
|
|
909
|
-
console.log(` Aggregated into ${orders.length} order(s) (
|
|
981
|
+
const orders = aggregateByOrder(trades, orderUpdates);
|
|
982
|
+
console.log(` Aggregated into ${orders.length} order epoch(s) (partial fills collapsed; repriced orders kept separate)`);
|
|
910
983
|
items = orders;
|
|
911
984
|
}
|
|
912
985
|
|