diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index b396b8c..33e3015 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -631,7 +631,10 @@ function dailyLossGate( if (floor === null) return { reason: DLL_BASE_PENDING }; let room: number; if (typeof floor === 'number') { - room = floorRoom({ dailyLossLimit: dll, closedBalance: cash.amount, floor }); + room = floorRoom({ + dailyLossLimit: dll, closedBalance: cash.amount, floor, + realizedToday: Math.min(cash.realizedPnL, combinedRealized), + }); } else { const dayStart: number | null | undefined = client.dayStartBalance?.(accountId); if (dayStart === null) return { reason: DLL_BASE_PENDING }; diff --git a/lib/leg-estimate.ts b/lib/leg-estimate.ts index 3086e29..4074025 100644 --- a/lib/leg-estimate.ts +++ b/lib/leg-estimate.ts @@ -31,7 +31,12 @@ export function dayLossRoom( 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 }); + if (typeof floor === 'number') { + return floorRoom({ + dailyLossLimit: dll, closedBalance: cash.amount, floor, + realizedToday: Math.min(cash.realizedPnL, combinedRealized), + }); + } const dayStart = client.dayStartBalance?.(accountId); return dayStart === null ? null : computeDllRoom({ dailyLossLimit: dll, closedBalance: cash.amount, dayStartBalance: dayStart, diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index 915009e..61fb1e1 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -1368,12 +1368,22 @@ export function stageInputsFor( /** * 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 - * transfers): never more than the stage's limit, nor than the floor leaves. - * Infinity when there is no limit. + * transfers): what's left of the stage's limit after the day's losses, and + * never more than the floor leaves. The firm's floor alone isn't enough — a + * 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. */ -export function floorRoom(o: { dailyLossLimit: number; closedBalance: number; floor: number }): number { +export function floorRoom(o: { + dailyLossLimit: number; + closedBalance: number; + floor: number; + /** The day's realized P&L since its base (Vest: since this client's + * latest claim today, else since the reset). */ + realizedToday: number; +}): number { if (!(o.dailyLossLimit > 0)) return Infinity; - return round2(Math.min(o.dailyLossLimit, o.closedBalance - o.floor)); + return round2(Math.min(o.dailyLossLimit + Math.min(0, o.realizedToday), o.closedBalance - o.floor)); } export function dllRoom(o: { diff --git a/scripts/check-trading-logic.mjs b/scripts/check-trading-logic.mjs index b0c97f9..e7b8bf0 100644 --- a/scripts/check-trading-logic.mjs +++ b/scripts/check-trading-logic.mjs @@ -999,17 +999,24 @@ test('same-day stages: today\'s payouts pick the stage, and a stage may carry it assert.deepEqual(stageInputsFor({}, 7, { stagesSameDay: true }), { dailyLossLimit: 0, stagesSameDay: true, sameDayWithdrawals: 0 }); }); -test('floorRoom: the platform\'s own floor, never more than the stage\'s limit', () => { +test('floorRoom: the platform\'s own floor, never more than what\'s left of the stage\'s limit', () => { // Fresh day: $50k, Vest floor $48,500, the type's $1,229. - assert.equal(floorRoom({ dailyLossLimit: 1229, closedBalance: 50000, floor: 48500 }), 1229); - // Won $1,500 and claimed it: balance $50k, Vest's floor lowered to $47k. - // Stage 2's $3,000 limit gets all of it. - assert.equal(floorRoom({ dailyLossLimit: 3000, closedBalance: 50000, floor: 47000 }), 3000); + assert.equal(floorRoom({ dailyLossLimit: 1229, closedBalance: 50000, floor: 48500, realizedToday: 0 }), 1229); + // Lost $1,111.65 today: Vest still allows $388.35, but the bot's own + // $1,229 day has only $117.35 left. + assert.equal(floorRoom({ dailyLossLimit: 1229, closedBalance: 48888.35, floor: 48500, realizedToday: -1111.65 }), 117.35); + // A green day doesn't widen the limit. + assert.equal(floorRoom({ dailyLossLimit: 1229, closedBalance: 50600, floor: 48500, realizedToday: 600 }), 1229); + // Won $1,500 and claimed it: balance $50k, Vest's floor lowered to $47k, + // the day's P&L counted from the claim. Stage 2's $3,000 gets all of it. + assert.equal(floorRoom({ dailyLossLimit: 3000, closedBalance: 50000, floor: 47000, realizedToday: 0 }), 3000); + // ...and after a $500 stage-2 loss, $2,500 either way. + assert.equal(floorRoom({ dailyLossLimit: 3000, closedBalance: 49500, floor: 47000, realizedToday: -500 }), 2500); // If Vest hadn't lowered the floor, only what it really leaves. - assert.equal(floorRoom({ dailyLossLimit: 3000, closedBalance: 50000, floor: 48500 }), 1500); + assert.equal(floorRoom({ dailyLossLimit: 3000, closedBalance: 50000, floor: 48500, realizedToday: 0 }), 1500); // Below the floor already: negative room (no trade). - assert.equal(floorRoom({ dailyLossLimit: 1229, closedBalance: 48400, floor: 48500 }), -100); - assert.equal(floorRoom({ dailyLossLimit: 0, closedBalance: 50000, floor: 48500 }), Infinity); + assert.equal(floorRoom({ dailyLossLimit: 1229, closedBalance: 48400, floor: 48500, realizedToday: -1600 }), -371); + assert.equal(floorRoom({ dailyLossLimit: 0, closedBalance: 50000, floor: 48500, realizedToday: -900 }), Infinity); }); test('the day after a claim: a fresh cycle that aims at the next stage\'s target', () => {