mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-24 04:58:08 +00:00
Surface shared state provider failures
This commit is contained in:
@@ -15,12 +15,12 @@ https://polymarket-site-eta.vercel.app/personal.html
|
|||||||
|
|
||||||
The site fetches live Polymarket markets, generates agent suggestions, lets you
|
The site fetches live Polymarket markets, generates agent suggestions, lets you
|
||||||
run frequent paper cycles, and syncs the shared arena state through Neon or
|
run frequent paper cycles, and syncs the shared arena state through Neon or
|
||||||
Vercel Blob. Build 59 also installs an offline app shell and caches timestamped
|
Vercel Blob. Build 60 also installs an offline app shell and caches timestamped
|
||||||
market snapshots. During an outage, cycles continue locally; cached entries are
|
market snapshots. During an outage, cycles continue locally; cached entries are
|
||||||
allowed for 90 minutes, older snapshots become mark-only, and all cached data
|
allowed for 90 minutes, older snapshots become mark-only, and all cached data
|
||||||
expires after 24 hours.
|
expires after 24 hours.
|
||||||
|
|
||||||
Build 59 ranks the competition by each agent's return since Strategy 50 began.
|
Build 60 ranks the competition by each agent's return since Strategy 50 began.
|
||||||
Historical replay equity remains visible for context, but it no longer makes an
|
Historical replay equity remains visible for context, but it no longer makes an
|
||||||
agent look like the current leader when the live adaptive strategy is losing.
|
agent look like the current leader when the live adaptive strategy is losing.
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ feature views before repeatable positive evidence can increase size or repeatabl
|
|||||||
negative evidence can block a new entry. Mixed evidence stays close to neutral
|
negative evidence can block a new entry. Mixed evidence stays close to neutral
|
||||||
instead of being mistaken for an edge.
|
instead of being mistaken for an edge.
|
||||||
|
|
||||||
Build 59 enforces the documented offline boundary end to end. Cached snapshots
|
Build 60 enforces the documented offline boundary end to end. Cached snapshots
|
||||||
under 90 minutes old may continue paper execution. Older snapshots remain usable
|
under 90 minutes old may continue paper execution. Older snapshots remain usable
|
||||||
for valuation and chart snapshots for up to 24 hours, but cannot trigger entries,
|
for valuation and chart snapshots for up to 24 hours, but cannot trigger entries,
|
||||||
stop-losses, gain-stops, risk rebalances, settlements, or policy exits. Network
|
stop-losses, gain-stops, risk rebalances, settlements, or policy exits. Network
|
||||||
@@ -127,18 +127,24 @@ adaptive baselines, pending signal grades, and trade evidence remain in one stra
|
|||||||
lineage until the actual entry, sizing, or exit logic changes. Legacy build 40 and 41
|
lineage until the actual entry, sizing, or exit logic changes. Legacy build 40 and 41
|
||||||
records are migrated into the same strategy lineage without losing evidence.
|
records are migrated into the same strategy lineage without losing evidence.
|
||||||
|
|
||||||
Build 59 independently refreshes markets for matured pending signals that have
|
Build 60 independently refreshes markets for matured pending signals that have
|
||||||
left the current top-500 activity scan. Unavailable markets remain queued for a
|
left the current top-500 activity scan. Unavailable markets remain queued for a
|
||||||
bounded retry window. This prevents activity-rank survivorship from deciding
|
bounded retry window. This prevents activity-rank survivorship from deciding
|
||||||
which wins and losses reach the adaptive calibration ledger.
|
which wins and losses reach the adaptive calibration ledger.
|
||||||
|
|
||||||
Build 59 also allocates the 300 pending observation slots by evidence coverage.
|
Build 60 also allocates the 300 pending observation slots by evidence coverage.
|
||||||
Under-sampled signal/side/category cohorts are observed first, followed by
|
Under-sampled signal/side/category cohorts are observed first, followed by
|
||||||
under-sampled independent events and market sides, with conviction used only as
|
under-sampled independent events and market sides, with conviction used only as
|
||||||
a later tie-breaker. This prevents the same popular contracts from monopolizing
|
a later tie-breaker. This prevents the same popular contracts from monopolizing
|
||||||
the ledger and gives the learner a realistic path to promote or reject more
|
the ledger and gives the learner a realistic path to promote or reject more
|
||||||
diverse cohorts.
|
diverse cohorts.
|
||||||
|
|
||||||
|
Build 60 retains safe shared-state provider diagnostics from both reads and
|
||||||
|
writes. A device now says `local only` when Neon is paused or a Blob credential
|
||||||
|
is rejected, instead of presenting a local browser save as a successful
|
||||||
|
cross-device sync. Database URLs still fail over across the configured Neon
|
||||||
|
aliases without exposing credentials in the API response.
|
||||||
|
|
||||||
Strategy 50 coordinates high-risk exploration globally. Near-term, extreme-price,
|
Strategy 50 coordinates high-risk exploration globally. Near-term, extreme-price,
|
||||||
and other gap-prone positions may be held materially by only one agent, while
|
and other gap-prone positions may be held materially by only one agent, while
|
||||||
ordinary independently confirmed markets retain the two-agent cap. The robustly
|
ordinary independently confirmed markets retain the two-agent cap. The robustly
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function providerErrorCode(error) {
|
|||||||
const nested = error.sourceError || error.cause;
|
const nested = error.sourceError || error.cause;
|
||||||
const code = String(error.code || nested && (nested.code || nested.cause && nested.cause.code) || "").toUpperCase();
|
const code = String(error.code || nested && (nested.code || nested.cause && nested.cause.code) || "").toUpperCase();
|
||||||
const message = `${String(error.message || error)} ${String(nested && nested.message || "")}`.toLowerCase();
|
const message = `${String(error.message || error)} ${String(nested && nested.message || "")}`.toLowerCase();
|
||||||
|
if (/http status\s+402\b/.test(message) || /\b402 payment required\b/.test(message)) return "provider_payment_required";
|
||||||
if (code === "28P01" || code === "28000") return "authorization_failed";
|
if (code === "28P01" || code === "28000") return "authorization_failed";
|
||||||
if (code === "3D000") return "database_not_found";
|
if (code === "3D000") return "database_not_found";
|
||||||
if (code.startsWith("08")) return "connection_failed";
|
if (code.startsWith("08")) return "connection_failed";
|
||||||
|
|||||||
+44
-13
@@ -363,7 +363,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
<div class="personal-banner" id="personalBanner">
|
<div class="personal-banner" id="personalBanner">
|
||||||
<b>Personal research mode.</b> This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders without your manual approval.
|
<b>Personal research mode.</b> This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders without your manual approval.
|
||||||
</div>
|
</div>
|
||||||
<div class="live-build-banner"><b>Build 59 active:</b> unproven directional signals train the event-clustered learner without risking cash. New observations prioritize under-sampled strategy cohorts and independent events before repeats, while shared state normalizes and fails over across configured Neon URLs. Cohorts can trade only after positive promotion. Live-priced complete YES or NO negative-risk bundles may trade when their worst-case payout remains positive after estimated costs; cached bundle prices never open positions. This remains paper trading; profits are not guaranteed.</div>
|
<div class="live-build-banner"><b>Build 60 active:</b> unproven directional signals train the event-clustered learner without risking cash. New observations prioritize under-sampled strategy cohorts and independent events before repeats, while shared state normalizes and fails over across configured Neon URLs. Cloud provider failures now remain visible instead of being mistaken for a successful cross-device save. Cohorts can trade only after positive promotion. Live-priced complete YES or NO negative-risk bundles may trade when their worst-case payout remains positive after estimated costs; cached bundle prices never open positions. This remains paper trading; profits are not guaranteed.</div>
|
||||||
|
|
||||||
<!-- ============ OVERVIEW ============ -->
|
<!-- ============ OVERVIEW ============ -->
|
||||||
<section class="tabpanel" data-tab="overview">
|
<section class="tabpanel" data-tab="overview">
|
||||||
@@ -745,7 +745,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Build 59 · Adaptive strategy 50 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
|
Build 60 · Adaptive strategy 50 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
|
||||||
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
|
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -774,7 +774,7 @@ const POLITICS_TREND_MIN_HOLD_HOURS = 72;
|
|||||||
const EXIT_CONFIRM_HOURS = 6;
|
const EXIT_CONFIRM_HOURS = 6;
|
||||||
const AGENTS_KEY = "pma_agents_v2";
|
const AGENTS_KEY = "pma_agents_v2";
|
||||||
const SUG_KEY = "pma_suggestions_v5";
|
const SUG_KEY = "pma_suggestions_v5";
|
||||||
const BUILD_VERSION = 59;
|
const BUILD_VERSION = 60;
|
||||||
const SUGGESTION_ENGINE_VERSION = 50;
|
const SUGGESTION_ENGINE_VERSION = 50;
|
||||||
const PREVIOUS_STRATEGY_VERSION = 49;
|
const PREVIOUS_STRATEGY_VERSION = 49;
|
||||||
const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40});
|
const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40});
|
||||||
@@ -1545,6 +1545,22 @@ function stateFromSyncItems(items){
|
|||||||
try{return items&&items[AGENTS_KEY]?JSON.parse(items[AGENTS_KEY]):null;}
|
try{return items&&items[AGENTS_KEY]?JSON.parse(items[AGENTS_KEY]):null;}
|
||||||
catch(e){return null;}
|
catch(e){return null;}
|
||||||
}
|
}
|
||||||
|
let CLOUD_STATE_HEALTH={ok:null,degraded:false,providers:null,error:null};
|
||||||
|
window.PMA_CLOUD_STATE_HEALTH=CLOUD_STATE_HEALTH;
|
||||||
|
function rememberCloudHealth(next){
|
||||||
|
CLOUD_STATE_HEALTH={...CLOUD_STATE_HEALTH,...next};
|
||||||
|
window.PMA_CLOUD_STATE_HEALTH=CLOUD_STATE_HEALTH;
|
||||||
|
return CLOUD_STATE_HEALTH;
|
||||||
|
}
|
||||||
|
function localOnlyStatus(base,providerOverride=null){
|
||||||
|
const providers=providerOverride||CLOUD_STATE_HEALTH.providers||{};
|
||||||
|
const dbStatus=providers.database&&providers.database.status;
|
||||||
|
const blobStatus=providers.blob&&providers.blob.status;
|
||||||
|
if(dbStatus==="provider_payment_required")return `${base} · local only (Neon paused)`;
|
||||||
|
if(blobStatus==="authorization_failed")return `${base} · local only (cloud token invalid)`;
|
||||||
|
return `${base} · saved locally`;
|
||||||
|
}
|
||||||
|
window.PMA_CLOUD_STATUS_LABEL=localOnlyStatus;
|
||||||
async function pushCloudState(force=false){
|
async function pushCloudState(force=false){
|
||||||
try{
|
try{
|
||||||
const r=await fetchWithTimeout("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems(),force})},NETWORK_REQUEST_TIMEOUT_MS);
|
const r=await fetchWithTimeout("/api/state",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({items:collectSyncItems(),force})},NETWORK_REQUEST_TIMEOUT_MS);
|
||||||
@@ -1556,17 +1572,29 @@ async function pushCloudState(force=false){
|
|||||||
renderAll();
|
renderAll();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(!r.ok)throw new Error("cloud sync unavailable");
|
const d=await r.json().catch(()=>null);
|
||||||
|
if(!r.ok){
|
||||||
|
rememberCloudHealth({ok:false,degraded:true,providers:d&&d.providers||null,error:d&&d.error||"Cloud sync unavailable"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rememberCloudHealth({ok:true,degraded:false,providers:null,error:null});
|
||||||
return true;
|
return true;
|
||||||
}catch(e){return false;}
|
}catch(e){rememberCloudHealth({ok:false,degraded:true,error:"Cloud sync unavailable"});return false;}
|
||||||
}
|
}
|
||||||
async function pullCloudState(){
|
async function pullCloudState(){
|
||||||
try{
|
try{
|
||||||
const r=await fetchWithTimeout("/api/state",{cache:"no-store"},NETWORK_REQUEST_TIMEOUT_MS);
|
const r=await fetchWithTimeout("/api/state",{cache:"no-store"},NETWORK_REQUEST_TIMEOUT_MS);
|
||||||
if(!r.ok)return {ok:false,state:null};
|
if(!r.ok){
|
||||||
|
const d=await r.json().catch(()=>null);
|
||||||
|
rememberCloudHealth({ok:false,degraded:true,providers:d&&d.providers||null,error:d&&d.error||"Cloud sync unavailable"});
|
||||||
|
return {ok:false,state:null,degraded:true,providers:d&&d.providers||null,error:d&&d.error};
|
||||||
|
}
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
return {ok:true,state:(d&&d.state)||null,degraded:Boolean(d&&d.degraded),error:d&&d.error};
|
const result={ok:true,state:(d&&d.state)||null,degraded:Boolean(d&&d.degraded),error:d&&d.error,providers:d&&d.providers||null};
|
||||||
|
rememberCloudHealth(result);
|
||||||
|
return result;
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
|
rememberCloudHealth({ok:false,degraded:true,error:"Cloud sync unavailable"});
|
||||||
return {ok:false,state:null};
|
return {ok:false,state:null};
|
||||||
}
|
}
|
||||||
async function loadAuthoritativeCloudState(){
|
async function loadAuthoritativeCloudState(){
|
||||||
@@ -1575,9 +1603,9 @@ async function loadAuthoritativeCloudState(){
|
|||||||
const cloudState=cloud.state;
|
const cloudState=cloud.state;
|
||||||
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
|
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
|
||||||
applySyncItems(cloudState.items);
|
applySyncItems(cloudState.items);
|
||||||
return {ok:true,loaded:true,degraded:Boolean(cloud.degraded)};
|
return {ok:true,loaded:true,degraded:Boolean(cloud.degraded),providers:cloud.providers||null};
|
||||||
}
|
}
|
||||||
return {ok:true,loaded:false,degraded:Boolean(cloud.degraded)};
|
return {ok:true,loaded:false,degraded:Boolean(cloud.degraded),providers:cloud.providers||null};
|
||||||
}
|
}
|
||||||
async function refreshFromCloudAndRender(silent=true){
|
async function refreshFromCloudAndRender(silent=true){
|
||||||
const cloud=await loadAuthoritativeCloudState();
|
const cloud=await loadAuthoritativeCloudState();
|
||||||
@@ -1585,6 +1613,9 @@ async function refreshFromCloudAndRender(silent=true){
|
|||||||
renderAll();
|
renderAll();
|
||||||
setStatus("cloud synced",false);
|
setStatus("cloud synced",false);
|
||||||
if(!silent)toast("Shared cloud state refreshed.");
|
if(!silent)toast("Shared cloud state refreshed.");
|
||||||
|
}else if(cloud.degraded){
|
||||||
|
setStatus(localOnlyStatus("cloud storage paused"),false);
|
||||||
|
if(!silent)toast("Cloud storage is unavailable. This device is using its local backup.");
|
||||||
}else if(!cloud.ok){
|
}else if(!cloud.ok){
|
||||||
setStatus("cloud sync unavailable",false);
|
setStatus("cloud sync unavailable",false);
|
||||||
if(!silent)toast("Cloud sync is unavailable. Results may be stale until it reconnects.");
|
if(!silent)toast("Cloud sync is unavailable. Results may be stale until it reconnects.");
|
||||||
@@ -2714,7 +2745,7 @@ async function runDailyCycle(){
|
|||||||
saveState(st);
|
saveState(st);
|
||||||
const pushed=cloud.ok?await pushCloudState():false;
|
const pushed=cloud.ok?await pushCloudState():false;
|
||||||
if(pushed){st.pending_sync=false;saveState(st);setStatus(runMode==="live"?"up to date":(executeTrades?"offline cycle synced":"offline mark-only synced"),false);await notifyTradeDigest(emailEvents);}
|
if(pushed){st.pending_sync=false;saveState(st);setStatus(runMode==="live"?"up to date":(executeTrades?"offline cycle synced":"offline mark-only synced"),false);await notifyTradeDigest(emailEvents);}
|
||||||
else{st.pending_sync=true;saveState(st);setStatus(`${runMode==="live"?"live data":(executeTrades?"offline cycle":"offline mark-only")} · saved locally`,false);}
|
else{st.pending_sync=true;saveState(st);setStatus(localOnlyStatus(runMode==="live"?"live data":(executeTrades?"offline cycle":"offline mark-only")),false);}
|
||||||
return {suggestions:sugs.length,offline:runMode!=="live",pendingSync:!pushed,entriesAllowed,executeTrades};
|
return {suggestions:sugs.length,offline:runMode!=="live",pendingSync:!pushed,entriesAllowed,executeTrades};
|
||||||
}finally{SNAP_TS=null;CYCLE_RUNNING=false;}
|
}finally{SNAP_TS=null;CYCLE_RUNNING=false;}
|
||||||
}
|
}
|
||||||
@@ -5006,10 +5037,10 @@ if("serviceWorker" in navigator){navigator.serviceWorker.register("/sw.js").catc
|
|||||||
let st=loadState();
|
let st=loadState();
|
||||||
if(cloud.loaded){
|
if(cloud.loaded){
|
||||||
toast("Cloud state loaded for this device.");
|
toast("Cloud state loaded for this device.");
|
||||||
}else if(cloud.ok&&st.seeded){
|
}else if(cloud.ok&&!cloud.degraded&&st.seeded){
|
||||||
pushCloudState();
|
pushCloudState();
|
||||||
}else if(!cloud.ok){
|
}else if(cloud.degraded||!cloud.ok){
|
||||||
setStatus("offline-ready · local state",false);
|
setStatus(localOnlyStatus("offline-ready"),false);
|
||||||
}
|
}
|
||||||
renderAll();
|
renderAll();
|
||||||
const btn=$("runBtn");
|
const btn=$("runBtn");
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const compactedState = compactAgentState({
|
|||||||
});
|
});
|
||||||
assert.equal(compactedState.signal_ledger.expired_ungraded, 7);
|
assert.equal(compactedState.signal_ledger.expired_ungraded, 7);
|
||||||
assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed");
|
assert.equal(providerErrorCode(new Error("403 Forbidden")), "authorization_failed");
|
||||||
|
assert.equal(providerErrorCode(new Error("Error connecting to database: HTTP status 402")), "provider_payment_required");
|
||||||
assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string");
|
assert.equal(providerErrorCode(new Error("invalid connection string")), "invalid_connection_string");
|
||||||
assert.equal(providerErrorCode(Object.assign(new Error("database rejected login"), { code: "28P01" })), "authorization_failed");
|
assert.equal(providerErrorCode(Object.assign(new Error("database rejected login"), { code: "28P01" })), "authorization_failed");
|
||||||
const nestedFetchError = Object.assign(new Error("Error connecting to database"), {
|
const nestedFetchError = Object.assign(new Error("Error connecting to database"), {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = "polymarket-arena-build-59";
|
const CACHE_NAME = "polymarket-arena-build-60";
|
||||||
const APP_SHELL = ["/", "/index.html", "/personal.html", "/cycle-worker.js"];
|
const APP_SHELL = ["/", "/index.html", "/personal.html", "/cycle-worker.js"];
|
||||||
|
|
||||||
self.addEventListener("install", event => {
|
self.addEventListener("install", event => {
|
||||||
|
|||||||
Reference in New Issue
Block a user