Vest: a split day's next leg gives back the day's gain plus the limit

On a split day the next Vest leg's daily-loss room now adds the day's gain
still in the account to the bot's limit (floorRoom's `gain`), never past
Vest's own floor, and the leg sizes up for the same target hold (Dingo's
next eval leg: $455k -> $842k at 1.5 h, win 28% -> 56%). Other platforms
are unchanged: dllRoom still never widens on a green day.

The gain only widens a leg all but sure to close before Vest's 8 PM ET
reset (resetClearStop, held to the shortest leg the day may carve), and
none of a hand claim's profit counts. The last-stand margin is measured
against the limit, so a green day can't drop a leg's stop.

Around the reset: no new Vest position opens in the last 7 minutes, and
the guard re-fits every 15 s on fresh positions instead of one pass. An
account's positions share the next-day room (stops first, least at risk
first; locked-in profit counts), a hand-moved stop is fitted again, and a
close or profit-side stop that doesn't take is retried or closed.

The dashboard's next stop now reserves for the leg's notional, as fireLeg
does, so DXtrade labels read smaller (display only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 07:21:52 -05:00
co-authored by Claude Opus 5.5
parent 1e844178de
commit f0c1569d96
9 changed files with 364 additions and 89 deletions
+6 -5
View File
@@ -18,7 +18,7 @@ import {
BIG_DAY_THRESHOLD, LEG_COMPLETION_EPSILON, MAX_LEGS_PER_DAY,
FALLBACK_COMMISSION_PER_CONTRACT, MAX_COMMISSION_PER_CONTRACT, DLL_MIN_STOP,
CME_EVENING_OPEN_MIN, foldReason, withinFirmCaps,
fundRowsKey, markFinishedCycle, unbookedPayoutDrop, machineSide, stageInputsFor, floorRoom,
fundRowsKey, markFinishedCycle, unbookedPayoutDrop, machineSide, stageInputsFor, floorRoom, shortestNextLeg,
type DayPlan, type FinishedCycleMark,
} from './trading-logic';
import { getSetting, setSetting, loadAccountMeta, saveAccountMeta } from './db';
@@ -621,7 +621,7 @@ function dailyLossGate(
cfg: { maxPositionSize: number; firmId: string },
cash: { amount: number; realizedPnL: number },
combinedRealized: number,
remainingToday: number,
plan: { remainingToday: number; splitAllowed: boolean },
singleContract: boolean,
): { room: number } | { reason: string } {
if (!(dll > 0)) return { room: Infinity };
@@ -634,6 +634,7 @@ function dailyLossGate(
room = floorRoom({
dailyLossLimit: dll, closedBalance: cash.amount, floor,
realizedToday: Math.min(cash.realizedPnL, combinedRealized),
gain: client.dayGainForRoom?.(accountId, { leg: shortestNextLeg(plan), limit: dll }) ?? 0,
});
} else {
const dayStart: number | null | undefined = client.dayStartBalance?.(accountId);
@@ -651,7 +652,7 @@ function dailyLossGate(
// could use. Percent-fee firms size by notional, which isn't known
// here — fireLeg re-checks with the real size before any entry.
const contractsBound = singleContract ? 1
: Math.min(Math.max(1, Math.ceil(remainingToday / 1000)), cfg.maxPositionSize > 0 ? cfg.maxPositionSize : Infinity);
: Math.min(Math.max(1, Math.ceil(plan.remainingToday / 1000)), cfg.maxPositionSize > 0 ? cfg.maxPositionSize : Infinity);
const percentFees = (getCachedFirm(cfg.firmId)?.commissionPct ?? 0) > 0;
const feeEstimate = percentFees ? 0 : 2 * FALLBACK_COMMISSION_PER_CONTRACT * contractsBound;
if (!dllViable(room, dll, feeEstimate)) {
@@ -837,7 +838,7 @@ function evaluateAccountForTrade(client: any, acc: { id: number; name: string; a
// (day done, challenge complete, leg cap) win. A daily-loss stop-out
// leaves about the reserve plus fees, which is below what a trade needs —
// so a stop-out benches the account for the rest of the day by itself.
const gate = dailyLossGate(client, acc.id, effective.dailyLossLimit, cfg, cash, combined.realizedPnL, plan.remainingToday, isExtraDay || effective.drain);
const gate = dailyLossGate(client, acc.id, effective.dailyLossLimit, cfg, cash, combined.realizedPnL, plan, isExtraDay || effective.drain);
if ('reason' in gate) return { ...evaluated, eligible: false, reason: gate.reason };
if (Number.isFinite(gate.room)) return { ...evaluated, eligible: true, reason: '', dllRoom: gate.room };
@@ -1868,7 +1869,7 @@ export async function fireManualLeg(accountId: number, action: 'Buy' | 'Sell' |
// The once-a-day return comes before the daily-loss check: run it here.
let room = ev.dllRoom;
if (!ev.eligible) {
const gate = dailyLossGate(client, acc.id, ev.dll, cfg, ev.cash, ev.cash.realizedPnL, ev.plan.remainingToday, ev.drain);
const gate = dailyLossGate(client, acc.id, ev.dll, cfg, ev.cash, ev.cash.realizedPnL, ev.plan, ev.drain);
if ('reason' in gate) throw new Error(gate.reason);
room = gate.room;
}