/* ============================================================================= App — Options Scanner Cleaned-up version of the design's app.jsx: - Removed `TWEAK_DEFAULTS`, `useTweaks`, `applyDensity`, `applyAccent`, `ACCENTS` palette, and the entire tree. Those were the artifact's design-tool plumbing, not runtime UX. The mint accent and balanced density become the only built-in look. - `window.IDEAS` and `window.SCAN` are loaded from `data.js`, which `dashboard.py` writes alongside `index.html` on every build. ===========================================================================*/ const { useState, useEffect, useMemo, useRef } = React; // Per-column comparators for the click-to-sort headers. Each one returns // a positive/negative number per the standard JS sort contract; the App's // sort runner multiplies by ±1 for the desc/asc direction. Nulls are // pushed to the end of an ascending sort (so "missing" data isn't // accidentally ranked as "best" or "worst"). const SORT_COMPARATORS = { ticker: (a, b) => a.ticker.localeCompare(b.ticker), underlying: (a, b) => (a.underlying || 0) - (b.underlying || 0), strategy: (a, b) => a.strategy.localeCompare(b.strategy), exp: (a, b) => (a.exp || "").localeCompare(b.exp || ""), dte: (a, b) => (a.dte || 0) - (b.dte || 0), credit: (a, b) => (a.credit || 0) - (b.credit || 0), maxProfit: (a, b) => { // null = unbounded (LC). Treat as +Infinity so it sorts to the top // of a descending-by-profit view; the "Unlimited" label communicates this. const av = a.maxProfit === null || a.maxProfit === undefined ? Infinity : a.maxProfit; const bv = b.maxProfit === null || b.maxProfit === undefined ? Infinity : b.maxProfit; return av - bv; }, pop: (a, b) => (a.pop || 0) - (b.pop || 0), ivRank: (a, b) => (a.ivRank || 0) - (b.ivRank || 0), confluence: (a, b) => (a.confluence || 0) - (b.confluence || 0), }; // Default direction when a column is first clicked. Numeric/quantitative // columns default to desc (operator wants the highest first). String / // date columns default to asc (alphabetical / chronological). const SORT_DEFAULTS = { ticker: "asc", underlying: "desc", strategy: "asc", exp: "asc", dte: "asc", credit: "desc", maxProfit: "desc", pop: "desc", ivRank: "desc", confluence: "desc", }; function App() { const [theme, setTheme] = useState(() => { const saved = localStorage.getItem("os-theme"); if (saved) return saved; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; }); // Top-level tab: "ideas" (the original scanner) or "positions" (manual // open-position tracker). Persists across reloads via localStorage. const [tab, setTab] = useState(() => localStorage.getItem("os-tab") || "ideas"); useEffect(() => { localStorage.setItem("os-tab", tab); }, [tab]); // For the Add-position form: when an idea row's "Track" button is // clicked we pre-fill these fields. ``null`` = form is closed. const [addPositionPrefill, setAddPositionPrefill] = useState(null); // For mark-as-closed: id of the position being closed. ``null`` = // close modal not open. const [closingPositionId, setClosingPositionId] = useState(null); // For the position drawer (Pass 2): id of the position whose // detail panel is open. Null = drawer closed. const [selectedPositionId, setSelectedPositionId] = useState(null); const [activeStrat, setActiveStrat] = useState("ALL"); // Sort is column-aware so the table headers can act as the sort UI: // first click on a column sets its default direction (desc for numerics // where the operator wants high values first; asc for alpha/date), // second click on the same column toggles, click on a different column // resets to that column's default. const [sort, setSort] = useState({ column: "confluence", direction: "desc" }); const [view, setView] = useState("list"); const [query, setQuery] = useState(""); const [selectedId, setSelectedId] = useState(null); useEffect(() => { document.body.dataset.theme = theme; localStorage.setItem("os-theme", theme); }, [theme]); // Live refresh controller — talks to scripts/refresh_server.py. // Falls through silently if the dashboard is being served by a plain // static file server (e.g. python -m http.server) without the FastAPI // refresh API; buttons just won't do anything in that mode. const refresh = useRefreshController(); const ideas = useMemo(() => { let list = (window.IDEAS || []).slice(); if (activeStrat !== "ALL") list = list.filter(i => i.strategy === activeStrat); if (query.trim()) list = list.filter(i => i.ticker.toLowerCase().includes(query.trim().toLowerCase())); const cmp = SORT_COMPARATORS[sort.column]; if (cmp) { const sign = sort.direction === "desc" ? -1 : 1; list.sort((a, b) => sign * cmp(a, b)); } return list; }, [activeStrat, sort, query]); // Toggle direction if clicking the same column; otherwise switch to the // new column with its column-type default direction. Numerics start // descending (highest value first), alpha/date start ascending. const onSort = (column) => { setSort(prev => { if (prev.column === column) { return { column, direction: prev.direction === "desc" ? "asc" : "desc" }; } return { column, direction: SORT_DEFAULTS[column] || "desc" }; }); }; const selected = useMemo( () => (window.IDEAS || []).find(i => i.id === selectedId) || null, [selectedId] ); return ( <> setTheme(theme === "dark" ? "light" : "dark")} refresh={refresh} tab={tab} onTabChange={setTab} />
{tab === "history" ? ( ) : tab === "positions" ? ( setAddPositionPrefill({})} onCloseClick={(id) => setClosingPositionId(id)} onSelectPosition={(id) => setSelectedPositionId(id)} /> ) : ( <>
Top ideas
ranked by confluence — click any row for the full breakdown
{ideas.length === 0 ? (
No ideas match those filters.
) : view === "list" ? ( setAddPositionPrefill({ ticker: idea.ticker, strategy: idea.strategy.toLowerCase(), longK: idea.longK, shortK: idea.shortK, expiration: idea.exp, // Suggested entry: signed credit/debit. Idea's // `credit` is already signed (PCS/CSP positive, // LC/CDS negative). entry_price: idea.credit, })} /> ) : ( )} )}
read-only analysis · execution is manual · scan {(window.SCAN && window.SCAN.id) || ""}
{/* Modals + drawers are mounted at the App root so they overlay either tab. The position drawer slides in from the right; the modals are centered overlays. */} p.id === selectedPositionId) || null} onClose={() => setSelectedPositionId(null)} onCloseClick={(id) => { // Closing the position implicitly closes the drawer too. setSelectedPositionId(null); setClosingPositionId(id); }} /> {addPositionPrefill !== null && ( setAddPositionPrefill(null)} /> )} {closingPositionId !== null && ( setClosingPositionId(null)} /> )} setSelectedId(null)} /> ); } ReactDOM.createRoot(document.getElementById("root")).render();