Editable expiry per leg — fetches the new chain on demand
The Expiry cell is now a dropdown of available expiries for that leg's symbol (fetched from /api/expirations on page open / symbol switch / Reload). Picking a different expiry pulls that expiry's chain on-demand (cached), finds the same strike (or the closest available) for the leg's type, and updates entry price, IV and mark. Lock clears since it's a new contract. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -173,7 +173,12 @@
|
||||
</select>
|
||||
<span x-show="!hasStrikeOpts(lv)" x-text="lv.strike"></span>
|
||||
</td>
|
||||
<td class="mono small" x-text="lv.expiry"></td>
|
||||
<td class="mono small" style="width:8.5rem">
|
||||
<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">
|
||||
<template x-for="e in expiryOpts(lv)" :key="e"><option :value="e" x-text="e"></option></template>
|
||||
</select>
|
||||
<span x-show="!hasExpiryOpts(lv)" x-text="lv.expiry"></span>
|
||||
</td>
|
||||
<td style="width:8.5rem">
|
||||
<div class="input-group input-group-sm flex-nowrap">
|
||||
<input type="number" min="0" step="0.01" class="form-control form-control-sm text-end" :class="lv.locked ? 'opacity-75' : ''" :value="lv.entryPrice" :disabled="lv.locked" @change="updateLeg(lv.id, { entryPrice: Math.max(0, +$event.target.value||0) })">
|
||||
@@ -322,10 +327,12 @@
|
||||
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
||||
chart: null, _renderTimer: null,
|
||||
_chainCache: {}, // "SYMBOL@EXPIRY" -> { "strike@type": optionRow }
|
||||
_expiryCache: {}, // "SYMBOL" -> ["2026-06-20", ...]
|
||||
|
||||
init() {
|
||||
this.reload();
|
||||
this._chainCache = this._seedChainCache();
|
||||
if (this.symbol) this._ensureExpiries(this.symbol);
|
||||
// pull live spot / marks / IVs (and per-expiry chains) on open
|
||||
if (this.legs.length > 0 && this.symbol) this.reloadMarket(false);
|
||||
// re-sync if another tab changed the basket
|
||||
@@ -374,6 +381,84 @@
|
||||
this.updateLeg(id, patch);
|
||||
},
|
||||
|
||||
// ---- expiry picker -----------------------------------------------
|
||||
hasExpiryOpts(lv) { return Array.isArray(this._expiryCache[lv.symbol]) && this._expiryCache[lv.symbol].length > 0; },
|
||||
expiryOpts(lv) {
|
||||
const list = (this._expiryCache[lv.symbol] || []).slice();
|
||||
if (!list.includes(lv.expiry)) list.push(lv.expiry);
|
||||
return list.sort();
|
||||
},
|
||||
|
||||
async _ensureExpiries(sym) {
|
||||
if (!sym) return;
|
||||
const have = this._expiryCache[sym];
|
||||
if (Array.isArray(have) && have.length > 0) return;
|
||||
try {
|
||||
const r = await fetch('/api/expirations?symbol=' + encodeURIComponent(sym));
|
||||
if (!r.ok) return;
|
||||
const e = await r.json();
|
||||
const data = e.data ?? e;
|
||||
const list = data.expirations || (Array.isArray(data) ? data : []);
|
||||
if (list.length) this._expiryCache = { ...this._expiryCache, [sym]: list };
|
||||
} catch {}
|
||||
},
|
||||
|
||||
async _ensureChain(sym, exp) {
|
||||
const key = sym + '@' + exp;
|
||||
if (this._chainCache[key]) return this._chainCache[key];
|
||||
try {
|
||||
const r = await fetch('/api/chain?symbol=' + encodeURIComponent(sym) + '&expiry=' + encodeURIComponent(exp));
|
||||
if (!r.ok) return null;
|
||||
const e = await r.json();
|
||||
const snap = e.data?.snapshots?.[0];
|
||||
if (!snap) return null;
|
||||
const map = {};
|
||||
for (const o of (snap.chain || [])) {
|
||||
const t = (o.type || o.optionType || '').toLowerCase();
|
||||
map[Number(o.strike) + '@' + t] = o;
|
||||
}
|
||||
this._chainCache = { ...this._chainCache, [key]: map };
|
||||
if (snap.spot > 0) {
|
||||
this.spot = snap.spot;
|
||||
const st = StrategyStore.load(); st.spotSnapshot = snap.spot; StrategyStore.save(st);
|
||||
}
|
||||
return map;
|
||||
} catch { return null; }
|
||||
},
|
||||
|
||||
async changeExpiry(id, newExpiry) {
|
||||
const leg = this.legs.find(l => l.id === id);
|
||||
if (!leg || !newExpiry || newExpiry === leg.expiry) return;
|
||||
this.refreshing = true;
|
||||
try {
|
||||
const map = await this._ensureChain(leg.symbol, newExpiry);
|
||||
if (!map) { this.flash('No chain available for ' + newExpiry); return; }
|
||||
// exact strike if it exists for this type, else closest
|
||||
let opt = map[Number(leg.strike) + '@' + leg.type];
|
||||
let pickedStrike = leg.strike;
|
||||
if (!opt) {
|
||||
let bestD = Infinity;
|
||||
for (const k of Object.keys(map)) {
|
||||
if (!k.endsWith('@' + leg.type)) continue;
|
||||
const s = parseFloat(k);
|
||||
const d = Math.abs(s - leg.strike);
|
||||
if (d < bestD) { bestD = d; opt = map[k]; pickedStrike = s; }
|
||||
}
|
||||
}
|
||||
const patch = { expiry: newExpiry, strike: pickedStrike, locked: false };
|
||||
if (opt) {
|
||||
const mid = Math.round(((opt.midPrice ?? opt.mid ?? opt.bsPrice ?? leg.entryPrice) || 0) * 100) / 100;
|
||||
patch.entryPrice = mid;
|
||||
patch.currentMark = mid;
|
||||
if (opt.iv > 0) patch.iv = opt.iv;
|
||||
}
|
||||
this.updateLeg(id, patch);
|
||||
this.flash('Expiry → ' + newExpiry + (pickedStrike !== leg.strike ? ' (strike ' + leg.strike + ' → ' + pickedStrike + ')' : ''));
|
||||
} finally {
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
|
||||
reload() {
|
||||
const st = StrategyStore.load();
|
||||
this.symbol = st.symbol || '';
|
||||
@@ -389,6 +474,7 @@
|
||||
StrategyStore.setActive(sym);
|
||||
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
||||
this.reload();
|
||||
this._ensureExpiries(sym);
|
||||
if (this.legs.length > 0) this.reloadMarket(false);
|
||||
},
|
||||
|
||||
@@ -525,6 +611,7 @@
|
||||
async reloadMarket(repriceUnlocked = true) {
|
||||
if (!this.symbol || this.legs.length === 0) return;
|
||||
this.refreshing = true;
|
||||
this._ensureExpiries(this.symbol); // fire-and-forget; fills the expiry dropdown
|
||||
try {
|
||||
const expiries = [...new Set(this.legs.map(l => l.expiry).filter(Boolean))];
|
||||
const byExpiry = {}; // expiry -> { strike@type -> option }
|
||||
|
||||
Reference in New Issue
Block a user