diff --git a/index.html b/index.html
index 358d82c..c0323d4 100644
--- a/index.html
+++ b/index.html
@@ -739,7 +739,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
@@ -766,7 +766,7 @@ const EXIT_STALE_DAYS = 10;
const EXIT_RESOLUTION_DAYS = 2;
const AGENTS_KEY = "pma_agents_v2";
const SUG_KEY = "pma_suggestions_v5";
-const SUGGESTION_ENGINE_VERSION = 11;
+const SUGGESTION_ENGINE_VERSION = 12;
const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1";
@@ -1551,26 +1551,63 @@ function peerMarketStats(st,selfId){
function peerAdjustedSuggestion(s,peer){
const m=(peer&&peer[`${s.market_id}:${s.side}`])||{};
let boost=0,peerNote="";
- if(m.same&&m.samePnl>50){boost+=Math.min(8,2+m.same*1.5);peerNote=`peer confirmation from ${m.names.slice(0,3).join(", ")}`;}
- if(m.same&&m.samePnl< -50){boost-=Math.min(10,3+m.same*2);peerNote=`peer warning: same-side holders are losing`;}
+ if(m.same){
+ boost-=Math.min(14,4+m.same*3);
+ peerNote=`diversity guard: ${m.names.slice(0,3).join(", ")} already hold this side`;
+ }
+ if(m.same&&m.samePnl< -50){boost-=Math.min(8,2+m.same*1.5);peerNote=`peer warning: same-side holders are losing`;}
if(m.opposite&&m.oppositePnl>50){boost-=Math.min(12,4+m.opposite*2);peerNote=`peer warning: opposite-side holders are winning`;}
- if(m.bestRet>8&&m.same){boost+=3;peerNote=peerNote||`leaderboard winner also owns this side`;}
return Object.assign({},s,{peer_boost:boost,peer_note:peerNote,peer_conviction:+(Number(s.conviction||0)+boost).toFixed(1)});
}
+function addPortfolioMarketsToSet(set,p){
+ (p&&p.positions||[]).forEach(pos=>{const id=pos.market_id||pos.asset;if(id)set.add(String(id));});
+ return set;
+}
+function occupiedStrategyMarkets(st,selfId=null){
+ const ids=new Set();
+ AGENTS.filter(a=>a.kind==="strategy").forEach(a=>{
+ if(a.id===selfId)return;
+ addPortfolioMarketsToSet(ids,st.agents&&st.agents[a.id]);
+ });
+ return ids;
+}
+function overlapKeeperScore(pos,cfg){
+ const conviction=Number(pos.conviction||cfg.minConv||50);
+ const pnlBoost=Math.max(-8,Math.min(8,Number(pos.unrealized_pnl||0)/75));
+ const valueBoost=Math.min(5,Number(pos.value||pos.cost||0)/1000);
+ return conviction+pnlBoost+valueBoost;
+}
+function reduceStrategyOverlap(st){
+ const groups={};
+ AGENTS.filter(a=>a.kind==="strategy").forEach(cfg=>{
+ const p=st.agents&&st.agents[cfg.id]; if(!p)return;
+ (p.positions||[]).forEach(pos=>{
+ const id=pos.market_id||pos.asset; if(!id)return;
+ if(!groups[id])groups[id]=[];
+ groups[id].push({cfg,p,pos,score:overlapKeeperScore(pos,cfg)});
+ });
+ });
+ 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)));
+ group.slice(1).forEach(({p,pos})=>closePosition(p,pos,"Overlap guard rotated this agent into a different market","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);
+ });
+}
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};
p.lastDecision=decision||null;
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)return [];
+ const avoid=avoidMarketIds||new Set();
const cands=rankedSugs.map(s=>peerAdjustedSuggestion(s,peerStats)).filter(s=>s.trade_ready&&(s.side==="YES"||s.side==="NO")&&s.peer_conviction>=d.minConv
&&s.conviction>=48&&s.entry_price>=0.08&&s.entry_price<=0.92
&&Math.abs(s.edge)>=MIN_ENTRY_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)
- &&!hasPosition(p,s.market_id)&&!hasStoppedToday(p,s.market_id)&&(focus==="All"||!focus||s.category===focus))
+ &&!avoid.has(String(s.market_id))&&!hasPosition(p,s.market_id)&&!hasStoppedToday(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)));
- const avoid=avoidMarketIds||new Set();
- const ordered=cands.filter(s=>!avoid.has(s.market_id)).concat(cands.filter(s=>avoid.has(s.market_id)));
let opened=0,openedIds=[];
- for(const s of ordered){
+ for(const s of cands){
if(opened>=d.maxNew)break;
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)break;
const eq=equity(p),investable=p.cash-eq*d.reserve;
@@ -1594,7 +1631,7 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
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)}${s.peer_note?` (${s.peer_note})`:""}`});
- opened++;openedIds.push(s.market_id);
+ opened++;openedIds.push(String(s.market_id));
}
return openedIds;
}
@@ -1743,7 +1780,8 @@ async function runWhaleAgent(p,wallet,decision){
function runCopycat(p,leader,priceMap,candidate=null){
const reason=candidate?copycatReason(candidate):"watching the strategy board and rotating toward the account with the best momentum/MACD setup.";
const safeToCopy=candidate&&candidate.m.last>0&&candidate.m.recent>-3&&candidate.openPnl>-300;
- p.lastDecision={mode:safeToCopy?"Selective Momentum Copy":"Copycat Guard",reason:safeToCopy?reason:`No strategy is healthy enough to copy aggressively, so I am protecting cash instead. ${reason}`,minConv:0,maxNew:safeToCopy?leader.positions.length:0,maxFrac:0.1,reserve:0.12,
+ const copyLimit=4;
+ p.lastDecision={mode:safeToCopy?"Selective Momentum Copy":"Copycat Guard",reason:safeToCopy?reason:`No strategy is healthy enough to copy aggressively, so I am protecting cash instead. ${reason}`,minConv:0,maxNew:safeToCopy?Math.min(copyLimit,leader.positions.length):0,maxFrac:0.07,reserve:0.12,
copyScore:candidate?+candidate.score.toFixed(2):null,
macd:candidate?+candidate.m.macd.toFixed(2):null,
momentum:candidate?+candidate.m.recent.toFixed(2):null};
@@ -1764,13 +1802,15 @@ function runCopycat(p,leader,priceMap,candidate=null){
}else keep.push(pos);
}
p.positions=keep;
- const leaderPicks=leader.positions.filter(x=>(x.unrealized_pnl||0)>=-50&&(x.current_price||0)>0.05&&(x.current_price||0)<0.95);
+ const leaderPicks=leader.positions.filter(x=>(x.unrealized_pnl||0)>=-50&&(x.current_price||0)>0.05&&(x.current_price||0)<0.95)
+ .sort((a,b)=>((b.unrealized_pnl||0)/(b.cost||1))-((a.unrealized_pnl||0)/(a.cost||1))||((b.conviction||0)-(a.conviction||0))||((b.value||b.cost)-(a.value||a.cost)))
+ .slice(0,copyLimit);
const book=leaderPicks.reduce((s,x)=>s+(x.value||x.cost),0)||1;
for(const lp of [...leaderPicks].sort((a,b)=>(b.value||b.cost)-(a.value||a.cost))){
if(hasPosition(p,lp.market_id)||hasStoppedToday(p,lp.market_id))continue;
- const eq=equity(p),investable=p.cash-eq*0.05; if(investable<=50)break;
+ const eq=equity(p),investable=p.cash-eq*0.12; if(investable<=50)break;
const weight=(lp.value||lp.cost)/book;
- const stake=Math.min(eq*Math.min(0.1,weight),investable);
+ const stake=Math.min(eq*Math.min(0.07,weight),investable);
if(stake<50)continue;
const entry=lp.current_price; if(entry<=0||entry>=1)continue;
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
@@ -1889,11 +1929,17 @@ async function backtestWeek(st){
}
const sugs=generateSuggestions(snaps);
const priceMap={}; snaps.forEach(s=>priceMap[s.id]={yes_price:s.yes_price,no_price:s.no_price});
- const claimedMarkets=new Set();
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
const p=st.agents[cfg.id];
markToMarket(p,priceMap,cfg,{policyExits:true});
- openPositions(p,cfg,cfg.rank(sugs),focus,null,claimedMarkets,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
+ }
+ reduceStrategyOverlap(st);
+ const claimedMarkets=new Set();
+ for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
+ const p=st.agents[cfg.id];
+ const occupied=occupiedStrategyMarkets(st,cfg.id);
+ claimedMarkets.forEach(id=>occupied.add(id));
+ openPositions(p,cfg,cfg.rank(sugs),focus,null,occupied,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
recordSnapshot(p);
}
const copyPick=chooseCopycatLeader(st);
@@ -1941,13 +1987,19 @@ async function runDailyCycle(){
setStatus("strategy agents trading…",true);
const preBoard=AGENTS.filter(a=>a.kind==="strategy").map(a=>({id:a.id,eq:equity(st.agents[a.id])})).sort((x,y)=>y.eq-x.eq);
const leaderEq=preBoard[0]?preBoard[0].eq:STARTING_BALANCE;
- const claimedMarkets=new Set();
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
const p=st.agents[cfg.id];
markToMarket(p,priceMap,cfg,{policyExits:true});
+ }
+ reduceStrategyOverlap(st);
+ const claimedMarkets=new Set();
+ for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
+ const p=st.agents[cfg.id];
const rank=preBoard.findIndex(x=>x.id===cfg.id)+1||preBoard.length;
const decision=adaptiveDecision(cfg,p,rank,preBoard.length,leaderEq);
- openPositions(p,cfg,cfg.rank(sugs),focus,decision,claimedMarkets,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
+ const occupied=occupiedStrategyMarkets(st,cfg.id);
+ claimedMarkets.forEach(id=>occupied.add(id));
+ openPositions(p,cfg,cfg.rank(sugs),focus,decision,occupied,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id));
recordSnapshot(p);
}
// 2) copycat shadows the in-house agent with the strongest MACD/momentum score