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