The consistency cap was using profitTarget × consistency, which is correct only when starting equity = 0 (fresh stage). After a loss within a cycle, the cap should still respect the cycle's intended net total. Now computes: cycleStartEquity = equityProfit − tradingProfit (constant per cycle) cycleNetTarget = profitTarget − cycleStartEquity maxConsistencyDay = cycleNetTarget × consistency This way prior losses don't expand the daily cap. For PAAPEX stage 2 with -$3000 day 1 and $7100 target: today's cap = $1421.95 (50% of the cycle's $2843.90 net target), preserving consistency at exactly $7100. Stage 1 behavior unchanged (cycleStartEquity = 0 → cycleNetTarget = profitTarget). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
164 lines
8.0 KiB
TypeScript
164 lines
8.0 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,
|
||
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.
|
||
// cycleStartEquity = equityProfit − tradingProfit (equity at the start of this cycle).
|
||
// cycleNetTarget = profitTarget − cycleStartEquity (cycle's net P&L needed to hit target).
|
||
// maxConsistencyDay = cycleNetTarget × consistency — the largest day the rule allows
|
||
// measured against the cycle's net total (not against profitTarget directly), so prior
|
||
// losses don't artificially expand the cap.
|
||
//
|
||
// Day 1: min(remaining × consistency, maxConsistencyDay).
|
||
// Day 2+: max(maxDay, maxConsistencyDay).
|
||
// - If maxDay ≤ maxConsistencyDay: room to grow days up to that ceiling.
|
||
// - If maxDay > maxConsistencyDay: consistency already broken at target, total must
|
||
// grow to at least maxDay / consistency. Each day can be up to maxDay (going higher
|
||
// creates a new maxDay requiring even more total).
|
||
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 {
|
||
const cycleStartEquity = equityProfit - tradingProfit;
|
||
const cycleNetTarget = Math.max(0, profitTarget - cycleStartEquity);
|
||
const maxConsistencyDay = cycleNetTarget * consistency;
|
||
if (daysTraded === 0) {
|
||
consistencyCap = Math.min(remaining * consistency, maxConsistencyDay);
|
||
path = 'first_day';
|
||
} else {
|
||
const maxDay = Math.max(...qualifyingDays.map((d) => d.pnl));
|
||
consistencyCap = Math.max(maxDay, maxConsistencyDay);
|
||
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 };
|
||
}
|