mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-24 04:58:08 +00:00
Add evidence-gated paired maker strategy
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 62 also installs an offline app shell and caches timestamped
|
||||
Vercel Blob. Build 63 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 62 ranks the competition by each agent's return since Strategy 51 began.
|
||||
Build 63 ranks the competition by each agent's return since Strategy 52 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 51 treats each binary stake as capable of falling to zero even when the
|
||||
Strategy 52 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
|
||||
@@ -70,13 +70,33 @@ masquerade as broad evidence. Set `EVAL_MARKETS`, `EVAL_CONCURRENCY`, `EVAL_HORI
|
||||
`EVAL_COST_CENTS` to change the audit. Set `EVAL_SUMMARY=1` for the compact,
|
||||
decision-focused report.
|
||||
|
||||
Run `npm run evaluate:adaptive` for a stricter chronological search across 1,920
|
||||
predefined price-action rules. It uses a 60/20/20 train, validation, and holdout
|
||||
split and never selects a rule from the holdout period. The August 19 run loaded
|
||||
all 300 requested histories and found no directional rule that passed both train
|
||||
and validation at either 24 or 72 hours. Strategy 52 therefore keeps directional
|
||||
signals in the walk-forward observation ledger until current, independent-event
|
||||
evidence proves an edge.
|
||||
|
||||
Run `npm run evaluate:liquidity` to inspect live reward-scoring markets for
|
||||
paired resting YES and NO bids whose combined cost is below the $1 settlement
|
||||
payout. Strategy 52 lets Value Hunter stage a maximum of six paper quote pairs,
|
||||
reserving at most 10% of equity and 2% per quote. A later live cycle must cross
|
||||
each bid before a fill is recorded. The first unmatched leg is unwound after 24
|
||||
hours; once both legs fill, the complete hedge is held intact through settlement.
|
||||
The engine does not credit hypothetical fills or unverified liquidity rewards.
|
||||
See Polymarket's official [fees](https://docs.polymarket.com/trading/fees),
|
||||
[maker rebates](https://docs.polymarket.com/market-makers/maker-rebates), and
|
||||
[liquidity rewards](https://docs.polymarket.com/market-makers/liquidity-rewards)
|
||||
documentation for the live venue rules this paper simulation approximates.
|
||||
|
||||
The latest 120-active-market audit produced 1,241 twelve-hour observations from
|
||||
41 markets with no fetch failures. The broad rule averaged -1.35% net and was
|
||||
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 51 therefore keeps reversal entries observation-only until
|
||||
segments. Strategy 52 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 and 72 hours.
|
||||
|
||||
@@ -85,7 +105,7 @@ 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 51 therefore
|
||||
market-clustered 90% intervals were entirely below zero. Strategy 52 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
|
||||
@@ -94,7 +114,7 @@ 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 51 therefore makes every directional trend or
|
||||
robustly negative. Strategy 52 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.
|
||||
@@ -103,7 +123,7 @@ 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
|
||||
zero, and no tested directional rule was robustly positive. Strategy 52
|
||||
therefore requires positive evidence at both 24 and 72 hours rather than
|
||||
allowing one favorable short-horizon mark to authorize cash exposure.
|
||||
|
||||
@@ -113,24 +133,24 @@ 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 51 gives previously opened Politics trend positions that 72-hour observation window before
|
||||
Strategy 52 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 51 also subtracts a half-cent round-trip cost when grading each live
|
||||
Strategy 52 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 51 adds uncertainty-aware, multi-horizon promotion and demotion. A
|
||||
Strategy 52 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 62 enforces the documented offline boundary end to end. Cached snapshots
|
||||
Build 63 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
|
||||
@@ -143,26 +163,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 62 independently refreshes markets for due pending signals that have
|
||||
Build 63 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 62 also allocates the 300 pending observation slots by evidence coverage.
|
||||
Build 63 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 62 retains safe shared-state provider diagnostics from both reads and
|
||||
Build 63 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 51 coordinates high-risk exploration globally. Near-term, extreme-price,
|
||||
Strategy 52 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.
|
||||
@@ -188,7 +208,7 @@ 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 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
|
||||
0.25% negative after modeled costs. Strategy 52 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
|
||||
@@ -211,19 +231,19 @@ 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 51 therefore treats the result as a provisional prior instead
|
||||
segment. Strategy 52 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 51 also excludes path-dependent barriers such as "reach $66,000," "hit
|
||||
Strategy 52 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 51 clusters live walk-forward observations by Polymarket event and checkpoint before
|
||||
Strategy 52 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.
|
||||
|
||||
+189
-15
@@ -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 51 · build 62</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 52 · build 63</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 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>
|
||||
<div class="live-build-banner"><b>Build 63 active:</b> Value Hunter can stage paired resting bids in low-volatility, reward-scoring markets when the two quotes cost less than their $1 combined payout. Paper profit is never credited from a hypothetical reward or unfilled order: each leg must cross on a later live cycle, and unmatched inventory is closed after 24 hours. Directional promotion still requires separate positive 24-hour and 72-hour evidence from independent current-strategy events. 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 62 · Adaptive strategy 51 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
|
||||
Build 63 · Adaptive strategy 52 · 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 = 62;
|
||||
const SUGGESTION_ENGINE_VERSION = 51;
|
||||
const PREVIOUS_STRATEGY_VERSION = 50;
|
||||
const BUILD_VERSION = 63;
|
||||
const SUGGESTION_ENGINE_VERSION = 52;
|
||||
const PREVIOUS_STRATEGY_VERSION = 51;
|
||||
const LEGACY_BUILD_STRATEGY_LINEAGE = Object.freeze({40:40,41:40});
|
||||
function normalizedStrategyVersion(value){
|
||||
const version=Number(value||0);
|
||||
@@ -861,7 +861,7 @@ async function refreshTradeEmailStatus(){
|
||||
Five core and five aggressive strategies share the same live suggestion pool. */
|
||||
const AGENTS = [
|
||||
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
|
||||
blurb:"Scans complete negative-risk events for executable multi-leg pricing gaps whose worst-case payout remains positive after estimated costs. Directional ideas stay observation-only until their own category, side, and signal cohorts earn adaptive promotion.",
|
||||
blurb:"Runs the arena's non-directional book: complete negative-risk bundles plus paired resting YES/NO bids in calm, reward-scoring markets. A paired quote earns nothing until live prices cross each leg; two fills below $1 lock settlement value, while unmatched inventory is unwound after 24 hours.",
|
||||
rank:(s)=>[...s].sort((a,b)=>(Math.abs(b.net_edge||b.edge||0)*120+b.conviction+Number(b.evidence_score||0)*18)-(Math.abs(a.net_edge||a.edge||0)*120+a.conviction+Number(a.evidence_score||0)*18)),
|
||||
maxNew:4, maxFrac:0.055, minConv:58, kelly:0.24, targetExposure:0.62, drawdownExposure:0.42},
|
||||
{id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy",
|
||||
@@ -1011,7 +1011,10 @@ function normalizeMarket(raw,{allowClosed=false}={}){
|
||||
spread:toNum(raw.spread),best_bid:toNum(raw.bestBid),best_ask:toNum(raw.bestAsk),
|
||||
price_change_1h:toNum(raw.oneHourPriceChange),price_change_1d:toNum(raw.oneDayPriceChange),
|
||||
price_change_1w:toNum(raw.oneWeekPriceChange),price_change_1m:toNum(raw.oneMonthPriceChange),
|
||||
days_to_resolution:daysUntil(raw.endDate),image:raw.image||"",
|
||||
days_to_resolution:daysUntil(raw.endDate),image:raw.image||"",condition_id:String(raw.conditionId||""),
|
||||
tick_size:toNum(raw.orderPriceMinTickSize,0.01),order_min_size:toNum(raw.orderMinSize,5),
|
||||
competitive:toNum(raw.competitive||(ev&&ev.competitive)),rewards_daily_rate:(raw.clobRewards||[]).reduce((sum,reward)=>sum+toNum(reward.rewardsDailyRate),0),
|
||||
rewards_min_size:toNum(raw.rewardsMinSize),rewards_max_spread:toNum(raw.rewardsMaxSpread),fees_enabled:Boolean(raw.feesEnabled),
|
||||
closed:Boolean(raw.closed),accepting_orders:raw.acceptingOrders!==false,
|
||||
url:ev.slug?`https://polymarket.com/event/${ev.slug}`:""};
|
||||
}
|
||||
@@ -1151,6 +1154,11 @@ const NEG_RISK_EVENT_SCAN_LIMIT=500;
|
||||
const NEG_RISK_MIN_LIQUIDITY=1000;
|
||||
const NEG_RISK_MIN_NET_PROFIT=0.003;
|
||||
const NEG_RISK_MIN_NET_RETURN=0.0015;
|
||||
const MAKER_MAX_QUOTES=6;
|
||||
const MAKER_MAX_QUOTE_CAPITAL_PCT=0.02;
|
||||
const MAKER_TOTAL_CAPITAL_PCT=0.10;
|
||||
const MAKER_QUOTE_EXPIRY_HOURS=24;
|
||||
const MAKER_MIN_LOCK_MARGIN=0.005;
|
||||
const clamp=(x,a,b)=>Math.max(a,Math.min(b,x));
|
||||
function liquiditySignal(m){const vol=Math.min(1,Math.log10(m.volume+1)/6.0);const liq=Math.min(1,Math.log10(m.liquidity+1)/5.3);return 0.6*vol+0.4*liq;}
|
||||
function momentumSignal(m){const da=m.volume_1wk?m.volume_1wk/7:0;if(da<=0)return m.volume_24hr>0?0.3:0;return clamp((m.volume_24hr/da-0.5)/2.0,0,1);}
|
||||
@@ -1398,7 +1406,7 @@ function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTI
|
||||
}
|
||||
|
||||
/* ---------- Multi-agent store ---------- */
|
||||
function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],closed:[],history:[],snapshots:[],stopped:{},lastDecision:null};}
|
||||
function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],maker_quotes:[],closed:[],history:[],snapshots:[],stopped:{},lastDecision:null};}
|
||||
function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,last_cycle_hour:null,
|
||||
engine_version:BUILD_VERSION,strategy_version:SUGGESTION_ENGINE_VERSION,engine_started_at:nowIso(),build_started_at:nowIso(),agents,signal_ledger:defaultSignalLedger(),seeded:false};}
|
||||
function reconcileStateVersions(st){
|
||||
@@ -1438,6 +1446,7 @@ function loadState(){
|
||||
AGENTS.forEach(a=>{
|
||||
if(!st.agents[a.id])st.agents[a.id]=defaultPortfolio();
|
||||
if(!st.agents[a.id].stopped)st.agents[a.id].stopped={};
|
||||
if(!Array.isArray(st.agents[a.id].maker_quotes))st.agents[a.id].maker_quotes=[];
|
||||
if(!("lastDecision" in st.agents[a.id]))st.agents[a.id].lastDecision=null;
|
||||
});
|
||||
migrated=reconcileStateVersions(st)||migrated;
|
||||
@@ -1449,6 +1458,7 @@ function compactPortfolioForSync(p,limits=SYNC_LIMITS){
|
||||
if(!p||typeof p!=="object")return p;
|
||||
const out=Object.assign({},p);
|
||||
out.positions=Array.isArray(p.positions)?p.positions:[];
|
||||
out.maker_quotes=Array.isArray(p.maker_quotes)?p.maker_quotes.slice(-MAKER_MAX_QUOTES):[];
|
||||
out.closed=Array.isArray(p.closed)?p.closed.slice(-limits.closed):[];
|
||||
out.history=Array.isArray(p.history)?p.history.slice(-limits.history):[];
|
||||
out.snapshots=Array.isArray(p.snapshots)?p.snapshots.slice(-limits.snapshots):[];
|
||||
@@ -1519,7 +1529,9 @@ function compactCachedMarket(m){
|
||||
volume_24hr:m.volume_24hr,volume_1wk:m.volume_1wk,liquidity:m.liquidity,spread:m.spread,
|
||||
best_bid:m.best_bid,best_ask:m.best_ask,price_change_1h:m.price_change_1h,
|
||||
price_change_1d:m.price_change_1d,price_change_1w:m.price_change_1w,price_change_1m:m.price_change_1m,
|
||||
days_to_resolution:m.days_to_resolution,end_date:m.end_date,closed:m.closed,accepting_orders:m.accepting_orders};
|
||||
days_to_resolution:m.days_to_resolution,end_date:m.end_date,closed:m.closed,accepting_orders:m.accepting_orders,
|
||||
condition_id:m.condition_id,tick_size:m.tick_size,order_min_size:m.order_min_size,competitive:m.competitive,
|
||||
rewards_daily_rate:m.rewards_daily_rate,rewards_min_size:m.rewards_min_size,rewards_max_spread:m.rewards_max_spread,fees_enabled:m.fees_enabled};
|
||||
}
|
||||
function saveMarketCache(markets,suggestions,priceMap={}){
|
||||
const payload={version:BUILD_VERSION,strategy_version:SUGGESTION_ENGINE_VERSION,captured_at:nowIso(),markets:(markets||[]).slice(0,MARKET_ANALYSIS_LIMIT).map(compactCachedMarket),
|
||||
@@ -2298,6 +2310,10 @@ function markToMarket(p,priceMap,cfg=null,{policyExits=false,executeTrades=true}
|
||||
pos.peak_price=+Math.max(Number(pos.peak_price||pos.entry_price||price),price).toFixed(4);
|
||||
if(executeTrades)delete pos.price_status;else pos.price_status="cached-mark-only";
|
||||
if(!executeTrades){stillOpen.push(pos);continue;}
|
||||
if(pos.signal_type==="maker-pair"&&pos.maker_pair_pending){
|
||||
if(fresh.closed||fresh.accepting_orders===false){closePosition(p,pos,"Paired maker inventory settled before the second fill","CLOSE");continue;}
|
||||
stillOpen.push(pos);continue;
|
||||
}
|
||||
if(pos.requires_complete_bundle){
|
||||
if(fresh.closed||fresh.accepting_orders===false){closePosition(p,pos,"Complete bundle leg settled","CLOSE");continue;}
|
||||
stillOpen.push(pos);continue;
|
||||
@@ -2321,6 +2337,108 @@ function markToMarket(p,priceMap,cfg=null,{policyExits=false,executeTrades=true}
|
||||
}
|
||||
p.positions=stillOpen;
|
||||
}
|
||||
function makerPairCandidate(m){
|
||||
if(!m||m.closed||m.accepting_orders===false)return null;
|
||||
const bid=Number(m.best_bid),ask=Number(m.best_ask),spread=ask-bid,tick=Math.max(0.001,Number(m.tick_size||0.01));
|
||||
const reward=Number(m.rewards_daily_rate||0),minSize=Math.max(5,Number(m.rewards_min_size||0),Number(m.order_min_size||0));
|
||||
const maxSpread=Number(m.rewards_max_spread||0)/100,dayMove=Math.abs(Number(m.price_change_1d||0));
|
||||
const yesQuote=bid,noQuote=1-ask,pairedCost=yesQuote+noQuote,requiredCapital=minSize*pairedCost;
|
||||
if(!(reward>=1&&spread>=0.02&&spread<=0.08&&maxSpread>0&&spread/2<=maxSpread&&yesQuote>=tick&&noQuote>=tick
|
||||
&&pairedCost<=1-MAKER_MIN_LOCK_MARGIN&&dayMove<=Math.max(0.02,spread)&&Number(m.liquidity||0)>=5000
|
||||
&&Number(m.volume_24hr||0)>=2000&&requiredCapital>0))return null;
|
||||
const rewardYield=reward/requiredCapital,competition=clamp(Number(m.competitive||0),0,1);
|
||||
return {market_id:String(m.id),condition_id:m.condition_id||"",question:m.question,event:m.event,url:m.url||"",category:m.category||"Other",
|
||||
yes_quote:+yesQuote.toFixed(4),no_quote:+noQuote.toFixed(4),paired_cost:+pairedCost.toFixed(4),units:+minSize.toFixed(2),
|
||||
required_capital:+requiredCapital.toFixed(2),locked_profit:+(minSize*(1-pairedCost)).toFixed(2),reward_daily_rate:+reward.toFixed(3),
|
||||
competitive:+competition.toFixed(4),spread:+spread.toFixed(4),tick_size:tick,day_move:+dayMove.toFixed(4),
|
||||
score:+(rewardYield*(1-competition*0.5)*100+spread*25).toFixed(4)};
|
||||
}
|
||||
function stageMakerQuotes(p,markets){
|
||||
p.maker_quotes=Array.isArray(p.maker_quotes)?p.maker_quotes:[];
|
||||
const eq=equity(p),existing=new Set(p.maker_quotes.map(quote=>String(quote.market_id))),held=new Set((p.positions||[]).map(pos=>String(pos.market_id)));
|
||||
let reserved=p.maker_quotes.reduce((sum,quote)=>sum+Number(quote.required_capital||0),0),staged=0;
|
||||
const candidates=(markets||[]).map(makerPairCandidate).filter(Boolean).sort((a,b)=>b.score-a.score);
|
||||
for(const candidate of candidates){
|
||||
if(p.maker_quotes.length>=MAKER_MAX_QUOTES||reserved>=eq*MAKER_TOTAL_CAPITAL_PCT)break;
|
||||
if(existing.has(candidate.market_id)||held.has(candidate.market_id)||candidate.required_capital>eq*MAKER_MAX_QUOTE_CAPITAL_PCT
|
||||
||reserved+candidate.required_capital>Math.min(p.cash,eq*MAKER_TOTAL_CAPITAL_PCT))continue;
|
||||
const quote=Object.assign({},candidate,{id:`maker:${candidate.market_id}:${Date.now()}:${p.maker_quotes.length}`,
|
||||
created_at:cycleIso(),strategy_version:SUGGESTION_ENGINE_VERSION,build_version:BUILD_VERSION,
|
||||
yes_filled_at:null,no_filled_at:null,yes_fill_price:null,no_fill_price:null});
|
||||
p.maker_quotes.push(quote);existing.add(candidate.market_id);reserved+=candidate.required_capital;staged++;
|
||||
p.history.push({date:logDay(),action:"QUOTE",question:candidate.question,side:"PAIR",
|
||||
detail:`Staged ${candidate.units} resting YES @ ${pct(candidate.yes_quote)} and NO @ ${pct(candidate.no_quote)} · paired cost ${pct(candidate.paired_cost)} · ${fmtUSD(candidate.locked_profit)} locked settlement margin only if both legs fill · no hypothetical reward credited`});
|
||||
}
|
||||
return {staged,active:p.maker_quotes.length,candidates:candidates.length,reserved:+reserved.toFixed(2)};
|
||||
}
|
||||
function makerQuotePosition(p,quote,side){return (p.positions||[]).find(pos=>pos.maker_quote_id===quote.id&&pos.side===side);}
|
||||
function fillMakerLeg(p,quote,side,fresh){
|
||||
const price=Number(side==="YES"?quote.yes_quote:quote.no_quote),cost=+(Number(quote.units)*price).toFixed(2);
|
||||
if(!(cost>0)||cost>p.cash)return false;
|
||||
const current=side==="YES"?Number(fresh.yes_price):Number(fresh.no_price),tokenIndex=side==="YES"?0:1;
|
||||
p.cash=+(p.cash-cost).toFixed(2);
|
||||
p.positions.push({market_id:quote.market_id,question:quote.question,side,shares:quote.units,token_id:fresh.clob_token_ids&&fresh.clob_token_ids[tokenIndex]||null,
|
||||
entry_price:+price.toFixed(4),current_price:+current.toFixed(4),cost,value:+(quote.units*current).toFixed(2),original_shares:quote.units,original_cost:cost,
|
||||
unrealized_pnl:+(quote.units*current-cost).toFixed(2),conviction:80,peer_conviction:80,category:quote.category,opened_at:cycleIso(),url:quote.url||"",
|
||||
peer_note:"paired resting quote",entry_reason:"Resting maker bid crossed on a later live cycle; no reward assumed",net_edge:1-quote.paired_cost,
|
||||
evidence_score:1,evidence_source_count:0,friction:0,chase_penalty:0,quality:"maker-pair",strategy_version:SUGGESTION_ENGINE_VERSION,
|
||||
build_version:BUILD_VERSION,momentum_strength:0,signal_strength:1,signal_confidence:1,signal_type:"maker-pair",price_change_1d:quote.day_move,
|
||||
price_change_1w:0,days_to_resolution:null,jump_risk:false,maker_quote_id:quote.id,maker_pair_pending:true,requires_complete_bundle:false,
|
||||
learning_score:0,learning_confidence:0,market_learning_score:0,market_learning_confidence:0,learning_state:"maker-fill",market_learning_state:"maker-fill",
|
||||
historical_prior_score:0,historical_prior_confidence:0,historical_prior_features:[],historical_requires_promotion:false,learning_multiplier:1,
|
||||
learning_exploration:false,risk_budget_pct:2,peak_price:+current.toFixed(4),gain_stops:{},stop_losses:{}});
|
||||
quote[`${side.toLowerCase()}_filled_at`]=cycleIso();quote[`${side.toLowerCase()}_fill_price`]=+price.toFixed(4);
|
||||
p.history.push({date:logDay(),action:"MAKER_FILL",question:quote.question,side,
|
||||
detail:`Resting ${side} bid filled at ${pct(price)} for ${fmtUSD(cost)}; the complementary quote remains live and this inventory is not counted as a locked pair yet`});
|
||||
return true;
|
||||
}
|
||||
function completeMakerPair(p,quote){
|
||||
const positions=[makerQuotePosition(p,quote,"YES"),makerQuotePosition(p,quote,"NO")];
|
||||
if(positions.some(pos=>!pos))return false;
|
||||
const margin=1-Number(quote.yes_fill_price)-Number(quote.no_fill_price);
|
||||
if(margin<MAKER_MIN_LOCK_MARGIN-0.0001)return false;
|
||||
positions.forEach(pos=>{pos.maker_pair_pending=false;pos.requires_complete_bundle=true;pos.bundle_id=quote.id;pos.bundle_name=quote.question;
|
||||
pos.bundle_leg_count=2;pos.bundle_payout_per_unit=1;pos.bundle_net_profit_per_unit=+margin.toFixed(4);});
|
||||
p.history.push({date:logDay(),action:"MAKER_LOCK",question:quote.question,side:"PAIR",
|
||||
detail:`Both resting legs filled for ${pct(1-margin)} combined · locked settlement value ${fmtUSD(Number(quote.units)*margin)} before any separately verified reward`});
|
||||
return true;
|
||||
}
|
||||
function unwindMakerInventory(p,quote,fresh){
|
||||
const side=quote.yes_filled_at&&!quote.no_filled_at?"YES":quote.no_filled_at&&!quote.yes_filled_at?"NO":null;
|
||||
if(!side)return true;
|
||||
const pos=makerQuotePosition(p,quote,side);if(!pos)return true;
|
||||
const executable=side==="YES"?Number(fresh.best_bid):1-Number(fresh.best_ask);
|
||||
if(!(executable>0&&executable<1))return false;
|
||||
pos.current_price=+executable.toFixed(4);pos.value=+(pos.shares*pos.current_price).toFixed(2);pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
|
||||
closePosition(p,pos,`Paired maker quote expired after ${MAKER_QUOTE_EXPIRY_HOURS}h without the complementary fill`,"MAKER_EXIT");
|
||||
p.positions=(p.positions||[]).filter(item=>item!==pos);
|
||||
return true;
|
||||
}
|
||||
function manageMakerQuotes(p,marketMap,{executeTrades=false}={}){
|
||||
p.maker_quotes=Array.isArray(p.maker_quotes)?p.maker_quotes:[];
|
||||
if(!executeTrades)return {active:p.maker_quotes.length,fills:0,locked:0,expired:0};
|
||||
const keep=[];let fills=0,locked=0,expired=0;
|
||||
for(const quote of p.maker_quotes){
|
||||
const fresh=marketMap[String(quote.market_id)];
|
||||
if(!fresh){keep.push(quote);continue;}
|
||||
const age=hoursSince(quote.created_at),oldEnough=age>=RUN_INTERVAL_MS/3600000*0.8;
|
||||
if(oldEnough&&!quote.yes_filled_at&&Number(fresh.best_ask)>0&&Number(fresh.best_ask)<=Number(quote.yes_quote)+0.00001){if(fillMakerLeg(p,quote,"YES",fresh))fills++;}
|
||||
const noAsk=1-Number(fresh.best_bid);
|
||||
if(oldEnough&&!quote.no_filled_at&&Number(fresh.best_bid)>0&&noAsk<=Number(quote.no_quote)+0.00001){if(fillMakerLeg(p,quote,"NO",fresh))fills++;}
|
||||
if(quote.yes_filled_at&"e.no_filled_at){if(completeMakerPair(p,quote)){locked++;continue;}}
|
||||
if(age>=MAKER_QUOTE_EXPIRY_HOURS){if(unwindMakerInventory(p,quote,fresh)){expired++;continue;}}
|
||||
if(quote.yes_filled_at&&!quote.no_filled_at){
|
||||
const cap=1-Number(quote.yes_fill_price)-MAKER_MIN_LOCK_MARGIN,currentNoBid=1-Number(fresh.best_ask);
|
||||
quote.no_quote=+Math.min(cap,Math.max(Number(quote.no_quote),currentNoBid)).toFixed(4);
|
||||
}else if(quote.no_filled_at&&!quote.yes_filled_at){
|
||||
const cap=1-Number(quote.no_fill_price)-MAKER_MIN_LOCK_MARGIN,currentYesBid=Number(fresh.best_bid);
|
||||
quote.yes_quote=+Math.min(cap,Math.max(Number(quote.yes_quote),currentYesBid)).toFixed(4);
|
||||
}
|
||||
keep.push(quote);
|
||||
}
|
||||
p.maker_quotes=keep;
|
||||
return {active:keep.length,fills,locked,expired};
|
||||
}
|
||||
function peerMarketStats(st,selfId){
|
||||
const stats={};
|
||||
AGENTS.forEach(a=>{
|
||||
@@ -2328,6 +2446,7 @@ function peerMarketStats(st,selfId){
|
||||
const p=st.agents&&st.agents[a.id]; if(!p)return;
|
||||
const ret=(equity(p)/Math.max(p.starting_balance||STARTING_BALANCE,1)-1)*100;
|
||||
(p.positions||[]).forEach(pos=>{
|
||||
if(pos.requires_complete_bundle)return;
|
||||
if(!isMaterialPosition(p,pos))return;
|
||||
const id=pos.market_id||pos.asset; if(!id)return;
|
||||
const key=`${id}:${pos.side}`;
|
||||
@@ -2388,6 +2507,7 @@ function reduceStrategyOverlap(st){
|
||||
AGENTS.filter(a=>a.kind==="strategy").forEach(cfg=>{
|
||||
const p=st.agents&&st.agents[cfg.id]; if(!p)return;
|
||||
(p.positions||[]).forEach(pos=>{
|
||||
if(pos.requires_complete_bundle)return;
|
||||
if(!isMaterialPosition(p,pos))return;
|
||||
const id=pos.market_id||pos.asset; if(!id)return;
|
||||
if(!groups[id])groups[id]=[];
|
||||
@@ -2726,6 +2846,7 @@ async function runDailyCycle(){
|
||||
const positionIds=new Set();
|
||||
AGENTS.forEach(a=>
|
||||
(st.agents[a.id].positions||[]).forEach(pos=>{if(pos.market_id)positionIds.add(String(pos.market_id));}));
|
||||
AGENTS.forEach(a=>(st.agents[a.id].maker_quotes||[]).forEach(quote=>{if(quote.market_id)positionIds.add(String(quote.market_id));}));
|
||||
setStatus("marking positions…",true);
|
||||
const priceMap={};
|
||||
const cachedById={};
|
||||
@@ -2758,6 +2879,9 @@ async function runDailyCycle(){
|
||||
const leaderEq=preBoard[0]?preBoard[0].eq:STARTING_BALANCE;
|
||||
const cachePolicy=offlineCachePolicy(cacheAgeMs);
|
||||
const executeTrades=runMode==="live"||cachePolicy.exitsAllowed;
|
||||
const quoteMarketMap=Object.assign({},cachedById,freshById);
|
||||
const valuePortfolio=st.agents.value;
|
||||
const makerActivity=manageMakerQuotes(valuePortfolio,quoteMarketMap,{executeTrades:runMode==="live"&&executeTrades});
|
||||
for(const cfg of AGENTS){
|
||||
const p=st.agents[cfg.id];
|
||||
markToMarket(p,priceMap,cfg,{policyExits:executeTrades,executeTrades});
|
||||
@@ -2773,6 +2897,14 @@ async function runDailyCycle(){
|
||||
const occupied=occupiedStrategyMarkets(st,cfg.id);
|
||||
claimedMarkets.forEach(id=>occupied.add(id));
|
||||
openPositions(p,cfg,cfg.rank(cycleSuggestions),focus,decision,occupied,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
|
||||
if(cfg.id==="value"&&runMode==="live"&&entriesAllowed){
|
||||
const staged=stageMakerQuotes(p,analysisMarkets);
|
||||
p.lastDecision=Object.assign({},p.lastDecision,{makerQuotes:p.maker_quotes.length,makerCandidates:staged.candidates,makerStaged:staged.staged,
|
||||
makerFills:makerActivity.fills,makerLocked:makerActivity.locked,makerExpired:makerActivity.expired,makerReserved:staged.reserved});
|
||||
if(staged.staged||makerActivity.fills||makerActivity.locked||makerActivity.expired){
|
||||
p.lastDecision.allocationStatus=`${p.lastDecision.allocationStatus||""} Paired maker book: ${p.maker_quotes.length} active quote${p.maker_quotes.length===1?"":"s"}, ${makerActivity.fills} fill${makerActivity.fills===1?"":"s"}, ${makerActivity.locked} newly locked pair${makerActivity.locked===1?"":"s"}; no hypothetical rewards credited.`.trim();
|
||||
}
|
||||
}
|
||||
recordSnapshot(p);
|
||||
}
|
||||
st.date=todayStr();st.last_run=SNAP_TS;st.last_cycle_hour=hour;st.run_mode=runMode;
|
||||
@@ -2961,7 +3093,8 @@ function decisionSummary(p){
|
||||
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 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}`;
|
||||
const maker=d.makerQuotes!=null?` Paired maker book: ${d.makerQuotes} active, ${d.makerFills||0} fills and ${d.makerLocked||0} completed locks this cycle, ${fmtUSD(d.makerReserved||0)} quote capital reserved. Rewards remain excluded until externally verified.`:"";
|
||||
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}${maker}${exposure}${allocation}${candidates}${blockers}`;
|
||||
}
|
||||
function renderAgentBrief(cfg,p,st){
|
||||
const root=$("agentBrief"); if(!root)return;
|
||||
@@ -2970,11 +3103,13 @@ function renderAgentBrief(cfg,p,st){
|
||||
const cadence=`${cfg.maxNew||0} new trades max per cycle`;
|
||||
const exitRule=`Exit after a confirmed reversal, a 12h unconfirmed fade after 48h, a trailing-profit retrace, or ${EXIT_STALE_DAYS}d stale while losing; Politics trends get 72h before ordinary signal exits`;
|
||||
const thesis=agentPlainBlurb(cfg,st);
|
||||
const quoteSummary=(p.maker_quotes||[]).slice(0,3).map(quote=>`${quote.question.slice(0,34)}${quote.question.length>34?"...":""}: YES ${pct(quote.yes_quote)} + NO ${pct(quote.no_quote)}${quote.yes_filled_at||quote.no_filled_at?" (one leg filled)":""}`).join("; ");
|
||||
root.innerHTML=`
|
||||
<div class="brief-main" style="border-top:3px solid ${cfg.color}">
|
||||
<div class="brief-title"><span>${cfg.emoji}</span><span>${cfg.name}</span></div>
|
||||
<p>${esc(thesis)}</p>
|
||||
<p style="margin-top:9px"><b>Adaptation:</b> ${esc(decisionSummary(p))}</p>
|
||||
${quoteSummary?`<p style="margin-top:9px"><b>Resting maker quotes:</b> ${esc(quoteSummary)}</p>`:""}
|
||||
</div>
|
||||
<div class="brief-mini"><div class="k">Style</div><div class="v">${esc(risk)}</div></div>
|
||||
<div class="brief-mini"><div class="k">Trade Rule</div><div class="v">${esc(cadence)}</div></div>
|
||||
@@ -3214,6 +3349,7 @@ function renderPortfolioTab(){
|
||||
{ic:"🕒",label:"24h change",value:format24h(change24h),cls:signClass(change24h.pct)},
|
||||
{ic:"🧪",label:"Adaptive",value:fmtPct(engineBase>0?(eq/engineBase-1)*100:0),cls:signClass(enginePnl)},
|
||||
{ic:"📂",label:"Open",value:p.positions.length},
|
||||
{ic:"⇄",label:"Resting quotes",value:(p.maker_quotes||[]).length},
|
||||
];
|
||||
$("statsPf").innerHTML=stats.map(s=>`<div class="stat"><div class="ic">${s.ic}</div><div class="label">${s.label}</div><div class="value ${s.cls||""}">${s.value}</div></div>`).join("");
|
||||
const sort=$("positionSort");
|
||||
@@ -3265,7 +3401,7 @@ function renderAgentTechnical(agentId){
|
||||
}
|
||||
function agentCompetitionPlan(cfg,row,rank,leader){
|
||||
if(rank===1)return "Plan: press the lead through the strongest qualifying trades while keeping stop-loss and exposure limits active.";
|
||||
if(cfg.id==="value")return "Plan: close the gap by waiting for the cleanest confirmed movement after friction and avoiding crowded trades without enough signal margin.";
|
||||
if(cfg.id==="value")return "Plan: close the gap with complete pricing bundles and paired resting bids, counting return only after both complementary legs fill below their combined payout and unwinding unmatched inventory after 24 hours.";
|
||||
if(cfg.id==="momentum")return "Plan: attack fast-moving markets where fresh volume confirms attention, hoping speed beats slower value strategies.";
|
||||
if(cfg.id==="favorite")return "Plan: grind upward through high-probability favorites, aiming to outlast more volatile agents during choppy markets.";
|
||||
if(cfg.id==="longshot")return "Plan: keep risk small but search for one underpriced outsider that can reprice sharply and leapfrog the leaderboard.";
|
||||
@@ -3279,7 +3415,7 @@ function agentCompetitionPlan(cfg,row,rank,leader){
|
||||
}
|
||||
function recentActionSummary(p){
|
||||
const recent=(p.history||[]).slice(-8);
|
||||
const counts={STOP:0,GAIN:0,EXIT:0,CLOSE:0,OPEN:0,RISK:0};
|
||||
const counts={STOP:0,GAIN:0,EXIT:0,CLOSE:0,OPEN:0,RISK:0,QUOTE:0,MAKER_FILL:0,MAKER_LOCK:0,MAKER_EXIT:0};
|
||||
recent.forEach(h=>{if(counts[h.action]!==undefined)counts[h.action]++;});
|
||||
const bits=[];
|
||||
if(counts.STOP)bits.push(`${counts.STOP} stop-loss sale${counts.STOP===1?"":"s"}`);
|
||||
@@ -3287,6 +3423,10 @@ function recentActionSummary(p){
|
||||
if(counts.EXIT||counts.CLOSE)bits.push(`${counts.EXIT+counts.CLOSE} exit${counts.EXIT+counts.CLOSE===1?"":"s"}`);
|
||||
if(counts.RISK)bits.push(`${counts.RISK} binary-risk rebalance${counts.RISK===1?"":"s"}`);
|
||||
if(counts.OPEN)bits.push(`${counts.OPEN} new entr${counts.OPEN===1?"y":"ies"}`);
|
||||
if(counts.QUOTE)bits.push(`${counts.QUOTE} paired quote${counts.QUOTE===1?"":"s"} staged`);
|
||||
if(counts.MAKER_FILL)bits.push(`${counts.MAKER_FILL} maker fill${counts.MAKER_FILL===1?"":"s"}`);
|
||||
if(counts.MAKER_LOCK)bits.push(`${counts.MAKER_LOCK} locked pair${counts.MAKER_LOCK===1?"":"s"}`);
|
||||
if(counts.MAKER_EXIT)bits.push(`${counts.MAKER_EXIT} unmatched maker exit${counts.MAKER_EXIT===1?"":"s"}`);
|
||||
return bits.length?bits.join(", "):"no major recent trade actions";
|
||||
}
|
||||
function snapshotNearDayAgo(snaps,lastTime){
|
||||
@@ -3297,7 +3437,7 @@ function snapshotNearDayAgo(snaps,lastTime){
|
||||
}
|
||||
function dailyActionSummary(p,lastTime){
|
||||
const cutoff=lastTime-86400000;
|
||||
const counts={STOP:0,GAIN:0,EXIT:0,CLOSE:0,OPEN:0,RISK:0};
|
||||
const counts={STOP:0,GAIN:0,EXIT:0,CLOSE:0,OPEN:0,RISK:0,QUOTE:0,MAKER_FILL:0,MAKER_LOCK:0,MAKER_EXIT:0};
|
||||
(p.history||[]).forEach(h=>{
|
||||
const t=h.date?new Date(`${h.date}T12:00:00`).getTime():NaN;
|
||||
if(Number.isFinite(t)&&t>=cutoff&&counts[h.action]!==undefined)counts[h.action]++;
|
||||
@@ -3308,6 +3448,10 @@ function dailyActionSummary(p,lastTime){
|
||||
if(counts.STOP)bits.push(`${counts.STOP} stop-loss sale${counts.STOP===1?"":"s"}`);
|
||||
if(counts.EXIT||counts.CLOSE)bits.push(`${counts.EXIT+counts.CLOSE} exit${counts.EXIT+counts.CLOSE===1?"":"s"}`);
|
||||
if(counts.RISK)bits.push(`${counts.RISK} binary-risk rebalance${counts.RISK===1?"":"s"}`);
|
||||
if(counts.QUOTE)bits.push(`${counts.QUOTE} paired quote${counts.QUOTE===1?"":"s"} staged`);
|
||||
if(counts.MAKER_FILL)bits.push(`${counts.MAKER_FILL} maker fill${counts.MAKER_FILL===1?"":"s"}`);
|
||||
if(counts.MAKER_LOCK)bits.push(`${counts.MAKER_LOCK} locked pair${counts.MAKER_LOCK===1?"":"s"}`);
|
||||
if(counts.MAKER_EXIT)bits.push(`${counts.MAKER_EXIT} unmatched maker exit${counts.MAKER_EXIT===1?"":"s"}`);
|
||||
return bits.length?bits.join(", "):recentActionSummary(p);
|
||||
}
|
||||
function recentClosedAttribution(p,lastTime){
|
||||
@@ -4560,7 +4704,8 @@ window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
|
||||
onePendingObservationPerMarketSide:true,oldestPendingEvidenceFirst:true,coverageAwareObservationSampling:true,uncertaintyGatedCalibration:true,
|
||||
directionalSignalsRequirePromotion:true,liveOnlyCompleteBundles:true,negativeRiskBundleSides:["YES","NO"],negativeRiskEventScanLimit:NEG_RISK_EVENT_SCAN_LIMIT,
|
||||
negativeRiskMinimumNetReturnPct:NEG_RISK_MIN_NET_RETURN*100,
|
||||
historicalPrior:"All directional trends and reversals require positive independent cohort promotion; Sports and Crypto trends remain excluded; exact ranges and path-dependent barriers are excluded; live-priced complete negative-risk bundles may trade when positive after estimated costs"}),
|
||||
pairedMakerQuotes:true,makerQuoteExpiryHours:MAKER_QUOTE_EXPIRY_HOURS,makerMinimumLockMarginPct:MAKER_MIN_LOCK_MARGIN*100,hypotheticalRewardsCredited:false,
|
||||
historicalPrior:"All directional trends and reversals require positive independent cohort promotion; Sports and Crypto trends remain excluded; exact ranges and path-dependent barriers are excluded; live-priced complete negative-risk bundles and crossed paired maker quotes may trade"}),
|
||||
});
|
||||
function runEngineSelfTest(){
|
||||
const market=(overrides={})=>Object.assign({
|
||||
@@ -4727,6 +4872,23 @@ function runEngineSelfTest(){
|
||||
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 makerMarket=market({id:"maker-test",question:"Will the paired quote test pass?",yes_price:0.42,no_price:0.58,best_bid:0.40,best_ask:0.44,spread:0.04,
|
||||
tick_size:0.01,order_min_size:5,rewards_daily_rate:10,rewards_min_size:50,rewards_max_spread:4.5,competitive:0.8,price_change_1d:0.01});
|
||||
const makerBook=defaultPortfolio(),makerStage=stageMakerQuotes(makerBook,[makerMarket]),makerQuote=makerBook.maker_quotes[0];
|
||||
makerQuote.created_at=hoursAgo(1);
|
||||
const makerFirst=manageMakerQuotes(makerBook,{"maker-test":market(Object.assign({},makerMarket,{yes_price:0.39,no_price:0.61,best_bid:0.38,best_ask:0.40}))},{executeTrades:true});
|
||||
const makerSingleFill=makerBook.positions.length===1&&makerBook.positions[0].side==="YES"&&makerBook.maker_quotes.length===1;
|
||||
const makerSecond=manageMakerQuotes(makerBook,{"maker-test":market(Object.assign({},makerMarket,{yes_price:0.60,no_price:0.40,best_bid:0.57,best_ask:0.61}))},{executeTrades:true});
|
||||
const makerExpectedProfit=+(makerQuote.units*(1-Number(makerQuote.yes_fill_price)-Number(makerQuote.no_fill_price))).toFixed(2);
|
||||
const makerOverlapState=defaultState();makerOverlapState.agents.value=makerBook;reduceStrategyOverlap(makerOverlapState);
|
||||
const makerPairSurvivesOverlap=makerBook.positions.length===2&&makerBook.positions.every(pos=>pos.requires_complete_bundle);
|
||||
markToMarket(makerBook,{"maker-test":market(Object.assign({},makerMarket,{yes_price:1,no_price:0,best_bid:1,best_ask:1,closed:true,accepting_orders:false}))},AGENTS[0],{policyExits:true,executeTrades:true});
|
||||
const makerExpiryBook=defaultPortfolio();stageMakerQuotes(makerExpiryBook,[makerMarket]);makerExpiryBook.maker_quotes[0].created_at=hoursAgo(1);
|
||||
manageMakerQuotes(makerExpiryBook,{"maker-test":market(Object.assign({},makerMarket,{yes_price:0.39,no_price:0.61,best_bid:0.38,best_ask:0.40}))},{executeTrades:true});
|
||||
makerExpiryBook.maker_quotes[0].created_at=hoursAgo(25);
|
||||
const makerExpired=manageMakerQuotes(makerExpiryBook,{"maker-test":market(Object.assign({},makerMarket,{yes_price:0.36,no_price:0.64,best_bid:0.35,best_ask:0.37}))},{executeTrades:true});
|
||||
const makerOfflineBook=defaultPortfolio();stageMakerQuotes(makerOfflineBook,[makerMarket]);makerOfflineBook.maker_quotes[0].created_at=hoursAgo(1);
|
||||
const makerOffline=manageMakerQuotes(makerOfflineBook,{"maker-test":market(Object.assign({},makerMarket,{yes_price:0.39,no_price:0.61,best_bid:0.38,best_ask:0.40}))},{executeTrades:false});
|
||||
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",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},
|
||||
@@ -4908,6 +5070,18 @@ function runEngineSelfTest(){
|
||||
riskTrimConservesEquity:+equity(riskBook).toFixed(2)===10000,
|
||||
adaptiveReturnSetsLeader:adaptiveRankFixture[0].c.id==="adaptive-leader",
|
||||
},
|
||||
makerLiquidity:{
|
||||
stagesEligiblePairedQuote:makerStage.staged===1&&makerStage.active===1&&makerQuote.paired_cost===0.96,
|
||||
firstCrossCreatesOnlyOneInventoryLeg:makerFirst.fills===1&&makerSingleFill,
|
||||
secondCrossLocksComplementaryPair:makerSecond.fills===1&&makerSecond.locked===1&&makerBook.maker_quotes.length===0,
|
||||
overlapCleanupPreservesLockedPair:makerPairSurvivesOverlap,
|
||||
lockedPairSettlesAtQuotedProfit:makerBook.positions.length===0&&Math.abs(makerBook.cash-(10000+makerExpectedProfit))<=0.02,
|
||||
unmatchedInventoryExpiresAtExecutableBid:makerExpired.expired===1&&makerExpiryBook.positions.length===0&&makerExpiryBook.maker_quotes.length===0&&Math.abs(makerExpiryBook.cash-9997.5)<=0.02,
|
||||
offlineSnapshotCannotInventFills:makerOffline.fills===0&&makerOfflineBook.positions.length===0&&makerOfflineBook.maker_quotes.length===1,
|
||||
noHypotheticalRewardCredited:makerStage.staged===1&&Math.abs(makerBook.cash-(10000+makerExpectedProfit))<=0.02,
|
||||
rejectsUnrewardedMarket:makerPairCandidate(Object.assign({},makerMarket,{rewards_daily_rate:0}))===null,
|
||||
rejectsVolatileMarket:makerPairCandidate(Object.assign({},makerMarket,{price_change_1d:0.12}))===null,
|
||||
},
|
||||
bundleArbitrage:{
|
||||
rejectsNonBinaryMarketLabels:nonBinaryMarketRejected,
|
||||
rejectsNonBinaryBundleLabels:nonBinaryBundleRejected,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"scripts": {
|
||||
"evaluate:neg-risk": "node scripts/evaluate-neg-risk.mjs",
|
||||
"evaluate:dominance": "node scripts/evaluate-dominance.mjs",
|
||||
"evaluate:adaptive": "node scripts/evaluate-adaptive.mjs",
|
||||
"evaluate:liquidity": "node scripts/evaluate-liquidity.mjs",
|
||||
"evaluate:signals": "node scripts/evaluate-signals.mjs",
|
||||
"evaluate:settlements": "node scripts/evaluate-settlements.mjs",
|
||||
"test:server-state": "node scripts/test-server-state.mjs"
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
const GAMMA = "https://gamma-api.polymarket.com";
|
||||
const CLOB = "https://clob.polymarket.com";
|
||||
const MARKET_LIMIT = Math.max(50, Math.min(500, Number(process.env.ADAPTIVE_MARKETS || 300)));
|
||||
const CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.ADAPTIVE_CONCURRENCY || 6)));
|
||||
const COST = Math.max(0, Math.min(0.05, Number(process.env.ADAPTIVE_COST_CENTS || 0.5) / 100));
|
||||
const HOUR = 3600;
|
||||
|
||||
function parseJson(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
try { return JSON.parse(value || "[]"); } catch { return []; }
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
function categoryOf(raw) {
|
||||
const text = `${raw.question || ""} ${(raw.tags || []).map((tag) => tag.slug || tag.label || "").join(" ")}`.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(nba|nfl|nhl|mlb|soccer|football|baseball|basketball|tennis|ufc|boxing|championship|match|game|tournament|league)\b/.test(text)) return "Sports";
|
||||
if (/\b(fed|inflation|gdp|recession|stock|company|economy|tariff|interest rate|unemployment|earnings)\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 fetchMarkets(limit) {
|
||||
const markets = [], seen = new Set(), pageSize = 100;
|
||||
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(pageSize), offset: String(offset), order: "volume24hr", ascending: "false" });
|
||||
const page = await fetchJson(`${GAMMA}/markets?${params}`);
|
||||
if (!Array.isArray(page) || !page.length) break;
|
||||
for (const raw of page) {
|
||||
const id = String(raw.id || ""), labels = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
|
||||
const tokenId = String(parseJson(raw.clobTokenIds)[0] || "");
|
||||
if (!id || seen.has(id) || !tokenId || labels[0] !== "yes" || labels[1] !== "no") continue;
|
||||
seen.add(id);
|
||||
markets.push({ id, tokenId, question: raw.question || "", category: categoryOf(raw),
|
||||
eventKey: String(raw.events?.[0]?.id || raw.events?.[0]?.slug || raw.eventId || id) });
|
||||
if (markets.length >= limit) break;
|
||||
}
|
||||
if (page.length < pageSize) break;
|
||||
}
|
||||
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 atOrAfter(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]; hi = mid - 1; }
|
||||
else lo = mid + 1;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
function observations(market, points) {
|
||||
const rows = [], seenBuckets = new Set();
|
||||
for (const current of points) {
|
||||
const bucket = Math.floor(current.t / (12 * HOUR));
|
||||
if (seenBuckets.has(bucket) || current.p < 0.08 || current.p > 0.92) continue;
|
||||
const history = {};
|
||||
let complete = true;
|
||||
for (const hours of [1, 6, 24, 72, 168]) {
|
||||
const prior = atOrBefore(points, current.t - hours * HOUR);
|
||||
if (!prior || current.t - hours * HOUR - prior.t > 3 * HOUR) { complete = false; break; }
|
||||
history[hours] = current.p - prior.p;
|
||||
}
|
||||
if (!complete) continue;
|
||||
const future = {};
|
||||
for (const hours of [24, 72]) {
|
||||
const next = atOrAfter(points, current.t + hours * HOUR);
|
||||
if (next && next.t - (current.t + hours * HOUR) <= 3 * HOUR) future[hours] = next.p;
|
||||
}
|
||||
if (!Number.isFinite(future[24]) && !Number.isFinite(future[72])) continue;
|
||||
seenBuckets.add(bucket);
|
||||
rows.push({ marketId: market.id, eventKey: market.eventKey, category: market.category,
|
||||
observedAt: current.t, price: current.p, moves: history, future });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function bandOf(price) {
|
||||
if (price < 0.3) return "longshot";
|
||||
if (price < 0.7) return "mid";
|
||||
return "favorite";
|
||||
}
|
||||
|
||||
function tradeFor(row, rule, horizon) {
|
||||
const move = row.moves[rule.lookback], magnitude = Math.abs(move);
|
||||
if (!move || magnitude < rule.minMove || magnitude > rule.maxMove) return null;
|
||||
const longSign = Math.sign(row.moves[24]), slowSign = Math.sign(row.moves[168]);
|
||||
if (rule.agreement === "same" && (!longSign || longSign !== slowSign)) return null;
|
||||
if (rule.agreement === "opposite" && (!longSign || longSign === slowSign)) return null;
|
||||
if (rule.category !== "All" && row.category !== rule.category) return null;
|
||||
const direction = Math.sign(move) * (rule.mode === "follow" ? 1 : -1);
|
||||
const side = direction > 0 ? "YES" : "NO", entry = side === "YES" ? row.price : 1 - row.price;
|
||||
const futureYes = row.future[horizon], exit = side === "YES" ? futureYes : 1 - futureYes;
|
||||
if (!Number.isFinite(exit) || entry < 0.08 || entry > 0.92 || (rule.band !== "all" && bandOf(entry) !== rule.band)) return null;
|
||||
return { eventKey: row.eventKey, marketId: row.marketId, observedAt: row.observedAt,
|
||||
netReturn: exit / entry - 1 - COST / entry };
|
||||
}
|
||||
|
||||
function summary(trades, confidence = 1.645) {
|
||||
if (!trades.length) return { trades: 0, markets: 0, events: 0, mean: 0, eventMean: 0, lower: 0, upper: 0, winRate: 0 };
|
||||
const buckets = new Map();
|
||||
for (const trade of trades) {
|
||||
const values = buckets.get(trade.eventKey) || [];
|
||||
values.push(trade.netReturn); buckets.set(trade.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: trades.length, markets: new Set(trades.map((trade) => trade.marketId)).size, events: eventReturns.length,
|
||||
mean: trades.reduce((sum, trade) => sum + trade.netReturn, 0) / trades.length,
|
||||
eventMean, lower: eventMean - margin, upper: eventMean + margin,
|
||||
winRate: trades.filter((trade) => trade.netReturn > 0).length / trades.length };
|
||||
}
|
||||
|
||||
const rules = [];
|
||||
for (const lookback of [6, 24, 72, 168]) {
|
||||
for (const mode of ["follow", "fade"]) {
|
||||
for (const [minMove, maxMove] of [[0.005, 0.04], [0.01, 0.08], [0.02, 0.15], [0.04, 1]]) {
|
||||
for (const band of ["all", "longshot", "mid", "favorite"]) {
|
||||
for (const category of ["All", "Politics", "Sports", "Crypto", "Other"]) {
|
||||
for (const agreement of ["any", "same", "opposite"]) {
|
||||
const id = `${mode}_${lookback}h_${minMove}-${maxMove}_${band}_${category}_${agreement}`;
|
||||
rules.push({ id, lookback, mode, minMove, maxMove, band, category, agreement });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const markets = await fetchMarkets(MARKET_LIMIT);
|
||||
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) }))
|
||||
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.p)).sort((a, b) => a.t - b.t);
|
||||
return { rows: observations(market, points), points: points.length };
|
||||
});
|
||||
const rows = histories.filter((result) => result && !result.error).flatMap((result) => result.rows);
|
||||
const times = rows.map((row) => row.observedAt).sort((a, b) => a - b);
|
||||
const trainCut = times[Math.floor(times.length * 0.6)] || 0, validationCut = times[Math.floor(times.length * 0.8)] || 0;
|
||||
|
||||
function evaluateHorizon(horizon) {
|
||||
const trainRows = rows.filter((row) => row.observedAt < trainCut && row.observedAt + horizon * HOUR < trainCut);
|
||||
const validationRows = rows.filter((row) => row.observedAt >= trainCut && row.observedAt + horizon * HOUR < validationCut);
|
||||
const testRows = rows.filter((row) => row.observedAt >= validationCut);
|
||||
const selected = [];
|
||||
for (const rule of rules) {
|
||||
const train = summary(trainRows.map((row) => tradeFor(row, rule, horizon)).filter(Boolean));
|
||||
if (train.trades < 80 || train.events < 15 || train.lower <= 0) continue;
|
||||
const validation = summary(validationRows.map((row) => tradeFor(row, rule, horizon)).filter(Boolean));
|
||||
if (validation.trades < 40 || validation.events < 10 || validation.lower <= 0) continue;
|
||||
selected.push({ rule, train, validation });
|
||||
}
|
||||
const tested = selected.map((candidate) => {
|
||||
const test = summary(testRows.map((row) => tradeFor(row, candidate.rule, horizon)).filter(Boolean));
|
||||
return { ...candidate, test, passesHoldout: test.trades >= 40 && test.events >= 10 && test.lower > 0 };
|
||||
}).sort((a, b) => (Number(b.passesHoldout) - Number(a.passesHoldout)) || b.test.lower - a.test.lower);
|
||||
return { horizon, trainRows: trainRows.length, validationRows: validationRows.length, testRows: testRows.length,
|
||||
validationSelected: selected.length, holdoutPassed: tested.filter((candidate) => candidate.passesHoldout).length,
|
||||
candidates: tested.slice(0, 30) };
|
||||
}
|
||||
|
||||
const compact = (stats) => Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, Number.isFinite(value) ? +value.toFixed(5) : value]));
|
||||
const horizons = [24, 72].map(evaluateHorizon).map((result) => ({ ...result,
|
||||
candidates: result.candidates.map((candidate) => ({ rule: candidate.rule, passesHoldout: candidate.passesHoldout,
|
||||
train: compact(candidate.train), validation: compact(candidate.validation), test: compact(candidate.test) })) }));
|
||||
const passedByHorizon = horizons.map((result) => new Set(result.candidates.filter((candidate) => candidate.passesHoldout).map((candidate) => candidate.rule.id)));
|
||||
const durableRuleIds = [...passedByHorizon[0]].filter((id) => passedByHorizon[1].has(id));
|
||||
|
||||
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: MARKET_LIMIT, fetchedMarkets: markets.length,
|
||||
historiesWithData: histories.filter((result) => result && !result.error && result.points).length,
|
||||
failures: histories.filter((result) => result?.error).length, observations: rows.length,
|
||||
methodology: { costCents: COST * 100, observationSpacingHours: 12, split: "60% train / 20% validation / 20% untouched holdout",
|
||||
clusterUnit: "Polymarket event", candidateRules: rules.length,
|
||||
promotionGate: "positive event-clustered 90% lower bound with minimum support in train, validation, and holdout" },
|
||||
durableRuleIds, horizons }, null, 2));
|
||||
@@ -0,0 +1,75 @@
|
||||
const GAMMA = "https://gamma-api.polymarket.com";
|
||||
const LIMIT = Math.max(100, Math.min(500, Number(process.env.LIQUIDITY_MARKETS || 500)));
|
||||
|
||||
function parseJson(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
try { return JSON.parse(value || "[]"); } catch { return []; }
|
||||
}
|
||||
|
||||
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 fetchMarkets(limit) {
|
||||
const markets = [];
|
||||
for (let offset = 0; markets.length < limit && offset < limit * 4; offset += 100) {
|
||||
const params = new URLSearchParams({ active: "true", closed: "false", archived: "false", include_tag: "true",
|
||||
limit: "100", offset: String(offset), order: "volume24hr", ascending: "false" });
|
||||
const page = await fetchJson(`${GAMMA}/markets?${params}`);
|
||||
if (!Array.isArray(page) || !page.length) break;
|
||||
for (const raw of page) {
|
||||
const labels = parseJson(raw.outcomes).map((outcome) => String(outcome).trim().toLowerCase());
|
||||
if (labels[0] !== "yes" || labels[1] !== "no") continue;
|
||||
markets.push(raw);
|
||||
if (markets.length >= limit) break;
|
||||
}
|
||||
if (page.length < 100) break;
|
||||
}
|
||||
return markets;
|
||||
}
|
||||
|
||||
function candidate(raw) {
|
||||
const bestBid = Number(raw.bestBid), bestAsk = Number(raw.bestAsk), spread = bestAsk - bestBid;
|
||||
const dailyRate = (raw.clobRewards || []).reduce((sum, reward) => sum + Number(reward.rewardsDailyRate || 0), 0);
|
||||
const minSize = Math.max(Number(raw.rewardsMinSize || 0), Number(raw.orderMinSize || 0), 5);
|
||||
const maxSpread = Number(raw.rewardsMaxSpread || 0) / 100;
|
||||
const tick = Number(raw.orderPriceMinTickSize || 0.01), dayMove = Math.abs(Number(raw.oneDayPriceChange || 0));
|
||||
const yesQuote = bestBid, noQuote = 1 - bestAsk, pairedCost = yesQuote + noQuote;
|
||||
const requiredCapital = minSize * pairedCost, lockedProfit = minSize * (1 - pairedCost);
|
||||
const scoring = dailyRate > 0 && spread > 0 && minSize > 0 && maxSpread > 0 && spread / 2 <= maxSpread
|
||||
&& Number.isFinite(yesQuote) && Number.isFinite(noQuote) && yesQuote >= tick && noQuote >= tick;
|
||||
return { marketId: String(raw.id || ""), conditionId: raw.conditionId || "", question: raw.question || "",
|
||||
url: raw.events?.[0]?.slug ? `https://polymarket.com/event/${raw.events[0].slug}` : "",
|
||||
dailyRate, minSize, maxSpread, spread, tick, bestBid, bestAsk, yesQuote, noQuote, pairedCost,
|
||||
lockedProfit, requiredCapital, maximumRewardYield: requiredCapital > 0 ? dailyRate / requiredCapital : 0,
|
||||
competitiveness: Number(raw.competitive || raw.events?.[0]?.competitive || 0), dayMove,
|
||||
volume24hr: Number(raw.volume24hr || 0), liquidity: Number(raw.liquidityNum || raw.liquidity || 0), scoring };
|
||||
}
|
||||
|
||||
const markets = await fetchMarkets(LIMIT), candidates = markets.map(candidate).filter((row) => row.scoring);
|
||||
const balanced = candidates.filter((row) => row.spread >= 0.02 && row.dayMove <= Math.max(0.02, row.spread)
|
||||
&& row.liquidity >= 5000 && row.volume24hr >= 2000 && row.requiredCapital <= 5000)
|
||||
.sort((a, b) => (b.dailyRate / Math.max(1, b.requiredCapital)) - (a.dailyRate / Math.max(1, a.requiredCapital)) || b.spread - a.spread);
|
||||
const compact = (row) => ({ ...row, dailyRate: +row.dailyRate.toFixed(3), maxSpread: +row.maxSpread.toFixed(4),
|
||||
spread: +row.spread.toFixed(4), bestBid: +row.bestBid.toFixed(4), bestAsk: +row.bestAsk.toFixed(4),
|
||||
yesQuote: +row.yesQuote.toFixed(4), noQuote: +row.noQuote.toFixed(4), pairedCost: +row.pairedCost.toFixed(4),
|
||||
lockedProfit: +row.lockedProfit.toFixed(2), requiredCapital: +row.requiredCapital.toFixed(2),
|
||||
maximumRewardYield: +row.maximumRewardYield.toFixed(4), competitiveness: +row.competitiveness.toFixed(4),
|
||||
dayMove: +row.dayMove.toFixed(4), volume24hr: +row.volume24hr.toFixed(2), liquidity: +row.liquidity.toFixed(2) });
|
||||
|
||||
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), requestedMarkets: LIMIT, fetchedEligibleMarkets: markets.length,
|
||||
rewardScoringMarkets: candidates.length, balancedPairedQuoteCandidates: balanced.length,
|
||||
methodology: { fillCredit: "none; scanner identifies resting-quote candidates only",
|
||||
rewardCredit: "none; maximumRewardYield assumes an impossible 100% reward share and is ranking context only",
|
||||
pairedPayout: "$1 if both complementary bids eventually fill", singleFillRisk: "directional until the complementary quote fills or inventory exits" },
|
||||
candidates: balanced.slice(0, 50).map(compact) }, null, 2));
|
||||
Reference in New Issue
Block a user