Vest: record every trade, the bot's and outside ones, and list them

Vest's position history (GET /v3/positions) gives each position's size,
open and close price, P&L, fees, funding and why it closed. The client
reads it hourly and a minute after a position closes (or a flat balance
moves) into a new trades table, and stores open positions as they
appear. Vest can't tell an API order from a click, so the bot marks its
own opens and closes as it sends them; an unmarked trade since marking
began was someone else's.

The dashboard's Vest firm row gets a Trades button listing every trade
on its accounts with who opened it and how it closed; the account page
shows its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-26 19:23:24 -05:00
co-authored by Claude Opus 5.5
parent 29eaf64a6b
commit 95cc3bd23f
10 changed files with 603 additions and 4 deletions
+51
View File
@@ -283,6 +283,57 @@ export function parsePosition(raw: unknown): VestPosition | null {
};
}
/**
* One position from GET /v3/positions — Vest's position history, the app's
* History tab: closed positions with how they ended (an open one has no
* close yet). pnl is Vest's realized P&L before fees and funding.
*/
export interface VestTrade {
positionId: string;
symbol: string;
side: 'long' | 'short';
/** 0 when Vest doesn't say (a store keeps the size it already knows). */
quantity: number;
leverage: number | null;
openPrice: number | null;
closePrice: number | null;
pnl: number | null;
fee: number | null;
funding: number | null;
openedAt: number;
closedAt: number | null;
/** user_requested, stop_loss, take_profit, liquidation, auto_deleverage,
* temporary_leverage_limit, corporate_action. */
closeReason: string | null;
}
export function parseHistoryPosition(raw: unknown): VestTrade | null {
const p = raw as Record<string, unknown>;
const id = p?.positionId ?? p?.position_id;
if (!p || typeof id !== 'string' || typeof p.symbol !== 'string') return null;
const openedAt = toMs(p.openDate ?? p.open_date ?? p.createdAt ?? p.created_at);
if (!Number.isFinite(openedAt)) return null;
const closedAt = toMs(p.closeDate ?? p.close_date);
const openPrice = num(p.openPrice ?? p.open_price);
const closePrice = num(p.closePrice ?? p.close_price);
const reason = p.closeReason ?? p.close_reason;
return {
positionId: id,
symbol: p.symbol,
side: String(p.side).toLowerCase() === 'short' ? 'short' : 'long',
quantity: Math.abs(num(p.quantity) ?? 0),
leverage: num(p.leverage) ?? null,
openPrice: openPrice !== undefined && openPrice > 0 ? openPrice : null,
closePrice: closePrice !== undefined && closePrice > 0 ? closePrice : null,
pnl: num(p.pnl) ?? null,
fee: num(p.fee) ?? null,
funding: num(p.funding) ?? null,
openedAt,
closedAt: Number.isFinite(closedAt) && closedAt > 0 ? closedAt : null,
closeReason: typeof reason === 'string' && reason !== '' ? reason : null,
};
}
/** Exit legs still waiting to trigger. */
export function awaitingLegs(p: VestPosition): VestLeg[] {
return [...p.stopLosses, ...p.takeProfits].filter((l) => l.status === 'awaiting');