A firm's client used to stay logged in around the clock, renewing on a fixed timer overnight and at weekends. Now, once a firm has nothing open, nothing pending and nothing left to trade before its next session for a random 15–45 minutes, its client disconnects and the dashboard shows it Resting. It logs back in a random 20–50 minutes before the next session opens, at a check-in every 3–7 Central daytime hours, or at once when trading starts, a manual leg, copy trade, manual order or AutoBuyer run needs it. Token renewals fall at random points (Tradovate 15–45 min before expiry, TopstepX every 45–150 min). Settings → Trading → Rest idle broker sessions (session_rest) turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
2.8 KiB
TypeScript
60 lines
2.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getCachedFirm } from '@/lib/firm-catalog';
|
|
import { firmInstruments, isFuturesFirm } from '@/lib/instruments';
|
|
import { getClients, isWaking, wakeFirm } from '@/lib/clients';
|
|
|
|
export async function POST(
|
|
req: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const { id: firmId } = await params;
|
|
|
|
const body = await req.json() as {
|
|
accountId: number;
|
|
symbol: string; // product name, e.g. "NQ"
|
|
qty: number;
|
|
action: 'Buy' | 'Sell';
|
|
orderType?: 'Market' | 'Limit';
|
|
price?: number;
|
|
};
|
|
|
|
const { accountId, symbol, qty, action, orderType = 'Market', price } = body;
|
|
if (!accountId || !symbol || !qty || !action) {
|
|
return NextResponse.json({ error: 'Missing required fields: accountId, symbol, qty, action' }, { status: 400 });
|
|
}
|
|
|
|
const clients = getClients();
|
|
const client = clients.get(firmId);
|
|
if (!client) return NextResponse.json({ error: 'Client not found for firm' }, { status: 404 });
|
|
// A resting session (lib/clients.ts) holds no login: start one, and
|
|
// let the caller come back once it's up.
|
|
if (wakeFirm(firmId, 'manual order') || isWaking(firmId)) {
|
|
return NextResponse.json({ error: "the firm's session is logging back in — try again in a minute" }, { status: 503 });
|
|
}
|
|
|
|
// A futures firm trades only what its instrument list (master Firms
|
|
// page) names.
|
|
const firm = getCachedFirm(firmId);
|
|
if (firm && isFuturesFirm(firm) && !firmInstruments(firm).includes(symbol)) {
|
|
return NextResponse.json({ error: `${symbol} isn't on ${firm.name}'s instrument list` }, { status: 400 });
|
|
}
|
|
|
|
// 1. Find the active contract chosen by the shared resolver
|
|
const contract = await client.findFrontMonthContract(symbol);
|
|
if (!contract) {
|
|
return NextResponse.json({ error: `Could not find an active contract for ${symbol}` }, { status: 404 });
|
|
}
|
|
console.log(`[order] active contract: ${contract.name} (id=${contract.id})`);
|
|
|
|
// 2. Place the order — resolves with the fill event (includes commission/fees)
|
|
const fill = await client.sendOrder(accountId, contract.name, qty, action, orderType, price);
|
|
console.log(`[order] fill received: price=${fill.price} qty=${fill.qty} commission=$${fill.commission} perContract=$${fill.perContractFee}`);
|
|
|
|
return NextResponse.json({ contract: { id: contract.id, name: contract.name }, fill });
|
|
} catch (err: any) {
|
|
console.error('[POST /api/firms/[id]/orders]', err);
|
|
return NextResponse.json({ error: err?.message ?? 'Order failed' }, { status: 500 });
|
|
}
|
|
}
|