dexbot 1.6.0 → 1.6.2

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 (114) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +5 -4
  3. package/analysis/ama_fitting/package.json +1 -1
  4. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +19 -5
  5. package/analysis/trend_detection/package.json +1 -1
  6. package/claw/package.json +1 -1
  7. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  8. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  9. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  10. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  11. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +19 -5
  12. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  13. package/dist/dexbot.js +1 -1
  14. package/dist/dexbot.js.map +1 -1
  15. package/dist/market_adapter/inputs/fetch_book_data.js +2 -2
  16. package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -1
  17. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +22 -3
  18. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  19. package/dist/market_adapter/inputs/fetch_lp_data.js +15 -15
  20. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  21. package/dist/market_adapter/inputs/kibana_feed_source.js +2 -2
  22. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -1
  23. package/dist/market_adapter/inputs/window_cache.d.ts +54 -8
  24. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -1
  25. package/dist/market_adapter/inputs/window_cache.js +328 -87
  26. package/dist/market_adapter/inputs/window_cache.js.map +1 -1
  27. package/dist/modules/account_bots.js +2 -2
  28. package/dist/modules/account_bots.js.map +1 -1
  29. package/dist/modules/bitshares-native/subscriptions.d.ts +1 -1
  30. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  31. package/dist/modules/bitshares-native/subscriptions.js +18 -4
  32. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  33. package/dist/modules/bitshares_client.d.ts.map +1 -1
  34. package/dist/modules/bitshares_client.js +10 -0
  35. package/dist/modules/bitshares_client.js.map +1 -1
  36. package/dist/modules/constants.d.ts +12 -3
  37. package/dist/modules/constants.d.ts.map +1 -1
  38. package/dist/modules/constants.js +66 -20
  39. package/dist/modules/constants.js.map +1 -1
  40. package/dist/modules/credit_runtime.d.ts.map +1 -1
  41. package/dist/modules/credit_runtime.js +24 -6
  42. package/dist/modules/credit_runtime.js.map +1 -1
  43. package/dist/modules/dexbot_class.d.ts +34 -6
  44. package/dist/modules/dexbot_class.d.ts.map +1 -1
  45. package/dist/modules/dexbot_class.js +94 -17
  46. package/dist/modules/dexbot_class.js.map +1 -1
  47. package/dist/modules/dexbot_cow_runtime.d.ts +57 -6
  48. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  49. package/dist/modules/dexbot_cow_runtime.js +474 -38
  50. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  51. package/dist/modules/dexbot_fill_runtime.d.ts +53 -1
  52. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  53. package/dist/modules/dexbot_fill_runtime.js +277 -9
  54. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  55. package/dist/modules/dexbot_maintenance_runtime.d.ts +69 -0
  56. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  57. package/dist/modules/dexbot_maintenance_runtime.js +277 -7
  58. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  59. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  60. package/dist/modules/dexbot_startup_runtime.js +6 -1
  61. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  62. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  63. package/dist/modules/dexbot_state_recovery.js +32 -4
  64. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  65. package/dist/modules/market_adapter_whitelist.js +1 -1
  66. package/dist/modules/market_adapter_whitelist.js.map +1 -1
  67. package/dist/modules/order/grid.d.ts +3 -1
  68. package/dist/modules/order/grid.d.ts.map +1 -1
  69. package/dist/modules/order/grid.js +93 -16
  70. package/dist/modules/order/grid.js.map +1 -1
  71. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  72. package/dist/modules/order/grid_reconcile_internal.js +73 -7
  73. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  74. package/dist/modules/order/logger.js +1 -1
  75. package/dist/modules/order/logger.js.map +1 -1
  76. package/dist/modules/order/manager.d.ts +14 -1
  77. package/dist/modules/order/manager.d.ts.map +1 -1
  78. package/dist/modules/order/manager.js +54 -2
  79. package/dist/modules/order/manager.js.map +1 -1
  80. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  81. package/dist/modules/order/sync_engine.js +183 -10
  82. package/dist/modules/order/sync_engine.js.map +1 -1
  83. package/dist/modules/order/utils/math.d.ts +65 -15
  84. package/dist/modules/order/utils/math.d.ts.map +1 -1
  85. package/dist/modules/order/utils/math.js +93 -22
  86. package/dist/modules/order/utils/math.js.map +1 -1
  87. package/dist/modules/order/utils/order.d.ts +51 -1
  88. package/dist/modules/order/utils/order.d.ts.map +1 -1
  89. package/dist/modules/order/utils/order.js +122 -2
  90. package/dist/modules/order/utils/order.js.map +1 -1
  91. package/dist/modules/utils/errors.d.ts +22 -0
  92. package/dist/modules/utils/errors.d.ts.map +1 -1
  93. package/dist/modules/utils/errors.js +30 -0
  94. package/dist/modules/utils/errors.js.map +1 -1
  95. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  96. package/dist/scripts/generate_market_adapter_whitelist.js +3 -2
  97. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  98. package/docs/BITSHARES_ONBOARDING.md +14 -6
  99. package/docs/COPY_ON_WRITE_MASTER_PLAN.md +1 -1
  100. package/docs/DEXBOT_COMPARISON.md +3 -7
  101. package/docs/EVOLUTION.md +9 -7
  102. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -9
  103. package/docs/GRID_RECALCULATION.md +8 -7
  104. package/docs/LIFECYCLE.md +2 -2
  105. package/docs/README.md +9 -1
  106. package/docs/architecture.md +7 -7
  107. package/docs/developer_guide.md +4 -4
  108. package/market_adapter/README.md +14 -11
  109. package/package.json +1 -1
  110. package/scripts/README.md +4 -4
  111. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +0 -2455
  112. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +0 -218
  113. package/analysis/tradingview/h-bts_tradingview.html +0 -1570
  114. package/analysis/tradingview/t-bts_tradingview.html +0 -1570
