Fix expiration P/L curve in strategy analyzer

- Use exact (not floored) days-to-earliest-expiry so the "Expiration P/L"
  curve uses true intrinsic value — long call/put now show a flat loss floor
  at -premium, a sharp kink at the strike, and the correct breakeven, instead
  of a soft-cornered curve with ~1 day of residual time value
- Clamp the y-axis when one tail is unbounded so the loss floor stays visible
  instead of being squished to a sliver by the runaway profit/loss tail
- Slightly tighter default x-window; denser sampling (161 pts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ojy
2026-05-13 04:07:26 +00:00
parent 3109df842d
commit 2ebc0eeb20

View File

@@ -301,7 +301,9 @@
get netCost() { return this.legs.reduce((s,l)=> s + legCost(l), 0); },
get strategyName() { return detectStrategy(this.legs); },
get maxDTE() { return Math.max(1, Math.ceil(Math.max(0, ...this.legs.map(legDTE)))); },
get minDTE() { return Math.max(0, Math.floor(Math.min(...this.legs.map(legDTE)))); },
// exact (not floored) days to the earliest expiry — so the "expiration"
// curve uses true intrinsic value (sharp hockey stick), not a near-expiry BS approx
get minDTE() { return Math.max(0, Math.min(...this.legs.map(legDTE))); },
get dteLabel() {
const d = new Date(Date.now() + this.dteOffset * DAY_MS);
const ds = d.toISOString().slice(0,10);
@@ -441,19 +443,25 @@
const strikes = legs.map(l=>l.strike);
const minK = Math.min(...strikes), maxK = Math.max(...strikes);
const span = maxK - minK;
const half = Math.max(spot*0.30, span*1.4, (maxK-spot)*1.3, (spot-minK)*1.3, 5);
const half = Math.max(spot*0.22, span*1.5, (maxK-spot)*1.4, (spot-minK)*1.4, 5);
const lo = Math.max(0.01, spot - half), hi = spot + half;
const N = 141;
const expAt = this.minDTE;
const N = 161;
const expData = [], tnData = [];
let yMin = Infinity, yMax = -Infinity;
for (let i = 0; i <= N; i++) {
const x = lo + (hi-lo)*i/N;
const ye = plAt(legs, net, x, this.minDTE);
const ye = plAt(legs, net, x, expAt);
const yt = plAt(legs, net, x, this.dteOffset);
expData.push([x, +ye.toFixed(2)]);
tnData.push([x, +yt.toFixed(2)]);
yMin = Math.min(yMin, ye, yt); yMax = Math.max(yMax, ye, yt);
}
// Keep the chart readable when one tail is (near-)unbounded: don't let it
// dominate the y-axis so badly the rest of the curve is a flat sliver.
const aHi = Math.max(yMax, 0), aLo = Math.max(-yMin, 0);
if (aLo > 1 && aHi > 6 * aLo) yMax = 6 * aLo;
if (aHi > 1 && aLo > 6 * aHi) yMin = -6 * aHi;
const pad = Math.max((yMax - yMin) * 0.08, 1);
yMin -= pad; yMax += pad;