Drain: random first day, then size the rest by consistency

Only the drain cycle's first day draws from the $4-$15 grid. Under a
consistency rule, later days follow the standard consistency math and
the day that can close the cycle aims at the midpoint of the amounts
that close it, so accounts drained together finish together whatever
their first draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-24 16:34:43 -05:00
co-authored by Claude Opus 5.5
parent b932452e98
commit 61384c9fa6
+36 -26
View File
@@ -3,9 +3,10 @@ export interface WithdrawalStage {
consistency: number; consistency: number;
minTradingDays: number; minTradingDays: number;
/** Spam-withdraw stage: no profit target — book minTradingDays qualifying /** Spam-withdraw stage: no profit target — book minTradingDays qualifying
* days of ~minDayPnL (tiny MNQ trades), then wait for the withdrawal. * days of a few dollars (tiny MNQ trades), then wait for the withdrawal.
* The stage's consistency still applies: days keep booking until the * The stage's consistency still applies: the first day is a random draw,
* cycle's biggest day fits inside that share of the cycle total. */ * later days are sized so the cycle's biggest day fits inside that share
* of the cycle total (see computeDayPlan). */
drain?: boolean; drain?: boolean;
/** Drain stages only: the per-day booked amount (default $5; randomized /** Drain stages only: the per-day booked amount (default $5; randomized
* the fixed $4–$15 half-dollar grid by drainDayAmount; this field is * 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); return new Date(date).toISOString().slice(0, 10);
} }
/** /** Drain-day target grid: $4–$15 in $0.50 steps (23 values, MNQ's
* Drain-day target: the stage's min-day amount plus a deterministic bump of * per-contract tick), always traded on MNQ. Drain days qualify at ≥
* $0–$10 in $0.50 steps — MNQ's per-contract tick value, so every possible * DRAIN_DAY_MIN, so every draw counts. Seeded per (account, trade date) like
* target lands on a realizable exit ($5 min day → $5.00, $5.50, … $15.00 * dailyBump so every consumer of the plan computes the same number. Under a
* depending on the (account, trade date) pair). Seeded like dailyBump so * consistency rule only the cycle's first drain day draws from it. */
* 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. */
export const DRAIN_DAY_MIN = 4; export const DRAIN_DAY_MIN = 4;
export const DRAIN_DAY_MAX = 15; export const DRAIN_DAY_MAX = 15;
const DRAIN_DAY_STEPS = Math.round((DRAIN_DAY_MAX - DRAIN_DAY_MIN) / 0.5) + 1; 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); const priorDays = dailyPnL.filter((d) => d.date !== tradeDate);
// Drain stage: no profit target at all. Book minTradingDays qualifying // Drain stage: no profit target at all. Book minTradingDays qualifying
// days at the randomized min-day amount, then go quiet until the next // days, then go quiet until the next withdrawal resets the cycle. One
// withdrawal resets the cycle. One unsplit tiny trade per day. // 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) { if (drain) {
const qualifying = minDayPnL > 0 const qualifying = minDayPnL > 0
? priorDays.filter((d) => d.pnl >= minDayPnL) ? priorDays.filter((d) => d.pnl >= minDayPnL)
: priorDays.filter((d) => d.pnl !== 0); : priorDays.filter((d) => d.pnl !== 0);
// The stage's consistency rule outlives the profit target: the cycle's const daysDone = qualifying.length >= minTradingDays;
// biggest day must fit inside `consistency` of the cycle total, so the let dayTarget: number;
// drain keeps booking days past the min-day count until the total if (consistency <= 0 || consistency >= 1 || qualifying.length === 0) {
// covers maxDay / consistency. dayTarget = daysDone ? 0 : drainDayAmount(accountId, tradeDate);
let consistencyShort = false; } else {
if (consistency > 0 && consistency < 1 && priorDays.length > 0) { const maxDay = Math.max(...qualifying.map((d) => d.pnl));
const maxDay = Math.max(0, ...priorDays.map((d) => d.pnl)); const total = round2(priorDays.reduce((s, d) => s + d.pnl, 0));
const total = priorDays.reduce((s, d) => s + d.pnl, 0); const needed = round2(maxDay / consistency - total);
consistencyShort = maxDay > 0 && total < round2(maxDay / consistency) - 0.005; // 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 { return {
dayTarget, dayTarget,
remainingToday: round2(dayTarget - realizedPnL), remainingToday: round2(dayTarget - realizedPnL),