Vest (next.vestmarkets.com) is a perps prop firm: index, metal and energy
perps on CME hours, plus stocks, FX and crypto. It joins as a fourth
platform, 'vest', on the DXtrade engine paths. Those are now keyed by
isNotionalPlatform() instead of === 'dxtrade': the firm's own symbols,
percent fees, notional sizing from volatility, and a scheduler lane of its
own.
- Login is relayed, not stored. Vest signs people in with Google through
Privy and renews the session with a device key the browser won't export.
Extension 0.17.0's vest-relay.js reads the token the Vest app renews for
itself (~15 min). It renews nothing, so the tab is never logged out. The
background script posts that token to POST /api/vest/session, which holds
it in memory only. With no Vest tab open on a machine that trades Vest,
the extension opens one.
- The firm's credential is the Vest login's wallet address. The dashboard's
form offers the relayed wallet with one click.
- VestClient (lib/vest-class.ts), mapped from the web app's traffic and code
since Vest publishes no API:
- Reads the capital accounts, balances (free amount plus position
collateral), positions and open orders with the user token.
- Mints per-account tokens for orders and transactions.
- Names accounts for their rules (V50K-E-D3-T10-34, V50K-F-D3-34,
V25K-I-7) so account-type prefixes can match.
- Takes Vest's own max-drawdown and daily-loss floors. dayStartBalance =
floor + the type's limit, so the engine's room is never more than
Vest's.
- Uses each symbol's taker fee and margin rate from exchangeInfo, and
hourly volatility over in-session hours only.
- Places take-profit and stop as position legs, which Vest cancels as an
OCO pair.
- Daily P&L and fund rows come from /v3/transactions by Vest's 20:00 ET
day. A books check holds an account, naming the transaction types, when
the balance and the transactions disagree.
- scripts/check-vest.mjs (in npm run check): Vest's day across DST, account
names, parsing, volatility gaps, market hours, the session store.
Not yet run against Vest with a live session: the order and exit bodies
come from the app's code, and the transaction classification from its enum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.5 KiB
TypeScript
103 lines
4.5 KiB
TypeScript
/**
|
|
* What each firm is trading, by instrument — its open positions now and its
|
|
* entries today — for the master's fleet-wide mix.
|
|
*
|
|
* Open positions come off the client's position map, each contract named by
|
|
* the instrument it is: a DXtrade position carries its symbol; a futures one
|
|
* is looked up among the contracts this machine has entered (recorded at the
|
|
* fill, kept across restarts), TopstepX's own, and every contract the
|
|
* resolver has resolved in this process (never expiring).
|
|
* Entries are counted as they fill and kept in a setting per trade date, so
|
|
* a restart doesn't lose the day.
|
|
*/
|
|
|
|
import { getSetting, setSetting } from './db';
|
|
import { currentTradeDateCT } from './trading-logic';
|
|
import { rootOfContract } from './contract-resolver';
|
|
import { errorMessage } from './platform-client';
|
|
import type { InstrumentCount, TradeMix } from './master-protocol';
|
|
import { isNotionalPlatform } from './master-protocol';
|
|
|
|
const TODAY_KEY = 'trade_mix_today';
|
|
const NAMES_KEY = 'trade_mix_contracts';
|
|
/** Contract ids remembered at most — the front months of a few roots. */
|
|
const MAX_NAMES = 200;
|
|
|
|
type Tally = Record<string, { long: number; short: number; contracts: number }>;
|
|
interface Today {
|
|
date: string;
|
|
byFirm: Record<string, Tally>;
|
|
}
|
|
|
|
function readJson<T>(key: string): T | null {
|
|
try {
|
|
return JSON.parse(getSetting(key) ?? 'null') as T | null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function today(date: string): Today {
|
|
const saved = readJson<Today>(TODAY_KEY);
|
|
return saved?.date === date && saved.byFirm && typeof saved.byFirm === 'object' ? saved : { date, byFirm: {} };
|
|
}
|
|
|
|
/**
|
|
* Count one filled entry toward the firm's mix for today, and remember which
|
|
* instrument its contract is. Bookkeeping: it never throws into a trade.
|
|
*/
|
|
export function recordEntry(firm: string, instrument: string, side: 'Buy' | 'Sell', contracts: number, contractId?: unknown): void {
|
|
try {
|
|
const t = today(currentTradeDateCT());
|
|
const tally = (t.byFirm[firm] ??= {});
|
|
const entry = (tally[instrument] ??= { long: 0, short: 0, contracts: 0 });
|
|
if (side === 'Buy') entry.long++;
|
|
else entry.short++;
|
|
entry.contracts += Number.isFinite(contracts) ? contracts : 0;
|
|
setSetting(TODAY_KEY, JSON.stringify(t));
|
|
|
|
if (contractId !== undefined && contractId !== null && contractId !== '') {
|
|
const names = readJson<Record<string, string>>(NAMES_KEY) ?? {};
|
|
names[String(contractId)] = instrument;
|
|
const kept = Object.entries(names).slice(-MAX_NAMES);
|
|
setSetting(NAMES_KEY, JSON.stringify(Object.fromEntries(kept)));
|
|
}
|
|
} catch (err) {
|
|
console.warn(`[trade-mix] could not record an entry: ${errorMessage(err)}`);
|
|
}
|
|
}
|
|
|
|
/** The firm's open positions and today's entries, by instrument. */
|
|
export function tradeMixFor(
|
|
firm: { name: string; platform: string },
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any,
|
|
): TradeMix {
|
|
// Recorded at the fill, then TopstepX's own contracts, then every
|
|
// contract this process resolved (Tradovate's, however long ago).
|
|
const names = new Map<string, string>();
|
|
const own = typeof client.contractRoots === 'function' ? (client.contractRoots() as Map<string, string>) : null;
|
|
for (const [id, root] of own ?? []) names.set(id, root);
|
|
for (const [id, instrument] of Object.entries(readJson<Record<string, string>>(NAMES_KEY) ?? {})) names.set(id, instrument);
|
|
const instrumentOf = (contractId: unknown): string => (isNotionalPlatform(firm.platform)
|
|
? String(contractId).split('|')[0]
|
|
: names.get(String(contractId)) ?? rootOfContract(contractId as number | string) ?? `#${String(contractId)}`);
|
|
|
|
const open = new Map<string, InstrumentCount>();
|
|
for (const acc of (client.accountList ?? []) as { id: number }[]) {
|
|
const pos = client.positions?.[acc.id] as { contractId: unknown; netPos: number } | undefined;
|
|
if (!pos || !pos.netPos) continue;
|
|
const instrument = instrumentOf(pos.contractId);
|
|
const entry = open.get(instrument) ?? { instrument, long: 0, short: 0, contracts: 0 };
|
|
if (pos.netPos > 0) entry.long++;
|
|
else entry.short++;
|
|
entry.contracts += Math.abs(pos.netPos);
|
|
open.set(instrument, entry);
|
|
}
|
|
const tally = today(currentTradeDateCT()).byFirm[firm.name] ?? {};
|
|
return {
|
|
open: [...open.values()],
|
|
today: Object.entries(tally).map(([instrument, t]) => ({ instrument, long: t.long, short: t.short, contracts: t.contracts })),
|
|
};
|
|
}
|