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
+57 -13
View File
@@ -1339,17 +1339,6 @@ export function dllReserve(dailyLossLimit: number, notional = 0): number {
* anything tighter just spends the day's room on fees. */
export const DLL_MIN_STOP = 50;
/**
* How much the firm's day may still lose before a hard daily-loss breach.
* Infinity when the account type has no limit.
*
* The day's P&L is the closed balance against the platform's snapshot at the
* firm's daily reset when the client knows it (DXTrade: 22:00 UTC), else the
* broker's realized P&L for the session. Only a red day counts: a green day
* does NOT widen the room past the limit, because a position still open at
* the next reset has its whole move counted against the NEW day — room
* borrowed from today's profit would be a breach tomorrow.
*/
/** The stage inputs for one account: the type's daily loss limit, and for a
* type whose stages reset each day, the client's count of today's payouts. */
export function stageInputsFor(
@@ -1365,6 +1354,37 @@ export function stageInputsFor(
};
}
/** A leg is given this many of its target holds to be done before a daily
* reset counts its open loss against the next day: under a fair walk, a leg
* whose take-profit and stop are within 2:1 is still open after four holds
* about 1–1.5% of the time, and the reset guard fits those. */
export const RESET_CLEAR_HOLDS = 4;
/** The shortest take-profit the plan's next leg may have — what
* resetClearStop is held to: a split day can carve a leg as small as
* MIN_LEG (pickLegAmount), else the leg is all that remains. */
export function shortestNextLeg(plan: { remainingToday: number; splitAllowed: boolean }): number {
return plan.splitAllowed ? Math.min(plan.remainingToday, MIN_LEG) : plan.remainingToday;
}
/**
* The widest stop a leg whose take-profit is `leg` away may carry and still
* be all but surely closed by a daily reset `resetInMs` away; 0 when the
* reset is under RESET_CLEAR_HOLDS holds off. The walk's exit time has a
* tail that decays as e^(−π²·t/(2g·hold)), g = (A+B)²/(A·B): a lopsided leg
* (a $5,000 stop on a $500 take-profit) lingers far longer than its hold. So
* the leg may be as lopsided as k = holds-to-reset ÷ RESET_CLEAR_HOLDS times
* the 2:1 shape (g = 4.5) allows — g ≤ 4.5k — which keeps its chance of
* still being open at the reset what a 2:1 leg's is at four holds.
*/
export function resetClearStop(o: { leg: number; resetInMs: number; holdHours: number }): number {
if (!(o.leg > 0) || !(o.holdHours > 0) || !Number.isFinite(o.resetInMs)) return 0;
const k = o.resetInMs / (RESET_CLEAR_HOLDS * o.holdHours * 3_600_000);
if (k < 1) return 0;
const c = 4.5 * k - 2;
return (o.leg * (c + Math.sqrt(c * c - 4))) / 2;
}
/**
* dllRoom where the platform states its daily-loss floor itself (Vest: the
* equity the account may not fall below today, already moved by the day's
@@ -1373,6 +1393,14 @@ export function stageInputsFor(
* limit set below the firm's (Vest's 3% is $1,500; the bot's day stops at
* $1,229) would let a day that already lost $1,111 trade the $389 Vest still
* allows. Infinity when there is no limit.
*
* `gain` widens it: on a split day the next leg may give back what the
* earlier legs made, plus the limit — a $1,400 day up $2,604 gets $4,004 —
* as Vest's own floor does. The client says how much (dayGainForRoom): the
* gain still in the account, no more than keeps the leg all but sure to be
* closed by the reset (resetClearStop) — a leg still open there has all of
* its loss counted against the next day. Never more than the day's realized
* gain either way.
*/
export function floorRoom(o: {
dailyLossLimit: number;
@@ -1381,11 +1409,25 @@ export function floorRoom(o: {
/** The day's realized P&L since its base (Vest: since this client's
* latest claim today, else since the reset). */
realizedToday: number;
/** What of the day's gain the room may grow by; 0 (default) = none. */
gain?: number;
}): number {
if (!(o.dailyLossLimit > 0)) return Infinity;
return round2(Math.min(o.dailyLossLimit + Math.min(0, o.realizedToday), o.closedBalance - o.floor));
const widen = Math.min(Math.max(0, o.realizedToday), Math.max(0, o.gain ?? 0));
return round2(Math.min(o.dailyLossLimit + Math.min(0, o.realizedToday) + widen, o.closedBalance - o.floor));
}
/**
* How much the firm's day may still lose before a hard daily-loss breach.
* Infinity when the account type has no limit.
*
* The day's P&L is the closed balance against the platform's snapshot at the
* firm's daily reset when the client knows it (DXTrade: 22:00 UTC), else the
* broker's realized P&L for the session. Only a red day counts: a green day
* does NOT widen the room past the limit, because a position still open at
* the next reset has its whole move counted against the NEW day — room
* borrowed from today's profit would be a breach tomorrow.
*/
export function dllRoom(o: {
dailyLossLimit: number;
closedBalance: number;
@@ -1447,7 +1489,9 @@ export const LAST_STAND_FRACTION = 0.15;
*/
export function lastStand(o: { dllRoom: number; dailyLossLimit: number; maxLossRoom: number | null }): boolean {
if (o.maxLossRoom === null || !(o.dailyLossLimit > 0) || !Number.isFinite(o.dllRoom) || !(o.dllRoom > 0)) return false;
return o.maxLossRoom - o.dllRoom <= LAST_STAND_FRACTION * o.dllRoom;
// A full day's loss is the limit, however much a green day widened the
// room (floorRoom): a stop-out leaving $300 over the floor keeps a day.
return o.maxLossRoom - o.dllRoom <= LAST_STAND_FRACTION * Math.min(o.dllRoom, o.dailyLossLimit);
}
/** On a last stand: how far price can move against the leg before the firm's