Files
options-pricer/frontend/assets/strategy-store.js

149 lines
4.7 KiB
JavaScript
Raw Normal View History

/**
* Strategy basket persistence localStorage-backed.
* Exposed as window.StrategyStore. No modules, no deps.
*
* Stored shape (key "optionsPricer:strategy"):
* {
* v: 1,
* symbol: "SPY",
* spotSnapshot: 738.18, // spot when last leg was added (reference only)
* updatedAt: "2026-05-13T...Z",
* legs: [
* { id, symbol, expiry:"YYYY-MM-DD", type:"call"|"put",
* strike, side:"long"|"short", qty, entryPrice, iv }
* ]
* }
*/
(function () {
"use strict";
const KEY = "optionsPricer:strategy";
const VERSION = 1;
const MULTIPLIER = 100; // standard equity option contract size
/** Round a price to 2 decimals (kills float artifacts like 15.000000001). */
function round2(v) {
const n = Number(v);
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
}
function empty() {
return { v: VERSION, symbol: "", spotSnapshot: 0, updatedAt: null, legs: [] };
}
function load() {
try {
const raw = localStorage.getItem(KEY);
if (!raw) return empty();
const obj = JSON.parse(raw);
if (!obj || obj.v !== VERSION || !Array.isArray(obj.legs)) return empty();
// sanitize: drop legs with no usable strike / type; round prices to 2dp (self-heal)
obj.legs = obj.legs
.filter((l) => l && Number.isFinite(Number(l.strike)) && (l.type === "call" || l.type === "put"))
.map((l) => ({
...l,
strike: round2(l.strike),
entryPrice: round2(l.entryPrice),
currentMark: (typeof l.currentMark === "number" ? round2(l.currentMark) : (l.currentMark ?? null)),
}));
return obj;
} catch {
return empty();
}
}
function save(state) {
const s = state || empty();
s.v = VERSION;
s.updatedAt = new Date().toISOString();
try {
localStorage.setItem(KEY, JSON.stringify(s));
} catch (e) {
console.warn("[StrategyStore] save failed:", e);
}
return s;
}
function clear() {
try { localStorage.removeItem(KEY); } catch {}
return empty();
}
/**
* Add a leg. `leg` should provide: symbol, expiry, type, strike, side, qty, entryPrice, iv.
* Returns { state, replacedSymbol } replacedSymbol is the old symbol if the basket
* was wiped due to a symbol mismatch (caller should have confirmed first via mismatch()).
*/
function addLeg(leg) {
const strike = Number(leg && leg.strike);
if (!Number.isFinite(strike) || strike <= 0) {
console.warn("[StrategyStore] addLeg: invalid strike", leg);
return { state: load(), replacedSymbol: null };
}
let state = load();
let replacedSymbol = null;
if (state.legs.length > 0 && state.symbol && leg.symbol && state.symbol !== leg.symbol) {
replacedSymbol = state.symbol;
state = empty();
}
state.symbol = leg.symbol || state.symbol || "";
if (typeof leg.spotSnapshot === "number" && leg.spotSnapshot > 0) {
state.spotSnapshot = leg.spotSnapshot;
}
state.legs.push({
id: ((typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : "id-" + Date.now() + "-" + Math.random().toString(36).slice(2)),
symbol: leg.symbol || state.symbol,
expiry: leg.expiry,
type: leg.type,
strike: round2(strike),
side: leg.side === "short" ? "short" : "long",
qty: Math.max(1, Math.round(Number(leg.qty) || 1)),
entryPrice: round2(leg.entryPrice),
iv: Number(leg.iv) || 0,
locked: leg.locked === true,
currentMark: (typeof leg.currentMark === "number" ? round2(leg.currentMark) : null),
});
save(state);
return { state, replacedSymbol };
}
function removeLeg(id) {
const state = load();
state.legs = state.legs.filter((l) => l.id !== id);
if (state.legs.length === 0) { return clear(); }
save(state);
return state;
}
function updateLeg(id, patch) {
const state = load();
const leg = state.legs.find((l) => l.id === id);
if (leg) {
const p = { ...patch };
if ("entryPrice" in p) p.entryPrice = round2(p.entryPrice);
if ("currentMark" in p && p.currentMark != null) p.currentMark = round2(p.currentMark);
if ("strike" in p) p.strike = round2(p.strike);
Object.assign(leg, p);
}
save(state);
return state;
}
/** True if adding a leg of `symbol` would wipe an existing different-symbol basket. */
function mismatch(symbol) {
const state = load();
return state.legs.length > 0 && state.symbol && symbol && state.symbol !== symbol
? state.symbol
: null;
}
function count() {
return load().legs.length;
}
window.StrategyStore = {
KEY, VERSION, MULTIPLIER,
empty, load, save, clear, addLeg, removeLeg, updateLeg, mismatch, count,
};
})();