2026-05-13 07:04:52 +00:00
<!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 > Positions — 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; }
.pos-table th { background:#1a1c2e; color:#8b95a7; font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid #2d3045; }
.pos-table td { border-bottom:1px solid #1e2030; color:#d0d5e0; font-size:.85rem; vertical-align:middle; }
.summary-card { background:#1e2030; border:1px solid #2d3045; border-radius:.5rem; padding:.75rem 1rem; }
.summary-card .lbl { color:#8b95a7; font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; font-weight:600; }
.summary-card .val { font-size:1.15rem; font-weight:700; color:#fff; }
.summary-card .val.pos { color:#51cf66; }
.summary-card .val.neg { color:#ff6b6b; }
< / style >
< / head >
< body class = "antialiased" >
< div class = "wrapper" x-data = "positionsApp()" 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" aria-label = "Toggle navigation" > < 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 active" > < 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" > < 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" > Positions< / h2 >
< div class = "text-secondary mt-1" > Strategies you've entered — live P/L with commissions.< / div >
< / div >
< div class = "col-auto d-flex gap-2 align-items-end" >
< div >
< label class = "form-label text-secondary mb-1 small" for = "filterSym" > Symbol< / label >
< select id = "filterSym" class = "form-select form-select-sm" x-model = "filterSymbol" @ change = "applyFilter()" style = "min-width:8rem;" >
< option value = "" > All symbols< / option >
< template x-for = "s in symbolList" :key = "s" > < option :value = "s" x-text = "s" > < / option > < / template >
< / select >
< / div >
< button class = "btn btn-outline-primary btn-sm" @ click = "reload()" :disabled = "loading" >
< span x-show = "loading" class = "spinner-border spinner-border-sm me-1" > < / span > Refresh
< / button >
< / div >
< / div >
< / div >
< / div >
< div class = "page-body" >
< div class = "container-xl" >
<!-- Summary cards -->
< div class = "row g-2 mb-3" x-show = "visible.length > 0" x-cloak >
< div class = "col-6 col-md-3" > < div class = "summary-card" > < div class = "lbl" > Open positions< / div > < div class = "val" x-text = "openCount" > < / div > < / div > < / div >
< div class = "col-6 col-md-3" > < div class = "summary-card" > < div class = "lbl" > Total entered< / div > < div class = "val mono" x-text = "fmtMoney(totals.entered)" > < / div > < / div > < / div >
< div class = "col-6 col-md-3" > < div class = "summary-card" > < div class = "lbl" > Current value< / div > < div class = "val mono" x-text = "fmtMoney(totals.current)" > < / div > < / div > < / div >
< div class = "col-6 col-md-3" > < div class = "summary-card" > < div class = "lbl" > Net P/L (after commissions)< / div > < div class = "val mono" :class = "totals.netPL >= 0 ? 'pos' : 'neg'" x-text = "fmtMoney(totals.netPL)" > < / div > < / div > < / div >
< / div >
<!-- Empty state -->
< div class = "card text-center py-5" x-show = "!loading && visible.length === 0" x-cloak >
< div class = "card-body" >
< h3 class = "text-secondary" > No positions yet< / h3 >
< p class = "text-muted" > Go to < a href = "strategy.html" > Strategy< / a > , build a position, and click < span class = "badge bg-success" > Enter Position< / span > .< / p >
< / div >
< / div >
<!-- Positions table -->
< div class = "card" x-show = "visible.length > 0" x-cloak style = "background:#161824; border:1px solid #2d3045;" >
< div class = "card-header d-flex justify-content-between align-items-center" style = "border-bottom:1px solid #2d3045;" >
< h3 class = "card-title text-white mb-0" >
< span x-text = "visible.length" > < / span > position< span x-show = "visible.length !== 1" > s< / span >
< span class = "text-secondary small fw-normal ms-2" x-show = "filterSymbol" x-text = "'· ' + filterSymbol" > < / span >
< / h3 >
< span class = "text-secondary small" >
Commission: < span class = "mono" x-text = "commissionLabel" > < / span >
· < a href = "settings.html" class = "text-decoration-none" > change< / a >
< / span >
< / div >
< div class = "table-responsive" >
< table class = "table table-sm pos-table mb-0" >
< thead >
< tr >
< th > Sym< / th > < th > Strategy< / th > < th > Legs< / th >
< th class = "text-end" > Entered $< / th >
< th class = "text-end" > Current $< / th >
< th class = "text-end" > Comm. (RT)< / th >
< th class = "text-end" > Gross P/L< / th >
< th class = "text-end" > Net P/L< / th >
< th class = "text-end" > P/L %< / th >
< th > Opened< / th >
< th > Status< / th >
2026-05-13 07:08:52 +00:00
< th > < / th > < th > < / th > < th > < / th >
2026-05-13 07:04:52 +00:00
< / tr >
< / thead >
< tbody >
< template x-for = "row in visible" :key = "row.o.id" >
< tr :style = "row.o.status === 'closed' ? 'opacity:.55' : ''" >
< td class = "mono fw-semibold" x-text = "row.o.symbol" > < / td >
< td > < span class = "badge bg-purple-lt" x-text = "row.o.name || ('Custom (' + row.o.legs.length + ' legs)')" > < / span > < / td >
< td class = "mono small text-secondary" x-text = "row.legsSummary" > < / td >
< td class = "text-end mono" x-text = "fmtMoney(row.entered)" > < / td >
< td class = "text-end mono" x-text = "fmtMoney(row.value)" > < / td >
< td class = "text-end mono text-warning" x-text = "'-' + fmtMoney(row.commission)" > < / td >
< td class = "text-end mono" :class = "row.grossPL >= 0 ? 'text-success' : 'text-danger'" x-text = "fmtMoney(row.grossPL)" > < / td >
< td class = "text-end mono fw-bold" :class = "row.netPL >= 0 ? 'text-success' : 'text-danger'" x-text = "fmtMoney(row.netPL)" > < / td >
< td class = "text-end mono small" :class = "row.netPL >= 0 ? 'text-success' : 'text-danger'" x-text = "row.plPct.toFixed(1) + '%'" > < / td >
< td class = "small text-secondary mono" x-text = "(row.o.created_at||'').slice(0,10)" > < / td >
< td > < span class = "badge" :class = "row.o.status === 'open' ? 'bg-success-lt text-success' : 'bg-secondary-lt text-secondary'" x-text = "row.o.status" > < / span > < / td >
2026-05-13 07:09:53 +00:00
< td > < button class = "btn btn-sm btn-outline-primary" @ click = "openInStrategy(row.o)" title = "Open this position in the Strategy P/L chart" > P/L Chart< / button > < / td >
2026-05-13 07:04:52 +00:00
< td > < button class = "btn btn-sm" :class = "row.o.status === 'open' ? 'btn-outline-warning' : 'btn-outline-success'" @ click = "toggleClose(row.o)" x-text = "row.o.status === 'open' ? 'Close' : 'Reopen'" > < / button > < / td >
< td > < button class = "btn btn-sm btn-ghost-danger" @ click = "remove(row.o.id)" aria-label = "Remove position" > ✕< / button > < / td >
< / tr >
< / template >
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- 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 >
< script src = "/assets/tabler.min.js" defer > < / script >
< script src = "/assets/settings-store.js" > < / script >
2026-05-13 07:08:52 +00:00
< script src = "/assets/strategy-store.js" > < / script >
2026-05-13 07:04:52 +00:00
< script src = "/assets/alpine.min.js" defer > < / script >
< script >
function positionsApp() {
return {
orders: [],
chains: {}, // "SYMBOL@EXPIRY" -> { "strike@type": option }
loading: false,
toast: '',
filterSymbol: '',
async init() { await this.reload(); },
async reload() {
this.loading = true;
try {
const r = await fetch('/api/orders');
const d = await r.json();
this.orders = d.data?.orders || [];
await this.refreshChains();
} catch (e) {
this.flash('Load failed: ' + e.message);
} finally {
this.loading = false;
}
},
async refreshChains() {
// fetch one chain per unique (symbol, expiry) used by the listed orders
const need = new Set();
for (const o of this.orders) for (const l of (o.legs||[])) {
if (l.symbol & & l.expiry) need.add(l.symbol + '@' + l.expiry);
}
const cache = { ...this.chains };
for (const key of need) {
if (cache[key]) continue;
const [sym, exp] = key.split('@');
try {
const r = await fetch('/api/chain?symbol=' + encodeURIComponent(sym) + '& expiry=' + encodeURIComponent(exp));
if (!r.ok) continue;
const d = await r.json();
const snap = d.data?.snapshots?.[0];
if (!snap) continue;
const map = {};
for (const o of (snap.chain || [])) {
const t = (o.type || o.optionType || '').toLowerCase();
map[Number(o.strike) + '@' + t] = o;
}
cache[key] = map;
} catch {}
}
this.chains = cache;
},
get symbolList() {
return [...new Set(this.orders.map(o => o.symbol))].sort();
},
rowFor(o) {
let entered = 0, value = 0;
for (const l of (o.legs||[])) {
const sign = l.side === 'short' ? -1 : 1;
entered += sign * l.qty * 100 * (l.entryPrice || 0);
const m = this.chains[l.symbol + '@' + l.expiry];
const oo = m & & m[Number(l.strike) + '@' + l.type];
const mid = oo ? Number(oo.midPrice ?? oo.mid ?? oo.bsPrice ?? 0) : 0;
value += sign * l.qty * 100 * mid;
}
const baseEntered = (o.entry_cost != null) ? Number(o.entry_cost) : entered;
const grossPL = value - baseEntered;
const commission = SettingsStore.estimate(o.legs || []).roundTrip;
const netPL = grossPL - commission;
const denom = Math.max(Math.abs(baseEntered), 1);
return {
o, entered: baseEntered, value, grossPL, commission, netPL,
plPct: netPL / denom * 100,
legsSummary: (o.legs||[]).map(l =>
(l.side === 'long' ? '+' : '-') + l.qty + ' ' + l.strike + (l.type === 'call' ? 'C' : 'P') + '·' + (l.expiry||'').slice(5)
).join(' / '),
};
},
get visible() {
const list = this.filterSymbol ? this.orders.filter(o => o.symbol === this.filterSymbol) : this.orders;
return list.map(o => this.rowFor(o));
},
get openCount() { return this.visible.filter(r => r.o.status === 'open').length; },
get totals() {
let entered = 0, current = 0, comm = 0, gross = 0;
for (const r of this.visible) {
if (r.o.status !== 'open') continue;
entered += r.entered; current += r.value; comm += r.commission; gross += r.grossPL;
}
return { entered, current, commission: comm, grossPL: gross, netPL: gross - comm };
},
get commissionLabel() {
const c = SettingsStore.load().commission;
const planName = c.plan === 'ibkr-fixed' ? 'IBKR Fixed'
: c.plan === 'ibkr-tiered' ? 'IBKR Tiered'
: 'Custom';
return planName + ' · $' + c.perContract.toFixed(2) + '/ct, min $' + c.perOrderMin.toFixed(2);
},
applyFilter() {/* x-model already drives `visible` */},
async toggleClose(o) {
const newStatus = o.status === 'open' ? 'closed' : 'open';
try {
const r = await fetch('/api/orders/' + o.id, {
method:'PATCH', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ status: newStatus }),
});
const d = await r.json();
if (d.ok) {
const idx = this.orders.findIndex(x => x.id === o.id);
if (idx >= 0) this.orders[idx] = d.data;
}
} catch {}
},
async remove(id) {
if (!confirm('Remove this position?')) return;
try {
await fetch('/api/orders/' + id, { method:'DELETE' });
this.orders = this.orders.filter(o => o.id !== id);
this.flash('Removed');
} catch {}
},
2026-05-13 07:08:52 +00:00
// Load this order's legs into the Strategy basket (replacing any existing
// legs for the same symbol), then open the Strategy page.
openInStrategy(o) {
const sym = o.symbol;
let existingLegs = 0;
try {
const doc = JSON.parse(localStorage.getItem('optionsPricer:strategy') || '{}');
existingLegs = doc?.baskets?.[sym]?.legs?.length || 0;
} catch {}
if (existingLegs > 0 & & !confirm(`Replace your current ${sym} strategy (${existingLegs} leg${existingLegs===1?'':'s'}) with order #${o.id}?`)) return;
const legs = (o.legs || []).map(l => ({
id: 'id-' + Date.now() + '-' + Math.random().toString(36).slice(2),
symbol: l.symbol,
expiry: l.expiry,
type: l.type,
strike: Number(l.strike),
side: l.side === 'short' ? 'short' : 'long',
qty: Math.max(1, Math.round(Number(l.qty) || 1)),
entryPrice: Number(l.entryPrice) || 0,
iv: Number(l.iv) || 0,
locked: true, // it's an entered position — lock the entry prices
currentMark: null,
}));
StrategyStore.save({ symbol: sym, spotSnapshot: 0, legs });
StrategyStore.setActive(sym);
this.flash('Loaded into Strategy — opening…');
setTimeout(() => { window.location.href = '/strategy.html'; }, 400);
},
2026-05-13 07:04:52 +00:00
flash(msg) { this.toast = msg; setTimeout(() => { this.toast = ''; }, 2500); },
fmtMoney(v) {
if (v == null || !isFinite(v)) return '—';
const a = Math.abs(v), sign = v < 0 ? ' - ' : ' ' ;
return sign + '$' + a.toLocaleString('en-US', { maximumFractionDigits: a < 100 ? 2 : 0 } ) ;
},
};
}
< / script >
< / body >
< / html >