mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-24 04:58:08 +00:00
Remove emotion-driven agent sizing
This commit is contained in:
@@ -15,7 +15,7 @@ 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 66 also installs an offline app shell and caches timestamped
|
||||
Vercel Blob. Build 67 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.
|
||||
@@ -26,7 +26,7 @@ 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 66 ranks the competition by each agent's return since Strategy 55 began.
|
||||
Build 67 ranks the competition by each agent's return since Strategy 56 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.
|
||||
|
||||
@@ -37,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 55 treats each binary stake as capable of falling to zero even when the
|
||||
Strategy 56 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
|
||||
@@ -82,9 +82,25 @@ Run `npm run evaluate:sports-favorites` for the separate pregame favorite audit.
|
||||
It anchors decisions to the published game start, rejects stale prices, charges a
|
||||
modeled five-cent cost, forms equal-dollar event baskets, and uses a chronological
|
||||
60/20/20 split. The clean 3,000-market run produced nine train-pass rules but zero
|
||||
validation selections, so Strategy 55 does not promote the apparent sports-favorite
|
||||
validation selections, so Strategy 56 does not promote the apparent sports-favorite
|
||||
edge or use the holdout set to rescue it.
|
||||
|
||||
Run `npm run evaluate:settlement-calibration` for the stricter settlement-bias
|
||||
search across up to 5,000 resolved markets. It uses a 60/20/20 chronological
|
||||
split, a one-market-per-event limit, 95% event-clustered confidence bounds, four
|
||||
stability windows, a 24-hour market-age minimum, and a recent non-flat price
|
||||
history requirement. Before those activity and overlap controls, four sports
|
||||
rules appeared to pass holdout because correlated props shared one event and
|
||||
some histories contained inactive default prices. After correction, 11 of
|
||||
1,400 rules passed training and zero passed validation. Strategy 56 therefore
|
||||
does not install a static side, category, price-band, or settlement-horizon bet.
|
||||
|
||||
Strategy 56 also removes the last emotion-driven sizing path. Agent mood and
|
||||
leaderboard urgency remain visible in reports, but neither can increase capital.
|
||||
A positive peer signal receives at most a 5% sizing lift, and only when both the
|
||||
agent's realized-trade cohort and the independent walk-forward market cohort are
|
||||
already promoted. One learner, popularity, or urgency alone leaves size at 1.00x.
|
||||
|
||||
Run `npm run evaluate:liquidity` to inspect live reward-scoring markets using
|
||||
both public outcome books. It recomputes the minimum-size-adjusted midpoint,
|
||||
upper-bounds competing maker score from visible qualifying depth, enforces the
|
||||
@@ -100,7 +116,7 @@ and 24-hour horizons. Zero rules passed training, validation, or untouched
|
||||
holdout. At three hours, the broad 0.5-cent quote-gap rule still lost 0.63% per
|
||||
event in holdout; only 0.61% of observations completed both legs while 23.33%
|
||||
produced adverse one-leg inventory. Wider quotes traded less but remained
|
||||
negative. Strategy 55 therefore does not risk paper capital on an unproven
|
||||
negative. Strategy 56 therefore does not risk paper capital on an unproven
|
||||
maker rule.
|
||||
|
||||
Value Hunter now tracks up to six zero-capital shadow quote pairs selected by
|
||||
|
||||
+18
-8
@@ -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 55 · build 66</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 56 · build 67</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 66 active:</b> directional and sports-favorite rules failed clean holdout tests. The maker learner now measures both public order books, qualifying depth, competition, and the $1 reward payout floor before starting a zero-capital observation. Estimated rewards remain separate from P&L, and capital stays disabled until current category, spread, and reward-yield cohorts independently pass. Offline snapshots can value positions but cannot invent fills. This remains paper trading; profits are not guaranteed.</div>
|
||||
<div class="live-build-banner"><b>Build 67 active:</b> a corrected 5,000-market settlement audit rejected every static side, price, category, and horizon rule after event-overlap and stale-price controls. Emotion is commentary only and can never increase stake size; a small peer boost requires independent promotion from both personal outcomes and the shared walk-forward ledger. Reward-book maker observations remain zero-capital until current cohorts prove positive net outcomes. Offline snapshots can value positions but cannot invent fills. This remains paper trading; profits are not guaranteed.</div>
|
||||
|
||||
<!-- ============ 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>
|
||||
|
||||
<footer>
|
||||
Build 66 · Adaptive strategy 55 · Paper trading only · Live prices from Polymarket's public Gamma and CLOB APIs · Not financial advice ·
|
||||
Build 67 · Adaptive strategy 56 · Paper trading only · Live prices from Polymarket's public Gamma and CLOB APIs · 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 = 66;
|
||||
const SUGGESTION_ENGINE_VERSION = 55;
|
||||
const PREVIOUS_STRATEGY_VERSION = 54;
|
||||
const BUILD_VERSION = 67;
|
||||
const SUGGESTION_ENGINE_VERSION = 56;
|
||||
const PREVIOUS_STRATEGY_VERSION = 55;
|
||||
const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40});
|
||||
function normalizedStrategyVersion(value){
|
||||
const version=Number(value||0);
|
||||
@@ -2048,6 +2048,9 @@ function emotionalState(ret,trail,trend,rank){
|
||||
if(ret<0)return {mood:"restless",urgency:0.58,label:"Need progress"};
|
||||
return {mood:"focused",urgency:0.50,label:"Focused"};
|
||||
}
|
||||
function peerEvidenceSizeMultiplier(s){
|
||||
return Number(s&&s.peer_boost||0)>0&&s&&s.learning_state==="promoted"&&s.market_learning_state==="promoted"?1.05:1;
|
||||
}
|
||||
function adaptiveDecision(cfg,p,rank,total,leaderEq,marketLearning=null){
|
||||
const eq=equity(p),ret=(eq/p.starting_balance-1)*100,trail=((leaderEq||eq)-eq)/p.starting_balance*100;
|
||||
const positionValue=(p.positions||[]).reduce((sum,pos)=>sum+Number(pos.value||pos.shares*pos.current_price||0),0);
|
||||
@@ -2848,7 +2851,7 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
|
||||
else if(cfg.flat){frac=d.maxFrac;}
|
||||
else{const base=(s.peer_conviction/100)*Math.min(1,effectiveEntryEdge(s)/EDGE_SCALE);frac=Math.min(d.maxFrac,cfg.kelly*base);}
|
||||
if(s.peer_boost<0)frac*=0.82;
|
||||
if(s.peer_boost>0&&decision&&decision.urgency>0.65)frac*=1.08;
|
||||
frac*=peerEvidenceSizeMultiplier(s);
|
||||
frac*=Number(s.learning_multiplier||1);
|
||||
let stake=Math.min(eq*frac,investable);
|
||||
const riskBudgetPct=tradeLossBudgetPct(cfg,s);
|
||||
@@ -4893,6 +4896,7 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
|
||||
buildAdaptiveProfile,
|
||||
historicalOpportunityPrior,
|
||||
learnedOpportunity,
|
||||
peerEvidenceSizeMultiplier,
|
||||
tradeLossBudgetPct,
|
||||
boundedStakeForRisk,
|
||||
historicalPriceFeatures,
|
||||
@@ -4915,7 +4919,8 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
|
||||
makerTouchSource:"Polymarket CLOB batch price history",makerOutcomeLimit:MAKER_OUTCOME_LIMIT,makerMinimumCohortAttempts:MAKER_MIN_COHORT_ATTEMPTS,
|
||||
makerMinimumPromotionLocks:MAKER_MIN_PROMOTION_LOCKS,makerMaximumLearningMultiplier:1.25,makerCapitalExplorationPct:0,hypotheticalRewardsCredited:false,
|
||||
makerCompetitionSource:"shared public CLOB order-book audit",makerPromotionCohorts:["category","spread","reward-yield"],makerRewardPayoutMinimum:1,
|
||||
historicalPrior:"All tested directional and sports-favorite rules failed clean validation and remain observation-only; live-priced complete negative-risk bundles may trade; paired maker quotes require a public-book reward audit and remain zero-capital observations until current category, spread, and reward-yield cohorts independently promote"}),
|
||||
emotionCanIncreaseSize:false,dualPromotionPeerBoostPct:5,
|
||||
historicalPrior:"All tested directional, sports-favorite, and corrected settlement-calibration rules failed clean validation and remain observation-only; live-priced complete negative-risk bundles may trade; paired maker quotes require a public-book reward audit and remain zero-capital observations until current category, spread, and reward-yield cohorts independently promote"}),
|
||||
});
|
||||
function runEngineSelfTest(){
|
||||
const market=(overrides={})=>Object.assign({
|
||||
@@ -5049,6 +5054,9 @@ function runEngineSelfTest(){
|
||||
const lossProfile=buildAdaptiveProfile(lossLearner);
|
||||
const lossOpportunity=learnedOpportunity(AGENTS[0],lossLearner,{market_id:lossMarketId,signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42},lossProfile);
|
||||
const lossDecision=adaptiveDecision(AGENTS[0],lossLearner,5,10,STARTING_BALANCE);
|
||||
const urgencyWithoutEvidenceMultiplier=peerEvidenceSizeMultiplier({peer_boost:4,learning_state:"observing",market_learning_state:"observing"});
|
||||
const urgencyWithOnePromotionMultiplier=peerEvidenceSizeMultiplier({peer_boost:4,learning_state:"promoted",market_learning_state:"observing"});
|
||||
const dualPromotionMultiplier=peerEvidenceSizeMultiplier({peer_boost:4,learning_state:"promoted",market_learning_state:"promoted"});
|
||||
const legacyLossLearner=defaultPortfolio();
|
||||
for(let i=0;i<4;i++)legacyLossLearner.closed.push({strategy_version:PREVIOUS_STRATEGY_VERSION,signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42,original_cost:100,
|
||||
realized_pnl:-50,opened_at:hoursAgo(72+i),closed_at:closedAt});
|
||||
@@ -5310,6 +5318,8 @@ function runEngineSelfTest(){
|
||||
stableNegativeIsBlocked:stableNegativeCalibration.state==="demoted"&&!stableNegativeCalibration.allowed,
|
||||
noisyEvidenceStaysNeutral:noisyCalibration.state==="observing"&&noisyCalibration.multiplier>=0.99&&noisyCalibration.multiplier<=1.01,
|
||||
blocksConfirmedLosingRegime:!lossOpportunity.allowed,containmentMode:lossDecision.mode,containmentMaxNew:lossDecision.maxNew,
|
||||
urgencyCannotIncreaseSize:urgencyWithoutEvidenceMultiplier===1&&urgencyWithOnePromotionMultiplier===1,
|
||||
dualIndependentPromotionAllowsBoundedPeerBoost:dualPromotionMultiplier===1.05,
|
||||
legacyLossDoesNotFreezeCurrentEngine:legacyLossDecision.mode!=="Loss Regime Containment"},
|
||||
riskBudget:{core:coreRiskBudget,aggressiveGap:aggressiveGapBudget,blocksExactRange:intervalContract.jump_risk&&!intervalContract.trade_ready,
|
||||
blocksPathDependentBarrier:pathBarrierContract.jump_risk&&!pathBarrierContract.trade_ready,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"evaluate:maker": "node scripts/evaluate-maker.mjs",
|
||||
"evaluate:signals": "node scripts/evaluate-signals.mjs",
|
||||
"evaluate:settlements": "node scripts/evaluate-settlements.mjs",
|
||||
"evaluate:settlement-calibration": "node scripts/evaluate-settlement-calibration.mjs",
|
||||
"evaluate:sports-favorites": "node scripts/evaluate-sports-favorites.mjs",
|
||||
"test:server-state": "node scripts/test-server-state.mjs"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
const GAMMA = "https://gamma-api.polymarket.com";
|
||||
const CLOB = "https://clob.polymarket.com";
|
||||
const MARKET_LIMIT = Math.max(500, Math.min(5000, Number(process.env.CALIBRATION_MARKETS || 5000)));
|
||||
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.CALIBRATION_CONCURRENCY || 10)));
|
||||
const COST = Math.max(0, Math.min(0.10, Number(process.env.CALIBRATION_COST_CENTS || 1) / 100));
|
||||
const HORIZONS = [...new Set(String(process.env.CALIBRATION_HORIZONS || "1,3,7,14,30")
|
||||
.split(",").map(Number).filter((value) => Number.isFinite(value) && value >= 1 && value <= 180))].sort((a, b) => a - b);
|
||||
const DAY = 86400;
|
||||
|
||||
function parseJson(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
try { return JSON.parse(value || "[]"); } catch { return []; }
|
||||
}
|
||||
|
||||
function timestamp(value) {
|
||||
const parsed = Date.parse(String(value || "").replace(" ", "T").replace(/\+00$/, "Z"));
|
||||
return Number.isFinite(parsed) ? parsed / 1000 : null;
|
||||
}
|
||||
|
||||
function categoryOf(raw) {
|
||||
const tags = [...(raw.tags || []), ...(raw.events || []).flatMap((event) => event.tags || [])]
|
||||
.map((tag) => `${tag.slug || ""} ${tag.label || ""}`).join(" ");
|
||||
const text = `${raw.category || ""} ${raw.question || ""} ${raw.sportsMarketType || ""} ${tags}`.toLowerCase();
|
||||
if (/\b(election|president|politic|senate|congress|parliament|minister|governor|government|nominee|primary)\b/.test(text)) return "Politics";
|
||||
if (/\b(bitcoin|crypto|ethereum|btc|eth|solana|xrp|token|stablecoin)\b/.test(text)) return "Crypto";
|
||||
if (/\b(sports?|soccer|football|basketball|baseball|tennis|hockey|cricket|golf|boxing|ufc|nba|nfl|nhl|mlb|fifa|epl|match|game|tournament)\b/.test(text)) return "Sports";
|
||||
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings|ipo)\b/.test(text)) return "Economy";
|
||||
if (/\b(movie|music|album|box office|television|celebrity|award|gaming|youtube|stream)\b/.test(text)) return "Pop Culture";
|
||||
return "Other";
|
||||
}
|
||||
|
||||
async function fetchJson(url, attempts = 4) {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(30000), 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, 600 * (attempt + 1)));
|
||||
}
|
||||
throw lastError || new Error("request failed");
|
||||
}
|
||||
|
||||
async function mapLimit(items, limit, task) {
|
||||
const output = new Array(items.length);
|
||||
let cursor = 0;
|
||||
async function worker() {
|
||||
while (cursor < items.length) {
|
||||
const index = cursor++;
|
||||
try { output[index] = await task(items[index]); }
|
||||
catch (error) { output[index] = { error: error.message }; }
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
||||
return output;
|
||||
}
|
||||
|
||||
async function fetchResolvedMarkets(limit) {
|
||||
const markets = [], seen = new Set();
|
||||
let cursor = "";
|
||||
while (markets.length < limit) {
|
||||
const params = new URLSearchParams({ closed: "true", order: "closedTime", ascending: "false", limit: "100", include_tag: "true" });
|
||||
if (cursor) params.set("after_cursor", cursor);
|
||||
const payload = await fetchJson(`${GAMMA}/markets/keyset?${params}`), page = payload?.markets;
|
||||
if (!Array.isArray(page) || !page.length) break;
|
||||
for (const raw of page) {
|
||||
const id = String(raw.id || ""), labels = parseJson(raw.outcomes).map((value) => String(value).trim().toLowerCase());
|
||||
const outcomes = parseJson(raw.outcomePrices).map(Number), tokens = parseJson(raw.clobTokenIds).map(String);
|
||||
const finalYes = outcomes[0] >= 0.99 && outcomes[1] <= 0.01 ? 1 : outcomes[1] >= 0.99 && outcomes[0] <= 0.01 ? 0 : null;
|
||||
const closedAt = timestamp(raw.closedTime || raw.endDate), createdAt = timestamp(raw.createdAt);
|
||||
if (!id || seen.has(id) || labels[0] !== "yes" || labels[1] !== "no" || tokens.length !== 2 || finalYes == null || !closedAt || !createdAt) continue;
|
||||
seen.add(id);
|
||||
markets.push({ id, question: raw.question || "", eventKey: String(raw.events?.[0]?.id || id), tokenId: tokens[0],
|
||||
finalYes, closedAt, createdAt, category: categoryOf(raw) });
|
||||
if (markets.length >= limit) break;
|
||||
}
|
||||
if (page.length < 100 || !payload.next_cursor || payload.next_cursor === cursor) break;
|
||||
cursor = payload.next_cursor;
|
||||
}
|
||||
return markets;
|
||||
}
|
||||
|
||||
function atOrBefore(points, target) {
|
||||
let lo = 0, hi = points.length - 1, answer = null;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (points[mid].t <= target) { answer = points[mid]; lo = mid + 1; }
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
function observations(market, points) {
|
||||
return HORIZONS.flatMap((horizonDays) => {
|
||||
const decisionAt = market.closedAt - horizonDays * DAY, point = atOrBefore(points, decisionAt);
|
||||
const recent = points.filter((candidate) => candidate.t >= decisionAt - 7 * DAY && candidate.t <= decisionAt);
|
||||
const recentRange = recent.length ? Math.max(...recent.map((candidate) => candidate.p)) - Math.min(...recent.map((candidate) => candidate.p)) : 0;
|
||||
if (!point || decisionAt < market.createdAt + DAY || decisionAt - point.t > 36 * 3600
|
||||
|| recent.length < 2 || recentRange < 0.005 || point.p <= 0.03 || point.p >= 0.97) return [];
|
||||
return ["YES", "NO"].map((side) => {
|
||||
const entry = side === "YES" ? point.p : 1 - point.p;
|
||||
const won = side === (market.finalYes ? "YES" : "NO");
|
||||
return { marketId: market.id, eventKey: market.eventKey, question: market.question, category: market.category,
|
||||
closedAt: market.closedAt, decisionAt, horizonDays, side, entry, favorite: entry >= 0.5, won,
|
||||
netReturn: (won ? 1 : 0) / entry - 1 - COST / entry };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function tradesFor(rows, rule) {
|
||||
const grouped = new Map();
|
||||
rows.filter((row) => matches(row, rule)).forEach((row) => {
|
||||
const current = grouped.get(row.eventKey);
|
||||
if (!current || row.entry > current.entry || (row.entry === current.entry && row.marketId < current.marketId)) {
|
||||
grouped.set(row.eventKey, row);
|
||||
}
|
||||
});
|
||||
return [...grouped.values()];
|
||||
}
|
||||
|
||||
function summarize(rows, confidence = 1.96) {
|
||||
if (!rows.length) return { trades: 0, events: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, winRate: 0 };
|
||||
const buckets = new Map();
|
||||
rows.forEach((row) => { const values = buckets.get(row.eventKey) || []; values.push(row.netReturn); buckets.set(row.eventKey, values); });
|
||||
const eventReturns = [...buckets.values()].map((values) => values.reduce((sum, value) => sum + value, 0) / values.length);
|
||||
const eventMean = eventReturns.reduce((sum, value) => sum + value, 0) / eventReturns.length;
|
||||
const variance = eventReturns.length > 1
|
||||
? eventReturns.reduce((sum, value) => sum + (value - eventMean) ** 2, 0) / (eventReturns.length - 1) : 0;
|
||||
const margin = confidence * Math.sqrt(variance / Math.max(1, eventReturns.length));
|
||||
return { trades: rows.length, events: eventReturns.length,
|
||||
mean: rows.reduce((sum, row) => sum + row.netReturn, 0) / rows.length,
|
||||
eventMean, lower: eventMean - margin, upper: eventMean + margin,
|
||||
winRate: rows.filter((row) => row.won).length / rows.length };
|
||||
}
|
||||
|
||||
const PRICE_RANGES = [
|
||||
[0.03, 0.97], [0.05, 0.25], [0.10, 0.30], [0.20, 0.40], [0.30, 0.50],
|
||||
[0.40, 0.60], [0.50, 0.70], [0.60, 0.80], [0.70, 0.90], [0.75, 0.95],
|
||||
];
|
||||
const CATEGORIES = ["All", "Politics", "Sports", "Crypto", "Economy", "Pop Culture", "Other"];
|
||||
const rules = [];
|
||||
for (const horizonDays of HORIZONS) {
|
||||
for (const side of ["YES", "NO", "FAVORITE", "UNDERDOG"]) {
|
||||
for (const [minEntry, maxEntry] of PRICE_RANGES) {
|
||||
for (const category of CATEGORIES) {
|
||||
rules.push({ id: `${horizonDays}d_${side.toLowerCase()}_${minEntry}-${maxEntry}_${category.toLowerCase().replace(/\s+/g, "-")}`,
|
||||
horizonDays, side, minEntry, maxEntry, category });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function matches(row, rule) {
|
||||
return row.horizonDays === rule.horizonDays && row.entry >= rule.minEntry && row.entry < rule.maxEntry
|
||||
&& (rule.category === "All" || row.category === rule.category)
|
||||
&& (rule.side === row.side || (rule.side === "FAVORITE" && row.favorite) || (rule.side === "UNDERDOG" && !row.favorite));
|
||||
}
|
||||
|
||||
function splitByTime(rows) {
|
||||
const times = [...new Set(rows.map((row) => row.closedAt))].sort((a, b) => a - b);
|
||||
const trainCut = times[Math.floor(times.length * 0.60)] || 0, validationCut = times[Math.floor(times.length * 0.80)] || 0;
|
||||
return {
|
||||
train: rows.filter((row) => row.closedAt < trainCut),
|
||||
validation: rows.filter((row) => row.closedAt >= trainCut && row.closedAt < validationCut),
|
||||
holdout: rows.filter((row) => row.closedAt >= validationCut),
|
||||
trainCut, validationCut,
|
||||
};
|
||||
}
|
||||
|
||||
function stabilityWindows(rows) {
|
||||
const times = [...new Set(rows.map((row) => row.closedAt))].sort((a, b) => a - b);
|
||||
const cuts = [0, 0.25, 0.5, 0.75, 1].map((fraction) => times[Math.min(times.length - 1, Math.floor(times.length * fraction))] || 0);
|
||||
return Array.from({ length: 4 }, (_, index) => rows.filter((row) => row.closedAt >= cuts[index]
|
||||
&& (index === 3 || row.closedAt < cuts[index + 1])));
|
||||
}
|
||||
|
||||
const markets = await fetchResolvedMarkets(MARKET_LIMIT);
|
||||
const histories = await mapLimit(markets, CONCURRENCY, async (market) => {
|
||||
const data = await fetchJson(`${CLOB}/prices-history?market=${encodeURIComponent(market.tokenId)}&interval=max&fidelity=1440`);
|
||||
const points = (data.history || []).map((point) => ({ t: Number(point.t), p: Number(point.p) }))
|
||||
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
|
||||
return { market, points, observations: observations(market, points) };
|
||||
});
|
||||
const usable = histories.filter((row) => row && !row.error && row.points.length), rows = usable.flatMap((row) => row.observations);
|
||||
const partitions = splitByTime(rows), windows = stabilityWindows(rows);
|
||||
const evaluated = rules.map((rule) => {
|
||||
const train = summarize(tradesFor(partitions.train, rule));
|
||||
const validation = summarize(tradesFor(partitions.validation, rule));
|
||||
const holdoutRows = tradesFor(partitions.holdout, rule), holdout = summarize(holdoutRows);
|
||||
const stable = windows.map((window) => summarize(tradesFor(window, rule)));
|
||||
const trainPassed = train.trades >= 100 && train.events >= 50 && train.lower > 0;
|
||||
const validationPassed = trainPassed && validation.trades >= 40 && validation.events >= 20 && validation.lower > 0
|
||||
&& stable.every((summary) => summary.trades >= 20 && summary.events >= 10 && summary.eventMean > 0);
|
||||
const passesHoldout = validationPassed && holdout.trades >= 40 && holdout.events >= 20 && holdout.lower > 0;
|
||||
return { rule, train, validation, holdout, stable, trainPassed, validationPassed, passesHoldout, holdoutRows };
|
||||
});
|
||||
const selected = evaluated.filter((row) => row.validationPassed)
|
||||
.sort((a, b) => Number(b.passesHoldout) - Number(a.passesHoldout) || b.holdout.lower - a.holdout.lower);
|
||||
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
|
||||
const candidate = (row) => ({ rule: row.rule, train: compact(row.train), validation: compact(row.validation),
|
||||
holdout: compact(row.holdout), stabilityMeans: row.stable.map((summary) => +summary.eventMean.toFixed(5)), passesHoldout: row.passesHoldout });
|
||||
|
||||
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, resolvedMarkets: markets.length,
|
||||
historiesWithData: usable.length, failures: histories.filter((row) => row?.error).length, observations: rows.length / 2,
|
||||
methodology: { selection: "most recently closed eligible Yes/No markets", horizonsDays: HORIZONS,
|
||||
historyFidelityMinutes: 1440, maximumPriceStalenessHours: 36, modeledCostCents: COST * 100,
|
||||
split: "60% train / 20% validation / 20% untouched holdout", confidence: "event-clustered 95% lower bound",
|
||||
stability: "positive event mean in each of four chronological windows; at most one highest-entry market per event",
|
||||
activityGate: "market open for at least 24h with at least two recent observations and a 0.5-cent seven-day price range", testedRules: rules.length,
|
||||
note: "No final volume or settlement outcome enters rule features. Market selection remains a recent-closure cohort, and midpoint-plus-cost is still an execution approximation." },
|
||||
partitionRows: { train: partitions.train.length / 2, validation: partitions.validation.length / 2, holdout: partitions.holdout.length / 2 },
|
||||
trainPassed: evaluated.filter((row) => row.trainPassed).length, validationSelected: selected.length,
|
||||
holdoutPassed: selected.filter((row) => row.passesHoldout).length, candidates: selected.slice(0, 25).map(candidate),
|
||||
holdoutExamples: (selected.find((row) => row.passesHoldout)?.holdoutRows || []).slice(0, 15)
|
||||
.map((row) => ({ marketId: row.marketId, question: row.question, category: row.category, side: row.side,
|
||||
entry: +row.entry.toFixed(4), won: row.won, netReturn: +row.netReturn.toFixed(4),
|
||||
decisionAt: new Date(row.decisionAt * 1000).toISOString(), closedAt: new Date(row.closedAt * 1000).toISOString() }))
|
||||
}, null, 2));
|
||||
Reference in New Issue
Block a user