dexbot 1.6.2 → 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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/analysis/README.md +2 -2
  3. package/analysis/ama_fitting/package.json +1 -1
  4. package/analysis/grid_correction_check.ts +80 -7
  5. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
  6. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
  7. package/analysis/tradingview/h-bts_tradingview.html +1570 -0
  8. package/analysis/tradingview/t-bts_tradingview.html +1570 -0
  9. package/analysis/trend_detection/package.json +1 -1
  10. package/claw/package.json +1 -1
  11. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  12. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  13. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  14. package/dist/analysis/grid_correction_check.d.ts +5 -1
  15. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  16. package/dist/analysis/grid_correction_check.js +81 -7
  17. package/dist/analysis/grid_correction_check.js.map +1 -1
  18. package/dist/modules/dexbot_class.d.ts +1 -1
  19. package/dist/modules/dexbot_cow_runtime.d.ts +81 -2
  20. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  21. package/dist/modules/dexbot_cow_runtime.js +482 -8
  22. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  23. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  24. package/dist/modules/dexbot_maintenance_runtime.js +6 -1
  25. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  26. package/dist/modules/order/manager.d.ts.map +1 -1
  27. package/dist/modules/order/manager.js +3 -1
  28. package/dist/modules/order/manager.js.map +1 -1
  29. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  30. package/dist/modules/order/sync_engine.js +17 -5
  31. package/dist/modules/order/sync_engine.js.map +1 -1
  32. package/dist/modules/order/utils/order.d.ts +45 -7
  33. package/dist/modules/order/utils/order.d.ts.map +1 -1
  34. package/dist/modules/order/utils/order.js +160 -15
  35. package/dist/modules/order/utils/order.js.map +1 -1
  36. package/docs/DEXBOT_COMPARISON.md +3 -3
  37. package/docs/EVOLUTION.md +8 -7
  38. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  39. package/docs/README.md +1 -1
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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
+
5
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
6
18
 
7
19
  ### 2026-09-14
@@ -14,11 +26,13 @@ All notable changes to this project will be documented in this file.
14
26
 
15
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`).
16
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
+
17
31
  ### 2026-09-13
18
32
 
19
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).
20
34
 
21
- - **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: 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).
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).
22
36
 
23
37
  ## [1.6.1] - 2026-09-13 - Never-Run-Stale Hardening, Whitelist Range-Scaling Opt-In, Live-Save Docs, Shelf-Count Hardening
24
38
 
@@ -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 (multi-fill orders collapsed to weighted-avg price by default) → consecutive same-direction pair comparison → violation report with daily histogram.
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); multi-fill orders are collapsed to a weighted-average price for the default per-order mode, so a single order's partial fills at identical prices never count as 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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ama_fitting",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Tools for fitting AMA parameters to market data",
5
5
  "main": "../../dist/analysis/ama_fitting/optimizer_high_resolution.js",
6
6
  "scripts": {
@@ -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 (collapses multi-fill orders to weighted avg price) ──
582
- function aggregateByOrder(trades: TradeFill[]): AggregatedOrder[] {
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
- const k = t.orderId || `__fill_${t.sequence}`;
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 fetchAllFills({}, accountId, gte, lte);
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) (multi-fill orders collapsed by weighted avg price)`);
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