@@ -98,10 +98,32 @@ import * as Format from './format.js';
98
98
  import { lookupAsset, sleep, resolveAccountRef } from './utils/system.js';
99
99
  import * as chainOrders from '../chain_orders.js';
100
100
  import * as client from '../bitshares_client.js';
101
+ /**
102
+ * Test seam for targeted single-order refetches (drift refetch + sub-dust
103
+ * residual verification). When manager._readSingleOrderFn is a function it
104
+ * is used instead of the chain read. The chain path starts with
105
+ * waitForConnected (30s timeout against live nodes); tests that drive fill
106
+ * paths with a drift signal or a sub-dust residual would otherwise hang on
107
+ * that connect for a read whose result they do not even assert on.
108
+ * Production never sets this; default is the chain.
109
+ */
110
+ function readSingleOrderSeam(mgr, orderId, timeoutMs) {
111
+ const seam = mgr?._readSingleOrderFn;
112
+ if (typeof seam === 'function')
113
+ return seam(orderId, timeoutMs);
114
+ return chainOrders.readSingleOrder(orderId, timeoutMs);
115
+ }
116
+ /** Same seam for the batched drift refetch (batch path). */
117
+ function batchReadOrdersSeam(mgr, orderIds, timeoutMs) {
118
+ const seam = mgr?._batchReadOrdersFn;
119
+ if (typeof seam === 'function')
120
+ return seam(orderIds, timeoutMs);
121
+ return chainOrders.batchReadOrders(orderIds, timeoutMs);
122
+ }
101
123
  const { BitShares } = client;
102
124
  import { NATIVE_CLIENT } from '../constants.js';
103
125
  const { toFiniteNumber } = Format;
104
- import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, getSellStartIdx, slotIndexForPrice, isChainPriceOutOfGrid, isSlotInRail, isSlotIndexInGapBand, priceSlotEqual } from './utils/math.js';
126
+ import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, getSellStartIdx, slotIndexForPrice, isChainPriceOutOfGrid, isSlotInRail, isSlotIndexInGapBand, priceForSlot, priceSlotEqual } from './utils/math.js';
105
127
  import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, convertToSpreadPlaceholder, virtualizeOrder, buildFillKey, isOrderPlaced, hasOnChainId, isOrderVirtual, resolveSpreadOrderSide, duplicateOrphanLogInfo } from './utils/order.js';
106
128
  import { parseSlotIndex } from './utils/slot.js';
107
129
  import { resolveProcessedFillPersistenceMode } from './processed_fill_store.js';
@@ -117,6 +139,9 @@ import { getErrorMessage } from '../utils/errors.js';
117
139
  * Test seam: when manager._confirmEmptyReadFn is a function it is used
