Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/openapi.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -5194,6 +5194,13 @@
],
"description": "Downpayment amount (LS_cltr_amnt_stable: asset_micro_units × price)"
},
"loan_amount_stable": {
"type": [
"string",
"null"
],
"description": "Loan principal in stable USD at lease open (LS_loan_amnt_stable, 6\ndecimals). Used to compute frozen initial leverage that doesn't drift\nwith accruing interest or manual repayments."
},
"collateral_symbol": {
"type": [
"string",
Expand Down
5 changes: 5 additions & 0 deletions backend/src/external/etl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ pub struct EtlLeaseInfo {
pub downpayment_amount: Option<String>,
#[serde(rename = "LS_loan_amnt_asset")]
pub loan_amount: Option<String>,
/// LS_loan_amnt_stable — the loan principal in stable USD at lease open
/// (6 decimals). Used to compute the frozen initial leverage, which
/// doesn't drift with interest accrual or partial repayments.
#[serde(rename = "LS_loan_amnt_stable")]
pub loan_amount_stable: Option<String>,
/// LS_asset_symbol is the leveraged asset (e.g., ALL_BTC)
#[serde(rename = "LS_asset_symbol")]
pub lease_position_ticker: Option<String>,
Expand Down
6 changes: 6 additions & 0 deletions backend/src/handlers/leases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ pub struct LeaseOpeningStateInfo {
pub struct LeaseEtlData {
/// Downpayment amount (LS_cltr_amnt_stable: asset_micro_units × price)
pub downpayment_amount: Option<String>,
/// Loan principal in stable USD at lease open (LS_loan_amnt_stable, 6
/// decimals). Used to compute frozen initial leverage that doesn't drift
/// with accruing interest or manual repayments.
pub loan_amount_stable: Option<String>,
/// Collateral symbol (e.g., "USDC_NOBLE" or "ALL_BTC") — determines downpayment decimals
pub collateral_symbol: Option<String>,
/// Opening price per asset
Expand Down Expand Up @@ -861,6 +865,7 @@ async fn fetch_lease_info(
// Note: Some fields are at the top level of EtlLeaseOpening, not inside lease
let etl_info = etl_data.as_ref().map(|d| LeaseEtlData {
downpayment_amount: d.lease.downpayment_amount.clone(),
loan_amount_stable: d.lease.loan_amount_stable.clone(),
collateral_symbol: d.lease.collateral_symbol.clone(),
// Opening price is inside lease.opening_price (was LS_opening_price)
price: d.lease.opening_price.clone(),
Expand Down Expand Up @@ -1800,6 +1805,7 @@ mod tests {
timestamp: None,
downpayment_amount: Some(downpayment.to_string()),
loan_amount: None,
loan_amount_stable: None,
lease_position_ticker: None,
collateral_symbol: collateral_symbol.map(|s| s.to_string()),
opening_price: None,
Expand Down
3 changes: 3 additions & 0 deletions src/common/api/types/leases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export interface LeaseOpeningStateInfo {
export interface LeaseEtlData {
/** Downpayment amount (LS_cltr_amnt_stable: asset_micro_units × price) */
downpayment_amount?: string;
/** Loan principal in stable USD at lease open (LS_loan_amnt_stable, 6 decimals).
* Used to compute frozen initial leverage. */
loan_amount_stable?: string;
/** Collateral symbol (e.g., "USDC_NOBLE" or "ALL_BTC") — determines downpayment decimals */
collateral_symbol?: string;
/** Opening price per asset */
Expand Down
17 changes: 17 additions & 0 deletions src/common/utils/LeaseCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface LeaseDisplayData {
openingPrice: Dec;
fee: Dec;
repaymentValue: Dec;
/** Frozen initial leverage from the lease-open snapshot
* `(downPayment + loanAtOpen) / downPayment`. null when ETL didn't expose
* `loan_amount_stable` (older leases ingested before that field shipped). */
leverageAtOpen: Dec | null;
// In progress status
inProgressType: "opening" | "repayment" | "close" | "liquidation" | "slippage_protection" | null;
// Asset amounts
Expand Down Expand Up @@ -132,6 +136,18 @@ export class LeaseCalculator {
currency?.decimal_digits ?? 8
);

// Frozen initial leverage from the lease-open snapshot. LS_loan_amnt_stable
// is scaled by the LPN's native decimals (1e6 for OSMO, 1e8 for ALL_BTC,
// etc.), not by stable decimals — using a fixed 6 here inflated the loan
// 100x on BTC shorts and produced ~76x leverage on a real 1.75x position.
// Falls back to null if ETL hasn't exposed the field for this lease — the
// share-pnl card then uses the live (drifting) formula as a degraded
// fallback.
const leverageAtOpen =
lease.etl_data?.loan_amount_stable && downPayment.isPositive()
? downPayment.add(new Dec(lease.etl_data.loan_amount_stable, lpnDecimals)).quo(downPayment)
: null;

// Calculate PnL
const { pnlAmount, pnlPercent, pnlPositive } = this.calculatePnl(
assetValueUsd,
Expand Down Expand Up @@ -174,6 +190,7 @@ export class LeaseCalculator {
openingPrice,
fee,
repaymentValue,
leverageAtOpen,
inProgressType,
unitAsset,
stableAsset
Expand Down
18 changes: 12 additions & 6 deletions src/modules/leases/components/single-lease/SharePnLDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,22 @@ const positionSizeUsd = () => {

const pnlNumber = () => Number(leaseDisplayData?.pnlPercent.toString(2) ?? "0");

// Effective leverage at open: (downPayment + debt) / downPayment. For a
// freshly opened lease the debt equals the borrowed principal, so this is the
// classic "I went 2.5x on this trade" number. As interest accrues totalDebt
// drifts up, which would creep this slightly over its initial value — small
// effect over the lifetime of a lease and acceptable for a share card.
// Prefer the frozen initial leverage from the lease-open snapshot — it
// matches what the user actually opened at (e.g. exactly 2.5x at protocol
// max) and doesn't drift with interest accrual or manual repayments. Falls
// back to a live computation when ETL hasn't exposed loan_amount_stable for
// the lease (older rows pre-dating that field). The previous live formula
// added downPayment (USD) to totalDebt (LPN units) which produced a wildly
// inflated leverage on shorts; the fallback uses totalDebtUsd to keep the
// magnitude sane even when frozen data is unavailable.
const leverageMultiple = (): string | null => {
if (!leaseDisplayData) return null;
if (leaseDisplayData.leverageAtOpen && leaseDisplayData.leverageAtOpen.isPositive()) {
return `x${Number(leaseDisplayData.leverageAtOpen.toString()).toFixed(1)}`;
}
const dp = leaseDisplayData.downPayment;
if (!dp.isPositive()) return null;
const lev = dp.add(leaseDisplayData.totalDebt).quo(dp);
const lev = dp.add(leaseDisplayData.totalDebtUsd).quo(dp);
return `x${Number(lev.toString()).toFixed(1)}`;
};

Expand Down
Loading