/** * Resolve the effective profit target and consistency for an account based on its withdrawal strategy. * * Mode A (targetSameEquity=true): the account must reach the same cumulative equity level. * remainingProfit = priorProfit + totalWithdrawals (profit still in the account after payouts) * effectiveProfitTarget = profitTarget − remainingProfit * * Mode B (withdrawalStages non-empty): Stage 1 (no withdrawals yet) uses base profitTarget/consistency. * After the Nth withdrawal, use withdrawalStages[N-1]; last stage repeats. * Fallback: returns base profitTarget and consistency unchanged. */ export function resolveEffectiveConfig( profitTarget: number, consistency: number, minTradingDays: number, targetSameEquity: boolean, withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[], priorProfit: number, fundTransactions: { date: string; amount: number }[] ): { profitTarget: number; consistency: number; minTradingDays: number } { if (targetSameEquity) { // Simple Mode A: effective target = profitTarget - equityProfit, computed in computeDailyTarget. // Since computeDailyTarget already compares (amount - accountSize) against profitTarget, // no adjustment needed here — just return the base values. return { profitTarget, consistency, minTradingDays }; } if (withdrawalStages.length > 0) { const withdrawalCount = fundTransactions.filter((f) => f.amount < 0).length; if (withdrawalCount === 0) { return { profitTarget, consistency, minTradingDays }; // Stage 1 = base values } const idx = Math.min(withdrawalCount - 1, withdrawalStages.length - 1); const stage = withdrawalStages[idx]; // Fall back to base minTradingDays when the stage doesn't specify one (0 or missing) return { profitTarget: stage.profit, consistency: stage.consistency, minTradingDays: stage.minTradingDays || minTradingDays }; } return { profitTarget, consistency, minTradingDays }; } /** @deprecated Use resolveEffectiveConfig instead */ export function resolveEffectiveProfitTarget( profitTarget: number, targetSameEquity: boolean, withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[], priorProfit: number, fundTransactions: { date: string; amount: number }[] ): number { return resolveEffectiveConfig(profitTarget, 0, 0, targetSameEquity, withdrawalStages, priorProfit, fundTransactions).profitTarget; } /** Dollar-per-point value for common futures products. */ export const POINT_VALUES: { [symbol: string]: number } = { NQ: 20, MNQ: 2, ES: 50, MES: 5, YM: 5, MYM: 0.5, RTY: 50, M2K: 10, GC: 100, MGC: 10, SI: 50, CL: 1000, MCL: 100, NG: 10000, ZB: 1000, ZN: 1000, ZF: 1000, '6E': 125000, '6J': 12500000, '6B': 62500, }; /** * Compute the next trading day's profit target for an account. * * Path 1 – No positive trading days yet: * target = profitTarget × consistency * * Path 2 – At least one positive day exists: * maxDay = highest single-day P&L so far * realTarget = maxDay / consistency (the total profit at which maxDay ≤ consistency% of total) * needed = realTarget − totalProfit * * if needed > maxDay → target maxDay (still a long way from the real target; trade a normal day) * else → target needed (close to the real target; aim for exactly what's left) * * Min-day reservation (only when minDayPnL > 0): * When there are still mandatory trading days remaining, today's target is capped so that * enough profit is reserved for each future mandatory day to meet minDayPnL. * Cap = (profitTarget - totalProfit) − (remainingDaysAfterToday × minDayPnL) * Floor = minDayPnL (we must make at least this today) */ export function computeDailyTarget( profitTarget: number, consistency: number, dailyPnL: { date: string; pnl: number }[], minDayPnL: number, // 0 = no minimum per day minTradingDays: number, // 0 = no minimum trading days equityProfit: number | null | undefined // amount − accountSize. Null/undefined → skip (return null). 0 is valid. ): { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } | null { // Don't run if we don't have a real account balance to compute against — 0 is valid. if (typeof equityProfit !== 'number' || !Number.isFinite(equityProfit)) { return null; } const qualifyingDays = minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= minDayPnL); const daysTraded = qualifyingDays.length; const effectiveMinDay = Math.max(0, minDayPnL); const tradingProfit = dailyPnL.reduce((s, d) => s + d.pnl, 0); // cycle-local trading profit // Shortfall to hit profitTarget (measured in equity terms — accounts for deposits/withdrawals) const equityShortfall = Math.max(0, profitTarget - equityProfit); // Shortfall to satisfy consistency rule: total trading profit must be ≥ maxDay / consistency, // otherwise the biggest day would exceed the consistency ratio. let consistencyShortfall = 0; if (consistency > 0 && consistency < 1 && qualifyingDays.length > 0) { const maxDay = Math.max(...qualifyingDays.map((d) => d.pnl)); const realTarget = maxDay / consistency; consistencyShortfall = Math.max(0, realTarget - tradingProfit); } const remaining = Math.max(equityShortfall, consistencyShortfall); // 1. Both target and consistency already satisfied — coast on min-day if mandatory days remain. if (remaining <= 0) { const needsMoreDays = minTradingDays > daysTraded; return { amount: needsMoreDays ? effectiveMinDay : 0, path: 'reduced_day' }; } // 2. Min-day reservation: future mandatory days each reserve minDayPnL. // cappedByFuture = remaining profit available after reserving. const daysLeft = Math.max(1, minTradingDays - daysTraded); // includes today const futureReserve = (daysLeft - 1) * effectiveMinDay; const cappedByFuture = remaining - futureReserve; // 3. Consistency cap: // - 0% or 100%: no constraint // - Day 1 of cycle: max allowed = remaining × consistency // - Day 2+ of cycle: max allowed = maxDay (keeps consistency ratio stable) let consistencyCap: number; let path: 'first_day' | 'normal_day' | 'reduced_day'; if (consistency === 0 || consistency >= 1) { consistencyCap = Infinity; path = daysTraded === 0 ? 'first_day' : 'reduced_day'; } else if (daysTraded === 0) { consistencyCap = remaining * consistency; path = 'first_day'; } else { const maxDay = Math.max(...qualifyingDays.map((d) => d.pnl)); consistencyCap = maxDay; path = 'normal_day'; } // 4. Combine caps and apply min-day floor when mandatory days remain. const raw = Math.min(consistencyCap, cappedByFuture); const mustFloor = minTradingDays > daysTraded; const amount = mustFloor ? Math.max(effectiveMinDay, raw) : Math.max(0, raw); return { amount: Math.round(amount * 100) / 100, path }; }