Tune agent risk and entry quality

This commit is contained in:
Theodore Song
2026-07-16 18:04:49 -04:00
parent 8ede79e4a0
commit 840031086a
+34 -28
View File
@@ -745,11 +745,11 @@ const GAIN_STOP_TIERS = [
{id:"150",gain:1.50,multiple:2.50,sellFrac:0.25,label:"+150%"}, {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:"300",gain:3.00,multiple:4.00,sellFrac:0.25,label:"+300%"},
]; ];
const EXIT_STALE_DAYS = 14; const EXIT_STALE_DAYS = 7;
const EXIT_RESOLUTION_DAYS = 2; const EXIT_RESOLUTION_DAYS = 3;
const AGENTS_KEY = "pma_agents_v2"; const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v3"; const SUG_KEY = "pma_suggestions_v4";
const SUGGESTION_ENGINE_VERSION = 4; const SUGGESTION_ENGINE_VERSION = 5;
const FOCUS_KEY = "pma_focus_v1"; const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1"; const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1"; const PF_SORT_KEY = "pma_portfolio_sort_v1";
@@ -820,23 +820,23 @@ const AGENTS = [
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy", {id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
blurb:"Looks for the widest gap between the model's fair value and the current market price. 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 widest gap between the model's fair value and the current market price. 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.",
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction), rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
maxNew:4, maxFrac:0.10, minConv:45, kelly:0.25}, maxNew:2, maxFrac:0.06, minConv:62, kelly:0.14},
{id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy", {id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy",
blurb:"Chases markets where attention is accelerating. It weights conviction by fresh 24-hour volume, so it tends to enter fast-moving events with strong liquidity and recent trader interest.", blurb:"Chases markets where attention is accelerating. It weights conviction by fresh 24-hour volume, so it tends to enter fast-moving events with strong liquidity and recent trader interest.",
rank:(s)=>[...s].sort((a,b)=>(b.volume_24hr*(b.conviction+20))-(a.volume_24hr*(a.conviction+20))), rank:(s)=>[...s].sort((a,b)=>(b.volume_24hr*(b.conviction+20))-(a.volume_24hr*(a.conviction+20))),
maxNew:5, maxFrac:0.12, minConv:40, kelly:0.30}, maxNew:2, maxFrac:0.065, minConv:60, kelly:0.15},
{id:"favorite", name:"Favorite Backer", emoji:"🛡️", color:"#38d2e6", kind:"strategy", {id:"favorite", name:"Favorite Backer", emoji:"🛡️", color:"#38d2e6", kind:"strategy",
blurb:"A lower-drama agent that only considers outcomes already priced as favorites. It tries to grind out steadier returns by backing high-probability markets when the model still sees a useful edge.", blurb:"A lower-drama agent that only considers outcomes already priced as favorites. It tries to grind out steadier returns by backing high-probability markets when the model still sees a useful edge.",
rank:(s)=>[...s].filter(x=>x.entry_price>=0.6).sort((a,b)=>b.entry_price-a.entry_price||b.conviction-a.conviction), rank:(s)=>[...s].filter(x=>x.entry_price>=0.6).sort((a,b)=>b.entry_price-a.entry_price||b.conviction-a.conviction),
maxNew:4, maxFrac:0.14, minConv:38, kelly:0.40}, maxNew:2, maxFrac:0.07, minConv:58, kelly:0.16},
{id:"longshot", name:"Longshot Hunter", emoji:"🎰", color:"#fbbf24", kind:"strategy", {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, accepting more volatility in exchange for the chance that one mispriced longshot rerates sharply.", blurb:"The swing-for-upside strategy. It hunts cheaper contracts that the model thinks are being ignored, accepting more volatility in exchange for the chance that one mispriced longshot rerates sharply.",
rank:(s)=>[...s].filter(x=>x.entry_price<=0.4).sort((a,b)=>a.entry_price-b.entry_price||b.conviction-a.conviction), rank:(s)=>[...s].filter(x=>x.entry_price<=0.4).sort((a,b)=>a.entry_price-b.entry_price||b.conviction-a.conviction),
maxNew:5, maxFrac:0.08, minConv:38, kelly:0.20}, maxNew:2, maxFrac:0.035, minConv:64, kelly:0.08},
{id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy", {id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy",
blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across many high-conviction ideas to reduce single-market risk and show how the whole suggestion pool performs.", blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across many high-conviction ideas to reduce single-market risk and show how the whole suggestion pool performs.",
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction), rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
maxNew:8, maxFrac:0.05, minConv:35, kelly:0.15, flat:true}, maxNew:4, maxFrac:0.025, minConv:58, kelly:0.08, flat:true},
{id:"copycat", name:"The Copycat", emoji:"🐒", color:"#ec4899", kind:"copycat", {id:"copycat", name:"The Copycat", emoji:"🐒", color:"#ec4899", kind:"copycat",
blurb:"A meta-agent that does not pick markets directly. It watches the in-house strategies, scores their return trend, MACD, recent momentum, and drawdown risk, then mirrors the agent most likely to keep improving rather than blindly chasing the current leader."}, blurb:"A meta-agent that does not pick markets directly. It watches the in-house strategies, scores their return trend, MACD, recent momentum, and drawdown risk, then mirrors the agent most likely to keep improving rather than blindly chasing the current leader."},
{id:"whale1", name:"Apex Mirror", emoji:"🐋", color:"#2dd4bf", kind:"whale", slot:0, {id:"whale1", name:"Apex Mirror", emoji:"🐋", color:"#2dd4bf", kind:"whale", slot:0,
@@ -938,7 +938,11 @@ async function fetchMarketPrice(id){
/* ---------- Analysis engine ---------- */ /* ---------- Analysis engine ---------- */
const W={LIQ:0.25,MOM:0.25,MIS:0.30,TIME:0.20}; const W={LIQ:0.25,MOM:0.25,MIS:0.30,TIME:0.20};
const MIN_VOLUME=5000; const MIN_VOLUME=18000;
const MIN_LIQUIDITY=1500;
const MIN_ENTRY_EDGE=0.03;
const EDGE_SCALE=0.14;
const MAX_STRATEGY_POSITIONS=14;
const SUGGESTION_TOTAL=180; const SUGGESTION_TOTAL=180;
const SUGGESTION_PER_CATEGORY=40; const SUGGESTION_PER_CATEGORY=40;
const VISIBLE_SUGGESTIONS=120; const VISIBLE_SUGGESTIONS=120;
@@ -951,14 +955,14 @@ function fairValue(m){const p=m.yes_price,mom=momentumSignal(m),liq=liquiditySig
return clamp(p+(mom-0.5)*0.06,0.02,0.98);} return clamp(p+(mom-0.5)*0.06,0.02,0.98);}
function timingSignal(d){if(d==null)return 0.4;if(d<1)return 0.1;if(d<=3)return 0.5;if(d<=90)return 1.0-(d-3)/87.0*0.4;return 0.35;} function timingSignal(d){if(d==null)return 0.4;if(d<1)return 0.1;if(d<=3)return 0.5;if(d<=90)return 1.0-(d-3)/87.0*0.4;return 0.35;}
function analyzeMarket(m){ function analyzeMarket(m){
if(m.volume<MIN_VOLUME)return null; if(m.volume<MIN_VOLUME||m.liquidity<MIN_LIQUIDITY)return null;
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p,edge=Math.abs(edgeYes); const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p,edge=Math.abs(edgeYes);
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution); const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution);
const mis=Math.min(1,edge/0.10); const mis=Math.min(1,edge/EDGE_SCALE);
const conviction=+((W.LIQ*liq+W.MOM*mom+W.MIS*mis+W.TIME*timing)*100).toFixed(1); const conviction=+((W.LIQ*liq+W.MOM*mom+W.MIS*mis+W.TIME*timing)*100).toFixed(1);
let side,entry,rationale; let side,entry,rationale;
if(edgeYes>0.015){side="YES";entry=p;rationale=`Model fair value ${pct(fair)} vs market ${pct(p)} — YES looks underpriced by ~${(edgeYes*100).toFixed(1)}c.`;} if(edgeYes>MIN_ENTRY_EDGE){side="YES";entry=p;rationale=`Model fair value ${pct(fair)} vs market ${pct(p)} — YES looks underpriced by ~${(edgeYes*100).toFixed(1)}c.`;}
else if(edgeYes<-0.015){side="NO";entry=m.no_price;rationale=`Model fair value ${pct(fair)} vs market ${pct(p)} — YES looks overpriced, so NO at ${pct(m.no_price)} has the edge.`;} else if(edgeYes<-MIN_ENTRY_EDGE){side="NO";entry=m.no_price;rationale=`Model fair value ${pct(fair)} vs market ${pct(p)} — YES looks overpriced, so NO at ${pct(m.no_price)} has the edge.`;}
else{side="HOLD";entry=p;rationale=`Price ${pct(p)} is close to model fair value ${pct(fair)}; no clear edge — watch only.`;} else{side="HOLD";entry=p;rationale=`Price ${pct(p)} is close to model fair value ${pct(fair)}; no clear edge — watch only.`;}
const drivers=[]; const drivers=[];
if(liq>0.6)drivers.push("deep/liquid market"); if(liq>0.6)drivers.push("deep/liquid market");
@@ -1075,28 +1079,28 @@ function recentReturnDelta(p){
function adaptiveDecision(cfg,p,rank,total,leaderEq){ function adaptiveDecision(cfg,p,rank,total,leaderEq){
const eq=equity(p),ret=(eq/p.starting_balance-1)*100,trail=((leaderEq||eq)-eq)/p.starting_balance*100; const eq=equity(p),ret=(eq/p.starting_balance-1)*100,trail=((leaderEq||eq)-eq)/p.starting_balance*100;
const trend=recentReturnDelta(p); const trend=recentReturnDelta(p);
let minConv=cfg.minConv??0,maxNew=cfg.maxNew??12,maxFrac=cfg.maxFrac??0.34,reserve=0.05,mode="Balanced",reason="following its base strategy while watching the leaderboard."; let minConv=cfg.minConv??0,maxNew=cfg.maxNew??12,maxFrac=cfg.maxFrac??0.34,reserve=0.10,mode="Balanced",reason="following its base strategy while keeping a larger cash cushion.";
if(rank===1){ if(rank===1){
mode="Defend";minConv+=5;maxNew=Math.max(1,maxNew-1);maxFrac*=0.82;reserve=0.09; mode="Defend";minConv+=6;maxNew=Math.max(1,maxNew-1);maxFrac*=0.75;reserve=0.15;
reason="leading the race, so it raises standards and protects cash instead of overtrading."; reason="leading the race, so it raises standards and protects cash instead of overtrading.";
}else if(trail>6){ }else if(trail>6){
mode="Chase";minConv-=5;maxNew+=1;maxFrac*=1.18;reserve=0.035; mode="Selective Chase";minConv+=2;maxFrac*=0.90;reserve=0.09;
reason="far behind the leader, so it accepts a little more risk to find catch-up trades."; reason="far behind the leader, so it looks for cleaner catch-up trades instead of forcing risk.";
}else if(trend>1.5){ }else if(trend>1.5){
mode="Press";minConv-=2;maxNew+=1;maxFrac*=1.08;reserve=0.04; mode="Measured Press";minConv+=2;maxFrac*=0.92;reserve=0.08;
reason="recent momentum is positive, so it leans into its edge while the tape is friendly."; reason="recent momentum is positive, so it keeps participating without lowering standards.";
} }
if(ret<-5){ if(ret<-5){
mode="Recover";minConv+=6;maxNew=Math.max(1,maxNew-1);maxFrac*=0.72;reserve=0.08; mode="Recover";minConv+=8;maxNew=Math.max(1,maxNew-1);maxFrac*=0.55;reserve=0.16;
reason="in drawdown, so it tightens entries, shrinks bets, and waits for stronger confirmation."; reason="in drawdown, so it tightens entries, shrinks bets, and waits for stronger confirmation.";
} }
if(ret<-18){ if(ret<-18){
mode="Crisis Guard";minConv+=12;maxNew=1;maxFrac=Math.min(maxFrac*0.45,0.045);reserve=0.16; mode="Crisis Guard";minConv+=14;maxNew=1;maxFrac=Math.min(maxFrac*0.35,0.025);reserve=0.25;
reason="down more than 18%, so it stops chasing, keeps more cash, and only allows one high-conviction recovery trade."; reason="down more than 18%, so it stops chasing, keeps more cash, and only allows one high-conviction recovery trade.";
} }
if((p.cash/Math.max(eq,1))<0.12){maxNew=Math.max(1,Math.min(maxNew,2));reason+=" Cash is tight, so new entries are rationed.";} if((p.cash/Math.max(eq,1))<0.20){maxNew=1;reason+=" Cash is tight, so new entries are rationed.";}
const maxCap=cfg.kind==="whale"?0.34:0.18; const maxCap=cfg.kind==="whale"?0.34:0.08;
return {mode,reason,minConv:Math.max(0,Math.round(minConv)),maxNew:Math.max(1,Math.round(maxNew)),maxFrac:+Math.min(maxCap,Math.max(0.02,maxFrac)).toFixed(3),reserve}; return {mode,reason,minConv:Math.max(0,Math.round(minConv)),maxNew:Math.max(1,Math.round(maxNew)),maxFrac:+Math.min(maxCap,Math.max(0.01,maxFrac)).toFixed(3),reserve};
} }
const stopKey=(posOrId)=>typeof posOrId==="string"?posOrId:String(posOrId.asset||posOrId.market_id||""); const stopKey=(posOrId)=>typeof posOrId==="string"?posOrId:String(posOrId.asset||posOrId.market_id||"");
const hasStoppedToday=(p,posOrId)=>p.stopped&&p.stopped[stopKey(posOrId)]===logDay(); const hasStoppedToday=(p,posOrId)=>p.stopped&&p.stopped[stopKey(posOrId)]===logDay();
@@ -1246,7 +1250,7 @@ function exitReason(pos,fresh,analysis,cfg){
if(daysHeld(pos)>=EXIT_STALE_DAYS)return `Stale exit after ${Math.floor(daysHeld(pos))} days`; if(daysHeld(pos)>=EXIT_STALE_DAYS)return `Stale exit after ${Math.floor(daysHeld(pos))} days`;
if(analysis.side==="HOLD")return "Edge faded"; if(analysis.side==="HOLD")return "Edge faded";
if(analysis.side&&analysis.side!==pos.side)return `Model flipped to ${analysis.side}`; if(analysis.side&&analysis.side!==pos.side)return `Model flipped to ${analysis.side}`;
const minExit=Math.max(25,(cfg&&cfg.minConv?cfg.minConv:35)-10); const minExit=Math.max(35,(cfg&&cfg.minConv?cfg.minConv:45)-5);
if(analysis.conviction<minExit)return `Conviction faded to ${Math.round(analysis.conviction)}`; if(analysis.conviction<minExit)return `Conviction faded to ${Math.round(analysis.conviction)}`;
return null; return null;
} }
@@ -1275,18 +1279,20 @@ function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds){ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds){
const d=decision||{minConv:cfg.minConv,maxNew:cfg.maxNew,maxFrac:cfg.maxFrac,reserve:0.05}; const d=decision||{minConv:cfg.minConv,maxNew:cfg.maxNew,maxFrac:cfg.maxFrac,reserve:0.05};
p.lastDecision=decision||null; p.lastDecision=decision||null;
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)return [];
const cands=rankedSugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=d.minConv const cands=rankedSugs.filter(s=>(s.side==="YES"||s.side==="NO")&&s.conviction>=d.minConv
&&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus)); &&Math.abs(s.edge)>=MIN_ENTRY_EDGE&&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus));
const avoid=avoidMarketIds||new Set(); const avoid=avoidMarketIds||new Set();
const ordered=cands.filter(s=>!avoid.has(s.market_id)).concat(cands.filter(s=>avoid.has(s.market_id))); const ordered=cands.filter(s=>!avoid.has(s.market_id)).concat(cands.filter(s=>avoid.has(s.market_id)));
let opened=0,openedIds=[]; let opened=0,openedIds=[];
for(const s of ordered){ for(const s of ordered){
if(opened>=d.maxNew)break; if(opened>=d.maxNew)break;
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)break;
const eq=equity(p),investable=p.cash-eq*d.reserve; const eq=equity(p),investable=p.cash-eq*d.reserve;
if(investable<=50)break; if(investable<=50)break;
let frac; let frac;
if(cfg.flat){frac=d.maxFrac;} if(cfg.flat){frac=d.maxFrac;}
else{const base=(s.conviction/100)*Math.min(1,Math.abs(s.edge)/0.10);frac=Math.min(d.maxFrac,cfg.kelly*base);} else{const base=(s.conviction/100)*Math.min(1,Math.abs(s.edge)/EDGE_SCALE);frac=Math.min(d.maxFrac,cfg.kelly*base);}
let stake=Math.min(eq*frac,investable); let stake=Math.min(eq*frac,investable);
if(stake<50)continue; if(stake<50)continue;
const entry=s.entry_price; if(entry<=0||entry>=1)continue; const entry=s.entry_price; if(entry<=0||entry>=1)continue;