From 736978c91b304916c0b6d58996f2c76e0225512c Mon Sep 17 00:00:00 2001 From: ojy Date: Thu, 14 May 2026 03:10:45 +0000 Subject: [PATCH] Positions: add natural-mark P/L column + spread-cost tracker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standard "current value" used bid/ask MID per leg — what the broker shows. That's optimistic for multi-leg positions because you can't actually close at mid; longs sell at the BID, shorts buy back at the ASK. For a 4-leg LEAPS structure the bid-ask gap can swing the mark by hundreds of dollars vs the real close-now value. Now the table shows both side-by-side: - Current $ (mid) - broker-style mark - Current $ (nat) - realistic close-now value - Net P/L (mid) - what the broker reports - Net P/L (close now) - what you'd actually pocket today - Spread $ - difference (= bid-ask cost to close) Summary cards row gains the same split: "Net P/L (mid)", "Net P/L (close now)", "Spread cost". Tooltips on every header explain the methodology. Live test on the open INTC iron-fly: mid P/L shows +$235, natural P/L shows -$38 — the $272 spread cost is exactly what makes the day-1 paper P/L look better than reality. The natural column is the honest decision-making number. Co-Authored-By: Claude Sonnet 4.6 --- frontend/positions.html | 64 +++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/frontend/positions.html b/frontend/positions.html index 55d4f03..28937e9 100644 --- a/frontend/positions.html +++ b/frontend/positions.html @@ -195,10 +195,11 @@
-
Open positions
-
Total entered
-
Current value
-
Net P/L (after commissions)
+
Open positions
+
Total entered
+
Net P/L (mid mark)
+
Net P/L (close now)
+
Spread cost
@@ -227,10 +228,12 @@ SymStrategyLegs Entered $ - Current $ + Current $ (mid) + Current $ (nat) Comm. (RT) - Gross P/L - Net P/L + Net P/L (mid) + Net P/L (close now) + Spread $ P/L % Opened Status @@ -245,10 +248,12 @@ + - - - + + + + @@ -333,7 +338,7 @@ }, rowFor(o) { - let entered = 0, value = 0; + let entered = 0, value = 0, valueNatural = 0; for (const l of (o.legs||[])) { const sign = l.side === 'short' ? -1 : 1; entered += sign * l.qty * 100 * (l.entryPrice || 0); @@ -341,15 +346,36 @@ const oo = m && m[Number(l.strike) + '@' + l.type]; const mid = oo ? Number(oo.midPrice ?? oo.mid ?? oo.bsPrice ?? 0) : 0; value += sign * l.qty * 100 * mid; + + // Natural mark = realistic close-now price: + // long legs sell at the BID (other side takes them off your hands) + // short legs buy back at the ASK (you pay to take them off) + // If quotes are missing (after-hours / illiquid), fall back to mid. + const bid = oo ? Number(oo.bid ?? 0) : 0; + const ask = oo ? Number(oo.ask ?? 0) : 0; + let natural; + if (l.side === 'long') natural = bid > 0 ? bid : mid; + else natural = ask > 0 ? ask : mid; + valueNatural += sign * l.qty * 100 * natural; } const baseEntered = (o.entry_cost != null) ? Number(o.entry_cost) : entered; const grossPL = value - baseEntered; const commission = SettingsStore.estimate(o.legs || []).roundTrip; const netPL = grossPL - commission; + + // Natural-mark P/L: what you'd actually capture closing right now at + // the worst end of each leg's spread, after commissions. + const grossPLNatural = valueNatural - baseEntered; + const netPLNatural = grossPLNatural - commission; + const denom = Math.max(Math.abs(baseEntered), 1); return { - o, entered: baseEntered, value, grossPL, commission, netPL, + o, entered: baseEntered, value, valueNatural, + grossPL, commission, netPL, + grossPLNatural, netPLNatural, plPct: netPL / denom * 100, + plPctNatural: netPLNatural / denom * 100, + spreadCost: netPL - netPLNatural, legsSummary: (o.legs||[]).map(l => (l.side === 'long' ? '+' : '-') + l.qty + ' ' + l.strike + (l.type === 'call' ? 'C' : 'P') + '·' + (l.expiry||'').slice(5) ).join(' / '), @@ -364,12 +390,20 @@ get openCount() { return this.visible.filter(r => r.o.status === 'open').length; }, get totals() { - let entered = 0, current = 0, comm = 0, gross = 0; + let entered = 0, current = 0, currentNat = 0, comm = 0, gross = 0, grossNat = 0; for (const r of this.visible) { if (r.o.status !== 'open') continue; - entered += r.entered; current += r.value; comm += r.commission; gross += r.grossPL; + entered += r.entered; + current += r.value; currentNat += r.valueNatural; + comm += r.commission; + gross += r.grossPL; grossNat += r.grossPLNatural; } - return { entered, current, commission: comm, grossPL: gross, netPL: gross - comm }; + return { + entered, current, currentNat, + commission: comm, + grossPL: gross, netPL: gross - comm, + grossPLNatural: grossNat, netPLNatural: grossNat - comm, + }; }, get commissionLabel() {