Replaces the tangled first_day/consistency/min-day branches with a cleaner flow: 1. If profit target met, coast on min-day (or nothing) 2. Compute cappedByFuture (reserve future min-days) 3. Compute consistencyCap: - Day 1 of cycle: remaining × consistency - Day 2+: current maxDay - 0/100% consistency: no cap 4. Combine and floor at minDayPnL when mandatory days remain Fixes a bug where Stage 2+ Day 1 used the full profitTarget × consistency instead of remaining × consistency, allowing day 1 to exceed 50% of cycle-local profit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
6.2 KiB
TypeScript
133 lines
6.2 KiB
TypeScript
/**
|
||
* 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,
|
||
totalProfit: number,
|
||
dailyPnL: { date: string; pnl: number }[],
|
||
minDayPnL: number = 0, // 0 = no minimum per day
|
||
minTradingDays: number = 0, // 0 = no minimum trading days
|
||
equityProfit?: number // amount − accountSize; used for profitTarget comparison. Defaults to totalProfit.
|
||
): { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } {
|
||
const qualifyingDays = minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= minDayPnL);
|
||
const daysTraded = qualifyingDays.length;
|
||
const effectiveMinDay = Math.max(0, minDayPnL);
|
||
const currentProfit = equityProfit ?? totalProfit;
|
||
const remaining = profitTarget - currentProfit;
|
||
|
||
// 1. Profit target already met — coast on min-day if mandatory days remain, else nothing to do.
|
||
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 };
|
||
}
|