Add privacy toggle, min-day P&L reservation, and extra-day MNQ trades
- Privacy button: masks account names beyond the first 5 chars with bullets; eye/eye-off icon toggles the mode in the header toolbar - computeDailyTarget: accepts minDayPnL + minTradingDays params; when a positive min floor is set and mandatory days remain, reserves future-day profit so each day hits the floor (cap = remaining - futureReserve, floor = minDayPnL); returns effectiveMinDay directly once profit target is met but days are not yet satisfied - auto-trade: passes minDayPnL/minTradingDays to computeDailyTarget; for zero-floor accounts that have met the profit target but still owe trading days, trades 1 MNQ in-and-out at market (extra-day mode) and bypasses the normal target=0 skip gate via isMnqExtraDay flag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
49580c6bb4
commit
ce2075f603
+41
-5
@@ -60,6 +60,32 @@ function DetailsIcon() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EyeIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EyeOffIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
|
||||||
|
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
|
||||||
|
<line x1="1" y1="1" x2="23" y2="23" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }: {
|
function SortHeader({ label, col, sortKey, sortDir, onSort }: {
|
||||||
label: string;
|
label: string;
|
||||||
col: SortKey;
|
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 cfg = getAccountConfig(account.name, firm);
|
||||||
const dead = isAccountDead(account);
|
const dead = isAccountDead(account);
|
||||||
|
|
||||||
@@ -96,8 +122,8 @@ function AccountRow({ account, firm, hideDead }: { account: AccountState; firm:
|
|||||||
return (
|
return (
|
||||||
<tr className={`border-b border-slate-100 hover:bg-slate-50 transition-colors${!account.active || dead ? ' opacity-50' : ''}`}>
|
<tr className={`border-b border-slate-100 hover:bg-slate-50 transition-colors${!account.active || dead ? ' opacity-50' : ''}`}>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2 font-medium text-slate-800 pl-4">
|
<div className="flex items-center gap-2 font-medium text-slate-800 pl-4 font-mono">
|
||||||
{account.name}
|
{maskName(account.name, privacy)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-slate-600 tabular-nums">${fmt(account.amount)}</td>
|
<td className="px-4 py-3 text-slate-600 tabular-nums">${fmt(account.amount)}</td>
|
||||||
@@ -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;
|
state: FirmState;
|
||||||
firm: FirmConfig;
|
firm: FirmConfig;
|
||||||
deleteMode: boolean;
|
deleteMode: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
hideDead: boolean;
|
hideDead: boolean;
|
||||||
|
privacy: boolean;
|
||||||
sortKey: SortKey | null;
|
sortKey: SortKey | null;
|
||||||
sortDir: SortDir;
|
sortDir: SortDir;
|
||||||
}) {
|
}) {
|
||||||
@@ -209,7 +236,7 @@ function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, sortK
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
sortedAccounts.map((acc) => (
|
sortedAccounts.map((acc) => (
|
||||||
<AccountRow key={acc.id} account={acc} firm={firm} hideDead={hideDead} />
|
<AccountRow key={acc.id} account={acc} firm={firm} hideDead={hideDead} privacy={privacy} />
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -232,6 +259,7 @@ export default function Home() {
|
|||||||
const [deleteMode, setDeleteMode] = useState(false);
|
const [deleteMode, setDeleteMode] = useState(false);
|
||||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||||
const [hideDead, setHideDead] = useState(false);
|
const [hideDead, setHideDead] = useState(false);
|
||||||
|
const [privacy, setPrivacy] = useState(false);
|
||||||
const [sortKey, setSortKey] = useState<SortKey | null>(null);
|
const [sortKey, setSortKey] = useState<SortKey | null>(null);
|
||||||
const [sortDir, setSortDir] = useState<SortDir>('asc');
|
const [sortDir, setSortDir] = useState<SortDir>('asc');
|
||||||
|
|
||||||
@@ -396,6 +424,13 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPrivacy((p) => !p)}
|
||||||
|
className={`px-3 py-2 rounded-lg transition-colors inline-flex items-center ${privacy ? 'text-blue-600 bg-blue-50 hover:bg-blue-100' : 'text-slate-400 hover:text-slate-600 hover:bg-slate-100'}`}
|
||||||
|
title={privacy ? 'Show account names' : 'Hide account names'}
|
||||||
|
>
|
||||||
|
{privacy ? <EyeOffIcon /> : <EyeIcon />}
|
||||||
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/settings"
|
href="/settings"
|
||||||
className="px-3 py-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors inline-flex items-center"
|
className="px-3 py-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors inline-flex items-center"
|
||||||
@@ -496,6 +531,7 @@ export default function Home() {
|
|||||||
selected={selected.has(cfg.id)}
|
selected={selected.has(cfg.id)}
|
||||||
onToggle={() => toggleSelected(cfg.id)}
|
onToggle={() => toggleSelected(cfg.id)}
|
||||||
hideDead={hideDead}
|
hideDead={hideDead}
|
||||||
|
privacy={privacy}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDir={sortDir}
|
sortDir={sortDir}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+42
-4
@@ -101,8 +101,14 @@ export async function runTrade(action: 'Buy' | 'Sell', symbol: string) {
|
|||||||
if (cash.realizedPnL !== 0) continue;
|
if (cash.realizedPnL !== 0) continue;
|
||||||
|
|
||||||
// Use the same target formula as the dashboard — skip if $0 (challenge complete)
|
// Use the same target formula as the dashboard — skip if $0 (challenge complete)
|
||||||
const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL);
|
const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL, cfg.minDayPnL, cfg.minTradingDays);
|
||||||
if (target.amount <= 0) continue;
|
|
||||||
|
// 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 });
|
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 ──
|
// ── Phase 2: fire the batch simultaneously ──
|
||||||
const tradeResults = await Promise.allSettled(batch.map(async (item) => {
|
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 cfg = getAccountConfig(acc.name, firmConfig)!;
|
||||||
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
|
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 rawContracts = Math.max(1, Math.ceil(target.amount / 1000));
|
||||||
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
|
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
|
||||||
|
|||||||
+52
-12
@@ -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)
|
* 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)
|
* 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(
|
export function computeDailyTarget(
|
||||||
profitTarget: number,
|
profitTarget: number,
|
||||||
consistency: number,
|
consistency: number,
|
||||||
totalProfit: 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' } {
|
): { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } {
|
||||||
const positiveDays = dailyPnL.filter((d) => d.pnl > 0);
|
const positiveDays = dailyPnL.filter((d) => d.pnl > 0);
|
||||||
|
const daysTraded = positiveDays.length;
|
||||||
|
|
||||||
if (positiveDays.length === 0) {
|
// --- Base target via consistency logic ---
|
||||||
return {
|
let baseAmount: number;
|
||||||
amount: Math.round(profitTarget * consistency * 100) / 100,
|
let path: 'first_day' | 'normal_day' | 'reduced_day';
|
||||||
path: 'first_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));
|
// --- Min-day reservation (only when minDayPnL is a positive value) ---
|
||||||
const realTarget = maxDay / consistency;
|
const effectiveMinDay = minDayPnL > 0 ? minDayPnL : 0;
|
||||||
const needed = realTarget - totalProfit;
|
|
||||||
|
|
||||||
if (needed > maxDay) {
|
if (effectiveMinDay > 0 && minTradingDays > daysTraded) {
|
||||||
return { amount: Math.round(maxDay * 100) / 100, path: 'normal_day' };
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user