Master expiry control — link all legs to one date
The Expiry column header now has a master expiry selector with a 🔓/🔒 toggle. Locking it snaps every leg to the most common expiry, then any change to the master propagates to all legs (each re-priced from its new expiry's chain). Per-leg expiry selects are disabled while locked. Lock resets when switching symbols or clearing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -153,7 +153,19 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center" style="width:3rem">Show</th>
|
<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>Side</th><th>Qty</th><th>Type</th><th class="text-end">Strike</th>
|
||||||
|
<th style="min-width:13rem">
|
||||||
|
<div class="d-flex align-items-center gap-1 flex-nowrap" style="text-transform:none; letter-spacing:0;">
|
||||||
|
<span style="text-transform:uppercase; letter-spacing:.05em; font-size:.7rem;">Expiry</span>
|
||||||
|
<select class="form-select form-select-sm ms-1" :disabled="!expiryLocked" :value="masterExpiry" @change="onMasterExpiry($event.target.value)"
|
||||||
|
:title="expiryLocked ? 'All legs share this expiry — pick to change them all' : 'Click 🔒 to link all legs to one expiry'">
|
||||||
|
<template x-for="e in masterExpiryOpts" :key="e"><option :value="e" x-text="e"></option></template>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary px-1" @click="toggleExpiryLock()"
|
||||||
|
:title="expiryLocked ? 'Unlock — each leg can have its own expiry' : 'Lock all legs to one expiry'"
|
||||||
|
x-text="expiryLocked ? '🔒' : '🔓'"></button>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
<th class="text-end">Entry $</th><th class="text-end">Mark</th><th class="text-end">IV</th>
|
<th class="text-end">Entry $</th><th class="text-end">Mark</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>
|
<th class="text-end">Cost</th><th class="text-end">Δ</th><th class="text-end">Θ/d</th><th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -177,7 +189,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="mono small" style="width:11rem">
|
<td class="mono small" style="width:11rem">
|
||||||
<div class="d-flex align-items-center gap-1 flex-nowrap">
|
<div class="d-flex align-items-center gap-1 flex-nowrap">
|
||||||
<select x-show="hasExpiryOpts(lv)" class="form-select form-select-sm" :value="lv.expiry" @change="changeExpiry(lv.id, $event.target.value)" title="Change expiry — strike, entry, IV & mark update from the new chain">
|
<select x-show="hasExpiryOpts(lv)" class="form-select form-select-sm" :value="lv.expiry" :disabled="expiryLocked" @change="changeExpiry(lv.id, $event.target.value)" :title="expiryLocked ? 'Use the master Expiry at the top to change all legs together' : 'Change expiry — strike, entry, IV & mark update from the new chain'">
|
||||||
<template x-for="e in expiryOpts(lv)" :key="e"><option :value="e" x-text="e"></option></template>
|
<template x-for="e in expiryOpts(lv)" :key="e"><option :value="e" x-text="e"></option></template>
|
||||||
</select>
|
</select>
|
||||||
<span x-show="!hasExpiryOpts(lv)" x-text="lv.expiry"></span>
|
<span x-show="!hasExpiryOpts(lv)" x-text="lv.expiry"></span>
|
||||||
@@ -328,6 +340,7 @@
|
|||||||
symbol: '', symbols: [], spot: 0, legs: [],
|
symbol: '', symbols: [], spot: 0, legs: [],
|
||||||
dteOffset: 0,
|
dteOffset: 0,
|
||||||
xZoom: 1, xPan: 0, lastHalfPct: 0,
|
xZoom: 1, xPan: 0, lastHalfPct: 0,
|
||||||
|
expiryLocked: false, masterExpiry: '',
|
||||||
refreshing: false, showManual: false, toast: '',
|
refreshing: false, showManual: false, toast: '',
|
||||||
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
||||||
chart: null, _renderTimer: null,
|
chart: null, _renderTimer: null,
|
||||||
@@ -431,6 +444,37 @@
|
|||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ---- master expiry (lock-all-legs-to-one-date) -------------------
|
||||||
|
get masterExpiryOpts() {
|
||||||
|
const set = new Set();
|
||||||
|
for (const l of this.legs) if (l.expiry) set.add(l.expiry);
|
||||||
|
const cache = this._expiryCache[this.symbol] || [];
|
||||||
|
for (const e of cache) set.add(e);
|
||||||
|
return [...set].sort();
|
||||||
|
},
|
||||||
|
toggleExpiryLock() {
|
||||||
|
if (this.expiryLocked) { this.expiryLocked = false; return; }
|
||||||
|
// turning on — pick the most common leg expiry as the master, then snap
|
||||||
|
const counts = {};
|
||||||
|
for (const l of this.legs) counts[l.expiry] = (counts[l.expiry] || 0) + 1;
|
||||||
|
let best = '', bestN = -1;
|
||||||
|
for (const e of Object.keys(counts)) if (counts[e] > bestN) { best = e; bestN = counts[e]; }
|
||||||
|
this.masterExpiry = best || (this.legs[0] && this.legs[0].expiry) || '';
|
||||||
|
this.expiryLocked = true;
|
||||||
|
if (this.masterExpiry) this.changeAllExpiry(this.masterExpiry);
|
||||||
|
},
|
||||||
|
async onMasterExpiry(newExp) {
|
||||||
|
if (!this.expiryLocked || !newExp) return;
|
||||||
|
this.masterExpiry = newExp;
|
||||||
|
await this.changeAllExpiry(newExp);
|
||||||
|
},
|
||||||
|
async changeAllExpiry(newExp) {
|
||||||
|
const ids = this.legs.filter(l => l.expiry !== newExp).map(l => l.id);
|
||||||
|
for (const id of ids) {
|
||||||
|
await this.changeExpiry(id, newExp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async changeExpiry(id, newExpiry) {
|
async changeExpiry(id, newExpiry) {
|
||||||
const leg = this.legs.find(l => l.id === id);
|
const leg = this.legs.find(l => l.id === id);
|
||||||
if (!leg || !newExpiry || newExpiry === leg.expiry) return;
|
if (!leg || !newExpiry || newExpiry === leg.expiry) return;
|
||||||
@@ -470,6 +514,9 @@
|
|||||||
this.symbols = st.symbols || [];
|
this.symbols = st.symbols || [];
|
||||||
this.spot = st.spotSnapshot || 0;
|
this.spot = st.spotSnapshot || 0;
|
||||||
this.legs = st.legs || [];
|
this.legs = st.legs || [];
|
||||||
|
if (this.legs.length && (!this.masterExpiry || !this.legs.some(l => l.expiry === this.masterExpiry))) {
|
||||||
|
this.masterExpiry = this.legs[0].expiry || '';
|
||||||
|
}
|
||||||
if (this.dteOffset > this.maxDTE) this.dteOffset = this.maxDTE;
|
if (this.dteOffset > this.maxDTE) this.dteOffset = this.maxDTE;
|
||||||
if (this.legs.length > 0) this.$nextTick(() => this.renderChart()); else if (this.chart) this.chart.updateSeries([{name:'P/L',data:[]},{name:'P/L',data:[]}]);
|
if (this.legs.length > 0) this.$nextTick(() => this.renderChart()); else if (this.chart) this.chart.updateSeries([{name:'P/L',data:[]},{name:'P/L',data:[]}]);
|
||||||
},
|
},
|
||||||
@@ -478,6 +525,7 @@
|
|||||||
if (!sym || sym === this.symbol) return;
|
if (!sym || sym === this.symbol) return;
|
||||||
StrategyStore.setActive(sym);
|
StrategyStore.setActive(sym);
|
||||||
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
||||||
|
this.expiryLocked = false; this.masterExpiry = '';
|
||||||
this.reload();
|
this.reload();
|
||||||
this._ensureExpiries(sym);
|
this._ensureExpiries(sym);
|
||||||
if (this.legs.length > 0) this.reloadMarket(false);
|
if (this.legs.length > 0) this.reloadMarket(false);
|
||||||
@@ -617,6 +665,7 @@
|
|||||||
if (!confirm('Clear all ' + (this.symbol || '') + ' legs?')) return;
|
if (!confirm('Clear all ' + (this.symbol || '') + ' legs?')) return;
|
||||||
StrategyStore.clear();
|
StrategyStore.clear();
|
||||||
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
||||||
|
this.expiryLocked = false; this.masterExpiry = '';
|
||||||
if (this.chart) { this.chart.destroy(); this.chart = null; }
|
if (this.chart) { this.chart.destroy(); this.chart = null; }
|
||||||
this.reload(); // re-renders the chart if another symbol's basket is now active
|
this.reload(); // re-renders the chart if another symbol's basket is now active
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user