mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-12 23:28:05 +00:00
Add manual real fill tracking
This commit is contained in:
@@ -14,6 +14,9 @@ wallet, your own money, manual approval, and audit logging.
|
|||||||
non-custodial manual trade tickets. These tickets contain the market, side,
|
non-custodial manual trade tickets. These tickets contain the market, side,
|
||||||
amount, limit, rationale, Polymarket link, and review instructions, but never
|
amount, limit, rationale, Polymarket link, and review instructions, but never
|
||||||
a private key or signed order.
|
a private key or signed order.
|
||||||
|
- `/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/accounts` creates, logs into, and saves password-backed paper accounts
|
- `/api/accounts` creates, logs into, and saves password-backed paper accounts
|
||||||
through Vercel Blob storage.
|
through Vercel Blob storage.
|
||||||
- `/api/config` exposes safe public provider configuration, including the
|
- `/api/config` exposes safe public provider configuration, including the
|
||||||
@@ -50,6 +53,8 @@ Personal mode is narrower than the public business launch:
|
|||||||
you do anything in Polymarket.
|
you do anything in Polymarket.
|
||||||
- Trade execution happens outside Poly Arena through your own Polymarket wallet
|
- Trade execution happens outside Poly Arena through your own Polymarket wallet
|
||||||
or UI. Poly Arena remains the analyst/ticketing layer.
|
or UI. Poly Arena remains the analyst/ticketing layer.
|
||||||
|
- 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`.
|
- Public `LIVE_TRADING_ENABLED` stays `false`.
|
||||||
|
|
||||||
Personal mode still needs:
|
Personal mode still needs:
|
||||||
|
|||||||
+196
@@ -101,6 +101,50 @@ export async function ensureSchema() {
|
|||||||
on trade_tickets (user_id, status, created_at desc)
|
on trade_tickets (user_id, status, created_at desc)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create table if not exists real_fills (
|
||||||
|
id bigserial primary key,
|
||||||
|
user_id text not null,
|
||||||
|
ticket_id bigint,
|
||||||
|
agent_id text,
|
||||||
|
market_id text not null,
|
||||||
|
question text,
|
||||||
|
market_url text,
|
||||||
|
side text not null,
|
||||||
|
action text not null,
|
||||||
|
shares numeric not null,
|
||||||
|
price numeric not null,
|
||||||
|
fees numeric not null default 0,
|
||||||
|
tx_note text,
|
||||||
|
filled_at timestamptz not null default now(),
|
||||||
|
created_at timestamptz not null default now()
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create index if not exists real_fills_user_market_idx
|
||||||
|
on real_fills (user_id, market_id, side, filled_at desc)
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create table if not exists real_positions (
|
||||||
|
id bigserial primary key,
|
||||||
|
user_id text not null,
|
||||||
|
agent_id text,
|
||||||
|
market_id text not null,
|
||||||
|
question text,
|
||||||
|
market_url text,
|
||||||
|
side text not null,
|
||||||
|
shares numeric not null default 0,
|
||||||
|
avg_price numeric not null default 0,
|
||||||
|
cost_basis numeric not null default 0,
|
||||||
|
realized_pnl numeric not null default 0,
|
||||||
|
current_price numeric,
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
unique (user_id, market_id, side)
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
schemaReady = true;
|
schemaReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +320,158 @@ export async function updateTradeTicketStatus(userId, ticketId, status) {
|
|||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function recordRealFill(fill) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const action = String(fill.action || "BUY").toUpperCase();
|
||||||
|
const shares = Number(fill.shares || 0);
|
||||||
|
const price = Number(fill.price || 0);
|
||||||
|
const fees = Number(fill.fees || 0);
|
||||||
|
|
||||||
|
const rows = await db`
|
||||||
|
insert into real_fills (
|
||||||
|
user_id,
|
||||||
|
ticket_id,
|
||||||
|
agent_id,
|
||||||
|
market_id,
|
||||||
|
question,
|
||||||
|
market_url,
|
||||||
|
side,
|
||||||
|
action,
|
||||||
|
shares,
|
||||||
|
price,
|
||||||
|
fees,
|
||||||
|
tx_note,
|
||||||
|
filled_at
|
||||||
|
) values (
|
||||||
|
${fill.userId},
|
||||||
|
${fill.ticketId ? Number(fill.ticketId) : null},
|
||||||
|
${fill.agentId || null},
|
||||||
|
${fill.marketId},
|
||||||
|
${fill.question || null},
|
||||||
|
${fill.marketUrl || null},
|
||||||
|
${fill.side},
|
||||||
|
${action},
|
||||||
|
${shares},
|
||||||
|
${price},
|
||||||
|
${fees},
|
||||||
|
${fill.txNote || null},
|
||||||
|
${fill.filledAt ? new Date(fill.filledAt).toISOString() : new Date().toISOString()}
|
||||||
|
)
|
||||||
|
returning *
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existing = await db`
|
||||||
|
select *
|
||||||
|
from real_positions
|
||||||
|
where user_id = ${fill.userId}
|
||||||
|
and market_id = ${fill.marketId}
|
||||||
|
and side = ${fill.side}
|
||||||
|
limit 1
|
||||||
|
`;
|
||||||
|
const pos = existing[0];
|
||||||
|
|
||||||
|
if (action === "SELL" && pos) {
|
||||||
|
const sellShares = Math.min(shares, Number(pos.shares || 0));
|
||||||
|
const avgPrice = Number(pos.avg_price || 0);
|
||||||
|
const proceeds = sellShares * price - fees;
|
||||||
|
const costRemoved = sellShares * avgPrice;
|
||||||
|
const nextShares = Number(pos.shares || 0) - sellShares;
|
||||||
|
const nextCost = Math.max(0, Number(pos.cost_basis || 0) - costRemoved);
|
||||||
|
const realized = Number(pos.realized_pnl || 0) + proceeds - costRemoved;
|
||||||
|
await db`
|
||||||
|
update real_positions
|
||||||
|
set shares = ${nextShares},
|
||||||
|
cost_basis = ${nextCost},
|
||||||
|
realized_pnl = ${realized},
|
||||||
|
current_price = ${price},
|
||||||
|
updated_at = now()
|
||||||
|
where id = ${pos.id}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const currentShares = pos ? Number(pos.shares || 0) : 0;
|
||||||
|
const currentCost = pos ? Number(pos.cost_basis || 0) : 0;
|
||||||
|
const nextShares = currentShares + shares;
|
||||||
|
const nextCost = currentCost + shares * price + fees;
|
||||||
|
const avgPrice = nextShares > 0 ? nextCost / nextShares : 0;
|
||||||
|
await db`
|
||||||
|
insert into real_positions (
|
||||||
|
user_id,
|
||||||
|
agent_id,
|
||||||
|
market_id,
|
||||||
|
question,
|
||||||
|
market_url,
|
||||||
|
side,
|
||||||
|
shares,
|
||||||
|
avg_price,
|
||||||
|
cost_basis,
|
||||||
|
realized_pnl,
|
||||||
|
current_price,
|
||||||
|
updated_at
|
||||||
|
) values (
|
||||||
|
${fill.userId},
|
||||||
|
${fill.agentId || null},
|
||||||
|
${fill.marketId},
|
||||||
|
${fill.question || null},
|
||||||
|
${fill.marketUrl || null},
|
||||||
|
${fill.side},
|
||||||
|
${nextShares},
|
||||||
|
${avgPrice},
|
||||||
|
${nextCost},
|
||||||
|
${pos ? Number(pos.realized_pnl || 0) : 0},
|
||||||
|
${price},
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
on conflict (user_id, market_id, side) do update set
|
||||||
|
agent_id = coalesce(excluded.agent_id, real_positions.agent_id),
|
||||||
|
question = coalesce(excluded.question, real_positions.question),
|
||||||
|
market_url = coalesce(excluded.market_url, real_positions.market_url),
|
||||||
|
shares = excluded.shares,
|
||||||
|
avg_price = excluded.avg_price,
|
||||||
|
cost_basis = excluded.cost_basis,
|
||||||
|
current_price = excluded.current_price,
|
||||||
|
updated_at = now()
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fill.ticketId) {
|
||||||
|
await updateTradeTicketStatus(fill.userId, fill.ticketId, "placed_manually");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRealPositionMark(userId, positionId, currentPrice) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const rows = await db`
|
||||||
|
update real_positions
|
||||||
|
set current_price = ${Number(currentPrice)}, updated_at = now()
|
||||||
|
where user_id = ${userId} and id = ${Number(positionId)}
|
||||||
|
returning *
|
||||||
|
`;
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRealPortfolio(userId) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const positions = await db`
|
||||||
|
select *
|
||||||
|
from real_positions
|
||||||
|
where user_id = ${userId}
|
||||||
|
order by updated_at desc
|
||||||
|
`;
|
||||||
|
const fills = await db`
|
||||||
|
select *
|
||||||
|
from real_fills
|
||||||
|
where user_id = ${userId}
|
||||||
|
order by filled_at desc
|
||||||
|
limit 100
|
||||||
|
`;
|
||||||
|
return { positions, fills };
|
||||||
|
}
|
||||||
|
|
||||||
export async function providerEventSummary() {
|
export async function providerEventSummary() {
|
||||||
await ensureSchema();
|
await ensureSchema();
|
||||||
const db = sql();
|
const db = sql();
|
||||||
|
|||||||
+80
-1
@@ -1,4 +1,12 @@
|
|||||||
import { createTradeTicket, listTradeTickets, recordAuditEvent, updateTradeTicketStatus } from "./_db.js";
|
import {
|
||||||
|
createTradeTicket,
|
||||||
|
listRealPortfolio,
|
||||||
|
listTradeTickets,
|
||||||
|
recordAuditEvent,
|
||||||
|
recordRealFill,
|
||||||
|
updateRealPositionMark,
|
||||||
|
updateTradeTicketStatus,
|
||||||
|
} from "./_db.js";
|
||||||
|
|
||||||
const REQUIRED_ENV = [
|
const REQUIRED_ENV = [
|
||||||
["PRODUCTION_APP_URL", "Production app URL"],
|
["PRODUCTION_APP_URL", "Production app URL"],
|
||||||
@@ -233,6 +241,7 @@ function validateIntent(intent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
||||||
|
const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]);
|
||||||
|
|
||||||
function marketSearchUrl(question) {
|
function marketSearchUrl(question) {
|
||||||
return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`;
|
return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`;
|
||||||
@@ -280,6 +289,36 @@ function validateTicket(ticket) {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFill(body) {
|
||||||
|
return {
|
||||||
|
userId: String(body.user_id || "local-readiness-user").trim(),
|
||||||
|
ticketId: body.ticket_id || null,
|
||||||
|
agentId: String(body.agent_id || "").trim(),
|
||||||
|
marketId: String(body.market_id || "").trim(),
|
||||||
|
question: String(body.question || "").trim(),
|
||||||
|
marketUrl: String(body.market_url || "").trim(),
|
||||||
|
side: String(body.side || "").trim().toUpperCase(),
|
||||||
|
action: String(body.fill_action || body.trade_action || "BUY").trim().toUpperCase(),
|
||||||
|
shares: Number(body.shares || 0),
|
||||||
|
price: Number(body.price || 0),
|
||||||
|
fees: Number(body.fees || 0),
|
||||||
|
txNote: String(body.tx_note || "").trim(),
|
||||||
|
filledAt: body.filled_at || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFill(fill) {
|
||||||
|
const errors = [];
|
||||||
|
if (!fill.userId) errors.push("user_id");
|
||||||
|
if (!fill.marketId) errors.push("market_id");
|
||||||
|
if (!["YES", "NO"].includes(fill.side)) errors.push("side must be YES or NO");
|
||||||
|
if (!VALID_FILL_ACTIONS.has(fill.action)) errors.push("fill_action must be BUY or SELL");
|
||||||
|
if (!Number.isFinite(fill.shares) || fill.shares <= 0) errors.push("shares must be positive");
|
||||||
|
if (!Number.isFinite(fill.price) || fill.price <= 0 || fill.price >= 1) errors.push("price must be between 0 and 1");
|
||||||
|
if (!Number.isFinite(fill.fees) || fill.fees < 0) errors.push("fees must be 0 or greater");
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
|
|
||||||
@@ -289,6 +328,11 @@ export default async function handler(req, res) {
|
|||||||
const tickets = await listTradeTickets(userId, req.query?.limit || 50);
|
const tickets = await listTradeTickets(userId, req.query?.limit || 50);
|
||||||
return res.status(200).json({ ok: true, tickets });
|
return res.status(200).json({ ok: true, tickets });
|
||||||
}
|
}
|
||||||
|
if (req.query?.action === "real_portfolio") {
|
||||||
|
const userId = String(req.query?.user_id || "local-readiness-user").trim();
|
||||||
|
const portfolio = await listRealPortfolio(userId);
|
||||||
|
return res.status(200).json({ ok: true, ...portfolio });
|
||||||
|
}
|
||||||
return res.status(200).json(baseStatus());
|
return res.status(200).json(baseStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +375,41 @@ export default async function handler(req, res) {
|
|||||||
return res.status(200).json({ ok: true, ticket });
|
return res.status(200).json({ ok: true, ticket });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.action === "record_fill") {
|
||||||
|
const fill = normalizeFill(body);
|
||||||
|
const fillErrors = validateFill(fill);
|
||||||
|
if (fillErrors.length) return res.status(400).json({ ok: false, error: "Invalid manual fill", details: fillErrors });
|
||||||
|
const saved = await recordRealFill(fill);
|
||||||
|
await recordAuditEvent("REAL_FILL_RECORDED", {
|
||||||
|
user_id: fill.userId,
|
||||||
|
ticket_id: fill.ticketId,
|
||||||
|
market_id: fill.marketId,
|
||||||
|
side: fill.side,
|
||||||
|
action: fill.action,
|
||||||
|
shares: fill.shares,
|
||||||
|
price: fill.price,
|
||||||
|
private_key_used: false,
|
||||||
|
live_order_placed_by_site: false,
|
||||||
|
});
|
||||||
|
return res.status(201).json({
|
||||||
|
ok: true,
|
||||||
|
fill: saved,
|
||||||
|
private_key_used: false,
|
||||||
|
live_order_placed_by_site: false,
|
||||||
|
note: "Manual fill recorded for tracking only. Poly Arena did not sign or place this trade.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.action === "mark_position") {
|
||||||
|
const userId = String(body.user_id || "local-readiness-user").trim();
|
||||||
|
const price = Number(body.current_price || 0);
|
||||||
|
if (!Number.isFinite(price) || price <= 0 || price >= 1) return res.status(400).json({ ok: false, error: "current_price must be between 0 and 1" });
|
||||||
|
const position = await updateRealPositionMark(userId, body.position_id, price);
|
||||||
|
if (!position) return res.status(404).json({ ok: false, error: "Position not found" });
|
||||||
|
await recordAuditEvent("REAL_POSITION_MARK_UPDATED", { user_id: userId, position_id: position.id, current_price: price });
|
||||||
|
return res.status(200).json({ ok: true, position });
|
||||||
|
}
|
||||||
|
|
||||||
const intent = body.intent || body;
|
const intent = body.intent || body;
|
||||||
const errors = validateIntent(intent);
|
const errors = validateIntent(intent);
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
|
|||||||
+95
-4
@@ -606,6 +606,17 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
<div class="card-h"><h3>Manual trade ticket queue</h3><span class="small muted">AI suggests, you sign elsewhere</span></div>
|
<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>
|
<div id="personalTicketQueue"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-h"><h3>Tracked real positions</h3><span class="small muted">manual fills only</span></div>
|
||||||
|
<div id="realPositionSummary"></div>
|
||||||
|
<div id="realPositions"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-h"><h3>Manual fill history</h3><span class="small muted">no private key used</span></div>
|
||||||
|
<div id="realFills"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
||||||
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
||||||
@@ -717,6 +728,7 @@ let PAPER_MARKET_QUERY = "";
|
|||||||
let LIVE_BACKEND_STATUS = null;
|
let LIVE_BACKEND_STATUS = null;
|
||||||
let LIVE_POLICY_STATUS = null;
|
let LIVE_POLICY_STATUS = null;
|
||||||
let PERSONAL_TICKETS = [];
|
let PERSONAL_TICKETS = [];
|
||||||
|
let REAL_PORTFOLIO = {positions:[],fills:[]};
|
||||||
let PROVIDER_CONFIG = null;
|
let PROVIDER_CONFIG = null;
|
||||||
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
||||||
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
||||||
@@ -2553,11 +2565,16 @@ async function stageSuggestionTicket(marketId){
|
|||||||
async function loadTradeTickets(){
|
async function loadTradeTickets(){
|
||||||
if(!PERSONAL_MODE)return;
|
if(!PERSONAL_MODE)return;
|
||||||
try{
|
try{
|
||||||
const r=await fetch(`/api/live?action=tickets&user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"});
|
const [ticketRes,portfolioRes]=await Promise.all([
|
||||||
const d=await r.json();
|
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"}),
|
||||||
|
]);
|
||||||
|
const d=await ticketRes.json(),pf=await portfolioRes.json();
|
||||||
PERSONAL_TICKETS=d.tickets||[];
|
PERSONAL_TICKETS=d.tickets||[];
|
||||||
}catch(e){PERSONAL_TICKETS=[];}
|
REAL_PORTFOLIO={positions:pf.positions||[],fills:pf.fills||[]};
|
||||||
|
}catch(e){PERSONAL_TICKETS=[];REAL_PORTFOLIO={positions:[],fills:[]};}
|
||||||
renderTradeTickets();
|
renderTradeTickets();
|
||||||
|
renderRealPortfolio();
|
||||||
}
|
}
|
||||||
async function updateTicketStatus(ticketId,status){
|
async function updateTicketStatus(ticketId,status){
|
||||||
try{
|
try{
|
||||||
@@ -2583,13 +2600,87 @@ function renderTradeTickets(){
|
|||||||
<div class="trade-actions">
|
<div class="trade-actions">
|
||||||
<a class="btn" href="${esc(url)}" target="_blank" rel="noopener">Open Polymarket</a>
|
<a class="btn" href="${esc(url)}" target="_blank" rel="noopener">Open Polymarket</a>
|
||||||
<button class="btn ghost" data-ticket-status="${t.id}:reviewed">Reviewed</button>
|
<button class="btn ghost" data-ticket-status="${t.id}:reviewed">Reviewed</button>
|
||||||
<button class="btn ghost" data-ticket-status="${t.id}:placed_manually">Placed manually</button>
|
|
||||||
<button class="btn ghost" data-ticket-status="${t.id}:skipped">Skipped</button>
|
<button class="btn ghost" data-ticket-status="${t.id}:skipped">Skipped</button>
|
||||||
<button class="btn ghost" data-ticket-status="${t.id}:cancelled">Cancel</button>
|
<button class="btn ghost" data-ticket-status="${t.id}:cancelled">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="account-row" style="margin-top:12px">
|
||||||
|
<select class="select" data-fill-action="${t.id}"><option>BUY</option><option>SELL</option></select>
|
||||||
|
<input class="input" type="number" min="0.01" step="0.01" placeholder="Shares" data-fill-shares="${t.id}">
|
||||||
|
<input class="input" type="number" min="0.01" max="0.99" step="0.01" placeholder="Actual price" data-fill-price="${t.id}">
|
||||||
|
<input class="input" type="number" min="0" step="0.01" placeholder="Fees" data-fill-fees="${t.id}">
|
||||||
|
<input class="input" placeholder="Tx/link note" data-fill-note="${t.id}">
|
||||||
|
<button class="btn primary" data-record-fill="${t.id}">Record manual fill</button>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
document.querySelectorAll("[data-ticket-status]").forEach(btn=>btn.onclick=()=>{const [id,status]=btn.dataset.ticketStatus.split(":");updateTicketStatus(id,status);});
|
document.querySelectorAll("[data-ticket-status]").forEach(btn=>btn.onclick=()=>{const [id,status]=btn.dataset.ticketStatus.split(":");updateTicketStatus(id,status);});
|
||||||
|
document.querySelectorAll("[data-record-fill]").forEach(btn=>btn.onclick=()=>recordTicketFill(btn.dataset.recordFill));
|
||||||
|
}
|
||||||
|
function ticketById(id){return PERSONAL_TICKETS.find(t=>String(t.id)===String(id));}
|
||||||
|
async function recordTicketFill(ticketId){
|
||||||
|
const t=ticketById(ticketId);
|
||||||
|
if(!t)return toast("Ticket not found.");
|
||||||
|
const fill={
|
||||||
|
action:"record_fill",
|
||||||
|
user_id:PERSONAL_USER_ID,
|
||||||
|
ticket_id:t.id,
|
||||||
|
agent_id:t.agent_id,
|
||||||
|
market_id:t.market_id,
|
||||||
|
question:t.question,
|
||||||
|
market_url:t.market_url,
|
||||||
|
side:t.side,
|
||||||
|
fill_action:(document.querySelector(`[data-fill-action="${ticketId}"]`)||{}).value||"BUY",
|
||||||
|
shares:Number((document.querySelector(`[data-fill-shares="${ticketId}"]`)||{}).value||0),
|
||||||
|
price:Number((document.querySelector(`[data-fill-price="${ticketId}"]`)||{}).value||0),
|
||||||
|
fees:Number((document.querySelector(`[data-fill-fees="${ticketId}"]`)||{}).value||0),
|
||||||
|
tx_note:(document.querySelector(`[data-fill-note="${ticketId}"]`)||{}).value||"",
|
||||||
|
};
|
||||||
|
try{
|
||||||
|
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(fill)});
|
||||||
|
const d=await r.json();
|
||||||
|
if(!r.ok)throw new Error(d.error||"Manual fill failed");
|
||||||
|
await loadTradeTickets();
|
||||||
|
toast("Manual fill recorded for tracking.");
|
||||||
|
}catch(e){toast(e.message||"Manual fill failed.");}
|
||||||
|
}
|
||||||
|
async function updateRealMark(positionId){
|
||||||
|
const input=document.querySelector(`[data-real-mark="${positionId}"]`);
|
||||||
|
const price=Number(input&&input.value||0);
|
||||||
|
try{
|
||||||
|
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"mark_position",user_id:PERSONAL_USER_ID,position_id:positionId,current_price:price})});
|
||||||
|
const d=await r.json();
|
||||||
|
if(!r.ok)throw new Error(d.error||"Mark update failed");
|
||||||
|
await loadTradeTickets();
|
||||||
|
toast("Position mark updated.");
|
||||||
|
}catch(e){toast(e.message||"Mark update failed.");}
|
||||||
|
}
|
||||||
|
function renderRealPortfolio(){
|
||||||
|
const summary=$("realPositionSummary"),posRoot=$("realPositions"),fillsRoot=$("realFills");
|
||||||
|
if(!summary||!posRoot||!fillsRoot)return;
|
||||||
|
const positions=REAL_PORTFOLIO.positions||[],fills=REAL_PORTFOLIO.fills||[];
|
||||||
|
const invested=positions.reduce((s,p)=>s+Number(p.cost_basis||0),0);
|
||||||
|
const marketValue=positions.reduce((s,p)=>s+Number(p.shares||0)*Number(p.current_price||p.avg_price||0),0);
|
||||||
|
const realized=positions.reduce((s,p)=>s+Number(p.realized_pnl||0),0);
|
||||||
|
const pnl=marketValue+realized-invested;
|
||||||
|
summary.innerHTML=`<div class="invest-total">
|
||||||
|
<div class="invest-num"><div class="k">Tracked value</div><div class="v">${fmtUSD(marketValue)}</div></div>
|
||||||
|
<div class="invest-num"><div class="k">Cost basis</div><div class="v">${fmtUSD(invested)}</div></div>
|
||||||
|
<div class="invest-num"><div class="k">Tracked P&L</div><div class="v ${signClass(pnl)}">${fmtUSD(pnl)}</div></div>
|
||||||
|
</div>`;
|
||||||
|
posRoot.innerHTML=positions.length?positions.map(p=>{
|
||||||
|
const mark=Number(p.current_price||p.avg_price||0),value=Number(p.shares||0)*mark,pnl=value+Number(p.realized_pnl||0)-Number(p.cost_basis||0);
|
||||||
|
return `<div class="trade-card">
|
||||||
|
<div class="trade-head"><div><div class="trade-title">${esc(p.question||p.market_id)}</div><div class="trade-meta"><span>${esc(p.side)}</span><span>${Number(p.shares||0).toFixed(2)} shares</span><span>avg ${Math.round(Number(p.avg_price||0)*100)}¢</span></div></div><b class="${signClass(pnl)}">${fmtUSD(pnl)}</b></div>
|
||||||
|
<div class="small muted">Value ${fmtUSD(value)} · Cost ${fmtUSD(Number(p.cost_basis||0))} · Realized ${fmtUSD(Number(p.realized_pnl||0))}</div>
|
||||||
|
<div class="trade-actions">
|
||||||
|
${p.market_url?`<a class="btn ghost" href="${esc(p.market_url)}" target="_blank" rel="noopener">Open market</a>`:""}
|
||||||
|
<input class="input" type="number" min="0.01" max="0.99" step="0.01" value="${mark||""}" data-real-mark="${p.id}">
|
||||||
|
<button class="btn ghost" data-update-mark="${p.id}">Update mark</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join(""):`<div class="empty">No real fills recorded yet. Record a manual fill from a ticket after you trade outside Poly Arena.</div>`;
|
||||||
|
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 saveRiskProfileToServer(s){
|
async function saveRiskProfileToServer(s){
|
||||||
const permissions=JSON.parse(JSON.stringify(s.agents||{}));
|
const permissions=JSON.parse(JSON.stringify(s.agents||{}));
|
||||||
|
|||||||
Reference in New Issue
Block a user