Implementing Limit Order Placement in a Mobile Exchange App
A limit order is a type of request that the exchange executes only when the specified price or better is reached. Users enter price and volume, click "Buy" — the order sits in the order book until execution, cancellation, or expiration (GTC, IOC, FOK). At first glance, a form with two fields. In practice — linked logic for three InputFields, real-time recalculation, balance validation, confirmation with current price, and handling API edge cases.
Order Form: Mutual Field Recalculation
Three fields — Price, Amount, Total — linked by formula Total = Price × Amount. Users can change any of them, the other two recalculate automatically. This breaks simple reactivity: listening to onChange for all three simultaneously creates an infinite loop.
Solution — single source of truth: changing Price or Amount recalculates Total; changing Total recalculates Amount (Price stays fixed). Flag isUserEditing or 150ms debounce prevents cycling.
// iOS — mutual field recalculation via Combine
class LimitOrderViewModel: ObservableObject {
@Published var price: String = ""
@Published var amount: String = ""
@Published var total: String = ""
private var cancellables = Set<AnyCancellable>()
private var isUpdating = false
init() {
Publishers.CombineLatest($price, $amount)
.debounce(for: .milliseconds(100), scheduler: RunLoop.main)
.sink { [weak self] p, a in
guard let self, !self.isUpdating else { return }
guard let price = Decimal(string: p), let amount = Decimal(string: a) else { return }
self.isUpdating = true
self.total = "\(price * amount)"
self.isUpdating = false
}
.store(in: &cancellables)
}
}
On Android — use TextWatcher or Flow with distinctUntilChanged() in ViewModel. MutableStateFlow for each field, combine in CoroutineScope.
Balance Percentage Slider
Standard UX: 25% / 50% / 75% / 100% buttons below the Amount field. Clicking 50% sets Amount to available balance / 2 / current price. If Price is empty, buttons are inactive. If balance is below the exchange's minimum lot, show a warning but don't disable.
Validation Before Sending
Minimum client-side checks:
- Price > 0 and within range (exchange returns
minPrice,maxPrice,tickSizeinexchangeInfo) - Amount >=
minQty, Amount is a multiple ofstepSize - Total >=
minNotional(minimum trade sum, e.g., 10 USDT) - Available balance >= Total (for buy) or >= Amount (for sell)
tickSize and stepSize matter more than they seem. Binance API returns -1013 LOT_SIZE if Amount isn't a multiple of stepSize. Round via floor(amount / stepSize) * stepSize with BigDecimal (not float!) to prevent this error.
// Android — rounding with stepSize
fun roundToStep(value: BigDecimal, step: BigDecimal): BigDecimal {
return (value.divide(step, 0, RoundingMode.FLOOR)).multiply(step)
.setScale(step.scale(), RoundingMode.FLOOR)
}
Order Confirmation
Before sending to API, show a confirmation dialog with final parameters. Price may have changed — display the current market price next to the limit price so users see the distance to market. Confirmation button includes haptic feedback (UIImpactFeedbackGenerator / HapticFeedback in Jetpack Compose).
After successful API response, update the open orders list. Either a WebSocket executionReport event (Binance) or polling every 1–2 seconds.
Handling API Errors
Typical Binance REST API error codes for order placement:
| Code | Cause | UI Response |
|---|---|---|
| -1013 LOT_SIZE | Amount isn't a multiple of stepSize | Auto-round |
| -1013 MIN_NOTIONAL | Total < minNotional | Show minimum sum |
| -2010 Account has insufficient balance | Insufficient funds | Highlight Amount red |
| -1021 Timestamp for this request | Clock skew | Retry with corrected time |
Error -1021 shouldn't be shown to users — retry with adjusted recvWindow or sync time via /api/v3/time.
Timeline: 2–3 days for mutual field recalculation, balance percentage slider, exchange validation, confirmation dialog, API error handling.







