Add strategy P/L analyzer + view-state persistence
- New strategy.html: thinkorswim-style P/L diagram (expiration + T+N curves via Black-Scholes, days-to-expiry slider, net debit/credit, max profit/loss with unbounded detection, breakevens, net Greeks, auto-detected strategy name) - chain.html: per-row Buy/Sell buttons add legs to a localStorage basket; basket badge in toolbar; auto-scroll to ATM row on load - Persist per-page view state (symbol, expiry, loaded data, charts) across navigation via viewstate-store.js for chain/surface/tracker/dashboard - New assets: blackscholes.js (frontend BS port), strategy-store.js, viewstate-store.js - Strategy P/L nav link added to all sidebars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,18 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="strategy.html">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 19l4 -6l4 2l4 -8l4 5" /><path d="M4 4v16h16" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">Strategy P/L</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="tracker.html">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
@@ -214,6 +226,7 @@
|
||||
id="select-type"
|
||||
class="form-select"
|
||||
x-model="optionType"
|
||||
@change="_persist()"
|
||||
aria-label="Option type filter"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
@@ -241,8 +254,15 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Strategy basket -->
|
||||
<div class="col-auto ms-auto">
|
||||
<a href="strategy.html" class="btn btn-sm" :class="basketCount > 0 ? 'btn-purple' : 'btn-outline-secondary'" aria-label="Open strategy P/L">
|
||||
🧺 Strategy<span x-show="basketCount > 0" class="badge bg-white text-dark ms-2" x-text="basketCount"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Spot price badge -->
|
||||
<div class="col-auto ms-auto" x-show="spot > 0">
|
||||
<div class="col-auto" x-show="spot > 0">
|
||||
<span class="badge bg-blue-lt spot-badge fs-6 px-3 py-2">
|
||||
<span class="text-muted me-1">Spot</span>
|
||||
<strong x-text="'$' + spot.toFixed(2)"></strong>
|
||||
@@ -296,6 +316,7 @@
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width:4.5rem">Add</th>
|
||||
<th scope="col">Strike</th>
|
||||
<th scope="col">Bid</th>
|
||||
<th scope="col">Ask</th>
|
||||
@@ -313,6 +334,10 @@
|
||||
<tbody>
|
||||
<template x-for="row in calls" :key="row.strike">
|
||||
<tr :class="rowClass(row, 'call')">
|
||||
<td class="text-nowrap">
|
||||
<button class="btn btn-success btn-icon btn-sm py-0 px-1" style="--tblr-btn-size:1.1rem" @click="addToStrategy(row, 'call', 'long')" :title="'Buy 1 ' + row.strike + 'C'">B</button>
|
||||
<button class="btn btn-danger btn-icon btn-sm py-0 px-1 ms-1" style="--tblr-btn-size:1.1rem" @click="addToStrategy(row, 'call', 'short')" :title="'Sell 1 ' + row.strike + 'C'">S</button>
|
||||
</td>
|
||||
<td class="fw-semibold" x-text="row.strike"></td>
|
||||
<td x-text="fmt2(row.bid)"></td>
|
||||
<td x-text="fmt2(row.ask)"></td>
|
||||
@@ -365,6 +390,7 @@
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width:4.5rem">Add</th>
|
||||
<th scope="col">Strike</th>
|
||||
<th scope="col">Bid</th>
|
||||
<th scope="col">Ask</th>
|
||||
@@ -382,6 +408,10 @@
|
||||
<tbody>
|
||||
<template x-for="row in puts" :key="row.strike">
|
||||
<tr :class="rowClass(row, 'put')">
|
||||
<td class="text-nowrap">
|
||||
<button class="btn btn-success btn-icon btn-sm py-0 px-1" style="--tblr-btn-size:1.1rem" @click="addToStrategy(row, 'put', 'long')" :title="'Buy 1 ' + row.strike + 'P'">B</button>
|
||||
<button class="btn btn-danger btn-icon btn-sm py-0 px-1 ms-1" style="--tblr-btn-size:1.1rem" @click="addToStrategy(row, 'put', 'short')" :title="'Sell 1 ' + row.strike + 'P'">S</button>
|
||||
</td>
|
||||
<td class="fw-semibold" x-text="row.strike"></td>
|
||||
<td x-text="fmt2(row.bid)"></td>
|
||||
<td x-text="fmt2(row.ask)"></td>
|
||||
@@ -420,9 +450,15 @@
|
||||
</div>
|
||||
</footer>
|
||||
</div><!-- /page-wrapper -->
|
||||
<!-- Toast -->
|
||||
<div x-show="toast" x-transition x-cloak style="position:fixed;bottom:1rem;right:1rem;z-index:1080;">
|
||||
<div class="alert alert-success py-2 px-3 mb-0" x-text="toast"></div>
|
||||
</div>
|
||||
</div><!-- /wrapper -->
|
||||
|
||||
<script src="/assets/tabler.min.js" defer></script>
|
||||
<script src="/assets/strategy-store.js"></script>
|
||||
<script src="/assets/viewstate-store.js"></script>
|
||||
<script src="/assets/alpine.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
@@ -439,12 +475,64 @@
|
||||
loading: false,
|
||||
lookingUp: false,
|
||||
error: '',
|
||||
basketCount: 0,
|
||||
toast: '',
|
||||
|
||||
// ── lifecycle ──────────────────────────────────────────
|
||||
async init() {
|
||||
// no auto-load — user must click Lookup first
|
||||
// restore last loaded chain (survives navigating away & back)
|
||||
const vs = ViewState.load('chain');
|
||||
if (vs) {
|
||||
this.symbol = vs.symbol ?? this.symbol;
|
||||
this.expirations = vs.expirations ?? [];
|
||||
this.expiry = vs.expiry ?? '';
|
||||
this.optionType = vs.optionType ?? 'all';
|
||||
this.calls = vs.calls ?? [];
|
||||
this.puts = vs.puts ?? [];
|
||||
this.spot = vs.spot ?? 0;
|
||||
if (this.calls.length || this.puts.length) this.$nextTick(() => this._scrollToATM());
|
||||
}
|
||||
this.basketCount = StrategyStore.count();
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key === StrategyStore.KEY) this.basketCount = StrategyStore.count();
|
||||
});
|
||||
},
|
||||
|
||||
_persist() {
|
||||
ViewState.save('chain', {
|
||||
symbol: this.symbol, expirations: this.expirations, expiry: this.expiry,
|
||||
optionType: this.optionType, calls: this.calls, puts: this.puts, spot: this.spot,
|
||||
});
|
||||
},
|
||||
|
||||
_scrollToATM() {
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('.chain-scroll .row-atm').forEach((el) =>
|
||||
el.scrollIntoView({ block: 'center' }));
|
||||
}, 60);
|
||||
},
|
||||
|
||||
// ── strategy basket ────────────────────────────────────
|
||||
addToStrategy(row, type, side) {
|
||||
const sym = (this.symbol || 'SPY').toUpperCase().trim();
|
||||
const other = StrategyStore.mismatch(sym);
|
||||
if (other && !confirm(`Strategy basket has ${other} legs. Clear it and start a ${sym} strategy?`)) return;
|
||||
const entry = row.midPrice || row.bsPrice || row.ask || row.bid || 0;
|
||||
StrategyStore.addLeg({
|
||||
symbol: sym,
|
||||
expiry: row.expiry || this.expiry,
|
||||
type,
|
||||
strike: row.strike,
|
||||
side, qty: 1,
|
||||
entryPrice: entry,
|
||||
iv: row.iv || 0,
|
||||
spotSnapshot: this.spot || 0,
|
||||
});
|
||||
this.basketCount = StrategyStore.count();
|
||||
this.flash(`${side === 'long' ? 'Bought' : 'Sold'} 1 ${sym} ${row.strike}${type === 'call' ? 'C' : 'P'} @ $${entry.toFixed(2)}`);
|
||||
},
|
||||
flash(msg) { this.toast = msg; clearTimeout(this._t); this._t = setTimeout(() => { this.toast = ''; }, 2500); },
|
||||
|
||||
// ── data fetching ──────────────────────────────────────
|
||||
async fetchExpirations() {
|
||||
if (!this.symbol) return;
|
||||
@@ -462,6 +550,7 @@
|
||||
const data = env.data ?? env;
|
||||
this.expirations = data.expirations ?? (Array.isArray(data) ? data : []);
|
||||
if (this.expirations.length > 0) this.expiry = this.expirations[0];
|
||||
this._persist();
|
||||
} catch (err) {
|
||||
this.error = 'Failed to look up symbol: ' + err.message;
|
||||
} finally {
|
||||
@@ -496,6 +585,8 @@
|
||||
this.puts = this._sortByStrike(
|
||||
chain.filter(r => (r.optionType || r.type || '').toLowerCase() === 'put')
|
||||
);
|
||||
this._persist();
|
||||
this.$nextTick(() => this._scrollToATM());
|
||||
} else {
|
||||
throw new Error('Unexpected API response shape.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user