Split Positions from Tracker; add Settings (commission)

Tracker is for symbol price/IV history, not positions. The "Save to
Tracker" button now adds the symbol to a watchlist (localStorage); the
Tracker page shows the watchlist as clickable chips.

New "Enter Position" button on the Strategy page posts the active legs
to /api/orders, then opens the new Positions page.

New Positions page (positions.html): lists entered positions with live
mid value, round-trip commission, gross & net P/L (Net = Gross − round-
trip commission), per-symbol filter, summary totals, close/reopen and
remove actions.

New Settings page (settings.html) configures the commission used on
Positions. Defaults to Interactive Brokers Fixed / IBKR Lite: $0.65
per contract, $1.00 minimum per order
(https://www.interactivebrokers.com/en/pricing/commissions-options.php).
Per-leg vs per-order toggle for complex orders.

Sidebar nav now: Dashboard · Options Chain · Vol Surface · Strategy P/L
· Positions · Tracker · Settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ojy
2026-05-13 07:04:52 +00:00
parent c1520d7962
commit 9de7f14573
8 changed files with 696 additions and 141 deletions

172
frontend/settings.html Normal file
View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Settings — Options Pricer</title>
<link rel="stylesheet" href="/assets/tabler.min.css" />
<link rel="stylesheet" href="/assets/tabler-vendors.min.css" />
<style>
.mono { font-family:'JetBrains Mono','Fira Code',monospace; }
[x-cloak] { display:none !important; }
</style>
</head>
<body class="antialiased">
<div class="wrapper" x-data="settingsApp()" x-init="init()">
<!-- Sidebar -->
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sb-menu"><span class="navbar-toggler-icon"></span></button>
<h1 class="navbar-brand navbar-brand-autodark">
<a href="index.html" class="text-decoration-none d-flex align-items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><rect x="4" y="8" width="4" height="8" rx="1"/><line x1="6" y1="4" x2="6" y2="8"/><line x1="6" y1="16" x2="6" y2="20"/><rect x="16" y="6" width="4" height="10" rx="1"/><line x1="18" y1="2" x2="18" y2="6"/><line x1="18" y1="16" x2="18" y2="22"/></svg>
<span class="fw-bold">Options Pricer</span>
</a>
</h1>
<div class="collapse navbar-collapse" id="sb-menu">
<ul class="navbar-nav pt-lg-3">
<li class="nav-item"><a class="nav-link" href="index.html"><span class="nav-link-title">Dashboard</span></a></li>
<li class="nav-item"><a class="nav-link" href="chain.html"><span class="nav-link-title">Options Chain</span></a></li>
<li class="nav-item"><a class="nav-link" href="surface.html"><span class="nav-link-title">Vol Surface</span></a></li>
<li class="nav-item"><a class="nav-link" href="strategy.html"><span class="nav-link-title">Strategy P/L</span></a></li>
<li class="nav-item"><a class="nav-link" href="positions.html"><span class="nav-link-title">Positions</span></a></li>
<li class="nav-item"><a class="nav-link" href="tracker.html"><span class="nav-link-title">Tracker</span></a></li>
<li class="nav-item active"><a class="nav-link" href="settings.html"><span class="nav-link-title">Settings</span></a></li>
</ul>
</div>
</div>
</aside>
<!-- Page -->
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col"><h2 class="page-title">Settings</h2></div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card mb-3" style="background:#161824; border:1px solid #2d3045;">
<div class="card-header" style="border-bottom:1px solid #2d3045;">
<h3 class="card-title text-white mb-0">Options Commission</h3>
</div>
<div class="card-body">
<p class="text-secondary small mb-3">
Used when computing Net P/L on the <a href="positions.html">Positions</a> page.
Defaults match <strong>Interactive Brokers Fixed / IBKR Lite</strong> for US equity options:
<span class="mono">$0.65 per contract, $1.00 minimum per order</span>
(<a href="https://www.interactivebrokers.com/en/pricing/commissions-options.php" target="_blank" rel="noopener">IBKR docs</a>).
</p>
<div class="row g-3">
<div class="col-12">
<label class="form-label small text-secondary">Plan</label>
<div class="btn-group" role="group" aria-label="Commission plan preset">
<button class="btn btn-sm" :class="c.plan === 'ibkr-fixed' ? 'btn-primary' : 'btn-outline-secondary'" @click="setPlan('ibkr-fixed')">IBKR Fixed / Lite</button>
<button class="btn btn-sm" :class="c.plan === 'ibkr-tiered' ? 'btn-primary' : 'btn-outline-secondary'" @click="setPlan('ibkr-tiered')">IBKR Tiered</button>
<button class="btn btn-sm" :class="c.plan === 'custom' ? 'btn-primary' : 'btn-outline-secondary'" @click="setPlan('custom')">Custom</button>
</div>
<div class="text-secondary small mt-2" x-show="c.plan === 'ibkr-tiered'">
IBKR Tiered: ~$0.15$0.65 per contract by volume, plus exchange / regulatory fees. Estimated effective ~$0.55/ct, $1.00 min.
</div>
</div>
<div class="col-md-4">
<label class="form-label small text-secondary" for="cm-pc">$ per contract</label>
<input id="cm-pc" type="number" min="0" step="0.01" class="form-control" :value="c.perContract" @change="update('perContract', +$event.target.value || 0)" />
</div>
<div class="col-md-4">
<label class="form-label small text-secondary" for="cm-min">$ min per order</label>
<input id="cm-min" type="number" min="0" step="0.01" class="form-control" :value="c.perOrderMin" @change="update('perOrderMin', +$event.target.value || 0)" />
</div>
<div class="col-md-4">
<label class="form-label small text-secondary" for="cm-max">$ max per order <span class="text-secondary">(0 = none)</span></label>
<input id="cm-max" type="number" min="0" step="0.01" class="form-control" :value="c.perOrderMax" @change="update('perOrderMax', +$event.target.value || 0)" />
</div>
<div class="col-12">
<div class="form-check form-switch">
<input id="cm-leg" type="checkbox" class="form-check-input" :checked="c.applyPerLeg" @change="update('applyPerLeg', $event.target.checked)" />
<label class="form-check-label small" for="cm-leg">Charge each leg as a separate order
<span class="text-secondary">— off (default): multi-leg complex orders count as one, so the per-order minimum applies once.</span>
</label>
</div>
</div>
</div>
<hr class="my-4" style="border-color:#2d3045;">
<h4 class="text-secondary text-uppercase small mb-2" style="letter-spacing:.05em;">Preview</h4>
<div class="row g-3">
<div class="col-md-4"><div class="p-3" style="background:#1e2030; border:1px solid #2d3045; border-radius:.5rem;">
<div class="text-secondary small">1 single-leg, 1 contract</div>
<div class="mono fs-5">Round-trip: <span x-text="'$' + preview([{qty:1}]).toFixed(2)"></span></div>
</div></div>
<div class="col-md-4"><div class="p-3" style="background:#1e2030; border:1px solid #2d3045; border-radius:.5rem;">
<div class="text-secondary small">2-leg vertical, 1 contract each</div>
<div class="mono fs-5">Round-trip: <span x-text="'$' + preview([{qty:1},{qty:1}]).toFixed(2)"></span></div>
</div></div>
<div class="col-md-4"><div class="p-3" style="background:#1e2030; border:1px solid #2d3045; border-radius:.5rem;">
<div class="text-secondary small">4-leg iron condor, 1 contract each</div>
<div class="mono fs-5">Round-trip: <span x-text="'$' + preview([{qty:1},{qty:1},{qty:1},{qty:1}]).toFixed(2)"></span></div>
</div></div>
</div>
<div class="mt-4 d-flex justify-content-between align-items-center">
<button class="btn btn-outline-danger btn-sm" @click="resetDefaults()">Reset to IBKR Fixed defaults</button>
<span class="text-secondary small" x-show="savedFlash" x-cloak x-text="savedFlash"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/assets/tabler.min.js" defer></script>
<script src="/assets/settings-store.js"></script>
<script src="/assets/alpine.min.js" defer></script>
<script>
function settingsApp() {
return {
c: { plan:'ibkr-fixed', perContract:0.65, perOrderMin:1.00, perOrderMax:0, applyPerLeg:false },
savedFlash: '',
init() { this.c = SettingsStore.load().commission; },
update(key, val) {
this.c[key] = val;
// changing numeric fields manually -> mark as custom
if (['perContract','perOrderMin','perOrderMax','applyPerLeg'].includes(key)) this.c.plan = 'custom';
this._save();
},
setPlan(plan) {
this.c.plan = plan;
if (plan === 'ibkr-fixed') { this.c.perContract = 0.65; this.c.perOrderMin = 1.00; this.c.perOrderMax = 0; this.c.applyPerLeg = false; }
if (plan === 'ibkr-tiered') { this.c.perContract = 0.55; this.c.perOrderMin = 1.00; this.c.perOrderMax = 0; this.c.applyPerLeg = false; }
this._save();
},
resetDefaults() {
this.c = SettingsStore.reset().commission;
this._save(true);
},
_save(silent) {
SettingsStore.save({ commission: this.c });
if (!silent) {
this.savedFlash = 'Saved · ' + new Date().toLocaleTimeString();
clearTimeout(this._t); this._t = setTimeout(() => { this.savedFlash = ''; }, 1500);
}
},
preview(legs) { return SettingsStore.estimate(legs).roundTrip; },
};
}
</script>
</body>
</html>