The min-day block was capping the target at baseAmount (consistency-derived) via Math.min(baseAmount, cappedByFuture), which could produce a target too low to hit profitTarget with min-day reservations for remaining days. Now uses cappedByFuture directly so the target always stays on track to hit profitTarget when there are remaining mandatory days. Consistency can still be enforced via the firm's consistency setting (1.0 = no cap). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
7.1 KiB
TypeScript
156 lines
7.1 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) {
|
||
// Withdrawals reduce the profit remaining in the account
|
||
const totalWithdrawals = fundTransactions
|
||
.filter((f) => f.amount < 0)
|
||
.reduce((s, f) => s + f.amount, 0); // negative sum
|
||
const remainingProfit = priorProfit + totalWithdrawals;
|
||
return { profitTarget: Math.max(0, profitTarget - remainingProfit), 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];
|
||
return { profitTarget: stage.profit, consistency: stage.consistency, minTradingDays: stage.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
|
||
): { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } {
|
||
const qualifyingDays = minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= minDayPnL);
|
||
const daysTraded = qualifyingDays.length;
|
||
|
||
// --- 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 (totalProfit >= 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));
|
||
const realTarget = maxDay / consistency;
|
||
const needed = realTarget - totalProfit;
|
||
|
||
if (needed > maxDay) {
|
||
baseAmount = maxDay;
|
||
path = 'normal_day';
|
||
} else {
|
||
baseAmount = Math.max(0, needed);
|
||
path = 'reduced_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 - totalProfit; // intentionally NOT clamped — can be negative
|
||
|
||
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;
|
||
// Floor: must make at least minDayPnL today (or whatever is left if less)
|
||
const floor = Math.min(effectiveMinDay, remaining);
|
||
|
||
// Target what's needed to stay on track for profitTarget (cappedByFuture), floored at minDayPnL.
|
||
// Consistency-based baseAmount is intentionally ignored here — profitTarget takes priority.
|
||
const amount = Math.max(floor, cappedByFuture);
|
||
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 - totalProfit);
|
||
}
|
||
|
||
return { amount: Math.round(Math.max(baseAmount, effectiveMinDay) * 100) / 100, path };
|
||
}
|