130 lines
3.9 KiB
JavaScript
130 lines
3.9 KiB
JavaScript
|
|
/**
|
||
|
|
* 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
|
||
|
|
|
||
|
|
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
|
||
|
|
obj.legs = obj.legs.filter(
|
||
|
|
(l) => l && Number.isFinite(Number(l.strike)) && (l.type === "call" || l.type === "put")
|
||
|
|
);
|
||
|
|
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: strike,
|
||
|
|
side: leg.side === "short" ? "short" : "long",
|
||
|
|
qty: Math.max(1, Math.round(Number(leg.qty) || 1)),
|
||
|
|
entryPrice: Number(leg.entryPrice) || 0,
|
||
|
|
iv: Number(leg.iv) || 0,
|
||
|
|
});
|
||
|
|
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) Object.assign(leg, patch);
|
||
|
|
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,
|
||
|
|
};
|
||
|
|
})();
|