When consistency is between 0 and 1 (exclusive) and profitTarget not met, the daily target should cap at maxDay (can't exceed the current max without breaking the consistency ratio). The previous 'needed' calc could return less than maxDay, stalling progress toward profitTarget. Also restored Math.min(baseAmount, cappedByFuture) in the min-day reservation so consistency is enforced when both mandates are active. Traces: - 0 days, $11111 target, 50%: first_day $5555.50, cappedByFuture $10511 -> consistencyCap = min($5555.50, $10511) = $5555.50 ✓ - 3 days, $5946.44 equity, maxDay $2415.20, 50%: baseAmount $2415.20, cappedByFuture $5014.56 -> min = $2415.20 ✓ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
152 lines
7.4 KiB
TypeScript
152 lines
7.4 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, // -999 or 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 effectiveProfit = equityProfit ?? totalProfit; // used for profitTarget comparison
|
||
|
||
// --- Base target via consistency logic ---
|
||
let baseAmount: number;
|
||
let path: 'first_day' | 'normal_day' | 'reduced_day';
|
||
|
||
if (daysTraded === 0) {
|
||
// 0% or 100% consistency = no constraint; let min-day reservation drive the target
|
||
baseAmount = (consistency === 0 || consistency >= 1) ? 0 : profitTarget * consistency;
|
||
path = 'first_day';
|
||
} else if (consistency === 0 || consistency >= 1) {
|
||
// No consistency rule to satisfy — base amount is $0.
|
||
// The min-day reservation block below handles any mandatory-day targeting.
|
||
baseAmount = 0;
|
||
path = 'reduced_day';
|
||
} else if (effectiveProfit >= profitTarget) {
|
||
// Profit target already met — stop solving for consistency, let min-day reservation handle remaining days
|
||
baseAmount = 0;
|
||
path = 'reduced_day';
|
||
} else {
|
||
const maxDay = Math.max(...qualifyingDays.map((d) => d.pnl));
|
||
// Consistency-cap the daily target at maxDay (can't exceed maxDay without breaking consistency ratio).
|
||
// Let min-day block handle the lower bound and cappedByFuture logic.
|
||
baseAmount = maxDay;
|
||
path = 'normal_day';
|
||
}
|
||
|
||
// --- Min-day reservation (only when minDayPnL is a positive value) ---
|
||
const effectiveMinDay = minDayPnL > 0 ? minDayPnL : 0;
|
||
|
||
if (effectiveMinDay > 0 && minTradingDays > daysTraded) {
|
||
const remaining = profitTarget - effectiveProfit; // use equity-based profit to know how close we are to target
|
||
|
||
if (remaining <= 0) {
|
||
// Profit target already met but mandatory trading days not yet satisfied.
|
||
// Trade exactly minDayPnL each remaining day.
|
||
return { amount: effectiveMinDay, path };
|
||
}
|
||
|
||
const remainingMandatoryDays = minTradingDays - daysTraded; // includes today
|
||
const futureReserve = (remainingMandatoryDays - 1) * effectiveMinDay;
|
||
|
||
// Cap: don't take more than what's available after reserving future days
|
||
const cappedByFuture = remaining - futureReserve;
|
||
|
||
// Target what's needed to stay on track for profitTarget (cappedByFuture), floored at minDayPnL.
|
||
// Cap at baseAmount when the consistency calc produced a positive value — ensures we don't
|
||
// exceed maxDay / (maxDay/consistency) constraint. When baseAmount is 0 (0/100% consistency
|
||
// or target already met), cappedByFuture drives the target directly.
|
||
const consistencyCap = baseAmount > 0 ? Math.min(baseAmount, cappedByFuture) : cappedByFuture;
|
||
const amount = Math.max(effectiveMinDay, consistencyCap);
|
||
return { amount: Math.round(amount * 100) / 100, path };
|
||
}
|
||
|
||
// When no consistency constraint and no min-day reservation applied, target the full remaining profit
|
||
if (baseAmount <= 0 && (consistency === 0 || consistency >= 1)) {
|
||
baseAmount = Math.max(0, profitTarget - effectiveProfit);
|
||
}
|
||
|
||
return { amount: Math.round(Math.max(baseAmount, effectiveMinDay) * 100) / 100, path };
|
||
}
|