Add per-leg entry-price lock + market Reload button
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -65,8 +65,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" x-show="legs.length > 0" x-cloak>
|
||||
<button class="btn btn-outline-secondary btn-sm me-1" @click="refreshSpot()" :disabled="refreshing">
|
||||
<span x-show="refreshing" class="spinner-border spinner-border-sm me-1" role="status"></span>Refresh spot
|
||||
<button class="btn btn-outline-primary btn-sm me-1" @click="reloadMarket()" :disabled="refreshing" title="Re-fetch spot, marks & IVs; refresh entry price on unlocked legs">
|
||||
<span x-show="refreshing" class="spinner-border spinner-border-sm me-1" role="status"></span>Reload
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" @click="clearAll()">Clear all</button>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@
|
||||
<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">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>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -150,7 +150,18 @@
|
||||
<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 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) })">
|
||||
<button class="btn btn-sm btn-outline-secondary px-1" @click="updateLeg(lv.id, { locked: !lv.locked })" :title="lv.locked ? 'Entry price locked — click to unlock (reload will update it)' : 'Click to lock entry price (reload won\'t change it)'" x-text="lv.locked ? '🔒' : '🔓'"></button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end mono small">
|
||||
<template x-if="lv.currentMark != null">
|
||||
<span>$<span x-text="lv.currentMark.toFixed(2)"></span><span x-show="Math.abs(lv.currentMark - lv.entryPrice) > 0.005" :class="(lv.currentMark - lv.entryPrice) >= 0 ? 'text-success' : 'text-danger'" x-text="(lv.currentMark - lv.entryPrice >= 0 ? ' +' : ' ') + (lv.currentMark - lv.entryPrice).toFixed(2)"></span></span>
|
||||
</template>
|
||||
<template x-if="lv.currentMark == null"><span class="text-secondary">—</span></template>
|
||||
</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>
|
||||
@@ -169,6 +180,7 @@
|
||||
<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></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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user