diff --git a/README.md b/README.md index 7aed22f..f94bf4f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ small samples toward neutral, caps sizing changes to 0.72x-1.28x, and reserves 15% of candidates for deterministic exploration so a stale regime cannot become permanent. +A separate walk-forward ledger records each trade-ready signal before its future +price is known, grades it at least 12 hours later, and combines that broad market +calibration with each agent's personal outcomes. This expands the learning sample +without backfilling future information into old decisions. + Paper accounts created with a password are also saved through the backend, so a user can log in from another device and see the same paper portfolio, activity, and value history. Passwordless paper accounts remain local-only. diff --git a/api/state.js b/api/state.js index ab9c53f..38a1751 100644 --- a/api/state.js +++ b/api/state.js @@ -10,6 +10,7 @@ const PAPER_KEY = "pma_paper_accounts_v1"; const LIVE_KEY = "pma_live_readiness_v1"; const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "catalyst", "reversal", "breakout", "tailalpha", "conviction"]; const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 900, paperHistory: 120, paperSnapshots: 120, audit: 120 }; +const SIGNAL_LEDGER_LIMITS = { pending: 300, outcomes: 500 }; function withBlobAuth(options = {}) { const token = process.env.BLOB_READ_WRITE_TOKEN; @@ -160,6 +161,12 @@ function compactAgentState(st) { for (const id of AGENT_IDS) { out.agents[id] = compactPortfolio(st.agents && st.agents[id]); } + if (st.signal_ledger && typeof st.signal_ledger === "object") { + out.signal_ledger = { + pending: Array.isArray(st.signal_ledger.pending) ? st.signal_ledger.pending.slice(-SIGNAL_LEDGER_LIMITS.pending) : [], + outcomes: Array.isArray(st.signal_ledger.outcomes) ? st.signal_ledger.outcomes.slice(-SIGNAL_LEDGER_LIMITS.outcomes) : [], + }; + } delete out.whales; delete out.copycatLeader; return out; diff --git a/index.html b/index.html index 9221ace..4d88d4a 100644 --- a/index.html +++ b/index.html @@ -910,6 +910,9 @@ const todayStr = () => {const p=partsInLocalTime();return `${p.year}-${p.month}- const RUN_INTERVAL_MS = 60000; const OFFLINE_ENTRY_MAX_AGE_MS = 90*60*1000; const OFFLINE_CACHE_MAX_AGE_MS = 24*60*60*1000; +const SIGNAL_EVAL_HOURS = 12; +const SIGNAL_LEDGER_PENDING_LIMIT = 300; +const SIGNAL_LEDGER_OUTCOME_LIMIT = 500; const currentCycleHour = () => {const p=partsInLocalTime();return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}|v${SUGGESTION_ENGINE_VERSION}`;}; function cycleHourFromIso(iso){ const d=new Date(iso); @@ -1258,7 +1261,7 @@ function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTI /* ---------- Multi-agent store ---------- */ function defaultPortfolio(){return {cash:STARTING_BALANCE,starting_balance:STARTING_BALANCE,positions:[],closed:[],history:[],snapshots:[],stopped:{},lastDecision:null};} -function 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 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,signal_ledger:defaultSignalLedger(),seeded:false};} function loadState(){ let st; try{st=JSON.parse(localStorage.getItem(AGENTS_KEY));}catch(e){st=null;} if(!st||!st.agents)st=defaultState(); @@ -1267,6 +1270,7 @@ function loadState(){ Object.keys(st.agents).forEach(id=>{if(!activeIds.has(id))delete st.agents[id];}); delete st.whales; delete st.copycatLeader; + if(!st.signal_ledger||typeof st.signal_ledger!=="object")st.signal_ledger=defaultSignalLedger(); AGENTS.forEach(a=>{ if(!st.agents[a.id])st.agents[a.id]=defaultPortfolio(); if(!st.agents[a.id].stopped)st.agents[a.id].stopped={}; @@ -1303,6 +1307,10 @@ function compactAgentStateForSync(st,limits=SYNC_LIMITS){ if(!st||typeof st!=="object")return st; const out=Object.assign({},st,{agents:{}}); AGENTS.forEach(a=>{out.agents[a.id]=compactPortfolioForSync(st.agents&&st.agents[a.id],limits)||defaultPortfolio();}); + if(st.signal_ledger&&typeof st.signal_ledger==="object")out.signal_ledger={ + pending:(st.signal_ledger.pending||[]).slice(-SIGNAL_LEDGER_PENDING_LIMIT), + outcomes:(st.signal_ledger.outcomes||[]).slice(-SIGNAL_LEDGER_OUTCOME_LIMIT), + }; delete out.whales; delete out.copycatLeader; return out; @@ -1540,6 +1548,49 @@ function learningFeatures(trade){ return [`signal:${trade.signal_type||"unknown"}`,`quality:${trade.quality||"unknown"}`, `category:${trade.category||"Other"}`,`side:${trade.side||"unknown"}`,`price:${entryBand(trade.entry_price)}`]; } +function defaultSignalLedger(){return {pending:[],outcomes:[]};} +function signalPrice(m,side){return side==="YES"?Number(m&&m.yes_price):Number(m&&m.no_price);} +function updateSignalLedger(st,markets,suggestions){ + const ledger=st.signal_ledger&&typeof st.signal_ledger==="object"?st.signal_ledger:defaultSignalLedger(); + ledger.pending=Array.isArray(ledger.pending)?ledger.pending:[];ledger.outcomes=Array.isArray(ledger.outcomes)?ledger.outcomes:[]; + const now=Date.now(),marketMap=Object.fromEntries((markets||[]).map(m=>[String(m.id),m])),stillPending=[]; + for(const item of ledger.pending){ + const started=new Date(item.observed_at||0).getTime(),ageHours=Number.isFinite(started)?(now-started)/3600000:Infinity; + const fresh=marketMap[String(item.market_id)],future=signalPrice(fresh,item.side); + if(ageHours>=SIGNAL_EVAL_HOURS&&Number.isFinite(future)&&future>=0&&future<=1){ + const outcome=clamp(future/Math.max(0.01,Number(item.entry_price||0.01))-1,-1,2); + ledger.outcomes.push(Object.assign({},item,{evaluated_at:nowIso(),horizon_hours:+ageHours.toFixed(1),future_price:+future.toFixed(4),return:+outcome.toFixed(4)})); + }else if(ageHours<=72)stillPending.push(item); + } + const existing=new Set(stillPending.map(x=>x.key)),bucket=Math.floor(now/(6*3600000)); + for(const s of (suggestions||[]).filter(x=>x.trade_ready)){ + const key=`${s.market_id}:${s.side}:${bucket}`;if(existing.has(key))continue; + existing.add(key);stillPending.push({key,market_id:String(s.market_id),observed_at:nowIso(),side:s.side,entry_price:Number(s.entry_price), + signal_type:s.signal_type||"unknown",quality:s.quality||"unknown",category:s.category||"Other",conviction:Number(s.conviction||0)}); + } + ledger.pending=stillPending.slice(-SIGNAL_LEDGER_PENDING_LIMIT); + ledger.outcomes=ledger.outcomes.slice(-SIGNAL_LEDGER_OUTCOME_LIMIT); + st.signal_ledger=ledger;return ledger; +} +function buildSignalCalibration(ledger){ + const buckets={}; + for(const outcome of (ledger&&ledger.outcomes)||[]){ + const age=Math.max(0,Date.now()-new Date(outcome.evaluated_at||0).getTime()),weight=Math.exp(-age/(30*86400000)); + const ret=clamp(Number(outcome.return||0),-1,2);if(!Number.isFinite(ret))continue; + learningFeatures(outcome).forEach(key=>{const b=buckets[key]||(buckets[key]={weight:0,sum:0,wins:0,count:0}); + b.weight+=weight;b.sum+=ret*weight;b.wins+=(ret>0?weight:0);b.count++;}); + } + const learned=Object.fromEntries(Object.entries(buckets).map(([key,b])=>[key,{samples:b.count,weight:+b.weight.toFixed(2), + score:+(b.sum/(b.weight+12)).toFixed(4),win_rate:b.weight?b.wins/b.weight:0}])); + return {version:SUGGESTION_ENGINE_VERSION,samples:((ledger&&ledger.outcomes)||[]).length,buckets:learned, + pending:((ledger&&ledger.pending)||[]).length}; +} +function calibratedOpportunity(s,calibration){ + const rows=learningFeatures(s).map(k=>calibration&&calibration.buckets&&calibration.buckets[k]).filter(Boolean); + const weight=rows.reduce((sum,r)=>sum+r.weight,0),score=rows.length?rows.reduce((sum,r)=>sum+r.score,0)/rows.length:0; + const confidence=weight/(weight+20),blocked=Number(calibration&&calibration.samples||0)>=12&&confidence>=0.35&&score<-0.04; + return {score:+score.toFixed(4),confidence:+confidence.toFixed(3),multiplier:+clamp(1+score*1.8,0.80,1.20).toFixed(3),allowed:!blocked}; +} function tradeReturnForLearning(trade,closed){ const basis=Math.max(1,Number(trade.original_cost||trade.cost||0)); const pnl=closed?Number(trade.realized_pnl||0):Number(trade.unrealized_pnl||0)+Number(trade.partial_realized_pnl||0); @@ -1581,13 +1632,14 @@ function stableExploration(agentId,marketId){ for(let i=0;i>>0)%100<15; } -function learnedOpportunity(cfg,p,s,profile=null){ +function learnedOpportunity(cfg,p,s,profile=null,calibration=null){ const model=profile||buildAdaptiveProfile(p),features=learningFeatures(s),rows=features.map(k=>model.buckets[k]).filter(Boolean); const weight=rows.reduce((sum,r)=>sum+r.weight,0),score=rows.length?rows.reduce((sum,r)=>sum+r.score,0)/rows.length:0; const confidence=weight/(weight+12),exploration=stableExploration(cfg.id,s.market_id); - const blocked=((model.samples>=4&&confidence>=0.32&&score<-0.045)||(model.samples>=8&&confidence>=0.45&&score<-0.035))&&!exploration; - return {score:+score.toFixed(4),confidence:+confidence.toFixed(3),multiplier:+clamp(1+score*2.4,0.72,1.28).toFixed(3), - exploration,allowed:!blocked,features}; + const market=calibratedOpportunity(s,calibration||{samples:0,buckets:{}}); + const blocked=(((model.samples>=4&&confidence>=0.32&&score<-0.045)||(model.samples>=8&&confidence>=0.45&&score<-0.035))||!market.allowed)&&!exploration; + return {score:+score.toFixed(4),confidence:+confidence.toFixed(3),market_score:market.score,market_confidence:market.confidence, + multiplier:+clamp((1+score*2.4)*market.multiplier,0.68,1.30).toFixed(3),exploration,allowed:!blocked,features}; } function emotionalState(ret,trail,trend,rank){ if(ret<-18)return {mood:"alarmed",urgency:0.95,label:"Crisis pressure"}; @@ -1598,7 +1650,7 @@ function emotionalState(ret,trail,trend,rank){ if(ret<0)return {mood:"restless",urgency:0.58,label:"Need progress"}; return {mood:"focused",urgency:0.50,label:"Focused"}; } -function adaptiveDecision(cfg,p,rank,total,leaderEq){ +function adaptiveDecision(cfg,p,rank,total,leaderEq,marketLearning=null){ const eq=equity(p),ret=(eq/p.starting_balance-1)*100,trail=((leaderEq||eq)-eq)/p.starting_balance*100; const positionValue=(p.positions||[]).reduce((sum,pos)=>sum+Number(pos.value||pos.shares*pos.current_price||0),0); const currentExposure=eq>0?positionValue/eq:0; @@ -1639,7 +1691,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=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,learning:profile, + 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,learning:profile,marketLearning:marketLearning||{samples:0,pending:0,buckets:{}}, currentExposure:+currentExposure.toFixed(3),targetExposure:+targetExposure.toFixed(3),minExposure:+minExposure.toFixed(3),belowFloor}; } const stopKey=(posOrId)=>typeof posOrId==="string"?posOrId:String(posOrId.asset||posOrId.market_id||""); @@ -1984,8 +2036,9 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats= } const avoid=avoidMarketIds||new Set(); const cands=rankedSugs.map(s=>{ - const peerAdjusted=peerAdjustedSuggestion(s,peerStats),learning=learnedOpportunity(cfg,p,peerAdjusted,learningProfile); + const peerAdjusted=peerAdjustedSuggestion(s,peerStats),learning=learnedOpportunity(cfg,p,peerAdjusted,learningProfile,d.marketLearning); return Object.assign({},peerAdjusted,{learning_score:learning.score,learning_confidence:learning.confidence, + market_learning_score:learning.market_score,market_learning_confidence:learning.market_confidence, learning_multiplier:learning.multiplier,learning_exploration:learning.exploration,learning_allowed:learning.allowed}); }).filter(s=>s.trade_ready&&agentAcceptsSuggestion(cfg,s)&&s.learning_allowed&&(s.side==="YES"||s.side==="NO") &&(cfg.aggressive?s.conviction>=d.minConv:s.peer_conviction>=d.minConv) @@ -2030,7 +2083,8 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats= 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,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, - learning_score:s.learning_score,learning_confidence:s.learning_confidence,learning_multiplier:s.learning_multiplier,learning_exploration:s.learning_exploration, + learning_score:s.learning_score,learning_confidence:s.learning_confidence,market_learning_score:s.market_learning_score,market_learning_confidence:s.market_learning_confidence, + learning_multiplier:s.learning_multiplier,learning_exploration:s.learning_exploration, 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)} · signal margin ${((Math.abs(s.net_edge!=null?s.net_edge:s.edge))*100).toFixed(1)}c · learned weight ${Number(s.learning_multiplier||1).toFixed(2)}x · evidence ${Math.round((s.evidence_score||0)*100)}${s.peer_note?` (${s.peer_note})`:""}`}); @@ -2190,6 +2244,8 @@ async function runDailyCycle(){ priceMap[id]=fresh||cachedById[String(id)]||null; } if(runMode==="live")saveMarketCache(analysisMarkets,sugs,priceMap); + if(runMode==="live")updateSignalLedger(st,analysisMarkets,sugs); + const marketLearning=buildSignalCalibration(st.signal_ledger); setStatus("ten strategy agents trading…",true); const preBoard=AGENTS.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; @@ -2204,7 +2260,7 @@ async function runDailyCycle(){ for(const cfg of strategyExecutionOrder(st)){ 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); + const decision=adaptiveDecision(cfg,p,rank,preBoard.length,leaderEq,marketLearning); const occupied=occupiedStrategyMarkets(st,cfg.id); claimedMarkets.forEach(id=>occupied.add(id)); openPositions(p,cfg,cfg.rank(cycleSuggestions),focus,decision,occupied,peerMarketStats(st,cfg.id)).forEach(id=>claimedMarkets.add(id)); @@ -2388,7 +2444,8 @@ function decisionSummary(p){ 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.`:""; const learning=d.learning?` Learning: ${d.learning.samples} completed v34+ trades, ${(d.learning.global_score*100).toFixed(2)}% shrunk expectancy${d.learning.best?`; strongest ${d.learning.best.feature.replace(":"," ")}`:""}${d.learning.worst?`; weakest ${d.learning.worst.feature.replace(":"," ")}`:""}.`:""; - return `${d.mode} mode: ${d.reason}${emotion} Limits now: ${d.maxNew} new trade${d.maxNew===1?"":"s"}, max ${(d.maxFrac*100).toFixed(1)}% per position${d.minConv?`, conviction ${d.minConv}+`:""}.${learning}${exposure}${allocation}${candidates}`; + const calibration=d.marketLearning?` Walk-forward calibration: ${d.marketLearning.samples||0} graded signals, ${d.marketLearning.pending||0} awaiting a future price.`:""; + return `${d.mode} mode: ${d.reason}${emotion} Limits now: ${d.maxNew} new trade${d.maxNew===1?"":"s"}, max ${(d.maxFrac*100).toFixed(1)}% per position${d.minConv?`, conviction ${d.minConv}+`:""}.${learning}${calibration}${exposure}${allocation}${candidates}`; } function renderAgentBrief(cfg,p,st){ const root=$("agentBrief"); if(!root)return; @@ -3974,8 +4031,15 @@ function runEngineSelfTest(){ for(let i=0;i<8;i++)learner.closed.push({strategy_version:34,signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42,original_cost:100, realized_pnl:-20,opened_at:hoursAgo(72+i),closed_at:closedAt}); const learningProfile=buildAdaptiveProfile(learner); - const learnedTrend=learnedOpportunity(AGENTS[0],learner,{market_id:"learn-trend",signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42},learningProfile); - const learnedReversal=learnedOpportunity(AGENTS[0],learner,{market_id:"learn-reversal",signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42},learningProfile); + const calibrationLedger=defaultSignalLedger(); + for(let i=0;i<16;i++)calibrationLedger.outcomes.push({signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42,return:0.12,evaluated_at:closedAt}); + for(let i=0;i<16;i++)calibrationLedger.outcomes.push({signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42,return:-0.12,evaluated_at:closedAt}); + const calibrationProfile=buildSignalCalibration(calibrationLedger); + const learnedTrend=learnedOpportunity(AGENTS[0],learner,{market_id:"learn-trend",signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42},learningProfile,calibrationProfile); + const learnedReversal=learnedOpportunity(AGENTS[0],learner,{market_id:"learn-reversal",signal_type:"reversal",quality:"reversal",category:"Sports",side:"NO",entry_price:0.42},learningProfile,calibrationProfile); + const ledgerState={signal_ledger:{pending:[{key:"ledger-test",market_id:"ledger-test",observed_at:hoursAgo(13),side:"YES",entry_price:0.40, + signal_type:"trend",quality:"confirmed",category:"Politics"}],outcomes:[]}}; + updateSignalLedger(ledgerState,[market({id:"ledger-test",yes_price:0.50,no_price:0.50})],[]); const lossLearner=defaultPortfolio(); for(let i=0;i<4;i++)lossLearner.closed.push({strategy_version:35,signal_type:"trend",quality:"confirmed",category:"Politics",side:"YES",entry_price:0.42,original_cost:100, realized_pnl:-50,opened_at:hoursAgo(72+i),closed_at:closedAt}); @@ -3994,6 +4058,8 @@ function runEngineSelfTest(){ 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), adaptation:{samples:learningProfile.samples,trendMultiplier:learnedTrend.multiplier,reversalMultiplier:learnedReversal.multiplier,learnsDirection:learnedTrend.multiplier>learnedReversal.multiplier, + calibrationSamples:calibrationProfile.samples,trendMarketScore:learnedTrend.market_score,reversalMarketScore:learnedReversal.market_score, + ledgerMaturesWithoutLookahead:ledgerState.signal_ledger.pending.length===0&&ledgerState.signal_ledger.outcomes.length===1&&ledgerState.signal_ledger.outcomes[0].return===0.25, blocksConfirmedLosingRegime:!lossOpportunity.allowed,containmentMode:lossDecision.mode,containmentMaxNew:lossDecision.maxNew}, offline:{fresh:offlineCachePolicy(30*60000),staleEntry:offlineCachePolicy(3*3600000),expired:offlineCachePolicy(25*3600000)}, exits:{youngConflict:exitReason(young,fresh,conflict,AGENTS[0]),matureConflict:exitReason(mature,fresh,conflict,AGENTS[0]),trailing:trailingProfitReason(trailing)},