In DCA volume based on → Required change, the computed safety-order size can come out negative. Nothing catches it: the only guard is Math.min(maxVolumeSize, orderSize), which caps the top but not the bottom.
The rule behind it: buying more can only pull the reference (Take Profit / Average / Breakeven) closer to your buy price — never further away, and never past it. So the Required change % has to land inside a window:
- Floor — the distance that remains even with infinite volume: Average
0%, Breakeven≈ round-trip fees, Take Profit= TP% - Ceiling — the distance that already exists the moment the safety order fills
Outside that window the formula doesn’t return “a big number” — it returns a negative one.
Three constellations (long; short is mirrored):
1. Trigger too close to the average — Required change > the distance that already exists.
Average 100, SO fills at 98.50 (−1.5%), Required change 5% → −0.68 units.
This is the common case for indicator-triggered DCA: the trigger distance isn’t configured, the indicator decides when it fires. A fixed 5% is impossible if the indicator fires 1.5% below the average.
2. Required change below the floor.
Same setup, Required change 0.1% → −16.26 units.
Note 0.1 is the UI’s own min on that field — and for Breakeven the floor is the round-trip fee (~0.2%), so the smallest value the UI allows is already inside the broken zone.
3. Trigger while in profit (price above the average) — negative regardless of Required change.
Average 100, SO fills at 102: Required change 5% → −1.39, 0.1% → −20.6.
Buying at 102 moves the average up toward 102 but can never push it above 102, while “Required change” presupposes the average sits above the price (i.e. you’re underwater). Indicator-driven DCA can absolutely fire here.
Suggested fix — one check, covers all three modes and both directions:
orderSize = Math.min(maxVolumeSize, orderSize)
if (!Number.isFinite(orderSize) || orderSize <= 0) {
orderSize = Number.isFinite(maxVolumeSize) ? maxVolumeSize : _orderSize
}
Why validate the result instead of the input: the valid window depends on the deal’s current average, which only exists at runtime — it cannot be checked in the bot config form. Checking the result catches all three constellations with a single condition, and also handles the Infinity / division-by-zero edge where the target lands exactly on the buy price.