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 };
}