Files
autofirmer-expanded/lib/trading-logic.ts
T
Brandon LiandClaude Opus 4.6 70b1362d3e Add per-stage withdrawal targets with consistency and min trading days
- Add withdrawal stage system: each stage defines profit target, consistency,
  and min trading days for post-withdrawal challenge cycles
- Target Same Equity mode accounts for withdrawn amounts when computing
  effective profit target (profitTarget - remainingProfit)
- Store fund transaction timestamps for time-aware cycle filtering
  (withdrawals before 9 AM CT include that day in new cycle)
- Expose full P&L history (fullDailyPnL) for calendar/equity curve display
  across all cycles, with DB fallback for pre-restart data
- Show stage number (#1, #2, etc.) on calendar cells
- Hide consistency reference line when consistency is 0% or 100%
- Settings UI: "After First W/D" column with same-equity checkbox,
  expandable stage sub-rows with profit/consistency/days inputs
- Default target_same_equity to 1 for new and existing account configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 05:27:13 -05:00

145 lines
6.4 KiB
TypeScript
Raw 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) {
// 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) {
baseAmount = profitTarget * consistency;
path = 'first_day';
} else if (consistency === 0) {
// 0% consistency means no consistency rule to satisfy — base amount is always $0.
// The min-day reservation block below handles any mandatory-day targeting.
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);
// 0% consistency → no upper bound, take cappedByFuture; otherwise cap at baseAmount
const amount = Math.max(floor, baseAmount > 0 ? Math.min(baseAmount, cappedByFuture) : cappedByFuture);
return { amount: Math.round(amount * 100) / 100, path };
}
return { amount: Math.round(baseAmount * 100) / 100, path };
}