Skip to content

fix(assets): price the spot Market Sell instead of sending a zero - #406

Merged
guyverino merged 1 commit into
mainfrom
fix/spot-market-sell-price
Sep 3, 2026
Merged

fix(assets): price the spot Market Sell instead of sending a zero#406
guyverino merged 1 commit into
mainfrom
fix/spot-market-sell-price

Conversation

@guyverino

Copy link
Copy Markdown
Collaborator

What & why

Market Sell on an Assets spot row failed silently. TDoSellOrderCommand carries no
market-order flag
— unlike ClosePosition, whose market_sell bool is real — so the
price=0 this path sent was never "at market". The core substituted the market's lot step
for the zero and divided the size by it: a 0.005 BTC holding reached Binance as
quantity=499.99999, was rejected five times by NOTIONAL, and nothing surfaced in the UI.

Moonbot's own Market Sell fills like a market order by pricing a LIMIT order through the
book
, so this reproduces that: last * 0.8, measured from the core's own sale (ask
78528.68 → order 62822.94 → filled at 78528.67). A venue with a tighter band corrects it
itself — Bitget answered 25205 trading price cannot be below 2% and the core refilled at
its own bound.

Notable decisions

  • The size stays the coin quantity. It was tempting to carry over NewOrderParams::size
    ("in the core account's balance currency"), but that documents an opening order — how
    much balance to spend — while this command says how much coin to sell. Two measured
    attempts settled it: a balance-currency notional sold 79.67 of a 198.01 AERO holding, and
    offered $0.37 of a $103 MANTRA holding, which the venue rejected for its 1 USDT minimum.
  • The price is read live at confirmation, not captured at render. A wallet-derived row
    (Bitget and others expose spot holdings only through transfer_assets) carries a USDT
    valuation rather than a quote price, and a dialog left open goes stale against the book it
    has to cross.
  • latest_price had to be fixed to serve that. Its fallback to the snapshot's p_last
    sat unreachable under an early return demanding history readers, which moonproto only
    builds for markets inside the client's TradeStorageScope. History is an optimisation
    here, not a requirement; the now-unreachable LatestPriceError::NoHistoryReaders is gone.
  • A dropped command now says so. The confirmation previously showed nothing when no
    command was sent.

Known limitations

  • Not verified live on an exchange. The defect and the two mis-sized attempts are all
    measured from core logs; the corrected build has not yet placed a real sale. The one check
    that closes it: one Market Sell on a real holding, with Sell: <whole balance> and
    SELL order DONE ! Quantity: equal to it in the core log.
  • The 0.8 factor is ported from one observed core sale. Venues with a tighter band rely on
    the core's own correction, as Bitget did above.
  • Errors the core reports after accepting the command still do not reach the UI — a
    rejected order is visible only in the core log. Out of scope here.

How to verify

cargo build -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets
cargo clippy -p moon-core -p moon-ui-gpui --target x86_64-pc-windows-msvc --all-targets -- -D warnings
cargo test -p moon-core -p moon-ui-gpui --target x86_64-pc-windows-msvc
  • build (all targets): exit 0
  • tests: moon-core 1528 passed / 0 failed, moon-ui-gpui 1387 + 262 passed / 0 failed,
    everything else green (7 ignored, pre-existing)
  • clippy: repo carries pre-existing debt, so the gate is no new finding — 16 findings in the
    touched files on this branch against 18 on origin/main, none inside this diff's hunks
    (trade.rs 163–233, read.rs 203–232)
  • FireTest: not run — this change touches neither chart, render, windows nor input

🤖 Generated with Claude Code

`TDoSellOrderCommand` carries no market-order flag — unlike `ClosePosition`,
whose `market_sell` bool is real — so `price=0` was never a market order. The
core substituted the market's lot step for the zero and divided by it: a
0.005 BTC holding reached Binance as `quantity=499.99999`, rejected five times
by NOTIONAL, and the button failed silently (2026-09-03 BinKEAUSDC log).

Moonbot's own Market Sell fills like a market order by pricing a LIMIT order
through the book, so reproduce that: `last * 0.8`, measured from the core's own
sale (ask 78528.68 -> order 62822.94, filled at 78528.67). A venue with a
tighter band corrects it itself — Bitget answered `25205 trading price cannot
be below 2%` and the core refilled at its own bound.

The size stays the coin quantity, which is what this command has always taken.
Two measured attempts prove it: sending a balance-currency notional sold
79.67 of 198.01 AERO, and offered $0.37 of a $103 MANTRA holding, which the
venue rejected for its 1 USDT minimum.

The price is read live at confirmation rather than captured at render: a
wallet-derived row (Bitget and others expose spot holdings only through
`transfer_assets`) carries a USDT valuation, not a quote price, and an open
dialog goes stale against the book it must cross. `latest_price` could not
serve that: its fallback to the snapshot's `p_last` sat unreachable under an
early return demanding history readers, which moonproto only builds for markets
inside the client's `TradeStorageScope`.

A refusal now says so — the confirmation showed nothing at all when the command
was dropped.
@guyverino
guyverino merged commit 985379f into main Sep 3, 2026
6 checks passed
@guyverino
guyverino deleted the fix/spot-market-sell-price branch September 3, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant