Move legs table to bottom; per-leg show/hide in chart
- Legs table now sits below the P/L chart and stats - Each leg row has a leftmost "Show" checkbox; unchecked legs are excluded from the chart, stats, breakevens and net Greeks (and dimmed in the table), so you can isolate or build up a strategy incrementally - All derived values (chart, netCost, maxDTE/minDTE, strategy name, stats) now operate on the active (checked) legs only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,66 +87,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Legs table -->
|
|
||||||
<div class="card mb-3" x-show="legs.length > 0 || showManual" x-cloak>
|
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
|
||||||
<h3 class="card-title mb-0">Legs</h3>
|
|
||||||
<div>
|
|
||||||
<span class="me-3" :class="netCost >= 0 ? 'text-danger' : 'text-success'">
|
|
||||||
Net <strong x-text="netCost >= 0 ? 'debit' : 'credit'"></strong>:
|
|
||||||
<strong class="mono" x-text="fmtMoney(Math.abs(netCost))"></strong>
|
|
||||||
</span>
|
|
||||||
<button class="btn btn-outline-primary btn-sm" @click="showManual = !showManual" x-text="showManual ? 'Hide manual entry' : '+ Add leg manually'"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm leg-table mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Side</th><th>Qty</th><th>Type</th><th class="text-end">Strike</th><th>Expiry</th>
|
|
||||||
<th class="text-end">Entry $</th><th class="text-end">IV</th>
|
|
||||||
<th class="text-end">Cost</th><th class="text-end">Δ</th><th class="text-end">Θ/d</th><th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template x-for="lv in legsView" :key="lv.id">
|
|
||||||
<tr>
|
|
||||||
<td style="width:7rem">
|
|
||||||
<select class="form-select form-select-sm" :value="lv.side" @change="updateLeg(lv.id, { side: $event.target.value })">
|
|
||||||
<option value="long">Long</option><option value="short">Short</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td style="width:5rem"><input type="number" min="1" step="1" class="form-control form-control-sm" :value="lv.qty" @change="updateLeg(lv.id, { qty: Math.max(1, Math.round(+$event.target.value||1)) })"></td>
|
|
||||||
<td><span class="badge" :class="lv.type==='call' ? 'bg-success-lt text-success' : 'bg-danger-lt text-danger'" x-text="lv.type"></span></td>
|
|
||||||
<td class="text-end mono" x-text="lv.strike"></td>
|
|
||||||
<td class="mono small" x-text="lv.expiry"></td>
|
|
||||||
<td style="width:6.5rem"><input type="number" min="0" step="0.01" class="form-control form-control-sm text-end" :value="lv.entryPrice" @change="updateLeg(lv.id, { entryPrice: Math.max(0, +$event.target.value||0) })"></td>
|
|
||||||
<td class="text-end mono small" x-text="lv.iv > 0 ? (lv.iv*100).toFixed(1)+'%' : '—'"></td>
|
|
||||||
<td class="text-end mono" :class="lv.cost >= 0 ? 'text-danger' : 'text-success'" x-text="fmtMoney(lv.cost)"></td>
|
|
||||||
<td class="text-end mono small" :class="lv.delta>=0?'text-success':'text-danger'" x-text="lv.delta.toFixed(1)"></td>
|
|
||||||
<td class="text-end mono small text-danger" x-text="lv.theta.toFixed(1)"></td>
|
|
||||||
<td class="text-end"><button class="btn btn-sm btn-ghost-danger" @click="removeLeg(lv.id)" aria-label="Remove leg">✕</button></td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Manual entry row -->
|
|
||||||
<template x-if="showManual">
|
|
||||||
<tr style="background:#161824;">
|
|
||||||
<td><select class="form-select form-select-sm" x-model="manual.side"><option value="long">Long</option><option value="short">Short</option></select></td>
|
|
||||||
<td><input type="number" min="1" step="1" class="form-control form-control-sm" x-model.number="manual.qty"></td>
|
|
||||||
<td><select class="form-select form-select-sm" x-model="manual.type"><option value="call">call</option><option value="put">put</option></select></td>
|
|
||||||
<td><input type="number" step="0.5" class="form-control form-control-sm text-end" placeholder="strike" x-model.number="manual.strike"></td>
|
|
||||||
<td><input type="date" class="form-control form-control-sm" x-model="manual.expiry"></td>
|
|
||||||
<td><input type="number" min="0" step="0.01" class="form-control form-control-sm text-end" placeholder="entry $" x-model.number="manual.entryPrice"></td>
|
|
||||||
<td><input type="number" min="0" step="0.5" class="form-control form-control-sm text-end" placeholder="IV %" x-model.number="manual.ivPct"></td>
|
|
||||||
<td colspan="4" class="text-end"><button class="btn btn-sm btn-primary" @click="addManualLeg()">Add leg</button></td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- P/L chart -->
|
<!-- P/L chart -->
|
||||||
<div class="chart-card mb-3" x-show="legs.length > 0" x-cloak>
|
<div class="chart-card mb-3" x-show="legs.length > 0" x-cloak>
|
||||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
@@ -175,6 +115,69 @@
|
|||||||
<div class="col-6 col-md-3 col-xl"><div class="stat-box"><div class="lbl">Net Vega / 1%</div><div class="val mono" :class="stats.vega>=0?'pos':'neg'" x-text="fmtMoney(stats.vega)"></div></div></div>
|
<div class="col-6 col-md-3 col-xl"><div class="stat-box"><div class="lbl">Net Vega / 1%</div><div class="val mono" :class="stats.vega>=0?'pos':'neg'" x-text="fmtMoney(stats.vega)"></div></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Legs table (at the bottom) -->
|
||||||
|
<div class="card mb-4" x-show="legs.length > 0 || showManual" x-cloak>
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between">
|
||||||
|
<h3 class="card-title mb-0">Legs <span class="text-secondary small fw-normal">— uncheck a leg to drop it from the chart</span></h3>
|
||||||
|
<div>
|
||||||
|
<span class="me-3" :class="netCost >= 0 ? 'text-danger' : 'text-success'">
|
||||||
|
Net <strong x-text="netCost >= 0 ? 'debit' : 'credit'"></strong>:
|
||||||
|
<strong class="mono" x-text="fmtMoney(Math.abs(netCost))"></strong>
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-outline-primary btn-sm" @click="showManual = !showManual" x-text="showManual ? 'Hide manual entry' : '+ Add leg manually'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm leg-table mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" style="width:3rem">Show</th>
|
||||||
|
<th>Side</th><th>Qty</th><th>Type</th><th class="text-end">Strike</th><th>Expiry</th>
|
||||||
|
<th class="text-end">Entry $</th><th class="text-end">IV</th>
|
||||||
|
<th class="text-end">Cost</th><th class="text-end">Δ</th><th class="text-end">Θ/d</th><th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="lv in legsView" :key="lv.id">
|
||||||
|
<tr :style="lv.enabled === false ? 'opacity:.4' : ''">
|
||||||
|
<td class="text-center"><input type="checkbox" class="form-check-input m-0" :checked="lv.enabled !== false" @change="toggleLeg(lv.id)" :aria-label="'Show leg '+lv.strike+lv.type"></td>
|
||||||
|
<td style="width:7rem">
|
||||||
|
<select class="form-select form-select-sm" :value="lv.side" @change="updateLeg(lv.id, { side: $event.target.value })">
|
||||||
|
<option value="long">Long</option><option value="short">Short</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td style="width:5rem"><input type="number" min="1" step="1" class="form-control form-control-sm" :value="lv.qty" @change="updateLeg(lv.id, { qty: Math.max(1, Math.round(+$event.target.value||1)) })"></td>
|
||||||
|
<td><span class="badge" :class="lv.type==='call' ? 'bg-success-lt text-success' : 'bg-danger-lt text-danger'" x-text="lv.type"></span></td>
|
||||||
|
<td class="text-end mono" x-text="lv.strike"></td>
|
||||||
|
<td class="mono small" x-text="lv.expiry"></td>
|
||||||
|
<td style="width:6.5rem"><input type="number" min="0" step="0.01" class="form-control form-control-sm text-end" :value="lv.entryPrice" @change="updateLeg(lv.id, { entryPrice: Math.max(0, +$event.target.value||0) })"></td>
|
||||||
|
<td class="text-end mono small" x-text="lv.iv > 0 ? (lv.iv*100).toFixed(1)+'%' : '—'"></td>
|
||||||
|
<td class="text-end mono" :class="lv.cost >= 0 ? 'text-danger' : 'text-success'" x-text="fmtMoney(lv.cost)"></td>
|
||||||
|
<td class="text-end mono small" :class="lv.delta>=0?'text-success':'text-danger'" x-text="lv.delta.toFixed(1)"></td>
|
||||||
|
<td class="text-end mono small text-danger" x-text="lv.theta.toFixed(1)"></td>
|
||||||
|
<td class="text-end"><button class="btn btn-sm btn-ghost-danger" @click="removeLeg(lv.id)" aria-label="Remove leg">✕</button></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Manual entry row -->
|
||||||
|
<template x-if="showManual">
|
||||||
|
<tr style="background:#161824;">
|
||||||
|
<td></td>
|
||||||
|
<td><select class="form-select form-select-sm" x-model="manual.side"><option value="long">Long</option><option value="short">Short</option></select></td>
|
||||||
|
<td><input type="number" min="1" step="1" class="form-control form-control-sm" x-model.number="manual.qty"></td>
|
||||||
|
<td><select class="form-select form-select-sm" x-model="manual.type"><option value="call">call</option><option value="put">put</option></select></td>
|
||||||
|
<td><input type="number" step="0.5" class="form-control form-control-sm text-end" placeholder="strike" x-model.number="manual.strike"></td>
|
||||||
|
<td><input type="date" class="form-control form-control-sm" x-model="manual.expiry"></td>
|
||||||
|
<td><input type="number" min="0" step="0.01" class="form-control form-control-sm text-end" placeholder="entry $" x-model.number="manual.entryPrice"></td>
|
||||||
|
<td><input type="number" min="0" step="0.5" class="form-control form-control-sm text-end" placeholder="IV %" x-model.number="manual.ivPct"></td>
|
||||||
|
<td colspan="4" class="text-end"><button class="btn btn-sm btn-primary" @click="addManualLeg()">Add leg</button></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -298,12 +301,14 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── derived ───────────────────────────────────────────
|
// ── derived ───────────────────────────────────────────
|
||||||
get netCost() { return this.legs.reduce((s,l)=> s + legCost(l), 0); },
|
// only legs with the "Show" checkbox ticked drive the chart / stats / Greeks
|
||||||
get strategyName() { return detectStrategy(this.legs); },
|
get activeLegs() { return this.legs.filter(l => l.enabled !== false); },
|
||||||
get maxDTE() { return Math.max(1, Math.ceil(Math.max(0, ...this.legs.map(legDTE)))); },
|
get netCost() { return this.activeLegs.reduce((s,l)=> s + legCost(l), 0); },
|
||||||
|
get strategyName() { return detectStrategy(this.activeLegs); },
|
||||||
|
get maxDTE() { const a = this.activeLegs; return a.length ? Math.max(1, Math.ceil(Math.max(0, ...a.map(legDTE)))) : 1; },
|
||||||
// exact (not floored) days to the earliest expiry — so the "expiration"
|
// exact (not floored) days to the earliest expiry — so the "expiration"
|
||||||
// curve uses true intrinsic value (sharp hockey stick), not a near-expiry BS approx
|
// curve uses true intrinsic value (sharp hockey stick), not a near-expiry BS approx
|
||||||
get minDTE() { return Math.max(0, Math.min(...this.legs.map(legDTE))); },
|
get minDTE() { const a = this.activeLegs; return a.length ? Math.max(0, Math.min(...a.map(legDTE))) : 0; },
|
||||||
get dteLabel() {
|
get dteLabel() {
|
||||||
const d = new Date(Date.now() + this.dteOffset * DAY_MS);
|
const d = new Date(Date.now() + this.dteOffset * DAY_MS);
|
||||||
const ds = d.toISOString().slice(0,10);
|
const ds = d.toISOString().slice(0,10);
|
||||||
@@ -331,7 +336,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
get stats() {
|
get stats() {
|
||||||
const legs = this.legs, net = this.netCost, spot = this.spot;
|
const legs = this.activeLegs, net = this.netCost, spot = this.spot;
|
||||||
|
if (legs.length === 0) return { maxProfit:'—', maxLoss:'—', breakevens:'—', delta:0, gamma:0, theta:0, vega:0 };
|
||||||
// dense expiration-curve sample for breakevens / extremes / unbounded
|
// dense expiration-curve sample for breakevens / extremes / unbounded
|
||||||
const lo0 = 0.01;
|
const lo0 = 0.01;
|
||||||
const hi0 = Math.max(spot * 2, ...legs.map(l=>l.strike)) * 1.5 + 10;
|
const hi0 = Math.max(spot * 2, ...legs.map(l=>l.strike)) * 1.5 + 10;
|
||||||
@@ -386,6 +392,16 @@
|
|||||||
StrategyStore.updateLeg(id, patch);
|
StrategyStore.updateLeg(id, patch);
|
||||||
this.reload();
|
this.reload();
|
||||||
},
|
},
|
||||||
|
toggleLeg(id) {
|
||||||
|
const leg = this.legs.find(l => l.id === id);
|
||||||
|
const cur = leg ? leg.enabled !== false : true;
|
||||||
|
StrategyStore.updateLeg(id, { enabled: !cur });
|
||||||
|
// refresh legs without resetting the date slider
|
||||||
|
const st = StrategyStore.load();
|
||||||
|
this.legs = st.legs || [];
|
||||||
|
if (this.dteOffset > this.maxDTE) this.dteOffset = this.maxDTE;
|
||||||
|
this.$nextTick(() => this.renderChart());
|
||||||
|
},
|
||||||
removeLeg(id) {
|
removeLeg(id) {
|
||||||
StrategyStore.removeLeg(id);
|
StrategyStore.removeLeg(id);
|
||||||
this.reload();
|
this.reload();
|
||||||
@@ -442,8 +458,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderChart() {
|
renderChart() {
|
||||||
if (this.legs.length === 0) return;
|
const legs = this.activeLegs, net = this.netCost;
|
||||||
const legs = this.legs, net = this.netCost;
|
if (legs.length === 0) {
|
||||||
|
if (this.chart) this.chart.updateSeries([{ name: 'P/L', data: [] }, { name: 'P/L', data: [] }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const spot = this.spot > 0 ? this.spot : (legs.reduce((s,l)=>s+l.strike,0)/legs.length);
|
const spot = this.spot > 0 ? this.spot : (legs.reduce((s,l)=>s+l.strike,0)/legs.length);
|
||||||
const strikes = legs.map(l=>l.strike);
|
const strikes = legs.map(l=>l.strike);
|
||||||
const minK = Math.min(...strikes), maxK = Math.max(...strikes);
|
const minK = Math.min(...strikes), maxK = Math.max(...strikes);
|
||||||
|
|||||||
Reference in New Issue
Block a user