118
140
  * instead of the chain read (must resolve to a raw order array, or null
119
141
  * when the re-read itself is ambiguous). A throwing seam is 'ambiguous'.
142
+ * When manager._skipEmptyReadConfirmDelay is truthy the production
143
+ * SYNC_EMPTY_READ_CONFIRM_DELAY_MS pacing is skipped (tests); the confirm
144
+ * semantics (confirmed / contradicted / ambiguous / unavailable) are unchanged.
120
145
  *
121
146
  * @param {Object} mgr - OrderManager instance (accountId, config, logger).
122
147
  * @returns {Promise<string>} 'confirmed' (still empty — accept),
@@ -130,7 +155,9 @@ async function confirmSuspectEmptyRead(mgr) {
130
155
  if (mgr?.config?.dryRun)
131
156
  return 'unavailable';
132
157
  const seam = mgr?._confirmEmptyReadFn;
133
- const delay = Math.max(0, Number(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS) || 0);
158
+ const delay = mgr?._skipEmptyReadConfirmDelay
159
+ ? 0
160
+ : Math.max(0, Number(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS) || 0);
134
161
  if (typeof seam === 'function') {
135
162
  if (delay > 0)
136
163
  await sleep(delay);
@@ -264,7 +291,75 @@ function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcToler
264
291
  * @param {Function} calcToleranceFn - calculatePriceTolerance function
265
292
  * @returns {{candidateSlotId: string, candidateSlotPrice: number, priceDiff: number, tolerance: number}|null}
266
293
  */
294
+ /**
295
+ * Assert (non-fatally) that an adopted slot still carries its own genesis
296
+ * level, not a price inherited from the chain order.
297
+ *
298
+ * Both adoption paths keep the slot's price by NOT assigning it — an absence of
299
+ * a write. That is correct but invisible: nothing would notice if a later edit
300
+ * added `price: chainOrder.price` back, and nothing would notice a slot whose
301
+ * price had already been corrupted before it reached adoption. This makes the
302
+ * rule explicit and warns when it is broken.
303
+ *
304
+ * Deliberately non-throwing: adoption already has handled rejection paths, and
305
+ * turning a warning into a crash here would strand a live chain order. The
306
+ * caller's existing rejection handling stays the enforcement; this is the
307
+ * signal that surfaces the corruption in the log.
308
+ *
309
+ * @returns {boolean} true when the slot's price is its genesis level (or cannot
310
+ * be judged), false when it demonstrably is not.
311
+ */
312
+ function adoptedSlotKeepsItsOwnPrice(mgr, adopted, chainOrderId, path) {
313
+ try {
314
+ const genesis = mgr?._genesis;
315
+ const levels = genesis?.priceLevels;
316
+ if (!Array.isArray(levels) || levels.length === 0)
317
+ return true; // no ladder: unjudgeable, fail open
318
+ const idx = parseSlotIndex(adopted?.id);
319
+ if (idx === null || !Number.isFinite(idx) || idx < 0 || idx >= levels.length)
320
+ return true;
321
+ const expected = Number(priceForSlot(idx, genesis));
322
+ const got = Number(adopted?.price);
323
+ if (!Number.isFinite(expected) || expected <= 0)
324
+ return true;
325
+ if (!Number.isFinite(got))
326
+ return true;
327
+ const diff = Math.abs(got - expected);
328
+ const rel = diff / Math.max(1e-12, Math.abs(expected));
329
+ if (rel > 1e-9 && diff > 1e-12) {
330
+ mgr.logger?.log?.(`[SYNC] Adoption (${path}) for ${chainOrderId} into slot ${adopted.id}: slot price ${got} is NOT the slot's ` +
331
+ `genesis level ${expected} (drift ${(rel * 100).toFixed(3)}%). Adoption must not write the chain order's price ` +
332
+ `into slot.price — the slot keeps its own level. Adopting anyway (the chain order is real); investigate what ` +
333
+ `corrupted this slot's price.`, 'warn');
334
+ return false;
335
+ }
336
+ return true;
337
+ }
338
+ catch {
339
+ return true; // never let an observability check break adoption
340
+ }
341
+ }
267
342
  async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting) {
343
+ // ADOPTION NEVER TAKES THE CHAIN ORDER'S PRICE.
344
+ //
345
+ // Two different prices are conflated on this path, and only one of them is
346
+ // a grid position:
347
+ //
348
+ // slot.price what this slot's LEVEL is -> must equal priceForSlot(idx, genesis)
349
+ // chainOrder.price where the order RESTS on chain -> may differ legitimately
350
+ //
351
+ // They legitimately differ after a grid regeneration: the grid is
352
+ // recentered, old chain orders keep resting at the OLD grid's levels, and
353
+ // adoption adopts the order into the nearest slot (slotIndexForPrice). That
354
+ // is normal and is why this function accepts a differing chainOrder.price.
355
+ //
356
+ // It must not WRITE it. The resting price is chain metadata about where the
357
+ // order was placed; it disappears on the next rotation/cancel. Writing it
358
+ // into slot.price is the S1 corruption this whole invariant exists to
359
+ // prevent, so `price` is deliberately absent from the assignments below and
360
+ // the slot keeps its own level in `bestMatch` (spread from `slot`).
361
+ //
362
+ // Size is the opposite: size IS real state, so the chain value is adopted.
268
363
  let bestMatch = { ...slot };
269
364
  const wasVirtual = slot.state === ORDER_STATES.VIRTUAL;
270
365
  const wasPartial = slot.state === ORDER_STATES.PARTIAL;
@@ -329,6 +424,7 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
329
424
  else if (wasPartial) {
330
425
  bestMatch.state = ORDER_STATES.PARTIAL;
331
426
  }
427
+ adoptedSlotKeepsItsOwnPrice(mgr, bestMatch, chainOrderId, 'genesis');
332
428
  const applied = await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
333
429
  if (applied === false) {
334
430
  // Same un-poisoning as the filled path above: the slot counts as
@@ -1219,6 +1315,20 @@ class SyncEngine {
1219
1315
  });
1220
1316
  if (adoptedSlot && !matchedGridOrderIds.has(adoptedSlot.id) && !adoptedSlot.orderId) {
1221
1317
  const precision = (chainOrder.type === ORDER_TYPES.SELL) ? assetAPrecision : assetBPrecision;
1318
+ // Legacy path parity with the genesis adoption path: the
1319
+ // slot's price is its GENESIS level and is never overwritten
1320
+ // with the chain order's price. Adopting chainOrder.price
1321
+ // here made the slot's price whichever price happened to be
1322
+ // on the book, which is how an off-grid order could become a
1323
+ // grid level and then be re-emitted as a plan price. The
1324
+ // size below is still the chain value: size is real state,
1325
+ // price is a ladder position.
1326
+ const legacyRail = isSlotInRail(mgr.boundaryIdx, mgr._gapSlots ?? 0, chainOrder.type, adoptedSlot);
1327
+ if (!legacyRail) {
1328
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'out-of-rail-deferred', candidateSlotId: adoptedSlot.id });
1329
+ mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) — NOT adopted into slot ${adoptedSlot.id}: slot is outside the active rail for a ${chainOrder.type}; deferred`, 'warn');
1330
+ continue;
1331
+ }
1222
1332
  const chainInt = floatToBlockchainInt(chainOrder.size, precision);
1223
1333
  const adoptedRaw = rawChainOrders.get(chainOrderId);
1224
1334
  const adoptedState = chainInt > 0 ? ORDER_STATES.PARTIAL : ORDER_STATES.VIRTUAL;
@@ -1226,16 +1336,32 @@ class SyncEngine {
1226
1336
  const rawFee = toFiniteNumber(adoptedRaw.deferred_fee, null);
1227
1337
  return rawFee !== null && rawFee > 0 ? { deferredFee: blockchainToFloat(rawFee, BTS_PRECISION) } : undefined;
1228
1338
  })() : undefined;
1339
+ // ADOPTION NEVER TAKES THE CHAIN ORDER'S PRICE (legacy parity).
1340
+ //
1341
+ // Same rule as adoptChainOrderIntoSlot, stated here because
1342
+ // this path is the one that used to get it wrong: it set
1343
+ // `price: chainOrder.price`, so a slot's id and its price
1344
+ // could disagree — an off-grid order became a grid level and
1345
+ // was then re-emitted as a plan price (S1).
1346
+ //
1347
+ // `...adoptedSlot` below carries the slot's own level and
1348
+ // `price` is deliberately NOT reassigned. A differing
1349
+ // chainOrder.price is expected (a regeneration leaves old
1350
+ // orders resting at old levels) and is not corruption; it
1351
+ // simply is not this slot's price.
1352
+ //
1353
+ // Size differs: size is real state, so the chain value is
1354
+ // adopted. Price is a ladder position; it is not.
1229
1355
  const adoptedOrder = {
1230
1356
  ...adoptedSlot,
1231
1357
  orderId: chainOrderId,
1232
1358
  type: chainOrder.type,
1233
1359
  state: adoptedState,
1234
1360
  size: chainOrder.size,
1235
- price: chainOrder.price,
1236
1361
  rawOnChain: adoptedRaw ? { ...adoptedRaw, fetchedAt: Date.now() } : adoptedRaw,
1237
1362
  ...(adoptedBtsFeeState ? { btsFeeState: adoptedBtsFeeState } : {}),
1238
1363
  };
1364
+ adoptedSlotKeepsItsOwnPrice(mgr, adoptedOrder, chainOrderId, 'legacy-fallback');
1239
1365
  const applied = await mgr._applyOrderUpdate(adoptedOrder, 'sync-pass2-adopt-orphan', { skipAccounting: skipAccounting, fee: 0 });
1240
1366
  if (applied === false) {
1241
1367
  // Fatal rejection: parity with the genesis adoption path —
@@ -1392,7 +1518,7 @@ class SyncEngine {
1392
1518
  }
1393
1519
  else {
1394
1520
  try {
1395
- const residualOrder = await chainOrders.readSingleOrder(matchedGridOrder.orderId, 3000);
1521
+ const residualOrder = await readSingleOrderSeam(mgr, matchedGridOrder.orderId, 3000);
1396
1522
  residualForSale = residualOrder ? toFiniteNumber(residualOrder.for_sale, null) : null;
1397
1523
  }
1398
1524
  catch (residualErr) {
@@ -1538,7 +1664,7 @@ class SyncEngine {
1538
1664
  let chainRefetched = false;
1539
1665
  if (ctx.driftSignal) {
1540
1666
  try {
1541
- const fresh = await chainOrders.readSingleOrder(orderId, 3000);
1667
+ const fresh = await readSingleOrderSeam(mgr, orderId, 3000);
1542
1668
  if (fresh) {
1543
1669
  const freshForSale = toFiniteNumber(fresh.for_sale, null);
1544
1670
  if (freshForSale !== null && Number.isFinite(freshForSale)) {
@@ -1749,7 +1875,7 @@ class SyncEngine {
1749
1875
  const refetchMap = new Map();
1750
1876
  if (driftOrderIds.size > 0) {
1751
1877
  try {
1752
- const batchResults = await chainOrders.batchReadOrders([...driftOrderIds], 3000);
1878
+ const batchResults = await batchReadOrdersSeam(mgr, [...driftOrderIds], 3000);
1753
1879
  for (const [orderId, freshOrder] of batchResults) {
1754
1880
  if (freshOrder) {
1755
1881
  const freshForSale = toFiniteNumber(freshOrder.for_sale, null);
@@ -2049,8 +2175,30 @@ class SyncEngine {
2049
2175
  ? rawType
2050
2176
  : null;
2051
2177
  const descriptorPrice = toFiniteNumber(placedDescriptor?.price, NaN);
2178
+ // Resolve the slot's authority: the GENESIS LEVEL,
2179
+ // not the descriptor. Used for BOTH price and type.
2180
+ // Deriving the type from descriptorPrice (as this
2181
+ // once did) let a corrupt descriptor pick the side
2182
+ // while the price was corrected to the ladder, so a
2183
+ // slot whose level is on the BUY side could
2184
+ // materialize as SELL @ <buy-level> — price and type
2185
+ // disagreeing in one order object.
2186
+ const materializeIdx = parseSlotIndex(gridOrderId);
2187
+ let ladderLevel = null;
2188
+ try {
2189
+ const genesis = mgr._genesis;
2190
+ if (materializeIdx !== null && Array.isArray(genesis?.priceLevels) && genesis.priceLevels.length > 0) {
2191
+ const onLadder = Number(priceForSlot(materializeIdx, genesis));
2192
+ if (Number.isFinite(onLadder) && onLadder > 0)
2193
+ ladderLevel = onLadder;
2194
+ }
2195
+ }
2196
+ catch { /* fall back to the descriptor below */ }
2197
+ // Only fall back to the descriptor when the ladder
2198
+ // cannot speak (migration / unparseable id).
2052
2199
  if (!materializeType && Number.isFinite(descriptorPrice)) {
2053
- materializeType = resolveSpreadOrderSide(descriptorPrice, mgr.config.startPrice);
2200
+ const sidePrice = ladderLevel ?? descriptorPrice;
2201
+ materializeType = resolveSpreadOrderSide(sidePrice, mgr.config.startPrice);
2054
2202
  }
2055
2203
  const descriptorSize = toFiniteNumber(placedDescriptor?.size, NaN);
2056
2204
  const hasDescriptor = !!materializeType
@@ -2065,14 +2213,39 @@ class SyncEngine {
2065
2213
  // slot-scheme metadata are absent. The next
2066
2214
  // readOpenOrders pass repopulates rawOnChain
2067
2215
  // from the live order (slot-scheme ids only).
2216
+ //
2217
+ // PRICE comes from the GENESIS LADDER, not
2218
+ // from the descriptor. The descriptor is
2219
+ // whatever order object the caller carried
2220
+ // in, and writing it here made a slot's price
2221
+ // whichever price that object happened to
2222
+ // hold — the same carried-price-into-
2223
+ // slot.price class as the adoption writer,
2224
+ // and for a slot being materialized at a new
2225
+ // index it can be a price from a different
2226
+ // grid entirely. The descriptor price is kept
2227
+ // only when genesis is unavailable
2228
+ // (migration), where there is no ladder to
2229
+ // derive from.
2230
+ let materializePrice = descriptorPrice;
2231
+ let priceSource = 'descriptor';
2232
+ if (ladderLevel !== null && materializeIdx !== null) {
2233
+ materializePrice = ladderLevel;
2234
+ priceSource = 'genesis';
2235
+ }
2068
2236
  const materializedOrder = {
2069
2237
  id: gridOrderId,
2070
2238
  type: materializeType,
2071
- price: descriptorPrice,
2239
+ price: materializePrice,
2072
2240
  size: descriptorSize,
2073
2241
  state: isPartialPlacement ? ORDER_STATES.PARTIAL : ORDER_STATES.ACTIVE,
2074
2242
  orderId: chainOrderId,
2075
2243
  };
2244
+ if (priceSource === 'descriptor' && materializeIdx !== null) {
2245
+ mgr.logger?.log?.(`[SYNC] createOrder for ${gridOrderId}: materialized with the DESCRIPTOR price ` +
2246
+ `${descriptorPrice} because no genesis ladder was available to derive ` +
2247
+ `the slot level — verify this slot's price on the next sync`, 'warn');
2248
+ }
2076
2249
  if (chainData.deferredFee !== undefined && chainData.deferredFee !== null) {
2077
2250
  materializedOrder.btsFeeState = { deferredFee: Math.max(0, chainData.deferredFee) };
2078
2251
  }
@@ -2081,10 +2254,10 @@ class SyncEngine {
2081
2254
  fee: fee,
2082
2255
  });
2083
2256
  if (materialized === false) {
2084
- mgr.logger?.log?.(`[SYNC] createOrder linkage FAILED: grid order ${gridOrderId} not in master and materializing chain order ${chainOrderId} (${materializeType} @${descriptorPrice} x${descriptorSize}) was rejected — chain order left untracked`, 'error');
2257
+ mgr.logger?.log?.(`[SYNC] createOrder linkage FAILED: grid order ${gridOrderId} not in master and materializing chain order ${chainOrderId} (${materializeType} @${materializePrice} x${descriptorSize}) was rejected — chain order left untracked`, 'error');
2085
2258
  }
2086
2259
  else {
2087
- mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: materialized ${materializeType} @${descriptorPrice} x${descriptorSize} -> ${chainOrderId} (master lost the slot mid-broadcast)`, 'warn');
2260
+ mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: materialized ${materializeType} @${materializePrice} x${descriptorSize} (price source: ${priceSource}) -> ${chainOrderId} (master lost the slot mid-broadcast)`, 'warn');
2088
2261
  }
2089
2262
  }
2090
2263
  }