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
|
@@ -198,6 +198,117 @@ function _filterUnmatchedChainOrders(manager, chainOrderId) {
|
|
|
198
198
|
manager._lastUnmatchedChainOrders = manager._lastUnmatchedChainOrders.filter((u) => (u?.id || u?.orderId || u?.chainOrderId) !== chainOrderId);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Remove a single correction entry by its full queue key
|
|
203
|
+
* (chainOrderId + surplus flag). The queue's upsert key is
|
|
204
|
+
* (chainOrderId, isSurplus), so a chain-order-only filter would silently
|
|
205
|
+
* discard a sibling entry (e.g. a cancel-only orphan sharing the id with
|
|
206
|
+
* a price update). Callers pass the entry's own isSurplus flag.
|
|
207
|
+
*/
|
|
208
|
+
function _removeCorrectionEntry(manager, chainOrderId, isSurplus) {
|
|
209
|
+
const surplus = Boolean(isSurplus);
|
|
210
|
+
if (manager && Array.isArray(manager.ordersNeedingPriceCorrection)) {
|
|
211
|
+
manager.ordersNeedingPriceCorrection = manager.ordersNeedingPriceCorrection.filter((c) => c?.chainOrderId !== chainOrderId || Boolean(c?.isSurplus) !== surplus);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Stamp queue provenance on a correction entry: queued-at timestamp plus
|
|
216
|
+
* the detector that produced it. Existing provenance (e.g. a fresher
|
|
217
|
+
* re-queue refreshing queuedAt) is preserved on merge — the sync_engine
|
|
218
|
+
* upsert spreads the new entry over the old one, so a re-queued entry
|
|
219
|
+
* keeps its original queuedAt unless the caller explicitly refreshes it.
|
|
220
|
+
* @param {Object} entry - Correction entry being queued
|
|
221
|
+
* @param {string} source - provenance tag (see queuedBy values)
|
|
222
|
+
* @returns {Object} The same entry, stamped
|
|
223
|
+
*/
|
|
224
|
+
function _stampCorrectionProvenance(entry, source) {
|
|
225
|
+
if (entry && typeof entry === 'object') {
|
|
226
|
+
if (entry.queuedAt == null)
|
|
227
|
+
entry.queuedAt = Date.now();
|
|
228
|
+
if (entry.queuedBy == null)
|
|
229
|
+
entry.queuedBy = source;
|
|
230
|
+
}
|
|
231
|
+
return entry;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Drain-time staleness validation for a price-update correction entry
|
|
235
|
+
* (Fix 1): verify the queued intent still matches the LIVE grid geometry
|
|
236
|
+
* before broadcasting.
|
|
237
|
+
*
|
|
238
|
+
* A correction entry snapshots {slot id, chainOrderId, expectedPrice} at
|
|
239
|
+
* queue time. Any geometry-changing resync (trigger-file resync,
|
|
240
|
+
* reconcileGridOrders startup path, COW commit re-map) can re-slot the
|
|
241
|
+
* chain order or move the slot's price afterwards, leaving the entry
|
|
242
|
+
* stale. Broadcasting it would REVERT the resync's placement — the
|
|
243
|
+
* duplicate-price-level incident class (stale UPDATE is the exact
|
|
244
|
+
* negation of the resync's placement, to the satoshi).
|
|
245
|
+
*
|
|
246
|
+
* An entry is actionable only when the live slot:
|
|
247
|
+
* 1. still exists in the master grid,
|
|
248
|
+
* 2. still owns this chainOrderId (not re-slotted / adopted elsewhere),
|
|
249
|
+
* 3. still targets the queued price — via priceSlotEqual on genesis
|
|
250
|
+
* grids (same integer-round-trip predicate the pass-1 detector
|
|
251
|
+
* uses) or calculatePriceTolerance on legacy grids (same predicate
|
|
252
|
+
* the detector uses there).
|
|
253
|
+
*
|
|
254
|
+
* Cancel-type entries (cancelOnly / isSurplus) are exempt — a cancel is
|
|
255
|
+
* idempotent (gone orders resolve via the orderGone path) and never
|
|
256
|
+
* re-prices onto a stale level.
|
|
257
|
+
*
|
|
258
|
+
* @param {Object} manager - OrderManager instance (live grid + assets)
|
|
259
|
+
* @param {Object} entry - Queued correction entry
|
|
260
|
+
* @returns {{valid: boolean, reason: string}} valid=false drops the entry
|
|
261
|
+
*/
|
|
262
|
+
function _validatePriceCorrectionEntry(manager, entry) {
|
|
263
|
+
if (!entry || entry.cancelOnly === true || entry.isSurplus === true) {
|
|
264
|
+
return { valid: true, reason: 'cancel-type' };
|
|
265
|
+
}
|
|
266
|
+
const slotId = entry?.gridOrder?.id;
|
|
267
|
+
const slot = (slotId && manager?.orders instanceof Map) ? manager.orders.get(slotId) : null;
|
|
268
|
+
if (!slot) {
|
|
269
|
+
return { valid: false, reason: `slot ${slotId || '?'} no longer exists` };
|
|
270
|
+
}
|
|
271
|
+
if (slot.orderId !== entry.chainOrderId) {
|
|
272
|
+
return { valid: false, reason: `slot ${slotId} now owns ${slot.orderId || 'no order'} (entry targets ${entry.chainOrderId})` };
|
|
273
|
+
}
|
|
274
|
+
const assets = manager?.assets;
|
|
275
|
+
const precision = entry.type === ORDER_TYPES.SELL ? assets?.assetA?.precision : assets?.assetB?.precision;
|
|
276
|
+
let priceMatches = false;
|
|
277
|
+
try {
|
|
278
|
+
const genesis = manager?._genesis;
|
|
279
|
+
if (genesis && Array.isArray(genesis.priceLevels)) {
|
|
280
|
+
priceMatches = priceSlotEqual(slot.price, entry.expectedPrice, precision);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const tolerance = MathUtils.calculatePriceTolerance(entry.expectedPrice, entry.size, entry.type, assets);
|
|
284
|
+
priceMatches = Math.abs(slot.price - entry.expectedPrice) <= (tolerance ?? 0);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
priceMatches = slot.price === entry.expectedPrice;
|
|
289
|
+
}
|
|
290
|
+
if (!priceMatches) {
|
|
291
|
+
return { valid: false, reason: `slot ${slotId} now targets ${slot.price} (entry queued ${entry.expectedPrice})` };
|
|
292
|
+
}
|
|
293
|
+
// Size check: the broadcast sends amountToSell from the QUEUED snapshot.
|
|
294
|
+
// A partial fill between queue and drain changes the slot's booked size;
|
|
295
|
+
// pushing the stale size would over-write the fill (chain side rebuilds
|
|
296
|
+
// the delta from a live re-read, so it cannot corrupt, but it can still
|
|
297
|
+
// surprise). Integer-quantum comparison, same convention as the
|
|
298
|
+
// pass-1 size check — a fill-changed entry drops and the next sync
|
|
299
|
+
// re-queues from the fresh size if the order is still off-target.
|
|
300
|
+
try {
|
|
301
|
+
const sizePrecision = entry.type === ORDER_TYPES.SELL ? assets?.assetA?.precision : assets?.assetB?.precision;
|
|
302
|
+
if (isValidNumber(slot.size) && isValidNumber(entry.size)
|
|
303
|
+
&& floatToBlockchainInt(slot.size, sizePrecision) !== floatToBlockchainInt(entry.size, sizePrecision)) {
|
|
304
|
+
return { valid: false, reason: `slot ${slotId} size moved ${entry.size} -> ${slot.size} (fill changed it after queueing)` };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Precision unavailable — fail open on size (ownership + price hold).
|
|
309
|
+
}
|
|
310
|
+
return { valid: true, reason: 'live-slot-match' };
|
|
311
|
+
}
|
|
201
312
|
// ================================================================================
|
|
202
313
|
// SECTION 1: CHAIN ORDER MATCHING & RECONCILIATION
|
|
203
314
|
// ================================================================================
|
|
@@ -416,7 +527,7 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
|
|
|
416
527
|
}
|
|
417
528
|
finally {
|
|
418
529
|
if (shouldRemove) {
|
|
419
|
-
manager
|
|
530
|
+
_removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
|
|
420
531
|
}
|
|
421
532
|
}
|
|
422
533
|
}
|
|
@@ -448,7 +559,7 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
|
|
|
448
559
|
}
|
|
449
560
|
finally {
|
|
450
561
|
if (shouldRemove) {
|
|
451
|
-
manager
|
|
562
|
+
_removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
|
|
452
563
|
}
|
|
453
564
|
}
|
|
454
565
|
}
|
|
@@ -478,14 +589,20 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
|
|
|
478
589
|
// The guard returns before the try/finally below, so drop the entry
|
|
479
590
|
// from the correction queue here — otherwise it would linger forever
|
|
480
591
|
// and re-attempt on every sync cycle.
|
|
481
|
-
manager
|
|
592
|
+
_removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
|
|
482
593
|
return { success: false, skipped: true, error: 'crossed-placement-guard' };
|
|
483
594
|
}
|
|
484
595
|
try {
|
|
485
596
|
const updateResult = await accountOrders.updateOrder(accountName, privateKey, chainOrderId, { amountToSell, minToReceive });
|
|
486
597
|
if (updateResult === null) {
|
|
598
|
+
// Zero-delta no-op: the chain order already equals the target
|
|
599
|
+
// (replayed correction, sub-unit rounding, or a landed update
|
|
600
|
+
// observed via a lagging read). Resolved, not failed — counting
|
|
601
|
+
// it as failed turns routine no-ops into permanent MAINT WARN
|
|
602
|
+
// noise ("Delta is 0; skipping" every cycle) that hides real
|
|
603
|
+
// reverts.
|
|
487
604
|
shouldRemove = true;
|
|
488
|
-
return { success:
|
|
605
|
+
return { success: true, skipped: true };
|
|
489
606
|
}
|
|
490
607
|
shouldRemove = true;
|
|
491
608
|
return { success: true };
|
|
@@ -510,7 +627,7 @@ async function correctOrderPriceOnChain(manager, correctionInfo, accountName, pr
|
|
|
510
627
|
}
|
|
511
628
|
finally {
|
|
512
629
|
if (shouldRemove) {
|
|
513
|
-
manager
|
|
630
|
+
_removeCorrectionEntry(manager, chainOrderId, correctionInfo?.isSurplus);
|
|
514
631
|
}
|
|
515
632
|
}
|
|
516
633
|
}
|
|
@@ -533,8 +650,7 @@ async function _resolveCancelledCorrection(manager, entry) {
|
|
|
533
650
|
});
|
|
534
651
|
}
|
|
535
652
|
_filterUnmatchedChainOrders(manager, chainOrderId);
|
|
536
|
-
|
|
537
|
-
.filter((c) => c.chainOrderId !== chainOrderId);
|
|
653
|
+
_removeCorrectionEntry(manager, chainOrderId, entry?.isSurplus);
|
|
538
654
|
}
|
|
539
655
|
/**
|
|
540
656
|
* Broadcast all cancel-type corrections (cancelOnly duplicate orphans +
|
|
@@ -671,20 +787,49 @@ async function correctAllPriceMismatches(manager, accountName, privateKey, accou
|
|
|
671
787
|
const results = [];
|
|
672
788
|
let corrected = 0;
|
|
673
789
|
let failed = 0;
|
|
790
|
+
let staleDropped = 0;
|
|
791
|
+
// Dedupe on the full queue key (chainOrderId + surplus flag),
|
|
792
|
+
// matching the sync upsert key. A chain-order-only key would drop a
|
|
793
|
+
// sibling entry (price update + cancel sharing one chain id) before
|
|
794
|
+
// it ever drains.
|
|
674
795
|
const seen = new Set();
|
|
675
796
|
const ordersToCorrect = (manager.ordersNeedingPriceCorrection || []).filter((c) => {
|
|
676
|
-
if (!c.chainOrderId
|
|
797
|
+
if (!c.chainOrderId)
|
|
798
|
+
return false;
|
|
799
|
+
const key = `${c.chainOrderId}|${Boolean(c.isSurplus)}`;
|
|
800
|
+
if (seen.has(key))
|
|
677
801
|
return false;
|
|
678
|
-
seen.add(
|
|
802
|
+
seen.add(key);
|
|
679
803
|
return true;
|
|
680
804
|
});
|
|
681
|
-
|
|
805
|
+
// Fix 1 — drain-time staleness validation for price-update entries:
|
|
806
|
+
// a geometry-changing resync between queue and drain leaves entries
|
|
807
|
+
// whose slot was re-slotted or re-priced. Broadcasting them would
|
|
808
|
+
// revert the resync's placement (duplicate-price-level incident).
|
|
809
|
+
// Validate against the LIVE slot; drop stale entries (the next sync
|
|
810
|
+
// re-queues if the order is genuinely still off-target). Cancel-type
|
|
811
|
+
// entries are exempt (idempotent, never re-price).
|
|
812
|
+
const liveEntries = [];
|
|
813
|
+
for (const entry of ordersToCorrect) {
|
|
814
|
+
const check = _validatePriceCorrectionEntry(manager, entry);
|
|
815
|
+
if (check.valid) {
|
|
816
|
+
liveEntries.push(entry);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
_removeCorrectionEntry(manager, entry.chainOrderId, entry?.isSurplus);
|
|
820
|
+
staleDropped++;
|
|
821
|
+
results.push({ ...entry, result: { success: true, skipped: true, staleDropped: true, staleReason: check.reason } });
|
|
822
|
+
manager?.logger?.log?.(`[CORRECTION] Dropping stale price correction for ${entry.chainOrderId} ` +
|
|
823
|
+
`(queued ${entry.queuedBy || 'unknown-source'}@${entry.queuedAt ? new Date(entry.queuedAt).toISOString() : 'unknown-time'}): ` +
|
|
824
|
+
`${check.reason}; re-queued by next sync if still off-target`, 'info');
|
|
825
|
+
}
|
|
826
|
+
const canBatch = liveEntries.length > 1
|
|
682
827
|
&& typeof accountOrders?.buildCancelOrderOp === 'function'
|
|
683
828
|
&& typeof accountOrders?.executeBatch === 'function';
|
|
684
|
-
let serialEntries =
|
|
829
|
+
let serialEntries = liveEntries;
|
|
685
830
|
if (canBatch) {
|
|
686
|
-
const cancelEntries =
|
|
687
|
-
const updateEntries =
|
|
831
|
+
const cancelEntries = liveEntries.filter((c) => c.cancelOnly === true || c.isSurplus === true);
|
|
832
|
+
const updateEntries = liveEntries.filter((c) => !(c.cancelOnly === true || c.isSurplus === true));
|
|
688
833
|
if (cancelEntries.length > 1) {
|
|
689
834
|
const batchOutcome = await _batchCancelCorrections(manager, cancelEntries, accountName, privateKey, accountOrders);
|
|
690
835
|
corrected += batchOutcome.corrected;
|
|
@@ -708,7 +853,7 @@ async function correctAllPriceMismatches(manager, accountName, privateKey, accou
|
|
|
708
853
|
if (corrected > 0 && typeof manager.persistGrid === 'function') {
|
|
709
854
|
await manager.persistGrid();
|
|
710
855
|
}
|
|
711
|
-
return { corrected, failed, results };
|
|
856
|
+
return { corrected, failed, results, staleDropped };
|
|
712
857
|
});
|
|
713
858
|
}
|
|
714
859
|
// ================================================================================
|
|
@@ -1269,6 +1414,123 @@ function isNonBlockingUnmatchedOrder(order) {
|
|
|
1269
1414
|
const reason = order?.reason;
|
|
1270
1415
|
return typeof reason === 'string' && reason.endsWith('-deferred');
|
|
1271
1416
|
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Reasons a deferred hold is a candidate for STRUCTURAL RESYNC escalation.
|
|
1419
|
+
*
|
|
1420
|
+
* Deliberately narrower than `isNonBlockingUnmatchedOrder`. That predicate is
|
|
1421
|
+
* a broad "do not treat this as a blocker" net (by design, so a new defer
|
|
1422
|
+
* reason cannot silently freeze the grid). Escalation asks a stricter
|
|
1423
|
+
* question: can a structural resync plausibly resolve this hold?
|
|
1424
|
+
*
|
|
1425
|
+
* The distinction matters because a resync cannot end a broadcast region or
|
|
1426
|
+
* re-evaluate a boundary that has not committed. Escalating on those holds
|
|
1427
|
+
* spends a full grid reload (and possibly a reset) on something the owning
|
|
1428
|
+
* machinery already resolves on its own. Excluded deliberately:
|
|
1429
|
+
* - `broadcast-active-deferred`: transient; "rebalance retries after the
|
|
1430
|
+
* region ends". Self-resolving, and unaffected by a resync.
|
|
1431
|
+
* - `boundary-hold-trailing-market`: owned by boundary/trailing-market
|
|
1432
|
+
* machinery, which re-evaluates when the market moves.
|
|
1433
|
+
* - `held-plan-unchanged-deferred`: an identical replan was suppressed
|
|
1434
|
+
* because nothing changed; it clears on the next fill, not on a reload.
|
|
1435
|
+
* - `boundary-unknown-deferred`: gap geometry is unknown pre-boundary; the
|
|
1436
|
+
* comment at sync_engine.ts names the accepted cost as one sync cycle
|
|
1437
|
+
* after the boundary commits. Self-resolving, not stranded.
|
|
1438
|
+
*
|
|
1439
|
+
* Anything not listed is treated as NOT escalatable, so a future transient
|
|
1440
|
+
* reason is excluded by default rather than silently becoming a resync
|
|
1441
|
+
* trigger.
|
|
1442
|
+
*/
|
|
1443
|
+
const STRANDED_HOLD_REASONS = new Set([
|
|
1444
|
+
'out-of-rail-deferred',
|
|
1445
|
+
'out-of-grid-deferred',
|
|
1446
|
+
]);
|
|
1447
|
+
/**
|
|
1448
|
+
* @param {Object} order - Unmatched chain order entry.
|
|
1449
|
+
* @returns {boolean} True when the hold is stranded and a structural resync is
|
|
1450
|
+
* a plausible remedy (see STRANDED_HOLD_REASONS).
|
|
1451
|
+
*/
|
|
1452
|
+
function isStrandedHoldOrder(order) {
|
|
1453
|
+
const reason = order?.reason;
|
|
1454
|
+
return typeof reason === 'string' && STRANDED_HOLD_REASONS.has(reason);
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* GRID-PRICE-INVARIANT — the price emitted for a slot must be its genesis
|
|
1458
|
+
* level. Range guards (isChainPriceOutOfGrid) only test the configured min/max
|
|
1459
|
+
* bounds, so an off-grid price can sit inside the bounds while being far
|
|
1460
|
+
* outside the active window; that band has no check unless this one runs.
|
|
1461
|
+
*
|
|
1462
|
+
* Unjudgeable inputs evaluate to ok:true (missing genesis, unparseable slot id,
|
|
1463
|
+
* non-finite price) — the guard must never fire on bad metadata, only on a
|
|
1464
|
+
* genuine mismatch. Never throws.
|
|
1465
|
+
*
|
|
1466
|
+
* See docs/GRID_PRICE_INVARIANT.md.
|
|
1467
|
+
*
|
|
1468
|
+
* @param {string} slotId - Grid slot id (slot-<idx>)
|
|
1469
|
+
* @param {number} price - Price about to be emitted
|
|
1470
|
+
* @param {any} genesis - manager._genesis (priceLevels table)
|
|
1471
|
+
* @returns {{ok: boolean, reason: string, expected: number|null, idx: number|null, drift: number|null}}
|
|
1472
|
+
*/
|
|
1473
|
+
function checkGridPriceInvariant(slotId, price, genesis) {
|
|
1474
|
+
const pass = { ok: true, expected: null, idx: null, drift: null };
|
|
1475
|
+
try {
|
|
1476
|
+
if (!genesis || !Array.isArray(genesis?.priceLevels) || genesis.priceLevels.length === 0) {
|
|
1477
|
+
return { ...pass, reason: 'no-genesis' };
|
|
1478
|
+
}
|
|
1479
|
+
const idx = (typeof slotId === 'string') ? parseSlotIndex(slotId) : null;
|
|
1480
|
+
if (idx === null || !Number.isFinite(idx) || idx < 0 || idx >= genesis.priceLevels.length) {
|
|
1481
|
+
return { ...pass, reason: 'uncheckable-slot' };
|
|
1482
|
+
}
|
|
1483
|
+
const p = Number(price);
|
|
1484
|
+
if (!Number.isFinite(p) || p <= 0)
|
|
1485
|
+
return { ...pass, reason: 'invalid-price', idx };
|
|
1486
|
+
const expected = Number(MathUtils.priceForSlot(idx, genesis));
|
|
1487
|
+
if (!Number.isFinite(expected) || expected <= 0)
|
|
1488
|
+
return { ...pass, reason: 'invalid-level', idx };
|
|
1489
|
+
// Same equality as assertSlotPriceInvariant at grid build/load:
|
|
1490
|
+
// relative 1e-9 with an absolute floor.
|
|
1491
|
+
const diff = Math.abs(p - expected);
|
|
1492
|
+
const rel = diff / Math.max(1e-12, Math.abs(expected));
|
|
1493
|
+
const ok = !(rel > 1e-9 && diff > 1e-12);
|
|
1494
|
+
return { ok, reason: ok ? 'ok' : 'off-grid-price', expected, idx, drift: rel };
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
return { ...pass, reason: 'check-failed' };
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Report one GRID-PRICE-INVARIANT check. Never throws: an unjudgeable check
|
|
1502
|
+
* returns true so a bad check can never block a legitimate emission. Warns on a
|
|
1503
|
+
* mismatch so the offending slot, site, expected level and drift are named at
|
|
1504
|
+
* the point of emission. Silent when the check passes or the price is not
|
|
1505
|
+
* checkable.
|
|
1506
|
+
*
|
|
1507
|
+
* Returns the check result so calling sites can choose their policy: the
|
|
1508
|
+
* reconcile sites treat `false` as blocking (skip the emission and let the next
|
|
1509
|
+
* cycle re-plan). See docs/GRID_PRICE_INVARIANT.md.
|
|
1510
|
+
*
|
|
1511
|
+
* @param {any} manager - OrderManager (reads _genesis, writes logger)
|
|
1512
|
+
* @param {string} slotId - Destination slot id whose level the price must match
|
|
1513
|
+
* @param {number} price - Price about to be emitted
|
|
1514
|
+
* @param {string} site - Emitting site label (CREATE / UPDATE / RECONCILE-*)
|
|
1515
|
+
* @returns {boolean} True when the price is acceptable (or unjudgeable). Callers
|
|
1516
|
+
* at the reconcile sites treat `false` as a blocking mismatch and skip the
|
|
1517
|
+
* emission, so this must return the check result rather than void.
|
|
1518
|
+
*/
|
|
1519
|
+
function reportGridPriceInvariant(manager, slotId, price, site) {
|
|
1520
|
+
try {
|
|
1521
|
+
const inv = checkGridPriceInvariant(slotId, price, manager?._genesis);
|
|
1522
|
+
if (inv.ok)
|
|
1523
|
+
return true;
|
|
1524
|
+
manager?.logger?.log?.(`[GRID-PRICE-INVARIANT] ${site} for ${slotId} at ${Format.formatPrice6(Number(price))} ` +
|
|
1525
|
+
`is NOT the genesis level ${inv.expected != null ? Format.formatPrice6(inv.expected) : 'n/a'} ` +
|
|
1526
|
+
`(slot idx ${inv.idx}, drift ${inv.drift != null ? (inv.drift * 100).toFixed(4) + '%' : 'n/a'}) ` +
|
|
1527
|
+
`— off-grid price SKIPPED (emission blocked at this site)`, 'warn');
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
catch { /* invariant check is best-effort: never block on a thrown check */
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1272
1534
|
/**
|
|
1273
1535
|
* Check if order is on blockchain (ACTIVE or PARTIAL state).
|
|
1274
1536
|
*
|
|
@@ -2711,6 +2973,6 @@ function collectKnownOnChainOrderIds(mgr, placedResults, placedContexts, extraCr
|
|
|
2711
2973
|
const all = new Set([...masterIds, ...createIds]);
|
|
2712
2974
|
return { masterIds: [...masterIds], createIds: [...createIds], all: [...all] };
|
|
2713
2975
|
}
|
|
2714
|
-
export { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, buildFillKey, correctOrderPriceOnChain, correctAllPriceMismatches, buildCreateOrderArgs, getOrderTypeFromUpdatedFlags, resolveConfiguredPriceBound, virtualizeOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, geometryTypeForSlotIndex, detectGapEvacuationCandidates, updateGapEvacuationStreaks, resolveSpreadOrderSide, chainOrderMatchesSlot, chainOrderMatchesSlotWithTolerance, crossingCandidateChainId, isCrossingCheckCandidate, buildCrossingCheckCandidates, parseSlotIndex, filterOrdersByType, buildOutsideInPairGroups, extractBatchOperationResults, formatUnmatchedChainOrder, isNonBlockingUnmatchedOrder, isOrderOnChain, isOrderVirtual, hasOnChainId, isOrderPlaced, isPhantomOrder, isSlotAvailable, isEmptyGridSlot, isOrderHealthy, checkSizeThreshold, checkSizesBeforeMinimum, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType, shouldFlagOutOfSpread, buildIndexes, validateIndexes, ordersEqual, buildDelta, deriveTargetBoundary, isShiftEligibleFill, resolveReserveCount, resolveReserveOrders, selectReserveEdgeSlots, getActiveOrdersTotal, getSideBudget, calculateBudgetedSizes, buildCreateOpFingerprint, isOrderGoneErrorMessage, recordDuplicateOrphanDetection, clearDuplicateOrphanDetection, duplicateOrphanLogInfo, chainOrderUnchangedFromCache, detectCrossedBookPlan, collectKnownOnChainOrderIds, reserveEdgeIdSet, liveWindowIdSet };
|
|
2976
|
+
export { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, buildFillKey, correctOrderPriceOnChain, correctAllPriceMismatches, _validatePriceCorrectionEntry, _stampCorrectionProvenance, buildCreateOrderArgs, getOrderTypeFromUpdatedFlags, resolveConfiguredPriceBound, virtualizeOrder, convertToSpreadPlaceholder, toRailHolePlaceholder, geometryTypeForSlotIndex, detectGapEvacuationCandidates, updateGapEvacuationStreaks, resolveSpreadOrderSide, chainOrderMatchesSlot, chainOrderMatchesSlotWithTolerance, crossingCandidateChainId, isCrossingCheckCandidate, buildCrossingCheckCandidates, parseSlotIndex, filterOrdersByType, buildOutsideInPairGroups, extractBatchOperationResults, formatUnmatchedChainOrder, isNonBlockingUnmatchedOrder, isStrandedHoldOrder, isOrderOnChain, isOrderVirtual, hasOnChainId, isOrderPlaced, isPhantomOrder, isSlotAvailable, isEmptyGridSlot, isOrderHealthy, checkSizeThreshold, checkSizesBeforeMinimum, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType, shouldFlagOutOfSpread, buildIndexes, validateIndexes, ordersEqual, buildDelta, deriveTargetBoundary, isShiftEligibleFill, resolveReserveCount, resolveReserveOrders, selectReserveEdgeSlots, getActiveOrdersTotal, getSideBudget, calculateBudgetedSizes, buildCreateOpFingerprint, isOrderGoneErrorMessage, recordDuplicateOrphanDetection, clearDuplicateOrphanDetection, duplicateOrphanLogInfo, chainOrderUnchangedFromCache, detectCrossedBookPlan, collectKnownOnChainOrderIds, reserveEdgeIdSet, liveWindowIdSet, checkGridPriceInvariant, reportGridPriceInvariant };
|
|
2715
2977
|
export { resolveReserveEdgeAnchorPrice, resolveLiveReserveEdgeAnchorPrice, compareReserveEdge, collectRefillSlotIds };
|
|
2716
2978
|
//# sourceMappingURL=order.js.map
|