@@ -160,8 +160,10 @@ pub struct LeaseOpeningStateInfo {
160160
161161#[ derive( Debug , Clone , Serialize , Deserialize ) ]
162162pub struct LeaseEtlData {
163- /// Downpayment amount in USD
163+ /// Downpayment amount (LS_cltr_amnt_stable: asset_micro_units × price)
164164 pub downpayment_amount : Option < String > ,
165+ /// Collateral symbol (e.g., "USDC_NOBLE" or "ALL_BTC") — determines downpayment decimals
166+ pub collateral_symbol : Option < String > ,
165167 /// Opening price per asset
166168 pub price : Option < String > ,
167169 /// LPN price at opening (for short positions)
@@ -738,6 +740,7 @@ async fn fetch_lease_info(
738740 // Note: Some fields are at the top level of EtlLeaseOpening, not inside lease
739741 let etl_info = etl_data. as_ref ( ) . map ( |d| LeaseEtlData {
740742 downpayment_amount : d. lease . downpayment_amount . clone ( ) ,
743+ collateral_symbol : d. lease . collateral_symbol . clone ( ) ,
741744 // Opening price is inside lease.opening_price (was LS_opening_price)
742745 price : d. lease . opening_price . clone ( ) ,
743746 // lpn_price and fee are at the top level, not inside lease
@@ -1120,7 +1123,7 @@ fn parse_opened_status(status: &Option<serde_json::Value>) -> Option<LeaseInProg
11201123/// Where:
11211124/// - assetValueUsd = Dec(amount, assetDecimals) * assetPrice
11221125/// - totalDebtUsd = Dec(debtTotal, lpnDecimals) * lpnPrice
1123- /// - downPayment = Dec(etl.downpayment_amount, lpnDecimals )
1126+ /// - downPayment = Dec(etl.downpayment_amount, collateralDecimals )
11241127/// - fee = Dec(etl.fee, assetDecimals)
11251128/// - repaymentValue = parseFloat(etl.repayment_value) (already formatted)
11261129fn calculate_pnl (
@@ -1163,7 +1166,20 @@ fn calculate_pnl(
11631166 let debt_total: f64 = total_debt. parse ( ) . ok ( ) ?;
11641167 let total_debt_usd = ( debt_total / 10_f64 . powi ( lpn_decimals) ) * lpn_price;
11651168
1166- // Parse ETL data: downpayment uses LPN decimals
1169+ // Parse ETL data: downpayment uses collateral asset decimals.
1170+ // LS_cltr_amnt_stable = asset_micro_units × price, so the decimal factor
1171+ // comes from the collateral asset, not LPN (they differ when the user pays
1172+ // the downpayment in a non-LPN asset like BTC).
1173+ let cltr_decimals = etl
1174+ . lease
1175+ . collateral_symbol
1176+ . as_deref ( )
1177+ . and_then ( |cltr_ticker| {
1178+ let cltr_key = format ! ( "{}@{}" , cltr_ticker, protocol) ;
1179+ currencies. currencies . get ( & cltr_key) . map ( |c| c. decimal_digits as i32 )
1180+ } )
1181+ . unwrap_or ( lpn_decimals) ;
1182+
11671183 // None legitimately means 0 (no downpayment recorded); parse failure is a data issue
11681184 let downpayment_raw: f64 = match etl. lease . downpayment_amount . as_deref ( ) {
11691185 Some ( s) => s. parse ( ) . unwrap_or_else ( |_| {
@@ -1175,7 +1191,7 @@ fn calculate_pnl(
11751191 } ) ,
11761192 None => 0.0 ,
11771193 } ;
1178- let downpayment = downpayment_raw / 10_f64 . powi ( lpn_decimals ) ;
1194+ let downpayment = downpayment_raw / 10_f64 . powi ( cltr_decimals ) ;
11791195
11801196 // Fee uses asset decimals (matching frontend behavior)
11811197 let fee_raw: f64 = match etl. fee . as_deref ( ) {
@@ -1646,13 +1662,22 @@ mod tests {
16461662 }
16471663
16481664 fn make_etl ( downpayment : & str , fee : & str , repayment_value : & str ) -> EtlLeaseOpening {
1665+ make_etl_with_collateral ( downpayment, fee, repayment_value, None )
1666+ }
1667+
1668+ fn make_etl_with_collateral (
1669+ downpayment : & str ,
1670+ fee : & str ,
1671+ repayment_value : & str ,
1672+ collateral_symbol : Option < & str > ,
1673+ ) -> EtlLeaseOpening {
16491674 EtlLeaseOpening {
16501675 lease : EtlLeaseInfo {
16511676 timestamp : None ,
16521677 downpayment_amount : Some ( downpayment. to_string ( ) ) ,
16531678 loan_amount : None ,
16541679 lease_position_ticker : None ,
1655- collateral_symbol : None ,
1680+ collateral_symbol : collateral_symbol . map ( |s| s . to_string ( ) ) ,
16561681 opening_price : None ,
16571682 history : None ,
16581683 } ,
@@ -1857,6 +1882,56 @@ mod tests {
18571882 assert ! ( result. is_none( ) ) ;
18581883 }
18591884
1885+ #[ test]
1886+ fn test_calculate_pnl_btc_collateral ( ) {
1887+ // Reproduces the bug: user pays downpayment in BTC, not USDC.
1888+ // LS_cltr_amnt_stable = asset_micro × price = 13200000 × 75583.85 ≈ 997706865842
1889+ // Correct: divide by 10^8 (BTC decimals) = $9977.07
1890+ // Bug: divide by 10^6 (USDC decimals) = $997706.87 (100x too large)
1891+ //
1892+ // BTC at $90,000, holding 0.46 BTC = $41,400
1893+ // Debt: 14974 USDC at $1.00 = $14,974
1894+ // Downpayment: 0.132 BTC at $75,583.85 → LS_cltr_amnt_stable = 997706865842
1895+ // Correct downpayment = 997706865842 / 10^8 = $9977.07
1896+ // Repayment: $1000
1897+ // PnL = 41400 - 14974 - 9977.07 + 0 - 1000 = $15448.93
1898+ // PnL% = 15448.93 / (9977.07 + 1000) * 100 = 140.77%
1899+ let opened = make_opened ( "ALL_BTC" , "46000000" , "USDC_NOBLE" ) ; // 0.46 BTC
1900+ let etl = make_etl_with_collateral (
1901+ "997706865842" , // LS_cltr_amnt_stable (asset_micro × price)
1902+ "0" ,
1903+ "1000" ,
1904+ Some ( "ALL_BTC" ) , // collateral is BTC, not USDC
1905+ ) ;
1906+ let ( prices, currencies) = make_prices_and_currencies ( "90000" , "1.0" ) ;
1907+
1908+ let result = calculate_pnl (
1909+ "TEST-PROTOCOL" ,
1910+ & opened,
1911+ "14974000000" , // ~14974 USDC debt
1912+ Some ( & etl) ,
1913+ Some ( & prices) ,
1914+ Some ( & currencies) ,
1915+ ) ;
1916+
1917+ let pnl = result. unwrap ( ) ;
1918+ let amount: f64 = pnl. amount . parse ( ) . unwrap ( ) ;
1919+ let percent: f64 = pnl. percent . parse ( ) . unwrap ( ) ;
1920+ // With correct BTC decimals (8): downpayment = $9977.07
1921+ // PnL ≈ $15448.93, PnL% ≈ 140.77%
1922+ assert ! (
1923+ ( amount - 15448.93 ) . abs( ) < 1.0 ,
1924+ "PnL amount should be ~$15449 but got {}" ,
1925+ amount
1926+ ) ;
1927+ assert ! (
1928+ ( percent - 140.77 ) . abs( ) < 1.0 ,
1929+ "PnL percent should be ~140.8% but got {}" ,
1930+ percent
1931+ ) ;
1932+ assert ! ( pnl. pnl_positive) ;
1933+ }
1934+
18601935 #[ test]
18611936 fn test_enrich_history_action_liquidation_with_cause ( ) {
18621937 assert_eq ! (
0 commit comments