Replace forced exposure with confirmed signals

This commit is contained in:
Theodore Song
2026-08-09 15:47:06 -04:00
parent 0ef05ea734
commit ab333dd045
+286 -264
View File
@@ -340,7 +340,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">Exposure engine · v33</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">Confirmation engine · v34</div></div>
</div>
<div class="tabs" id="tabs">
<button class="tab" data-tab="overview">Overview</button>
@@ -362,7 +362,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 v33 active:</b> every agent now has a persistent exposure band and a quality-checked allocation lane that rebuilds positions when cash rises above its target. Refills still require positive post-cost edge, evidence, liquidity, and settlement time. Aggressive agents can lose more; no return is guaranteed.</div>
<div class="live-build-banner"><b>Build v34 active:</b> exposure targets are ceilings, not quotas. New trades need a confirmed multi-window signal, liquidity, evidence, and a positive post-friction signal margin. Exits use holding hysteresis, reachable profit locks, and strict two-agent overlap. This remains paper trading; profits are not guaranteed.</div>
<!-- ============ OVERVIEW ============ -->
<section class="tabpanel" data-tab="overview">
@@ -377,12 +377,12 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<g fill="#d9b4ff"><circle cx="380" cy="40" r="6"/></g>
</svg>
<h1>Ten AI agents race to <span class="grad">beat the market</span></h1>
<p>Each cycle loads the 500 most active Polymarket markets, ranks them by activity, liquidity, timing, and estimated price gap, then lets agents trade from that tighter pool.</p>
<p>Each cycle loads the 500 most active Polymarket markets, then requires direction to agree across independent time windows before liquidity, evidence, timing, and friction checks can make an idea trade-ready.</p>
<div class="badges">
<span class="hbadge">🏆 10 competing agents</span>
<span class="hbadge">5 aggressive strategies</span>
<span class="hbadge">Top 500 active markets</span>
<span class="hbadge">Ranked by activity + gap</span>
<span class="hbadge">Ranked by activity + confirmation</span>
<span class="hbadge">Live Polymarket data</span>
</div>
</div>
@@ -713,7 +713,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<!-- ============ ABOUT ============ -->
<section class="tabpanel" data-tab="about">
<div class="section-title">️ The competition</div>
<p class="muted" style="margin-top:-6px;max-width:820px">Every agent starts with <b>$10,000</b> in paper money. Five core strategies use disciplined entry rules, while five aggressive strategies pursue catalysts, reversals, breakouts, asymmetric tails, and concentrated conviction. The aggressive group accepts thinner but still positive measured edges, holds less cash, and risks more per position, so both gains and drawdowns can be substantially larger.</p>
<p class="muted" style="margin-top:-6px;max-width:820px">Every agent starts with <b>$10,000</b> in paper money. Five core strategies use disciplined entry rules, while five aggressive strategies pursue catalysts, reversals, breakouts, asymmetric tails, and concentrated conviction. Aggressive agents may size confirmed setups more heavily, but cash is retained whenever their strategy has no qualifying signal.</p>
<div class="about-grid" id="aboutAgents"></div>
<div class="card" style="margin-top:16px">
<div class="card-h"><h3>The live cycle</h3></div>
@@ -744,7 +744,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</section>
<footer>
Build exposure-v33 · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
Build confirmation-v34 · 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>
@@ -763,15 +763,16 @@ const STOP_LOSS_TIERS = [
{id:"18",drawdown:0.18,sellFrac:1.00,label:"-18%"},
];
const GAIN_STOP_TIERS = [
{id:"75",gain:0.75,multiple:1.75,sellFrac:0.25,label:"+75%"},
{id:"150",gain:1.50,multiple:2.50,sellFrac:0.25,label:"+150%"},
{id:"300",gain:3.00,multiple:4.00,sellFrac:0.25,label:"+300%"},
{id:"lock1",gain:0.15,maxGainFrac:0.35,sellFrac:0.25,label:"first profit lock"},
{id:"lock2",gain:0.30,maxGainFrac:0.60,sellFrac:0.25,label:"second profit lock"},
{id:"lock3",gain:0.50,maxGainFrac:0.85,sellFrac:0.25,label:"final profit lock"},
];
const EXIT_STALE_DAYS = 10;
const EXIT_RESOLUTION_DAYS = 2;
const EXIT_STALE_DAYS = 21;
const MIN_POLICY_HOLD_HOURS = 24;
const EXIT_CONFIRM_HOURS = 6;
const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v5";
const SUGGESTION_ENGINE_VERSION = 33;
const SUGGESTION_ENGINE_VERSION = 34;
const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1";
@@ -850,7 +851,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:"Looks for the widest net gap between model fair value and current market price after liquidity friction, chase penalties, and real-world evidence checks. It prefers trades where the crowd appears too pessimistic or too optimistic, then sizes positions moderately so one bad read does not dominate the portfolio.",
blurb:"Looks for the strongest confirmed movement that remains after liquidity friction, chase penalties, and evidence checks. It requires agreement across independent time windows and sizes positions moderately so one bad read does not dominate the portfolio.",
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",
@@ -862,7 +863,7 @@ const AGENTS = [
rank:(s)=>[...s].filter(x=>x.entry_price>=0.6).sort((a,b)=>(b.entry_price+Math.abs(b.net_edge||b.edge||0)+Number(b.evidence_score||0)*.2)-(a.entry_price+Math.abs(a.net_edge||a.edge||0)+Number(a.evidence_score||0)*.2)||b.conviction-a.conviction),
maxNew:4, maxFrac:0.05, minConv:58, kelly:0.24, targetExposure:0.58, drawdownExposure:0.38},
{id:"longshot", name:"Longshot Hunter", emoji:"🎰", color:"#fbbf24", kind:"strategy",
blurb:"The swing-for-upside strategy. It hunts cheaper contracts that the model thinks are being ignored, but now discounts thin tails unless evidence and net edge are strong enough to justify the volatility.",
blurb:"The swing-for-upside strategy. It considers cheaper contracts only when price direction is independently confirmed and the remaining signal margin, evidence, and liquidity justify the volatility.",
rank:(s)=>[...s].filter(x=>x.entry_price<=0.48).sort((a,b)=>(Math.abs(b.net_edge||b.edge||0)*100+b.conviction+Number(b.evidence_score||0)*22-b.entry_price*12)-(Math.abs(a.net_edge||a.edge||0)*100+a.conviction+Number(a.evidence_score||0)*22-a.entry_price*12)),
maxNew:6, maxFrac:0.07, minConv:57, kelly:0.25, allocationFrac:0.065, targetExposure:0.50, drawdownExposure:0.38},
{id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy",
@@ -874,7 +875,7 @@ const AGENTS = [
rank:(s)=>[...s].sort((a,b)=>(Number(b.evidence_source_count||0)*28+Number(b.evidence_score||0)*32+effectiveEntryEdge(b)*170+b.conviction+Math.log10(Number(b.volume_24hr||0)+1)*5)-(Number(a.evidence_source_count||0)*28+Number(a.evidence_score||0)*32+effectiveEntryEdge(a)*170+a.conviction+Math.log10(Number(a.volume_24hr||0)+1)*5)),
maxNew:7, maxFrac:0.09, minConv:52, kelly:0.46, allocationFrac:0.055, maxPositions:18, maxCategoryPct:0.40, maxCycleDrawdown:8, targetExposure:0.72, drawdownExposure:0.60},
{id:"reversal", name:"Reversal Edge", emoji:"↻", color:"#ff5f91", kind:"strategy", aggressive:true,
blurb:"A contrarian mean-reversion strategy that looks for sharp one-day moves the fair-value model believes went too far. It buys against the recent direction only when the remaining post-cost gap, evidence, and liquidity support a snapback, then uses larger positions than the core group to make successful reversals matter.",
blurb:"A contrarian mean-reversion strategy that waits for an hourly reversal after a sharp one-day move. It buys against the earlier direction only when that snapback is confirmed by liquidity, evidence, and a positive post-friction signal margin.",
rank:(s)=>[...s].map(reversalCandidate).filter(Boolean).sort((a,b)=>(Math.abs(Number(b.price_change_1d||0))*95+effectiveEntryEdge(b)*190+Number(b.evidence_score||0)*24+b.conviction)-(Math.abs(Number(a.price_change_1d||0))*95+effectiveEntryEdge(a)*190+Number(a.evidence_score||0)*24+a.conviction)),
maxNew:6, maxFrac:0.08, minConv:54, kelly:0.42, maxPositions:18, maxCategoryPct:0.40, maxCycleDrawdown:8, targetExposure:0.80, drawdownExposure:0.65},
{id:"breakout", name:"Breakout Velocity", emoji:"▲", color:"#00d5b5", kind:"strategy", aggressive:true,
@@ -882,11 +883,11 @@ const AGENTS = [
rank:(s)=>[...s].sort((a,b)=>(Number(b.momentum_strength||0)*65+Math.log10(Number(b.volume_24hr||0)+1)*9+effectiveEntryEdge(b)*130+b.conviction)-(Number(a.momentum_strength||0)*65+Math.log10(Number(a.volume_24hr||0)+1)*9+effectiveEntryEdge(a)*130+a.conviction)),
maxNew:8, maxFrac:0.075, minConv:50, kelly:0.44, maxPositions:22, maxCategoryPct:0.40, maxCycleDrawdown:8, targetExposure:0.88, drawdownExposure:0.72},
{id:"tailalpha", name:"Tail Risk Alpha", emoji:"✦", color:"#ffd166", kind:"strategy", aggressive:true,
blurb:"An asymmetric-upside strategy focused on cheaper contracts whose implied odds look too low relative to evidence and model value. It accepts a low hit rate and volatile results in exchange for larger potential payoffs, but still requires liquidity, time to resolution, and a positive post-cost edge before entering.",
blurb:"An asymmetric-upside strategy focused on cheaper contracts with confirmed direction and room to reprice. It accepts a low hit rate and volatile results in exchange for larger potential payoffs, but still requires evidence, liquidity, and time to resolution.",
rank:(s)=>[...s].filter(x=>Number(x.entry_price||1)<=0.42).sort((a,b)=>(effectiveEntryEdge(b)*220+Number(b.evidence_score||0)*42+b.conviction-Number(b.entry_price||0)*35)-(effectiveEntryEdge(a)*220+Number(a.evidence_score||0)*42+a.conviction-Number(a.entry_price||0)*35)),
maxNew:6, maxFrac:0.07, minConv:54, kelly:0.38, allocationFrac:0.065, maxPositions:18, maxCategoryPct:0.40, maxCycleDrawdown:8, targetExposure:0.72, drawdownExposure:0.58},
{id:"conviction", name:"Conviction Max", emoji:"◆", color:"#b58cff", kind:"strategy", aggressive:true,
blurb:"A concentrated highest-conviction strategy that ignores mediocre opportunities and commits heavily to the few markets with the strongest combined edge, evidence, and confidence. It deliberately runs a smaller book with position sizes up to ten percent of equity, producing the clearest risk-and-reward behavior of the aggressive group.",
blurb:"A concentrated highest-conviction strategy that ignores mediocre opportunities and commits to the few markets with the strongest combined confirmation, evidence, and signal margin. It deliberately runs a smaller book so its best ideas matter without filling exposure for its own sake.",
rank:(s)=>[...s].sort((a,b)=>(effectiveEntryEdge(b)*250+Number(b.evidence_score||0)*55+b.conviction*1.4+Number(b.evidence_source_count||0)*12)-(effectiveEntryEdge(a)*250+Number(a.evidence_score||0)*55+a.conviction*1.4+Number(a.evidence_source_count||0)*12)),
maxNew:4, maxFrac:0.10, minConv:60, kelly:0.58, maxPositions:12, maxCategoryPct:0.45, maxCycleDrawdown:8, targetExposure:0.82, drawdownExposure:0.68},
];
@@ -1002,21 +1003,21 @@ async function fetchMarketPrice(id){
}
/* ---------- Analysis engine ---------- */
const W={LIQ:0.28,MOM:0.22,MIS:0.34,TIME:0.16};
const W={LIQ:0.24,MOM:0.18,SIGNAL:0.38,TIME:0.20};
const MIN_VOLUME=18000;
const MIN_LIQUIDITY=1400;
const MIN_SCOUT_VOLUME=1500;
const MIN_SCOUT_LIQUIDITY=150;
const MIN_SCOUT_EDGE=0.006;
const MIN_ENTRY_EDGE=0.032;
const MIN_ENTRY_EDGE=0.028;
const MIN_SELECTIVE_ENTRY_EDGE=0.022;
const MIN_TREND_EDGE=0.012;
const MIN_LIQUIDITY_EDGE=0.018;
const MIN_CORE_PROBE_EDGE=0.015;
const MIN_AGGRESSIVE_EDGE=0.008;
const MIN_ALLOCATION_EDGE=0.006;
const MIN_ENTRY_DAYS=7.0;
const EDGE_SCALE=0.13;
const MIN_TREND_EDGE=0.018;
const MIN_LIQUIDITY_EDGE=0.020;
const MIN_CORE_PROBE_EDGE=0.024;
const MIN_AGGRESSIVE_EDGE=0.016;
const MIN_ALLOCATION_EDGE=MIN_AGGRESSIVE_EDGE;
const MIN_ENTRY_DAYS=3.0;
const EDGE_SCALE=0.08;
const MAX_STRATEGY_POSITIONS=30;
const MAX_NEW_POSITION_PCT=0.06;
const MAX_PROBE_POSITION_PCT=0.015;
@@ -1037,13 +1038,13 @@ function liquiditySignal(m){const vol=Math.min(1,Math.log10(m.volume+1)/6.0);con
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);}
function categoryPolicy(cat){
return ({
Politics:{minEdge:0.040,minVol:22000,minLiq:1800,minEvidence:0.48,uncertainty:0.006,label:"politics needs outside confirmation"},
Sports:{minEdge:0.050,minVol:26000,minLiq:2200,minEvidence:0.55,uncertainty:0.010,label:"sports needs a large edge and low settlement-gap risk"},
Crypto:{minEdge:0.045,minVol:28000,minLiq:2400,minEvidence:0.54,uncertainty:0.009,label:"crypto needs a larger edge after jump-risk penalties"},
Economy:{minEdge:0.044,minVol:22000,minLiq:1800,minEvidence:0.50,uncertainty:0.007,label:"macro markets need stronger evidence"},
"Pop Culture":{minEdge:0.040,minVol:16000,minLiq:1300,minEvidence:0.46,uncertainty:0.005,label:"culture markets need attention confirmation"},
Other:{minEdge:0.048,minVol:24000,minLiq:1900,minEvidence:0.50,uncertainty:0.009,label:"uncategorized markets need the highest proof"},
})[cat||"Other"]||({minEdge:0.048,minVol:24000,minLiq:1900,minEvidence:0.50,uncertainty:0.009,label:"uncategorized markets need the highest proof"});
Politics:{minEdge:0.022,minVol:22000,minLiq:1800,minEvidence:0.48,uncertainty:0.005,label:"politics needs confirmed price action and outside context"},
Sports:{minEdge:0.024,minVol:26000,minLiq:2200,minEvidence:0.55,uncertainty:0.007,label:"sports needs strong confirmation and low settlement-gap risk"},
Crypto:{minEdge:0.024,minVol:28000,minLiq:2400,minEvidence:0.54,uncertainty:0.007,label:"crypto needs confirmation after jump-risk penalties"},
Economy:{minEdge:0.023,minVol:22000,minLiq:1800,minEvidence:0.50,uncertainty:0.006,label:"macro markets need strong evidence"},
"Pop Culture":{minEdge:0.020,minVol:16000,minLiq:1300,minEvidence:0.46,uncertainty:0.004,label:"culture markets need attention confirmation"},
Other:{minEdge:0.025,minVol:24000,minLiq:1900,minEvidence:0.52,uncertainty:0.007,label:"uncategorized markets need the highest proof"},
})[cat||"Other"]||({minEdge:0.025,minVol:24000,minLiq:1900,minEvidence:0.52,uncertainty:0.007,label:"uncategorized markets need the highest proof"});
}
function frictionPenalty(m){
const liq=liquiditySignal(m);
@@ -1054,13 +1055,13 @@ function frictionPenalty(m){
return clamp(thinPenalty+tailPenalty+lowLiqPenalty,0.002,0.034);
}
function chasePenalty(m,rawEdge){
const mom=momentumSignal(m),absEdge=Math.abs(rawEdge);
if(mom<0.72)return 0;
return clamp((mom-0.72)*0.018+(absEdge<0.07?0.005:0),0,0.016);
const day=Math.abs(Number(m.price_change_1d||0)),hour=Math.abs(Number(m.price_change_1h||0));
if(day<0.045&&hour<0.012)return 0;
return clamp(Math.max(0,day-0.045)*0.20+Math.max(0,hour-0.012)*0.35,0,0.024);
}
function textEvidenceSignal(m,external={}){
const text=`${m.question||""} ${(m.tags||[]).join(" ")}`.toLowerCase();
let evidence=0.42;
let evidence=0.30;
const drivers=[];
if(/\b(poll|approval|election|primary|senate|house|minister|president)\b/.test(text)){evidence+=0.08;drivers.push("political-event context");}
if(/\b(injury|starter|lineup|playoff|final|championship|tournament|wins?|score)\b/.test(text)){evidence+=0.09;drivers.push("sports-event context");}
@@ -1068,7 +1069,7 @@ function textEvidenceSignal(m,external={}){
if(/\b(release|box office|views|album|movie|stream|award)\b/.test(text)){evidence+=0.06;drivers.push("culture-attention context");}
if(m.days_to_resolution!=null&&m.days_to_resolution<=14){evidence+=0.07;drivers.push("near-term resolution");}
if(m.volume_24hr>1500){evidence+=0.05;drivers.push("fresh market activity");}
if(external.source_count){evidence+=Math.min(0.16,0.04*external.source_count);drivers.push(`${external.source_count} recent news/context hit${external.source_count===1?"":"s"}`);}
if(external.source_count){evidence+=Math.min(0.20,0.05*external.source_count);drivers.push(`${external.source_count} recent context hit${external.source_count===1?"":"s"} (coverage only; direction comes from market confirmation)`);}
if(external.latest_title)drivers.push(`latest context: ${external.latest_title.slice(0,72)}${external.latest_title.length>72?"...":""}`);
return {score:clamp(evidence,0,1),drivers,source_count:external.source_count||0,latest_title:external.latest_title||""};
}
@@ -1084,15 +1085,30 @@ async function fetchRealWorldSignals(markets){
return d.signals||{};
}catch(e){return {};}
}
function fairValue(m){
const p=m.yes_price,mom=momentumSignal(m),liq=liquiditySignal(m),trend=directionalPriceSignal(m);
if(p>0.92)return Math.min(0.995,p+0.025*liq*(1-p)*4);
if(p<0.08)return Math.max(0.005,p-0.12*p);
const direction=trend.side==="YES"?1:(trend.side==="NO"?-1:0);
const directional=trend.aligned?direction*(0.018+trend.strength*0.050)*(0.72+0.28*liq):0;
const activityConfirmation=trend.aligned?direction*clamp((mom-0.45)*0.018,-0.004,0.010):0;
const liquidAnchor=(0.5-p)*0.008*liq;
return clamp(p+directional+activityConfirmation+liquidAnchor,0.03,0.97);
function confirmedMarketSignal(m,evidence={score:0,source_count:0}){
const hour=Number(m.price_change_1h||0),day=Number(m.price_change_1d||0),week=Number(m.price_change_1w||0);
const daySign=Math.sign(day),weekSign=Math.sign(week),hourSign=Math.sign(hour);
const trendAligned=daySign&&daySign===weekSign&&Math.abs(day)>=0.006&&Math.abs(week)>=0.012
&&Math.abs(day)<=0.08&&Math.abs(week)<=0.18&&(!hourSign||hourSign===daySign||Math.abs(hour)<0.008);
const reversalConfirmed=daySign&&Math.abs(day)>=0.04&&Math.abs(day)<=0.18&&hourSign===-daySign&&Math.abs(hour)>=0.004
&&(!weekSign||weekSign!==daySign||Math.abs(week)<Math.abs(day)*1.6);
if(!trendAligned&&!reversalConfirmed)return {side:null,type:"none",strength:0,gross_move:0,confidence:0,confirmed:false};
const type=reversalConfirmed?"reversal":"trend";
const sign=reversalConfirmed?-daySign:daySign;
const liq=liquiditySignal(m),activity=momentumSignal(m);
const moveStrength=type==="trend"
?clamp(0.30+Math.abs(day)*4+Math.abs(week)*1.5+(daySign===weekSign?0.16:0),0,1)
:clamp(0.30+Math.abs(day)*3.2+Math.abs(hour)*8,0,1);
const confidence=clamp(moveStrength*0.48+liq*0.22+activity*0.12+Number(evidence.score||0)*0.18,0,1);
const side=sign>0?"YES":"NO",availableMove=side==="YES"?1-Number(m.yes_price||0):Number(m.yes_price||0);
const gross=Math.min(type==="trend"?0.018+moveStrength*0.052:0.016+moveStrength*0.045,Math.max(0,availableMove)*0.80);
return {side,type,strength:moveStrength,gross_move:gross,confidence,confirmed:confidence>=0.56};
}
function fairValue(m,external={}){
const evidence=textEvidenceSignal(m,external),signal=confirmedMarketSignal(m,evidence);
if(!signal.side)return m.yes_price;
const direction=signal.side==="YES"?1:-1;
return clamp(m.yes_price+direction*signal.gross_move,0.03,0.97);
}
function quickMarketGap(m){
if(!m||!Number.isFinite(m.yes_price))return 0;
@@ -1125,100 +1141,69 @@ function fastSettlementRisk(m){
return false;
}
function directionalPriceSignal(m){
const hour=Number(m.price_change_1h||0),day=Number(m.price_change_1d||0),week=Number(m.price_change_1w||0);
const anchor=Math.abs(day)>=0.006?Math.sign(day):Math.sign(week);
if(!anchor)return {side:null,strength:0,aligned:false};
const contrary=Math.sign(day)&&Math.sign(week)&&Math.sign(day)!==Math.sign(week)&&Math.abs(day)>=0.012&&Math.abs(week)>=0.02;
const aligned=!contrary&&(Math.abs(day)>=0.006||Math.abs(week)>=0.012)&&(Math.abs(hour)<0.02||!Math.sign(hour)||Math.sign(hour)===anchor);
const strength=aligned?clamp(0.28+Math.abs(day)*3.2+Math.abs(week)*1.5+(Math.sign(day)===Math.sign(week)?0.18:0),0,1):0;
return {side:anchor>0?"YES":"NO",strength,aligned};
const signal=confirmedMarketSignal(m,textEvidenceSignal(m));
return {side:signal.side,strength:signal.strength,aligned:signal.type==="trend"&&signal.confirmed,type:signal.type};
}
function effectiveEntryEdge(s){
const net=Math.abs(Number(s&&s.net_edge));
return Number.isFinite(net)?net:0;
}
function reversalCandidate(s){
const day=Number(s&&s.price_change_1d||0),magnitude=Math.abs(day);
if(!s||magnitude<0.025||magnitude>0.16||s.jump_risk)return null;
const side=day>0?"NO":"YES",entry=side==="YES"?Number(s.yes_price):Number(s.no_price);
const policy=categoryPolicy(s.category),gross=Math.min(0.075,magnitude*0.48);
const net=Math.max(0,gross-Number(s.friction||0)-policy.uncertainty);
const evidence=Number(s.evidence_score||0),days=Number(s.days_to_resolution);
const ready=net>=MIN_AGGRESSIVE_EDGE&&entry>=0.08&&entry<=0.92&&evidence>=0.46
&&Number(s.spread)>0&&Number(s.spread)<=0.035&&Number(s.volume)>=MIN_VOLUME&&Number(s.volume_24hr)>=500
&&Number(s.liquidity)>=MIN_LIQUIDITY&&Number.isFinite(days)&&days>=MIN_ENTRY_DAYS;
const quality=ready?(net>=MIN_SELECTIVE_ENTRY_EDGE?"selective":"allocation"):"watch";
return Object.assign({},s,{side,entry_price:+entry.toFixed(4),net_edge:+((side==="YES"?1:-1)*net).toFixed(4),quality,trade_ready:ready,
conviction:+Math.max(Number(s.conviction||0),Math.min(82,50+magnitude*220+evidence*8)).toFixed(1),
rationale:ready?`Mean-reversion setup: ${side} opposes a ${(magnitude*100).toFixed(1)}% one-day move and retains a ${(net*100).toFixed(1)}c post-cost snapback edge with usable evidence and liquidity.`:`Watch only: the one-day move has not left enough post-cost mean-reversion edge.`});
return s&&s.signal_type==="reversal"&&s.trade_ready?s:null;
}
function analyzeMarket(m,realWorldSignals={}){
if(m.volume<MIN_SCOUT_VOLUME||m.liquidity<MIN_SCOUT_LIQUIDITY)return null;
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p,edge=Math.abs(edgeYes),policy=categoryPolicy(m.category);
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution),evidence=textEvidenceSignal(m,realWorldSignals[m.id]||{});
const friction=frictionPenalty(m),chase=chasePenalty(m,edgeYes),rawNet=Math.max(0,edge-friction-chase-policy.uncertainty);
const netEdge=+(Math.sign(edgeYes)*rawNet).toFixed(4),absNet=Math.abs(netEdge);
const mis=Math.min(1,absNet/EDGE_SCALE);
const conviction=+((W.LIQ*liq+W.MOM*mom+W.MIS*mis+W.TIME*timing)*82+evidence.score*18).toFixed(1);
const minEdge=Math.max(MIN_ENTRY_EDGE,policy.minEdge);
const strictTradeReady=absNet>=minEdge&&m.volume>=Math.max(MIN_VOLUME,policy.minVol)&&m.liquidity>=Math.max(MIN_LIQUIDITY,policy.minLiq)&&evidence.score>=policy.minEvidence;
const external=realWorldSignals[m.id]||{},p=m.yes_price,policy=categoryPolicy(m.category);
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution),evidence=textEvidenceSignal(m,external);
const signal=confirmedMarketSignal(m,evidence),direction=signal.side==="YES"?1:(signal.side==="NO"?-1:0);
const grossMove=signal.gross_move||0,friction=frictionPenalty(m),chase=chasePenalty(m,grossMove);
const rawNet=Math.max(0,grossMove-friction-chase-policy.uncertainty),edgeYes=direction*grossMove;
const netEdge=+(direction*rawNet).toFixed(4),absNet=Math.abs(netEdge),fair=clamp(p+edgeYes,0.03,0.97);
const signalScore=Math.min(1,absNet/EDGE_SCALE);
const conviction=+((W.LIQ*liq+W.MOM*mom+W.SIGNAL*signal.confidence+W.TIME*timing)*100).toFixed(1);
const jumpRisk=fastSettlementRisk(m);
const selectiveTradeReady=absNet>=MIN_SELECTIVE_ENTRY_EDGE&&conviction>=62&&m.volume>=MIN_VOLUME&&m.liquidity>=MIN_LIQUIDITY&&evidence.score>=0.50&&m.yes_price>=0.08&&m.yes_price<=0.92&&!jumpRisk;
const trend=directionalPriceSignal(m);
const trendTradeReady=!strictTradeReady&&!selectiveTradeReady&&absNet>=MIN_TREND_EDGE
&&trend.aligned&&trend.strength>=0.65&&!jumpRisk&&evidence.score>=0.52
&&m.spread>0&&m.spread<=0.025&&m.volume_24hr>=1500&&m.liquidity>=2500
&&Math.abs(m.price_change_1d||0)<=0.10
&&m.days_to_resolution!=null&&m.days_to_resolution>=MIN_ENTRY_DAYS&&p>=0.12&&p<=0.88;
const liquidityTradeReady=!strictTradeReady&&!selectiveTradeReady&&!trendTradeReady&&!jumpRisk
&&absNet>=MIN_LIQUIDITY_EDGE&&conviction>=68&&evidence.score>=0.54&&m.spread>0&&m.spread<=0.02
&&m.volume_24hr>=1000&&m.liquidity>=2000&&m.days_to_resolution!=null&&m.days_to_resolution>=MIN_ENTRY_DAYS
&&p>=0.12&&p<=0.88&&Math.abs(m.price_change_1d||0)<=0.08;
const coreProbeTradeReady=!strictTradeReady&&!selectiveTradeReady&&!trendTradeReady&&!liquidityTradeReady&&!jumpRisk
&&absNet>=MIN_CORE_PROBE_EDGE&&conviction>=62&&evidence.score>=0.65
&&m.spread>0&&m.spread<=0.025&&m.volume_24hr>=25000&&m.liquidity>=10000
&&m.days_to_resolution!=null&&m.days_to_resolution>=14&&p>=0.12&&p<=0.88
&&Math.abs(m.price_change_1d||0)<=0.08;
const speculativeTradeReady=!strictTradeReady&&!selectiveTradeReady&&!trendTradeReady&&!liquidityTradeReady&&!coreProbeTradeReady&&!jumpRisk
&&absNet>=MIN_AGGRESSIVE_EDGE&&conviction>=55&&evidence.score>=0.48
&&m.spread>0&&m.spread<=0.03&&m.volume_24hr>=750&&m.liquidity>=1500
&&m.days_to_resolution!=null&&m.days_to_resolution>=10&&p>=0.10&&p<=0.90
&&Math.abs(m.price_change_1d||0)<=0.12&&(trend.strength>=0.50||evidence.source_count>=1);
const allocationTradeReady=!strictTradeReady&&!selectiveTradeReady&&!trendTradeReady&&!liquidityTradeReady&&!coreProbeTradeReady&&!speculativeTradeReady&&!jumpRisk
&&absNet>=MIN_ALLOCATION_EDGE&&conviction>=50&&evidence.score>=0.46
&&m.spread>0&&m.spread<=0.035&&m.volume>=MIN_VOLUME&&m.volume_24hr>=500&&m.liquidity>=MIN_LIQUIDITY
&&m.days_to_resolution!=null&&m.days_to_resolution>=MIN_ENTRY_DAYS&&p>=0.08&&p<=0.92
&&Math.abs(m.price_change_1d||0)<=0.12;
const explorationTradeReady=false;
const tradeReady=(strictTradeReady||selectiveTradeReady||trendTradeReady||liquidityTradeReady||coreProbeTradeReady||speculativeTradeReady||allocationTradeReady||explorationTradeReady)&&!jumpRisk;
const tradeConviction=trendTradeReady?Math.max(conviction,55+trend.strength*35):(liquidityTradeReady?Math.max(conviction,70):(coreProbeTradeReady?Math.max(conviction,62):(speculativeTradeReady?Math.max(conviction,55):(allocationTradeReady?Math.max(conviction,50):(explorationTradeReady?Math.max(conviction,60):conviction)))));
let side=trendTradeReady?trend.side:(edgeYes>=0?"YES":"NO");
let entry=side==="YES"?p:m.no_price,rationale;
if(trendTradeReady){rationale=`Confirmed trend trade: ${side} has ${Math.round(trend.strength*100)} trend strength, a ${(absNet*100).toFixed(1)}c post-cost edge, ${pct(m.spread)} spread, strong live activity, and enough time before resolution.`;}
else if(liquidityTradeReady){rationale=`Liquidity-confirmed trade: ${side} retains a ${(absNet*100).toFixed(1)}c post-cost edge in a high-conviction, tight-spread market with enough time before resolution.`;}
else if(coreProbeTradeReady){rationale=`Evidence-heavy core probe: ${side} retains a ${(absNet*100).toFixed(1)}c post-cost edge with ${Math.round(evidence.score*100)} evidence, deep liquidity, strong live volume, and at least two weeks before resolution. Core agents may enter only at probe size.`;}
else if(speculativeTradeReady){rationale=`Aggressive setup: ${side} retains a measured ${(absNet*100).toFixed(1)}c post-cost edge with ${Math.round(evidence.score*100)} evidence, adequate liquidity, and a live catalyst or directional signal. Only aggressive agents may take this higher-risk entry.`;}
else if(allocationTradeReady){rationale=`Allocation candidate: ${side} retains a positive ${(absNet*100).toFixed(1)}c post-cost edge with ${Math.round(evidence.score*100)} evidence, usable liquidity, and at least one week before resolution. Agents below their exposure floor may use it at reduced size.`;}
else if(selectiveTradeReady&&!strictTradeReady){rationale=`Selective BUY from top-500 active scan: raw gap ${(edge*100).toFixed(1)}c, conviction ${Math.round(conviction)}, high activity/liquidity, and evidence ${Math.round(evidence.score*100)}. Net edge is conservative, so sizing stays disciplined.`;}
else if(tradeReady&&edgeYes>0){rationale=`Trade-ready after costs: fair value ${pct(fair)} vs market ${pct(p)} leaves ${(absNet*100).toFixed(1)}c net edge for YES after liquidity, chase, and ${m.category} evidence checks.`;}
else if(tradeReady&&edgeYes<0){rationale=`Trade-ready after costs: fair value ${pct(fair)} vs market ${pct(p)} makes YES look overpriced; NO has ${(absNet*100).toFixed(1)}c net edge after penalties.`;}
const commonReady=signal.confirmed&&!jumpRisk&&direction!==0&&m.spread>0&&m.spread<=0.03
&&m.volume>=MIN_VOLUME&&m.liquidity>=MIN_LIQUIDITY&&m.volume_24hr>=750
&&m.days_to_resolution!=null&&m.days_to_resolution>=MIN_ENTRY_DAYS&&p>=0.08&&p<=0.92;
const strictTradeReady=commonReady&&absNet>=Math.max(MIN_ENTRY_EDGE,policy.minEdge)&&conviction>=70
&&m.volume>=Math.max(MIN_VOLUME,policy.minVol)&&m.liquidity>=Math.max(MIN_LIQUIDITY,policy.minLiq)&&evidence.score>=policy.minEvidence;
const catalystTradeReady=!strictTradeReady&&commonReady&&evidence.source_count>=1&&evidence.score>=0.55
&&absNet>=MIN_SELECTIVE_ENTRY_EDGE&&signal.confidence>=0.62;
const reversalTradeReady=!strictTradeReady&&!catalystTradeReady&&commonReady&&signal.type==="reversal"
&&absNet>=MIN_LIQUIDITY_EDGE&&signal.confidence>=0.64&&m.spread<=0.025;
const liquidityTradeReady=!strictTradeReady&&!catalystTradeReady&&!reversalTradeReady&&commonReady&&signal.type==="trend"
&&absNet>=MIN_LIQUIDITY_EDGE&&signal.confidence>=0.68&&evidence.score>=0.50&&m.spread<=0.018&&m.liquidity>=5000;
const trendTradeReady=!strictTradeReady&&!catalystTradeReady&&!reversalTradeReady&&!liquidityTradeReady&&commonReady&&signal.type==="trend"
&&absNet>=Math.max(MIN_TREND_EDGE,policy.minEdge)&&signal.confidence>=0.60&&evidence.score>=policy.minEvidence;
const tradeReady=strictTradeReady||catalystTradeReady||reversalTradeReady||liquidityTradeReady||trendTradeReady;
const quality=strictTradeReady?"confirmed":(catalystTradeReady?"catalyst":(reversalTradeReady?"reversal":(liquidityTradeReady?"liquid-trend":(trendTradeReady?"trend":"watch"))));
const side=signal.side||((Number(m.price_change_1d||0)>=0)?"YES":"NO");
const entry=side==="YES"?p:m.no_price;
let rationale;
if(strictTradeReady){rationale=`Confirmed setup: ${side} has ${Math.round(signal.strength*100)} signal strength across independent time windows and a ${(absNet*100).toFixed(1)}c signal margin after friction and category uncertainty.`;}
else if(catalystTradeReady){rationale=`Catalyst-confirmed setup: ${side} price action agrees across the required windows, recent outside coverage exists, and ${(absNet*100).toFixed(1)}c of estimated movement remains after friction. News coverage confirms activity, not direction.`;}
else if(reversalTradeReady){rationale=`Confirmed reversal: the one-day move is reversing in the hourly window with adequate liquidity and a ${(absNet*100).toFixed(1)}c post-friction signal margin.`;}
else if(liquidityTradeReady){rationale=`Liquid trend: ${side} is aligned across one-day and one-week windows in a tight, deep market with a ${(absNet*100).toFixed(1)}c post-friction signal margin.`;}
else if(trendTradeReady){rationale=`Confirmed trend: ${side} is aligned across one-day and one-week windows, avoids chase conditions, and retains a ${(absNet*100).toFixed(1)}c post-friction signal margin.`;}
else if(jumpRisk){rationale="Watch only: this market can jump directly to settlement before an hourly stop can protect the position, so agents will not buy it.";}
else{rationale=`Watch only: raw gap ${(edge*100).toFixed(1)}c becomes ${(absNet*100).toFixed(1)}c after friction/chase/category penalties, so agents need stronger evidence before buying.`;}
else if(!signal.side){rationale="Watch only: no direction is confirmed across independent time windows. Recent price movement alone is not treated as fair value.";}
else{rationale=`Watch only: the ${signal.type} setup leaves ${(absNet*100).toFixed(1)}c after friction and uncertainty, but it has not cleared every confirmation, liquidity, evidence, and timing gate.`;}
const drivers=[policy.label];
if(liq>0.6)drivers.push("deep/liquid market");
if(mom>0.6)drivers.push("strong fresh volume (24h surge)");
if(timing>0.8)drivers.push("resolves in a good window");
if(mis>0.4)drivers.push("net price/value gap after costs");
if(signal.type==="trend")drivers.push("one-day and one-week direction agree");
if(signal.type==="reversal")drivers.push("hourly reversal confirms the snapback");
if(signalScore>0.4)drivers.push("positive signal margin after modeled friction");
drivers.push(...evidence.drivers.slice(0,3));
if(!drivers.length)drivers.push("thin signal");
return {market_id:m.id,question:m.question,event:m.event,url:m.url,category:m.category,tags:m.tags,
clob_yes:(m.clob_token_ids||[])[0]||null,clob_no:(m.clob_token_ids||[])[1]||null,
yes_price:p,no_price:m.no_price,fair_value:+fair.toFixed(4),edge:+edgeYes.toFixed(4),
net_edge:netEdge,friction:+friction.toFixed(4),chase_penalty:+chase.toFixed(4),evidence_score:+evidence.score.toFixed(2),evidence_source_count:evidence.source_count||0,
quality:strictTradeReady?"EV+":(selectiveTradeReady?"selective":(trendTradeReady?"trend":(liquidityTradeReady?"liquidity":(coreProbeTradeReady?"core-probe":(speculativeTradeReady?"speculative":(allocationTradeReady?"allocation":(explorationTradeReady?"explore":"watch"))))))),
side,entry_price:+entry.toFixed(4),conviction:+tradeConviction.toFixed(1),volume:m.volume,volume_24hr:m.volume_24hr,liquidity:m.liquidity,
quality,side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,liquidity:m.liquidity,
spread:m.spread,price_change_1h:m.price_change_1h,price_change_1d:m.price_change_1d,price_change_1w:m.price_change_1w,
momentum_strength:+trend.strength.toFixed(2),trade_ready:tradeReady,jump_risk:jumpRisk,
momentum_strength:+signal.strength.toFixed(2),signal_strength:+signal.strength.toFixed(2),signal_confidence:+signal.confidence.toFixed(2),signal_type:signal.type,trade_ready:tradeReady,jump_risk:jumpRisk,
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,rationale};
}
const pct=(x)=>Math.round(x*100)+"%";
@@ -1229,7 +1214,7 @@ function suggestionBand(s){
}
function marketWatchSuggestion(m,realWorldSignals={}){
if(!m||!m.question)return null;
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p;
const p=m.yes_price,fair=fairValue(m,realWorldSignals[m.id]||{}),edgeYes=fair-p;
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution),policy=categoryPolicy(m.category),evidence=textEvidenceSignal(m,realWorldSignals[m.id]||{});
const friction=frictionPenalty(m),chase=chasePenalty(m,edgeYes),rawNet=Math.max(0,Math.abs(edgeYes)-friction-chase-policy.uncertainty),netEdge=+(Math.sign(edgeYes)*rawNet).toFixed(4);
const side=edgeYes>=0?"YES":"NO";
@@ -1247,7 +1232,7 @@ function marketWatchSuggestion(m,realWorldSignals={}){
side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,liquidity:m.liquidity,
trade_ready:false,watch_only:true,
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,
rationale:`Market watchlist: included for coverage from the full Polymarket scan. Net edge is ${(Math.abs(netEdge)*100).toFixed(1)}c and evidence score is ${Math.round(evidence.score*100)}, so agents will not buy it unless conditions improve.`};
rationale:`Market watchlist: included for coverage from the active-market scan. Signal margin is ${(Math.abs(netEdge)*100).toFixed(1)}c and evidence score is ${Math.round(evidence.score*100)}, so agents will not buy it unless confirmation improves.`};
}
function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTION_PER_CATEGORY,realWorldSignals={}){
const actionable=markets.map(m=>analyzeMarket(m,realWorldSignals)).filter(Boolean).filter(a=>a.side!=="HOLD").sort((a,b)=>b.conviction-a.conviction);
@@ -1270,10 +1255,11 @@ 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 defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,last_cycle_hour:null,agents,seeded:false};}
function defaultState(){const agents={};AGENTS.forEach(a=>agents[a.id]=defaultPortfolio());return {date:null,last_run:null,last_cycle_hour:null,engine_version:SUGGESTION_ENGINE_VERSION,engine_started_at:nowIso(),agents,seeded:false};}
function loadState(){
let st; try{st=JSON.parse(localStorage.getItem(AGENTS_KEY));}catch(e){st=null;}
if(!st||!st.agents)st=defaultState();
let migrated=false;
const activeIds=new Set(AGENTS.map(a=>a.id));
Object.keys(st.agents).forEach(id=>{if(!activeIds.has(id))delete st.agents[id];});
delete st.whales;
@@ -1283,7 +1269,19 @@ function loadState(){
if(!st.agents[a.id].stopped)st.agents[a.id].stopped={};
if(!("lastDecision" in st.agents[a.id]))st.agents[a.id].lastDecision=null;
});
if(st.engine_version!==SUGGESTION_ENGINE_VERSION){
const started=nowIso();
st.previous_engine_version=st.engine_version||null;
st.engine_version=SUGGESTION_ENGINE_VERSION;
st.engine_started_at=started;
AGENTS.forEach(a=>{
const p=st.agents[a.id],eq=Number(p.cash||0)+(p.positions||[]).reduce((sum,pos)=>sum+Number(pos.value||Number(pos.shares||0)*Number(pos.current_price||0)),0);
p.engine_baseline={version:SUGGESTION_ENGINE_VERSION,started_at:started,equity:+eq.toFixed(2)};
});
migrated=true;
}
if(!st.last_cycle_hour&&st.last_run)st.last_cycle_hour=cycleHourFromIso(st.last_run);
if(migrated)localStorage.setItem(AGENTS_KEY,JSON.stringify(compactAgentStateForSync(st)));
return st;
}
function compactPortfolioForSync(p,limits=SYNC_LIMITS){
@@ -1315,6 +1313,7 @@ function compactSuggestionForSync(s){
net_edge:s.net_edge,friction:s.friction,chase_penalty:s.chase_penalty,evidence_score:s.evidence_score,evidence_source_count:s.evidence_source_count,quality:s.quality,
conviction:s.conviction,volume:s.volume,volume_24hr:s.volume_24hr,liquidity:s.liquidity,
spread:s.spread,price_change_1h:s.price_change_1h,price_change_1d:s.price_change_1d,price_change_1w:s.price_change_1w,momentum_strength:s.momentum_strength,
signal_strength:s.signal_strength,signal_confidence:s.signal_confidence,signal_type:s.signal_type,
trade_ready:s.trade_ready,watch_only:s.watch_only,jump_risk:s.jump_risk,
days_to_resolution:s.days_to_resolution,drivers:s.drivers,rationale:s.rationale,
};
@@ -1522,44 +1521,21 @@ function adaptiveDecision(cfg,p,rank,total,leaderEq){
const trend=recentReturnDelta(p);
const emo=emotionalState(ret,trail,trend,rank);
const aggressive=!!cfg.aggressive;
let minConv=cfg.minConv??0,maxNew=cfg.maxNew??6,maxFrac=cfg.maxFrac??0.06,reserve=aggressive?0.02:0.08;
let minConv=Math.max(cfg.minConv??0,aggressive?60:64),maxNew=Math.min(cfg.maxNew??4,aggressive?4:3),maxFrac=Math.min(cfg.maxFrac??0.05,aggressive?0.07:0.045),reserve=aggressive?0.10:0.18;
let targetExposure=cfg.targetExposure??(aggressive?0.80:0.62);
let mode=aggressive?"Aggressive Hunt":"Quality First";
let reason=aggressive?"keeping cash near 2% and pressing qualified edge with a larger risk budget.":"deploying capital only when a strategy-specific setup clears its edge and evidence floors.";
if(rank===1){
mode="Compound the Lead";minConv+=aggressive?0:1;maxFrac*=aggressive?1.02:0.97;reserve=aggressive?0.03:0.10;
targetExposure=Math.max(aggressive?0.72:0.52,targetExposure-0.04);
reason=aggressive?"leading the race and continuing to press its strongest qualifying setups while its wider risk cap remains active.":"leading the race, so it compounds only through setups that preserve the quality of its existing book.";
}else if(trail>6){
mode=aggressive?"Aggressive Catch-Up":"Disciplined Catch-Up";minConv+=aggressive?0:2;maxNew+=aggressive?1:0;maxFrac*=aggressive?1.05:0.95;reserve=aggressive?0.02:0.10;
if(aggressive)targetExposure=Math.min(0.90,targetExposure+0.02);
reason=aggressive?"behind the leader, so it can add one more qualified entry and deploy at its full risk budget without weakening the edge floor.":"behind the leader, but it will recover through better expected value rather than increasing low-quality turnover.";
}else if(trend>1.5){
mode="Momentum Press";minConv-=1;maxNew+=1;maxFrac*=1.08;reserve=0.02;
reason="recent momentum is positive, so it presses the advantage while risk caps stay active.";
}
let mode=aggressive?"Confirmed Opportunity":"Quality First";
let reason=`exposure is capped at ${Math.round(targetExposure*100)}%; cash is acceptable when no independently confirmed setup clears this strategy's rules.`;
if(ret<-5){
mode=aggressive?"Risk-On Recovery":"Selective Recovery";minConv+=aggressive?1:4;maxNew=Math.min(maxNew,aggressive?5:3);maxFrac*=aggressive?0.90:0.80;reserve=aggressive?0.05:0.15;
targetExposure=Math.max(aggressive?0.60:0.38,targetExposure-(aggressive?0.05:0.08));
reason=aggressive?"in drawdown, but it remains invested through its best qualifying signals with modestly smaller sizing.":"in drawdown, so it raises its evidence threshold and reduces new-trade size until the signal quality recovers.";
mode="Drawdown Quality Control";minConv+=3;maxNew=Math.min(maxNew,2);maxFrac*=0.78;reserve=Math.max(reserve,aggressive?0.16:0.24);
targetExposure=Math.max(aggressive?0.52:0.38,targetExposure-0.10);
reason="in drawdown, so it requires stronger confirmation and smaller entries instead of attempting a forced recovery.";
}
if(ret<-18){
mode=aggressive?"Aggressive Drawdown Control":"Drawdown Repair";minConv+=aggressive?3:6;maxNew=Math.min(maxNew,aggressive?4:4);maxFrac=Math.min(maxFrac,aggressive?0.06:0.04);reserve=aggressive?0.12:0.18;
targetExposure=cfg.drawdownExposure??(aggressive?0.65:0.42);
reason=aggressive?"down more than 18%, so it rebuilds toward a reduced exposure target through four positive-edge recovery slots while capping position size.":"down more than 18%, so it rebuilds a diversified recovery book toward a reduced exposure target instead of becoming trapped in cash.";
mode="Drawdown Repair";minConv+=4;maxNew=1;maxFrac=Math.min(maxFrac,aggressive?0.045:0.03);reserve=aggressive?0.25:0.32;
targetExposure=Math.min(targetExposure,cfg.drawdownExposure??(aggressive?0.55:0.40));
reason="down more than 18%, so it can add only one small, strongly confirmed position. It will not revenge trade.";
}
if(emo.mood==="impatient"||emo.mood==="frustrated"){
if(aggressive)maxNew+=1;else minConv+=1;
reason+=aggressive?` Emotion layer: ${emo.label.toLowerCase()} adds one qualified attack slot but cannot weaken the positive-edge rule.`:` Emotion layer: ${emo.label.toLowerCase()} is reported, but it cannot loosen entry standards or increase size.`;
}else if(emo.mood==="confident"){
maxNew+=1;maxFrac*=1.05;
reason+=` Emotion layer: confidence lets it press winners a little harder.`;
}else if(emo.mood==="alarmed"){
maxNew=Math.min(maxNew,4);maxFrac=Math.min(maxFrac,aggressive?0.06:0.04);reserve=Math.max(reserve,aggressive?0.12:0.18);
reason+=` Emotion layer: alarm cannot trigger revenge trading; only qualifying setups remain eligible.`;
}
const operatingReserve=aggressive?0.025:0.06;
if((p.cash/Math.max(eq,1))<operatingReserve){maxNew=Math.max(1,Math.min(maxNew,aggressive?3:2));reason+=` Cash is below the ${Math.round(operatingReserve*1000)/10}% operating reserve, so new entries are temporarily rationed.`;}
reason+=` Emotion is reported as ${emo.label.toLowerCase()} but cannot increase size, reduce confirmation, or add attack slots.`;
const todaySnaps=(p.snapshots||[]).filter(s=>s.date===todayStr());
const cycleStart=todaySnaps.length?Number(todaySnaps[0].equity||eq):eq;
const cycleDrawdown=cycleStart>0?(eq/cycleStart-1)*100:0;
@@ -1570,9 +1546,7 @@ function adaptiveDecision(cfg,p,rank,total,leaderEq){
}
const maxPositionPct=aggressive?0.10:MAX_NEW_POSITION_PCT;
targetExposure=clamp(targetExposure,0,1-reserve);
const minExposure=Math.max(0,Math.min(targetExposure,targetExposure-(aggressive?0.14:0.16)));
const belowFloor=currentExposure+0.001<minExposure;
if(belowFloor&&maxNew>0)reason+=` Current exposure is ${Math.round(currentExposure*100)}%, below its ${Math.round(minExposure*100)}% floor, so this cycle must prioritize small qualifying allocation trades.`;
const minExposure=0,belowFloor=false;
return {mode,reason,emotion:emo.mood,urgency:+emo.urgency.toFixed(2),minConv:Math.max(0,Math.round(minConv)),maxNew:Math.max(0,Math.round(maxNew)),maxFrac:+Math.min(maxPositionPct,Math.max(0.01,maxFrac)).toFixed(3),reserve,
currentExposure:+currentExposure.toFixed(3),targetExposure:+targetExposure.toFixed(3),minExposure:+minExposure.toFixed(3),belowFloor};
}
@@ -1611,16 +1585,22 @@ function triggeredStopLosses(pos){
}
function doneGainStops(pos){
const done=pos.gain_stops&&typeof pos.gain_stops==="object"?Object.assign({},pos.gain_stops):{};
if(pos.took_profit_at&&!done["75"]&&!done["150"]){
done["75"]=pos.took_profit_at;
done["150"]=pos.took_profit_at;
}
if(done["75"]&&!done.lock1)done.lock1=done["75"];
if(done["150"]&&!done.lock2)done.lock2=done["150"];
if(done["300"]&&!done.lock3)done.lock3=done["300"];
if(pos.took_profit_at&&!done.lock1)done.lock1=pos.took_profit_at;
return done;
}
function gainStopReturn(pos,tier){
const entry=Number(pos.entry_price);
if(!Number.isFinite(entry)||entry<=0||entry>=1)return null;
const maximumReturn=(1-entry)/entry;
return Math.max(0.01,Math.min(tier.gain,maximumReturn*tier.maxGainFrac));
}
function gainStopTarget(pos,tier){
const entry=Number(pos.entry_price);
if(Number.isFinite(entry)&&entry>0)return +(entry*tier.multiple).toFixed(4);
if(pos.shares>0&&pos.cost>0)return +((pos.cost/pos.shares)*tier.multiple).toFixed(4);
const targetReturn=gainStopReturn(pos,tier);
if(Number.isFinite(entry)&&entry>0&&targetReturn!=null)return +Math.min(0.999,entry*(1+targetReturn)).toFixed(4);
return null;
}
function nextGainStop(pos){
@@ -1644,9 +1624,9 @@ function gainStopLabel(pos){
const target=gainStopTarget(pos,tier);
const prefix=doneCount?`Gain stop: ${doneCount*25}% sold; next `:"Gain stop: ";
if(target==null)return `${prefix}waiting for ${tier.label}`;
if(target>1)return `${prefix}${tier.label} unreachable from ${pct(pos.entry_price)} entry`;
const gap=Math.max(0,target-(pos.current_price||0));
return `${prefix}sell 25% at ${pct(target)} (${tier.label})${gap>0?` — ${(gap*100).toFixed(1)}c away`:" — armed"}`;
const targetReturn=gainStopReturn(pos,tier);
return `${prefix}sell 25% at ${pct(target)} (${tier.label}, +${(targetReturn*100).toFixed(1)}%)${gap>0?` — ${(gap*100).toFixed(1)}c away`:" — armed"}`;
}
function stopLossLabel(pos){
const done=doneStopLosses(pos),doneCount=STOP_LOSS_TIERS.filter(t=>done[t.id]).length;
@@ -1660,7 +1640,7 @@ function stopOutPosition(p,pos){
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.exit_price=pos.current_price;pos.closed_at=cycleIso();pos.close_reason="Stop loss -18%";pos.close_action="STOP";
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
pos.realized_pnl=+(proceeds-pos.cost+Number(pos.partial_realized_pnl||0)).toFixed(2);
p.closed.push(pos);rememberStop(p,pos);
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
detail:`Stop-loss sold '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
@@ -1687,6 +1667,7 @@ function scaleStopLossPosition(p,pos,tier){
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
pos.stop_losses=doneStopLosses(pos);
pos.stop_losses[tier.id]=cycleIso();
pos.partial_realized_pnl=+(Number(pos.partial_realized_pnl||0)+realizedPnl).toFixed(2);
p.history.push({date:logDay(),action:"STOP",question:pos.question,side:pos.side,
detail:`Stop-loss ${tier.label} sold 33% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
}
@@ -1694,7 +1675,7 @@ function closePosition(p,pos,reason,action="CLOSE"){
const proceeds=+(pos.shares*pos.current_price).toFixed(2);
p.cash=+(p.cash+proceeds).toFixed(2);
pos.exit_price=pos.current_price;pos.closed_at=cycleIso();pos.close_reason=reason;pos.close_action=action;
pos.realized_pnl=+(proceeds-pos.cost).toFixed(2);
pos.realized_pnl=+(proceeds-pos.cost+Number(pos.partial_realized_pnl||0)).toFixed(2);
p.closed.push(pos);
p.history.push({date:logDay(),action,question:pos.question,side:pos.side,
detail:`${reason} '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (P&L ${fmtUSD(pos.realized_pnl)})`});
@@ -1717,8 +1698,9 @@ function takeProfitPosition(p,pos,tier){
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
pos.gain_stops=doneGainStops(pos);
pos.gain_stops[tier.id]=cycleIso();
pos.partial_realized_pnl=+(Number(pos.partial_realized_pnl||0)+realizedPnl).toFixed(2);
p.history.push({date:logDay(),action:"GAIN",question:pos.question,side:pos.side,
detail:`Gain stop ${tier.label} sold 25% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
detail:`${tier.label} sold 25% of '${pos.question.slice(0,40)}' at ${pct(pos.current_price)} for ${fmtUSD(proceeds)} (realized ${fmtUSD(realizedPnl)})`});
}
function daysHeld(pos){
if(!pos.opened_at)return 0;
@@ -1726,19 +1708,37 @@ function daysHeld(pos){
if(!Number.isFinite(start)||!Number.isFinite(end))return 0;
return Math.max(0,(end-start)/86400000);
}
function hoursSince(iso){
const start=new Date(iso||"").getTime(),end=new Date(cycleIso()).getTime();
return Number.isFinite(start)&&Number.isFinite(end)?Math.max(0,(end-start)/3600000):0;
}
function trailingProfitReason(pos){
const entry=Number(pos.entry_price),current=Number(pos.current_price),peak=Number(pos.peak_price||current);
if(!entry||!current||!peak)return null;
const peakReturn=peak/entry-1,currentReturn=current/entry-1;
const firstTarget=gainStopReturn(pos,GAIN_STOP_TIERS[0]);
if(firstTarget==null||peakReturn<firstTarget)return null;
const retainedFloor=Math.max(0.02,peakReturn*0.55);
return currentReturn<=retainedFloor?`Trailing profit lock after a ${(peakReturn*100).toFixed(1)}% peak`:null;
}
function exitReason(pos,fresh,analysis,cfg){
if(fastSettlementRisk(fresh))return "Risk policy removed fast-settling event exposure";
if(fresh.days_to_resolution!=null&&fresh.days_to_resolution<=EXIT_RESOLUTION_DAYS)return `Close before resolution (${fresh.days_to_resolution.toFixed(1)}d left)`;
if(daysHeld(pos)>=EXIT_STALE_DAYS)return `Stale exit after ${Math.floor(daysHeld(pos))} days`;
const entryEdge=Math.abs(Number(pos.net_edge||0));
if(pos.quality==="explore")return "Quality audit retired zero-edge exploration exposure";
if(pos.quality==="trend"&&entryEdge<MIN_TREND_EDGE)return "Quality audit retired a trend trade without enough post-cost edge";
if(pos.quality==="liquidity"&&entryEdge<MIN_LIQUIDITY_EDGE)return "Quality audit retired a liquidity trade without enough post-cost edge";
const trail=trailingProfitReason(pos);if(trail)return trail;
const heldHours=daysHeld(pos)*24;
if(daysHeld(pos)>=EXIT_STALE_DAYS&&(pos.unrealized_pnl||0)<=0)return `Stale losing exit after ${Math.floor(daysHeld(pos))} days`;
if(!analysis)return null;
const net=Math.abs(analysis.net_edge!=null?analysis.net_edge:analysis.edge||0);
if(analysis.side&&analysis.side!==pos.side&&net>=MIN_ENTRY_EDGE)return `Model flipped to ${analysis.side}`;
const conflicting=analysis.trade_ready&&analysis.side&&analysis.side!==pos.side&&net>=MIN_SELECTIVE_ENTRY_EDGE;
if(conflicting){
if(!pos.signal_conflict_since)pos.signal_conflict_since=cycleIso();
if(heldHours>=MIN_POLICY_HOLD_HOURS&&hoursSince(pos.signal_conflict_since)>=EXIT_CONFIRM_HOURS)return `Confirmed signal reversal to ${analysis.side}`;
}else delete pos.signal_conflict_since;
const minExit=Math.max(30,(cfg&&cfg.minConv?cfg.minConv:45)-8);
if((pos.unrealized_pnl||0)<0&&analysis.conviction<minExit&&net<MIN_ENTRY_EDGE*0.25)return `Conviction faded to ${Math.round(analysis.conviction)}`;
const faded=(pos.unrealized_pnl||0)<0&&analysis.conviction<minExit&&!analysis.trade_ready;
if(faded){
if(!pos.signal_fade_since)pos.signal_fade_since=cycleIso();
if(heldHours>=48&&hoursSince(pos.signal_fade_since)>=12)return `Signal remained unconfirmed for 12 hours`;
}else delete pos.signal_fade_since;
return null;
}
function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
@@ -1753,6 +1753,7 @@ function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
const price=pos.side==="YES"?fresh.yes_price:fresh.no_price;
pos.current_price=+price.toFixed(4);pos.value=+(pos.shares*price).toFixed(2);
pos.unrealized_pnl=+(pos.value-pos.cost).toFixed(2);
pos.peak_price=+Math.max(Number(pos.peak_price||pos.entry_price||price),price).toFixed(4);
delete pos.price_status;
for(const stopTier of triggeredStopLosses(pos))scaleStopLossPosition(p,pos,stopTier);
if(pos._closedByStop){delete pos._closedByStop;continue;}
@@ -1840,9 +1841,7 @@ function reduceStrategyOverlap(st){
});
Object.values(groups).filter(g=>g.length>1).forEach(group=>{
group.sort((a,b)=>(b.score-a.score)||((b.pos.value||0)-(a.pos.value||0)));
const probeOnly=group.every(({pos})=>["trend","liquidity","core-probe","speculative","allocation","explore"].includes(pos.quality));
const coreProbe=group.some(({pos})=>pos.quality==="core-probe");
group.slice(coreProbe?4:(probeOnly?3:2)).forEach(({p,pos})=>closePosition(p,pos,"Overlap guard rotated this agent into a different market","EXIT"));
group.slice(2).forEach(({p,pos})=>closePosition(p,pos,"Strict overlap guard limited this market to two agents","EXIT"));
});
AGENTS.filter(a=>a.kind==="strategy").forEach(cfg=>{
const p=st.agents&&st.agents[cfg.id]; if(p)p.positions=(p.positions||[]).filter(pos=>!pos.closed_at);
@@ -1850,56 +1849,39 @@ function reduceStrategyOverlap(st){
}
function agentAcceptsSuggestion(cfg,s){
const quality=s.quality||"watch",edge=effectiveEntryEdge(s),evidence=Number(s.evidence_score||0);
if(quality==="explore"||quality==="watch")return false;
if(cfg.id==="value")return (["EV+","selective"].includes(quality)&&edge>=MIN_SELECTIVE_ENTRY_EDGE)
||(quality==="core-probe"&&edge>=0.017&&evidence>=0.68)
||(quality==="allocation"&&edge>=0.010&&evidence>=0.46);
if(cfg.id==="momentum")return ["EV+","selective"].includes(quality)
||(quality==="trend"&&edge>=0.015&&Number(s.momentum_strength||0)>=0.65&&evidence>=0.54)
||(quality==="core-probe"&&Number(s.momentum_strength||0)>=0.48&&evidence>=0.68)
||(quality==="allocation"&&edge>=0.008&&Number(s.momentum_strength||0)>=0.35&&evidence>=0.46);
if(quality==="watch")return false;
if(cfg.id==="value")return ["confirmed","liquid-trend"].includes(quality)&&edge>=MIN_SELECTIVE_ENTRY_EDGE
&&Number(s.entry_price||0)>=0.20&&Number(s.entry_price||0)<=0.80;
if(cfg.id==="momentum")return ["confirmed","trend","liquid-trend","catalyst"].includes(quality)
&&s.signal_type==="trend"&&Number(s.signal_strength||0)>=0.60&&evidence>=0.48;
if(cfg.id==="favorite")return Number(s.entry_price||0)>=0.62
&&((["EV+","selective","liquidity","core-probe"].includes(quality)&&edge>=MIN_CORE_PROBE_EDGE)
||(quality==="allocation"&&edge>=0.008&&evidence>=0.46));
&&["confirmed","liquid-trend","catalyst"].includes(quality)&&edge>=MIN_LIQUIDITY_EDGE&&evidence>=0.50;
if(cfg.id==="longshot")return Number(s.entry_price||0)<=0.48
&&(["EV+","selective"].includes(quality)||(quality==="trend"&&edge>=0.025&&evidence>=0.62)||(quality==="core-probe"&&edge>=0.017&&evidence>=0.72)
||(quality==="allocation"&&edge>=0.010&&evidence>=0.48));
if(cfg.id==="diversifier")return ["EV+","selective","liquidity"].includes(quality)&&edge>=MIN_LIQUIDITY_EDGE
||(quality==="trend"&&edge>=0.020&&evidence>=0.58)
||(quality==="core-probe"&&edge>=MIN_CORE_PROBE_EDGE&&evidence>=0.68)
||(quality==="allocation"&&edge>=0.008&&evidence>=0.46);
if(cfg.id==="catalyst")return ["EV+","selective","trend","liquidity","speculative","allocation"].includes(quality)
&&edge>=MIN_AGGRESSIVE_EDGE&&(Number(s.evidence_source_count||0)>=1||evidence>=0.62);
if(cfg.id==="reversal"){
const day=Number(s.price_change_1d||0),reversesMove=(s.side==="YES"&&day<=-0.02)||(s.side==="NO"&&day>=0.02);
return reversesMove&&["EV+","selective","speculative","allocation"].includes(quality)
&&edge>=(quality==="allocation"?0.008:0.012)&&evidence>=(quality==="allocation"?0.46:0.50);
}
if(cfg.id==="breakout"){
const day=Number(s.price_change_1d||0),followsMove=(s.side==="YES"&&day>0)||(s.side==="NO"&&day<0);
return followsMove&&["EV+","selective","trend","speculative","allocation"].includes(quality)
&&edge>=(quality==="allocation"?MIN_ALLOCATION_EDGE:(quality==="speculative"?MIN_AGGRESSIVE_EDGE:MIN_TREND_EDGE))
&&Number(s.momentum_strength||0)>=(quality==="allocation"?0.35:0.50)&&evidence>=(quality==="allocation"?0.46:0.48);
}
&&["confirmed","catalyst","reversal"].includes(quality)&&edge>=MIN_SELECTIVE_ENTRY_EDGE&&evidence>=0.52;
if(cfg.id==="diversifier")return ["confirmed","liquid-trend","catalyst","reversal"].includes(quality)
&&edge>=MIN_LIQUIDITY_EDGE&&evidence>=0.50;
if(cfg.id==="catalyst")return ["catalyst","confirmed"].includes(quality)&&edge>=MIN_SELECTIVE_ENTRY_EDGE
&&Number(s.evidence_source_count||0)>=1&&Number(s.signal_confidence||0)>=0.62;
if(cfg.id==="reversal")return ["reversal","confirmed"].includes(quality)&&s.signal_type==="reversal"
&&edge>=MIN_LIQUIDITY_EDGE&&Number(s.signal_confidence||0)>=0.64;
if(cfg.id==="breakout")return ["confirmed","trend","liquid-trend","catalyst"].includes(quality)
&&s.signal_type==="trend"&&edge>=MIN_TREND_EDGE&&Number(s.signal_strength||0)>=0.64&&evidence>=0.48;
if(cfg.id==="tailalpha")return Number(s.entry_price||0)<=0.42
&&["EV+","selective","trend","speculative","allocation"].includes(quality)
&&edge>=(quality==="allocation"?0.008:0.012)&&evidence>=(quality==="allocation"?0.48:0.55);
if(cfg.id==="conviction")return ["EV+","selective","speculative","allocation"].includes(quality)
&&edge>=(quality==="allocation"?0.010:0.015)&&Number(s.conviction||0)>=(quality==="allocation"?56:60)&&evidence>=(quality==="allocation"?0.48:0.52);
return ["EV+","selective"].includes(quality)&&edge>=MIN_SELECTIVE_ENTRY_EDGE;
&&["confirmed","catalyst"].includes(quality)&&edge>=MIN_SELECTIVE_ENTRY_EDGE&&evidence>=0.55;
if(cfg.id==="conviction")return ["confirmed","catalyst"].includes(quality)
&&edge>=MIN_ENTRY_EDGE&&Number(s.conviction||0)>=72&&Number(s.signal_confidence||0)>=0.66&&evidence>=0.52;
return quality==="confirmed"&&edge>=MIN_ENTRY_EDGE;
}
function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=null){
const d=decision||{minConv:cfg.minConv,maxNew:cfg.maxNew,maxFrac:cfg.maxFrac,reserve:0.05,targetExposure:cfg.targetExposure??(cfg.aggressive?0.80:0.62),minExposure:(cfg.targetExposure??(cfg.aggressive?0.80:0.62))-(cfg.aggressive?0.14:0.16)};
const d=decision||{minConv:Math.max(cfg.minConv||0,cfg.aggressive?60:64),maxNew:Math.min(cfg.maxNew||4,cfg.aggressive?4:3),maxFrac:Math.min(cfg.maxFrac||0.05,cfg.aggressive?0.07:0.045),reserve:cfg.aggressive?0.10:0.18,targetExposure:cfg.targetExposure??(cfg.aggressive?0.75:0.62),minExposure:0};
const maxPositions=cfg.maxPositions||MAX_STRATEGY_POSITIONS;
const categoryCap=cfg.maxCategoryPct||MAX_CATEGORY_EXPOSURE_PCT;
const probeQualities=["trend","liquidity","core-probe","speculative","allocation","explore"];
const probeQualities=["trend","liquid-trend","reversal","catalyst"];
const eqBefore=equity(p);
const positionValueBefore=(p.positions||[]).reduce((sum,pos)=>sum+Number(pos.value||pos.shares*pos.current_price||0),0);
const currentExposure=eqBefore>0?positionValueBefore/eqBefore:0;
const targetExposure=clamp(Number(d.targetExposure??cfg.targetExposure??0.62),0,1);
const minExposure=clamp(Number(d.minExposure??Math.max(0,targetExposure-0.16)),0,targetExposure);
const belowFloor=currentExposure+0.001<minExposure;
const needsRefill=currentExposure+0.001<targetExposure;
const minExposure=0,belowFloor=false;
const tradeReadyCount=rankedSugs.filter(s=>s.trade_ready).length;
const strategyCount=rankedSugs.filter(s=>s.trade_ready&&agentAcceptsSuggestion(cfg,s)).length;
if((p.positions||[]).length>=maxPositions||d.maxNew<=0){
@@ -1909,42 +1891,34 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
}
const avoid=avoidMarketIds||new Set();
const cands=rankedSugs.map(s=>peerAdjustedSuggestion(s,peerStats)).filter(s=>s.trade_ready&&agentAcceptsSuggestion(cfg,s)&&(s.side==="YES"||s.side==="NO")
&&(s.quality==="allocation"&&needsRefill
?(cfg.aggressive?s.conviction>=Math.min(d.minConv,50):s.peer_conviction>=Math.min(d.minConv,52))
:(cfg.aggressive?s.conviction>=d.minConv:(s.quality==="core-probe"?s.conviction>=Math.min(d.minConv,60):s.peer_conviction>=d.minConv)))
&&s.conviction>=48&&s.entry_price>=0.08&&s.entry_price<=0.92
&&(probeQualities.includes(s.quality)||effectiveEntryEdge(s)>=MIN_SELECTIVE_ENTRY_EDGE)&&(s.days_to_resolution==null||s.days_to_resolution>=MIN_ENTRY_DAYS)
&&(cfg.aggressive?s.conviction>=d.minConv:s.peer_conviction>=d.minConv)
&&s.conviction>=58&&s.entry_price>=0.08&&s.entry_price<=0.92
&&effectiveEntryEdge(s)>=MIN_AGGRESSIVE_EDGE&&(s.days_to_resolution==null||s.days_to_resolution>=MIN_ENTRY_DAYS)
&&s.volume>=MIN_VOLUME&&s.liquidity>=MIN_LIQUIDITY&&(s.volume_24hr>=500||s.conviction>=62)
&&(s.evidence_score==null||s.evidence_score>=0.40||s.conviction>=62)
&&(!avoid.has(String(s.market_id))||(s.quality==="core-probe"&&!cfg.aggressive
?Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).same||0)<4&&Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).coreSame||0)<1
:probeQualities.includes(s.quality)
?Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).same||0)<3
:belowFloor&&Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).same||0)<2&&Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).opposite||0)===0))
&&(s.evidence_score==null||s.evidence_score>=0.46)
&&(!avoid.has(String(s.market_id))||(Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).same||0)<2
&&Number((peerStats&&peerStats[`${s.market_id}:${s.side}`]||{}).opposite||0)===0))
&&!hasPosition(p,s.market_id)&&!hasRecentStop(p,s.market_id)&&(focus==="All"||!focus||s.category===focus))
.sort((a,b)=>(b.peer_conviction-a.peer_conviction)||((b.peer_boost||0)-(a.peer_boost||0)));
let opened=0,openedCoreProbes=0,openedIds=[];
let cycleBudgetRemaining=eqBefore*(cfg.aggressive?0.22:0.16);
let opened=0,openedIds=[];
let cycleBudgetRemaining=eqBefore*(cfg.aggressive?0.14:0.10);
for(const s of cands){
if(opened>=d.maxNew)break;
if((p.positions||[]).length>=maxPositions)break;
if(!cfg.aggressive&&s.quality==="core-probe"&&openedCoreProbes>=1)continue;
const eq=equity(p);
const positionValue=(p.positions||[]).reduce((sum,pos)=>sum+Number(pos.value||pos.shares*pos.current_price||0),0);
const exposureGap=Math.max(0,eq*targetExposure-positionValue);
const investable=Math.min(p.cash-eq*d.reserve,exposureGap,cycleBudgetRemaining);
if(investable<=50)break;
let frac;
if(s.quality==="allocation"){frac=Math.min(d.maxFrac,cfg.allocationFrac??(cfg.aggressive?MAX_AGGRESSIVE_ALLOCATION_PCT:MAX_CORE_ALLOCATION_PCT));}
else if(probeQualities.includes(s.quality)){frac=Math.min(d.maxFrac,cfg.aggressive?0.06:MAX_PROBE_POSITION_PCT);}
if(probeQualities.includes(s.quality)){frac=Math.min(d.maxFrac,cfg.aggressive?0.05:0.025);}
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;
let stake=Math.min(eq*frac,investable);
const recoveryCategoryCap=s.quality==="allocation"&&needsRefill?Math.max(categoryCap,cfg.aggressive?0.50:0.35):categoryCap;
const categoryValue=(p.positions||[]).filter(pos=>(pos.category||"Other")===(s.category||"Other")).reduce((sum,pos)=>sum+Number(pos.value||0),0);
stake=Math.min(stake,Math.max(0,eq*recoveryCategoryCap-categoryValue));
stake=Math.min(stake,Math.max(0,eq*categoryCap-categoryValue));
if(stake<50)continue;
const entry=s.entry_price; if(entry<=0||entry>=1)continue;
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
@@ -1957,24 +1931,23 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:s.conviction,peer_conviction:s.peer_conviction,category:s.category,opened_at:cycleIso(),url:s.url||"",
peer_note:s.peer_note||"",entry_reason:s.rationale||"",net_edge:s.net_edge,evidence_score:s.evidence_score,evidence_source_count:s.evidence_source_count||0,friction:s.friction,chase_penalty:s.chase_penalty,quality:s.quality,
strategy_version:SUGGESTION_ENGINE_VERSION,
momentum_strength:s.momentum_strength,price_change_1d:s.price_change_1d,price_change_1w:s.price_change_1w,
gain_stops:{},stop_losses:{}});
momentum_strength:s.momentum_strength,signal_strength:s.signal_strength,signal_confidence:s.signal_confidence,signal_type:s.signal_type,price_change_1d:s.price_change_1d,price_change_1w:s.price_change_1w,
peak_price:+entry.toFixed(4),gain_stops:{},stop_losses:{}});
p.history.push({date:logDay(),action:"OPEN",question:s.question,side:s.side,
detail:`${decision?decision.mode+" mode — ":""}Bought ${shares} ${s.side} '${s.question.slice(0,40)}' @ ${pct(entry)} for ${fmtUSD(cost)} · net edge ${((Math.abs(s.net_edge!=null?s.net_edge:s.edge))*100).toFixed(1)}c · evidence ${Math.round((s.evidence_score||0)*100)}${s.peer_note?` (${s.peer_note})`:""}`});
opened++;if(!cfg.aggressive&&s.quality==="core-probe")openedCoreProbes++;openedIds.push(String(s.market_id));
opened++;openedIds.push(String(s.market_id));
}
const eqAfter=equity(p);
const positionValueAfter=(p.positions||[]).reduce((sum,pos)=>sum+Number(pos.value||pos.shares*pos.current_price||0),0);
const exposureAfter=eqAfter>0?positionValueAfter/eqAfter:0;
let allocationStatus;
if(opened)allocationStatus=`Refill active: opened ${opened} position${opened===1?"":"s"} and moved exposure from ${Math.round(currentExposure*100)}% to ${Math.round(exposureAfter*100)}% toward the ${Math.round(targetExposure*100)}% target.`;
else if(exposureAfter+0.001>=targetExposure)allocationStatus=`Exposure reached its ${Math.round(targetExposure*100)}% target.`;
else if(exposureAfter+0.001>=minExposure)allocationStatus=`Exposure is inside its ${Math.round(minExposure*100)}-${Math.round(targetExposure*100)}% operating band; qualified refills continue toward target.`;
else if(!tradeReadyCount)allocationStatus="No positive-edge markets cleared the portfolio safety checks this cycle.";
if(opened)allocationStatus=`Opened ${opened} confirmed position${opened===1?"":"s"}; exposure moved from ${Math.round(currentExposure*100)}% to ${Math.round(exposureAfter*100)}% under the ${Math.round(targetExposure*100)}% ceiling.`;
else if(exposureAfter+0.001>=targetExposure)allocationStatus=`The ${Math.round(targetExposure*100)}% exposure ceiling is full.`;
else if(!tradeReadyCount)allocationStatus="No independently confirmed markets cleared the portfolio checks this cycle; cash was retained.";
else if(!strategyCount)allocationStatus=`${tradeReadyCount} markets were trade-ready, but none fit this agent's strategy.`;
else if(!cands.length)allocationStatus=`${strategyCount} strategy matches were blocked by confidence, overlap, cooldown, or focus rules.`;
else allocationStatus="Qualified candidates existed, but position, category, reserve, or minimum-size limits blocked an entry.";
p.lastDecision=Object.assign({},d,{currentExposure:+exposureAfter.toFixed(3),exposureBefore:+currentExposure.toFixed(3),targetExposure:+targetExposure.toFixed(3),minExposure:+minExposure.toFixed(3),belowFloor:exposureAfter+0.001<minExposure,needsRefill:exposureAfter+0.001<targetExposure,
else allocationStatus="Confirmed candidates existed, but position, overlap, category, reserve, or minimum-size limits blocked an entry.";
p.lastDecision=Object.assign({},d,{currentExposure:+exposureAfter.toFixed(3),exposureBefore:+currentExposure.toFixed(3),targetExposure:+targetExposure.toFixed(3),minExposure:0,belowFloor:false,capacityAvailable:exposureAfter+0.001<targetExposure,
tradeReadyCount,strategyCandidates:strategyCount,eligibleCandidates:cands.length,opened,allocationStatus});
return openedIds;
}
@@ -2156,7 +2129,8 @@ function format24h(change){
function board(){
const st=loadState();
return AGENTS.map(c=>{const p=st.agents[c.id]||defaultPortfolio();const eq=equity(p);
return {c,p,eq,pnl:eq-STARTING_BALANCE,ret:(eq/STARTING_BALANCE-1)*100,change24h:portfolioChange24h(p,eq)};})
const baseline=Number(p.engine_baseline&&p.engine_baseline.version===SUGGESTION_ENGINE_VERSION?p.engine_baseline.equity:eq);
return {c,p,eq,pnl:eq-STARTING_BALANCE,ret:(eq/STARTING_BALANCE-1)*100,enginePnl:eq-baseline,engineRet:baseline>0?(eq/baseline-1)*100:0,change24h:portfolioChange24h(p,eq)};})
.sort((a,b)=>b.eq-a.eq);
}
const MEDALS=["🥇","🥈","🥉"];
@@ -2167,13 +2141,15 @@ function renderAll(){renderOverview();renderEmailAlerts();renderLeaderboard();re
function renderOverview(){
const b=board(); const lead=b[0];
const avgRet=b.reduce((s,r)=>s+r.ret,0)/b.length;
const engineAvg=b.reduce((s,r)=>s+r.engineRet,0)/b.length;
const core=b.filter(r=>!r.c.aggressive),coreAvg=core.reduce((s,r)=>s+r.ret,0)/Math.max(1,core.length);
const aggressive=b.filter(r=>r.c.aggressive),aggressiveAvg=aggressive.reduce((s,r)=>s+r.ret,0)/Math.max(1,aggressive.length);
const sugCount=(loadSuggestions().suggestions||[]).length;
const stats=[
{ic:lead.c.emoji,label:"Leader",value:lead.c.name.split(" ")[0]},
{ic:"📈",label:"Leader return",value:fmtPct(lead.ret),cls:signClass(lead.pnl)},
{ic:"⚖️",label:"Mixed-inception avg",value:fmtPct(avgRet),cls:signClass(avgRet)},
{ic:"⚖️",label:"Legacy avg",value:fmtPct(avgRet),cls:signClass(avgRet)},
{ic:"🧪",label:`v${SUGGESTION_ENGINE_VERSION} avg`,value:fmtPct(engineAvg),cls:signClass(engineAvg)},
{ic:"🧠",label:"Core strategy avg",value:fmtPct(coreAvg),cls:signClass(coreAvg)},
{ic:"⚡",label:"Aggressive avg",value:fmtPct(aggressiveAvg),cls:signClass(aggressiveAvg)},
{ic:"💡",label:"Scored ideas",value:sugCount},
@@ -2187,7 +2163,7 @@ function renderOverview(){
}
function renderTopPicks(){
const root=$("topPicks"); if(!root)return;
const top=((loadSuggestions().suggestions)||[]).slice(0,5);
const top=((loadSuggestions().suggestions)||[]).filter(s=>s.trade_ready).slice(0,5);
if(!top.length){root.innerHTML=`<div class="empty">Run a cycle to see top picks.</div>`;return;}
root.innerHTML=top.map(s=>`<div class="pos"><div>
<div class="pq">${s.url?`<a class="market-title" href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.question.slice(0,58))}${s.question.length>58?"…":""}</a>`:`${esc(s.question.slice(0,58))}${s.question.length>58?"…":""}`}</div>
@@ -2286,7 +2262,7 @@ function decisionSummary(p){
const d=p.lastDecision;
if(!d)return "No live adaptation yet; this agent is still using its base rulebook.";
const emotion=d.emotion?` Emotion: ${d.emotion}${d.urgency!=null?` (${Math.round(d.urgency*100)}% urgency)`:""}.`:"";
const exposure=d.currentExposure!=null&&d.targetExposure!=null?` Exposure ${Math.round(d.currentExposure*100)}%; operating band ${Math.round(Number(d.minExposure||0)*100)}-${Math.round(d.targetExposure*100)}%.`:"";
const exposure=d.currentExposure!=null&&d.targetExposure!=null?` Exposure ${Math.round(d.currentExposure*100)}%; ceiling ${Math.round(d.targetExposure*100)}%.`:"";
const allocation=d.allocationStatus?` ${d.allocationStatus}`:"";
const candidates=d.tradeReadyCount!=null?` Candidate audit: ${d.tradeReadyCount} trade-ready, ${d.strategyCandidates||0} strategy matches, ${d.eligibleCandidates||0} fully eligible, ${d.opened||0} opened.`:"";
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}+`:""}.${exposure}${allocation}${candidates}`;
@@ -2296,7 +2272,7 @@ function renderAgentBrief(cfg,p,st){
const bw=bestAndWorst(p);
const risk=cfg.aggressive?"Extreme risk":cfg.maxFrac>=0.055?"Active":cfg.maxFrac<=0.04?"Broad active":"Disciplined";
const cadence=`${cfg.maxNew||0} new trades max per cycle`;
const exitRule=`Exit if edge fades, conviction weakens, <${EXIT_RESOLUTION_DAYS}d to resolve, or ${EXIT_STALE_DAYS}d stale`;
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`;
const thesis=agentPlainBlurb(cfg,st);
root.innerHTML=`
<div class="brief-main" style="border-top:3px solid ${cfg.color}">
@@ -2307,7 +2283,7 @@ function renderAgentBrief(cfg,p,st){
<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>
<div class="brief-mini"><div class="k">Stop Loss</div><div class="v">Sell all at -18%</div></div>
<div class="brief-mini"><div class="k">Gain Stop</div><div class="v">Sell 25% at +75%, +150%, +300%</div></div>
<div class="brief-mini"><div class="k">Profit Locks</div><div class="v">Three reachable, entry-aware 25% sales</div></div>
<div class="brief-mini"><div class="k">Policy Exit</div><div class="v">${esc(exitRule)}</div></div>
<div class="brief-mini"><div class="k">Best / Worst</div><div class="v"><span class="pos-val">${esc(bw.best)}</span><br><span class="neg-val">${esc(bw.worst)}</span></div></div>`;
}
@@ -2319,7 +2295,7 @@ function renderLeaderboard(){
<span class="rankpill">#${i+1}</span>
<div class="lb-top"><span class="medal">${MEDALS[i]||""}</span><span class="lb-emoji">${r.c.emoji}</span>
<div><div class="lb-name">${r.c.name}</div><div class="lb-blurb">${agentBlurb(r.c,st)}</div></div></div>
<div class="lb-mid"><div class="lb-eq">${fmtUSD(r.eq)}</div><div><div class="lb-ret ${signClass(r.pnl)}">${fmtPct(r.ret)}</div><div class="small ${signClass(r.change24h.pct)}">24h ${format24h(r.change24h)}</div></div></div>
<div class="lb-mid"><div class="lb-eq">${fmtUSD(r.eq)}</div><div><div class="lb-ret ${signClass(r.pnl)}">${fmtPct(r.ret)}</div><div class="small ${signClass(r.enginePnl)}">v${SUGGESTION_ENGINE_VERSION} ${fmtPct(r.engineRet)}</div><div class="small ${signClass(r.change24h.pct)}">24h ${format24h(r.change24h)}</div></div></div>
<div class="lb-foot"><span class="muted small">${r.p.positions.length} open · ${r.p.closed.length} closed</span>${miniSpark(r.p.snapshots,r.c.color)}</div>
</div>`).join("");
document.querySelectorAll("#leaderboard .lb-card").forEach(el=>{
@@ -2492,8 +2468,8 @@ function renderSuggestions(){
const scanned=Number(data.market_count||0).toLocaleString();
const analyzed=Number(data.analyzed_count||data.market_count||0).toLocaleString();
$("focusNote").textContent=focus==="All"
? `Loaded the ${scanned} most active markets, analyzed ${analyzed} by activity, liquidity, spread, and price direction, and kept ${all.length} ideas (${buyCount} BUY, ${watchCount} WATCH). Showing ${filtered.length}; BUY includes strict EV setups, strategy probes, and reduced-size allocation candidates for agents below their exposure floor. Avg evidence ${evAvg}.`
: `Loaded the ${scanned} most active markets and analyzed ${analyzed}. Showing ${filtered.length} of ${pool.length} ${focus} ideas; agents may open strict EV entries, risk-capped probes, or tiny exploration trades while this is selected.`;
? `Loaded the ${scanned} most active markets, analyzed ${analyzed} by activity, liquidity, spread, and independent time-window confirmation, and kept ${all.length} ideas (${buyCount} BUY, ${watchCount} WATCH). Showing ${filtered.length}; BUY requires a confirmed trend, reversal, liquid setup, or catalyst with positive margin after modeled friction. Cash is retained when no setup qualifies. Avg evidence ${evAvg}.`
: `Loaded the ${scanned} most active markets and analyzed ${analyzed}. Showing ${filtered.length} of ${pool.length} ${focus} ideas; agents can buy only independently confirmed setups that also fit their strategy and risk limits.`;
if(!filtered.length){root.innerHTML=`<div class="empty" style="grid-column:1/-1">No ${esc(focus)} suggestions in this live batch.</div>`;return;}
root.innerHTML=filtered.map(s=>{
const col=catColor(s.category);
@@ -2513,7 +2489,7 @@ function renderSuggestions(){
<div class="rationale">${esc(s.rationale)}</div>
<div class="drivers">${drivers}</div>
<div class="metrics">
<span>Net edge <b>${((Math.abs(s.net_edge!=null?s.net_edge:s.edge))*100).toFixed(1)}c</b></span><span>Evidence <b>${Math.round((s.evidence_score||0)*100)}</b></span>
<span>Signal margin <b>${((Math.abs(s.net_edge!=null?s.net_edge:s.edge))*100).toFixed(1)}c</b></span><span>Evidence <b>${Math.round((s.evidence_score||0)*100)}</b></span>
<span>Vol <b>${fmtUSD(s.volume)}</b></span><span>24h <b>${fmtUSD(s.volume_24hr)}</b></span><span>Resolves <b>${days}</b></span>
${s.url?`<a class="market-link" href="${esc(s.url)}" target="_blank" rel="noopener">Open on Polymarket ↗</a>`:""}
${PERSONAL_MODE?`<button class="btn ghost" data-stage-suggestion="${esc(s.market_id)}">Stage ticket</button>`:""}
@@ -2532,6 +2508,7 @@ function renderPortfolioTab(){
document.querySelectorAll("#agentSel .segbtn").forEach(el=>el.addEventListener("click",()=>{localStorage.setItem(VIEW_KEY,el.dataset.agent);renderPortfolioTab();}));
const p=st.agents[viewId]||defaultPortfolio();const eq=equity(p),pnl=eq-p.starting_balance;
const change24h=portfolioChange24h(p,eq);
const engineBase=Number(p.engine_baseline&&p.engine_baseline.version===SUGGESTION_ENGINE_VERSION?p.engine_baseline.equity:eq),enginePnl=eq-engineBase;
renderAgentBrief(cfg,p,st);
const stats=[
{ic:"💰",label:"Equity",value:fmtUSD(eq)},
@@ -2539,6 +2516,7 @@ function renderPortfolioTab(){
{ic:"📈",label:"P&L",value:fmtUSD(pnl),cls:signClass(pnl)},
{ic:"🎯",label:"Return",value:fmtPct((eq/p.starting_balance-1)*100),cls:signClass(pnl)},
{ic:"🕒",label:"24h change",value:format24h(change24h),cls:signClass(change24h.pct)},
{ic:"🧪",label:`v${SUGGESTION_ENGINE_VERSION}`,value:fmtPct(engineBase>0?(eq/engineBase-1)*100:0),cls:signClass(enginePnl)},
{ic:"📂",label:"Open",value:p.positions.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("");
@@ -2591,14 +2569,14 @@ 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 model-versus-market mispricings and avoiding crowded trades without enough edge.";
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==="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.";
if(cfg.id==="diversifier")return "Plan: spread bets broadly, reduce single-market damage, and try to win through consistency rather than one heroic call.";
if(cfg.id==="catalyst")return "Plan: deploy quickly when fresh real-world context and measured edge align, then concentrate enough capital for a catalyst repricing to move the account.";
if(cfg.id==="reversal")return "Plan: attack overextended one-day moves from the opposite side, looking for liquid mean-reversion setups with enough evidence to justify the contrarian risk.";
if(cfg.id==="breakout")return "Plan: stay nearly fully deployed in confirmed momentum setups and press the fastest breakouts where direction, activity, and edge agree.";
if(cfg.id==="breakout")return "Plan: enter only confirmed momentum setups and press the cleanest breakouts where direction, activity, and signal margin agree.";
if(cfg.id==="tailalpha")return "Plan: hunt underpriced low-cost contracts with asymmetric upside, accepting a volatile hit rate while keeping each tail position survivable.";
if(cfg.id==="conviction")return "Plan: ignore marginal trades and commit the largest allowed stakes to a compact book of the strongest edge-and-evidence combinations.";
return "Plan: keep following its rulebook and use fresh market data to pressure the leaderboard.";
@@ -2850,7 +2828,7 @@ function agentChatReply(agentId,question,history=[]){
return `My style is ${agentVoice(cfg).tone}.\n\n${agentPlainBlurb(cfg,st)}\n\nCurrent adaptation: ${decisionSummary(p)}`;
}
if(/risk|stop|loss|gain|take profit|sell/.test(combined)){
return `My risk rules are mechanical:\n\nStop loss: sell the full remaining position at -18% from entry.\nGain stop: sell 25% at +75%, another 25% at +150%, and another 25% at +300%.\n\nRecent risk behavior: ${recentActionSummary(p)}.`;
return `My risk rules are mechanical:\n\nStop loss: sell the full remaining position at -18% from entry.\nProfit locks: sell 25% at three entry-aware targets that remain reachable below $1, then protect gains with a trailing exit. Signal exits need at least 24 hours of holding and six hours of confirmed conflict.\n\nRecent risk behavior: ${recentActionSummary(p)}.`;
}
if(/mistake|wrong|regret|fix|improve|better/.test(combined)){
return `${regretNarrative(row,p)}\n\nThe fix is not “bet harder.” The fix is stricter entries, fewer positions, and cutting trades when the model edge fades. That is the discipline I am trying to follow now.`;
@@ -3839,6 +3817,46 @@ function showTab(name){
document.querySelectorAll(".tab").forEach(t=>t.addEventListener("click",()=>showTab(t.dataset.tab)));
window.addEventListener("hashchange",()=>showTab(location.hash.slice(1)));
window.PMA_ENGINE_DIAGNOSTICS=Object.freeze({
version:SUGGESTION_ENGINE_VERSION,
analyzeMarket,
gainStopTargets:(entry)=>GAIN_STOP_TIERS.map(t=>gainStopTarget({entry_price:Number(entry),cost:1,shares:1,gain_stops:{}},t)),
rules:Object.freeze({minimumPolicyHoldHours:MIN_POLICY_HOLD_HOURS,exitConfirmationHours:EXIT_CONFIRM_HOURS,maxAgentOverlap:2,stopLossPct:18}),
});
function runEngineSelfTest(){
const market=(overrides={})=>Object.assign({
id:"self-test",question:"Will the president win the election?",event:"Election",category:"Politics",tags:["election"],
yes_price:0.42,no_price:0.58,volume:500000,volume_24hr:60000,volume_1wk:280000,liquidity:75000,spread:0.01,
price_change_1h:0.004,price_change_1d:0.035,price_change_1w:0.08,days_to_resolution:30,
clob_token_ids:["yes","no"],url:"",closed:false,accepting_orders:true,
},overrides);
const news={"self-test":{source_count:3,latest_title:"Election polling update"}};
const trend=analyzeMarket(market(),news);
const noSignal=analyzeMarket(market({price_change_1h:0,price_change_1d:0.002,price_change_1w:-0.002}),news);
const reversal=analyzeMarket(market({price_change_1h:-0.01,price_change_1d:0.07,price_change_1w:0.02}),news);
const targets=window.PMA_ENGINE_DIAGNOSTICS.gainStopTargets(0.82);
const hoursAgo=h=>new Date(Date.now()-h*3600000).toISOString();
const conflict={trade_ready:true,side:"NO",net_edge:-0.03,conviction:72};
const mature={entry_price:0.42,current_price:0.39,opened_at:hoursAgo(48),signal_conflict_since:hoursAgo(7),unrealized_pnl:-10,quality:"confirmed"};
const young={entry_price:0.42,current_price:0.39,opened_at:hoursAgo(2),signal_conflict_since:hoursAgo(1),unrealized_pnl:-10,quality:"confirmed"};
const trailing={entry_price:0.40,current_price:0.45,peak_price:0.55,opened_at:hoursAgo(48),unrealized_pnl:10,quality:"confirmed"};
const fresh=market();
const mock={agents:{}};AGENTS.forEach(a=>mock.agents[a.id]=defaultPortfolio());
["value","momentum","favorite"].forEach((id,i)=>mock.agents[id].positions.push({market_id:"overlap-test",question:"Overlap test",side:"YES",shares:100,current_price:0.5,entry_price:0.5,cost:50,value:50,unrealized_pnl:0,conviction:70-i,opened_at:hoursAgo(24)}));
reduceStrategyOverlap(mock);
const overlapRemaining=AGENTS.reduce((sum,a)=>sum+mock.agents[a.id].positions.filter(p=>p.market_id==="overlap-test").length,0);
return {version:SUGGESTION_ENGINE_VERSION,
trend:{ready:trend.trade_ready,quality:trend.quality,side:trend.side,margin:trend.net_edge},
noSignal:{ready:noSignal.trade_ready,quality:noSignal.quality,signal:noSignal.signal_type,margin:noSignal.net_edge},
reversal:{ready:reversal.trade_ready,quality:reversal.quality,side:reversal.side,margin:reversal.net_edge},
highEntryTargets:targets,targetsReachable:targets.every(x=>x>0.82&&x<1),
exits:{youngConflict:exitReason(young,fresh,conflict,AGENTS[0]),matureConflict:exitReason(mature,fresh,conflict,AGENTS[0]),trailing:trailingProfitReason(trailing)},
overlapRemaining,rules:window.PMA_ENGINE_DIAGNOSTICS.rules};
}
if(new URLSearchParams(location.search).get("engine_test")==="1"){
const output=document.createElement("output");output.id="engineSelfTest";output.hidden=true;output.textContent=JSON.stringify(runEngineSelfTest());document.body.appendChild(output);
}
/* ---------- Wire up ---------- */
applyPersonalMode();
initProviderConfig();
@@ -3969,7 +3987,7 @@ async function autoRunDueCycle(){
}
setInterval(()=>autoRunDueCycle(),RUN_INTERVAL_MS);
/* On load: first visit runs a 7-day backtest; afterwards the shared portfolio advances as often as the cycle key allows. */
/* On load: a new shared portfolio starts with an honest zero baseline; later cycles use live market snapshots. */
(async function init(){
showTab(location.hash.slice(1)||"overview");
fetchLiveBackendStatus();
@@ -3989,10 +4007,14 @@ setInterval(()=>autoRunDueCycle(),RUN_INTERVAL_MS);
return;
}
if(!st.seeded){
btn.disabled=true;btn.textContent="Backtesting…";
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=currentCycleHour();saveState(st);if(!(await pushCloudState()))throw new Error("Cloud sync unavailable");renderAll();
toast("7-day backtest complete — live tracking begins today.");}
catch(e){setStatus("backtest error — click Run",false);}
btn.disabled=true;btn.textContent="Starting…";
try{
SNAP_TS=nowIso();AGENTS.forEach(a=>recordSnapshot(st.agents[a.id]));SNAP_TS=null;
st.seeded=true;st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=null;saveState(st);
if(!(await pushCloudState()))throw new Error("Cloud sync unavailable");
await runDailyCycle();renderAll();toast("Live paper tracking started from a clean zero baseline.");
}
catch(e){setStatus("start error — click Run",false);}
btn.disabled=false;btn.textContent="Run cycle";
}else if(st.last_cycle_hour!==currentCycleHour()){
btn.disabled=true;btn.textContent="Running…";