diff --git a/arb/calc.py b/arb/calc.py index 577430232..93906d865 100644 --- a/arb/calc.py +++ b/arb/calc.py @@ -114,9 +114,20 @@ def evaluate(pair, kq, pq, settings, today=None): if not pair.get("poly_slug"): row["status"] = "NO PAIR" return row - if not kq or not pq or not ks or not ps: + # Surface which side is missing — bare "NO DATA" hid the real cause + # (commonly: Kalshi side liquid but the Polymarket slug doesn't resolve). + if not ks or not ps: row["status"] = "NO DATA" return row + if not kq and not pq: + row["status"] = "NO DATA" + return row + if not pq: + row["status"] = "NO POLY" + return row + if not kq: + row["status"] = "NO KALSHI" + return row kfee_rate = settings["kalshi_fee_rate"] pfee_rate = settings["poly_fee_rate"] diff --git a/arb/scan.py b/arb/scan.py index afe9b2383..d50f80fe1 100644 --- a/arb/scan.py +++ b/arb/scan.py @@ -87,7 +87,7 @@ def run_scan(force=False): summary = {"total": len(rows), "fetch_ms": fetch_ms} for st in ("ARB", "NO ARB", "BAD BASIS", "LOW SIZE", - "NO DATA", "NO PAIR"): + "NO DATA", "NO POLY", "NO KALSHI", "NO PAIR"): summary[st] = sum(1 for r in rows if r["status"] == st) payload = { diff --git a/docs/app.js b/docs/app.js index 86addccd6..16b15f2e1 100644 --- a/docs/app.js +++ b/docs/app.js @@ -34,9 +34,23 @@ const COLS_HOURLY = [ { k: "poly_volume", t: "P Vol", f: "money" }, { k: "status", t: "Status", align: "l", f: "status" }, ]; +const COLS_DAILY = [ + { k: "_mkt", t: "Market", align: "l", sort: "ibkr_label" }, + { k: "ibkr_strike", t: "IBKR Strike",f: "strike" }, + { k: "kalshi_strike", t: "K Strike", f: "strike" }, + { k: "basis_pct", t: "Basis", f: "pct" }, + { k: "best_side", t: "Side", align: "l" }, + { k: "_px", t: "K / I px", align: "l", sort: "combined_cost" }, + { k: "combined_cost", t: "Comb $", f: "px" }, + { k: "worst_pnl", t: "Min $/ct", f: "s4" }, + { k: "net_return", t: "Net Ret", f: "pctBig" }, + { k: "_settle", t: "Settle", align: "l" }, + { k: "status", t: "Status", align: "l", f: "status" }, +]; let MODE = "monthly", POS_VIEW = "venue"; -const COLS = () => (MODE === "hourly" ? COLS_HOURLY : COLS_MONTHLY); +const COLS = () => MODE === "daily" ? COLS_DAILY + : MODE === "hourly" ? COLS_HOURLY : COLS_MONTHLY; // Static snapshot URLs — no backend; cache-bust on each load so returning // users see fresh data after a GHA cron commit. const endpoint = () => { @@ -52,19 +66,6 @@ const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, const rid = (r) => (r.asset || "") + "|" + (r.kalshi_ticker || "") + "|" + (r.poly_slug || ""); -// "2026-05-21 16:15:07" (UTC, from GHA runner) -> "3 min ago" etc. -function ago(stamp) { - if (!stamp) return "—"; - const t = Date.parse(stamp.replace(" ", "T") + "Z"); - if (isNaN(t)) return stamp; - const s = Math.max(0, Math.round((Date.now() - t) / 1000)); - if (s < 45) return "just now"; - const m = Math.round(s / 60); - if (m < 60) return `${m} min ago`; - const h = Math.round(m / 60); - return `${h}h ago`; -} - function fmt(v, kind) { if (v === null || v === undefined || v === "") return '—'; @@ -102,26 +103,39 @@ function thumb(r, sz) { } function cellMarket(r) { - const title = r.kalshi_title || r.poly_question || + const title = r.ibkr_label || r.kalshi_title || r.poly_question || `${r.asset} ${r.direction} ${r.kalshi_strike ?? ""}`; - const strikes = `K $${r.kalshi_strike ?? "—"} · P $${r.poly_strike ?? "—"}`; + const secondLabel = (MODE === "daily") + ? `K $${r.kalshi_strike ?? "—"} · I $${r.ibkr_strike ?? "—"}` + : `K $${r.kalshi_strike ?? "—"} · P $${r.poly_strike ?? "—"}`; return `
data/ibkr_contracts.example.json → data/ibkr_contracts.json and fill in your daily-BTC conids. `;
+ } else if (reason === "gateway_not_running") {
+ titleEl.textContent = "IBKR Gateway not running.";
+ msgEl.innerHTML = ` Start the Client Portal Gateway on this Mac (default port 5000). Detail: ${esc(gw.message || "no connection")}. `;
+ } else if (payload.error) {
+ titleEl.textContent = "IBKR session needs re-auth.";
+ msgEl.innerHTML = ` ${esc(payload.error)} `;
+ } else if (reason && reason.startsWith("bad_json")) {
+ titleEl.textContent = "data/ibkr_contracts.json is invalid JSON.";
+ msgEl.innerHTML = ` ${esc(reason)} `;
+ } else {
+ titleEl.textContent = "IBKR not connected.";
+ msgEl.textContent = "";
+ }
+ $("ibkrSetup").hidden = false;
}
async function load(force) {
@@ -550,15 +596,31 @@ async function load(force) {
renderPositions(d);
$("meta").textContent =
`${d.generated_at} · ${d.totals.open_positions} open`;
+ } else if (MODE === "daily" && d.configured === false) {
+ RAW = [];
+ renderSummary({ total: 0 });
+ buildAssetChips();
+ renderHero(RAW);
+ renderBody();
+ showIbkrSetup(d);
+ $("meta").textContent = "IBKR not connected";
} else {
- RAW = d.rows;
- renderSummary(d.summary);
+ RAW = d.rows || [];
+ renderSummary(d.summary || { total: RAW.length });
buildAssetChips();
renderHero(RAW);
renderBody();
updateBanner(d.summary);
- const unit = MODE === "hourly" ? "assets" : "pairs";
- $("meta").textContent = `${ago(d.generated_at)} · ${d.summary.total} ${unit}`;
+ if (MODE === "daily") {
+ $("ibkrSetup").hidden = false;
+ $("ibkrSetupTitle").textContent = "IBKR connected.";
+ $("ibkrSetupMsg").innerHTML =
+ ' Local-only feature; updates as long as the Client Portal Gateway is running and authenticated. ';
+ }
+ const unit = MODE === "hourly" ? "assets"
+ : MODE === "daily" ? "pairs" : "pairs";
+ const ga = d.generated_at || "—";
+ $("meta").textContent = `${ga} · ${RAW.length} ${unit}`;
}
} catch (e) {
toast((MODE === "positions" ? "Load" : "Scan") + " failed: " + e.message, "err");
@@ -622,33 +684,28 @@ function wire() {
document.querySelector("#statusSeg button.on")?.classList.remove("on");
b.classList.add("on"); renderBody();
});
- // STATIC_MODE: no auto-refresh checkbox, no settings panel (DOM absent).
- if ($("auto")) {
- let timer = null;
- $("auto").onchange = (e) => {
- clearInterval(timer);
- if (e.target.checked) { load(true); timer = setInterval(() => load(true), 8000); }
+ let timer = null;
+ $("auto").onchange = (e) => {
+ clearInterval(timer);
+ if (e.target.checked) { load(true); timer = setInterval(() => load(true), 8000); }
+ };
+ $("settingsBtn").onclick = async () => {
+ await loadSettings(); $("settingsModal").hidden = false;
+ };
+ $("closeSettings").onclick = () => ($("settingsModal").hidden = true);
+ $("saveSettings").onclick = async () => {
+ const body = {
+ kalshi_fee_rate: +s_kfee.value, poly_fee_rate: +s_pfee.value,
+ min_net_return: +s_minret.value, min_poly_volume: +s_minvol.value,
+ min_contracts: +s_minct.value,
};
- }
- if ($("settingsBtn")) {
- $("settingsBtn").onclick = async () => {
- await loadSettings(); $("settingsModal").hidden = false;
- };
- $("closeSettings").onclick = () => ($("settingsModal").hidden = true);
- $("saveSettings").onclick = async () => {
- const body = {
- kalshi_fee_rate: +s_kfee.value, poly_fee_rate: +s_pfee.value,
- min_net_return: +s_minret.value, min_poly_volume: +s_minvol.value,
- min_contracts: +s_minct.value,
- };
- const r = await fetch("/api/settings", {
- method: "POST", headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
- if (r.ok) { $("settingsModal").hidden = true; toast("Saved — rescanning", "ok"); load(true); }
- else toast("Save failed", "err");
- };
- }
+ const r = await fetch("/api/settings", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+ if (r.ok) { $("settingsModal").hidden = true; toast("Saved — rescanning", "ok"); load(true); }
+ else toast("Save failed", "err");
+ };
}
renderHead();
diff --git a/docs/index.html b/docs/index.html
index f187d5604..e6627dc36 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -17,7 +17,7 @@