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>
114 lines
4.7 KiB
JavaScript
114 lines
4.7 KiB
JavaScript
const FIELDS = ['apiBase', 'pollSeconds', 'elementSelector'];
|
|
const DEFAULTS = {
|
|
apiBase: 'http://localhost:3000',
|
|
pollSeconds: 3,
|
|
elementSelector: '',
|
|
};
|
|
|
|
async function load() {
|
|
const cfg = { ...DEFAULTS, ...(await chrome.storage.local.get(FIELDS)) };
|
|
for (const f of FIELDS) document.getElementById(f).value = cfg[f];
|
|
render();
|
|
}
|
|
|
|
async function render() {
|
|
const { state = {} } = await chrome.storage.local.get('state');
|
|
|
|
// There is nothing to switch on: either it can reach the dashboard or it
|
|
// cannot. `waiting` below says why a tick captured nothing.
|
|
const color = state.connected ? '#22c55e' : '#ef4444';
|
|
const label = state.connected ? 'Capturing' : 'Disconnected';
|
|
|
|
const cap = state.lastCapture;
|
|
const detail = state.waiting
|
|
? `<div class="muted">${esc(state.waiting)}</div>`
|
|
: cap
|
|
? `<div class="muted">${esc(cap.title || cap.url)}<br>${(cap.bytes / 1024).toFixed(1)} KB · ${new Date(cap.at).toLocaleTimeString()}</div>`
|
|
: '';
|
|
|
|
document.getElementById('status').innerHTML =
|
|
`<span class="dot" style="background:${color}"></span><strong>${label}</strong>` +
|
|
detail +
|
|
(state.vest ? `<div class="muted">Vest: ${esc(state.vest)}</div>` : '') +
|
|
(state.error ? `<div class="err">${esc(state.error)}</div>` : '');
|
|
}
|
|
|
|
document.getElementById('save').addEventListener('click', async () => {
|
|
const patch = {};
|
|
for (const f of FIELDS) patch[f] = document.getElementById(f).value.trim();
|
|
patch.pollSeconds = Number(patch.pollSeconds) || DEFAULTS.pollSeconds;
|
|
patch.apiBase = patch.apiBase || DEFAULTS.apiBase;
|
|
await chrome.storage.local.set(patch);
|
|
setTimeout(render, 400);
|
|
});
|
|
|
|
document.getElementById('now').addEventListener('click', () => {
|
|
chrome.runtime.sendMessage({ type: 'capture-now' }, () => setTimeout(render, 200));
|
|
});
|
|
|
|
document.getElementById('tsCheck').addEventListener('click', () => {
|
|
// Kick off the scan and return. The worker writes progress and the result to
|
|
// storage; renderTopstepState (below) reflects whatever is there, so the
|
|
// popup can be closed and reopened mid-scan without losing the answer. The
|
|
// callback only acknowledges the request was received — it carries no result.
|
|
chrome.runtime.sendMessage({ type: 'topstep-check' }, () => void chrome.runtime.lastError);
|
|
});
|
|
|
|
/** Paint the Topstep panel from the stored scan state. */
|
|
function renderTopstepState(state) {
|
|
const btn = document.getElementById('tsCheck');
|
|
const out = document.getElementById('tsResult');
|
|
if (!state) { out.innerHTML = ''; return; }
|
|
|
|
if (state.running) {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Checking…';
|
|
out.innerHTML = '<div class="muted">Scanning the Topstep dashboard…</div>';
|
|
return;
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Check activatable accounts';
|
|
|
|
if (state.error) {
|
|
out.innerHTML = `<div class="err">${esc(state.error)}</div>`;
|
|
return;
|
|
}
|
|
const r = state.result;
|
|
if (!r) { out.innerHTML = ''; return; }
|
|
if (r.signedOut) {
|
|
out.innerHTML = '<div class="err">Not signed in to Topstep. Open the dashboard, sign in, then check again.</div>';
|
|
return;
|
|
}
|
|
if (r.error) {
|
|
out.innerHTML = `<div class="err">${esc(r.error)}</div>`;
|
|
return;
|
|
}
|
|
|
|
const n = r.activatableCount || 0;
|
|
const acctUrl = (id) => `https://dashboard.topstep.com/dashboard/accounts/${encodeURIComponent(id)}`;
|
|
const list = (r.activatable || [])
|
|
.map((a) => `<li><a href="${acctUrl(a.id)}" target="_blank" rel="noopener">${esc(a.name)}</a>${a.type ? ` · ${esc(a.type)}` : ''}</li>`)
|
|
.join('');
|
|
const when = r.checkedAt ? ` · ${new Date(r.checkedAt).toLocaleTimeString()}` : '';
|
|
out.innerHTML =
|
|
`<div class="big">${n} activatable</div>` +
|
|
`<div class="muted">${r.alreadyActivated || 0} already activated · ${r.passedTotal || 0} passed total${when}</div>` +
|
|
(list ? `<ul>${list}</ul>` : '<div class="muted" style="margin-top:6px">No passed accounts waiting on activation.</div>');
|
|
}
|
|
|
|
// Render whatever the last scan left behind when the popup opens…
|
|
chrome.storage.local.get('topstepScan').then(({ topstepScan }) => renderTopstepState(topstepScan));
|
|
// …and keep up with a scan that finishes while the popup is open.
|
|
chrome.storage.onChanged.addListener((c, area) => {
|
|
if (area === 'local' && 'topstepScan' in c) renderTopstepState(c.topstepScan.newValue);
|
|
});
|
|
|
|
chrome.storage.onChanged.addListener((c, area) => { if (area === 'local' && 'state' in c) render(); });
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/[&<>"]/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[ch]));
|
|
}
|
|
|
|
load();
|