On Vest, a claim lowers the daily-loss floor by the amount claimed. Winning $1,500 and claiming it leaves $3,000 of room the same day, the full drawdown. The bot now uses that. - Payout claims: a scheduler step (runClaims, under the lane mutex, before the flat gate) claims every payoutable account that reached its stage's target, via a platform that takes payouts by API. The claim is all profit above the starting balance. It then clears the account's legs and entry outcome for the day so it can trade its next stage. Failures retry after 5 minutes. The auto_claim setting 'off' disables it. - VestClient.claimProfit: POST /v3/capital/withdraw to the login's main account (account_type 1 on /v3/accounts). The payout is booked at once: today's fund row and the cycle cut, with today's P&L counted from the post-claim balance. The history read later rebooks it from Vest's WITHDRAW row, and the books check counts a claim until that row shows. - Same-day stages: an account type can pick its stage by the payouts since the firm's last daily reset (stagesSameDay, sameDayClaims). A stage can carry its own daily loss limit. - That limit replaces the type's everywhere room, stops and last stands are worked out: the batch, the manual leg, copy trade, the dashboard and the report. - Room from the platform's own floor: dailyLossFloor (Vest) makes the room min(stage limit, balance − Vest's floor). A same-day claim that lowers the floor therefore frees room, and the room is never more than Vest allows. VestClient.dayStartBalance is gone. - Before the 8 PM ET reset: from 7:55 PM ET, each open Vest position on an account with a daily limit is fitted to the room the reset will leave (resetSafeStop). Its stop comes in via PUT/POST stop-loss, or the position closes when there is no room. - Checks for same-day stages, the stage limit, floorRoom, the post-claim plan, the next reset across both time changes, and resetSafeStop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
194 lines
8.8 KiB
TypeScript
194 lines
8.8 KiB
TypeScript
/**
|
|
* An account's next leg, sized on every instrument its DXtrade client last
|
|
* surveyed — the numbers the local dashboard's hold estimate and the
|
|
* master's Firms page show. Same rule as the trade: planLegSize for the size,
|
|
* rankForTarget for the order (the cheapest that reaches the target hold,
|
|
* else the quickest), so what is shown is what a batch would pick.
|
|
*/
|
|
|
|
import { getAccountConfig } from './account-type-resolver';
|
|
import type { PlatformContract } from './platform-client';
|
|
import type { SymbolStats } from './master-protocol';
|
|
import {
|
|
combineCmeSessions, computeDayPlan, resolveEffectiveConfig, obfuscationStopDollars, dllRoom as computeDllRoom,
|
|
planLegSize, rankForTarget, maxLossRoom, lastStand, dllStopDollars, stageInputsFor, floorRoom,
|
|
FALLBACK_COMMISSION_PER_CONTRACT,
|
|
} from './trading-logic';
|
|
|
|
/**
|
|
* The day's hard-loss room as the engine's dailyLossGate works it out, for
|
|
* the dashboard paths: against the platform's own floor where it states one
|
|
* (Vest), else the day's P&L. null = not known yet (or no limit, dll ≤ 0).
|
|
*/
|
|
export function dayLossRoom(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any,
|
|
accountId: number,
|
|
dll: number,
|
|
cash: { amount: number; realizedPnL: number },
|
|
combinedRealized: number,
|
|
): number | null {
|
|
if (!(dll > 0)) return null;
|
|
const floor: number | null | undefined = client.dailyLossFloor?.(accountId);
|
|
if (floor === null) return null;
|
|
if (typeof floor === 'number') return floorRoom({ dailyLossLimit: dll, closedBalance: cash.amount, floor });
|
|
const dayStart = client.dayStartBalance?.(accountId);
|
|
return dayStart === null ? null : computeDllRoom({
|
|
dailyLossLimit: dll, closedBalance: cash.amount, dayStartBalance: dayStart,
|
|
realizedToday: Math.min(cash.realizedPnL, combinedRealized),
|
|
});
|
|
}
|
|
import { sizingHoldHours } from './person-size';
|
|
|
|
export type CandidateLeg = SymbolStats['symbols'][number] & {
|
|
swapReserve: number;
|
|
leverage: number | null;
|
|
winProbabilityNoFees: number | null;
|
|
};
|
|
|
|
interface FirmSizing {
|
|
commissionPct?: number;
|
|
targetHoldHours?: number;
|
|
financingPctPerDay?: number;
|
|
}
|
|
|
|
/** The next leg of `leg` dollars on each surveyed candidate, best first. */
|
|
export function rankNextLeg(o: {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any;
|
|
accountId: number;
|
|
cfg: { accountSize: number; maxLoss?: number; dailyLossLimit?: number };
|
|
firm: FirmSizing;
|
|
leg: number;
|
|
/** null = daily loss limit set but its day start isn't known yet. */
|
|
dllRoom: number | null;
|
|
obfuscationDollars: number;
|
|
cash: { amount: number; availableFunds?: number };
|
|
/** The trade date the hold is drawn for (sizingHoldHours); default today. */
|
|
tradeDate?: string;
|
|
}): CandidateLeg[] {
|
|
const candidates: PlatformContract[] = o.client.lastCandidates ?? [];
|
|
if (!(o.leg > 0) || candidates.length === 0) return [];
|
|
const target = sizingHoldHours(o.firm, o.tradeDate);
|
|
const floorRoom = maxLossRoom({
|
|
balance: o.cash.amount, accountSize: o.cfg.accountSize, maxLoss: o.cfg.maxLoss ?? 0,
|
|
autoLiqThreshold: o.client.autoLiqThresholds?.[o.accountId],
|
|
});
|
|
// The next leg opens flat: while one is open its margin is tied up, so
|
|
// size against the balance then.
|
|
const funds = o.client.positions?.[o.accountId] ? o.cash.amount : (o.cash.availableFunds ?? o.cash.amount);
|
|
const sized = candidates.flatMap((c): CandidateLeg[] => {
|
|
if (!c.volatilityPerHour) return [];
|
|
const plan = (costs: boolean) => planLegSize({
|
|
leg: o.leg,
|
|
contract: costs ? c : { ...c, spreadFraction: 0 },
|
|
commissionPct: costs ? (c.commissionPct ?? o.firm.commissionPct ?? 0) : 0,
|
|
targetHoldHours: target,
|
|
financingPctPerDay: costs ? (o.firm.financingPctPerDay ?? 0) : 0,
|
|
dailyLossLimit: o.cfg.dailyLossLimit ?? 0,
|
|
// Without fees the day's room would also have its fees back — but
|
|
// only this leg's are known, so the room stays as it is.
|
|
dllRoom: o.dllRoom ?? Infinity,
|
|
maxLossRoom: floorRoom,
|
|
obfuscationDollars: o.obfuscationDollars,
|
|
availableFunds: funds,
|
|
});
|
|
const p = plan(true);
|
|
if (!p.sizing || p.cost === null) return [];
|
|
const leverage = c.marginRate ? Math.round(10 / c.marginRate) / 10 : null;
|
|
return [{
|
|
symbol: String(c.id),
|
|
notional: Math.round(p.sizing.notional),
|
|
multiplier: o.cfg.accountSize > 0 ? Math.round((p.sizing.notional / o.cfg.accountSize) * 100) / 100 : 0,
|
|
maxMultiplier: leverage,
|
|
leverage,
|
|
holdHours: Math.round(p.sizing.predictedHoldHours * 10) / 10,
|
|
cost: Math.round(p.cost),
|
|
winProbability: p.sizing.winProbability,
|
|
winProbabilityNoFees: plan(false).sizing?.winProbability ?? null,
|
|
volatilityPerDay: c.volatilityPerHour * Math.sqrt(24),
|
|
spreadFraction: c.spreadFraction ?? 0,
|
|
swapReserve: p.sizing.swapReserve,
|
|
}];
|
|
});
|
|
return rankForTarget(sized, target).map((i) => sized[i]);
|
|
}
|
|
|
|
/** An account's day plan and daily-loss room, worked out from scratch the
|
|
* way the dashboard does. null without an account type or a plan. */
|
|
function accountPlan(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any,
|
|
acc: { id: number },
|
|
tradeDate: string,
|
|
) {
|
|
const cfg = getAccountConfig(acc.id).accountType;
|
|
if (!cfg) return null;
|
|
const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
|
|
const combined = combineCmeSessions(cfg.combineCmeHolidays === true, client.dailyPnL[acc.id] ?? [], tradeDate, cash.realizedPnL);
|
|
const effective = resolveEffectiveConfig(
|
|
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages,
|
|
client.priorProfit?.[acc.id] ?? 0, client.fundTransactions?.[acc.id] ?? [], cfg.minDayPnL,
|
|
stageInputsFor(client, acc.id, cfg),
|
|
);
|
|
const plan = computeDayPlan(
|
|
acc.id, combined.tradeDate, effective.profitTarget, effective.consistency,
|
|
combined.dailyPnL, effective.minDayPnL, effective.minTradingDays, cash.amount - cfg.accountSize, combined.realizedPnL,
|
|
cfg.splitEnabled, effective.drain,
|
|
);
|
|
if (!plan) return null;
|
|
const dll = effective.dailyLossLimit;
|
|
const room = dayLossRoom(client, acc.id, dll, cash, combined.realizedPnL);
|
|
const obfuscation = obfuscationStopDollars(cfg, combined.dailyPnL, plan, combined.tradeDate, plan.remainingToday);
|
|
return { cfg, cash, plan, dll, room, obfuscation };
|
|
}
|
|
|
|
/**
|
|
* The same for an account worked out from scratch — its day plan, daily-loss
|
|
* room and stops as the dashboard computes them. null when it has no
|
|
* account type, no plan, or nothing left to trade today.
|
|
*/
|
|
export function accountNextLeg(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any,
|
|
acc: { id: number },
|
|
firm: FirmSizing,
|
|
tradeDate: string,
|
|
): { leg: number; lastStand: boolean; candidates: CandidateLeg[] } | null {
|
|
const p = accountPlan(client, acc, tradeDate);
|
|
if (!p || !(p.plan.remainingToday > 0)) return null;
|
|
const floorRoom = maxLossRoom({
|
|
balance: p.cash.amount, accountSize: p.cfg.accountSize, maxLoss: p.cfg.maxLoss ?? 0,
|
|
autoLiqThreshold: client.autoLiqThresholds?.[acc.id],
|
|
});
|
|
return {
|
|
leg: p.plan.remainingToday,
|
|
lastStand: p.room !== null && lastStand({ dllRoom: p.room, dailyLossLimit: p.dll, maxLossRoom: floorRoom }),
|
|
candidates: rankNextLeg({
|
|
client, accountId: acc.id, cfg: { ...p.cfg, dailyLossLimit: p.dll }, firm, leg: p.plan.remainingToday, dllRoom: p.room,
|
|
obfuscationDollars: p.obfuscation, cash: p.cash, tradeDate,
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A futures account's next leg as the Auto draw sizes its hold: the day's
|
|
* remaining target, its stop (the tighter of the obfuscation and daily-loss
|
|
* stops; Infinity = none) and the account type's max contracts. null with
|
|
* nothing left to trade today.
|
|
*/
|
|
export function accountLegShape(
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
client: any,
|
|
acc: { id: number },
|
|
tradeDate: string,
|
|
): { leg: number; stopDollars: number; maxPositionSize: number } | null {
|
|
const p = accountPlan(client, acc, tradeDate);
|
|
if (!p || !(p.plan.remainingToday > 0)) return null;
|
|
const obf = Number.isFinite(p.obfuscation) && p.obfuscation > 0 ? p.obfuscation : Infinity;
|
|
const hard = p.room !== null && p.dll > 0
|
|
? dllStopDollars({ room: p.room, dailyLossLimit: p.dll, entryFee: FALLBACK_COMMISSION_PER_CONTRACT, commissionPct: 0 })
|
|
: Infinity;
|
|
return { leg: p.plan.remainingToday, stopDollars: Math.min(obf, hard), maxPositionSize: p.cfg.maxPositionSize };
|
|
}
|