Diversify adaptive evidence and repair state failover

This commit is contained in:
Theodore Song
2026-08-19 08:26:13 -04:00
parent 0dbc6bf650
commit 78b832f926
7 changed files with 206 additions and 54 deletions
+18 -5
View File
@@ -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 57 also installs an offline app shell and caches timestamped Vercel Blob. Build 58 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 57 ranks the competition by each agent's return since Strategy 50 began. Build 58 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 57 enforces the documented offline boundary end to end. Cached snapshots Build 58 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,11 +127,18 @@ 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 57 independently refreshes markets for matured pending signals that have Build 58 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 58 also allocates the 300 pending observation slots by evidence coverage.
Under-sampled signal/side/category cohorts are observed first, followed by
under-sampled independent events and market sides, with conviction used only as
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
diverse cohorts.
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
@@ -225,7 +232,13 @@ Pick one — all give you a public URL:
Use `.env.example` as the setup template. Use `.env.example` as the setup template.
- `DATABASE_URL` or `NEON_DATABASE_URL` enables Neon-backed shared state; - `DATABASE_URL` or `NEON_DATABASE_URL` enables Neon-backed shared state;
`BLOB_READ_WRITE_TOKEN` is the fallback provider. `BLOB_READ_WRITE_TOKEN` is the fallback provider. The backend accepts raw
Postgres URLs, quoted URLs, `DATABASE_URL=...`, and Neon dashboard
`psql 'postgresql://...'` copy formats, and prefers a syntactically valid
alias if the primary value is malformed. Shared-state reads and writes also
fail over across distinct configured database URLs when one has stale
credentials or points at an empty project. `/api/state` reports only
sanitized provider error codes when all stores are unavailable.
- `ACCOUNT_SESSION_SECRET` signs cloud paper-account sessions. If omitted, the - `ACCOUNT_SESSION_SECRET` signs cloud paper-account sessions. If omitted, the
app falls back to the existing server secret/token, but production should use app falls back to the existing server secret/token, but production should use
a dedicated value. a dedicated value.
+71 -33
View File
@@ -1,11 +1,31 @@
import { neon } from "@neondatabase/serverless"; import { neon } from "@neondatabase/serverless";
let sqlClient; const sqlClients = new Map();
let schemaReady; let schemaReady;
let sharedStateSchemaReady; const sharedStateSchemasReady = new Set();
export function normalizeDatabaseUrl(raw) {
let value = String(raw || "").trim();
const assignment = value.match(/^(?:DATABASE_URL|NEON_DATABASE_URL)\s*=\s*([\s\S]+)$/i);
if (assignment) value = assignment[1].trim();
const psql = value.match(/^psql\s+(['"])([\s\S]+)\1\s*$/i);
if (psql) value = psql[2].trim();
else if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
value = value.slice(1, -1).trim();
}
return value;
}
export function databaseUrls() {
const candidates = [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]
.map(normalizeDatabaseUrl).filter(Boolean);
const ordered = [...candidates.filter(value => /^postgres(?:ql)?:\/\//i.test(value)),
...candidates.filter(value => !/^postgres(?:ql)?:\/\//i.test(value))];
return [...new Set(ordered)];
}
export function databaseUrl() { export function databaseUrl() {
return process.env.DATABASE_URL || process.env.NEON_DATABASE_URL || ""; return databaseUrls()[0] || "";
} }
export function hasDatabase() { export function hasDatabase() {
@@ -13,17 +33,18 @@ export function hasDatabase() {
} }
export function sql() { export function sql() {
if (!sqlClient) { const url = databaseUrl();
const url = databaseUrl(); if (!url) throw new Error("Database is not configured");
if (!url) throw new Error("Database is not configured"); return sqlForUrl(url);
sqlClient = neon(url);
}
return sqlClient;
} }
async function ensureSharedStateSchema() { function sqlForUrl(url) {
if (sharedStateSchemaReady) return; if (!sqlClients.has(url)) sqlClients.set(url, neon(url));
const db = sql(); return sqlClients.get(url);
}
async function ensureSharedStateSchema(db, url) {
if (sharedStateSchemasReady.has(url)) return;
await db` await db`
create table if not exists shared_app_state ( create table if not exists shared_app_state (
state_key text primary key, state_key text primary key,
@@ -31,33 +52,50 @@ async function ensureSharedStateSchema() {
updated_at timestamptz not null default now() updated_at timestamptz not null default now()
) )
`; `;
sharedStateSchemaReady = true; sharedStateSchemasReady.add(url);
}
async function withSharedStateDatabase(operation, acceptResult = () => true) {
let lastError, lastResult, hadSuccess = false;
for (const url of databaseUrls()) {
try {
const db = sqlForUrl(url);
await ensureSharedStateSchema(db, url);
lastResult = await operation(db);
hadSuccess = true;
if (acceptResult(lastResult)) return lastResult;
} catch (error) {
lastError = error;
}
}
if (hadSuccess) return lastResult;
throw lastError || new Error("Database is not configured");
} }
export async function readSharedAppState(stateKey) { export async function readSharedAppState(stateKey) {
await ensureSharedStateSchema(); return withSharedStateDatabase(async db => {
const db = sql(); const rows = await db`
const rows = await db` select payload, updated_at
select payload, updated_at from shared_app_state
from shared_app_state where state_key = ${stateKey}
where state_key = ${stateKey} limit 1
limit 1 `;
`; return rows[0] || null;
return rows[0] || null; }, result => result !== null);
} }
export async function writeSharedAppState(stateKey, payload) { export async function writeSharedAppState(stateKey, payload) {
await ensureSharedStateSchema(); return withSharedStateDatabase(async db => {
const db = sql(); const rows = await db`
const rows = await db` insert into shared_app_state (state_key, payload, updated_at)
insert into shared_app_state (state_key, payload, updated_at) values (${stateKey}, ${JSON.stringify(payload)}::jsonb, now())
values (${stateKey}, ${JSON.stringify(payload)}::jsonb, now()) on conflict (state_key) do update set
on conflict (state_key) do update set payload = excluded.payload,
payload = excluded.payload, updated_at = now()
updated_at = now() returning updated_at
returning updated_at `;
`; return rows[0] || null;
return rows[0] || null; });
} }
export async function ensureSchema() { export async function ensureSchema() {
+34 -5
View File
@@ -17,14 +17,27 @@ function withBlobAuth(options = {}) {
return token ? { ...options, token } : options; return token ? { ...options, token } : options;
} }
export function providerErrorCode(error) {
if (!error) return "not_configured_or_not_attempted";
const message = String(error.message || error).toLowerCase();
if (message.includes("invalid") && (message.includes("url") || message.includes("connection string"))) return "invalid_connection_string";
if (message.includes("password authentication") || message.includes("unauthorized") || /\b(?:401|403)\b/.test(message)) return "authorization_failed";
if (message.includes("enotfound") || message.includes("getaddrinfo") || message.includes("dns")) return "dns_failed";
if (message.includes("timeout") || message.includes("timed out")) return "timeout";
if (message.includes("fetch failed") || message.includes("connection") || message.includes("connect")) return "connection_failed";
return "unavailable";
}
async function readJsonBlob() { async function readJsonBlob() {
let databaseAvailable = false; let databaseAvailable = false;
let databaseError = null;
if (hasDatabase()) { if (hasDatabase()) {
try { try {
const row = await readSharedAppState(DATABASE_STATE_KEY); const row = await readSharedAppState(DATABASE_STATE_KEY);
databaseAvailable = true; databaseAvailable = true;
if (row && row.payload) return row.payload; if (row && row.payload) return row.payload;
} catch { } catch (err) {
databaseError = err;
databaseAvailable = false; databaseAvailable = false;
} }
} }
@@ -44,7 +57,14 @@ async function readJsonBlob() {
if (!primaryError) primaryError = err; if (!primaryError) primaryError = err;
} }
if (primaryError && !databaseAvailable) throw primaryError; if (primaryError && !databaseAvailable) {
const error = new Error("Shared state providers are unavailable");
error.providers = {
database: hasDatabase() ? providerErrorCode(databaseError) : "not_configured",
blob: providerErrorCode(primaryError),
};
throw error;
}
return null; return null;
} }
@@ -155,7 +175,7 @@ function compactPortfolio(p) {
return out; return out;
} }
function compactAgentState(st) { export function compactAgentState(st) {
if (!st || typeof st !== "object") return st; if (!st || typeof st !== "object") return st;
const out = { ...st, agents: {} }; const out = { ...st, agents: {} };
for (const id of AGENT_IDS) { for (const id of AGENT_IDS) {
@@ -165,6 +185,7 @@ function compactAgentState(st) {
out.signal_ledger = { out.signal_ledger = {
pending: Array.isArray(st.signal_ledger.pending) ? st.signal_ledger.pending.slice(-SIGNAL_LEDGER_LIMITS.pending) : [], pending: Array.isArray(st.signal_ledger.pending) ? st.signal_ledger.pending.slice(-SIGNAL_LEDGER_LIMITS.pending) : [],
outcomes: Array.isArray(st.signal_ledger.outcomes) ? st.signal_ledger.outcomes.slice(-SIGNAL_LEDGER_LIMITS.outcomes) : [], outcomes: Array.isArray(st.signal_ledger.outcomes) ? st.signal_ledger.outcomes.slice(-SIGNAL_LEDGER_LIMITS.outcomes) : [],
expired_ungraded: Number(st.signal_ledger.expired_ungraded || 0),
}; };
} }
delete out.whales; delete out.whales;
@@ -172,7 +193,7 @@ function compactAgentState(st) {
return out; return out;
} }
function compactSuggestion(s) { export function compactSuggestion(s) {
if (!s || typeof s !== "object") return s; if (!s || typeof s !== "object") return s;
return { return {
market_id: s.market_id, question: s.question, event: s.event, url: s.url, category: s.category, market_id: s.market_id, question: s.question, event: s.event, url: s.url, category: s.category,
@@ -182,7 +203,13 @@ function compactSuggestion(s) {
evidence_score: s.evidence_score, evidence_source_count: s.evidence_source_count, quality: s.quality, evidence_score: s.evidence_score, evidence_source_count: s.evidence_source_count, quality: s.quality,
conviction: s.conviction, volume: s.volume, volume_24hr: s.volume_24hr, liquidity: s.liquidity, conviction: s.conviction, volume: s.volume, volume_24hr: s.volume_24hr, liquidity: s.liquidity,
spread: s.spread, price_change_1h: s.price_change_1h, price_change_1d: s.price_change_1d, price_change_1w: s.price_change_1w, spread: s.spread, price_change_1h: s.price_change_1h, price_change_1d: s.price_change_1d, price_change_1w: s.price_change_1w,
momentum_strength: s.momentum_strength, trade_ready: s.trade_ready, watch_only: s.watch_only, jump_risk: s.jump_risk, momentum_strength: s.momentum_strength, signal_strength: s.signal_strength, signal_confidence: s.signal_confidence,
signal_type: s.signal_type, trade_ready: s.trade_ready, entry_candidate: s.entry_candidate,
audited_observation_only: s.audited_observation_only, adaptive_promotion: s.adaptive_promotion,
watch_only: s.watch_only, jump_risk: s.jump_risk, requires_live: s.requires_live,
bundle_id: s.bundle_id, bundle_side: s.bundle_side, bundle_cost_per_unit: s.bundle_cost_per_unit,
bundle_payout_per_unit: s.bundle_payout_per_unit, bundle_net_profit_per_unit: s.bundle_net_profit_per_unit,
bundle_legs: s.bundle_legs,
days_to_resolution: s.days_to_resolution, drivers: s.drivers, rationale: s.rationale, days_to_resolution: s.days_to_resolution, drivers: s.drivers, rationale: s.rationale,
}; };
} }
@@ -243,6 +270,7 @@ export default async function handler(req, res) {
state: null, state: null,
degraded: true, degraded: true,
error: err && err.message ? err.message : "Cloud state provider unavailable", error: err && err.message ? err.message : "Cloud state provider unavailable",
providers: err && err.providers ? err.providers : undefined,
}); });
} }
} }
@@ -261,6 +289,7 @@ export default async function handler(req, res) {
degraded: true, degraded: true,
retryable: true, retryable: true,
error: err && err.message ? err.message : "Cloud state provider unavailable", error: err && err.message ? err.message : "Cloud state provider unavailable",
providers: err && err.providers ? err.providers : undefined,
}); });
} }
const incomingItems = { ...body.items }; const incomingItems = { ...body.items };
+35 -9
View File
@@ -341,7 +341,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<nav class="topnav"> <nav class="topnav">
<div class="brand"> <div class="brand">
<div class="logo">🏆</div> <div class="logo">🏆</div>
<div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · 5 core + 5 aggressive</div><div class="build-badge">Adaptive strategy 50 · build 57</div></div> <div><div class="brand-name">Polymarket Arena</div><div class="brand-sub">10 agents · 5 core + 5 aggressive</div><div class="build-badge">Adaptive strategy 50 · build 58</div></div>
</div> </div>
<div class="tabs" id="tabs"> <div class="tabs" id="tabs">
<button class="tab" data-tab="overview">Overview</button> <button class="tab" data-tab="overview">Overview</button>
@@ -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 57 active:</b> unproven directional signals train the event-clustered learner without risking cash. Cohorts can trade only after independent 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 58 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. 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 57 · Adaptive strategy 50 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice · Build 58 · 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 = 57; const BUILD_VERSION = 58;
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});
@@ -1717,6 +1717,22 @@ function summarizeLearningBucket(bucket,shrinkage){
} }
function defaultSignalLedger(){return {pending:[],outcomes:[]};} function defaultSignalLedger(){return {pending:[],outcomes:[]};}
function signalPrice(m,side){return side==="YES"?Number(m&&m.yes_price):Number(m&&m.no_price);} function signalPrice(m,side){return side==="YES"?Number(m&&m.yes_price):Number(m&&m.no_price);}
function signalObservationEventKey(item){return String(item&&item.event_key||item&&item.url||item&&item.event||item&&item.market_id||"").trim().toLowerCase();}
function signalObservationCohortKey(item){return `${item&&item.signal_type||"unknown"}|${item&&item.side||"unknown"}|${item&&item.category||"Other"}`;}
function prioritizeSignalObservations(suggestions,ledger,pending=[]){
const history=[...((ledger&&ledger.outcomes)||[]),...(pending||[])],eventCounts={},cohortCounts={},pairCounts={};
history.forEach(item=>{
const eventKey=signalObservationEventKey(item),cohortKey=signalObservationCohortKey(item),pair=`${item&&item.market_id||""}:${item&&item.side||""}`;
if(eventKey)eventCounts[eventKey]=(eventCounts[eventKey]||0)+1;
cohortCounts[cohortKey]=(cohortCounts[cohortKey]||0)+1;
if(pair!==":")pairCounts[pair]=(pairCounts[pair]||0)+1;
});
return (suggestions||[]).map((s,index)=>({s,index,eventCount:eventCounts[signalObservationEventKey(s)]||0,
cohortCount:cohortCounts[signalObservationCohortKey(s)]||0,pairCount:pairCounts[`${s.market_id}:${s.side}`]||0}))
.sort((a,b)=>(a.cohortCount-b.cohortCount)||(a.eventCount-b.eventCount)||(a.pairCount-b.pairCount)
||(Number(b.s.conviction||0)-Number(a.s.conviction||0))||(a.index-b.index))
.map(row=>row.s);
}
function pendingSignalMarketIds(ledger,knownIds=new Set(),now=Date.now()){ function pendingSignalMarketIds(ledger,knownIds=new Set(),now=Date.now()){
const rows=((ledger&&ledger.pending)||[]).map(item=>({item,started:new Date(item.observed_at||0).getTime()})) const rows=((ledger&&ledger.pending)||[]).map(item=>({item,started:new Date(item.observed_at||0).getTime()}))
.filter(row=>Number.isFinite(row.started)) .filter(row=>Number.isFinite(row.started))
@@ -1750,7 +1766,8 @@ function updateSignalLedger(st,markets,suggestions){
stillPending.sort((a,b)=>new Date(a.observed_at||0)-new Date(b.observed_at||0)); stillPending.sort((a,b)=>new Date(a.observed_at||0)-new Date(b.observed_at||0));
const existing=new Set(stillPending.map(x=>x.key)),pendingPairs=new Set(stillPending.map(x=>`${x.market_id}:${x.side}`)); const existing=new Set(stillPending.map(x=>x.key)),pendingPairs=new Set(stillPending.map(x=>`${x.market_id}:${x.side}`));
const bucket=Math.floor(now/(6*3600000)); const bucket=Math.floor(now/(6*3600000));
const observable=(suggestions||[]).filter(x=>x.trade_ready||(!x.jump_risk&&["trend","reversal"].includes(x.signal_type)&&Number(x.signal_confidence||0)>=0.56)); const observable=prioritizeSignalObservations((suggestions||[])
.filter(x=>x.trade_ready||(!x.jump_risk&&["trend","reversal"].includes(x.signal_type)&&Number(x.signal_confidence||0)>=0.56)),ledger,stillPending);
for(const s of observable){ for(const s of observable){
const pair=`${s.market_id}:${s.side}`,key=`${pair}:${bucket}`;if(existing.has(key)||pendingPairs.has(pair))continue; const pair=`${s.market_id}:${s.side}`,key=`${pair}:${bucket}`;if(existing.has(key)||pendingPairs.has(pair))continue;
if(stillPending.length>=SIGNAL_LEDGER_PENDING_LIMIT)break; if(stillPending.length>=SIGNAL_LEDGER_PENDING_LIMIT)break;
@@ -2874,7 +2891,7 @@ function decisionSummary(p){
const blockerRows=Object.entries(d.rejectionCounts||{}).filter(([,count])=>count>0).sort((a,b)=>b[1]-a[1]); const blockerRows=Object.entries(d.rejectionCounts||{}).filter(([,count])=>count>0).sort((a,b)=>b[1]-a[1]);
const blockers=blockerRows.length?` Blocks: ${blockerRows.slice(0,4).map(([key,count])=>`${blockerLabels[key]||key} ${count}`).join(", ")}.`:""; const blockers=blockerRows.length?` Blocks: ${blockerRows.slice(0,4).map(([key,count])=>`${blockerLabels[key]||key} ${count}`).join(", ")}.`:"";
const learning=d.learning?` Learning: ${d.learning.samples} completed trades retained with older strategies down-weighted, ${(d.learning.global_score*100).toFixed(2)}% shrunk expectancy; ${d.learning.current_samples||0} completed under adaptive strategy ${SUGGESTION_ENGINE_VERSION}${d.learning.best?`; strongest ${d.learning.best.feature.replace(":"," ")}`:""}${d.learning.worst?`; weakest ${d.learning.worst.feature.replace(":"," ")}`:""}.`:""; const learning=d.learning?` Learning: ${d.learning.samples} completed trades retained with older strategies down-weighted, ${(d.learning.global_score*100).toFixed(2)}% shrunk expectancy; ${d.learning.current_samples||0} completed under adaptive strategy ${SUGGESTION_ENGINE_VERSION}${d.learning.best?`; strongest ${d.learning.best.feature.replace(":"," ")}`:""}${d.learning.worst?`; weakest ${d.learning.worst.feature.replace(":"," ")}`:""}.`:"";
const calibration=d.marketLearning?` Walk-forward calibration: ${d.marketLearning.samples||0} net-of-cost observations across ${d.marketLearning.events||d.marketLearning.markets||0} event clusters / ${d.marketLearning.markets||0} markets graded after ${SIGNAL_EVAL_HOURS} hours (${d.marketLearning.current_samples||0} observations / ${d.marketLearning.current_events||0} events under adaptive strategy ${SUGGESTION_ENGINE_VERSION}), ${d.marketLearning.pending||0} awaiting a future price${d.marketLearning.expired_ungraded?`, ${d.marketLearning.expired_ungraded} expired ungraded`:""}; ${d.marketLearning.promoted_buckets||0} feature cohorts promoted and ${d.marketLearning.demoted_buckets||0} demoted. Repeated snapshots and correlated outcome markets in one event are clustered into one effective outcome, matured markets are repriced after leaving the active scan, and observation-only signals also train the ledger. Uncertainty gates sizing. Historical prior: Sports and Crypto trends stay observation-only; reversal and short-dated NO require promotion in their own recent cohorts; settlement-jump barriers are excluded; longshots and YES entries are sized down. Politics trends receive 72 hours before ordinary signal exits.`:""; const calibration=d.marketLearning?` Walk-forward calibration: ${d.marketLearning.samples||0} net-of-cost observations across ${d.marketLearning.events||d.marketLearning.markets||0} event clusters / ${d.marketLearning.markets||0} markets graded after ${SIGNAL_EVAL_HOURS} hours (${d.marketLearning.current_samples||0} observations / ${d.marketLearning.current_events||0} events under adaptive strategy ${SUGGESTION_ENGINE_VERSION}), ${d.marketLearning.pending||0} awaiting a future price${d.marketLearning.expired_ungraded?`, ${d.marketLearning.expired_ungraded} expired ungraded`:""}; ${d.marketLearning.promoted_buckets||0} feature cohorts promoted and ${d.marketLearning.demoted_buckets||0} demoted. New observations prioritize under-sampled signal/side/category cohorts and independent events before repeats. Correlated outcome markets in one event are clustered into one effective outcome, and matured markets are repriced after leaving the active scan. Uncertainty gates sizing. Historical prior: every directional trend and reversal remains observation-only until its exact recent cohorts independently promote; settlement-jump barriers stay excluded.`:"";
return `${d.mode} mode: ${d.reason}${emotion} Limits now: ${d.maxNew} new trade${d.maxNew===1?"":"s"}, max ${(d.maxFrac*100).toFixed(1)}% per position${d.minConv?`, conviction ${d.minConv}+`:""}.${learning}${calibration}${exposure}${allocation}${candidates}${blockers}`; return `${d.mode} mode: ${d.reason}${emotion} Limits now: ${d.maxNew} new trade${d.maxNew===1?"":"s"}, max ${(d.maxFrac*100).toFixed(1)}% per position${d.minConv?`, conviction ${d.minConv}+`:""}.${learning}${calibration}${exposure}${allocation}${candidates}${blockers}`;
} }
function renderAgentBrief(cfg,p,st){ function renderAgentBrief(cfg,p,st){
@@ -4461,6 +4478,7 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
historicalPriceFeatures, historicalPriceFeatures,
offlineCachePolicy, offlineCachePolicy,
prepareCycleSuggestions, prepareCycleSuggestions,
prioritizeSignalObservations,
gainStopTargets:(entry)=>GAIN_STOP_TIERS.map(t=>gainStopTarget({entry_price:Number(entry),cost:1,shares:1,gain_stops:{}},t)), gainStopTargets:(entry)=>GAIN_STOP_TIERS.map(t=>gainStopTarget({entry_price:Number(entry),cost:1,shares:1,gain_stops:{}},t)),
rules:Object.freeze({minimumPolicyHoldHours:MIN_POLICY_HOLD_HOURS,politicsTrendHoldHours:POLITICS_TREND_MIN_HOLD_HOURS,exitConfirmationHours:EXIT_CONFIRM_HOURS,maxAgentOverlap:2,gapProneMaxAgentOverlap:1,globalExplorationOwner:true,materialOverlapPct:1.25,stopLossPct:18, rules:Object.freeze({minimumPolicyHoldHours:MIN_POLICY_HOLD_HOURS,politicsTrendHoldHours:POLITICS_TREND_MIN_HOLD_HOURS,exitConfirmationHours:EXIT_CONFIRM_HOURS,maxAgentOverlap:2,gapProneMaxAgentOverlap:1,globalExplorationOwner:true,materialOverlapPct:1.25,stopLossPct:18,
coreTradeLossPct:MAX_CORE_TRADE_LOSS_PCT*100,aggressiveTradeLossPct:MAX_AGGRESSIVE_TRADE_LOSS_PCT*100, coreTradeLossPct:MAX_CORE_TRADE_LOSS_PCT*100,aggressiveTradeLossPct:MAX_AGGRESSIVE_TRADE_LOSS_PCT*100,
@@ -4470,7 +4488,7 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
staleCacheIsMarkOnly:true,strategyEvidenceSurvivesBuilds:true,signalEvaluationHours:SIGNAL_EVAL_HOURS,signalRetryHours:SIGNAL_LEDGER_RETRY_HOURS, staleCacheIsMarkOnly:true,strategyEvidenceSurvivesBuilds:true,signalEvaluationHours:SIGNAL_EVAL_HOURS,signalRetryHours:SIGNAL_LEDGER_RETRY_HOURS,
signalDueFetchLimit:SIGNAL_LEDGER_DUE_FETCH_LIMIT,survivorshipSafeSignalGrading:true, signalDueFetchLimit:SIGNAL_LEDGER_DUE_FETCH_LIMIT,survivorshipSafeSignalGrading:true,
signalRoundTripCostCents:SIGNAL_ROUND_TRIP_COST*100,independentConfidence:true,eventClusteredCalibration:true, signalRoundTripCostCents:SIGNAL_ROUND_TRIP_COST*100,independentConfidence:true,eventClusteredCalibration:true,
onePendingObservationPerMarketSide:true,oldestPendingEvidenceFirst:true,uncertaintyGatedCalibration:true, onePendingObservationPerMarketSide:true,oldestPendingEvidenceFirst:true,coverageAwareObservationSampling:true,uncertaintyGatedCalibration:true,
directionalSignalsRequirePromotion:true,liveOnlyCompleteBundles:true,negativeRiskBundleSides:["YES","NO"],negativeRiskEventScanLimit:NEG_RISK_EVENT_SCAN_LIMIT, directionalSignalsRequirePromotion:true,liveOnlyCompleteBundles:true,negativeRiskBundleSides:["YES","NO"],negativeRiskEventScanLimit:NEG_RISK_EVENT_SCAN_LIMIT,
negativeRiskMinimumNetReturnPct:NEG_RISK_MIN_NET_RETURN*100, negativeRiskMinimumNetReturnPct:NEG_RISK_MIN_NET_RETURN*100,
historicalPrior:"All directional trends and reversals require positive independent cohort promotion; Sports and Crypto trends remain excluded; exact ranges and path-dependent barriers are excluded; live-priced complete negative-risk bundles may trade when positive after estimated costs"}), historicalPrior:"All directional trends and reversals require positive independent cohort promotion; Sports and Crypto trends remain excluded; exact ranges and path-dependent barriers are excluded; live-priced complete negative-risk bundles may trade when positive after estimated costs"}),
@@ -4540,6 +4558,13 @@ function runEngineSelfTest(){
})),outcomes:[]}}; })),outcomes:[]}};
updateSignalLedger(queueLedgerState,[],Array.from({length:5},(_,i)=>({market_id:`new-market-${i}`,side:"YES",entry_price:0.42, updateSignalLedger(queueLedgerState,[],Array.from({length:5},(_,i)=>({market_id:`new-market-${i}`,side:"YES",entry_price:0.42,
signal_type:"trend",quality:"confirmed",signal_confidence:0.70,trade_ready:true,jump_risk:false,category:"Politics",days_to_resolution:45}))); signal_type:"trend",quality:"confirmed",signal_confidence:0.70,trade_ready:true,jump_risk:false,category:"Politics",days_to_resolution:45})));
const coverageLedger={pending:[],outcomes:Array.from({length:8},(_,i)=>({market_id:`popular-market-${i}`,event_key:"popular-event",
signal_type:"trend",side:"YES",category:"Politics",return:0.01,evaluated_at:closedAt}))};
const coveragePriority=prioritizeSignalObservations([
{market_id:"popular-repeat",event_key:"popular-event",signal_type:"trend",side:"YES",category:"Politics",conviction:99},
{market_id:"new-politics",event_key:"new-politics-event",signal_type:"trend",side:"YES",category:"Politics",conviction:80},
{market_id:"new-sports",event_key:"new-sports-event",signal_type:"trend",side:"NO",category:"Sports",conviction:70},
],coverageLedger,[]);
const dueFetchLedger={pending:[ const dueFetchLedger={pending:[
{key:"known",market_id:"known-active",observed_at:hoursAgo(25)}, {key:"known",market_id:"known-active",observed_at:hoursAgo(25)},
{key:"outside-a",market_id:"outside-active-scan",observed_at:hoursAgo(26)}, {key:"outside-a",market_id:"outside-active-scan",observed_at:hoursAgo(26)},
@@ -4712,11 +4737,11 @@ function runEngineSelfTest(){
minConv:0,maxNew:1,maxFrac:0.04,reserve:0.10,targetExposure:0.60,learning:buildAdaptiveProfile(staleEntryBook),marketLearning:{samples:0,pending:0,buckets:{}}, minConv:0,maxNew:1,maxFrac:0.04,reserve:0.10,targetExposure:0.60,learning:buildAdaptiveProfile(staleEntryBook),marketLearning:{samples:0,pending:0,buckets:{}},
},new Set(),{}); },new Set(),{});
const buildMigrationState=defaultState(); const buildMigrationState=defaultState();
buildMigrationState.engine_version=56;buildMigrationState.strategy_version=SUGGESTION_ENGINE_VERSION; buildMigrationState.engine_version=57;buildMigrationState.strategy_version=SUGGESTION_ENGINE_VERSION;
buildMigrationState.agents.value.engine_baseline={version:SUGGESTION_ENGINE_VERSION,started_at:hoursAgo(2),equity:9876.54}; buildMigrationState.agents.value.engine_baseline={version:SUGGESTION_ENGINE_VERSION,started_at:hoursAgo(2),equity:9876.54};
reconcileStateVersions(buildMigrationState); reconcileStateVersions(buildMigrationState);
const strategyMigrationState=defaultState(); const strategyMigrationState=defaultState();
strategyMigrationState.engine_version=56;strategyMigrationState.strategy_version=PREVIOUS_STRATEGY_VERSION; strategyMigrationState.engine_version=57;strategyMigrationState.strategy_version=PREVIOUS_STRATEGY_VERSION;
strategyMigrationState.agents.value.cash=9876.54; strategyMigrationState.agents.value.cash=9876.54;
strategyMigrationState.agents.value.engine_baseline={version:PREVIOUS_STRATEGY_VERSION,started_at:hoursAgo(2),equity:10000}; strategyMigrationState.agents.value.engine_baseline={version:PREVIOUS_STRATEGY_VERSION,started_at:hoursAgo(2),equity:10000};
reconcileStateVersions(strategyMigrationState); reconcileStateVersions(strategyMigrationState);
@@ -4751,6 +4776,7 @@ function runEngineSelfTest(){
&&queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="old-market-0") &&queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="old-market-0")
&&queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="new-market-0") &&queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="new-market-0")
&&!queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="new-market-1"), &&!queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="new-market-1"),
underObservedCohortsAndEventsSampleFirst:coveragePriority.map(x=>x.market_id).join(",")==="new-sports,new-politics,popular-repeat",
ledgerMaturesWithoutLookahead:ledgerState.signal_ledger.pending.length===0&&ledgerState.signal_ledger.outcomes.length===1&&ledgerState.signal_ledger.outcomes[0].return===0.2375, ledgerMaturesWithoutLookahead:ledgerState.signal_ledger.pending.length===0&&ledgerState.signal_ledger.outcomes.length===1&&ledgerState.signal_ledger.outcomes[0].return===0.2375,
holdsSignalsUntilPolicyHorizon:earlyLedgerState.signal_ledger.pending.length===1&&earlyLedgerState.signal_ledger.outcomes.length===0, holdsSignalsUntilPolicyHorizon:earlyLedgerState.signal_ledger.pending.length===1&&earlyLedgerState.signal_ledger.outcomes.length===0,
fetchesMaturedMarketsOutsideActiveScan:dueFetchIds.length===1&&dueFetchIds[0]==="outside-active-scan", fetchesMaturedMarketsOutsideActiveScan:dueFetchIds.length===1&&dueFetchIds[0]==="outside-active-scan",
+3 -1
View File
@@ -1,8 +1,10 @@
{ {
"type": "module",
"scripts": { "scripts": {
"evaluate:neg-risk": "node scripts/evaluate-neg-risk.mjs", "evaluate:neg-risk": "node scripts/evaluate-neg-risk.mjs",
"evaluate:signals": "node scripts/evaluate-signals.mjs", "evaluate:signals": "node scripts/evaluate-signals.mjs",
"evaluate:settlements": "node scripts/evaluate-settlements.mjs" "evaluate:settlements": "node scripts/evaluate-settlements.mjs",
"test:server-state": "node scripts/test-server-state.mjs"
}, },
"dependencies": { "dependencies": {
"@neondatabase/serverless": "^1.1.0", "@neondatabase/serverless": "^1.1.0",
+44
View File
@@ -0,0 +1,44 @@
import assert from "node:assert/strict";
import { databaseUrl, databaseUrls, normalizeDatabaseUrl } from "../api/_db.js";
import { compactAgentState, compactSuggestion, providerErrorCode } from "../api/state.js";
assert.equal(normalizeDatabaseUrl("psql 'postgresql://user:pass@example.test/db?sslmode=require'"),
"postgresql://user:pass@example.test/db?sslmode=require");
assert.equal(normalizeDatabaseUrl('"postgres://user:pass@example.test/db"'), "postgres://user:pass@example.test/db");
assert.equal(normalizeDatabaseUrl("DATABASE_URL=postgresql://user:pass@example.test/db"),
"postgresql://user:pass@example.test/db");
const originalDatabaseUrl = process.env.DATABASE_URL;
const originalNeonUrl = process.env.NEON_DATABASE_URL;
process.env.DATABASE_URL = "not-a-postgres-url";
process.env.NEON_DATABASE_URL = "postgresql://alias:pass@example.test/db";
assert.equal(databaseUrl(), process.env.NEON_DATABASE_URL);
assert.deepEqual(databaseUrls(), [process.env.NEON_DATABASE_URL, "not-a-postgres-url"]);
process.env.DATABASE_URL = "postgresql://stale:pass@example.test/db";
assert.deepEqual(databaseUrls(), [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]);
if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL;
else process.env.DATABASE_URL = originalDatabaseUrl;
if (originalNeonUrl === undefined) delete process.env.NEON_DATABASE_URL;
else process.env.NEON_DATABASE_URL = originalNeonUrl;
const compacted = compactSuggestion({
market_id: "bundle:1:yes", side: "YES", signal_type: "bundle-arb", signal_confidence: 1,
entry_candidate: true, adaptive_promotion: false, requires_live: true, bundle_id: "bundle:1:yes",
bundle_side: "YES", bundle_cost_per_unit: 0.95, bundle_payout_per_unit: 1,
bundle_net_profit_per_unit: 0.05, bundle_legs: [{ market_id: "1", side: "YES" }],
});
assert.equal(compacted.signal_type, "bundle-arb");
assert.equal(compacted.entry_candidate, true);
assert.equal(compacted.requires_live, true);
assert.equal(compacted.bundle_side, "YES");
assert.equal(compacted.bundle_legs.length, 1);
const compactedState = compactAgentState({
agents: {},
signal_ledger: { pending: [], outcomes: [], 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("invalid connection string")), "invalid_connection_string");
console.log("server state tests passed");
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = "polymarket-arena-build-57"; const CACHE_NAME = "polymarket-arena-build-58";
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 => {