diff --git a/frontend/strategy.html b/frontend/strategy.html
index 5b49076..46f5ada 100644
--- a/frontend/strategy.html
+++ b/frontend/strategy.html
@@ -173,7 +173,12 @@
-
|
+
+
+
+ |
@@ -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 }
|