Problem
The terminal converts a percentage stop-loss into a price for a short as the long's formula
mirrored, entry * (1 + pct/100). The core computes a short's stop as entry / (1 - pct/100)
(for StopLoss = -pct), so the terminal's price is lower than the core's:
| stop |
terminal (mirror) |
core (division) |
gap |
| -2.5 % |
entry × 1.02500 |
entry × 1.02564 |
0.06 % of the price |
| -5 % |
entry × 1.05000 |
entry × 1.05263 |
0.25 % |
| -10 % |
entry × 1.10000 |
entry × 1.11111 |
1.0 % |
Longs are fine: the core uses entry * (1 - pct/100) for them too.
Where
crates/moon-ui-gpui/src/backend/manual_trading.rs — stop_price: the per-order stop of a manual
order is sent to the core as a fixed absolute price (OrderStopsForm { sl: fixed: true, price }),
so a short's manual stop sits closer to the entry than the same percentage would put it through
the core's own percentage mode.
crates/moon-core/src/feed/live/convert.rs — pct_level_price (via stop_loss_line_price, and the
pct_stop closure used for the trailing-stop price): the chart's stop line of a short order is
drawn at the mirrored price, and the doc comment says the percent "is converted the way the core
would". The "core quirk" branch there (a short's percentage stop arriving resolved with the long
formula, mirrored back as 2 * entry - level) should be rechecked against the division formula
too.
Evidence
- Report rows (
orders_rep.sellreason, StopLoss fixed: X), strategies without the stop ladder and
without StopLossModifier: for shorts the printed level matches buy / (1 + StopLoss/100) on
11 099 rows and the mirror on 25; for longs buy * (1 + StopLoss/100) matches on 20 930 rows.
- Core log:
StopLoss applied (buyPrice 0.22650 stop -2.50% => 0.23231) for a short
(0.2265 / 0.975 = 0.23231; the mirror would give 0.23216). 215 short lines in the logs of
2026-09-20..24 land on the division, including distances already adjusted by StopLossModifier.
The tuner's replay model had the same mirror and now uses the division
(db/tuner/ticks/exit/stops.rs::stop_level, local branch feat/tuner-ticks-axis); these two
live-trading paths were left as they are on purpose, pending this issue.
Expected
A short's percentage stop becomes entry / (1 - pct/100) in both places (one shared helper), so the
fixed price the terminal sends and the line it draws match what the core itself would place.
Problem
The terminal converts a percentage stop-loss into a price for a short as the long's formula
mirrored,
entry * (1 + pct/100). The core computes a short's stop asentry / (1 - pct/100)(for
StopLoss = -pct), so the terminal's price is lower than the core's:Longs are fine: the core uses
entry * (1 - pct/100)for them too.Where
crates/moon-ui-gpui/src/backend/manual_trading.rs—stop_price: the per-order stop of a manualorder is sent to the core as a fixed absolute price (
OrderStopsForm { sl: fixed: true, price }),so a short's manual stop sits closer to the entry than the same percentage would put it through
the core's own percentage mode.
crates/moon-core/src/feed/live/convert.rs—pct_level_price(viastop_loss_line_price, and thepct_stopclosure used for the trailing-stop price): the chart's stop line of a short order isdrawn at the mirrored price, and the doc comment says the percent "is converted the way the core
would". The "core quirk" branch there (a short's percentage stop arriving resolved with the long
formula, mirrored back as
2 * entry - level) should be rechecked against the division formulatoo.
Evidence
orders_rep.sellreason,StopLoss fixed: X), strategies without the stop ladder andwithout
StopLossModifier: for shorts the printed level matchesbuy / (1 + StopLoss/100)on11 099 rows and the mirror on 25; for longs
buy * (1 + StopLoss/100)matches on 20 930 rows.StopLoss applied (buyPrice 0.22650 stop -2.50% => 0.23231)for a short(
0.2265 / 0.975 = 0.23231; the mirror would give0.23216). 215 short lines in the logs of2026-09-20..24 land on the division, including distances already adjusted by
StopLossModifier.The tuner's replay model had the same mirror and now uses the division
(
db/tuner/ticks/exit/stops.rs::stop_level, local branchfeat/tuner-ticks-axis); these twolive-trading paths were left as they are on purpose, pending this issue.
Expected
A short's percentage stop becomes
entry / (1 - pct/100)in both places (one shared helper), so thefixed price the terminal sends and the line it draws match what the core itself would place.