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>
|
</div>
|
||||||
<div class="col-auto" x-show="legs.length > 0" x-cloak>
|
<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">
|
<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>Refresh spot
|
<span x-show="refreshing" class="spinner-border spinner-border-sm me-1" role="status"></span>Reload
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger btn-sm" @click="clearAll()">Clear all</button>
|
<button class="btn btn-outline-danger btn-sm" @click="clearAll()">Clear all</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<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>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>
|
<th class="text-end">Cost</th><th class="text-end">Δ</th><th class="text-end">Θ/d</th><th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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><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="text-end mono" x-text="lv.strike"></td>
|
||||||
<td class="mono small" x-text="lv.expiry"></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 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" :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>
|
<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="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="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><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><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>
|
<td colspan="4" class="text-end"><button class="btn btn-sm btn-primary" @click="addManualLeg()">Add leg</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -296,7 +308,7 @@
|
|||||||
this.symbol = st.symbol || '';
|
this.symbol = st.symbol || '';
|
||||||
this.spot = st.spotSnapshot || 0;
|
this.spot = st.spotSnapshot || 0;
|
||||||
this.legs = st.legs || [];
|
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());
|
if (this.legs.length > 0) this.$nextTick(() => this.renderChart());
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -426,26 +438,53 @@
|
|||||||
this.reload();
|
this.reload();
|
||||||
this.flash('Leg added');
|
this.flash('Leg added');
|
||||||
},
|
},
|
||||||
async refreshSpot() {
|
// Re-fetch spot + each leg's current mark & IV. Unlocked legs also get
|
||||||
if (!this.symbol) return;
|
// 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;
|
this.refreshing = true;
|
||||||
try {
|
try {
|
||||||
// grab the nearest expiry to read current spot
|
const expiries = [...new Set(this.legs.map(l => l.expiry).filter(Boolean))];
|
||||||
const er = await fetch('/api/expirations?symbol=' + encodeURIComponent(this.symbol));
|
const byExpiry = {}; // expiry -> { strike@type -> option }
|
||||||
const ee = await er.json(); const ed = ee.data ?? ee;
|
let spot = 0;
|
||||||
const exps = ed.expirations ?? (Array.isArray(ed) ? ed : []);
|
for (const exp of expiries) {
|
||||||
const exp = exps[0];
|
const r = await fetch('/api/chain?symbol=' + encodeURIComponent(this.symbol) + '&expiry=' + encodeURIComponent(exp));
|
||||||
if (!exp) throw new Error('no expirations');
|
if (!r.ok) continue;
|
||||||
const cr = await fetch('/api/chain?symbol=' + encodeURIComponent(this.symbol) + '&expiry=' + encodeURIComponent(exp));
|
const e = await r.json();
|
||||||
const ce = await cr.json(); const snap = ce.data?.snapshots?.[0] ?? {};
|
const snap = e.data?.snapshots?.[0];
|
||||||
if (snap.spot > 0) {
|
if (!snap) continue;
|
||||||
this.spot = snap.spot;
|
if (snap.spot > 0) spot = snap.spot;
|
||||||
const st = StrategyStore.load(); st.spotSnapshot = snap.spot; StrategyStore.save(st);
|
const map = {};
|
||||||
this.renderChart();
|
for (const o of (snap.chain || [])) {
|
||||||
this.flash('Spot updated: $' + snap.spot.toFixed(2));
|
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) {
|
} catch (e) {
|
||||||
this.flash('Refresh failed: ' + e.message);
|
this.flash('Reload failed: ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user