mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-27 16:07:46 +00:00
Add personal capital tracker and market browser
This commit is contained in:
@@ -17,6 +17,9 @@ wallet, your own money, manual approval, and audit logging.
|
||||
- `/api/live` manual-fill actions record trades that you placed yourself on
|
||||
Polymarket, then update tracked real positions and P&L. These records are for
|
||||
reconciliation only: Poly Arena still does not sign or submit the trade.
|
||||
- `/api/live` personal-capital actions track money you control outside Poly
|
||||
Arena and bookkeeping allocations to agent return streams. This is not a
|
||||
deposit system, custody account, public investment product, or order router.
|
||||
- `/api/accounts` creates, logs into, and saves password-backed paper accounts
|
||||
through Vercel Blob storage.
|
||||
- `/api/config` exposes safe public provider configuration, including the
|
||||
@@ -53,6 +56,9 @@ Personal mode is narrower than the public business launch:
|
||||
you do anything in Polymarket.
|
||||
- Trade execution happens outside Poly Arena through your own Polymarket wallet
|
||||
or UI. Poly Arena remains the analyst/ticketing layer.
|
||||
- Personal agent shares are allocation notes for your own tracking only. They
|
||||
do not represent pooled investor shares, securities, or money held by Poly
|
||||
Arena.
|
||||
- After you manually trade, record the actual shares, price, fees, and note in
|
||||
Poly Arena so the personal cockpit can track real position value and P&L.
|
||||
- Public `LIVE_TRADING_ENABLED` stays `false`.
|
||||
|
||||
+138
@@ -145,6 +145,41 @@ export async function ensureSchema() {
|
||||
)
|
||||
`;
|
||||
|
||||
await db`
|
||||
create table if not exists personal_capital_accounts (
|
||||
user_id text primary key,
|
||||
cash numeric not null default 0,
|
||||
updated_at timestamptz not null default now()
|
||||
)
|
||||
`;
|
||||
|
||||
await db`
|
||||
create table if not exists personal_agent_allocations (
|
||||
user_id text not null,
|
||||
agent_id text not null,
|
||||
amount numeric not null default 0,
|
||||
updated_at timestamptz not null default now(),
|
||||
primary key (user_id, agent_id)
|
||||
)
|
||||
`;
|
||||
|
||||
await db`
|
||||
create table if not exists personal_capital_events (
|
||||
id bigserial primary key,
|
||||
user_id text not null,
|
||||
action text not null,
|
||||
agent_id text,
|
||||
amount numeric not null,
|
||||
note text,
|
||||
created_at timestamptz not null default now()
|
||||
)
|
||||
`;
|
||||
|
||||
await db`
|
||||
create index if not exists personal_capital_events_user_idx
|
||||
on personal_capital_events (user_id, created_at desc)
|
||||
`;
|
||||
|
||||
schemaReady = true;
|
||||
}
|
||||
|
||||
@@ -472,6 +507,109 @@ export async function listRealPortfolio(userId) {
|
||||
return { positions, fills };
|
||||
}
|
||||
|
||||
export async function listPersonalCapital(userId) {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
await db`
|
||||
insert into personal_capital_accounts (user_id, cash)
|
||||
values (${userId}, 0)
|
||||
on conflict (user_id) do nothing
|
||||
`;
|
||||
const accounts = await db`
|
||||
select *
|
||||
from personal_capital_accounts
|
||||
where user_id = ${userId}
|
||||
limit 1
|
||||
`;
|
||||
const allocations = await db`
|
||||
select *
|
||||
from personal_agent_allocations
|
||||
where user_id = ${userId} and amount > 0
|
||||
order by updated_at desc
|
||||
`;
|
||||
const events = await db`
|
||||
select *
|
||||
from personal_capital_events
|
||||
where user_id = ${userId}
|
||||
order by created_at desc
|
||||
limit 100
|
||||
`;
|
||||
return { account: accounts[0] || { user_id: userId, cash: 0 }, allocations, events };
|
||||
}
|
||||
|
||||
export async function recordPersonalCapitalAction(action) {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
const userId = action.userId;
|
||||
const type = String(action.action || "").toUpperCase();
|
||||
const agentId = action.agentId || null;
|
||||
const amount = Number(action.amount || 0);
|
||||
|
||||
await db`
|
||||
insert into personal_capital_accounts (user_id, cash)
|
||||
values (${userId}, 0)
|
||||
on conflict (user_id) do nothing
|
||||
`;
|
||||
|
||||
const accounts = await db`
|
||||
select *
|
||||
from personal_capital_accounts
|
||||
where user_id = ${userId}
|
||||
limit 1
|
||||
`;
|
||||
const account = accounts[0] || { cash: 0 };
|
||||
let nextCash = Number(account.cash || 0);
|
||||
|
||||
if (type === "DEPOSIT") {
|
||||
nextCash += amount;
|
||||
} else if (type === "WITHDRAW") {
|
||||
nextCash -= amount;
|
||||
} else if (type === "BUY_AGENT") {
|
||||
nextCash -= amount;
|
||||
await db`
|
||||
insert into personal_agent_allocations (user_id, agent_id, amount, updated_at)
|
||||
values (${userId}, ${agentId}, ${amount}, now())
|
||||
on conflict (user_id, agent_id) do update set
|
||||
amount = personal_agent_allocations.amount + excluded.amount,
|
||||
updated_at = now()
|
||||
`;
|
||||
} else if (type === "SELL_AGENT") {
|
||||
const rows = await db`
|
||||
select *
|
||||
from personal_agent_allocations
|
||||
where user_id = ${userId} and agent_id = ${agentId}
|
||||
limit 1
|
||||
`;
|
||||
const current = rows[0] ? Number(rows[0].amount || 0) : 0;
|
||||
const reduction = Math.min(amount, current);
|
||||
nextCash += reduction;
|
||||
await db`
|
||||
insert into personal_agent_allocations (user_id, agent_id, amount, updated_at)
|
||||
values (${userId}, ${agentId}, ${Math.max(0, current - reduction)}, now())
|
||||
on conflict (user_id, agent_id) do update set
|
||||
amount = excluded.amount,
|
||||
updated_at = now()
|
||||
`;
|
||||
}
|
||||
|
||||
if (nextCash < -0.000001) {
|
||||
throw new Error("Tracked personal cash is too low for that action");
|
||||
}
|
||||
|
||||
await db`
|
||||
update personal_capital_accounts
|
||||
set cash = ${nextCash}, updated_at = now()
|
||||
where user_id = ${userId}
|
||||
`;
|
||||
|
||||
await db`
|
||||
insert into personal_capital_events (user_id, action, agent_id, amount, note)
|
||||
values (${userId}, ${type}, ${agentId}, ${amount}, ${action.note || null})
|
||||
`;
|
||||
|
||||
return listPersonalCapital(userId);
|
||||
}
|
||||
|
||||
export async function providerEventSummary() {
|
||||
await ensureSchema();
|
||||
const db = sql();
|
||||
|
||||
+54
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
createTradeTicket,
|
||||
listPersonalCapital,
|
||||
listRealPortfolio,
|
||||
listTradeTickets,
|
||||
recordAuditEvent,
|
||||
recordPersonalCapitalAction,
|
||||
recordRealFill,
|
||||
updateRealPositionMark,
|
||||
updateTradeTicketStatus,
|
||||
@@ -242,6 +244,7 @@ function validateIntent(intent) {
|
||||
|
||||
const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
||||
const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]);
|
||||
const VALID_CAPITAL_ACTIONS = new Set(["DEPOSIT", "WITHDRAW", "BUY_AGENT", "SELL_AGENT"]);
|
||||
|
||||
function marketSearchUrl(question) {
|
||||
return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`;
|
||||
@@ -319,6 +322,25 @@ function validateFill(fill) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function normalizeCapitalAction(body) {
|
||||
return {
|
||||
userId: String(body.user_id || "local-readiness-user").trim(),
|
||||
action: String(body.capital_action || body.capitalAction || "").trim().toUpperCase(),
|
||||
agentId: String(body.agent_id || "").trim(),
|
||||
amount: Number(body.amount || 0),
|
||||
note: String(body.note || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function validateCapitalAction(action) {
|
||||
const errors = [];
|
||||
if (!action.userId) errors.push("user_id");
|
||||
if (!VALID_CAPITAL_ACTIONS.has(action.action)) errors.push("capital_action must be DEPOSIT, WITHDRAW, BUY_AGENT, or SELL_AGENT");
|
||||
if ((action.action === "BUY_AGENT" || action.action === "SELL_AGENT") && !action.agentId) errors.push("agent_id");
|
||||
if (!Number.isFinite(action.amount) || action.amount <= 0) errors.push("amount must be positive");
|
||||
return errors;
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
|
||||
@@ -333,6 +355,11 @@ export default async function handler(req, res) {
|
||||
const portfolio = await listRealPortfolio(userId);
|
||||
return res.status(200).json({ ok: true, ...portfolio });
|
||||
}
|
||||
if (req.query?.action === "personal_capital") {
|
||||
const userId = String(req.query?.user_id || "local-readiness-user").trim();
|
||||
const capital = await listPersonalCapital(userId);
|
||||
return res.status(200).json({ ok: true, ...capital });
|
||||
}
|
||||
return res.status(200).json(baseStatus());
|
||||
}
|
||||
|
||||
@@ -410,6 +437,33 @@ export default async function handler(req, res) {
|
||||
return res.status(200).json({ ok: true, position });
|
||||
}
|
||||
|
||||
if (body.action === "capital_action") {
|
||||
const capitalAction = normalizeCapitalAction(body);
|
||||
const capitalErrors = validateCapitalAction(capitalAction);
|
||||
if (capitalErrors.length) return res.status(400).json({ ok: false, error: "Invalid personal capital action", details: capitalErrors });
|
||||
let capital;
|
||||
try {
|
||||
capital = await recordPersonalCapitalAction(capitalAction);
|
||||
} catch (err) {
|
||||
return res.status(400).json({ ok: false, error: err?.message || "Personal capital tracker update failed" });
|
||||
}
|
||||
await recordAuditEvent("PERSONAL_CAPITAL_ACTION", {
|
||||
user_id: capitalAction.userId,
|
||||
action: capitalAction.action,
|
||||
agent_id: capitalAction.agentId,
|
||||
amount: capitalAction.amount,
|
||||
custody_by_site: false,
|
||||
real_order_placed_by_site: false,
|
||||
});
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
...capital,
|
||||
custody_by_site: false,
|
||||
real_order_placed_by_site: false,
|
||||
note: "Personal capital tracker updated for bookkeeping only. Poly Arena did not receive funds or place an order.",
|
||||
});
|
||||
}
|
||||
|
||||
const intent = body.intent || body;
|
||||
const errors = validateIntent(intent);
|
||||
if (errors.length) {
|
||||
|
||||
+158
-3
@@ -572,6 +572,21 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
||||
<div class="risk-note" style="margin-top:12px">Fund only your own wallet directly through approved provider flows. This site will not accept, hold, or withdraw money for other people.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Personal capital & agent shares</h3><span class="small muted">synced tracker, no custody</span></div>
|
||||
<div id="personalCapitalSummary"></div>
|
||||
<div class="account-row">
|
||||
<input class="input" id="personalCapitalAmount" type="number" min="1" step="25" placeholder="Amount" />
|
||||
<select class="select" id="personalCapitalAgent"></select>
|
||||
<button class="btn" id="personalDepositBtn">Record external deposit</button>
|
||||
<button class="btn ghost" id="personalWithdrawBtn">Record withdrawal</button>
|
||||
<button class="btn primary" id="personalBuyAgentBtn">Allocate to agent</button>
|
||||
<button class="btn ghost" id="personalSellAgentBtn">Reduce agent</button>
|
||||
</div>
|
||||
<div class="risk-note" style="margin-top:12px">This tracks money you personally control outside Poly Arena. Agent shares are bookkeeping allocations only, not securities, pooled deposits, or orders placed by the site.</div>
|
||||
<div id="personalAgentAllocations" style="margin-top:12px"></div>
|
||||
<div id="personalCapitalHistory" style="margin-top:12px"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Agent permissions & risk limits</h3><span class="small muted">manual approval only</span></div>
|
||||
<div id="liveAgentPermissions"></div>
|
||||
@@ -602,6 +617,18 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
||||
<div id="liveAuditTrail"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Browse Polymarket for manual tickets</h3><span class="small muted">search active markets</span></div>
|
||||
<div class="account-row">
|
||||
<input class="input" id="liveSearchInput" placeholder="Search Polymarket markets" />
|
||||
<button class="btn" id="liveSearchBtn">Search</button>
|
||||
<button class="btn" id="liveAllBtn">All active</button>
|
||||
<button class="btn ghost" id="liveMoreBtn">Load more</button>
|
||||
<button class="btn ghost" id="liveClearSearchBtn">Clear</button>
|
||||
</div>
|
||||
<div class="filterbar" id="liveMarketFilterbar"></div>
|
||||
<div class="trade-list" id="liveMarketList"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Manual trade ticket queue</h3><span class="small muted">AI suggests, you sign elsewhere</span></div>
|
||||
<div id="personalTicketQueue"></div>
|
||||
@@ -725,10 +752,14 @@ const CLOB = "https://clob.polymarket.com";
|
||||
let PAPER_SEARCH_RESULTS = null;
|
||||
let PAPER_MARKET_OFFSET = 0;
|
||||
let PAPER_MARKET_QUERY = "";
|
||||
let LIVE_SEARCH_RESULTS = [];
|
||||
let LIVE_MARKET_OFFSET = 0;
|
||||
let LIVE_MARKET_QUERY = "";
|
||||
let LIVE_BACKEND_STATUS = null;
|
||||
let LIVE_POLICY_STATUS = null;
|
||||
let PERSONAL_TICKETS = [];
|
||||
let REAL_PORTFOLIO = {positions:[],fills:[]};
|
||||
let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]};
|
||||
let PROVIDER_CONFIG = null;
|
||||
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
||||
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
||||
@@ -2565,16 +2596,19 @@ async function stageSuggestionTicket(marketId){
|
||||
async function loadTradeTickets(){
|
||||
if(!PERSONAL_MODE)return;
|
||||
try{
|
||||
const [ticketRes,portfolioRes]=await Promise.all([
|
||||
const [ticketRes,portfolioRes,capitalRes]=await Promise.all([
|
||||
fetch(`/api/live?action=tickets&user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"}),
|
||||
fetch(`/api/live?action=real_portfolio&user_id=${encodeURIComponent(PERSONAL_USER_ID)}`,{cache:"no-store"}),
|
||||
fetch(`/api/live?action=personal_capital&user_id=${encodeURIComponent(PERSONAL_USER_ID)}`,{cache:"no-store"}),
|
||||
]);
|
||||
const d=await ticketRes.json(),pf=await portfolioRes.json();
|
||||
const d=await ticketRes.json(),pf=await portfolioRes.json(),cap=await capitalRes.json();
|
||||
PERSONAL_TICKETS=d.tickets||[];
|
||||
REAL_PORTFOLIO={positions:pf.positions||[],fills:pf.fills||[]};
|
||||
}catch(e){PERSONAL_TICKETS=[];REAL_PORTFOLIO={positions:[],fills:[]};}
|
||||
PERSONAL_CAPITAL={account:cap.account||{cash:0},allocations:cap.allocations||[],events:cap.events||[]};
|
||||
}catch(e){PERSONAL_TICKETS=[];REAL_PORTFOLIO={positions:[],fills:[]};PERSONAL_CAPITAL={account:{cash:0},allocations:[],events:[]};}
|
||||
renderTradeTickets();
|
||||
renderRealPortfolio();
|
||||
renderPersonalCapital();
|
||||
}
|
||||
async function updateTicketStatus(ticketId,status){
|
||||
try{
|
||||
@@ -2682,6 +2716,117 @@ function renderRealPortfolio(){
|
||||
fillsRoot.innerHTML=fills.length?fills.slice(0,25).map(f=>`<div class="log-item"><span class="badge ${esc(f.action)}">${esc(f.action)}</span><span class="log-date">${new Date(f.filled_at).toLocaleString()}</span> — ${Number(f.shares||0).toFixed(2)} ${esc(f.side)} @ ${Math.round(Number(f.price||0)*100)}¢ on ${esc((f.question||f.market_id||"").slice(0,52))}</div>`).join(""):`<div class="empty">No manual fills yet.</div>`;
|
||||
document.querySelectorAll("[data-update-mark]").forEach(btn=>btn.onclick=()=>updateRealMark(btn.dataset.updateMark));
|
||||
}
|
||||
async function personalCapitalAction(capitalAction){
|
||||
const amount=Number(($("personalCapitalAmount")&&$("personalCapitalAmount").value)||0);
|
||||
const agentId=($("personalCapitalAgent")&&$("personalCapitalAgent").value)||"";
|
||||
try{
|
||||
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({
|
||||
action:"capital_action",
|
||||
user_id:PERSONAL_USER_ID,
|
||||
capital_action:capitalAction,
|
||||
agent_id:agentId,
|
||||
amount,
|
||||
note:"Personal cockpit bookkeeping action",
|
||||
})});
|
||||
const d=await r.json();
|
||||
if(!r.ok)throw new Error(d.error||"Capital tracker update failed");
|
||||
PERSONAL_CAPITAL={account:d.account||{cash:0},allocations:d.allocations||[],events:d.events||[]};
|
||||
renderPersonalCapital();
|
||||
toast("Personal tracker updated.");
|
||||
}catch(e){toast(e.message||"Capital tracker update failed.");}
|
||||
}
|
||||
function renderPersonalCapital(){
|
||||
const summary=$("personalCapitalSummary"),allocRoot=$("personalAgentAllocations"),histRoot=$("personalCapitalHistory"),agentSel=$("personalCapitalAgent");
|
||||
if(!summary||!allocRoot||!histRoot)return;
|
||||
if(agentSel)agentSel.innerHTML=AGENTS.map(a=>`<option value="${esc(a.id)}">${a.emoji} ${esc(a.name)}</option>`).join("");
|
||||
const cash=Number(PERSONAL_CAPITAL.account&&PERSONAL_CAPITAL.account.cash||0);
|
||||
const allocations=PERSONAL_CAPITAL.allocations||[];
|
||||
const allocated=allocations.reduce((s,a)=>s+Number(a.amount||0),0);
|
||||
summary.innerHTML=`<div class="invest-total">
|
||||
<div class="invest-num"><div class="k">Tracked outside cash</div><div class="v">${fmtUSD(cash)}</div></div>
|
||||
<div class="invest-num"><div class="k">Agent allocations</div><div class="v">${fmtUSD(allocated)}</div></div>
|
||||
<div class="invest-num"><div class="k">Personal tracker total</div><div class="v">${fmtUSD(cash+allocated)}</div></div>
|
||||
</div>`;
|
||||
allocRoot.innerHTML=allocations.length?allocations.map(a=>{
|
||||
const cfg=agentById(a.agent_id)||{name:a.agent_id,emoji:"",color:"#7c8cff"};
|
||||
return `<div class="leader-row"><span><b>${cfg.emoji} ${esc(cfg.name)}</b><div class="small muted">bookkeeping allocation only</div></span><b>${fmtUSD(Number(a.amount||0))}</b></div>`;
|
||||
}).join(""):`<div class="empty">No agent allocations yet. Record outside cash, then allocate any amount to an agent you want to shadow.</div>`;
|
||||
const events=(PERSONAL_CAPITAL.events||[]).slice(0,20);
|
||||
histRoot.innerHTML=events.length?events.map(e=>{
|
||||
const cfg=e.agent_id?agentById(e.agent_id):null;
|
||||
return `<div class="log-item"><span class="badge">${esc(e.action)}</span><span class="log-date">${new Date(e.created_at).toLocaleString()}</span> — ${fmtUSD(Number(e.amount||0))}${cfg?` · ${cfg.emoji} ${esc(cfg.name)}`:""}</div>`;
|
||||
}).join(""):`<div class="empty">No personal tracker activity yet.</div>`;
|
||||
}
|
||||
function currentLiveMarkets(){return LIVE_SEARCH_RESULTS||[];}
|
||||
function renderLiveMarkets(){
|
||||
const root=$("liveMarketList"),filter=$("liveMarketFilterbar");if(!root)return;
|
||||
const all=currentLiveMarkets(),focus=getFocus();
|
||||
if(filter){
|
||||
const counts={};CATEGORIES.forEach(c=>counts[c]=0);all.forEach(s=>{counts[counts[s.category]!==undefined?s.category:"Other"]++;});counts.All=all.length;
|
||||
filter.innerHTML=CATEGORIES.map(c=>`<span class="chip ${focus===c?"active":""}" data-live-cat="${c}">${c}<span class="ct">${counts[c]||0}</span></span>`).join("");
|
||||
document.querySelectorAll("[data-live-cat]").forEach(el=>el.onclick=()=>{setFocus(el.dataset.liveCat);renderLiveMarkets();});
|
||||
}
|
||||
const shown=all.filter(s=>focus==="All"||!focus||s.category===focus);
|
||||
if(!shown.length){root.innerHTML=`<div class="empty">Search Polymarket or load active markets to stage manual tickets from the cockpit.</div>`;return;}
|
||||
root.innerHTML=shown.map(s=>`<div class="trade-card">
|
||||
<div class="trade-head"><div><div class="trade-title"><a class="market-title" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">${esc(s.question)}</a></div>
|
||||
<div class="trade-meta"><span>${esc(s.category)}</span><span>YES ${pct(s.yes_price)}</span><span>NO ${pct(s.no_price)}</span><span>${Math.round(s.conviction||0)} conviction</span></div></div>
|
||||
<a class="btn ghost" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">Open Polymarket</a></div>
|
||||
<div class="rationale">${esc(s.rationale||"Active Polymarket market available for manual review.")}</div>
|
||||
<div class="trade-actions">
|
||||
<select class="select" data-live-side="${esc(s.market_id)}"><option>YES</option><option>NO</option></select>
|
||||
<input class="input" type="number" min="1" step="25" value="100" data-live-amount="${esc(s.market_id)}" aria-label="Manual amount for ${esc(s.question)}">
|
||||
<input class="input" type="number" min="0.01" max="0.99" step="0.01" value="${Number(s.entry_price||s.yes_price||0).toFixed(2)}" data-live-price="${esc(s.market_id)}" aria-label="Limit price for ${esc(s.question)}">
|
||||
<button class="btn primary" data-live-ticket="${esc(s.market_id)}">Stage manual ticket</button>
|
||||
</div>
|
||||
</div>`).join("");
|
||||
document.querySelectorAll("[data-live-ticket]").forEach(btn=>btn.onclick=()=>stageLiveMarketTicket(btn.dataset.liveTicket));
|
||||
}
|
||||
async function searchLiveMarkets(){
|
||||
const q=($("liveSearchInput").value||"").trim();
|
||||
if(!q)return toast("Enter a Polymarket search.");
|
||||
LIVE_MARKET_OFFSET=0;LIVE_MARKET_QUERY=q;
|
||||
await loadLiveMarketPage(true);
|
||||
}
|
||||
async function loadLiveMarketPage(reset=false){
|
||||
const q=LIVE_MARKET_QUERY||(($("liveSearchInput")&&$("liveSearchInput").value)||"").trim();
|
||||
setStatus("searching Polymarket...",true);
|
||||
try{
|
||||
if(reset){LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;}
|
||||
const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:"100",offset:String(LIVE_MARKET_OFFSET),order:"volume24hr",ascending:"false"});
|
||||
if(q)params.set("search",q);
|
||||
const r=await fetch(`${GAMMA}/markets?${params}`);
|
||||
if(!r.ok)throw new Error("market load failed");
|
||||
const raw=await r.json();
|
||||
const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket);
|
||||
LIVE_SEARCH_RESULTS=(LIVE_SEARCH_RESULTS||[]).concat(next);
|
||||
LIVE_MARKET_OFFSET+=raw.length;
|
||||
renderLiveMarkets();setStatus("up to date",false);
|
||||
toast(next.length?`Loaded ${LIVE_SEARCH_RESULTS.length} active markets.`:"No more active markets found.");
|
||||
}catch(e){setStatus("market load error",false);toast("Polymarket market load failed.");}
|
||||
}
|
||||
async function stageLiveMarketTicket(marketId){
|
||||
const s=currentLiveMarkets().find(x=>String(x.market_id)===String(marketId));
|
||||
if(!s)return toast("Market not found.");
|
||||
const side=(document.querySelector(`[data-live-side="${marketId}"]`)||{}).value||"YES";
|
||||
const amount=Number((document.querySelector(`[data-live-amount="${marketId}"]`)||{}).value||0);
|
||||
const price=Number((document.querySelector(`[data-live-price="${marketId}"]`)||{}).value||0);
|
||||
try{
|
||||
await createTradeTicket({
|
||||
user_id:PERSONAL_USER_ID,
|
||||
agent_id:($("liveIntentAgent")&&$("liveIntentAgent").value)||localStorage.getItem(VIEW_KEY)||"value",
|
||||
market_id:s.market_id,
|
||||
question:s.question,
|
||||
market_url:s.url,
|
||||
side,
|
||||
max_amount:amount,
|
||||
limit_price:price,
|
||||
rationale:`Manually staged from the personal Polymarket browser. ${s.rationale||""}`.trim(),
|
||||
});
|
||||
await loadTradeTickets();
|
||||
toast("Manual ticket staged. Open Polymarket to place it yourself.");
|
||||
}catch(e){toast(e.message||"Ticket staging failed.");}
|
||||
}
|
||||
async function saveRiskProfileToServer(s){
|
||||
const permissions=JSON.parse(JSON.stringify(s.agents||{}));
|
||||
Object.values(permissions).forEach(p=>{p.autoTrade=false;p.manualApprove=true;});
|
||||
@@ -2744,6 +2889,8 @@ function renderLiveMoneyTab(){
|
||||
}).join("");
|
||||
const audit=s.audit.slice(-10).reverse();
|
||||
$("liveAuditTrail").innerHTML=audit.length?audit.map(x=>`<div class="log-item"><span class="log-date">${new Date(x.at).toLocaleString()}</span> — ${esc(x.detail)}</div>`).join(""):`<div class="empty">No readiness changes logged yet.</div>`;
|
||||
renderPersonalCapital();
|
||||
renderLiveMarkets();
|
||||
document.querySelectorAll("[data-live-check]").forEach(el=>el.onchange=()=>{const next=loadLiveState();next.checks[el.dataset.liveCheck]=el.checked;saveLiveState(next,`${el.checked?"Completed":"Reopened"}: ${LIVE_CHECKS.find(([k])=>k===el.dataset.liveCheck)[1]}`);renderLiveMoneyTab();});
|
||||
document.querySelectorAll("[data-live-agent]").forEach(el=>el.onchange=()=>{const next=loadLiveState(),p=next.agents[el.dataset.liveAgent],field=el.dataset.field;p[field]=el.type==="checkbox"?el.checked:Number(el.value||0);saveLiveState(next,`Updated ${agentById(el.dataset.liveAgent).name} permission: ${field}`);renderLiveMoneyTab();});
|
||||
}
|
||||
@@ -2874,6 +3021,14 @@ $("saveLiveProfileBtn").addEventListener("click",async()=>{
|
||||
}
|
||||
});
|
||||
$("dryRunOrderBtn").addEventListener("click",()=>dryRunLiveOrderIntent());
|
||||
$("personalDepositBtn").addEventListener("click",()=>personalCapitalAction("DEPOSIT"));
|
||||
$("personalWithdrawBtn").addEventListener("click",()=>personalCapitalAction("WITHDRAW"));
|
||||
$("personalBuyAgentBtn").addEventListener("click",()=>personalCapitalAction("BUY_AGENT"));
|
||||
$("personalSellAgentBtn").addEventListener("click",()=>personalCapitalAction("SELL_AGENT"));
|
||||
$("liveSearchBtn").addEventListener("click",()=>searchLiveMarkets());
|
||||
$("liveAllBtn").addEventListener("click",()=>{LIVE_MARKET_QUERY="";LIVE_SEARCH_RESULTS=[];loadLiveMarketPage(true);});
|
||||
$("liveMoreBtn").addEventListener("click",()=>loadLiveMarketPage(false));
|
||||
$("liveClearSearchBtn").addEventListener("click",()=>{LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;LIVE_MARKET_QUERY="";renderLiveMarkets();});
|
||||
$("recordConsentBtn").addEventListener("click",()=>recordLiveConsent());
|
||||
window.addEventListener("focus",()=>refreshFromCloudAndRender(true));
|
||||
document.addEventListener("visibilitychange",()=>{
|
||||
|
||||
Reference in New Issue
Block a user