diff --git a/modules/bot_settings.ts b/modules/bot_settings.ts index 9707453d..367001cd 100644 --- a/modules/bot_settings.ts +++ b/modules/bot_settings.ts @@ -248,6 +248,70 @@ function collectValidationIssues(entries: any[], sourceName: string): { errors: if (entry.active) errors.push(issue); else warnings.push(issue); } + // Grid capacity check: activeOrders vs Range/Incr (issue #19) + // Per-side estimation mirrors createOrderGrid geometry: + // levels on each side grow geometrically from the center, + // gap slots are split around the boundary. + if (entry.active && entry.activeOrders && entry.incrementPercent && entry.targetSpreadPercent) { + try { + const incr = Number(entry.incrementPercent); + const spread = Number(entry.targetSpreadPercent); + const buy = Number(entry.activeOrders.buy || 0); + const sell = Number(entry.activeOrders.sell || 0); + if (Number.isFinite(incr) && incr > 0 && Number.isFinite(spread) && spread >= 0 && Number.isFinite(buy) && Number.isFinite(sell)) { + const parseFactor = (v: any): number | null => { + if (typeof v === 'string' && v.trim().toLowerCase().endsWith('x')) { + const n = parseFloat(v); + return Number.isFinite(n) && n > 0 ? n : null; + } + const n = Number(v); + return Number.isFinite(n) && n > 0 ? n : null; + }; + const isXFactor = (v: any) => typeof v === 'string' && String(v).trim().toLowerCase().endsWith('x'); + const minF = parseFactor(entry.minPrice); + const maxF = parseFactor(entry.maxPrice); + const minIsX = isXFactor(entry.minPrice); + const maxIsX = isXFactor(entry.maxPrice); + if (minF !== null && maxF !== null) { + const step = 1 + incr / 100; + const gapSlots = Math.max(2, Math.ceil(Math.log(1 + spread / 100) / Math.log(step)) - 1); + // Buy side spans from center down to minPrice: + // x-factor min "1.15x" -> distance log(1.15) + // absolute min -> distance log(center/min); approximate center + // by geometric mean of bounds (ratio-based, no price needed). + let buyDistance: number | null = null; + let sellDistance: number | null = null; + if (minIsX && maxIsX) { + buyDistance = Math.log(minF); + sellDistance = Math.log(maxF); + } else if (!minIsX && !maxIsX) { + const center = Math.sqrt(minF * maxF); + buyDistance = Math.log(center / minF); + sellDistance = Math.log(maxF / center); + } + if (buyDistance !== null && sellDistance !== null && buyDistance > 0 && sellDistance > 0) { + const buyCapacity = Math.floor(buyDistance / Math.log(step)) - Math.floor(gapSlots / 2) - 1; + const sellCapacity = Math.floor(sellDistance / Math.log(step)) - Math.ceil(gapSlots / 2) - 1; + const name = entry.name || ``; + if (sell > sellCapacity) { + warnings.push( + `Bot[${index}] '${name}' (${sourceName}): activeOrders sell:${sell} exceeds grid capacity — Range [${entry.minPrice}-${entry.maxPrice}] with Incr ${incr}% fits ~${Math.max(0, sellCapacity)} sell orders. ` + + `After reset the surplus silently stays unfilled (e.g. S:27 showed Active 22/27, Slots 23 sell). ` + + `Widen maxPrice, lower Incr, or reduce activeOrders.sell.` + ); + } + if (buy > buyCapacity) { + warnings.push( + `Bot[${index}] '${name}' (${sourceName}): activeOrders buy:${buy} exceeds grid capacity — Range [${entry.minPrice}-${entry.maxPrice}] with Incr ${incr}% fits ~${Math.max(0, buyCapacity)} buy orders. ` + + `After reset the surplus silently stays unfilled. ` + + `Widen minPrice, lower Incr, or reduce activeOrders.buy.` + ); + } + } + } + } + } catch {} + } }); // Cross-bot validation: check for duplicate botKeys (sanitized names)