Files
Brandon LiandClaude Opus 4.6 f6b2ee27fd Use cycle net target (not profit target) for consistency cap
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>
2026-05-04 18:21:14 -05:00

164 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
}