Positions: add natural-mark P/L column + spread-cost tracker
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 <noreply@anthropic.com>
This commit is contained in:
@@ -195,10 +195,11 @@
|
||||
|
||||
<!-- Summary cards -->
|
||||
<div class="row g-2 mb-3" x-show="visible.length > 0" x-cloak>
|
||||
<div class="col-6 col-md-3"><div class="summary-card"><div class="lbl">Open positions</div><div class="val" x-text="openCount"></div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="summary-card"><div class="lbl">Total entered</div><div class="val mono" x-text="fmtMoney(totals.entered)"></div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="summary-card"><div class="lbl">Current value</div><div class="val mono" x-text="fmtMoney(totals.current)"></div></div></div>
|
||||
<div class="col-6 col-md-3"><div class="summary-card"><div class="lbl">Net P/L (after commissions)</div><div class="val mono" :class="totals.netPL >= 0 ? 'pos' : 'neg'" x-text="fmtMoney(totals.netPL)"></div></div></div>
|
||||
<div class="col-6 col-md"><div class="summary-card"><div class="lbl">Open positions</div><div class="val" x-text="openCount"></div></div></div>
|
||||
<div class="col-6 col-md"><div class="summary-card"><div class="lbl">Total entered</div><div class="val mono" x-text="fmtMoney(totals.entered)"></div></div></div>
|
||||
<div class="col-6 col-md"><div class="summary-card" title="Net P/L using bid/ask MID for each leg — what your broker shows. Optimistic; doesn't account for the bid-ask spread you'd pay to actually close."><div class="lbl">Net P/L (mid mark)</div><div class="val mono" :class="totals.netPL >= 0 ? 'pos' : 'neg'" x-text="fmtMoney(totals.netPL)"></div></div></div>
|
||||
<div class="col-6 col-md"><div class="summary-card" title="Net P/L if you closed RIGHT NOW: longs sell at the BID, shorts buy back at the ASK — the realistic close-now value. This is what you'd actually pocket today."><div class="lbl">Net P/L (close now)</div><div class="val mono" :class="totals.netPLNatural >= 0 ? 'pos' : 'neg'" x-text="fmtMoney(totals.netPLNatural)"></div></div></div>
|
||||
<div class="col-6 col-md"><div class="summary-card" title="The bid-ask spread cost across all positions: how much you'd give up to actually close at market vs the optimistic mid-mark."><div class="lbl">Spread cost</div><div class="val mono text-warning" x-text="'-' + fmtMoney(Math.max(0, totals.netPL - totals.netPLNatural))"></div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
@@ -227,10 +228,12 @@
|
||||
<tr>
|
||||
<th>Sym</th><th>Strategy</th><th>Legs</th>
|
||||
<th class="text-end">Entered $</th>
|
||||
<th class="text-end">Current $</th>
|
||||
<th class="text-end" title="Mark using bid/ask MID per leg — broker-style. Optimistic.">Current $ (mid)</th>
|
||||
<th class="text-end" title="Realistic close-now value: longs at BID, shorts at ASK.">Current $ (nat)</th>
|
||||
<th class="text-end">Comm. (RT)</th>
|
||||
<th class="text-end">Gross P/L</th>
|
||||
<th class="text-end">Net P/L</th>
|
||||
<th class="text-end" title="Net P/L using mid mark — what your broker shows.">Net P/L (mid)</th>
|
||||
<th class="text-end" title="Net P/L if you closed right now at realistic fills: longs sell at BID, shorts buy back at ASK. This is what you'd actually pocket.">Net P/L (close now)</th>
|
||||
<th class="text-end" title="Bid-ask spread cost: difference between optimistic mid P/L and realistic close-now P/L.">Spread $</th>
|
||||
<th class="text-end">P/L %</th>
|
||||
<th>Opened</th>
|
||||
<th>Status</th>
|
||||
@@ -245,10 +248,12 @@
|
||||
<td class="mono small text-secondary" x-text="row.legsSummary"></td>
|
||||
<td class="text-end mono" x-text="fmtMoney(row.entered)"></td>
|
||||
<td class="text-end mono" x-text="fmtMoney(row.value)"></td>
|
||||
<td class="text-end mono text-secondary" x-text="fmtMoney(row.valueNatural)"></td>
|
||||
<td class="text-end mono text-warning" x-text="'-' + fmtMoney(row.commission)"></td>
|
||||
<td class="text-end mono" :class="row.grossPL >= 0 ? 'text-success' : 'text-danger'" x-text="fmtMoney(row.grossPL)"></td>
|
||||
<td class="text-end mono fw-bold" :class="row.netPL >= 0 ? 'text-success' : 'text-danger'" x-text="fmtMoney(row.netPL)"></td>
|
||||
<td class="text-end mono small" :class="row.netPL >= 0 ? 'text-success' : 'text-danger'" x-text="row.plPct.toFixed(1) + '%'"></td>
|
||||
<td class="text-end mono" :class="row.netPL >= 0 ? 'text-success' : 'text-danger'" x-text="fmtMoney(row.netPL)"></td>
|
||||
<td class="text-end mono fw-bold" :class="row.netPLNatural >= 0 ? 'text-success' : 'text-danger'" x-text="fmtMoney(row.netPLNatural)"></td>
|
||||
<td class="text-end mono small text-warning" x-text="'-' + fmtMoney(Math.max(0, row.netPL - row.netPLNatural))"></td>
|
||||
<td class="text-end mono small" :class="row.netPLNatural >= 0 ? 'text-success' : 'text-danger'" x-text="row.plPctNatural.toFixed(1) + '%'"></td>
|
||||
<td class="small text-secondary mono" x-text="(row.o.created_at||'').slice(0,10)"></td>
|
||||
<td><span class="badge" :class="row.o.status === 'open' ? 'bg-success-lt text-success' : 'bg-secondary-lt text-secondary'" x-text="row.o.status"></span></td>
|
||||
<td><button class="btn btn-sm btn-outline-primary" @click="openInStrategy(row.o)" title="Open this position in the Strategy P/L chart">P/L Chart</button></td>
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user