diff --git a/app/page.tsx b/app/page.tsx index f3a7a09..f278b4f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -60,6 +60,32 @@ function DetailsIcon() { ); } +function EyeIcon() { + return ( + + + + + ); +} + +function EyeOffIcon() { + return ( + + + + + + ); +} + +function maskName(name: string, privacy: boolean): string { + if (!privacy) return name; + const visible = name.slice(0, 5); + const hidden = name.slice(5); + return hidden.length > 0 ? visible + '•'.repeat(hidden.length) : visible; +} + function SortHeader({ label, col, sortKey, sortDir, onSort }: { label: string; col: SortKey; @@ -85,7 +111,7 @@ function SortHeader({ label, col, sortKey, sortDir, onSort }: { ); } -function AccountRow({ account, firm, hideDead }: { account: AccountState; firm: FirmConfig; hideDead: boolean }) { +function AccountRow({ account, firm, hideDead, privacy }: { account: AccountState; firm: FirmConfig; hideDead: boolean; privacy: boolean }) { const cfg = getAccountConfig(account.name, firm); const dead = isAccountDead(account); @@ -96,8 +122,8 @@ function AccountRow({ account, firm, hideDead }: { account: AccountState; firm: return ( -
- {account.name} +
+ {maskName(account.name, privacy)}
${fmt(account.amount)} @@ -135,13 +161,14 @@ function AccountRow({ account, firm, hideDead }: { account: AccountState; firm: ); } -function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, sortKey, sortDir }: { +function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, privacy, sortKey, sortDir }: { state: FirmState; firm: FirmConfig; deleteMode: boolean; selected: boolean; onToggle: () => void; hideDead: boolean; + privacy: boolean; sortKey: SortKey | null; sortDir: SortDir; }) { @@ -209,7 +236,7 @@ function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, sortK ) : ( sortedAccounts.map((acc) => ( - + )) ) )} @@ -232,6 +259,7 @@ export default function Home() { const [deleteMode, setDeleteMode] = useState(false); const [selected, setSelected] = useState>(new Set()); const [hideDead, setHideDead] = useState(false); + const [privacy, setPrivacy] = useState(false); const [sortKey, setSortKey] = useState(null); const [sortDir, setSortDir] = useState('asc'); @@ -396,6 +424,13 @@ export default function Home() { > Delete + toggleSelected(cfg.id)} hideDead={hideDead} + privacy={privacy} sortKey={sortKey} sortDir={sortDir} /> diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index 4b172f9..d4e13bc 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -101,8 +101,14 @@ export async function runTrade(action: 'Buy' | 'Sell', symbol: string) { if (cash.realizedPnL !== 0) continue; // Use the same target formula as the dashboard — skip if $0 (challenge complete) - const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL); - if (target.amount <= 0) continue; + const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL, cfg.minDayPnL, cfg.minTradingDays); + + // Allow through if it's an MNQ extra-day trade: no min day P&L, profit done, days still needed + const isMnqExtraDay = cfg.minDayPnL <= 0 + && cfg.minTradingDays > daysTraded + && totalProfit >= cfg.profitTarget; + + if (target.amount <= 0 && !isMnqExtraDay) continue; allEligible.push({ firmName: firm.name, client, acc, contract, firmConfig, cash, dailyPnL, daysTraded }); } @@ -120,10 +126,42 @@ export async function runTrade(action: 'Buy' | 'Sell', symbol: string) { // ── Phase 2: fire the batch simultaneously ── const tradeResults = await Promise.allSettled(batch.map(async (item) => { - const { client, acc, contract, firmConfig, cash, dailyPnL, daysTraded } = item; + const { client, acc, contract, firmConfig, dailyPnL, daysTraded } = item; const cfg = getAccountConfig(acc.name, firmConfig)!; const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0); - const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL); + + // Extra-day mode: profit target already met, no min day P&L, days still needed. + // Just trade 1 MNQ in and out at market immediately — P&L doesn't matter. + const isExtraDay = cfg.minDayPnL <= 0 + && cfg.minTradingDays > daysTraded + && totalProfit >= cfg.profitTarget; + + if (isExtraDay) { + const mnqContract = await client.findFrontMonthContract('MNQ'); + if (!mnqContract) throw new Error('MNQ contract not found for extra-day trade'); + + const fill = await client.sendOrder(acc.id, mnqContract.name, 1, action, 'Market'); + const exitAction: 'Buy' | 'Sell' = action === 'Buy' ? 'Sell' : 'Buy'; + const exitFill = await client.sendOrder(acc.id, mnqContract.name, 1, exitAction, 'Market'); + + console.log(`[auto-trade] ${acc.name} (${item.firmName}) extra-day: ${action} 1xMNQ @ ${fill.price} | exited @ ${exitFill.price} (market)`); + + return { + account: acc.name, + firm: item.firmName, + status: 'filled', + contracts: 1, + target: 0, + grossTarget: 0, + totalCommission: 0, + targetPath: 'extra_day', + entryPrice: fill.price, + exitPrice: exitFill.price, + commission: 0, + }; + } + + const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL, cfg.minDayPnL, cfg.minTradingDays); const rawContracts = Math.max(1, Math.ceil(target.amount / 1000)); const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts; diff --git a/lib/trading-logic.ts b/lib/trading-logic.ts index 2c9a9e6..a963284 100644 --- a/lib/trading-logic.ts +++ b/lib/trading-logic.ts @@ -20,28 +20,68 @@ export const POINT_VALUES: { [symbol: string]: number } = { * * 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 }[] + 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 positiveDays = dailyPnL.filter((d) => d.pnl > 0); + const daysTraded = positiveDays.length; - if (positiveDays.length === 0) { - return { - amount: Math.round(profitTarget * consistency * 100) / 100, - path: 'first_day', - }; + // --- 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 { + const maxDay = Math.max(...positiveDays.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'; + } } - const maxDay = Math.max(...positiveDays.map((d) => d.pnl)); - const realTarget = maxDay / consistency; - const needed = realTarget - totalProfit; + // --- Min-day reservation (only when minDayPnL is a positive value) --- + const effectiveMinDay = minDayPnL > 0 ? minDayPnL : 0; - if (needed > maxDay) { - return { amount: Math.round(maxDay * 100) / 100, path: 'normal_day' }; + 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); + + const amount = Math.max(floor, Math.min(baseAmount, cappedByFuture)); + return { amount: Math.round(amount * 100) / 100, path }; } - return { amount: Math.round(Math.max(0, needed) * 100) / 100, path: 'reduced_day' }; + + return { amount: Math.round(baseAmount * 100) / 100, path }; }