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>
57 lines
2.5 KiB
TypeScript
57 lines
2.5 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { getClients } from '@/lib/clients';
|
|
import { loadTrades, getSetting, TRADES_SINCE_SETTING } from '@/lib/db';
|
|
import type { TradeView } from '@/types';
|
|
|
|
/**
|
|
* GET /api/trades?accountId=… | ?firmId=… [&limit=…] — recorded positions,
|
|
* newest first: one account, a firm's current accounts, or all of them.
|
|
*
|
|
* Who opened and closed each one: the bot marks its own orders as it sends
|
|
* them, so after marking began (TRADES_SINCE_SETTING) anything unmarked was
|
|
* someone else's. A close Vest calls "user_requested" is an order on the
|
|
* account — the bot's if it marked it, else outside.
|
|
*/
|
|
export async function GET(req: Request) {
|
|
const url = new URL(req.url);
|
|
const accountId = Number(url.searchParams.get('accountId'));
|
|
const firmId = url.searchParams.get('firmId');
|
|
const limit = Math.min(Math.max(Math.floor(Number(url.searchParams.get('limit'))) || 200, 1), 1000);
|
|
|
|
let accountIds: number[] | undefined;
|
|
if (Number.isFinite(accountId) && accountId > 0) accountIds = [accountId];
|
|
else if (firmId) accountIds = getClients().get(firmId)?.accountList.map((a) => a.id) ?? [];
|
|
|
|
const since = Number(getSetting(TRADES_SINCE_SETTING) ?? NaN);
|
|
const tracked = (at: number) => Number.isFinite(since) && at >= since;
|
|
|
|
const trades: TradeView[] = loadTrades({ accountIds, limit }).map((t) => {
|
|
const open = t.closedAt === null && t.closeReason === null && t.closedBy === null;
|
|
const closedBy = open ? null
|
|
: t.closedBy === 'bot' ? 'bot'
|
|
: t.closeReason === 'user_requested' ? (tracked(t.closedAt ?? t.openedAt) ? 'outside' : 'user_requested')
|
|
: t.closeReason ?? 'unknown';
|
|
const net = open || t.pnl === null ? null
|
|
: Math.round((t.pnl - Math.abs(t.fee ?? 0) + (t.funding ?? 0)) * 100) / 100;
|
|
return {
|
|
positionId: t.positionId,
|
|
accountId: t.accountId,
|
|
accountName: t.accountName,
|
|
symbol: t.symbol,
|
|
side: t.side,
|
|
quantity: t.quantity,
|
|
openPrice: t.openPrice,
|
|
closePrice: t.closePrice,
|
|
pnl: t.pnl,
|
|
fee: t.fee,
|
|
funding: t.funding,
|
|
net,
|
|
openedAt: t.openedAt,
|
|
closedAt: t.closedAt,
|
|
openedBy: t.openedBy === 'bot' ? 'bot' : tracked(t.openedAt) ? 'outside' : null,
|
|
closedBy,
|
|
};
|
|
});
|
|
return NextResponse.json(trades);
|
|
}
|