// portfolio-data.jsx — ru number formatting helpers + API→PF mapping // Exports to window: fmt (formatting), mapPortfolio (API→PF), PF (mock fallback) // ---------- formatting ---------- const MINUS = "−"; // proper minus sign − const NBSP = " "; function group(intPart) { // space thousands separator, ru style return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, NBSP); } function num(value, decimals) { const neg = value < 0; const abs = Math.abs(value); let s = abs.toFixed(decimals); let [int, dec] = s.split("."); int = group(int); let out = dec != null && decimals > 0 ? int + "," + dec : int; return { neg, out }; } // auto-decimals for prices: <1 → 4 decimals, else 2 function priceDecimals(v) { return Math.abs(v) > 0 && Math.abs(v) < 1 ? 4 : 2; } // auto-decimals for quantity: <10 → up to 4, else 2 function qtyDecimals(v) { return Math.abs(v) < 10 ? 4 : 2; } const fmt = { MINUS, NBSP, // money: cur '$' | '€'; opts {dec, sign} money(value, cur = "$", opts = {}) { const dec = opts.dec != null ? opts.dec : 2; const { neg, out } = num(value, dec); const sign = opts.sign ? (neg ? MINUS : "+") : neg ? MINUS : ""; return sign + cur + out; }, price(value, cur = "$") { let { neg, out } = num(value, priceDecimals(value)); // trim trailing ",00" / extra zeros for cleaner prices if (out.indexOf(",") !== -1) out = out.replace(/0+$/, "").replace(/,$/, ""); return (neg ? MINUS : "") + cur + out; }, qty(value) { // trim trailing zeros but keep ru comma const dec = qtyDecimals(value); let { out } = num(value, dec); if (out.indexOf(",") !== -1) out = out.replace(/0+$/, "").replace(/,$/, ""); return out; }, pct(value, opts = {}) { const dec = opts.dec != null ? opts.dec : 1; const { neg, out } = num(value, dec); const sign = opts.sign ? (neg ? MINUS : "+") : neg ? MINUS : ""; return sign + out + "%"; }, rate(value) { const { out } = num(value, 4); return out; }, }; // ---------- API → PF mapping ---------- // Преобразует ответ portfolio_db.get_full_portfolio() в форму, которую рендерит // PortfolioScreen (см. структуру PF ниже). Расчёты не дублируются — берём готовые поля. function mapPortfolio(api) { const fx = api.eur_usd_rate || 1; const positions = api.positions || []; const lp = api.lp || []; const loans = api.loans || []; const cash = api.cash || { total_usd: 0, breakdown: [], percent_of_portfolio: 0 }; return { empty: !!api.empty, updatedAt: Date.now(), fx, summary: { investedEur: api.total_invested_eur || 0, valueUsd: api.total_assets_usd || 0, valueEur: fx > 0 ? (api.total_assets_usd || 0) / fx : 0, pnlEur: api.pnl_eur || 0, pnlPct: api.pnl_eur_pct || 0, debtUsd: api.loans_total_usd || 0, netUsd: api.net_value_usd || 0, netEur: api.net_value_eur || 0, // Валюта пользователя (Portfolio_saas мультивалютный): символ для money() и // код для меток курса. «eur»-имена полей выше оставлены ради переиспользования // дизайна price_alert, но значения — в этой валюте. cur: api.currency_symbol || "$", curCode: api.currency || "USD", }, coins: positions.map((p) => ({ ticker: p.symbol, name: p.symbol, share: p.percent_of_portfolio || 0, invested: p.invested_amount || 0, qty: p.quantity || 0, avg: p.avg_entry_price || 0, price: p.current_price || 0, value: p.current_value || 0, pnl: (p.current_value || 0) - (p.invested_amount || 0), pnlPct: (p.drawdown_percent || 0) - 100, })), cash: { total: cash.total_usd || 0, share: cash.percent_of_portfolio || 0, items: (cash.breakdown || []).map((b) => ({ ticker: b.symbol, amount: b.amount })), }, lp: lp.map((l) => ({ id: l.id, pair: `${l.token0_symbol} / ${l.token1_symbol}`, venue: l.dex || (l.is_v3 ? "Uniswap V3" : "Uniswap V2"), share: l.percent_of_portfolio || 0, isV3: !!l.is_v3, rangeLow: l.price_lower, rangeHigh: l.price_upper, curPrice: l.current_price, inRange: l.in_range, invested: l.invested_usd || 0, value: l.current_value_usd || 0, il: l.impermanent_loss_pct || 0, pnl: l.pnl_usd || 0, pnlPct: l.pnl_pct || 0, composition: [ { ticker: l.token0_symbol, qty: l.amount0 || 0, value: l.value0_usd || 0, weight: Math.round(l.proportion0_pct || 0) }, { ticker: l.token1_symbol, qty: l.amount1 || 0, value: l.value1_usd || 0, weight: Math.round(l.proportion1_pct || 0) }, ], })), loans: loans.map((l) => ({ id: l.id, amount: l.amount, ticker: l.currency, collateral: l.collateral || "—", })), debtTotal: api.loans_total_usd || 0, }; } // ---------- mock data (fallback for offline design preview only) ---------- const PF = { updatedAt: Date.now() - 3 * 60 * 1000, fx: 1.08, summary: { investedEur: 60000, valueUsd: 77220, valueEur: 71500, pnlEur: 8722, pnlPct: 14.5, debtUsd: 3000, netUsd: 74220, netEur: 68722 }, coins: [ { ticker: "BTC", name: "Bitcoin", share: 43.1, invested: 28000, qty: 0.35, avg: 80000, price: 95000, value: 33250, pnl: 5250, pnlPct: 18.8 }, { ticker: "ETH", name: "Ethereum", share: 16.9, invested: 12000, qty: 4.2, avg: 2857, price: 3100, value: 13020, pnl: 1020, pnlPct: 8.5 }, { ticker: "SOL", name: "Solana", share: 8.7, invested: 7500, qty: 50, avg: 150, price: 135, value: 6750, pnl: -750, pnlPct: -10.0 }, ], cash: { total: 8400, share: 10.9, items: [{ ticker: "USDC", amount: 5400 }, { ticker: "USDT", amount: 3000 }] }, lp: [ { id: 1, pair: "cbBTC / USDC", venue: "Uniswap V3", share: 20.5, isV3: true, rangeLow: 85000, rangeHigh: 110000, curPrice: 95000, inRange: true, invested: 15000, value: 15800, il: -0.4, pnl: 800, pnlPct: 5.3, composition: [{ ticker: "cbBTC", qty: 0.08, value: 7600, weight: 48 }, { ticker: "USDC", qty: 8200, value: 8200, weight: 52 }] }, ], loans: [{ id: 1, amount: 3000, ticker: "USDC", collateral: "cbBTC" }], debtTotal: 3000, }; Object.assign(window, { PF, fmt, mapPortfolio });