Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions modules/bot_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || `<unnamed-${index}>`;
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)
Expand Down