Skip to content

Short stop-loss is converted with the long formula mirrored (manual order stop price, chart stop line) #708

Description

@guyverino

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions