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:
co-authored by
Claude Opus 5.5
parent
29eaf64a6b
commit
95cc3bd23f
@@ -577,6 +577,124 @@ export function wipeAccountData(accountId: number): void {
|
||||
txn();
|
||||
}
|
||||
|
||||
// ── Trades ───────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Every position a platform's history shows — the bot's and anyone else's
|
||||
// (Vest: GET /v3/positions). The platform can't tell an API order from a
|
||||
// click, so opened_by / closed_by record what this instance did itself:
|
||||
// 'bot' when its own order opened or closed the position, NULL otherwise.
|
||||
// A wipe leaves these alone: they're refetched, but the marks aren't.
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
position_id TEXT PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL,
|
||||
account_name TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
quantity REAL NOT NULL,
|
||||
leverage REAL,
|
||||
open_price REAL,
|
||||
close_price REAL,
|
||||
pnl REAL,
|
||||
fee REAL,
|
||||
funding REAL,
|
||||
opened_at INTEGER NOT NULL,
|
||||
closed_at INTEGER,
|
||||
close_reason TEXT,
|
||||
opened_by TEXT,
|
||||
closed_by TEXT,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS trades_account_opened ON trades (account_id, opened_at);
|
||||
`);
|
||||
|
||||
/** settings key: when this instance began marking its own trades (epoch
|
||||
* ms). A position opened before it and never marked is of unknown origin;
|
||||
* one opened after it was opened outside the bot. */
|
||||
export const TRADES_SINCE_SETTING = 'trades_tracked_since';
|
||||
|
||||
export interface TradeRow {
|
||||
positionId: string;
|
||||
accountId: number;
|
||||
accountName: string;
|
||||
symbol: string;
|
||||
side: 'long' | 'short';
|
||||
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;
|
||||
closeReason: string | null;
|
||||
openedBy: string | null;
|
||||
closedBy: string | null;
|
||||
}
|
||||
|
||||
/** The platform's view of positions: inserts new ones, refreshes known ones.
|
||||
* A size it no longer reports (0) keeps the one already stored, a field it
|
||||
* leaves out keeps its value, and the bot's marks are never touched. */
|
||||
export function saveTrades(rows: Omit<TradeRow, 'openedBy' | 'closedBy'>[]): void {
|
||||
const up = db.prepare(`
|
||||
INSERT INTO trades (position_id, account_id, account_name, symbol, side, quantity, leverage, open_price, close_price,
|
||||
pnl, fee, funding, opened_at, closed_at, close_reason, updated_at)
|
||||
VALUES (@positionId, @accountId, @accountName, @symbol, @side, @quantity, @leverage, @openPrice, @closePrice,
|
||||
@pnl, @fee, @funding, @openedAt, @closedAt, @closeReason, @updatedAt)
|
||||
ON CONFLICT(position_id) DO UPDATE SET
|
||||
account_name = excluded.account_name,
|
||||
symbol = excluded.symbol,
|
||||
side = excluded.side,
|
||||
quantity = CASE WHEN excluded.quantity > 0 THEN excluded.quantity ELSE trades.quantity END,
|
||||
leverage = COALESCE(excluded.leverage, trades.leverage),
|
||||
open_price = COALESCE(excluded.open_price, trades.open_price),
|
||||
close_price = COALESCE(excluded.close_price, trades.close_price),
|
||||
pnl = COALESCE(excluded.pnl, trades.pnl),
|
||||
fee = COALESCE(excluded.fee, trades.fee),
|
||||
funding = COALESCE(excluded.funding, trades.funding),
|
||||
opened_at = excluded.opened_at,
|
||||
closed_at = COALESCE(excluded.closed_at, trades.closed_at),
|
||||
close_reason = COALESCE(excluded.close_reason, trades.close_reason),
|
||||
updated_at = excluded.updated_at
|
||||
`);
|
||||
const now = Date.now();
|
||||
const txn = db.transaction(() => {
|
||||
for (const r of rows) up.run({ ...r, updatedAt: now });
|
||||
});
|
||||
txn();
|
||||
}
|
||||
|
||||
/** This instance's own order opened (or closed) the position — stored at
|
||||
* once, before the platform's history shows it. */
|
||||
export function markTrade(
|
||||
by: 'opened' | 'closed',
|
||||
t: { positionId: string; accountId: number; accountName: string; symbol: string; side: 'long' | 'short'; quantity: number; openPrice: number | null; openedAt: number },
|
||||
): void {
|
||||
const col = by === 'opened' ? 'opened_by' : 'closed_by';
|
||||
db.prepare(`
|
||||
INSERT INTO trades (position_id, account_id, account_name, symbol, side, quantity, open_price, opened_at, ${col}, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'bot', ?)
|
||||
ON CONFLICT(position_id) DO UPDATE SET ${col} = 'bot', updated_at = excluded.updated_at
|
||||
`).run(t.positionId, t.accountId, t.accountName, t.symbol, t.side, t.quantity, t.openPrice, t.openedAt, Date.now());
|
||||
}
|
||||
|
||||
/** Newest first (by close, else open); every account when accountIds is
|
||||
* undefined. */
|
||||
export function loadTrades(o: { accountIds?: number[]; limit: number }): TradeRow[] {
|
||||
if (o.accountIds && o.accountIds.length === 0) return [];
|
||||
const where = o.accountIds ? `WHERE account_id IN (${o.accountIds.map(() => '?').join(', ')})` : '';
|
||||
return db.prepare(`
|
||||
SELECT position_id AS positionId, account_id AS accountId, account_name AS accountName, symbol, side, quantity, leverage,
|
||||
open_price AS openPrice, close_price AS closePrice, pnl, fee, funding, opened_at AS openedAt, closed_at AS closedAt,
|
||||
close_reason AS closeReason, opened_by AS openedBy, closed_by AS closedBy
|
||||
FROM trades ${where}
|
||||
ORDER BY COALESCE(closed_at, opened_at) DESC
|
||||
LIMIT ?
|
||||
`).all(...(o.accountIds ?? []), o.limit) as TradeRow[];
|
||||
}
|
||||
|
||||
// ── AutoBuyer captures ───────────────────────────────────────────────────────
|
||||
|
||||
db.exec(`
|
||||
|
||||
Reference in New Issue
Block a user