diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index bf70877..d97a748 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -3,9 +3,10 @@ export interface WithdrawalStage { consistency: number; minTradingDays: number; /** Spam-withdraw stage: no profit target — book minTradingDays qualifying - * days of ~minDayPnL (tiny MNQ trades), then wait for the withdrawal. - * The stage's consistency still applies: days keep booking until the - * cycle's biggest day fits inside that share of the cycle total. */ + * days of a few dollars (tiny MNQ trades), then wait for the withdrawal. + * The stage's consistency still applies: the first day is a random draw, + * later days are sized so the cycle's biggest day fits inside that share + * of the cycle total (see computeDayPlan). */ drain?: boolean; /** Drain stages only: the per-day booked amount (default $5; randomized * the fixed $4–$15 half-dollar grid by drainDayAmount; this field is @@ -139,15 +140,11 @@ export function currentTradeDateCT(now: Date = new Date()): string { return new Date(date).toISOString().slice(0, 10); } -/** - * Drain-day target: the stage's min-day amount plus a deterministic bump of - * $0–$10 in $0.50 steps — MNQ's per-contract tick value, so every possible - * target lands on a realizable exit ($5 min day → $5.00, $5.50, … $15.00 - * depending on the (account, trade date) pair). Seeded like dailyBump so - * every consumer of the plan computes the same number. - */ -/** Drain-day target grid: $4–$15 in $0.50 steps (23 values), always traded - * on MNQ. Drain days qualify at ≥ DRAIN_DAY_MIN, so every draw counts. */ +/** Drain-day target grid: $4–$15 in $0.50 steps (23 values, MNQ's + * per-contract tick), always traded on MNQ. Drain days qualify at ≥ + * DRAIN_DAY_MIN, so every draw counts. Seeded per (account, trade date) like + * dailyBump so every consumer of the plan computes the same number. Under a + * consistency rule only the cycle's first drain day draws from it. */ export const DRAIN_DAY_MIN = 4; export const DRAIN_DAY_MAX = 15; const DRAIN_DAY_STEPS = Math.round((DRAIN_DAY_MAX - DRAIN_DAY_MIN) / 0.5) + 1; @@ -987,25 +984,38 @@ export function computeDayPlan( const priorDays = dailyPnL.filter((d) => d.date !== tradeDate); // Drain stage: no profit target at all. Book minTradingDays qualifying - // days at the randomized min-day amount, then go quiet until the next - // withdrawal resets the cycle. One unsplit tiny trade per day. + // days, then go quiet until the next withdrawal resets the cycle. One + // unsplit tiny trade per day. The cycle's first day books a randomized + // grid amount (every day does when there is no consistency rule). + // + // The stage's consistency rule outlives the profit target: the cycle's + // biggest day must fit inside `consistency` of the cycle total. Past the + // random first day the drain follows the standard consistency math, so + // accounts drained together finish together whatever their first draw: + // a day that can close the cycle aims at the MIDPOINT of the amounts + // that close it (a fill a little over or under still lands inside), and + // earlier days size like computeDailyTarget with the target at 0. if (drain) { const qualifying = minDayPnL > 0 ? priorDays.filter((d) => d.pnl >= minDayPnL) : priorDays.filter((d) => d.pnl !== 0); - // The stage's consistency rule outlives the profit target: the cycle's - // biggest day must fit inside `consistency` of the cycle total, so the - // drain keeps booking days past the min-day count until the total - // covers maxDay / consistency. - let consistencyShort = false; - if (consistency > 0 && consistency < 1 && priorDays.length > 0) { - const maxDay = Math.max(0, ...priorDays.map((d) => d.pnl)); - const total = priorDays.reduce((s, d) => s + d.pnl, 0); - consistencyShort = maxDay > 0 && total < round2(maxDay / consistency) - 0.005; + const daysDone = qualifying.length >= minTradingDays; + let dayTarget: number; + if (consistency <= 0 || consistency >= 1 || qualifying.length === 0) { + dayTarget = daysDone ? 0 : drainDayAmount(accountId, tradeDate); + } else { + const maxDay = Math.max(...qualifying.map((d) => d.pnl)); + const total = round2(priorDays.reduce((s, d) => s + d.pnl, 0)); + const needed = round2(maxDay / consistency - total); + // A day of x closes the rule when it covers the shortfall without + // topping maxDay (x ≥ needed), or tops it but still fits as the + // new biggest day (total + x ≥ x / consistency). + const lo = Math.max(needed, minDayPnL); + const hi = round2((total * consistency) / (1 - consistency)); + if (daysDone && needed <= 0) dayTarget = 0; + else if (qualifying.length + 1 >= minTradingDays && lo <= hi) dayTarget = round2((lo + hi) / 2); + else dayTarget = computeDailyTarget(0, consistency, priorDays, minDayPnL, minTradingDays, total)?.amount ?? 0; } - const dayTarget = qualifying.length >= minTradingDays && !consistencyShort - ? 0 - : drainDayAmount(accountId, tradeDate); return { dayTarget, remainingToday: round2(dayTarget - realizedPnL),