From 92192202704fa9230b34423fcd6be8458457c501 Mon Sep 17 00:00:00 2001 From: ojy Date: Wed, 13 May 2026 04:22:59 +0000 Subject: [PATCH] Add per-leg entry-price lock + market Reload button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Each leg has a ๐Ÿ”“/๐Ÿ”’ toggle next to its entry price; locked = entry stays fixed (your fill), unlocked = entry re-prices to the current mark on Reload - New "Mark" column shows each leg's current market mid (with delta vs entry) - "Refresh spot" button replaced by "Reload": re-fetches spot, plus each leg's current mark and IV from the live chain (per unique expiry), re-pricing unlocked legs and refreshing IVs used by the T+0 curve - reload() no longer resets the days-to-expiry slider on edits Co-Authored-By: Claude Sonnet 4.6 --- frontend/strategy.html | 81 +++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/frontend/strategy.html b/frontend/strategy.html index 13742a3..b43bfe1 100644 --- a/frontend/strategy.html +++ b/frontend/strategy.html @@ -65,8 +65,8 @@
-
@@ -133,7 +133,7 @@ Show SideQtyTypeStrikeExpiry - Entry $IV + Entry $MarkIV Costฮ”ฮ˜/d @@ -150,7 +150,18 @@ - + +
+ + +
+ + + + + @@ -169,6 +180,7 @@ + @@ -296,7 +308,7 @@ this.symbol = st.symbol || ''; this.spot = st.spotSnapshot || 0; this.legs = st.legs || []; - this.dteOffset = 0; + if (this.dteOffset > this.maxDTE) this.dteOffset = this.maxDTE; if (this.legs.length > 0) this.$nextTick(() => this.renderChart()); }, @@ -426,26 +438,53 @@ this.reload(); this.flash('Leg added'); }, - async refreshSpot() { - if (!this.symbol) return; + // Re-fetch spot + each leg's current mark & IV. Unlocked legs also get + // their entry price reset to the current mark; locked legs keep their entry. + async reloadMarket() { + if (!this.symbol || this.legs.length === 0) return; this.refreshing = true; try { - // grab the nearest expiry to read current spot - const er = await fetch('/api/expirations?symbol=' + encodeURIComponent(this.symbol)); - const ee = await er.json(); const ed = ee.data ?? ee; - const exps = ed.expirations ?? (Array.isArray(ed) ? ed : []); - const exp = exps[0]; - if (!exp) throw new Error('no expirations'); - const cr = await fetch('/api/chain?symbol=' + encodeURIComponent(this.symbol) + '&expiry=' + encodeURIComponent(exp)); - const ce = await cr.json(); const snap = ce.data?.snapshots?.[0] ?? {}; - if (snap.spot > 0) { - this.spot = snap.spot; - const st = StrategyStore.load(); st.spotSnapshot = snap.spot; StrategyStore.save(st); - this.renderChart(); - this.flash('Spot updated: $' + snap.spot.toFixed(2)); + const expiries = [...new Set(this.legs.map(l => l.expiry).filter(Boolean))]; + const byExpiry = {}; // expiry -> { strike@type -> option } + let spot = 0; + for (const exp of expiries) { + const r = await fetch('/api/chain?symbol=' + encodeURIComponent(this.symbol) + '&expiry=' + encodeURIComponent(exp)); + if (!r.ok) continue; + const e = await r.json(); + const snap = e.data?.snapshots?.[0]; + if (!snap) continue; + if (snap.spot > 0) spot = snap.spot; + const map = {}; + for (const o of (snap.chain || [])) { + const t = (o.type || o.optionType || '').toLowerCase(); + map[Number(o.strike) + '@' + t] = o; + } + byExpiry[exp] = map; } + const st = StrategyStore.load(); + let updated = 0, relinked = 0; + for (const leg of st.legs) { + const map = byExpiry[leg.expiry]; + if (!map) continue; + const o = map[Number(leg.strike) + '@' + leg.type]; + if (!o) continue; + const mark = o.midPrice ?? o.mid ?? o.bsPrice ?? 0; + leg.currentMark = mark; + if (o.iv > 0) leg.iv = o.iv; + if (!leg.locked && mark > 0) { leg.entryPrice = mark; relinked++; } + updated++; + } + if (spot > 0) st.spotSnapshot = spot; + StrategyStore.save(st); + this.legs = st.legs; + if (spot > 0) this.spot = spot; + if (this.dteOffset > this.maxDTE) this.dteOffset = this.maxDTE; + this.$nextTick(() => this.renderChart()); + this.flash(updated > 0 + ? `Reloaded ${updated} leg${updated===1?'':'s'}${relinked?` (${relinked} re-priced)`:''} ยท spot $${(spot||this.spot).toFixed(2)}` + : 'Reloaded โ€” no matching contracts found in the current chain'); } catch (e) { - this.flash('Refresh failed: ' + e.message); + this.flash('Reload failed: ' + e.message); } finally { this.refreshing = false; }