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:
ojy
2026-05-13 04:01:57 +00:00
parent d08c2230a8
commit 3109df842d
8 changed files with 950 additions and 4 deletions

View File

@@ -197,6 +197,17 @@
</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="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 19l4 -6l4 2l4 -8l4 5"></path><path d="M4 4v16h16"></path>
</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">
@@ -557,6 +568,7 @@
</div>
</div>
<script src="/assets/viewstate-store.js"></script>
<script>
const CHART_BG = '#1e2030';
const CHART_GRID = '#2d3045';
@@ -604,7 +616,24 @@
termChartInstance: null,
async init() {
// no auto-load — user must click Lookup first
// restore last loaded surface (survives navigating away & back)
const vs = ViewState.load('surface');
if (vs) {
this.symbol = vs.symbol ?? this.symbol;
this.expirations = vs.expirations ?? [];
this.expiry = vs.expiry ?? '';
if (vs.analytics) {
this.analytics = vs.analytics;
this._processAnalytics(vs.analytics);
}
}
},
_persist(includeAnalytics) {
ViewState.save('surface', {
symbol: this.symbol, expirations: this.expirations, expiry: this.expiry,
analytics: includeAnalytics ? this.analytics : null,
});
},
async fetchExpirations() {
@@ -622,6 +651,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(false);
} catch (err) {
this.errorMsg = 'Failed to look up symbol: ' + err.message;
} finally {
@@ -644,6 +674,7 @@
const data = env.data ?? env;
this.analytics = data;
this._processAnalytics(data);
this._persist(true);
} catch (err) {
this.errorMsg = 'Failed to load surface: ' + err.message;
} finally {