Trades: an open position shows no close price or P&L

Vest's position history gives an open position a placeholder close price
and a $0 P&L (290.25 on an NDX short at 29,538.96). Only a closed
position keeps them now — in the parser, in the stored row (a placeholder
already stored is cleared on the next read) and in the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-26 20:02:11 -05:00
co-authored by Claude Opus 5.5
parent a03bfb5605
commit 016444239e
4 changed files with 20 additions and 8 deletions
+2 -2
View File
@@ -41,8 +41,8 @@ export async function GET(req: Request) {
side: t.side, side: t.side,
quantity: t.quantity, quantity: t.quantity,
openPrice: t.openPrice, openPrice: t.openPrice,
closePrice: t.closePrice, closePrice: open ? null : t.closePrice,
pnl: t.pnl, pnl: open ? null : t.pnl,
fee: t.fee, fee: t.fee,
funding: t.funding, funding: t.funding,
net, net,
+4 -2
View File
@@ -652,8 +652,10 @@ export function saveTrades(rows: Omit<TradeRow, 'openedBy' | 'closedBy'>[]): voi
quantity = CASE WHEN excluded.quantity > 0 THEN excluded.quantity ELSE trades.quantity END, quantity = CASE WHEN excluded.quantity > 0 THEN excluded.quantity ELSE trades.quantity END,
leverage = COALESCE(excluded.leverage, trades.leverage), leverage = COALESCE(excluded.leverage, trades.leverage),
open_price = COALESCE(excluded.open_price, trades.open_price), open_price = COALESCE(excluded.open_price, trades.open_price),
close_price = COALESCE(excluded.close_price, trades.close_price), close_price = CASE WHEN COALESCE(excluded.closed_at, trades.closed_at) IS NULL THEN NULL
pnl = COALESCE(excluded.pnl, trades.pnl), ELSE COALESCE(excluded.close_price, trades.close_price) END,
pnl = CASE WHEN COALESCE(excluded.closed_at, trades.closed_at) IS NULL THEN NULL
ELSE COALESCE(excluded.pnl, trades.pnl) END,
fee = COALESCE(excluded.fee, trades.fee), fee = COALESCE(excluded.fee, trades.fee),
funding = COALESCE(excluded.funding, trades.funding), funding = COALESCE(excluded.funding, trades.funding),
opened_at = excluded.opened_at, opened_at = excluded.opened_at,
+7 -4
View File
@@ -313,9 +313,12 @@ export function parseHistoryPosition(raw: unknown): VestTrade | null {
if (!p || typeof id !== 'string' || typeof p.symbol !== 'string') return null; if (!p || typeof id !== 'string' || typeof p.symbol !== 'string') return null;
const openedAt = toMs(p.openDate ?? p.open_date ?? p.createdAt ?? p.created_at); const openedAt = toMs(p.openDate ?? p.open_date ?? p.createdAt ?? p.created_at);
if (!Number.isFinite(openedAt)) return null; if (!Number.isFinite(openedAt)) return null;
const closedAt = toMs(p.closeDate ?? p.close_date); const closedMs = toMs(p.closeDate ?? p.close_date);
const closedAt = Number.isFinite(closedMs) && closedMs > 0 ? closedMs : null;
const openPrice = num(p.openPrice ?? p.open_price); const openPrice = num(p.openPrice ?? p.open_price);
const closePrice = num(p.closePrice ?? p.close_price); // An open position comes with a placeholder close price and a $0 P&L
// (seen: 290.25 on an NDX short at 29,538.96): only a close has them.
const closePrice = closedAt === null ? undefined : num(p.closePrice ?? p.close_price);
const reason = p.closeReason ?? p.close_reason; const reason = p.closeReason ?? p.close_reason;
return { return {
positionId: id, positionId: id,
@@ -325,11 +328,11 @@ export function parseHistoryPosition(raw: unknown): VestTrade | null {
leverage: num(p.leverage) ?? null, leverage: num(p.leverage) ?? null,
openPrice: openPrice !== undefined && openPrice > 0 ? openPrice : null, openPrice: openPrice !== undefined && openPrice > 0 ? openPrice : null,
closePrice: closePrice !== undefined && closePrice > 0 ? closePrice : null, closePrice: closePrice !== undefined && closePrice > 0 ? closePrice : null,
pnl: num(p.pnl) ?? null, pnl: closedAt === null ? null : num(p.pnl) ?? null,
fee: num(p.fee) ?? null, fee: num(p.fee) ?? null,
funding: num(p.funding) ?? null, funding: num(p.funding) ?? null,
openedAt, openedAt,
closedAt: Number.isFinite(closedAt) && closedAt > 0 ? closedAt : null, closedAt,
closeReason: typeof reason === 'string' && reason !== '' ? reason : null, closeReason: typeof reason === 'string' && reason !== '' ? reason : null,
}; };
} }
+7
View File
@@ -293,6 +293,13 @@ test('parseHistoryPosition: open, snake_case, seconds, and junk', () => {
assert.equal(open.openedAt, 1793065590000); assert.equal(open.openedAt, 1793065590000);
assert.equal(open.closedAt, null); assert.equal(open.closedAt, null);
assert.equal(open.closeReason, null); assert.equal(open.closeReason, null);
const held = parseHistoryPosition({
positionId: 'p3', symbol: 'NDX-USD-PERP', side: 'short', quantity: '85.7363', openPrice: '29538.96', closePrice: '290.25',
pnl: '0.0', fee: '63.314059', funding: '0.0', openDate: 1793065422000, closeDate: null,
});
assert.equal(held.closePrice, null, "an open position's placeholder close price is dropped");
assert.equal(held.pnl, null, 'and its $0 P&L');
assert.equal(held.fee, 63.314059, 'its entry fee is real');
assert.equal(parseHistoryPosition({ symbol: 'GC-PERP', openDate: 1 }), null, 'no id'); assert.equal(parseHistoryPosition({ symbol: 'GC-PERP', openDate: 1 }), null, 'no id');
assert.equal(parseHistoryPosition({ positionId: 'p2', symbol: 'GC-PERP' }), null, 'no open time'); assert.equal(parseHistoryPosition({ positionId: 'p2', symbol: 'GC-PERP' }), null, 'no open time');
assert.equal(parseHistoryPosition(null), null); assert.equal(parseHistoryPosition(null), null);