mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-28 00:17:46 +00:00
Compare commits
9 Commits
86fa65f8f1
...
49888c7b56
| Author | SHA1 | Date | |
|---|---|---|---|
| 49888c7b56 | |||
| e36ef12e6a | |||
| 29f9dcf29b | |||
| 1f1e0c39b8 | |||
| b5fed7980f | |||
| 4b3e03a353 | |||
| 734b989072 | |||
| 8d69619dad | |||
| 018d1d9294 |
+7
-1
@@ -85,11 +85,17 @@ CUSTOMER_SUPPORT_EMAIL=
|
|||||||
RISK_ADMIN_WALLET=
|
RISK_ADMIN_WALLET=
|
||||||
|
|
||||||
# Trade email alerts
|
# 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=
|
RESEND_API_KEY=
|
||||||
TRADE_EMAIL_FROM=Poly Arena <alerts@yourdomain.com>
|
TRADE_EMAIL_FROM=Poly Arena <alerts@yourdomain.com>
|
||||||
TRADE_EMAIL_REPLY_TO=
|
TRADE_EMAIL_REPLY_TO=
|
||||||
TRADE_EMAIL_WEBHOOK_URL=
|
TRADE_EMAIL_WEBHOOK_URL=
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=
|
||||||
|
SMTP_SECURE=true
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASS=
|
||||||
|
SMTP_FROM=
|
||||||
|
|
||||||
# Agent chat
|
# Agent chat
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
|||||||
+147
-7
@@ -9,6 +9,7 @@ import {
|
|||||||
updateRealPositionMark,
|
updateRealPositionMark,
|
||||||
updateTradeTicketStatus,
|
updateTradeTicketStatus,
|
||||||
} from "./_db.js";
|
} from "./_db.js";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
const REQUIRED_ENV = [
|
const REQUIRED_ENV = [
|
||||||
["PRODUCTION_APP_URL", "Production app URL"],
|
["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() {
|
function liveTradingReady() {
|
||||||
const requiredConfigured = providerStatus().every((x) => x.configured);
|
const requiredConfigured = providerStatus().every((x) => x.configured);
|
||||||
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
|
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.",
|
: "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,
|
providers,
|
||||||
provider_stack: stackStatus(),
|
provider_stack: stackStatus(),
|
||||||
|
trade_email: tradeEmailStatus(),
|
||||||
personal_requirements: personal,
|
personal_requirements: personal,
|
||||||
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
|
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
|
||||||
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
|
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 } };
|
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) {
|
if (process.env.TRADE_EMAIL_WEBHOOK_URL) {
|
||||||
const response = await fetch(process.env.TRADE_EMAIL_WEBHOOK_URL, {
|
const response = await fetch(process.env.TRADE_EMAIL_WEBHOOK_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -350,7 +399,8 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
|
|||||||
status: 501,
|
status: 501,
|
||||||
body: {
|
body: {
|
||||||
ok: false,
|
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;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripTags(value) {
|
||||||
|
return String(value || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeXml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, "\"")
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/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) {
|
export default async function handler(req, res) {
|
||||||
res.setHeader("Cache-Control", "no-store");
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
|
||||||
@@ -627,12 +754,21 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
if (body.action === "trade_email") {
|
if (body.action === "trade_email") {
|
||||||
const events = normalizeTradeEmailEvents(body.events);
|
const events = normalizeTradeEmailEvents(body.events);
|
||||||
const result = await sendTradeEmail({
|
let result;
|
||||||
to: body.to,
|
try {
|
||||||
events,
|
result = await sendTradeEmail({
|
||||||
appUrl: body.app_url,
|
to: body.to,
|
||||||
test: Boolean(body.test),
|
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) {
|
if (result.body?.ok) {
|
||||||
await recordAuditEvent("TRADE_EMAIL_ALERT_SENT", {
|
await recordAuditEvent("TRADE_EMAIL_ALERT_SENT", {
|
||||||
to_domain: String(body.to || "").split("@")[1] || null,
|
to_domain: String(body.to || "").split("@")[1] || null,
|
||||||
@@ -648,6 +784,10 @@ export default async function handler(req, res) {
|
|||||||
return handleAgentChat(body, res);
|
return handleAgentChat(body, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.action === "market_context") {
|
||||||
|
return handleMarketContext(body, res);
|
||||||
|
}
|
||||||
|
|
||||||
if (body.action === "ticket") {
|
if (body.action === "ticket") {
|
||||||
const ticket = normalizeTicket(body);
|
const ticket = normalizeTicket(body);
|
||||||
const ticketErrors = validateTicket(ticket);
|
const ticketErrors = validateTicket(ticket);
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@ const SUG_KEY = "pma_suggestions_v5";
|
|||||||
const PAPER_KEY = "pma_paper_accounts_v1";
|
const PAPER_KEY = "pma_paper_accounts_v1";
|
||||||
const LIVE_KEY = "pma_live_readiness_v1";
|
const LIVE_KEY = "pma_live_readiness_v1";
|
||||||
const AGENT_IDS = ["value", "momentum", "favorite", "longshot", "diversifier", "copycat", "whale1", "whale2", "whale3", "whale4"];
|
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() {
|
async function readJsonBlob() {
|
||||||
const blob = await get(STATE_PATH, { access: "private" });
|
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,
|
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,
|
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,
|
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,
|
days_to_resolution: s.days_to_resolution, drivers: s.drivers, rationale: s.rationale,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+312
-104
@@ -739,7 +739,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<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>
|
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -766,7 +766,7 @@ const EXIT_STALE_DAYS = 10;
|
|||||||
const EXIT_RESOLUTION_DAYS = 2;
|
const EXIT_RESOLUTION_DAYS = 2;
|
||||||
const AGENTS_KEY = "pma_agents_v2";
|
const AGENTS_KEY = "pma_agents_v2";
|
||||||
const SUG_KEY = "pma_suggestions_v5";
|
const SUG_KEY = "pma_suggestions_v5";
|
||||||
const SUGGESTION_ENGINE_VERSION = 6;
|
const SUGGESTION_ENGINE_VERSION = 14;
|
||||||
const FOCUS_KEY = "pma_focus_v1";
|
const FOCUS_KEY = "pma_focus_v1";
|
||||||
const VIEW_KEY = "pma_view_v1";
|
const VIEW_KEY = "pma_view_v1";
|
||||||
const PF_SORT_KEY = "pma_portfolio_sort_v1";
|
const PF_SORT_KEY = "pma_portfolio_sort_v1";
|
||||||
@@ -782,7 +782,7 @@ const PERSONAL_MODE = new URLSearchParams(location.search).get("personal") === "
|
|||||||
const PERSONAL_USER_ID = "local-readiness-user";
|
const PERSONAL_USER_ID = "local-readiness-user";
|
||||||
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
|
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_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",
|
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
|
||||||
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff",Copy:"#22d3ee"};
|
"Pop Culture":"#c77dff",Other:"#94a1bb",All:"#7c8cff",Copy:"#22d3ee"};
|
||||||
const DATA_API = "https://data-api.polymarket.com";
|
const DATA_API = "https://data-api.polymarket.com";
|
||||||
@@ -796,6 +796,7 @@ let LIVE_MARKET_OFFSET = 0;
|
|||||||
let LIVE_MARKET_QUERY = "";
|
let LIVE_MARKET_QUERY = "";
|
||||||
let LIVE_BACKEND_STATUS = null;
|
let LIVE_BACKEND_STATUS = null;
|
||||||
let LIVE_POLICY_STATUS = null;
|
let LIVE_POLICY_STATUS = null;
|
||||||
|
let TRADE_EMAIL_STATUS = null;
|
||||||
let PERSONAL_TICKETS = [];
|
let PERSONAL_TICKETS = [];
|
||||||
let REAL_PORTFOLIO = {positions:[],fills:[]};
|
let REAL_PORTFOLIO = {positions:[],fills:[]};
|
||||||
let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]};
|
let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]};
|
||||||
@@ -833,29 +834,37 @@ async function initProviderConfig(){
|
|||||||
}
|
}
|
||||||
}catch(e){}
|
}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 ----------
|
/* ---------- the ten competing agents ----------
|
||||||
Each shares the same frequently refreshed suggestions but ranks/sizes them differently. */
|
Each shares the same frequently refreshed suggestions but ranks/sizes them differently. */
|
||||||
const AGENTS = [
|
const AGENTS = [
|
||||||
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
|
{id:"value", name:"Value Hunter", emoji:"🎯", color:"#7c8cff", kind:"strategy",
|
||||||
blurb:"Looks for the widest gap between the model's fair value and the current market price. It prefers trades where the crowd appears too pessimistic or too optimistic, then sizes positions moderately so one bad read does not dominate the portfolio.",
|
blurb:"Looks for the widest 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)=>b.conviction-a.conviction),
|
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},
|
maxNew:3, maxFrac:0.09, minConv:54, kelly:0.20},
|
||||||
{id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy",
|
{id:"momentum", name:"Momentum Chaser", emoji:"🚀", color:"#34d399", kind:"strategy",
|
||||||
blurb:"Chases markets where attention is accelerating. It weights conviction by fresh 24-hour volume, so it tends to enter fast-moving events with strong liquidity and recent trader interest.",
|
blurb:"Chases markets where attention is accelerating, 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))-(a.volume_24hr*(a.conviction+20))),
|
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},
|
maxNew:4, maxFrac:0.10, minConv:52, kelly:0.24},
|
||||||
{id:"favorite", name:"Favorite Backer", emoji:"🛡️", color:"#38d2e6", kind:"strategy",
|
{id:"favorite", name:"Favorite Backer", emoji:"🛡️", color:"#38d2e6", kind:"strategy",
|
||||||
blurb:"A lower-drama agent that only considers outcomes already priced as favorites. It tries to grind out steadier returns by backing high-probability markets when the model still sees a useful edge.",
|
blurb:"A lower-drama agent that only considers outcomes already priced as favorites. It tries to grind out steadier returns by backing high-probability markets 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-a.entry_price||b.conviction-a.conviction),
|
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},
|
maxNew:3, maxFrac:0.095, minConv:50, kelly:0.22},
|
||||||
{id:"longshot", name:"Longshot Hunter", emoji:"🎰", color:"#fbbf24", kind:"strategy",
|
{id:"longshot", name:"Longshot Hunter", emoji:"🎰", color:"#fbbf24", kind:"strategy",
|
||||||
blurb:"The swing-for-upside strategy. It hunts cheaper contracts that the model thinks are being ignored, accepting more volatility in exchange for the chance that one mispriced longshot rerates sharply.",
|
blurb:"The swing-for-upside strategy. It hunts cheaper contracts that the model thinks are being ignored, 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)=>a.entry_price-b.entry_price||b.conviction-a.conviction),
|
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},
|
maxNew:3, maxFrac:0.055, minConv:56, kelly:0.12},
|
||||||
{id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy",
|
{id:"diversifier", name:"The Diversifier", emoji:"🌐", color:"#c77dff", kind:"strategy",
|
||||||
blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across many high-conviction ideas to reduce single-market risk and show how the whole suggestion pool performs.",
|
blurb:"Builds a broad basket instead of making a few concentrated bets. It spreads smaller, flatter positions across high-conviction, evidence-backed ideas while avoiding crowded overlap and weak net-edge fillers.",
|
||||||
rank:(s)=>[...s].sort((a,b)=>b.conviction-a.conviction),
|
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},
|
maxNew:6, maxFrac:0.04, minConv:50, kelly:0.12, flat:true},
|
||||||
{id:"copycat", name:"The Copycat", emoji:"🐒", color:"#ec4899", kind:"copycat",
|
{id:"copycat", name:"The Copycat", emoji:"🐒", color:"#ec4899", kind:"copycat",
|
||||||
blurb:"A meta-agent that does not pick markets directly. It watches the in-house strategies, scores their return trend, MACD, recent momentum, and drawdown risk, then mirrors the agent most likely to keep improving rather than blindly chasing the current leader."},
|
blurb:"A meta-agent that does not pick markets directly. It watches the in-house strategies, scores their return trend, MACD, recent momentum, and drawdown risk, then mirrors the agent most likely to keep improving rather than blindly chasing the current leader."},
|
||||||
@@ -939,15 +948,33 @@ function normalizeMarket(raw){
|
|||||||
}
|
}
|
||||||
async function fetchMarkets(pages=Infinity,perPage=100,onProgress=null){
|
async function fetchMarkets(pages=Infinity,perPage=100,onProgress=null){
|
||||||
const all=[];
|
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++){
|
for(let i=0;i<maxPages;i++){
|
||||||
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",
|
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",
|
||||||
limit:String(perPage),offset:String(i*perPage),order:"volume24hr",ascending:"false"});
|
limit:String(perPage),order:"volume24hr",ascending:"false"});
|
||||||
const r=await fetch(`${GAMMA}/markets?${params}`);
|
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;}
|
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(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);
|
return all.map(normalizeMarket).filter(m=>m&&m.question);
|
||||||
}
|
}
|
||||||
@@ -957,44 +984,109 @@ async function fetchMarketPrice(id){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Analysis engine ---------- */
|
/* ---------- Analysis engine ---------- */
|
||||||
const W={LIQ:0.25,MOM:0.25,MIS:0.30,TIME:0.20};
|
const W={LIQ:0.28,MOM:0.22,MIS:0.34,TIME:0.16};
|
||||||
const MIN_VOLUME=12000;
|
const MIN_VOLUME=18000;
|
||||||
const MIN_LIQUIDITY=900;
|
const MIN_LIQUIDITY=1400;
|
||||||
const MIN_ENTRY_EDGE=0.022;
|
const MIN_SCOUT_VOLUME=1500;
|
||||||
const MIN_ENTRY_DAYS=2.5;
|
const MIN_SCOUT_LIQUIDITY=150;
|
||||||
const EDGE_SCALE=0.11;
|
const MIN_SCOUT_EDGE=0.006;
|
||||||
const MAX_STRATEGY_POSITIONS=22;
|
const MIN_ENTRY_EDGE=0.032;
|
||||||
const SUGGESTION_TOTAL=180;
|
const MIN_ENTRY_DAYS=3.0;
|
||||||
const SUGGESTION_PER_CATEGORY=40;
|
const EDGE_SCALE=0.13;
|
||||||
const VISIBLE_SUGGESTIONS=120;
|
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));
|
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 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 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);
|
function categoryPolicy(cat){
|
||||||
if(p>0.92)return Math.min(0.995,p+0.04*liq*(1-p)*4);
|
return ({
|
||||||
if(p<0.08)return Math.max(0.005,p-0.20*p);
|
Politics:{minEdge:0.040,minVol:22000,minLiq:1800,minEvidence:0.48,uncertainty:0.006,label:"politics needs outside confirmation"},
|
||||||
return clamp(p+(mom-0.5)*0.06,0.02,0.98);}
|
Sports:{minEdge:0.038,minVol:18000,minLiq:1400,minEvidence:0.47,uncertainty:0.004,label:"sports needs fresh event context"},
|
||||||
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;}
|
Crypto:{minEdge:0.035,minVol:20000,minLiq:1600,minEvidence:0.46,uncertainty:0.003,label:"crypto allows faster trend reaction"},
|
||||||
function analyzeMarket(m){
|
Economy:{minEdge:0.044,minVol:22000,minLiq:1800,minEvidence:0.50,uncertainty:0.007,label:"macro markets need stronger evidence"},
|
||||||
if(m.volume<MIN_VOLUME||m.liquidity<MIN_LIQUIDITY)return null;
|
"Pop Culture":{minEdge:0.040,minVol:16000,minLiq:1300,minEvidence:0.46,uncertainty:0.005,label:"culture markets need attention confirmation"},
|
||||||
const p=m.yes_price,fair=fairValue(m),edgeYes=fair-p,edge=Math.abs(edgeYes);
|
Other:{minEdge:0.048,minVol:24000,minLiq:1900,minEvidence:0.50,uncertainty:0.009,label:"uncategorized markets need the highest proof"},
|
||||||
const liq=liquiditySignal(m),mom=momentumSignal(m),timing=timingSignal(m.days_to_resolution);
|
})[cat||"Other"]||({minEdge:0.048,minVol:24000,minLiq:1900,minEvidence:0.50,uncertainty:0.009,label:"uncategorized markets need the highest proof"});
|
||||||
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);
|
function frictionPenalty(m){
|
||||||
let side,entry,rationale;
|
const liq=liquiditySignal(m);
|
||||||
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.`;}
|
const price=Math.min(m.yes_price,m.no_price);
|
||||||
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.`;}
|
const thinPenalty=0.016*(1-liq);
|
||||||
else{side="HOLD";entry=p;rationale=`Price ${pct(p)} is close to model fair value ${pct(fair)}; no clear edge — watch only.`;}
|
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=[];
|
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(liq>0.6)drivers.push("deep/liquid market");
|
||||||
if(mom>0.6)drivers.push("strong fresh volume (24h surge)");
|
if(mom>0.6)drivers.push("strong fresh volume (24h surge)");
|
||||||
if(timing>0.8)drivers.push("resolves in a good window");
|
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");
|
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,
|
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,
|
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),
|
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};
|
days_to_resolution:m.days_to_resolution!=null?+m.days_to_resolution.toFixed(1):null,drivers,rationale};
|
||||||
}
|
}
|
||||||
const pct=(x)=>Math.round(x*100)+"%";
|
const pct=(x)=>Math.round(x*100)+"%";
|
||||||
@@ -1003,18 +1095,44 @@ function suggestionBand(s){
|
|||||||
if(s.entry_price>0.65)return "favorite";
|
if(s.entry_price>0.65)return "favorite";
|
||||||
return "balanced";
|
return "balanced";
|
||||||
}
|
}
|
||||||
function generateSuggestions(markets,total=SUGGESTION_TOTAL,perCategory=SUGGESTION_PER_CATEGORY){
|
function marketWatchSuggestion(m,realWorldSignals={}){
|
||||||
const actionable=markets.map(analyzeMarket).filter(Boolean).filter(a=>a.side!=="HOLD").sort((a,b)=>b.conviction-a.conviction);
|
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 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;
|
if(out.length>=total||seen.has(a.market_id))return false;
|
||||||
const c=a.category||"Other",bucket=`${c}:${a.side}:${suggestionBand(a)}`;
|
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);
|
byCat[c]=(byCat[c]||0)+1;byBucket[bucket]=(byBucket[bucket]||0)+1;seen.add(a.market_id);out.push(a);
|
||||||
return true;
|
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,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
|
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);
|
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,
|
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,
|
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,
|
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,
|
days_to_resolution:s.days_to_resolution,drivers:s.drivers,rationale:s.rationale,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1094,7 +1214,7 @@ function applySyncItems(items){
|
|||||||
const compact=compactSyncItems(items);
|
const compact=compactSyncItems(items);
|
||||||
try{SYNC_KEYS.forEach(k=>{if(compact[k]!=null)localStorage.setItem(k,compact[k]);});return true;}
|
try{SYNC_KEYS.forEach(k=>{if(compact[k]!=null)localStorage.setItem(k,compact[k]);});return true;}
|
||||||
catch(e){
|
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;}
|
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;}
|
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;
|
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.";
|
reason="leading the race, so it protects the lead without going completely inactive.";
|
||||||
}else if(trail>6){
|
}else if(trail>6){
|
||||||
mode="Selective Chase";minConv-=2;maxNew+=1;maxFrac*=1.10;reserve=0.055;
|
mode="Selective Catch-Up";minConv+=2;maxNew=Math.max(2,maxNew);maxFrac*=0.94;reserve=0.085;
|
||||||
reason="far behind the leader, so it widens its search and takes slightly larger catch-up shots.";
|
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){
|
}else if(trend>1.5){
|
||||||
mode="Measured Press";minConv-=1;maxNew+=1;maxFrac*=1.08;reserve=0.055;
|
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.";
|
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.";
|
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"){
|
if(emo.mood==="impatient"||emo.mood==="frustrated"){
|
||||||
minConv-=2;maxNew+=1;maxFrac*=1.08;reserve=Math.max(0.04,reserve-0.015);
|
minConv+=2;maxNew=Math.max(1,maxNew-1);maxFrac*=0.92;reserve=Math.max(0.09,reserve);
|
||||||
reason+=` Emotion layer: ${emo.label.toLowerCase()} makes it seek extra upside, but only inside the caps.`;
|
reason+=` Emotion layer: ${emo.label.toLowerCase()} raises its standards so it does not turn frustration into bad entries.`;
|
||||||
}else if(emo.mood==="confident"){
|
}else if(emo.mood==="confident"){
|
||||||
maxNew+=1;maxFrac*=1.05;
|
maxNew+=1;maxFrac*=1.05;
|
||||||
reason+=` Emotion layer: confidence lets it press winners a little harder.`;
|
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);
|
return Math.max(0,(end-start)/86400000);
|
||||||
}
|
}
|
||||||
function exitReason(pos,fresh,analysis,cfg){
|
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(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(daysHeld(pos)>=EXIT_STALE_DAYS)return `Stale exit after ${Math.floor(daysHeld(pos))} days`;
|
||||||
if(analysis.side==="HOLD")return "Edge faded";
|
if(!analysis)return null;
|
||||||
if(analysis.side&&analysis.side!==pos.side)return `Model flipped to ${analysis.side}`;
|
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);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
|
function markToMarket(p,priceMap,cfg=null,{policyExits=false}={}){
|
||||||
@@ -1491,31 +1611,71 @@ function peerMarketStats(st,selfId){
|
|||||||
function peerAdjustedSuggestion(s,peer){
|
function peerAdjustedSuggestion(s,peer){
|
||||||
const m=(peer&&peer[`${s.market_id}:${s.side}`])||{};
|
const m=(peer&&peer[`${s.market_id}:${s.side}`])||{};
|
||||||
let boost=0,peerNote="";
|
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){
|
||||||
if(m.same&&m.samePnl< -50){boost-=Math.min(10,3+m.same*2);peerNote=`peer warning: same-side holders are losing`;}
|
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.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)});
|
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){
|
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};
|
const d=decision||{minConv:cfg.minConv,maxNew:cfg.maxNew,maxFrac:cfg.maxFrac,reserve:0.05};
|
||||||
p.lastDecision=decision||null;
|
p.lastDecision=decision||null;
|
||||||
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)return [];
|
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 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=[];
|
let opened=0,openedIds=[];
|
||||||
for(const s of ordered){
|
for(const s of cands){
|
||||||
if(opened>=d.maxNew)break;
|
if(opened>=d.maxNew)break;
|
||||||
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)break;
|
if((p.positions||[]).length>=MAX_STRATEGY_POSITIONS)break;
|
||||||
const eq=equity(p),investable=p.cash-eq*d.reserve;
|
const eq=equity(p),investable=p.cash-eq*d.reserve;
|
||||||
if(investable<=50)break;
|
if(investable<=50)break;
|
||||||
let frac;
|
let frac;
|
||||||
if(cfg.flat){frac=d.maxFrac;}
|
if(cfg.flat){frac=d.maxFrac;}
|
||||||
else{const base=(s.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)frac*=0.82;
|
||||||
if(s.peer_boost>0&&decision&&decision.urgency>0.65)frac*=1.08;
|
if(s.peer_boost>0&&decision&&decision.urgency>0.65)frac*=1.08;
|
||||||
let stake=Math.min(eq*frac,investable);
|
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,
|
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,
|
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||"",
|
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:{}});
|
gain_stops:{},stop_losses:{}});
|
||||||
p.history.push({date:logDay(),action:"OPEN",question:s.question,side:s.side,
|
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})`:""}`});
|
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(s.market_id);
|
opened++;openedIds.push(String(s.market_id));
|
||||||
}
|
}
|
||||||
return openedIds;
|
return openedIds;
|
||||||
}
|
}
|
||||||
@@ -1655,8 +1815,8 @@ async function runWhaleAgent(p,wallet,decision){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.positions=keep;
|
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 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.06&&t.curPrice<=0.94).sort((a,b)=>b.value-a.value).slice(0,maxNew); // copy their biggest bets, adapted by stance
|
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;
|
const book=targets.reduce((s,t)=>s+t.value,0)||1;
|
||||||
for(const t of targets){
|
for(const t of targets){
|
||||||
if(hasAsset(p,t.asset)||hasStoppedToday(p,t.asset))continue;
|
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){
|
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 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;
|
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,
|
copyScore:candidate?+candidate.score.toFixed(2):null,
|
||||||
macd:candidate?+candidate.m.macd.toFixed(2):null,
|
macd:candidate?+candidate.m.macd.toFixed(2):null,
|
||||||
momentum:candidate?+candidate.m.recent.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);
|
}else keep.push(pos);
|
||||||
}
|
}
|
||||||
p.positions=keep;
|
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;
|
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))){
|
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;
|
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 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;
|
if(stake<50)continue;
|
||||||
const entry=lp.current_price; if(entry<=0||entry>=1)continue;
|
const entry=lp.current_price; if(entry<=0||entry>=1)continue;
|
||||||
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
|
const shares=+(stake/entry).toFixed(2),cost=+(shares*entry).toFixed(2);
|
||||||
@@ -1827,11 +1990,17 @@ async function backtestWeek(st){
|
|||||||
}
|
}
|
||||||
const sugs=generateSuggestions(snaps);
|
const sugs=generateSuggestions(snaps);
|
||||||
const priceMap={}; snaps.forEach(s=>priceMap[s.id]={yes_price:s.yes_price,no_price:s.no_price});
|
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")){
|
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||||||
const p=st.agents[cfg.id];
|
const p=st.agents[cfg.id];
|
||||||
markToMarket(p,priceMap,cfg,{policyExits:true});
|
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);
|
recordSnapshot(p);
|
||||||
}
|
}
|
||||||
const copyPick=chooseCopycatLeader(st);
|
const copyPick=chooseCopycatLeader(st);
|
||||||
@@ -1862,8 +2031,10 @@ async function runDailyCycle(){
|
|||||||
SNAP_TS=nowIso();
|
SNAP_TS=nowIso();
|
||||||
setStatus("fetching markets…",true);
|
setStatus("fetching markets…",true);
|
||||||
const markets=await fetchMarkets(Infinity,100,count=>setStatus(`fetching all active markets (${count})…`,true));
|
const markets=await fetchMarkets(Infinity,100,count=>setStatus(`fetching all active markets (${count})…`,true));
|
||||||
setStatus("analyzing…",true);
|
setStatus("checking real-world context…",true);
|
||||||
const sugs=generateSuggestions(markets);
|
const realWorldSignals=await fetchRealWorldSignals(markets);
|
||||||
|
setStatus("analyzing expected value…",true);
|
||||||
|
const sugs=generateSuggestions(markets,SUGGESTION_TOTAL,SUGGESTION_PER_CATEGORY,realWorldSignals);
|
||||||
saveSuggestions(sugs,markets.length);
|
saveSuggestions(sugs,markets.length);
|
||||||
st=loadState();
|
st=loadState();
|
||||||
const emailOffsets=agentHistoryOffsets(st);
|
const emailOffsets=agentHistoryOffsets(st);
|
||||||
@@ -1879,13 +2050,19 @@ async function runDailyCycle(){
|
|||||||
setStatus("strategy agents trading…",true);
|
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 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 leaderEq=preBoard[0]?preBoard[0].eq:STARTING_BALANCE;
|
||||||
const claimedMarkets=new Set();
|
|
||||||
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
for(const cfg of AGENTS.filter(a=>a.kind==="strategy")){
|
||||||
const p=st.agents[cfg.id];
|
const p=st.agents[cfg.id];
|
||||||
markToMarket(p,priceMap,cfg,{policyExits:true});
|
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 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);
|
||||||
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);
|
recordSnapshot(p);
|
||||||
}
|
}
|
||||||
// 2) copycat shadows the in-house agent with the strongest MACD/momentum score
|
// 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:""}">
|
<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)}
|
<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("");
|
</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
|
status.textContent=cfg.last_error
|
||||||
? `Last error: ${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 readAlertForm=()=>{
|
||||||
const email=input.value.trim();
|
const email=input.value.trim();
|
||||||
const events={};
|
const events={};
|
||||||
@@ -2011,6 +2194,7 @@ function renderEmailAlerts(){
|
|||||||
equity:STARTING_BALANCE,
|
equity:STARTING_BALANCE,
|
||||||
return_pct:0,
|
return_pct:0,
|
||||||
}],{test:true});
|
}],{test:true});
|
||||||
|
await refreshTradeEmailStatus();
|
||||||
pushCloudState(true);
|
pushCloudState(true);
|
||||||
};
|
};
|
||||||
document.querySelectorAll("[data-email-event],[data-email-agent]").forEach(el=>el.onchange=()=>{
|
document.querySelectorAll("[data-email-event],[data-email-agent]").forEach(el=>el.onchange=()=>{
|
||||||
@@ -2242,9 +2426,11 @@ function renderSuggestions(){
|
|||||||
const focus=getFocus();
|
const focus=getFocus();
|
||||||
const pool=(focus==="All"?all:all.filter(s=>s.category===focus));
|
const pool=(focus==="All"?all:all.filter(s=>s.category===focus));
|
||||||
const filtered=pool.slice(0,VISIBLE_SUGGESTIONS);
|
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"
|
$("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 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 new ${focus} positions while this is selected.`;
|
: `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;}
|
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=>{
|
root.innerHTML=filtered.map(s=>{
|
||||||
const col=catColor(s.category);
|
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="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="event">${esc(s.event||"")}</div>
|
||||||
<div class="sug-row">
|
<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>
|
<span class="muted small">@ ${Math.round(s.entry_price*100)}¢</span>
|
||||||
<div class="conv-bar"><span style="width:${s.conviction}%"></span></div>
|
<div class="conv-bar"><span style="width:${s.conviction}%"></span></div>
|
||||||
<span class="muted small">${Math.round(s.conviction)}</span>
|
<span class="muted small">${Math.round(s.conviction)}</span>
|
||||||
@@ -2263,6 +2449,7 @@ function renderSuggestions(){
|
|||||||
<div class="rationale">${esc(s.rationale)}</div>
|
<div class="rationale">${esc(s.rationale)}</div>
|
||||||
<div class="drivers">${drivers}</div>
|
<div class="drivers">${drivers}</div>
|
||||||
<div class="metrics">
|
<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>
|
<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>`:""}
|
${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>`:""}
|
${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)"/>`;
|
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>`;}
|
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);
|
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);
|
closes.forEach((c,i)=>{const o=i?closes[i-1]:c,up=c>=o,col=up?"#7c8cff":"#fb7185";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"/>`;});
|
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(" ");
|
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+=`<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)"/>`;
|
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>`;}
|
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;
|
svg.innerHTML=inner;
|
||||||
const last=closes.length-1,trend=ma5[last]>=ma10[last]?"bullish":"bearish",mom=hist[last]>=0?"positive":"negative";
|
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>
|
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)}.`;
|
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)}).`;
|
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){
|
function dailyPerformanceExplanation(row){
|
||||||
const snaps=(row.p.snapshots||[]).slice().sort((a,b)=>snapTime(a)-snapTime(b));
|
const snaps=(row.p.snapshots||[]).slice().sort((a,b)=>snapTime(a)-snapTime(b));
|
||||||
const last=snaps[snaps.length-1];
|
const last=snaps[snaps.length-1];
|
||||||
@@ -2419,11 +2615,11 @@ function dailyPerformanceExplanation(row){
|
|||||||
: "There were no major realized closes in the past day.";
|
: "There were no major realized closes in the past day.";
|
||||||
if(delta>0.25){
|
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.";
|
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){
|
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.";
|
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
|
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)}).`
|
? `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="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>
|
<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.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(stopLossLabel(p))}</div>
|
||||||
<div class="sub">${esc(gainStopLabel(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>
|
<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{
|
try{
|
||||||
if(reset){PAPER_SEARCH_RESULTS=[];PAPER_MARKET_OFFSET=0;}
|
if(reset){PAPER_SEARCH_RESULTS=[];PAPER_MARKET_OFFSET=0;}
|
||||||
let loaded=0,rawCount=0,keepGoing=true;
|
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++){
|
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);
|
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");
|
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;
|
rawCount+=raw.length;
|
||||||
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
||||||
PAPER_SEARCH_RESULTS=(PAPER_SEARCH_RESULTS||[]).concat(next);
|
PAPER_SEARCH_RESULTS=(PAPER_SEARCH_RESULTS||[]).concat(next);
|
||||||
PAPER_MARKET_OFFSET+=raw.length;
|
PAPER_MARKET_OFFSET+=raw.length;
|
||||||
|
if(loadAll)cursor=pageData.next_cursor||null;
|
||||||
loaded+=next.length;
|
loaded+=next.length;
|
||||||
if(loadAll)setStatus(`loading active markets (${PAPER_SEARCH_RESULTS.length})…`,true);
|
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);
|
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.");
|
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{
|
try{
|
||||||
if(reset){LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;}
|
if(reset){LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;}
|
||||||
let loaded=0,rawCount=0,keepGoing=true;
|
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++){
|
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);
|
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");
|
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;
|
rawCount+=raw.length;
|
||||||
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
||||||
LIVE_SEARCH_RESULTS=(LIVE_SEARCH_RESULTS||[]).concat(next);
|
LIVE_SEARCH_RESULTS=(LIVE_SEARCH_RESULTS||[]).concat(next);
|
||||||
LIVE_MARKET_OFFSET+=raw.length;
|
LIVE_MARKET_OFFSET+=raw.length;
|
||||||
|
if(loadAll)cursor=pageData.next_cursor||null;
|
||||||
loaded+=next.length;
|
loaded+=next.length;
|
||||||
if(loadAll)setStatus(`loading active markets (${LIVE_SEARCH_RESULTS.length})…`,true);
|
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);
|
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.");
|
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 ---------- */
|
/* ---------- Wire up ---------- */
|
||||||
applyPersonalMode();
|
applyPersonalMode();
|
||||||
initProviderConfig();
|
initProviderConfig();
|
||||||
|
refreshTradeEmailStatus();
|
||||||
$("runBtn").addEventListener("click",async()=>{
|
$("runBtn").addEventListener("click",async()=>{
|
||||||
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
|
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();}
|
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();}
|
||||||
|
|||||||
Generated
+10
@@ -7,6 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neondatabase/serverless": "^1.1.0",
|
"@neondatabase/serverless": "^1.1.0",
|
||||||
"@vercel/blob": "2.5.0",
|
"@vercel/blob": "2.5.0",
|
||||||
|
"nodemailer": "^9.0.3",
|
||||||
"svix": "^1.96.1"
|
"svix": "^1.96.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -222,6 +223,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/npm-run-path": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neondatabase/serverless": "^1.1.0",
|
"@neondatabase/serverless": "^1.1.0",
|
||||||
"@vercel/blob": "2.5.0",
|
"@vercel/blob": "2.5.0",
|
||||||
|
"nodemailer": "^9.0.3",
|
||||||
"svix": "^1.96.1"
|
"svix": "^1.96.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user