@@ -46,8 +46,8 @@ use crate::{
4646 common:: {
4747 consts:: { OKX_POST_ONLY_CANCEL_REASON , OKX_POST_ONLY_CANCEL_SOURCE } ,
4848 enums:: {
49- OKXBookAction , OKXCandleConfirm , OKXOrderCategory , OKXOrderStatus , OKXOrderType ,
50- OKXTriggerType ,
49+ OKXBookAction , OKXCandleConfirm , OKXInstrumentType , OKXOrderCategory , OKXOrderStatus ,
50+ OKXOrderType , OKXSide , OKXTargetCurrency , OKXTriggerType ,
5151 } ,
5252 models:: OKXInstrument ,
5353 parse:: {
@@ -892,8 +892,77 @@ pub fn parse_order_status_report(
892892 } ;
893893
894894 let size_precision = instrument. size_precision ( ) ;
895- let quantity = parse_quantity ( & msg. sz , size_precision) ?;
896- let filled_qty = parse_quantity ( & msg. acc_fill_sz . clone ( ) . unwrap_or_default ( ) , size_precision) ?;
895+
896+ // Parse quantities based on target currency
897+ // OKX always returns acc_fill_sz in base currency, but sz depends on tgt_ccy
898+
899+ // Determine if this is a quote-quantity order
900+ // Method 1: Explicit tgt_ccy field set to QuoteCcy
901+ let is_quote_qty_explicit = msg. tgt_ccy == Some ( OKXTargetCurrency :: QuoteCcy ) ;
902+
903+ // Method 2: Use OKX defaults when tgt_ccy is None (old orders or missing field)
904+ // OKX API defaults for SPOT market orders: BUY orders use quote_ccy, SELL orders use base_ccy
905+ // Note: tgtCcy only applies to SPOT market orders (not limit orders)
906+ // For limit orders, sz is always in base currency regardless of side
907+ let is_quote_qty_heuristic = msg. tgt_ccy . is_none ( )
908+ && ( msg. inst_type == OKXInstrumentType :: Spot || msg. inst_type == OKXInstrumentType :: Margin )
909+ && msg. side == OKXSide :: Buy
910+ && msg. ord_type == OKXOrderType :: Market ;
911+
912+ let ( quantity, filled_qty) = if is_quote_qty_explicit || is_quote_qty_heuristic {
913+ // Quote-quantity order: sz is in quote currency, need to convert to base
914+ let sz_quote = msg. sz . parse :: < f64 > ( ) . map_err ( |e| {
915+ anyhow:: anyhow!( "Failed to parse sz='{}' as quote quantity: {}" , msg. sz, e)
916+ } ) ?;
917+
918+ // Determine the price to use for conversion
919+ // Priority: 1) limit price (px) for limit orders, 2) avg_px for market orders
920+ let conversion_price = if !msg. px . is_empty ( ) && msg. px != "0" {
921+ // Limit order: use the limit price (msg.px)
922+ msg. px
923+ . parse :: < f64 > ( )
924+ . map_err ( |e| anyhow:: anyhow!( "Failed to parse px='{}': {}" , msg. px, e) ) ?
925+ } else if !msg. avg_px . is_empty ( ) && msg. avg_px != "0" {
926+ // Market order with fills: use average fill price
927+ msg. avg_px
928+ . parse :: < f64 > ( )
929+ . map_err ( |e| anyhow:: anyhow!( "Failed to parse avg_px='{}': {}" , msg. avg_px, e) ) ?
930+ } else {
931+ 0.0
932+ } ;
933+
934+ // Convert quote quantity to base: quantity_base = sz_quote / price
935+ let quantity_base = if conversion_price > 0.0 {
936+ Quantity :: new ( sz_quote / conversion_price, size_precision)
937+ } else {
938+ // No price available, can't convert - use sz as-is temporarily
939+ // This will be corrected once the order gets filled and price is available
940+ parse_quantity ( & msg. sz , size_precision) ?
941+ } ;
942+
943+ let filled_qty =
944+ parse_quantity ( & msg. acc_fill_sz . clone ( ) . unwrap_or_default ( ) , size_precision) ?;
945+
946+ ( quantity_base, filled_qty)
947+ } else {
948+ // Base-quantity order: both sz and acc_fill_sz are in base currency
949+ let quantity = parse_quantity ( & msg. sz , size_precision) ?;
950+ let filled_qty =
951+ parse_quantity ( & msg. acc_fill_sz . clone ( ) . unwrap_or_default ( ) , size_precision) ?;
952+
953+ ( quantity, filled_qty)
954+ } ;
955+
956+ // For quote-quantity orders marked as FILLED, adjust quantity to match filled_qty
957+ // to avoid precision mismatches from quote-to-base conversion
958+ let ( quantity, filled_qty) = if ( is_quote_qty_explicit || is_quote_qty_heuristic)
959+ && msg. state == OKXOrderStatus :: Filled
960+ && filled_qty. is_positive ( )
961+ {
962+ ( filled_qty, filled_qty)
963+ } else {
964+ ( quantity, filled_qty)
965+ } ;
897966
898967 let ts_accepted = parse_millisecond_timestamp ( msg. c_time ) ;
899968 let ts_last = parse_millisecond_timestamp ( msg. u_time ) ;
0 commit comments