Compare commits

...

9 Commits

Author SHA1 Message Date
Theodore Song 49888c7b56 Fix agent hold rules and chart artifacts 2026-07-26 17:39:39 -04:00
Theodore Song e36ef12e6a Add evidence based agent scoring 2026-07-26 16:55:21 -04:00
Theodore Song 29f9dcf29b Reduce agent trade overlap 2026-07-26 16:39:30 -04:00
Theodore Song 1f1e0c39b8 Fill suggestion list with broad watch ideas 2026-07-26 16:29:23 -04:00
Theodore Song b5fed7980f Expand suggestions with scout ideas 2026-07-26 16:23:54 -04:00
Theodore Song 4b3e03a353 Use keyset pagination for full market scans 2026-07-26 16:15:48 -04:00
Theodore Song 734b989072 Expand active market scan and suggestions 2026-07-26 16:07:08 -04:00
Theodore Song 8d69619dad Tighten agent entry and recovery rules 2026-07-26 15:52:34 -04:00
Theodore Song 018d1d9294 Add SMTP support for trade emails 2026-07-26 14:13:34 -04:00
6 changed files with 480 additions and 114 deletions
+7 -1
View File
@@ -85,11 +85,17 @@ CUSTOMER_SUPPORT_EMAIL=
RISK_ADMIN_WALLET=
# Trade email alerts
# Use either Resend directly or a webhook from Zapier/Make/another email automation.
# Use Resend, SMTP, or a webhook from Zapier/Make/another email automation.
RESEND_API_KEY=
TRADE_EMAIL_FROM=Poly Arena <alerts@yourdomain.com>
TRADE_EMAIL_REPLY_TO=
TRADE_EMAIL_WEBHOOK_URL=
SMTP_HOST=
SMTP_PORT=
SMTP_SECURE=true
SMTP_USER=
SMTP_PASS=
SMTP_FROM=
# Agent chat
OPENAI_API_KEY=
+147 -7
View File
@@ -9,6 +9,7 @@ import {
updateRealPositionMark,
updateTradeTicketStatus,
} from "./_db.js";
import nodemailer from "nodemailer";
const REQUIRED_ENV = [
["PRODUCTION_APP_URL", "Production app URL"],
@@ -173,6 +174,28 @@ function stackStatus() {
});
}
function tradeEmailStatus() {
const resend = Boolean(process.env.RESEND_API_KEY);
const webhook = Boolean(process.env.TRADE_EMAIL_WEBHOOK_URL);
const smtpMissing = ["SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASS"].filter((key) => !process.env[key]);
const smtp = smtpMissing.length === 0;
const provider = resend ? "resend" : (smtp ? "smtp" : (webhook ? "webhook" : null));
return {
configured: Boolean(provider),
provider,
options: {
resend: { configured: resend, missing: resend ? [] : ["RESEND_API_KEY"] },
smtp: { configured: smtp, missing: smtpMissing },
webhook: { configured: webhook, missing: webhook ? [] : ["TRADE_EMAIL_WEBHOOK_URL"] },
},
missing_any_one_of: provider ? [] : [
"RESEND_API_KEY",
"or SMTP_HOST + SMTP_PORT + SMTP_USER + SMTP_PASS",
"or TRADE_EMAIL_WEBHOOK_URL",
],
};
}
function liveTradingReady() {
const requiredConfigured = providerStatus().every((x) => x.configured);
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
@@ -214,6 +237,7 @@ function baseStatus() {
: "Live trading is locked until eligibility, payments, wallet/deposit-wallet signing, Polymarket CLOB credentials, audit storage, monitoring, and LIVE_TRADING_ENABLED=true are configured.",
providers,
provider_stack: stackStatus(),
trade_email: tradeEmailStatus(),
personal_requirements: personal,
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
@@ -336,6 +360,31 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
return { status: 200, body: { ok: true, provider: "resend", id: data?.id || null, sent: events.length } };
}
if (process.env.SMTP_HOST && process.env.SMTP_PORT && process.env.SMTP_USER && process.env.SMTP_PASS) {
const port = Number(process.env.SMTP_PORT);
const secure = String(process.env.SMTP_SECURE || "").toLowerCase() === "true" || port === 465;
const from = process.env.TRADE_EMAIL_FROM || process.env.SMTP_FROM || process.env.SMTP_USER;
const replyTo = process.env.TRADE_EMAIL_REPLY_TO || process.env.CUSTOMER_SUPPORT_EMAIL || undefined;
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port,
secure,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const info = await transporter.sendMail({
from,
to: email,
replyTo,
subject: payload.subject,
text: payload.text,
html: payload.html,
});
return { status: 200, body: { ok: true, provider: "smtp", id: info?.messageId || null, sent: events.length } };
}
if (process.env.TRADE_EMAIL_WEBHOOK_URL) {
const response = await fetch(process.env.TRADE_EMAIL_WEBHOOK_URL, {
method: "POST",
@@ -350,7 +399,8 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
status: 501,
body: {
ok: false,
error: "Email alerts need RESEND_API_KEY plus TRADE_EMAIL_FROM, or TRADE_EMAIL_WEBHOOK_URL, in Vercel environment variables.",
error: "Email alerts need a delivery provider in Vercel: RESEND_API_KEY, or SMTP_HOST + SMTP_PORT + SMTP_USER + SMTP_PASS, or TRADE_EMAIL_WEBHOOK_URL.",
trade_email: tradeEmailStatus(),
},
};
}
@@ -600,6 +650,83 @@ function validateCapitalAction(action) {
return errors;
}
function stripTags(value) {
return String(value || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}
function decodeXml(value) {
return String(value || "")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, "\"")
.replace(/&#39;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">");
}
function cleanMarketContextText(value, max = 220) {
return String(value || "").replace(/\s+/g, " ").trim().slice(0, max);
}
function contextQuery(market) {
const q = cleanMarketContextText(market?.question, 140)
.replace(/\bwill\b/ig, "")
.replace(/\?/g, "")
.replace(/\s+/g, " ")
.trim();
const cat = cleanMarketContextText(market?.category, 40);
if (cat === "Sports") return `${q} injury lineup odds`;
if (cat === "Politics") return `${q} poll election news`;
if (cat === "Crypto") return `${q} crypto market news`;
if (cat === "Economy") return `${q} economy fed inflation news`;
if (cat === "Pop Culture") return `${q} entertainment latest`;
return `${q} latest news`;
}
async function fetchNewsContext(market) {
const query = contextQuery(market);
if (!query || query.length < 8) return { source_count: 0, latest_title: "", query };
const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=en-US&gl=US&ceid=US:en`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 900);
try {
const response = await fetch(url, {
headers: { "User-Agent": "PolyArenaContext/1.0" },
signal: controller.signal,
});
if (!response.ok) return { source_count: 0, latest_title: "", query };
const xml = await response.text();
const items = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].slice(0, 5);
const titles = items.map((item) => {
const match = item[1].match(/<title><!\[CDATA\[([\s\S]*?)\]\]><\/title>|<title>([\s\S]*?)<\/title>/);
return cleanMarketContextText(decodeXml(stripTags(match ? (match[1] || match[2]) : "")), 160);
}).filter(Boolean);
return { source_count: titles.length, latest_title: titles[0] || "", query };
} catch (err) {
return { source_count: 0, latest_title: "", query, error: err?.name === "AbortError" ? "timeout" : "unavailable" };
} finally {
clearTimeout(timer);
}
}
async function handleMarketContext(body, res) {
const markets = (Array.isArray(body.markets) ? body.markets : []).slice(0, 120).map((market) => ({
id: cleanMarketContextText(market?.id, 80),
question: cleanMarketContextText(market?.question, 220),
category: cleanMarketContextText(market?.category, 60),
tags: Array.isArray(market?.tags) ? market.tags.slice(0, 6).map((tag) => cleanMarketContextText(tag, 40)) : [],
})).filter((market) => market.id && market.question);
const signals = {};
const batchSize = 24;
for (let i = 0; i < markets.length; i += batchSize) {
const batch = markets.slice(i, i + batchSize);
const results = await Promise.all(batch.map(fetchNewsContext));
results.forEach((signal, idx) => {
signals[batch[idx].id] = signal;
});
}
return res.status(200).json({ ok: true, count: Object.keys(signals).length, signals });
}
export default async function handler(req, res) {
res.setHeader("Cache-Control", "no-store");
@@ -627,12 +754,21 @@ export default async function handler(req, res) {
if (body.action === "trade_email") {
const events = normalizeTradeEmailEvents(body.events);
const result = await sendTradeEmail({
to: body.to,
events,
appUrl: body.app_url,
test: Boolean(body.test),
});
let result;
try {
result = await sendTradeEmail({
to: body.to,
events,
appUrl: body.app_url,
test: Boolean(body.test),
});
} catch (err) {
return res.status(502).json({
ok: false,
error: err?.response || err?.message || "Email provider failed to send the alert.",
trade_email: tradeEmailStatus(),
});
}
if (result.body?.ok) {
await recordAuditEvent("TRADE_EMAIL_ALERT_SENT", {
to_domain: String(body.to || "").split("@")[1] || null,
@@ -648,6 +784,10 @@ export default async function handler(req, res) {
return handleAgentChat(body, res);
}
if (body.action === "market_context") {
return handleMarketContext(body, res);
}
if (body.action === "ticket") {
const ticket = normalizeTicket(body);
const ticketErrors = validateTicket(ticket);
+3 -2
View File
@@ -6,7 +6,7 @@ const SUG_KEY = "pma_suggestions_v5";
const PAPER_KEY = "pma_paper_accounts_v1";
const LIVE_KEY = "pma_live_readiness_v1";
const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "copycat", "whale1", "whale2", "whale3", "whale4"];
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 120, paperHistory: 120, paperSnapshots: 120, audit: 120 };
const LIMITS = { closed: 80, history: 160, snapshots: 240, suggestions: 900, paperHistory: 120, paperSnapshots: 120, audit: 120 };
async function readJsonBlob() {
const blob = await get(STATE_PATH, { access: "private" });
@@ -51,7 +51,8 @@ function compactSuggestion(s) {
market_id: s.market_id, question: s.question, event: s.event, url: s.url, category: s.category,
clob_yes: s.clob_yes, clob_no: s.clob_no, yes_price: s.yes_price, no_price: s.no_price,
fair_value: s.fair_value, edge: s.edge, side: s.side, entry_price: s.entry_price,
conviction: s.conviction, volume: s.volume, volume_24hr: s.volume_24hr,
conviction: s.conviction, volume: s.volume, volume_24hr: s.volume_24hr, liquidity: s.liquidity,
trade_ready: s.trade_ready, watch_only: s.watch_only,
days_to_resolution: s.days_to_resolution, drivers: s.drivers, rationale: s.rationale,
};
}
+312 -104
View File
@@ -739,7 +739,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</section>
<footer>
Build agent-email-filters · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
Build hold-fix · 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>
@@ -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 = 6;
const SUGGESTION_ENGINE_VERSION = 14;
const FOCUS_KEY = "pma_focus_v1";
const VIEW_KEY = "pma_view_v1";
const PF_SORT_KEY = "pma_portfolio_sort_v1";
@@ -782,7 +782,7 @@ const PERSONAL_MODE = new URLSearchParams(location.search).get("personal") === "
const PERSONAL_USER_ID = "local-readiness-user";
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY,LIVE_KEY,EMAIL_ALERT_KEY];
const SYNC_LIMITS={closed:80,history:160,snapshots:240,suggestions:120,paperHistory:120,paperSnapshots:120,audit:120};
const SYNC_LIMITS={closed:80,history:160,snapshots:240,suggestions:900,paperHistory:120,paperSnapshots:120,audit:120};
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff",Copy:"#22d3ee"};
const DATA_API = "https://data-api.polymarket.com";
@@ -796,6 +796,7 @@ let LIVE_MARKET_OFFSET = 0;
let LIVE_MARKET_QUERY = "";
let LIVE_BACKEND_STATUS = null;
let LIVE_POLICY_STATUS = null;
let TRADE_EMAIL_STATUS = null;
let PERSONAL_TICKETS = [];
let REAL_PORTFOLIO = {positions:[],fills:[]};
let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]};
@@ -833,29 +834,37 @@ async function initProviderConfig(){
}
}catch(e){}
}
async function refreshTradeEmailStatus(){
try{
const r=await fetch("/api/live",{cache:"no-store"});
const d=await r.json();
TRADE_EMAIL_STATUS=d&&d.trade_email?d.trade_email:null;
}catch(e){TRADE_EMAIL_STATUS=null;}
renderEmailAlerts();
}
/* ---------- the ten competing agents ----------
Each shares the same frequently refreshed suggestions but ranks/sizes them differently. */
const AGENTS = [
{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.",
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
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.",
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:3, maxFrac:0.09, minConv:54, kelly:0.20},
{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.",
rank:(s)=>[...s].sort((a,b)=>(b.volume_24hr*(b.conviction+20))-(a.volume_24hr*(a.conviction+20))),
blurb:"Chases markets where attention is accelerating, but only after the move still leaves positive net edge. It weights conviction by fresh 24-hour volume and evidence quality so it is less likely to buy a move after the useful repricing already happened.",
rank:(s)=>[...s].sort((a,b)=>(b.volume_24hr*(b.conviction+20)*Math.max(.4,Math.abs(b.net_edge||b.edge||0)*18))-(a.volume_24hr*(a.conviction+20)*Math.max(.4,Math.abs(a.net_edge||a.edge||0)*18))),
maxNew:4, maxFrac:0.10, minConv:52, kelly:0.24},
{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.",
rank:(s)=>[...s].filter(x=>x.entry_price>=0.6).sort((a,b)=>b.entry_price-a.entry_price||b.conviction-a.conviction),
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 only when net edge and evidence quality survive the stricter checks.",
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:3, maxFrac:0.095, minConv:50, kelly:0.22},
{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.",
rank:(s)=>[...s].filter(x=>x.entry_price<=0.4).sort((a,b)=>a.entry_price-b.entry_price||b.conviction-a.conviction),
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.",
rank:(s)=>[...s].filter(x=>x.entry_price<=0.4).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:3, maxFrac:0.055, minConv:56, kelly:0.12},
{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.",
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across high-conviction, evidence-backed ideas while avoiding crowded overlap and weak net-edge fillers.",
rank:(s)=>[...s].sort((a,b)=>(b.conviction+Number(b.evidence_score||0)*15+Math.abs(b.net_edge||b.edge||0)*55)-(a.conviction+Number(a.evidence_score||0)*15+Math.abs(a.net_edge||a.edge||0)*55)),
maxNew:6, maxFrac:0.04, minConv:50, kelly:0.12, flat:true},
{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."},
@@ -939,15 +948,33 @@ function normalizeMarket(raw){
}
async function fetchMarkets(pages=Infinity,perPage=100,onProgress=null){
const all=[];
const maxPages=Number.isFinite(pages)?pages:250;
const maxPages=Number.isFinite(pages)?pages:MAX_ACTIVE_MARKET_PAGES;
const seen=new Set();
const useKeyset=!Number.isFinite(pages)||pages>20;
let cursor=null;
for(let i=0;i<maxPages;i++){
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",
limit:String(perPage),offset:String(i*perPage),order:"volume24hr",ascending:"false"});
const r=await fetch(`${GAMMA}/markets?${params}`);
limit:String(perPage),order:"volume24hr",ascending:"false"});
if(useKeyset){
if(cursor)params.set("after_cursor",cursor);
}else{
params.set("offset",String(i*perPage));
}
const r=await fetch(`${GAMMA}/markets${useKeyset?"/keyset":""}?${params}`);
if(!r.ok){if(i===0)throw new Error("Polymarket API "+r.status);break;}
const raw=await r.json(); if(!raw.length)break; all.push(...raw);
const page=await r.json();
const raw=useKeyset?(page.markets||[]):page;
if(!raw.length)break;
let added=0;
for(const m of raw){
const id=String(m.id||"");
if(id&&seen.has(id))continue;
if(id)seen.add(id);
all.push(m);added++;
}
if(useKeyset)cursor=page.next_cursor||null;
if(onProgress)onProgress(all.length);
if(raw.length<perPage)break;
if(raw.length<perPage||added===0||(useKeyset&&!cursor))break;
}
return all.map(normalizeMarket).filter(m=>m&&m.question);
}
@@ -957,44 +984,109 @@ async function fetchMarketPrice(id){
}
/* ---------- Analysis engine ---------- */
const W={LIQ:0.25,MOM:0.25,MIS:0.30,TIME:0.20};
const MIN_VOLUME=12000;
const MIN_LIQUIDITY=900;
const MIN_ENTRY_EDGE=0.022;
const MIN_ENTRY_DAYS=2.5;
const EDGE_SCALE=0.11;
const MAX_STRATEGY_POSITIONS=22;
const SUGGESTION_TOTAL=180;
const SUGGESTION_PER_CATEGORY=40;
const VISIBLE_SUGGESTIONS=120;
const W={LIQ:0.28,MOM:0.22,MIS:0.34,TIME:0.16};
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_DAYS=3.0;
const EDGE_SCALE=0.13;
const MAX_STRATEGY_POSITIONS=16;
const MAX_ACTIVE_MARKET_PAGES=1000;
const SUGGESTION_TOTAL=900;
const SUGGESTION_PER_CATEGORY=180;
const VISIBLE_SUGGESTIONS=900;
const REAL_WORLD_SIGNAL_LIMIT=120;
const clamp=(x,a,b)=>Math.max(a,Math.min(b,x));
function liquiditySignal(m){const vol=Math.min(1,Math.log10(m.volume+1)/6.0);const liq=Math.min(1,Math.log10(m.liquidity+1)/5.3);return 0.6*vol+0.4*liq;}
function momentumSignal(m){const da=m.volume_1wk?m.volume_1wk/7:0;if(da<=0)return m.volume_24hr>0?0.3:0;return clamp((m.volume_24hr/da-0.5)/2.0,0,1);}
function fairValue(m){const p=m.yes_price,mom=momentumSignal(m),liq=liquiditySignal(m);
if(p>0.92)return Math.min(0.995,p+0.04*liq*(1-p)*4);
if(p<0.08)return Math.max(0.005,p-0.20*p);
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 analyzeMarket(m){
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 liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution);
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);
let side,entry,rationale;
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<-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.`;}
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.038,minVol:18000,minLiq:1400,minEvidence:0.47,uncertainty:0.004,label:"sports needs fresh event context"},
Crypto:{minEdge:0.035,minVol:20000,minLiq:1600,minEvidence:0.46,uncertainty:0.003,label:"crypto allows faster trend reaction"},
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"});
}
function frictionPenalty(m){
const liq=liquiditySignal(m);
const price=Math.min(m.yes_price,m.no_price);
const thinPenalty=0.016*(1-liq);
const tailPenalty=price<0.12?0.007:0;
const lowLiqPenalty=m.liquidity<MIN_LIQUIDITY?0.008:0;
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);
}
function textEvidenceSignal(m,external={}){
const text=`${m.question||""} ${(m.tags||[]).join(" ")}`.toLowerCase();
let evidence=0.42;
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");}
if(/\b(bitcoin|btc|ethereum|eth|solana|crypto|etf|fed|cpi|inflation|rates?)\b/.test(text)){evidence+=0.08;drivers.push("macro/crypto context");}
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.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||""};
}
async function fetchRealWorldSignals(markets){
const selected=[...markets].sort((a,b)=>(b.volume_24hr-a.volume_24hr)||(b.volume-a.volume)).slice(0,REAL_WORLD_SIGNAL_LIMIT);
try{
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
action:"market_context",
markets:selected.map(m=>({id:m.id,question:m.question,category:m.category,tags:m.tags})),
})});
const d=await r.json().catch(()=>({}));
if(!r.ok||!d.ok)return {};
return d.signals||{};
}catch(e){return {};}
}
function fairValue(m){const p=m.yes_price,mom=momentumSignal(m),liq=liquiditySignal(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 directional=mom>0.62?(mom-0.62)*0.08:(mom<0.26?(mom-0.26)*0.035:0);
const liquidBump=(liq-0.55)*0.012;
return clamp(p+directional+liquidBump,0.03,0.97);}
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,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);
if(edge<MIN_SCOUT_EDGE)return null;
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 tradeReady=absNet>=minEdge&&m.volume>=Math.max(MIN_VOLUME,policy.minVol)&&m.liquidity>=Math.max(MIN_LIQUIDITY,policy.minLiq)&&evidence.score>=policy.minEvidence;
let side=edgeYes>=0?"YES":"NO",entry=side==="YES"?p:m.no_price,rationale;
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.`;}
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.`;}
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("notable price/value gap");
if(mis>0.4)drivers.push("net price/value gap after costs");
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),
side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,
net_edge:netEdge,friction:+friction.toFixed(4),chase_penalty:+chase.toFixed(4),evidence_score:+evidence.score.toFixed(2),
quality:tradeReady?"EV+":"watch",
side,entry_price:+entry.toFixed(4),conviction,volume:m.volume,volume_24hr:m.volume_24hr,liquidity:m.liquidity,
trade_ready:tradeReady,
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,rationale};
}
const pct=(x)=>Math.round(x*100)+"%";
@@ -1003,18 +1095,44 @@ function suggestionBand(s){
if(s.entry_price>0.65)return "favorite";
return "balanced";
}
function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTION_PER_CATEGORY){
const actionable=markets.map(analyzeMarket).filter(Boolean).filter(a=>a.side!=="HOLD").sort((a,b)=>b.conviction-a.conviction);
function marketWatchSuggestion(m,realWorldSignals={}){
if(!m||!m.question)return null;
const p=m.yes_price,fair=fairValue(m),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";
const entry=side==="YES"?p:m.no_price;
const conviction=+((0.36*liq+0.28*mom+0.18*timing+0.10*Math.min(1,Math.abs(netEdge)/EDGE_SCALE)+0.08*evidence.score)*100).toFixed(1);
const drivers=["broad market scan"];
if(m.volume>MIN_VOLUME)drivers.push("meaningful total volume");
if(m.volume_24hr>500)drivers.push("fresh 24h activity");
if(m.liquidity>MIN_LIQUIDITY)drivers.push("usable liquidity");
drivers.push(...evidence.drivers.slice(0,2));
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),quality:"watch",
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.`};
}
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);
const byCat={},byBucket={},seen=new Set(),out=[];
const add=(a,bucketCap=3)=>{
const add=(a,bucketCap=3,ignoreCategoryCap=false)=>{
if(out.length>=total||seen.has(a.market_id))return false;
const c=a.category||"Other",bucket=`${c}:${a.side}:${suggestionBand(a)}`;
if((byCat[c]||0)>=perCategory||(byBucket[bucket]||0)>=bucketCap)return false;
if((!ignoreCategoryCap&&(byCat[c]||0)>=perCategory)||(byBucket[bucket]||0)>=bucketCap)return false;
byCat[c]=(byCat[c]||0)+1;byBucket[bucket]=(byBucket[bucket]||0)+1;seen.add(a.market_id);out.push(a);
return true;
};
for(const a of actionable)add(a,3); // first pass: category, side, and price-band diversity
for(const a of actionable)add(a,999); // second pass: fill remaining slots with the best leftovers
const watchlist=markets.map(m=>marketWatchSuggestion(m,realWorldSignals)).filter(Boolean)
.filter(a=>!seen.has(a.market_id))
.sort((a,b)=>(b.volume_24hr-a.volume_24hr)||(b.volume-a.volume)||(b.conviction-a.conviction));
for(const a of watchlist)add(a,999,true); // final pass: show broad market coverage even when no trade edge is present
return out.sort((a,b)=>b.conviction-a.conviction);
}
@@ -1058,7 +1176,9 @@ function compactSuggestionForSync(s){
market_id:s.market_id,question:s.question,event:s.event,url:s.url,category:s.category,
clob_yes:s.clob_yes,clob_no:s.clob_no,yes_price:s.yes_price,no_price:s.no_price,
fair_value:s.fair_value,edge:s.edge,side:s.side,entry_price:s.entry_price,
conviction:s.conviction,volume:s.volume,volume_24hr:s.volume_24hr,
net_edge:s.net_edge,friction:s.friction,chase_penalty:s.chase_penalty,evidence_score:s.evidence_score,quality:s.quality,
conviction:s.conviction,volume:s.volume,volume_24hr:s.volume_24hr,liquidity:s.liquidity,
trade_ready:s.trade_ready,watch_only:s.watch_only,
days_to_resolution:s.days_to_resolution,drivers:s.drivers,rationale:s.rationale,
};
}
@@ -1094,7 +1214,7 @@ function applySyncItems(items){
const compact=compactSyncItems(items);
try{SYNC_KEYS.forEach(k=>{if(compact[k]!=null)localStorage.setItem(k,compact[k]);});return true;}
catch(e){
const tighter=compactSyncItems(items,{closed:30,history:60,snapshots:90,suggestions:60,paperHistory:60,paperSnapshots:60,audit:60});
const tighter=compactSyncItems(items,{closed:30,history:60,snapshots:90,suggestions:300,paperHistory:60,paperSnapshots:60,audit:60});
try{SYNC_KEYS.forEach(k=>{if(tighter[k]!=null)localStorage.setItem(k,tighter[k]);});toast("Loaded a compact mobile state.");return true;}
catch(err){setStatus("storage full",false);toast("This device's browser storage is full. Open in a normal tab or clear site data.");return false;}
}
@@ -1267,8 +1387,8 @@ function adaptiveDecision(cfg,p,rank,total,leaderEq){
mode="Defend";minConv+=3;maxNew=Math.max(2,maxNew-1);maxFrac*=0.88;reserve=0.10;
reason="leading the race, so it protects the lead without going completely inactive.";
}else if(trail>6){
mode="Selective Chase";minConv-=2;maxNew+=1;maxFrac*=1.10;reserve=0.055;
reason="far behind the leader, so it widens its search and takes slightly larger catch-up shots.";
mode="Selective Catch-Up";minConv+=2;maxNew=Math.max(2,maxNew);maxFrac*=0.94;reserve=0.085;
reason="behind the leader, so it tries to recover through cleaner edges instead of forcing lower-quality catch-up trades.";
}else if(trend>1.5){
mode="Measured Press";minConv-=1;maxNew+=1;maxFrac*=1.08;reserve=0.055;
reason="recent momentum is positive, so it presses the advantage while risk caps stay active.";
@@ -1282,8 +1402,8 @@ function adaptiveDecision(cfg,p,rank,total,leaderEq){
reason="down more than 18%, so it stops chasing, keeps more cash, and only allows one high-conviction recovery trade.";
}
if(emo.mood==="impatient"||emo.mood==="frustrated"){
minConv-=2;maxNew+=1;maxFrac*=1.08;reserve=Math.max(0.04,reserve-0.015);
reason+=` Emotion layer: ${emo.label.toLowerCase()} makes it seek extra upside, but only inside the caps.`;
minConv+=2;maxNew=Math.max(1,maxNew-1);maxFrac*=0.92;reserve=Math.max(0.09,reserve);
reason+=` Emotion layer: ${emo.label.toLowerCase()} raises its standards so it does not turn frustration into bad entries.`;
}else if(emo.mood==="confident"){
maxNew+=1;maxFrac*=1.05;
reason+=` Emotion layer: confidence lets it press winners a little harder.`;
@@ -1438,13 +1558,13 @@ function daysHeld(pos){
return Math.max(0,(end-start)/86400000);
}
function exitReason(pos,fresh,analysis,cfg){
if(!analysis)return "Model can no longer score";
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`;
if(analysis.side==="HOLD")return "Edge faded";
if(analysis.side&&analysis.side!==pos.side)return `Model flipped to ${analysis.side}`;
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 minExit=Math.max(30,(cfg&&cfg.minConv?cfg.minConv:45)-8);
if(analysis.conviction<minExit)return `Conviction faded to ${Math.round(analysis.conviction)}`;
if((pos.unrealized_pnl||0)<0&&analysis.conviction<minExit&&net<MIN_ENTRY_EDGE*0.25)return `Conviction faded to ${Math.round(analysis.conviction)}`;
return null;
}
function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
@@ -1491,31 +1611,71 @@ 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 cands=rankedSugs.map(s=>peerAdjustedSuggestion(s,peerStats)).filter(s=>(s.side==="YES"||s.side==="NO")&&s.peer_conviction>=d.minConv
&&Math.abs(s.edge)>=MIN_ENTRY_EDGE&&(s.days_to_resolution==null||s.days_to_resolution>=MIN_ENTRY_DAYS)
&&!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)));
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.net_edge!=null?s.net_edge: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)
&&(s.evidence_score==null||s.evidence_score>=0.50)
&&!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)));
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;
if(investable<=50)break;
let frac;
if(cfg.flat){frac=d.maxFrac;}
else{const base=(s.peer_conviction/100)*Math.min(1,Math.abs(s.edge)/EDGE_SCALE);frac=Math.min(d.maxFrac,cfg.kelly*base);}
else{const base=(s.peer_conviction/100)*Math.min(1,Math.abs(s.net_edge!=null?s.net_edge:s.edge)/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);
@@ -1528,11 +1688,11 @@ function openPositions(p,cfg,rankedSugs,focus,decision,avoidMarketIds,peerStats=
token_id:(s.side==="YES"?s.clob_yes:s.clob_no)||null,
entry_price:+entry.toFixed(4),current_price:+entry.toFixed(4),cost,value:cost,
original_shares:shares,original_cost:cost,unrealized_pnl:0,conviction:s.peer_conviction,category:s.category,opened_at:cycleIso(),url:s.url||"",
peer_note:s.peer_note||"",
peer_note:s.peer_note||"",entry_reason:s.rationale||"",net_edge:s.net_edge,evidence_score:s.evidence_score,friction:s.friction,chase_penalty:s.chase_penalty,quality:s.quality,
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);
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++;openedIds.push(String(s.market_id));
}
return openedIds;
}
@@ -1655,8 +1815,8 @@ async function runWhaleAgent(p,wallet,decision){
}
}
p.positions=keep;
const maxNew=Math.min(d.maxNew||6,6),maxFrac=Math.min(d.maxFrac||0.12,0.16),reserve=Math.max(d.reserve||0.12,0.10);
const targets=[...tps].filter(t=>t.curPrice>=0.06&&t.curPrice<=0.94).sort((a,b)=>b.value-a.value).slice(0,maxNew); // copy their biggest bets, adapted by stance
const maxNew=Math.min(d.maxNew||6,4),maxFrac=Math.min(d.maxFrac||0.10,0.12),reserve=Math.max(d.reserve||0.14,0.12);
const targets=[...tps].filter(t=>t.curPrice>=0.12&&t.curPrice<=0.88&&t.value>=20).sort((a,b)=>b.value-a.value).slice(0,maxNew); // copy their strongest liquid bets, adapted by stance
const book=targets.reduce((s,t)=>s+t.value,0)||1;
for(const t of targets){
if(hasAsset(p,t.asset)||hasStoppedToday(p,t.asset))continue;
@@ -1681,7 +1841,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};
@@ -1702,13 +1863,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);
@@ -1827,11 +1990,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);
@@ -1862,8 +2031,10 @@ async function runDailyCycle(){
SNAP_TS=nowIso();
setStatus("fetching markets…",true);
const markets=await fetchMarkets(Infinity,100,count=>setStatus(`fetching all active markets (${count})…`,true));
setStatus("analyzing…",true);
const sugs=generateSuggestions(markets);
setStatus("checking real-world context…",true);
const realWorldSignals=await fetchRealWorldSignals(markets);
setStatus("analyzing expected value…",true);
const sugs=generateSuggestions(markets,SUGGESTION_TOTAL,SUGGESTION_PER_CATEGORY,realWorldSignals);
saveSuggestions(sugs,markets.length);
st=loadState();
const emailOffsets=agentHistoryOffsets(st);
@@ -1879,13 +2050,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
@@ -1974,9 +2151,15 @@ function renderEmailAlerts(){
<label class="chip ${cfg.agents&&cfg.agents[a.id]?"active":""}" style="cursor:pointer;border-color:${cfg.agents&&cfg.agents[a.id]?a.color:""}">
<input type="checkbox" data-email-agent="${esc(a.id)}" ${cfg.agents&&cfg.agents[a.id]?"checked":""} style="display:none"> ${a.emoji} ${esc(a.name)}
</label>`).join("");
const providerLine=TRADE_EMAIL_STATUS
? (TRADE_EMAIL_STATUS.configured
? `Email backend ready via ${TRADE_EMAIL_STATUS.provider}.`
: `Email backend missing: add Resend, SMTP, or webhook env vars in Vercel.`)
: "Checking email backend...";
const sentLine=cfg.last_sent_at?` Last sent ${new Date(cfg.last_sent_at).toLocaleString()}.`:"";
status.textContent=cfg.last_error
? `Last error: ${cfg.last_error}`
: (cfg.last_sent_at?`Last sent ${new Date(cfg.last_sent_at).toLocaleString()}.`:"Send a digest whenever selected agents open, close, stop out, or take gains during a cycle.");
: `${providerLine}${sentLine} Sends a digest whenever selected agents trade during a cycle.`;
const readAlertForm=()=>{
const email=input.value.trim();
const events={};
@@ -2011,6 +2194,7 @@ function renderEmailAlerts(){
equity:STARTING_BALANCE,
return_pct:0,
}],{test:true});
await refreshTradeEmailStatus();
pushCloudState(true);
};
document.querySelectorAll("[data-email-event],[data-email-agent]").forEach(el=>el.onchange=()=>{
@@ -2242,9 +2426,11 @@ function renderSuggestions(){
const focus=getFocus();
const pool=(focus==="All"?all:all.filter(s=>s.category===focus));
const filtered=pool.slice(0,VISIBLE_SUGGESTIONS);
const buyCount=all.filter(s=>s.trade_ready).length,watchCount=all.length-buyCount;
const evAvg=all.length?Math.round(all.reduce((s,x)=>s+Number(x.evidence_score||0),0)/all.length*100):0;
$("focusNote").textContent=focus==="All"
? `Scanned ${Number(data.market_count||0).toLocaleString()} active markets and kept ${all.length} scored ideas. Showing ${filtered.length}; agents trade from the full wider pool.`
: `Scanned ${Number(data.market_count||0).toLocaleString()} active markets. Showing ${filtered.length} of ${pool.length} ${focus} ideas; agents only open new ${focus} positions while this is selected.`;
? `Scanned ${Number(data.market_count||0).toLocaleString()} active markets and kept ${all.length} ideas (${buyCount} BUY, ${watchCount} WATCH). Showing ${filtered.length}; agents trade only EV+ ideas after liquidity, chase, and real-world evidence checks. Avg evidence ${evAvg}.`
: `Scanned ${Number(data.market_count||0).toLocaleString()} active markets. Showing ${filtered.length} of ${pool.length} ${focus} ideas; agents only open EV+ BUY ${focus} positions while this is selected.`;
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);
@@ -2255,7 +2441,7 @@ function renderSuggestions(){
<div class="q">${s.url?`<a class="market-title" href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.question)}</a>`:esc(s.question)}</div>
<div class="event">${esc(s.event||"")}</div>
<div class="sug-row">
<span class="pill ${s.side}">BUY ${s.side}</span>
<span class="pill ${s.side}">${s.trade_ready?"BUY":"WATCH"} ${s.side}</span>
<span class="muted small">@ ${Math.round(s.entry_price*100)}¢</span>
<div class="conv-bar"><span style="width:${s.conviction}%"></span></div>
<span class="muted small">${Math.round(s.conviction)}</span>
@@ -2263,6 +2449,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>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>`:""}
@@ -2322,14 +2509,14 @@ function renderAgentTechnical(agentId){
let inner=`<rect x="${padL}" y="${padT}" width="${W-padL-padR}" height="${priceH-padT}" rx="12" fill="rgba(255,255,255,.025)" stroke="rgba(255,255,255,.08)"/>`;
for(let i=0;i<5;i++){const v=mn+(span*i/4),yy=y(v);inner+=`<line x1="${padL}" x2="${W-padR}" y1="${yy}" y2="${yy}" stroke="rgba(255,255,255,.06)"/><text x="${padL-10}" y="${yy+4}" text-anchor="end" fill="rgba(224,234,255,.62)" font-size="11">${fmtUSD(v)}</text>`;}
const w=Math.max(3,(W-padL-padR)/snaps.length*.48);
closes.forEach((c,i)=>{const o=i?closes[i-1]:c,hi=Math.max(o,c)*(1+0.003),lo=Math.min(o,c)*(1-0.003),up=c>=o,col=up?getComputedStyle(document.documentElement).getPropertyValue("--green").trim():getComputedStyle(document.documentElement).getPropertyValue("--red").trim();const xx=x(i);
inner+=`<line x1="${xx}" x2="${xx}" y1="${y(hi)}" y2="${y(lo)}" stroke="${col}" stroke-width="1.2"/><rect x="${xx-w/2}" y="${y(Math.max(o,c))}" width="${w}" height="${Math.max(2,Math.abs(y(c)-y(o)))}" fill="${col}" opacity=".85"/>`;});
closes.forEach((c,i)=>{const o=i?closes[i-1]:c,up=c>=o,col=up?"#7c8cff":"#fb7185";const xx=x(i);
inner+=`<rect x="${xx-w/2}" y="${y(Math.max(o,c))}" width="${w}" height="${Math.max(2,Math.abs(y(c)-y(o)))}" rx="1.5" fill="${col}" opacity=".68"/>`;});
const path=(arr)=>arr.map((v,i)=>`${i?"L":"M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
inner+=`<path d="${path(ma5)}" fill="none" stroke="#fbbf24" stroke-width="2.4"/><path d="${path(ma10)}" fill="none" stroke="#38d2e6" stroke-width="2.4"/>`;
inner+=`<line x1="${padL}" x2="${W-padR}" y1="${my(0)}" y2="${my(0)}" stroke="rgba(255,255,255,.18)"/>`;
hist.forEach((v,i)=>{const xx=x(i),yy=my(Math.max(0,v)),hh=Math.abs(my(v)-my(0));inner+=`<rect x="${xx-w/2}" y="${v>=0?yy:my(0)}" width="${w}" height="${Math.max(1,hh)}" fill="${v>=0?"#34d399":"#fb7185"}" opacity=".7"/>`;});
hist.forEach((v,i)=>{const xx=x(i),yy=my(Math.max(0,v)),hh=Math.abs(my(v)-my(0));inner+=`<rect x="${xx-w/2}" y="${v>=0?yy:my(0)}" width="${w}" height="${Math.max(1,hh)}" fill="${v>=0?"#94a3b8":"#fb7185"}" opacity=".55"/>`;});
for(const tick of chartTicks(times,4)){inner+=`<text x="${x(tick.i)}" y="${H-14}" text-anchor="middle" fill="rgba(224,234,255,.7)" font-size="11">${chartDateLabel(tick.t,true)}</text>`;}
inner+=`<text x="${padL}" y="18" fill="rgba(224,234,255,.7)" font-size="12">${cfg.name} equity candles · MA5 yellow · MA10 cyan · MACD bars</text>`;
inner+=`<text x="${padL}" y="18" fill="rgba(224,234,255,.7)" font-size="12">${cfg.name} equity candles · MA5 yellow · MA10 cyan · neutral MACD bars</text>`;
svg.innerHTML=inner;
const last=closes.length-1,trend=ma5[last]>=ma10[last]?"bullish":"bearish",mom=hist[last]>=0?"positive":"negative";
readout.innerHTML=`<div class="invest-num"><div class="k">Trend</div><div class="v">${trend}</div></div>
@@ -2396,6 +2583,15 @@ function reportPositionLine(p){
if(best===worst)return `Current focus: ${best.question.slice(0,58)}${best.question.length>58?"...":""}, marked ${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}.`;
return `Best mark: ${best.question.slice(0,42)}${best.question.length>42?"...":""} (${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}). Watch item: ${worst.question.slice(0,42)}${worst.question.length>42?"...":""} (${fmtUSD(worst.unrealized_pnl||0)}).`;
}
function attributionLine(pos){
if(!pos)return "No single trade explains the move.";
const bits=[];
if(pos.entry_reason)bits.push(pos.entry_reason);
if(pos.net_edge!=null)bits.push(`entry net edge ${((Math.abs(pos.net_edge))*100).toFixed(1)}c`);
if(pos.evidence_score!=null)bits.push(`evidence ${Math.round(Number(pos.evidence_score||0)*100)}`);
if(pos.friction!=null)bits.push(`friction ${((Number(pos.friction||0))*100).toFixed(1)}c`);
return bits.length?bits.join(" · "):"This older position was opened before detailed attribution was stored.";
}
function dailyPerformanceExplanation(row){
const snaps=(row.p.snapshots||[]).slice().sort((a,b)=>snapTime(a)-snapTime(b));
const last=snaps[snaps.length-1];
@@ -2419,11 +2615,11 @@ function dailyPerformanceExplanation(row){
: "There were no major realized closes in the past day.";
if(delta>0.25){
const openText=best?`The strongest open position helping it is '${shortQuestion(best.question)}' at ${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}.`:"It benefited mostly from cash discipline and closed-trade results rather than a single open winner.";
return `Past 24h: up ${delta.toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
return `Past 24h: up ${delta.toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} Attribution: ${attributionLine(best)} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
}
if(delta<-0.25){
const openText=worst?`The biggest open drag is '${shortQuestion(worst.question)}' at ${fmtUSD(worst.unrealized_pnl||0)}.`:"The decline came more from broad mark-downs or realized losses than one obvious open position.";
return `Past 24h: down ${Math.abs(delta).toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
return `Past 24h: down ${Math.abs(delta).toFixed(2)} return points (${fmtUSD(equityDelta)}). ${openText} Attribution: ${attributionLine(worst)} ${closedText} Trade behavior: ${actions}. Technical pressure: ${macdText}.`;
}
const openText=best&&worst&&best!==worst
? `Best open mark is '${shortQuestion(best.question,36)}' (${best.unrealized_pnl>=0?"+":""}${fmtUSD(best.unrealized_pnl||0)}) while the weakest is '${shortQuestion(worst.question,36)}' (${fmtUSD(worst.unrealized_pnl||0)}).`
@@ -2708,6 +2904,7 @@ function renderPositions(positions){
<div class="pq">${title}</div>
<div class="sub">${p.category?`<span class="cat-badge" style="background:${col}22;color:${col}">${esc(p.category)}</span> `:""}${p.shares} ${p.side} @ ${Math.round(p.entry_price*100)}¢ → ${Math.round(p.current_price*100)}¢</div>
${p.peer_note?`<div class="sub">Peer read: ${esc(p.peer_note)}</div>`:""}
${p.net_edge!=null||p.evidence_score!=null?`<div class="sub">Entry quality: ${p.net_edge!=null?`net edge ${((Math.abs(p.net_edge))*100).toFixed(1)}c`:"net edge n/a"} · ${p.evidence_score!=null?`evidence ${Math.round(Number(p.evidence_score||0)*100)}`:"evidence n/a"}</div>`:""}
<div class="sub">${esc(stopLossLabel(p))}</div>
<div class="sub">${esc(gainStopLabel(p))}</div>
<div class="sub"><a class="market-link" href="${esc(positionUrl(p))}" target="_blank" rel="noopener">Open exact market</a></div>
@@ -3076,20 +3273,25 @@ async function loadPaperMarketPage(reset=false,loadAll=false){
try{
if(reset){PAPER_SEARCH_RESULTS=[];PAPER_MARKET_OFFSET=0;}
let loaded=0,rawCount=0,keepGoing=true;
const limit=100,maxPages=loadAll?250:1;
const limit=100,maxPages=loadAll?MAX_ACTIVE_MARKET_PAGES:1;
let cursor=null;
for(let page=0;page<maxPages&&keepGoing;page++){
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:String(limit),offset:String(PAPER_MARKET_OFFSET),order:"volume24hr",ascending:"false"});
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:String(limit),order:"volume24hr",ascending:"false"});
if(loadAll){if(cursor)params.set("after_cursor",cursor);}
else params.set("offset",String(PAPER_MARKET_OFFSET));
if(q)params.set("search",q);
const r=await fetch(`${GAMMA}/markets?${params}`);
const r=await fetch(`${GAMMA}/markets${loadAll?"/keyset":""}?${params}`);
if(!r.ok)throw new Error("market load failed");
const raw=await r.json();
const pageData=await r.json();
const raw=loadAll?(pageData.markets||[]):pageData;
rawCount+=raw.length;
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
PAPER_SEARCH_RESULTS=(PAPER_SEARCH_RESULTS||[]).concat(next);
PAPER_MARKET_OFFSET+=raw.length;
if(loadAll)cursor=pageData.next_cursor||null;
loaded+=next.length;
if(loadAll)setStatus(`loading active markets (${PAPER_SEARCH_RESULTS.length})`,true);
if(raw.length<limit)keepGoing=false;
if(raw.length<limit||(loadAll&&!cursor))keepGoing=false;
}
renderPaperTab();setStatus("up to date",false);
toast(loaded?`Loaded ${PAPER_SEARCH_RESULTS.length} active markets.`:rawCount?"No tradable binary markets found in this page.":"No more active markets found.");
@@ -3437,20 +3639,25 @@ async function loadLiveMarketPage(reset=false,loadAll=false){
try{
if(reset){LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;}
let loaded=0,rawCount=0,keepGoing=true;
const limit=100,maxPages=loadAll?250:1;
const limit=100,maxPages=loadAll?MAX_ACTIVE_MARKET_PAGES:1;
let cursor=null;
for(let page=0;page<maxPages&&keepGoing;page++){
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:String(limit),offset:String(LIVE_MARKET_OFFSET),order:"volume24hr",ascending:"false"});
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:String(limit),order:"volume24hr",ascending:"false"});
if(loadAll){if(cursor)params.set("after_cursor",cursor);}
else params.set("offset",String(LIVE_MARKET_OFFSET));
if(q)params.set("search",q);
const r=await fetch(`${GAMMA}/markets?${params}`);
const r=await fetch(`${GAMMA}/markets${loadAll?"/keyset":""}?${params}`);
if(!r.ok)throw new Error("market load failed");
const raw=await r.json();
const pageData=await r.json();
const raw=loadAll?(pageData.markets||[]):pageData;
rawCount+=raw.length;
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
LIVE_SEARCH_RESULTS=(LIVE_SEARCH_RESULTS||[]).concat(next);
LIVE_MARKET_OFFSET+=raw.length;
if(loadAll)cursor=pageData.next_cursor||null;
loaded+=next.length;
if(loadAll)setStatus(`loading active markets (${LIVE_SEARCH_RESULTS.length})`,true);
if(raw.length<limit)keepGoing=false;
if(raw.length<limit||(loadAll&&!cursor))keepGoing=false;
}
renderLiveMarkets();setStatus("up to date",false);
toast(loaded?`Loaded ${LIVE_SEARCH_RESULTS.length} active markets.`:rawCount?"No tradable binary markets found in this page.":"No more active markets found.");
@@ -3564,6 +3771,7 @@ window.addEventListener("hashchange",()=>showTab(location.hash.slice(1)));
/* ---------- Wire up ---------- */
applyPersonalMode();
initProviderConfig();
refreshTradeEmailStatus();
$("runBtn").addEventListener("click",async()=>{
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
try{const r=await runDailyCycle();const b=board();toast(r.busy?`Cycle already running · ${b[0].c.emoji} ${b[0].c.name} leads`:r.skipped?`Already ran this minute · ${b[0].c.emoji} ${b[0].c.name} leads`:`Cycle done · ${r.suggestions} ideas · ${b[0].c.emoji} ${b[0].c.name} leads`);renderAll();}
+10
View File
@@ -7,6 +7,7 @@
"dependencies": {
"@neondatabase/serverless": "^1.1.0",
"@vercel/blob": "2.5.0",
"nodemailer": "^9.0.3",
"svix": "^1.96.1"
}
},
@@ -222,6 +223,15 @@
"node": ">=6"
}
},
"node_modules/nodemailer": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+1
View File
@@ -2,6 +2,7 @@
"dependencies": {
"@neondatabase/serverless": "^1.1.0",
"@vercel/blob": "2.5.0",
"nodemailer": "^9.0.3",
"svix": "^1.96.1"
}
}