Add price-range zoom controls to the P/L chart
−/+ buttons (with a ±% readout) and a Fit button in the chart header widen or narrow the underlying-price window the curve is sampled over, so you can inspect a tight range around spot or zoom out to see the full payoff shape. Bumped sampling density to 221 points. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,13 +96,23 @@
|
|||||||
|
|
||||||
<!-- P/L chart -->
|
<!-- P/L chart -->
|
||||||
<div class="chart-card mb-3" x-show="legs.length > 0" x-cloak>
|
<div class="chart-card mb-3" x-show="legs.length > 0" x-cloak>
|
||||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||||
<h3 class="card-title text-white mb-0">Profit / Loss vs. Underlying Price</h3>
|
<h3 class="card-title text-white mb-0">Profit / Loss vs. Underlying Price</h3>
|
||||||
<div class="d-flex align-items-center gap-2" style="min-width:320px;">
|
<div class="d-flex align-items-center flex-wrap gap-3">
|
||||||
<span class="text-secondary small text-nowrap">Now</span>
|
<!-- price-range zoom -->
|
||||||
<input type="range" class="form-range" min="0" :max="maxDTE" step="1" x-model.number="dteOffset" @input="scheduleRender()" style="min-width:160px;">
|
<div class="btn-group btn-group-sm" role="group" aria-label="Zoom price range">
|
||||||
<span class="text-secondary small text-nowrap">Exp</span>
|
<button class="btn btn-outline-secondary" @click="zoomOut()" title="Zoom out — wider price range">−</button>
|
||||||
<span class="badge bg-blue-lt text-nowrap" x-text="dteLabel"></span>
|
<button class="btn btn-outline-secondary" disabled style="min-width:5rem" x-text="'±' + lastHalfPct + '%'"></button>
|
||||||
|
<button class="btn btn-outline-secondary" @click="zoomIn()" title="Zoom in — narrower price range">+</button>
|
||||||
|
<button class="btn btn-outline-secondary" @click="zoomFit()" title="Reset zoom">Fit</button>
|
||||||
|
</div>
|
||||||
|
<!-- time slider -->
|
||||||
|
<div class="d-flex align-items-center gap-2" style="min-width:300px;">
|
||||||
|
<span class="text-secondary small text-nowrap">Now</span>
|
||||||
|
<input type="range" class="form-range" min="0" :max="maxDTE" step="1" x-model.number="dteOffset" @input="scheduleRender()" style="min-width:150px;">
|
||||||
|
<span class="text-secondary small text-nowrap">Exp</span>
|
||||||
|
<span class="badge bg-blue-lt text-nowrap" x-text="dteLabel"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@@ -300,6 +310,7 @@
|
|||||||
// state
|
// state
|
||||||
symbol: '', symbols: [], spot: 0, legs: [],
|
symbol: '', symbols: [], spot: 0, legs: [],
|
||||||
dteOffset: 0,
|
dteOffset: 0,
|
||||||
|
xZoom: 1, lastHalfPct: 0,
|
||||||
refreshing: false, showManual: false, toast: '',
|
refreshing: false, showManual: false, toast: '',
|
||||||
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
||||||
chart: null, _renderTimer: null,
|
chart: null, _renderTimer: null,
|
||||||
@@ -517,6 +528,9 @@
|
|||||||
clearTimeout(this._renderTimer);
|
clearTimeout(this._renderTimer);
|
||||||
this._renderTimer = setTimeout(() => this.renderChart(), 40);
|
this._renderTimer = setTimeout(() => this.renderChart(), 40);
|
||||||
},
|
},
|
||||||
|
zoomIn() { this.xZoom = Math.max(0.2, +(this.xZoom / 1.4).toFixed(3)); this.renderChart(); },
|
||||||
|
zoomOut() { this.xZoom = Math.min(8, +(this.xZoom * 1.4).toFixed(3)); this.renderChart(); },
|
||||||
|
zoomFit() { this.xZoom = 1; this.renderChart(); },
|
||||||
|
|
||||||
renderChart() {
|
renderChart() {
|
||||||
const legs = this.activeLegs, net = this.netCost;
|
const legs = this.activeLegs, net = this.netCost;
|
||||||
@@ -528,10 +542,12 @@
|
|||||||
const strikes = legs.map(l=>l.strike);
|
const strikes = legs.map(l=>l.strike);
|
||||||
const minK = Math.min(...strikes), maxK = Math.max(...strikes);
|
const minK = Math.min(...strikes), maxK = Math.max(...strikes);
|
||||||
const span = maxK - minK;
|
const span = maxK - minK;
|
||||||
const half = Math.max(spot*0.22, span*1.5, (maxK-spot)*1.4, (spot-minK)*1.4, 5);
|
const baseHalf = Math.max(spot*0.22, span*1.5, (maxK-spot)*1.4, (spot-minK)*1.4, 5);
|
||||||
|
const half = baseHalf * (this.xZoom || 1);
|
||||||
const lo = Math.max(0.01, spot - half), hi = spot + half;
|
const lo = Math.max(0.01, spot - half), hi = spot + half;
|
||||||
|
this.lastHalfPct = spot > 0 ? Math.round(half / spot * 100) : 0;
|
||||||
const expAt = this.minDTE;
|
const expAt = this.minDTE;
|
||||||
const N = 161;
|
const N = 221;
|
||||||
// x-grid = evenly spaced points + the exact strike kinks (so payoff
|
// x-grid = evenly spaced points + the exact strike kinks (so payoff
|
||||||
// vertices — butterfly peak, condor body, etc. — render sharply)
|
// vertices — butterfly peak, condor body, etc. — render sharply)
|
||||||
const xGrid = [];
|
const xGrid = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user