Require durable evidence before adaptive trades

This commit is contained in:
Theodore Song
2026-08-19 14:13:29 -04:00
parent b9d8d66264
commit c983c1e99f
8 changed files with 320 additions and 110 deletions
+58 -35
View File
@@ -15,12 +15,18 @@ https://polymarket-site-eta.vercel.app/personal.html
The site fetches live Polymarket markets, generates agent suggestions, lets you
run frequent paper cycles, and syncs the shared arena state through Neon or
Vercel Blob. Build 61 also installs an offline app shell and caches timestamped
Vercel Blob. Build 62 also installs an offline app shell and caches timestamped
market snapshots. During an outage, cycles continue locally; cached entries are
allowed for 90 minutes, older snapshots become mark-only, and all cached data
expires after 24 hours.
Build 61 ranks the competition by each agent's return since Strategy 50 began.
The live scan now continues through activity-ranked pages until it has the 500
most-active eligible Yes/No contracts. Markets whose actual outcome labels are
team names, Over/Under, or another pair are rejected instead of being silently
reinterpreted as Yes/No. The same semantic check applies to complete event
bundles and the offline evaluators.
Build 62 ranks the competition by each agent's return since Strategy 51 began.
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.
@@ -31,7 +37,7 @@ The learner shrinks small samples toward neutral, caps sizing changes to
15% of candidates for deterministic exploration so a stale regime cannot become
permanent.
Strategy 50 treats each binary stake as capable of falling to zero even when the
Strategy 51 treats each binary stake as capable of falling to zero even when the
18% stop cannot fill. New core positions are capped at 2.5%-4% of equity and
aggressive positions at 3%-5%, with lower limits for near-term, extreme-price,
reversal, and fast-moving setups. Oversized positions inherited from older
@@ -40,12 +46,13 @@ The two-agent overlap guard counts only positions worth at least 1.25% of an
agent's equity, so tiny profit-lock runners do not block a new material trade.
A separate walk-forward ledger records every confirmed signal before its future
price is known, grades it at least 24 hours later, and combines that broad market
calibration with each agent's personal outcomes. This expands the learning sample
without forcing observation-only signals into portfolios or backfilling future
information into old decisions. The 24-hour horizon
matches the engine's minimum ordinary holding policy; stops and profit locks still
act immediately from fresh prices.
price is known, grades it in separate 24-hour and 72-hour windows, and combines
that broad market calibration with each agent's personal outcomes. This expands
the learning sample without forcing observation-only signals into portfolios or
backfilling future information into old decisions. Missed windows expire instead
of borrowing an arbitrarily later price. The 24-hour checkpoint matches the
minimum ordinary holding policy while the 72-hour checkpoint tests persistence;
stops and profit locks still act immediately from fresh prices.
The initial seven-day chart seed is an approximate replay, not a live return.
It uses only prices available on each simulated date, computes daily and weekly
@@ -69,16 +76,16 @@ negative in all three chronological segments. Reversals averaged -3.69%, with a
market-clustered 90% interval entirely below zero. Crypto and Sports were also
negative but covered only three and five markets. The 24-hour cohort improved to
-0.82% row mean and +1.31% market mean, with no rule robustly negative across all
segments. Strategy 50 therefore keeps reversal entries observation-only until
segments. Strategy 51 therefore keeps reversal entries observation-only until
their recent signal and quality cohorts independently earn promotion, retains
their signals for paper grading, and evaluates adaptation at 24 hours.
their signals for paper grading, and evaluates adaptation at 24 and 72 hours.
The expanded active-market audit loaded history for 498 of the top 500 active
markets with no failures and produced 3,597 net-of-cost 24-hour outcomes across
142 markets. No tested follow or fade rule was robustly positive. Crypto trends
averaged -3.83% per observation and -3.99% per market; Sports trends averaged
-5.44% and -6.61%. Both stayed negative in every chronological segment and their
market-clustered 90% intervals were entirely below zero. Strategy 50 therefore
market-clustered 90% intervals were entirely below zero. Strategy 51 therefore
keeps Crypto and Sports trends observation-only while continuing to grade them.
The August 18 event-clustered rerun loaded 499 of 500 active markets and produced
@@ -87,34 +94,43 @@ The broad mean was -1.14%, the event mean was -1.11%, and the event-clustered
90% interval stayed below zero. No tested category, side, price band, signal
strength, or combined feature cohort was robustly positive. Broad trends,
YES trends, favorite trends, strong trends, and hour-confirmed trends were all
robustly negative. Strategy 50 therefore makes every directional trend or
robustly negative. Strategy 51 therefore makes every directional trend or
reversal observation-only until its own signal, side, and category cohorts each
earn positive promotion from recent independent events. This is a strategy reset,
so current adaptive returns begin from the portfolio equity at migration.
The final August 19 300-market rerun loaded all 300 eligible Yes/No price
histories without a failure and produced 2,598 twelve-hour observations across
64 independent events. The broad row mean was -0.65% and the event-cluster mean
was -1.71%. The 72-hour event-cluster mean was -3.59% with its 90% interval below
zero, and no tested directional rule was robustly positive. Strategy 51
therefore requires positive evidence at both 24 and 72 hours rather than
allowing one favorable short-horizon mark to authorize cash exposure.
A corrected 200-market audit paged through 197 markets with usable history and
1,912 twelve-hour outcomes. Reversals remained negative in every chronological
segment and averaged -4.13%. Sports trends were negative in train and test and
averaged -3.53% at 72 hours. Politics trends were the sole cohort with positive
row-level returns in all three 72-hour segments, but its market-cluster interval
still crossed zero; that supports a longer hold test, not a larger entry bet.
Strategy 50 gives previously opened Politics trend positions that 72-hour observation window before
Strategy 51 gives previously opened Politics trend positions that 72-hour observation window before
ordinary signal exits. Stops, profit locks, settlement handling, and risk-budget
reductions remain immediate.
Strategy 50 also subtracts a half-cent round-trip cost when grading each live
Strategy 51 also subtracts a half-cent round-trip cost when grading each live
walk-forward signal. Confidence uses the largest independent matching bucket,
not the sum of five overlapping feature buckets, and evidence from older engine
versions is down-weighted. This prevents a handful of duplicated observations
from authorizing larger positions or hiding a modest negative regime.
Strategy 50 adds uncertainty-aware promotion and demotion. A matching setup must
accumulate at least eight effective observations and agree across at least two
feature views before repeatable positive evidence can increase size or repeatable
negative evidence can block a new entry. Mixed evidence stays close to neutral
instead of being mistaken for an edge.
Strategy 51 adds uncertainty-aware, multi-horizon promotion and demotion. A
matching setup must accumulate at least eight effective independent-event
observations, including at least five from the current strategy, and agree across
at least two feature views at both the 24-hour and 72-hour checkpoints before it
can risk cash. Mixed or one-horizon evidence stays observation-only instead of
being mistaken for an edge.
Build 61 enforces the documented offline boundary end to end. Cached snapshots
Build 62 enforces the documented offline boundary end to end. Cached snapshots
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,
stop-losses, gain-stops, risk rebalances, settlements, or policy exits. Network
@@ -127,26 +143,26 @@ 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
records are migrated into the same strategy lineage without losing evidence.
Build 61 independently refreshes markets for matured pending signals that have
Build 62 independently refreshes markets for due pending signals that have
left the current top-500 activity scan. Unavailable markets remain queued for a
bounded retry window. This prevents activity-rank survivorship from deciding
which wins and losses reach the adaptive calibration ledger.
Build 61 also allocates the 300 pending observation slots by evidence coverage.
Build 62 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.
Build 61 retains safe shared-state provider diagnostics from both reads and
Build 62 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. Completed cycle statuses retain that `local only` warning
until a cloud provider succeeds. Database URLs still fail over across configured Neon
aliases without exposing credentials in the API response.
Strategy 50 coordinates high-risk exploration globally. Near-term, extreme-price,
Strategy 51 coordinates high-risk exploration globally. Near-term, extreme-price,
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
negative Sports- and Crypto-trend cohorts cannot enter through exploration.
@@ -170,15 +186,22 @@ liquid negative-risk events and zero positive worst-case bundle returns after
costs. Midpoint price sums sometimes looked attractive, but executable spreads
removed the apparent edge. The August 18 rerun found 35 eligible events and one
three-leg NO bundle with a 0.25%
modeled margin after estimated costs. The corrected August 19 scan found 39
eligible events and no currently actionable bundle; its closest complete bundle
remained 0.37% negative after modeled costs. Strategy 50 can paper-trade either a
modeled margin after estimated costs. The final August 19 scan found 49 eligible
events and no currently actionable bundle; its closest complete bundle remained
0.25% negative after modeled costs. Strategy 51 can paper-trade either a
complete YES or complete NO bundle only from live executable prices, opens every
leg together, and holds
the hedge intact until settlement. It also requires at least a 0.15% modeled net
return so large bundles cannot tie up capital for a negligible absolute edge.
Cached bundle prices are never allowed to open positions.
Run `npm run evaluate:dominance` to inspect logically nested threshold contracts
using executable prices. The corrected August 19 scan tested 807 eligible pairs
across 500 active events and found zero positive pairs after estimated costs. An
earlier parser had mistaken Over/Under outcome labels for Yes/No and reported
false opportunities; the label-aware scanner and live engine now reject that
failure mode.
The expanded event-clustered run loaded history for 498 of the 500 highest-volume
resolved markets with no fetch failures. No side, price band, category, trend,
or 1-90 day holding rule passed the required train/test confidence checks. In
@@ -188,28 +211,28 @@ static settlement-direction boost from this audit.
The earlier 200-resolved-market audit found short-dated NO entries strongly
negative, but the 500-market rerun did not reproduce that loss in its newer test
segment. Strategy 50 therefore treats the result as a provisional prior instead
segment. Strategy 51 therefore treats the result as a provisional prior instead
of a permanent ban: NO entries with 21 days or less remain observation-only until
the recent walk-forward calibration promotes their matching side and duration
cohorts. Exact numeric-range contracts are excluded from new entries because a
settlement jump can pass directly through an 18% stop; the live audit found that
this failure mode caused the largest latest-day loss.
Strategy 50 also excludes path-dependent barriers such as "reach $66,000," "hit
Strategy 51 also excludes path-dependent barriers such as "reach $66,000," "hit
$90," and "dip to $62,000." These contracts can resolve abruptly as soon as the
barrier is touched, so a later hourly stop cannot reliably cap the loss. Fixed-date
level questions such as "above $66,000 on August 23" remain eligible.
Strategy 50 clusters live walk-forward observations by Polymarket event before
Strategy 51 clusters live walk-forward observations by Polymarket event and checkpoint before
calculating confidence. Multiple six-hour snapshots and correlated outcome
markets from the same event are averaged into one effective outcome, so one
election or tournament cannot promote or demote an entire feature cohort.
Promotion still requires at least eight weighted event clusters and agreement
across two feature views.
Promotion requires agreement across two feature views at both horizons, with
current-strategy support preventing old lineage data from authorizing a new rule.
The pending signal ledger keeps only one ungraded observation for each market and
side. When its bounded queue is full, it preserves the oldest evidence until the
24-hour grade is available and admits new signals in ranked order as space opens.
side. When its bounded queue is full, it preserves the oldest evidence through
the 24-hour and 72-hour grades and admits new signals in ranked order as space opens.
This prevents frequent cycles from evicting every signal shortly before maturity.
Paper accounts created with a password are also saved through the backend, so a
+119 -65
View File
@@ -341,7 +341,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<nav class="topnav">
<div class="brand">
<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 59</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 51 · build 62</div></div>
</div>
<div class="tabs" id="tabs">
<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">
<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 class="live-build-banner"><b>Build 61 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 remain visible after every completed cycle 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>
<div class="live-build-banner"><b>Build 62 active:</b> unproven directional signals train the event-clustered learner without risking cash. Promotion now requires separate positive 24-hour and 72-hour net-of-cost evidence from independent events under the current strategy; missed grading windows expire instead of being mislabeled. Cloud provider failures remain visible after every completed cycle. 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 ============ -->
<section class="tabpanel" data-tab="overview">
@@ -378,11 +378,11 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<g fill="#d9b4ff"><circle cx="380" cy="40" r="6"/></g>
</svg>
<h1>Ten AI agents race to <span class="grad">beat the market</span></h1>
<p>Each cycle loads the 500 most active Polymarket markets, then requires direction to agree across independent time windows before liquidity, evidence, timing, and friction checks can make an idea trade-ready.</p>
<p>Each cycle loads the 500 most active eligible Yes/No Polymarket markets, then requires direction to agree across independent time windows before liquidity, evidence, timing, and friction checks can make an idea trade-ready.</p>
<div class="badges">
<span class="hbadge">🏆 10 competing agents</span>
<span class="hbadge">5 aggressive strategies</span>
<span class="hbadge">Top 500 active markets</span>
<span class="hbadge">Top 500 eligible Yes/No markets</span>
<span class="hbadge">Ranked by activity + confirmation</span>
<span class="hbadge">Live Polymarket data</span>
</div>
@@ -719,7 +719,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div class="card" style="margin-top:16px">
<div class="card-h"><h3>The live cycle</h3></div>
<ol class="steps">
<li><b>Fetch</b> — load the 500 most active Polymarket markets and tag each by category.</li>
<li><b>Fetch</b> — load the 500 most active eligible Yes/No Polymarket markets and tag each by category.</li>
<li><b>Score</b> — rank those active markets 0100 by conviction and estimate edge vs. a fair-value model.</li>
<li><b>Suggest</b> — surface the strongest picks per category with a plain-English rationale.</li>
<li><b>Compete</b> — the strategy agents trade the same suggestions their own way, stop-loss weak positions, scale out through gain-stop tiers, exit stale or fading trades, then snapshot equity.</li>
@@ -745,7 +745,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</section>
<footer>
Build 61 · Adaptive strategy 50 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
Build 62 · Adaptive strategy 51 · 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>
</footer>
</div>
@@ -774,9 +774,9 @@ const POLITICS_TREND_MIN_HOLD_HOURS = 72;
const EXIT_CONFIRM_HOURS = 6;
const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v5";
const BUILD_VERSION = 61;
const SUGGESTION_ENGINE_VERSION = 50;
const PREVIOUS_STRATEGY_VERSION = 49;
const BUILD_VERSION = 62;
const SUGGESTION_ENGINE_VERSION = 51;
const PREVIOUS_STRATEGY_VERSION = 50;
const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40});
function normalizedStrategyVersion(value){
const version=Number(value||0);
@@ -923,10 +923,12 @@ const OFFLINE_ENTRY_MAX_AGE_MS = 90*60*1000;
const OFFLINE_CACHE_MAX_AGE_MS = 24*60*60*1000;
const NETWORK_REQUEST_TIMEOUT_MS = 8000;
const PRICE_REQUEST_TIMEOUT_MS = 4000;
const SIGNAL_EVAL_HOURS = 24;
const SIGNAL_EVAL_HORIZONS = Object.freeze([24,72]);
const SIGNAL_EVAL_TOLERANCE_HOURS = 6;
const SIGNAL_EVAL_HOURS = SIGNAL_EVAL_HORIZONS[0];
const SIGNAL_LEDGER_PENDING_LIMIT = 300;
const SIGNAL_LEDGER_OUTCOME_LIMIT = 500;
const SIGNAL_LEDGER_RETRY_HOURS = 168;
const SIGNAL_LEDGER_RETRY_HOURS = SIGNAL_EVAL_HORIZONS[SIGNAL_EVAL_HORIZONS.length-1]+SIGNAL_EVAL_TOLERANCE_HOURS;
const SIGNAL_LEDGER_DUE_FETCH_LIMIT = 80;
const currentCycleHour = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}|s${SUGGESTION_ENGINE_VERSION}`;};
function cycleHourFromIso(iso){
@@ -975,6 +977,10 @@ async function mapWithConcurrency(items,limit,mapper){
function parseJsonField(v){if(v==null)return [];if(Array.isArray(v))return v;try{return JSON.parse(v);}catch(e){return [];}}
function toNum(v,d=0){const n=Number(v);return Number.isFinite(n)?n:d;}
function daysUntil(iso){if(!iso)return null;const dt=new Date(iso);if(isNaN(dt))return null;return (dt-new Date())/86400000;}
function hasYesNoOutcomes(raw){
const labels=parseJsonField(raw&&raw.outcomes).map(outcome=>String(outcome||"").trim().toLowerCase());
return labels.length===2&&labels[0]==="yes"&&labels[1]==="no";
}
const CATEGORY_RULES=[
["Politics",["politics","election","elections","us-politics","geopolitics","trump","world-leaders","government","congress","policy","biden","2026-election","democrats","republicans"]],
["Crypto",["crypto","bitcoin","ethereum","btc","eth","solana","memecoins","defi","stablecoin","xrp"]],
@@ -990,7 +996,7 @@ function classifyCategory(tags){
function normalizeMarket(raw,{allowClosed=false}={}){
const outcomes=parseJsonField(raw.outcomes);
const prices=parseJsonField(raw.outcomePrices).map(p=>toNum(p));
if(outcomes.length!==2||prices.length!==2)return null;
if(!hasYesNoOutcomes(raw)||prices.length!==2)return null;
if(!allowClosed&&(raw.acceptingOrders===false||raw.closed))return null;
const yes=prices[0];
if(!Number.isFinite(yes)||yes<0||yes>1||(!allowClosed&&(yes<=0||yes>=1)))return null;
@@ -1009,8 +1015,8 @@ function normalizeMarket(raw,{allowClosed=false}={}){
closed:Boolean(raw.closed),accepting_orders:raw.acceptingOrders!==false,
url:ev.slug?`https://polymarket.com/event/${ev.slug}`:""};
}
async function fetchMarkets(pages=Infinity,perPage=100,onProgress=null){
const all=[];
async function fetchMarkets(pages=Infinity,perPage=100,onProgress=null,eligibleLimit=null){
const all=[],eligible=[];
const maxPages=Number.isFinite(pages)?pages:MAX_ACTIVE_MARKET_PAGES;
const seen=new Set();
const useKeyset=!Number.isFinite(pages)||pages>20;
@@ -1034,12 +1040,14 @@ async function fetchMarkets(pages=Infinity,perPage=100,onProgress=null){
if(id&&seen.has(id))continue;
if(id)seen.add(id);
all.push(m);added++;
const normalized=normalizeMarket(m);
if(normalized&&normalized.question)eligible.push(normalized);
}
if(useKeyset)cursor=page.next_cursor||null;
if(onProgress)onProgress(all.length);
if(raw.length<perPage||added===0||(useKeyset&&!cursor))break;
if(onProgress)onProgress(eligible.length,all.length);
if((eligibleLimit&&eligible.length>=eligibleLimit)||raw.length<perPage||added===0||(useKeyset&&!cursor))break;
}
return all.map(normalizeMarket).filter(m=>m&&m.question);
return eligibleLimit?eligible.slice(0,eligibleLimit):eligible;
}
async function fetchMarketPrice(id){
try{const r=await fetchWithTimeout(`${GAMMA}/markets/${id}`,{},PRICE_REQUEST_TIMEOUT_MS);if(!r.ok)return null;return normalizeMarket(await r.json(),{allowClosed:true});}
@@ -1051,12 +1059,13 @@ function negativeRiskBundleSuggestion(event){
if(rawLegs.length<2||rawLegs.some(m=>m.closed||m.active===false||m.acceptingOrders===false))return null;
const eventTags=Array.isArray(event.tags)?event.tags:[],category=classifyCategory(eventTags);
const quotes=rawLegs.map(raw=>{
const outcomes=parseJsonField(raw.outcomes).map(outcome=>String(outcome||"").trim().toLowerCase());
const prices=parseJsonField(raw.outcomePrices).map(toNum),tokens=parseJsonField(raw.clobTokenIds).map(String);
return {market_id:String(raw.id||""),question:(raw.question||"").trim(),yes_bid:toNum(raw.bestBid,NaN),yes_ask:toNum(raw.bestAsk,NaN),
yes_mid:prices[0],no_mid:prices[1],yes_token:tokens[0]||null,no_token:tokens[1]||null,
liquidity:toNum(raw.liquidityNum||raw.liquidity),url:event.slug?`https://polymarket.com/event/${event.slug}`:""};
binary_labels:outcomes[0]==="yes"&&outcomes[1]==="no",liquidity:toNum(raw.liquidityNum||raw.liquidity),url:event.slug?`https://polymarket.com/event/${event.slug}`:""};
});
if(quotes.some(leg=>!leg.market_id||!leg.yes_token||!leg.no_token||!Number.isFinite(leg.yes_bid)||!Number.isFinite(leg.yes_ask)
if(quotes.some(leg=>!leg.binary_labels||!leg.market_id||!leg.yes_token||!leg.no_token||!Number.isFinite(leg.yes_bid)||!Number.isFinite(leg.yes_ask)
||leg.yes_bid<0||leg.yes_ask>1||leg.yes_ask<leg.yes_bid||!Number.isFinite(leg.yes_mid)||!Number.isFinite(leg.no_mid)
||leg.liquidity<NEG_RISK_MIN_LIQUIDITY))return null;
const makeCandidate=(side)=>{
@@ -1749,7 +1758,7 @@ function summarizeLearningBucket(bucket,shrinkage){
const pooledVariance=(priorWeight*priorVariance+weight*variance)/(priorWeight+weight||1);
const stderr=Math.sqrt(pooledVariance/Math.max(1,weight));
const score=Number(bucket&&bucket.sum||0)/(weight+shrinkage);
return {samples:Number(bucket&&bucket.count||0),observations:Number(bucket&&bucket.observations||bucket&&bucket.count||0),weight:+weight.toFixed(2),raw:+raw.toFixed(4),
return {samples:Number(bucket&&bucket.count||0),observations:Number(bucket&&bucket.observations||bucket&&bucket.count||0),weight:+weight.toFixed(2),current_weight:+Number(bucket&&bucket.currentWeight||0).toFixed(2),raw:+raw.toFixed(4),
score:+score.toFixed(4),stderr:+stderr.toFixed(4),lower_bound:+(score-1.28*stderr).toFixed(4),
upper_bound:+(score+1.28*stderr).toFixed(4),win_rate:weight?Number(bucket.wins||0)/weight:0};
}
@@ -1771,10 +1780,17 @@ function prioritizeSignalObservations(suggestions,ledger,pending=[]){
||(Number(b.s.conviction||0)-Number(a.s.conviction||0))||(a.index-b.index))
.map(row=>row.s);
}
function signalGradedHorizons(item){
return new Set((Array.isArray(item&&item.graded_horizons)?item.graded_horizons:[]).map(Number).filter(Number.isFinite));
}
function signalDueHorizon(item,ageHours){
const graded=signalGradedHorizons(item);
return SIGNAL_EVAL_HORIZONS.find(horizon=>!graded.has(horizon)&&ageHours>=horizon&&ageHours<=horizon+SIGNAL_EVAL_TOLERANCE_HOURS)||null;
}
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()}))
.filter(row=>Number.isFinite(row.started))
.filter(row=>{const age=(now-row.started)/3600000;return age>=SIGNAL_EVAL_HOURS&&age<=SIGNAL_LEDGER_RETRY_HOURS;})
.filter(row=>signalDueHorizon(row.item,(now-row.started)/3600000)!=null)
.sort((a,b)=>a.started-b.started);
const ids=[],seen=new Set();
for(const {item} of rows){
@@ -1791,15 +1807,22 @@ function updateSignalLedger(st,markets,suggestions){
const now=Date.now(),marketMap=Object.fromEntries((markets||[]).map(m=>[String(m.id),m])),stillPending=[];
for(const item of ledger.pending){
const started=new Date(item.observed_at||0).getTime(),ageHours=Number.isFinite(started)?(now-started)/3600000:Infinity;
const graded=signalGradedHorizons(item),dueHorizon=signalDueHorizon(item,ageHours);
const fresh=marketMap[String(item.market_id)],future=signalPrice(fresh,item.side);
if(ageHours>=SIGNAL_EVAL_HOURS&&Number.isFinite(future)&&future>=0&&future<=1){
if(dueHorizon!=null&&Number.isFinite(future)&&future>=0&&future<=1){
const entry=Math.max(0.01,Number(item.entry_price||0.01));
const gross=future/entry-1,estimatedCost=SIGNAL_ROUND_TRIP_COST/entry;
const outcome=clamp(gross-estimatedCost,-1,2);
ledger.outcomes.push(Object.assign({},item,{evaluated_at:nowIso(),horizon_hours:+ageHours.toFixed(1),future_price:+future.toFixed(4),
graded.add(dueHorizon);
ledger.outcomes.push(Object.assign({},item,{graded_horizons:undefined,evaluated_at:nowIso(),target_horizon_hours:dueHorizon,horizon_hours:+ageHours.toFixed(1),future_price:+future.toFixed(4),
gross_return:+gross.toFixed(4),estimated_cost_return:+estimatedCost.toFixed(4),return:+outcome.toFixed(4)}));
}else if(ageHours<=SIGNAL_LEDGER_RETRY_HOURS)stillPending.push(item);
else ledger.expired_ungraded=Number(ledger.expired_ungraded||0)+1;
}
const remaining=SIGNAL_EVAL_HORIZONS.filter(horizon=>!graded.has(horizon)&&ageHours<=horizon+SIGNAL_EVAL_TOLERANCE_HOURS);
if(remaining.length)stillPending.push(Object.assign({},item,{graded_horizons:[...graded].sort((a,b)=>a-b)}));
else{
const missed=SIGNAL_EVAL_HORIZONS.filter(horizon=>!graded.has(horizon)).length;
if(missed)ledger.expired_ungraded=Number(ledger.expired_ungraded||0)+missed;
}
}
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}`));
@@ -1809,7 +1832,7 @@ function updateSignalLedger(st,markets,suggestions){
for(const s of observable){
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;
existing.add(key);pendingPairs.add(pair);stillPending.push({key,market_id:String(s.market_id),event_key:String(s.url||s.event||s.market_id),observed_at:nowIso(),side:s.side,entry_price:Number(s.entry_price),
existing.add(key);pendingPairs.add(pair);stillPending.push({key,market_id:String(s.market_id),event_key:String(s.url||s.event||s.market_id),observed_at:nowIso(),graded_horizons:[],side:s.side,entry_price:Number(s.entry_price),
signal_type:s.signal_type||"unknown",quality:s.quality||"unknown",category:s.category||"Other",conviction:Number(s.conviction||0),
days_to_resolution:s.days_to_resolution,trade_ready_at_observation:Boolean(s.trade_ready),
strategy_version:SUGGESTION_ENGINE_VERSION,build_version:BUILD_VERSION});
@@ -1827,17 +1850,21 @@ function buildSignalCalibration(ledger){
const ret=clamp(Number(outcome.return||0),-1,2);if(!Number.isFinite(ret)||weight<=0)return;
const marketId=String(outcome.market_id||`unidentified-observation-${index}`);
const clusterId=String(outcome.event_key||outcome.event||marketId).trim().toLowerCase()||marketId;
learningFeatures(outcome).forEach(key=>{
const byEvent=clusters[key]||(clusters[key]={}),cluster=byEvent[clusterId]||(byEvent[clusterId]={weight:0,sum:0,observations:0});
cluster.weight+=weight;cluster.sum+=ret*weight;cluster.observations++;
const horizon=Number(outcome.target_horizon_hours),baseFeatures=learningFeatures(outcome);
const featureKeys=SIGNAL_EVAL_HORIZONS.includes(horizon)?[...baseFeatures,...baseFeatures.map(key=>`${key}|horizon:${horizon}`)]:baseFeatures;
featureKeys.forEach(key=>{
const byEvent=clusters[key]||(clusters[key]={}),cluster=byEvent[clusterId]||(byEvent[clusterId]={weight:0,currentWeight:0,sum:0,observations:0});
cluster.weight+=weight;if(version===SUGGESTION_ENGINE_VERSION)cluster.currentWeight+=weight;
cluster.sum+=ret*weight;cluster.observations++;
});
});
const buckets={};
Object.entries(clusters).forEach(([key,byEvent])=>{
const b=buckets[key]={weight:0,sum:0,sumSq:0,wins:0,count:0,observations:0};
const b=buckets[key]={weight:0,currentWeight:0,sum:0,sumSq:0,wins:0,count:0,observations:0};
Object.values(byEvent).forEach(cluster=>{
const clusterWeight=Math.min(1,Number(cluster.weight||0)),ret=Number(cluster.sum||0)/Math.max(0.0001,Number(cluster.weight||0));
b.weight+=clusterWeight;b.sum+=ret*clusterWeight;b.sumSq+=ret*ret*clusterWeight;b.wins+=(ret>0?clusterWeight:0);
b.currentWeight+=Math.min(clusterWeight,Number(cluster.currentWeight||0));
b.count++;b.observations+=Number(cluster.observations||0);
});
});
@@ -1850,21 +1877,24 @@ function buildSignalCalibration(ledger){
const currentEvents=new Set(currentOutcomes.map((outcome,index)=>String(outcome.event_key||outcome.event||outcome.market_id||`unidentified-current-${index}`).trim().toLowerCase()));
return {version:SUGGESTION_ENGINE_VERSION,samples:outcomes.length,markets:identifiedMarkets.size,events:identifiedEvents.size,
current_samples:currentOutcomes.length,current_markets:currentMarkets.size,current_events:currentEvents.size,buckets:learned,
promoted_buckets:learnedRows.filter(r=>r.weight>=8&&r.lower_bound>0.003).length,
demoted_buckets:learnedRows.filter(r=>r.weight>=8&&r.upper_bound<-0.003).length,
promoted_buckets:learnedRows.filter(r=>r.weight>=8&&r.current_weight>=5&&r.lower_bound>0.003).length,
demoted_buckets:learnedRows.filter(r=>r.weight>=8&&r.current_weight>=5&&r.upper_bound<-0.003).length,
expired_ungraded:Number(ledger&&ledger.expired_ungraded||0),
pending:((ledger&&ledger.pending)||[]).length};
}
function calibratedOpportunity(s,calibration){
const rows=learningFeatures(s).map(k=>calibration&&calibration.buckets&&calibration.buckets[k]).filter(Boolean);
const features=learningFeatures(s),buckets=calibration&&calibration.buckets||{};
const rows=features.map(k=>buckets[k]).filter(Boolean);
const independentWeight=rows.length?Math.max(...rows.map(r=>Number(r.weight||0))):0;
const score=rows.length?rows.reduce((sum,r)=>sum+r.score,0)/rows.length:0,confidence=independentWeight/(independentWeight+20);
const positiveRows=rows.filter(r=>Number(r.weight||0)>=8&&Number(r.lower_bound||0)>0.003);
const negativeRows=rows.filter(r=>Number(r.weight||0)>=8&&Number(r.upper_bound||0)<-0.003);
const horizonRows=features.map(feature=>({feature,rows:SIGNAL_EVAL_HORIZONS.map(horizon=>buckets[`${feature}|horizon:${horizon}`])}));
const rowIsCurrent=(row)=>Number(row&&row.weight||0)>=8&&Number(row&&row.current_weight||0)>=5;
const positiveRows=horizonRows.filter(group=>group.rows.every(row=>rowIsCurrent(row)&&Number(row.lower_bound||0)>0.003));
const negativeRows=horizonRows.filter(group=>group.rows.some(row=>rowIsCurrent(row)&&Number(row.upper_bound||0)<-0.003));
const promoted=positiveRows.length>=2&&negativeRows.length===0;
const demoted=negativeRows.length>=2&&positiveRows.length===0;
const trustedScore=promoted?positiveRows.reduce((sum,r)=>sum+Number(r.lower_bound||0),0)/positiveRows.length
:(demoted?negativeRows.reduce((sum,r)=>sum+Number(r.upper_bound||0),0)/negativeRows.length:score*0.10);
const trustedScore=promoted?positiveRows.reduce((sum,group)=>sum+Math.min(...group.rows.map(row=>Number(row.lower_bound||0))),0)/positiveRows.length
:(demoted?negativeRows.reduce((sum,group)=>sum+Math.min(...group.rows.filter(rowIsCurrent).map(row=>Number(row.upper_bound||0))),0)/negativeRows.length:score*0.10);
const state=promoted?"promoted":(demoted?"demoted":"observing");
return {score:+score.toFixed(4),confidence:+confidence.toFixed(3),state,promoted,demoted,
supporting_features:promoted?positiveRows.length:negativeRows.length,
@@ -1943,10 +1973,11 @@ function historicalOpportunityPrior(s){
requiredPromotionFeatures:[...new Set(rows.flatMap(row=>row.requiredPromotionFeatures||[]))],features:rows.map(row=>row.feature)};
}
function historicalPromotionMet(historical,calibrationModel){
return !historical.requiresPromotion||historical.requiredPromotionFeatures.every(key=>{
const row=calibrationModel&&calibrationModel.buckets&&calibrationModel.buckets[key];
return Number(row&&row.weight||0)>=8&&Number(row&&row.lower_bound||0)>0.003;
});
const buckets=calibrationModel&&calibrationModel.buckets||{};
return !historical.requiresPromotion||historical.requiredPromotionFeatures.every(key=>SIGNAL_EVAL_HORIZONS.every(horizon=>{
const row=buckets[`${key}|horizon:${horizon}`];
return Number(row&&row.weight||0)>=8&&Number(row&&row.current_weight||0)>=5&&Number(row&&row.lower_bound||0)>0.003;
}));
}
function applyAdaptiveMarketPromotion(s,calibrationModel){
if(!s||s.trade_ready||!s.entry_candidate)return s;
@@ -2664,8 +2695,8 @@ async function runDailyCycle(){
SNAP_TS=nowIso();
let markets=[],analysisMarkets=[],sugs=[],bundleSugs=[],cache=loadMarketCache(),runMode="live",cacheAgeMs=0;
try{
setStatus("loading the 500 most active markets…",true);
markets=await fetchMarkets(Math.ceil(ACTIVE_MARKET_FETCH_LIMIT/100),100,count=>setStatus(`loaded ${Math.min(count,ACTIVE_MARKET_FETCH_LIMIT).toLocaleString()} of 500 active markets…`,true));
setStatus("loading the 500 most active eligible markets…",true);
markets=await fetchMarkets(20,100,count=>setStatus(`loaded ${Math.min(count,ACTIVE_MARKET_FETCH_LIMIT).toLocaleString()} of 500 eligible active markets…`,true),ACTIVE_MARKET_FETCH_LIMIT);
analysisMarkets=selectMarketsForAnalysis(markets);
if(!analysisMarkets.length)throw new Error("No active markets returned");
setStatus(`analyzing ${analysisMarkets.length.toLocaleString()} most-active markets…`,true);
@@ -2929,7 +2960,7 @@ function decisionSummary(p){
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 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. 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.`:"";
const calibration=d.marketLearning?` Walk-forward calibration: ${d.marketLearning.samples||0} net-of-cost checkpoint observations across ${d.marketLearning.events||d.marketLearning.markets||0} event clusters / ${d.marketLearning.markets||0} markets, graded separately near ${SIGNAL_EVAL_HORIZONS.join("h and ")}h (${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 checkpoint${d.marketLearning.expired_ungraded?`, ${d.marketLearning.expired_ungraded} expired checkpoints`:""}; ${d.marketLearning.promoted_buckets||0} horizon-specific feature cohorts promoted and ${d.marketLearning.demoted_buckets||0} demoted. Promotion requires positive current-strategy evidence at both horizons across independent events. Missed windows expire rather than borrowing a later price. New observations prioritize under-sampled signal/side/category cohorts and independent events before repeats. Correlated outcome markets in one event count as one effective outcome. 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}`;
}
function renderAgentBrief(cfg,p,st){
@@ -4523,7 +4554,7 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
coreGapTradeLossPct:MAX_CORE_GAP_TRADE_LOSS_PCT*100,aggressiveGapTradeLossPct:MAX_AGGRESSIVE_GAP_TRADE_LOSS_PCT*100,
offlineEntryMaxAgeMinutes:OFFLINE_ENTRY_MAX_AGE_MS/60000,offlineCacheMaxAgeHours:OFFLINE_CACHE_MAX_AGE_MS/3600000,explorationPct:15,
networkTimeoutSeconds:NETWORK_REQUEST_TIMEOUT_MS/1000,priceTimeoutSeconds:PRICE_REQUEST_TIMEOUT_MS/1000,
staleCacheIsMarkOnly:true,strategyEvidenceSurvivesBuilds:true,signalEvaluationHours:SIGNAL_EVAL_HOURS,signalRetryHours:SIGNAL_LEDGER_RETRY_HOURS,
staleCacheIsMarkOnly:true,strategyEvidenceSurvivesBuilds:true,signalEvaluationHours:SIGNAL_EVAL_HOURS,signalEvaluationHorizons:SIGNAL_EVAL_HORIZONS,signalEvaluationToleranceHours:SIGNAL_EVAL_TOLERANCE_HOURS,signalRetryHours:SIGNAL_LEDGER_RETRY_HOURS,
signalDueFetchLimit:SIGNAL_LEDGER_DUE_FETCH_LIMIT,survivorshipSafeSignalGrading:true,
signalRoundTripCostCents:SIGNAL_ROUND_TRIP_COST*100,independentConfidence:true,eventClusteredCalibration:true,
onePendingObservationPerMarketSide:true,oldestPendingEvidenceFirst:true,coverageAwareObservationSampling:true,uncertaintyGatedCalibration:true,
@@ -4564,8 +4595,8 @@ function runEngineSelfTest(){
realized_pnl:-20,opened_at:hoursAgo(72+i),closed_at:closedAt});
const learningProfile=buildAdaptiveProfile(learner);
const calibrationLedger=defaultSignalLedger();
for(let i=0;i<16;i++)calibrationLedger.outcomes.push({market_id:`trend-market-${i}`,event_key:`trend-event-${i}`,signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42,return:0.12,evaluated_at:closedAt});
for(let i=0;i<16;i++)calibrationLedger.outcomes.push({market_id:`reversal-market-${i}`,event_key:`reversal-event-${i}`,signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42,return:-0.12,evaluated_at:closedAt});
for(let i=0;i<16;i++)SIGNAL_EVAL_HORIZONS.forEach(horizon=>calibrationLedger.outcomes.push({market_id:`trend-market-${i}`,event_key:`trend-event-${i}`,signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42,target_horizon_hours:horizon,strategy_version:SUGGESTION_ENGINE_VERSION,return:0.12,evaluated_at:closedAt}));
for(let i=0;i<16;i++)SIGNAL_EVAL_HORIZONS.forEach(horizon=>calibrationLedger.outcomes.push({market_id:`reversal-market-${i}`,event_key:`reversal-event-${i}`,signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42,target_horizon_hours:horizon,strategy_version:SUGGESTION_ENGINE_VERSION,return:-0.12,evaluated_at:closedAt}));
const calibrationProfile=buildSignalCalibration(calibrationLedger);
const learnedTrend=learnedOpportunity(AGENTS[0],learner,{market_id:"learn-trend",signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42},learningProfile,calibrationProfile);
const learnedReversal=learnedOpportunity(AGENTS[0],learner,{market_id:"learn-reversal",signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42},learningProfile,calibrationProfile);
@@ -4583,9 +4614,15 @@ function runEngineSelfTest(){
const ledgerState={signal_ledger:{pending:[{key:"ledger-test",market_id:"ledger-test",observed_at:hoursAgo(25),side:"YES",entry_price:0.40,
signal_type:"trend",quality:"confirmed",category:"Politics"}],outcomes:[]}};
updateSignalLedger(ledgerState,[market({id:"ledger-test",yes_price:0.50,no_price:0.50})],[]);
const secondHorizonState={signal_ledger:{pending:[{key:"ledger-second",market_id:"ledger-second",observed_at:hoursAgo(73),graded_horizons:[24],side:"YES",entry_price:0.40,
signal_type:"trend",quality:"confirmed",category:"Politics"}],outcomes:[]}};
updateSignalLedger(secondHorizonState,[market({id:"ledger-second",yes_price:0.52,no_price:0.48})],[]);
const earlyLedgerState={signal_ledger:{pending:[{key:"ledger-early",market_id:"ledger-early",observed_at:hoursAgo(13),side:"YES",entry_price:0.40,
signal_type:"trend",quality:"confirmed",category:"Politics"}],outcomes:[]}};
updateSignalLedger(earlyLedgerState,[market({id:"ledger-early",yes_price:0.50,no_price:0.50})],[]);
const missedFirstWindowState={signal_ledger:{pending:[{key:"ledger-missed-24",market_id:"ledger-missed-24",observed_at:hoursAgo(31),side:"YES",entry_price:0.40,
signal_type:"trend",quality:"confirmed",category:"Politics"}],outcomes:[]}};
updateSignalLedger(missedFirstWindowState,[market({id:"ledger-missed-24",yes_price:0.90,no_price:0.10})],[]);
const observationLedgerState={signal_ledger:defaultSignalLedger()};
updateSignalLedger(observationLedgerState,[],[{market_id:"observation-only",side:"YES",entry_price:0.42,signal_type:"trend",quality:"watch",
signal_confidence:0.62,trade_ready:false,jump_risk:false,category:"Politics",days_to_resolution:45}]);
@@ -4609,21 +4646,25 @@ function runEngineSelfTest(){
{key:"outside-b",market_id:"outside-active-scan",observed_at:hoursAgo(25)},
{key:"young",market_id:"too-young",observed_at:hoursAgo(2)}],outcomes:[]};
const dueFetchIds=pendingSignalMarketIds(dueFetchLedger,new Set(["known-active"]));
const retryLedgerState={signal_ledger:{pending:[{key:"retry",market_id:"temporarily-unavailable",observed_at:hoursAgo(100),side:"YES",entry_price:0.4}],outcomes:[]}};
const retryLedgerState={signal_ledger:{pending:[{key:"retry",market_id:"temporarily-unavailable",observed_at:hoursAgo(25),side:"YES",entry_price:0.4}],outcomes:[]}};
updateSignalLedger(retryLedgerState,[],[]);
const expiredLedgerState={signal_ledger:{pending:[{key:"expired",market_id:"never-returned",observed_at:hoursAgo(169),side:"YES",entry_price:0.4}],outcomes:[]}};
const expiredLedgerState={signal_ledger:{pending:[{key:"expired",market_id:"never-returned",observed_at:hoursAgo(79),side:"YES",entry_price:0.4}],outcomes:[]}};
updateSignalLedger(expiredLedgerState,[],[]);
const calibrationCandidate={signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42};
const calibrationFromReturns=(returns,candidate=calibrationCandidate)=>buildSignalCalibration({pending:[],outcomes:returns.map((ret,index)=>Object.assign({},candidate,
const calibrationFromReturns=(returns,candidate=calibrationCandidate,horizons=SIGNAL_EVAL_HORIZONS)=>buildSignalCalibration({pending:[],outcomes:returns.flatMap((ret,index)=>horizons.map(horizon=>Object.assign({},candidate,
{market_id:`${candidate.market_id||"calibration"}-${index}`,event_key:`${candidate.event_key||candidate.market_id||"calibration-event"}-${index}`,
strategy_version:SUGGESTION_ENGINE_VERSION,return:ret,evaluated_at:closedAt}))});
const repeatedMarketCalibration=buildSignalCalibration({pending:[],outcomes:Array.from({length:24},()=>Object.assign({},calibrationCandidate,
{market_id:"one-repeated-market",event_key:"one-repeated-event",strategy_version:SUGGESTION_ENGINE_VERSION,return:0.12,evaluated_at:closedAt}))});
target_horizon_hours:horizon,strategy_version:SUGGESTION_ENGINE_VERSION,return:ret,evaluated_at:closedAt})))});
const repeatedMarketCalibration=buildSignalCalibration({pending:[],outcomes:Array.from({length:24},()=>SIGNAL_EVAL_HORIZONS.map(horizon=>Object.assign({},calibrationCandidate,
{market_id:"one-repeated-market",event_key:"one-repeated-event",target_horizon_hours:horizon,strategy_version:SUGGESTION_ENGINE_VERSION,return:0.12,evaluated_at:closedAt}))).flat()});
const repeatedMarketState=calibratedOpportunity(calibrationCandidate,repeatedMarketCalibration);
const correlatedEventCalibration=buildSignalCalibration({pending:[],outcomes:Array.from({length:24},(_,index)=>Object.assign({},calibrationCandidate,
{market_id:`correlated-market-${index}`,event_key:"one-correlated-event",strategy_version:SUGGESTION_ENGINE_VERSION,return:0.12,evaluated_at:closedAt}))});
const correlatedEventCalibration=buildSignalCalibration({pending:[],outcomes:Array.from({length:24},(_,index)=>SIGNAL_EVAL_HORIZONS.map(horizon=>Object.assign({},calibrationCandidate,
{market_id:`correlated-market-${index}`,event_key:"one-correlated-event",target_horizon_hours:horizon,strategy_version:SUGGESTION_ENGINE_VERSION,return:0.12,evaluated_at:closedAt}))).flat()});
const correlatedEventState=calibratedOpportunity(calibrationCandidate,correlatedEventCalibration);
const singleCalibration=calibratedOpportunity(calibrationCandidate,calibrationFromReturns([0.10]));
const oneHorizonPositiveCalibration=calibratedOpportunity(calibrationCandidate,calibrationFromReturns(Array(24).fill(0.12),calibrationCandidate,[24]));
const previousOnlyProfile=buildSignalCalibration({pending:[],outcomes:Array.from({length:24},(_,index)=>SIGNAL_EVAL_HORIZONS.map(horizon=>Object.assign({},calibrationCandidate,
{market_id:`previous-only-${index}`,event_key:`previous-only-event-${index}`,target_horizon_hours:horizon,strategy_version:PREVIOUS_STRATEGY_VERSION,return:0.12,evaluated_at:closedAt}))).flat()});
const previousOnlyCalibration=calibratedOpportunity(calibrationCandidate,previousOnlyProfile);
const stablePositiveProfile=calibrationFromReturns(Array(24).fill(0.12));
const stablePositiveCalibration=calibratedOpportunity(calibrationCandidate,stablePositiveProfile);
const promotedTrendSuggestion=applyAdaptiveMarketPromotion(Object.assign({},trend,{entry_candidate:true}),stablePositiveProfile);
@@ -4685,20 +4726,25 @@ function runEngineSelfTest(){
const retiredFixture=accountingBook(0.45),retiredEquityBefore=equity(retiredFixture.book);
retiredFixture.pos.strategy_version=PREVIOUS_STRATEGY_VERSION;
markToMarket(retiredFixture.book,{[retiredFixture.pos.market_id]:market({id:retiredFixture.pos.market_id,yes_price:0.45,no_price:0.55})},AGENTS[0],{policyExits:true,executeTrades:true});
const nonBinaryMarketRejected=normalizeMarket({id:"over-under-test",question:"Over/Under 2.5",outcomes:'["Over","Under"]',outcomePrices:'["0.5","0.5"]',acceptingOrders:true})===null;
const bundleSuggestion=negativeRiskBundleSuggestion({id:"bundle-test",title:"Three-way result",slug:"bundle-test",negRisk:true,enableNegRisk:true,tags:[{slug:"sports",label:"Sports"}],markets:[
{id:"bundle-a",question:"A wins",outcomePrices:'["0.415","0.585"]',clobTokenIds:'["a-yes","a-no"]',bestBid:0.41,bestAsk:0.42,liquidityNum:30000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"bundle-b",question:"B wins",outcomePrices:'["0.315","0.685"]',clobTokenIds:'["b-yes","b-no"]',bestBid:0.31,bestAsk:0.32,liquidityNum:28000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"bundle-draw",question:"Draw",outcomePrices:'["0.305","0.695"]',clobTokenIds:'["d-yes","d-no"]',bestBid:0.30,bestAsk:0.31,liquidityNum:20000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"bundle-a",question:"A wins",outcomes:'["Yes","No"]',outcomePrices:'["0.415","0.585"]',clobTokenIds:'["a-yes","a-no"]',bestBid:0.41,bestAsk:0.42,liquidityNum:30000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"bundle-b",question:"B wins",outcomes:'["Yes","No"]',outcomePrices:'["0.315","0.685"]',clobTokenIds:'["b-yes","b-no"]',bestBid:0.31,bestAsk:0.32,liquidityNum:28000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"bundle-draw",question:"Draw",outcomes:'["Yes","No"]',outcomePrices:'["0.305","0.695"]',clobTokenIds:'["d-yes","d-no"]',bestBid:0.30,bestAsk:0.31,liquidityNum:20000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
]});
const yesBundleSuggestion=negativeRiskBundleSuggestion({id:"bundle-yes-test",title:"Three-way YES result",slug:"bundle-yes-test",negRisk:true,enableNegRisk:true,markets:[
{id:"yes-bundle-a",question:"A wins",outcomePrices:'["0.295","0.705"]',clobTokenIds:'["ya-yes","ya-no"]',bestBid:0.29,bestAsk:0.30,liquidityNum:30000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"yes-bundle-b",question:"B wins",outcomePrices:'["0.305","0.695"]',clobTokenIds:'["yb-yes","yb-no"]',bestBid:0.30,bestAsk:0.31,liquidityNum:28000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"yes-bundle-draw",question:"Draw",outcomePrices:'["0.335","0.665"]',clobTokenIds:'["yd-yes","yd-no"]',bestBid:0.33,bestAsk:0.34,liquidityNum:20000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"yes-bundle-a",question:"A wins",outcomes:'["Yes","No"]',outcomePrices:'["0.295","0.705"]',clobTokenIds:'["ya-yes","ya-no"]',bestBid:0.29,bestAsk:0.30,liquidityNum:30000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"yes-bundle-b",question:"B wins",outcomes:'["Yes","No"]',outcomePrices:'["0.305","0.695"]',clobTokenIds:'["yb-yes","yb-no"]',bestBid:0.30,bestAsk:0.31,liquidityNum:28000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
{id:"yes-bundle-draw",question:"Draw",outcomes:'["Yes","No"]',outcomePrices:'["0.335","0.665"]',clobTokenIds:'["yd-yes","yd-no"]',bestBid:0.33,bestAsk:0.34,liquidityNum:20000,volumeNum:100000,volume24hr:10000,acceptingOrders:true},
]});
const tinyReturnBundle=negativeRiskBundleSuggestion({id:"bundle-tiny",title:"Ten-way result",slug:"bundle-tiny",negRisk:true,enableNegRisk:true,markets:Array.from({length:10},(_,i)=>({
id:`bundle-tiny-${i}`,question:`Outcome ${i+1}`,outcomePrices:'["0.1055","0.8945"]',clobTokenIds:`["tiny-${i}-yes","tiny-${i}-no"]`,
id:`bundle-tiny-${i}`,question:`Outcome ${i+1}`,outcomes:'["Yes","No"]',outcomePrices:'["0.1055","0.8945"]',clobTokenIds:`["tiny-${i}-yes","tiny-${i}-no"]`,
bestBid:0.1055,bestAsk:0.1065,liquidityNum:30000,volumeNum:100000,volume24hr:10000,acceptingOrders:true,
}))});
const nonBinaryBundleRejected=negativeRiskBundleSuggestion({id:"bundle-label-test",title:"Over/Under",slug:"bundle-label-test",negRisk:true,enableNegRisk:true,markets:[
{id:"over-a",question:"Over/Under 1.5",outcomes:'["Over","Under"]',outcomePrices:'["0.45","0.55"]',clobTokenIds:'["over-a","under-a"]',bestBid:0.44,bestAsk:0.45,liquidityNum:30000,acceptingOrders:true},
{id:"over-b",question:"Over/Under 2.5",outcomes:'["Over","Under"]',outcomePrices:'["0.45","0.55"]',clobTokenIds:'["over-b","under-b"]',bestBid:0.44,bestAsk:0.45,liquidityNum:30000,acceptingOrders:true},
]})===null;
const bundleBook=defaultPortfolio();
const offlineBundleSuggestion=prepareCycleSuggestions([bundleSuggestion],"offline-cache",true)[0];
openPositions(bundleBook,AGENTS.find(a=>a.id==="value"),[bundleSuggestion],"All",{
@@ -4815,20 +4861,26 @@ function runEngineSelfTest(){
&&queueLedgerState.signal_ledger.pending.some(x=>x.market_id==="new-market-0")
&&!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,
firstHorizonGradesWithoutLookahead:ledgerState.signal_ledger.pending.length===1&&ledgerState.signal_ledger.pending[0].graded_horizons.includes(24)
&&ledgerState.signal_ledger.outcomes.length===1&&ledgerState.signal_ledger.outcomes[0].target_horizon_hours===24&&ledgerState.signal_ledger.outcomes[0].return===0.2375,
secondHorizonGradesAndCompletes:secondHorizonState.signal_ledger.pending.length===0&&secondHorizonState.signal_ledger.outcomes.length===1
&&secondHorizonState.signal_ledger.outcomes[0].target_horizon_hours===72,
holdsSignalsUntilPolicyHorizon:earlyLedgerState.signal_ledger.pending.length===1&&earlyLedgerState.signal_ledger.outcomes.length===0,
missed24hWindowIsNotBackfilled:missedFirstWindowState.signal_ledger.pending.length===1&&missedFirstWindowState.signal_ledger.outcomes.length===0,
fetchesMaturedMarketsOutsideActiveScan:dueFetchIds.length===1&&dueFetchIds[0]==="outside-active-scan",
keepsUnavailableGradesQueued:retryLedgerState.signal_ledger.pending.length===1&&retryLedgerState.signal_ledger.outcomes.length===0,
expiresOnlyAfterRetryWindow:expiredLedgerState.signal_ledger.pending.length===0&&expiredLedgerState.signal_ledger.expired_ungraded===1,
expiresOnlyAfterRetryWindow:expiredLedgerState.signal_ledger.pending.length===0&&expiredLedgerState.signal_ledger.expired_ungraded===2,
ledgerIsNetOfCosts:ledgerState.signal_ledger.outcomes[0].gross_return===0.25&&ledgerState.signal_ledger.outcomes[0].estimated_cost_return===0.0125,
independentCalibrationConfidence:singleCalibration.confidence<0.06,
repeatedSnapshotsCountAsOneMarket:repeatedMarketCalibration.markets===1&&repeatedMarketCalibration.buckets["signal:trend"].samples===1
&&repeatedMarketCalibration.buckets["signal:trend"].observations===24&&repeatedMarketState.state==="observing",
&&repeatedMarketCalibration.buckets["signal:trend"].observations===48&&repeatedMarketState.state==="observing",
correlatedMarketsCountAsOneEvent:correlatedEventCalibration.markets===24&&correlatedEventCalibration.events===1
&&correlatedEventCalibration.buckets["signal:trend"].samples===1&&correlatedEventState.state==="observing",
stablePositiveEventCount:stablePositiveProfile.events,
stablePositiveMarketCount:stablePositiveProfile.markets,
distinctEventsCanPromote:stablePositiveProfile.events===24&&stablePositiveCalibration.state==="promoted"&&stablePositiveCalibration.supporting_features>=2,
oneHorizonCannotPromote:oneHorizonPositiveCalibration.state==="observing"&&!oneHorizonPositiveCalibration.promoted,
previousStrategyCannotPromote:previousOnlyCalibration.state==="observing"&&!previousOnlyCalibration.promoted,
legacyBuildLineageRemainsHistorical:normalizedStrategyVersion(41)===40,
previousStrategyIsDownWeighted:legacyBuildCalibration.current_samples===0&&legacyBuildCalibration.buckets["signal:trend"].weight>0.54&&legacyBuildCalibration.buckets["signal:trend"].weight<=0.55,
currentStrategyKeepsFullWeight:currentStrategyCalibration.current_samples===1&&currentStrategyCalibration.buckets["signal:trend"].weight>0.99,
@@ -4857,6 +4909,8 @@ function runEngineSelfTest(){
adaptiveReturnSetsLeader:adaptiveRankFixture[0].c.id==="adaptive-leader",
},
bundleArbitrage:{
rejectsNonBinaryMarketLabels:nonBinaryMarketRejected,
rejectsNonBinaryBundleLabels:nonBinaryBundleRejected,
identifiesPositiveCompleteNoBundle:bundleSuggestion&&bundleSuggestion.trade_ready&&bundleSuggestion.bundle_side==="NO"&&bundleSuggestion.bundle_legs.every(leg=>leg.side==="NO")&&bundleSuggestion.bundle_net_profit_per_unit===0.005,
identifiesPositiveCompleteYesBundle:yesBundleSuggestion&&yesBundleSuggestion.trade_ready&&yesBundleSuggestion.bundle_side==="YES"&&yesBundleSuggestion.bundle_legs.every(leg=>leg.side==="YES")&&yesBundleSuggestion.bundle_net_profit_per_unit===0.035,
rejectsEconomicallyTinyLargeBundle:tinyReturnBundle===null,
+1
View File
@@ -2,6 +2,7 @@
"type": "module",
"scripts": {
"evaluate:neg-risk": "node scripts/evaluate-neg-risk.mjs",
"evaluate:dominance": "node scripts/evaluate-dominance.mjs",
"evaluate:signals": "node scripts/evaluate-signals.mjs",
"evaluate:settlements": "node scripts/evaluate-settlements.mjs",
"test:server-state": "node scripts/test-server-state.mjs"
+126
View File
@@ -0,0 +1,126 @@
const GAMMA = "https://gamma-api.polymarket.com";
const EVENT_LIMIT = Math.max(20, Math.min(1000, Number(process.env.DOMINANCE_EVENTS || 500)));
const COST_CENTS = Math.max(0, Math.min(5, Number(process.env.DOMINANCE_COST_CENTS || 0.5)));
const MIN_LIQUIDITY = Math.max(0, Number(process.env.DOMINANCE_MIN_LIQUIDITY || 1000));
const MIN_NET_PROFIT = Math.max(0, Number(process.env.DOMINANCE_MIN_NET_PROFIT || 0.003));
const MIN_NET_RETURN = Math.max(0, Number(process.env.DOMINANCE_MIN_NET_RETURN || 0.0015));
function parseJson(value) {
if (Array.isArray(value)) return value;
try { return JSON.parse(value || "[]"); } catch { return []; }
}
function number(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
async function fetchJson(url, attempts = 3) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(20000), headers: { accept: "application/json" } });
if (response.ok) return response.json();
lastError = new Error(`${response.status} ${response.statusText}`);
if (response.status !== 429 && response.status < 500) break;
} catch (error) { lastError = error; }
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
}
throw lastError || new Error("request failed");
}
async function fetchEvents(limit) {
const events = [], pageSize = 100;
for (let offset = 0; offset < limit; offset += pageSize) {
const size = Math.min(pageSize, limit - offset);
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false",
limit: String(size), offset: String(offset), order: "volume24hr", ascending: "false" });
const page = await fetchJson(`${GAMMA}/events?${params}`);
if (!Array.isArray(page) || !page.length) break;
events.push(...page);
if (page.length < size) break;
}
return events.slice(0, limit);
}
const THRESHOLD_PATTERNS = [
{ direction: "above", outcomeMode: "over-under", regex: /(\bover\s*\/\s*under\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
{ direction: "above", outcomeMode: "yes-no", regex: /(\b(?:above|over|at least|higher than|greater than)\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
{ direction: "below", outcomeMode: "yes-no", regex: /(\b(?:below|(?<!\/)under|at most|lower than|less than)\s*(?:[$€£]\s*)?)([0-9]+(?:,[0-9]{3})*(?:\.[0-9]+)?)\s*(k|m|b|%|bps)?\b/i },
];
function parseThreshold(question) {
const text = String(question || "").trim();
for (const pattern of THRESHOLD_PATTERNS) {
const match = pattern.regex.exec(text);
if (!match) continue;
const rawValue = Number(match[2].replaceAll(",", ""));
const suffix = String(match[3] || "").toLowerCase();
const multiplier = suffix === "k" ? 1e3 : suffix === "m" ? 1e6 : suffix === "b" ? 1e9 : 1;
const value = rawValue * multiplier;
if (!Number.isFinite(value)) continue;
const valueStart = match.index + match[1].length, valueEnd = valueStart + match[2].length;
const stem = `${text.slice(0, valueStart)}{threshold}${text.slice(valueEnd)}`.toLowerCase().replace(/\s+/g, " ").trim();
return { direction: pattern.direction, outcomeMode: pattern.outcomeMode, value, stem };
}
return null;
}
function quoteMarket(raw, event) {
const threshold = parseThreshold(raw.question);
const tokens = parseJson(raw.clobTokenIds).map(String), prices = parseJson(raw.outcomePrices).map(number);
const outcomes = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const bid = number(raw.bestBid), ask = number(raw.bestAsk), liquidity = number(raw.liquidityNum || raw.liquidity) || 0;
const expectedOutcomes = threshold?.outcomeMode === "over-under" ? ["over", "under"] : ["yes", "no"];
if (!threshold || tokens.length !== 2 || prices.length !== 2 || outcomes[0] !== expectedOutcomes[0] || outcomes[1] !== expectedOutcomes[1] || !tokens[0] || !tokens[1]
|| bid == null || ask == null || bid < 0 || ask > 1 || ask < bid || liquidity < MIN_LIQUIDITY
|| raw.closed || raw.active === false || raw.acceptingOrders === false) return null;
return { ...threshold, marketId: String(raw.id || ""), question: raw.question || "", yesBid: bid, yesAsk: ask,
yesMid: prices[0], noMid: prices[1], yesToken: tokens[0], noToken: tokens[1], liquidity,
url: event.slug ? `https://polymarket.com/event/${event.slug}` : "" };
}
function evaluateEvent(event) {
const quotes = (Array.isArray(event.markets) ? event.markets : []).map((market) => quoteMarket(market, event)).filter(Boolean);
const groups = new Map();
for (const quote of quotes) {
const key = `${quote.direction}|${quote.stem}`;
const group = groups.get(key) || [];
group.push(quote); groups.set(key, group);
}
const candidates = [];
for (const group of groups.values()) {
if (group.length < 2 || new Set(group.map((quote) => quote.value)).size !== group.length) continue;
const ordered = [...group].sort((a, b) => a.value - b.value);
for (let left = 0; left < ordered.length - 1; left++) {
for (let right = left + 1; right < ordered.length; right++) {
const lower = ordered[left], higher = ordered[right];
const superset = lower.direction === "above" ? lower : higher;
const subset = lower.direction === "above" ? higher : lower;
const yesEntry = superset.yesAsk + COST_CENTS / 100;
const noEntry = 1 - subset.yesBid + COST_CENTS / 100;
const cost = yesEntry + noEntry, profit = 1 - cost, netReturn = cost > 0 ? profit / cost : 0;
candidates.push({ eventId: String(event.id || ""), title: event.title || "", direction: lower.direction,
supersetThreshold: superset.value, subsetThreshold: subset.value, cost, payout: 1, profit, netReturn,
minimumLiquidity: Math.min(superset.liquidity, subset.liquidity), url: superset.url,
legs: [{ marketId: superset.marketId, question: superset.question, side: "YES", entry: yesEntry },
{ marketId: subset.marketId, question: subset.question, side: "NO", entry: noEntry }] });
}
}
}
return candidates;
}
const events = await fetchEvents(EVENT_LIMIT);
const candidates = events.flatMap(evaluateEvent).sort((a, b) => b.netReturn - a.netReturn);
const actionable = candidates.filter((candidate) => candidate.profit >= MIN_NET_PROFIT && candidate.netReturn >= MIN_NET_RETURN);
const compact = (candidate) => ({ eventId: candidate.eventId, title: candidate.title, direction: candidate.direction,
supersetThreshold: candidate.supersetThreshold, subsetThreshold: candidate.subsetThreshold,
cost: +candidate.cost.toFixed(4), payout: candidate.payout, profit: +candidate.profit.toFixed(4),
netReturn: +candidate.netReturn.toFixed(4), minimumLiquidity: +candidate.minimumLiquidity.toFixed(2),
url: candidate.url, legs: candidate.legs.map((leg) => ({ ...leg, entry: +leg.entry.toFixed(4) })) });
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedEvents: EVENT_LIMIT, fetchedEvents: events.length,
eligibleDominancePairs: candidates.length, actionablePairs: actionable.length, estimatedCostCentsPerLeg: COST_CENTS,
minimumLiquidityPerLeg: MIN_LIQUIDITY, minimumNetProfitPerPair: MIN_NET_PROFIT, minimumNetReturn: MIN_NET_RETURN,
actionable: actionable.slice(0, 50).map(compact), bestObserved: candidates.slice(0, 20).map(compact) }, null, 2));
+3 -1
View File
@@ -48,11 +48,13 @@ function evaluateEvent(event) {
const allMarkets = Array.isArray(event.markets) ? event.markets : [];
if (allMarkets.length < 2 || allMarkets.some((market) => market.closed || market.active === false || market.acceptingOrders === false)) return null;
const legs = allMarkets.map((market) => {
const outcomes = parseJson(market.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const prices = parseJson(market.outcomePrices).map(number);
return { id: String(market.id || ""), question: market.question || "", yes: prices[0],
binaryLabels: outcomes[0] === "yes" && outcomes[1] === "no",
bid: number(market.bestBid), ask: number(market.bestAsk), liquidity: number(market.liquidityNum || market.liquidity) || 0 };
});
if (legs.some((leg) => !leg.id || leg.yes == null || leg.bid == null || leg.ask == null
if (legs.some((leg) => !leg.binaryLabels || !leg.id || leg.yes == null || leg.bid == null || leg.ask == null
|| leg.bid < 0 || leg.ask > 1 || leg.ask < leg.bid || leg.liquidity < MIN_LIQUIDITY)) return null;
const count = legs.length, costPerLeg = COST_CENTS / 100;
const yesCost = legs.reduce((sum, leg) => sum + leg.ask, 0) + count * costPerLeg;
+3 -2
View File
@@ -95,11 +95,12 @@ async function fetchResolvedMarkets(limit) {
const page = await fetchJson(`${GAMMA}/markets?${params}`);
if (!Array.isArray(page) || !page.length) break;
for (const market of page) {
const id = String(market.id || ""), outcomes = parseJson(market.outcomePrices).map(Number);
const id = String(market.id || ""), labels = parseJson(market.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
const outcomes = parseJson(market.outcomePrices).map(Number);
const tokens = parseJson(market.clobTokenIds), closedAt = toTimestamp(market.closedTime || market.endDate);
const resolved = outcomes.length === 2 && outcomes.every(Number.isFinite)
&& ((outcomes[0] >= 0.99 && outcomes[1] <= 0.01) || (outcomes[1] >= 0.99 && outcomes[0] <= 0.01));
if (!id || seen.has(id) || !resolved || tokens.length !== 2 || !closedAt) continue;
if (!id || seen.has(id) || !resolved || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || !closedAt) continue;
seen.add(id); raw.push({ id, question: market.question || "", category: categoryOf(market),
eventId: String(market.events?.[0]?.id || id),
tokenId: String(tokens[0]), finalYes: outcomes[0] >= 0.99 ? 1 : 0, closedAt,
+9 -6
View File
@@ -260,25 +260,28 @@ function chronologicalEvaluation(rows) {
async function fetchActiveMarkets(limit) {
const markets = [], seen = new Set(), pageSize = 100;
for (let offset = 0; offset < limit; offset += pageSize) {
for (let offset = 0; markets.length < limit && offset < limit * 4; offset += pageSize) {
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false", include_tag: "true",
limit: String(Math.min(pageSize, limit - offset)), offset: String(offset), order: "volume24hr", ascending: "false" });
limit: String(pageSize), offset: String(offset), order: "volume24hr", ascending: "false" });
const page = await fetchJson(`${GAMMA}/markets?${params}`);
if (!Array.isArray(page) || !page.length) break;
for (const market of page) {
const id = String(market.id || "");
if (!id || seen.has(id)) continue;
const id = String(market.id || ""), labels = parseJson(market.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no") continue;
seen.add(id); markets.push(market);
if (markets.length >= limit) break;
}
if (page.length < Math.min(pageSize, limit - offset)) break;
if (page.length < pageSize) break;
}
return markets.slice(0, limit);
}
const rawMarkets = await fetchActiveMarkets(MARKET_LIMIT);
const markets = rawMarkets.map((raw) => ({ id: String(raw.id), question: raw.question || "", category: categoryOf(raw),
binaryLabels: parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase()),
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || raw.id),
tokenId: String(parseJson(raw.clobTokenIds)[0] || "") })).filter((market) => market.id && market.tokenId);
tokenId: String(parseJson(raw.clobTokenIds)[0] || "") })).filter((market) => market.id && market.tokenId
&& market.binaryLabels[0] === "yes" && market.binaryLabels[1] === "no");
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=1m&fidelity=60`);
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE_NAME = "polymarket-arena-build-61";
const CACHE_NAME = "polymarket-arena-build-62";
const APP_SHELL = ["/", "/index.html", "/personal.html", "/cycle-worker.js"];
self.addEventListener("install", event => {