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:
82
frontend/assets/settings-store.js
Normal file
82
frontend/assets/settings-store.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* App-wide settings — commission config, etc.
|
||||||
|
* Exposed as window.SettingsStore. localStorage key "optionsPricer:settings".
|
||||||
|
*
|
||||||
|
* Default plan = IBKR Fixed / Lite:
|
||||||
|
* $0.65 per contract, $1.00 per-order minimum, no per-order maximum.
|
||||||
|
* Source: https://www.interactivebrokers.com/en/pricing/commissions-options.php
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
const KEY = "optionsPricer:settings";
|
||||||
|
const VERSION = 1;
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
v: VERSION,
|
||||||
|
commission: {
|
||||||
|
plan: "ibkr-fixed", // ibkr-fixed | ibkr-tiered | custom
|
||||||
|
perContract: 0.65, // $ per option contract
|
||||||
|
perOrderMin: 1.00, // $ minimum per order
|
||||||
|
perOrderMax: 0, // $ cap per order; 0 = none
|
||||||
|
applyPerLeg: false, // true = each leg charged separately; false = whole position counts as one order
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function clone(o) { return JSON.parse(JSON.stringify(o)); }
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(KEY);
|
||||||
|
if (!raw) return clone(DEFAULTS);
|
||||||
|
const obj = JSON.parse(raw);
|
||||||
|
if (!obj || obj.v !== VERSION) return clone(DEFAULTS);
|
||||||
|
// merge over defaults so missing fields fall back
|
||||||
|
const out = clone(DEFAULTS);
|
||||||
|
if (obj.commission && typeof obj.commission === "object") {
|
||||||
|
Object.assign(out.commission, obj.commission);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
} catch { return clone(DEFAULTS); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(s) {
|
||||||
|
const out = clone(DEFAULTS);
|
||||||
|
if (s && s.commission) Object.assign(out.commission, s.commission);
|
||||||
|
try { localStorage.setItem(KEY, JSON.stringify(out)); } catch (e) { console.warn("[SettingsStore] save failed:", e); }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
try { localStorage.removeItem(KEY); } catch {}
|
||||||
|
return clone(DEFAULTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate commission for a position.
|
||||||
|
* legs: [{ qty }, ...] (only qty matters)
|
||||||
|
* Returns { entry, exit, roundTrip } in dollars.
|
||||||
|
*/
|
||||||
|
function estimate(legs) {
|
||||||
|
const c = load().commission;
|
||||||
|
if (!Array.isArray(legs) || legs.length === 0) return { entry: 0, exit: 0, roundTrip: 0 };
|
||||||
|
|
||||||
|
const orderCost = (contracts) => {
|
||||||
|
let v = contracts * c.perContract;
|
||||||
|
if (c.perOrderMin > 0) v = Math.max(v, c.perOrderMin);
|
||||||
|
if (c.perOrderMax > 0) v = Math.min(v, c.perOrderMax);
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry;
|
||||||
|
if (c.applyPerLeg) {
|
||||||
|
entry = legs.reduce((s, l) => s + orderCost(Math.max(1, Math.round(l.qty || 1))), 0);
|
||||||
|
} else {
|
||||||
|
const total = legs.reduce((s, l) => s + Math.max(1, Math.round(l.qty || 1)), 0);
|
||||||
|
entry = orderCost(total);
|
||||||
|
}
|
||||||
|
const exit = entry; // assume same fill style on close
|
||||||
|
return { entry, exit, roundTrip: entry + exit };
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SettingsStore = { KEY, VERSION, DEFAULTS: clone(DEFAULTS), load, save, reset, estimate };
|
||||||
|
})();
|
||||||
@@ -129,6 +129,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="positions.html">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M5 4h4v4h-4z"/><path d="M5 12h4v8h-4z"/><path d="M13 4h4v12h-4z"/><path d="M13 18h4v2h-4z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Positions</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="tracker.html">
|
<a class="nav-link" href="tracker.html">
|
||||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
@@ -145,6 +157,19 @@
|
|||||||
<span class="nav-link-title">Tracker</span>
|
<span class="nav-link-title">Tracker</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="settings.html">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Settings</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -213,6 +213,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Positions -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="positions.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" aria-hidden="true">
|
||||||
|
<path d="M5 4h4v4h-4z"/><path d="M5 12h4v8h-4z"/><path d="M13 4h4v12h-4z"/><path d="M13 18h4v2h-4z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Positions</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Tracker -->
|
<!-- Tracker -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="tracker.html">
|
<a class="nav-link" href="tracker.html">
|
||||||
@@ -226,6 +238,19 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="settings.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" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Settings</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
298
frontend/positions.html
Normal file
298
frontend/positions.html
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<!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>
|
||||||
|
<th></th><th></th>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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 {}
|
||||||
|
},
|
||||||
|
|
||||||
|
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>
|
||||||
172
frontend/settings.html
Normal file
172
frontend/settings.html
Normal 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>
|
||||||
@@ -44,7 +44,9 @@
|
|||||||
<li class="nav-item"><a class="nav-link" href="chain.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="3" y="5" width="18" height="14" rx="2"/><path d="M3 10l18 0"/><path d="M10 5v14"/></svg></span><span class="nav-link-title">Options Chain</span></a></li>
|
<li class="nav-item"><a class="nav-link" href="chain.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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="3" y="5" width="18" height="14" rx="2"/><path d="M3 10l18 0"/><path d="M10 5v14"/></svg></span><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-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"/><path d="M3 12c1.333 -4.667 2.667 -7 4 -7s2.667 2.333 4 7s2.667 7 4 7s2.667 -2.333 4 -7"/></svg></span><span class="nav-link-title">Vol Surface</span></a></li>
|
<li class="nav-item"><a class="nav-link" href="surface.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"/><path d="M3 12c1.333 -4.667 2.667 -7 4 -7s2.667 2.333 4 7s2.667 7 4 7s2.667 -2.333 4 -7"/></svg></span><span class="nav-link-title">Vol Surface</span></a></li>
|
||||||
<li class="nav-item active"><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="24" height="24" 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"/><path d="M4 19l4 -6l4 2l4 -8l4 5"/><path d="M4 4v16h16"/></svg></span><span class="nav-link-title">Strategy P/L</span></a></li>
|
<li class="nav-item active"><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="24" height="24" 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"/><path d="M4 19l4 -6l4 2l4 -8l4 5"/><path d="M4 4v16h16"/></svg></span><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-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h4v4h-4z"/><path d="M5 12h4v8h-4z"/><path d="M13 4h4v12h-4z"/><path d="M13 18h4v2h-4z"/></svg></span><span class="nav-link-title">Positions</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"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"/><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M12 12m-5 0a5 5 0 1 0 10 0a5 5 0 1 0 -10 0"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M15 12l-3 -3"/></svg></span><span class="nav-link-title">Tracker</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"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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"/><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M12 12m-5 0a5 5 0 1 0 10 0a5 5 0 1 0 -10 0"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M15 12l-3 -3"/></svg></span><span class="nav-link-title">Tracker</span></a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="settings.html"><span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span><span class="nav-link-title">Settings</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,9 +78,13 @@
|
|||||||
<button class="btn btn-outline-primary btn-sm me-1" @click="reloadMarket()" :disabled="refreshing" title="Re-fetch spot, marks & IVs; refresh entry price on unlocked legs">
|
<button class="btn btn-outline-primary btn-sm me-1" @click="reloadMarket()" :disabled="refreshing" title="Re-fetch spot, marks & IVs; refresh entry price on unlocked legs">
|
||||||
<span x-show="refreshing" class="spinner-border spinner-border-sm me-1" role="status"></span>Reload
|
<span x-show="refreshing" class="spinner-border spinner-border-sm me-1" role="status"></span>Reload
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-success btn-sm me-1" @click="saveOrder()" :disabled="savingOrder || activeLegs.length === 0"
|
<button class="btn btn-outline-info btn-sm me-1" @click="saveToTracker()" :disabled="!symbol"
|
||||||
title="Save this position to the Tracker (uses currently-checked legs)">
|
title="Add this symbol to the Tracker watchlist for IV / price-history monitoring">
|
||||||
<span x-show="savingOrder" class="spinner-border spinner-border-sm me-1" role="status"></span>Save to Tracker
|
Save to Tracker
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success btn-sm me-1" @click="enterPosition()" :disabled="savingOrder || activeLegs.length === 0"
|
||||||
|
title="Save this position (entered legs) — opens the Positions page">
|
||||||
|
<span x-show="savingOrder" class="spinner-border spinner-border-sm me-1" role="status"></span>Enter Position
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger btn-sm" @click="clearAll()">Clear all</button>
|
<button class="btn btn-outline-danger btn-sm" @click="clearAll()">Clear all</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -748,8 +754,20 @@
|
|||||||
},
|
},
|
||||||
flash(msg) { this.toast = msg; setTimeout(()=>{ this.toast=''; }, 2500); },
|
flash(msg) { this.toast = msg; setTimeout(()=>{ this.toast=''; }, 2500); },
|
||||||
|
|
||||||
// POST the current active basket to /api/orders for tracking
|
// Add the current symbol to the Tracker watchlist (localStorage).
|
||||||
async saveOrder() {
|
saveToTracker() {
|
||||||
|
if (!this.symbol) return;
|
||||||
|
const sym = this.symbol.toUpperCase().trim();
|
||||||
|
let wl = [];
|
||||||
|
try { wl = JSON.parse(localStorage.getItem('optionsPricer:watchlist') || '[]'); } catch {}
|
||||||
|
if (!wl.includes(sym)) wl.push(sym);
|
||||||
|
wl.sort();
|
||||||
|
try { localStorage.setItem('optionsPricer:watchlist', JSON.stringify(wl)); } catch {}
|
||||||
|
this.flash(sym + ' added to Tracker watchlist (' + wl.length + ' symbol' + (wl.length===1?'':'s') + ')');
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST the current active basket to /api/orders, then navigate to Positions.
|
||||||
|
async enterPosition() {
|
||||||
const legs = this.activeLegs;
|
const legs = this.activeLegs;
|
||||||
if (legs.length === 0 || !this.symbol) return;
|
if (legs.length === 0 || !this.symbol) return;
|
||||||
this.savingOrder = true;
|
this.savingOrder = true;
|
||||||
@@ -770,9 +788,10 @@
|
|||||||
});
|
});
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!r.ok || !d.ok) throw new Error(d.error || ('HTTP ' + r.status));
|
if (!r.ok || !d.ok) throw new Error(d.error || ('HTTP ' + r.status));
|
||||||
this.flash('Saved to Tracker · order #' + d.data.id + ' (' + this.strategyName + ')');
|
this.flash('Position #' + d.data.id + ' entered — opening Positions…');
|
||||||
|
setTimeout(() => { window.location.href = '/positions.html'; }, 700);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.flash('Save failed: ' + e.message);
|
this.flash('Enter failed: ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
this.savingOrder = false;
|
this.savingOrder = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="positions.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="M5 4h4v4h-4z"/><path d="M5 12h4v8h-4z"/><path d="M13 4h4v12h-4z"/><path d="M13 18h4v2h-4z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Positions</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="tracker.html">
|
<a class="nav-link" href="tracker.html">
|
||||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
@@ -221,6 +232,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="settings.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">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Settings</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -168,6 +168,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="positions.html">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 4h4v4h-4z"/><path d="M5 12h4v8h-4z"/><path d="M13 4h4v12h-4z"/><path d="M13 18h4v2h-4z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Positions</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active-page" href="tracker.html">
|
<a class="nav-link active-page" href="tracker.html">
|
||||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
@@ -181,6 +192,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="settings.html">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">Settings</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,52 +278,15 @@
|
|||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
|
|
||||||
<!-- Tracked Orders (persisted strategy positions) -->
|
<!-- Watchlist (symbols saved from Strategy page) -->
|
||||||
<div class="card mb-3" x-show="orders.length > 0 || ordersLoading" x-cloak style="background:#161824; border:1px solid #2d3045;">
|
<div class="mb-3 d-flex align-items-center flex-wrap gap-2" x-show="watchlist.length > 0" x-cloak>
|
||||||
<div class="card-header d-flex align-items-center justify-content-between" style="border-bottom:1px solid #2d3045;">
|
<span class="text-secondary small me-1">Watchlist:</span>
|
||||||
<h3 class="card-title text-white mb-0">
|
<template x-for="s in watchlist" :key="s">
|
||||||
Tracked Orders
|
<span class="badge bg-blue-lt" style="cursor:pointer; padding:.45rem .6rem;">
|
||||||
<span class="text-secondary small fw-normal" x-show="symbol">— <span x-text="symbol"></span></span>
|
<span @click="loadSymbol(s)" x-text="s" :class="s === symbol.toUpperCase().trim() ? 'fw-bold' : ''"></span>
|
||||||
</h3>
|
<span class="text-danger ms-1" style="cursor:pointer;" @click="removeWatch(s)" :title="'Remove ' + s + ' from watchlist'">✕</span>
|
||||||
<div class="d-flex align-items-center gap-2">
|
</span>
|
||||||
<span x-show="ordersLoading" class="spinner-border spinner-border-sm text-secondary"></span>
|
</template>
|
||||||
<button class="btn btn-sm btn-outline-secondary" @click="loadOrders()" :disabled="ordersLoading">Refresh</button>
|
|
||||||
<span class="text-secondary small" x-text="orders.length + ' order' + (orders.length===1?'':'s')"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-hover mb-0" style="color:#d0d5e0;">
|
|
||||||
<thead style="background:#1a1c2e;color:#8b95a7;font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;">
|
|
||||||
<tr>
|
|
||||||
<th>Strategy</th>
|
|
||||||
<th>Legs</th>
|
|
||||||
<th class="text-end">Entered</th>
|
|
||||||
<th class="text-end">Current</th>
|
|
||||||
<th class="text-end">P/L $</th>
|
|
||||||
<th class="text-end">P/L %</th>
|
|
||||||
<th>Opened</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th></th><th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template x-for="o in orders" :key="o.id">
|
|
||||||
<tr :style="o.status === 'closed' ? 'opacity:.55' : ''" style="font-size:.85rem;">
|
|
||||||
<td><span class="badge bg-purple-lt" x-text="o.name || ('Custom (' + o.legs.length + ' legs)')"></span></td>
|
|
||||||
<td class="text-secondary" style="font-family:'JetBrains Mono',monospace;font-size:.75rem;" x-text="legsSummary(o)"></td>
|
|
||||||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;" x-text="fmtMoney(orderPL(o).entered)"></td>
|
|
||||||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;" x-text="fmtMoney(orderPL(o).value)"></td>
|
|
||||||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;" :class="orderPL(o).pl >= 0 ? 'text-success' : 'text-danger'" x-text="fmtMoney(orderPL(o).pl)"></td>
|
|
||||||
<td class="text-end" style="font-family:'JetBrains Mono',monospace;" :class="orderPL(o).pl >= 0 ? 'text-success' : 'text-danger'" x-text="orderPL(o).plPct.toFixed(1) + '%'"></td>
|
|
||||||
<td class="small text-secondary" x-text="(o.created_at||'').slice(0,10)"></td>
|
|
||||||
<td><span class="badge" :class="o.status === 'open' ? 'bg-success-lt text-success' : 'bg-secondary-lt text-secondary'" x-text="o.status"></span></td>
|
|
||||||
<td><button class="btn btn-sm" :class="o.status === 'open' ? 'btn-outline-warning' : 'btn-outline-success'" @click="toggleCloseOrder(o)" x-text="o.status === 'open' ? 'Close' : 'Reopen'"></button></td>
|
|
||||||
<td><button class="btn btn-sm btn-ghost-danger" @click="removeOrder(o.id)" aria-label="Remove order">✕</button></td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
@@ -660,9 +646,7 @@
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: '',
|
error: '',
|
||||||
|
|
||||||
orders: [],
|
watchlist: [],
|
||||||
ordersLoading: false,
|
|
||||||
ordersChain: {}, // "SYMBOL@EXPIRY" -> { "strike@type": option }
|
|
||||||
|
|
||||||
_charts: { atmIv: null, rr25: null, fly25: null },
|
_charts: { atmIv: null, rr25: null, fly25: null },
|
||||||
|
|
||||||
@@ -676,96 +660,24 @@
|
|||||||
this.snapshots = vs.snapshots ?? [];
|
this.snapshots = vs.snapshots ?? [];
|
||||||
if (this.snapshots.length) this.$nextTick(() => this.renderCharts());
|
if (this.snapshots.length) this.$nextTick(() => this.renderCharts());
|
||||||
}
|
}
|
||||||
if (this.symbol) this.loadOrders();
|
this._loadWatchlist();
|
||||||
|
window.addEventListener('storage', (e) => { if (e.key === 'optionsPricer:watchlist') this._loadWatchlist(); });
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadOrders() {
|
_loadWatchlist() {
|
||||||
if (!this.symbol) return;
|
try { this.watchlist = JSON.parse(localStorage.getItem('optionsPricer:watchlist') || '[]'); }
|
||||||
this.ordersLoading = true;
|
catch { this.watchlist = []; }
|
||||||
try {
|
|
||||||
const r = await fetch('/api/orders?symbol=' + encodeURIComponent(this.symbol.trim().toUpperCase()));
|
|
||||||
const d = await r.json();
|
|
||||||
this.orders = d.data?.orders || [];
|
|
||||||
// fetch live chains for the unique expiries (one /api/chain call each)
|
|
||||||
const need = new Set();
|
|
||||||
for (const o of this.orders) for (const l of (o.legs||[])) if (l.expiry) need.add(o.symbol + '@' + l.expiry);
|
|
||||||
const cache = { ...this.ordersChain };
|
|
||||||
for (const key of need) {
|
|
||||||
if (cache[key]) continue;
|
|
||||||
const [sym, exp] = key.split('@');
|
|
||||||
try {
|
|
||||||
const cr = await fetch('/api/chain?symbol=' + encodeURIComponent(sym) + '&expiry=' + encodeURIComponent(exp));
|
|
||||||
if (!cr.ok) continue;
|
|
||||||
const ce = await cr.json();
|
|
||||||
const snap = ce.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.ordersChain = cache;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('loadOrders failed:', e);
|
|
||||||
} finally {
|
|
||||||
this.ordersLoading = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
orderPL(o) {
|
loadSymbol(s) {
|
||||||
let value = 0, entered = 0;
|
this.symbol = s;
|
||||||
for (const l of (o.legs||[])) {
|
this.fetchExpirations();
|
||||||
const sign = l.side === 'short' ? -1 : 1;
|
this.loadHistory();
|
||||||
entered += sign * l.qty * 100 * (l.entryPrice || 0);
|
|
||||||
const m = this.ordersChain[o.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 baseCost = (o.entry_cost != null) ? Number(o.entry_cost) : entered;
|
|
||||||
const pl = value - baseCost;
|
|
||||||
const plPct = Math.abs(baseCost) > 1e-6 ? (pl / Math.abs(baseCost)) * 100 : 0;
|
|
||||||
return { value, entered: baseCost, pl, plPct };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async toggleCloseOrder(o) {
|
removeWatch(s) {
|
||||||
const newStatus = o.status === 'open' ? 'closed' : 'open';
|
this.watchlist = this.watchlist.filter(x => x !== s);
|
||||||
try {
|
try { localStorage.setItem('optionsPricer:watchlist', JSON.stringify(this.watchlist)); } catch {}
|
||||||
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 (e) { /* silent */ }
|
|
||||||
},
|
|
||||||
|
|
||||||
async removeOrder(id) {
|
|
||||||
if (!confirm('Remove this tracked order?')) return;
|
|
||||||
try {
|
|
||||||
await fetch('/api/orders/' + id, { method:'DELETE' });
|
|
||||||
this.orders = this.orders.filter(o => o.id !== id);
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
|
|
||||||
legsSummary(o) {
|
|
||||||
return (o.legs||[]).map(l =>
|
|
||||||
(l.side === 'long' ? '+' : '-') + l.qty +
|
|
||||||
' ' + l.strike + (l.type === 'call' ? 'C' : 'P') +
|
|
||||||
'·' + (l.expiry || '').slice(5)
|
|
||||||
).join(' / ');
|
|
||||||
},
|
|
||||||
|
|
||||||
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 });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_persist() {
|
_persist() {
|
||||||
@@ -812,7 +724,6 @@
|
|||||||
}
|
}
|
||||||
this._persist();
|
this._persist();
|
||||||
this.$nextTick(() => this.renderCharts());
|
this.$nextTick(() => this.renderCharts());
|
||||||
this.loadOrders();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = e.message;
|
this.error = e.message;
|
||||||
this.snapshots = [];
|
this.snapshots = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user