Files
options-pricer/frontend/assets/settings-store.js
ojy 9de7f14573 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>
2026-05-13 07:04:52 +00:00

83 lines
2.8 KiB
JavaScript

/**
* 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 };
})();