/home/devscfvi/web.devsquantum.com/js/calendar.js
// ============================================
// P&L CALENDAR MODULE
// ============================================
// Render Calendar
// Auto-navigate to the month containing the most recent trade (if calendarDate not set)
function getDefaultCalendarDate() {
if (state.calendarDate) return state.calendarDate;
// Find the most recent trade_date from loaded trades
const trades = state.trades || [];
let latestDate = null;
for (const t of trades) {
const dp = tradeDateParts(t);
if (!dp) continue;
const d = new Date(dp.year, dp.month, dp.day);
if (!latestDate || d > latestDate) latestDate = d;
}
// If trades exist and the latest is NOT in the current month, go to that month
if (latestDate) {
const now = new Date();
const sameMonth = latestDate.getFullYear() === now.getFullYear() && latestDate.getMonth() === now.getMonth();
if (!sameMonth) return latestDate;
}
return new Date();
}
function renderCalendar() {
const currentDate = getDefaultCalendarDate();
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
return `
<div class="min-h-screen p-4">
<div class="max-w-6xl mx-auto">
<!-- Header -->
<div class="mb-6 pt-4">
<h1 class="text-2xl font-bold text-white mb-1">P&L Calendar</h1>
<p class="text-sm text-violet-300">Visual monthly performance</p>
</div>
<!-- Month Navigation -->
<div class="glass rounded-2xl p-4 mb-6 border border-violet-500/20">
<div class="flex items-center justify-between">
<button onclick="changeMonth(-1)"
class="p-2 hover:bg-violet-500/20 rounded-lg transition">
<svg class="w-6 h-6 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<div class="text-center">
<h2 class="text-2xl font-bold text-white">
${currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</h2>
<button onclick="goToToday()"
class="text-sm text-violet-400 hover:text-violet-300 mt-1 transition">
Today
</button>
</div>
<button onclick="changeMonth(1)"
class="p-2 hover:bg-violet-500/20 rounded-lg transition">
<svg class="w-6 h-6 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
<!-- Month Summary -->
${renderMonthSummary(year, month)}
<!-- Calendar Grid -->
${renderCalendarGrid(year, month)}
<!-- Legend -->
<div class="glass rounded-2xl p-4 mt-6 border border-violet-500/20">
<p class="text-sm text-slate-400 mb-3">Color Legend:</p>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 text-xs">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-green-500/30 border border-green-500/50"></div>
<span class="text-slate-300">Profit Day</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-red-500/30 border border-red-500/50"></div>
<span class="text-slate-300">Loss Day</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-slate-700 border border-slate-600"></div>
<span class="text-slate-300">Break Even</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-blue-500/30 border border-blue-500/50"></div>
<span class="text-slate-300">Today</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-slate-900/50 border border-slate-700"></div>
<span class="text-slate-300">No Trades</span>
</div>
</div>
</div>
</div>
</div>
`;
}
// Safe date-part extraction — avoids timezone shifts from new Date("YYYY-MM-DD")
// Prefers trade_date (actual trade date from IBKR) over created_at (import timestamp)
function tradeDateParts(trade) {
// trade_date is stored as "YYYY-MM-DD" — exact and timezone-safe
// created_at is "YYYY-MM-DD HH:MM:SS" — strip time and use directly
const raw = (trade.trade_date || trade.created_at || '').replace('T', ' ');
const dateOnly = raw.substring(0, 10); // "YYYY-MM-DD"
const parts = dateOnly.split('-');
if (parts.length !== 3) return null;
return { year: +parts[0], month: +parts[1] - 1, day: +parts[2] };
}
// Unified P&L resolver (handles actual_pnl from imports AND profit/loss_amount)
function tradePnl(t) {
if (t.actual_pnl !== undefined && t.actual_pnl !== null && t.actual_pnl !== '') {
return parseFloat(t.actual_pnl);
}
return t.status === 'win'
? parseFloat(t.profit_amount || 0)
: -parseFloat(t.loss_amount || 0);
}
// Render month summary
function renderMonthSummary(year, month) {
const trades = state.trades || [];
const monthTrades = trades.filter(t => {
const dp = tradeDateParts(t);
return dp && dp.year === year && dp.month === month &&
(t.status === 'win' || t.status === 'loss');
});
const wins = monthTrades.filter(t => t.status === 'win').length;
const losses = monthTrades.filter(t => t.status === 'loss').length;
const winRate = monthTrades.length > 0 ? ((wins / monthTrades.length) * 100).toFixed(1) : 0;
const monthPnL = monthTrades.reduce((sum, t) => sum + tradePnl(t), 0);
const tradingDays = new Set(monthTrades.map(t => {
const dp = tradeDateParts(t);
return dp ? `${dp.year}-${dp.month}-${dp.day}` : '';
}).filter(Boolean)).size;
// Find latest trade month across ALL trades (for the "jump" hint)
const allTrades = state.trades || [];
let latestTradeDateHint = '';
if (monthTrades.length === 0 && allTrades.length > 0) {
let latest = null;
for (const t of allTrades) {
const dp = tradeDateParts(t);
if (!dp) continue;
const d = new Date(dp.year, dp.month, dp.day);
if (!latest || d > latest) latest = d;
}
if (latest) {
latestTradeDateHint = latest.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
}
}
return `
${monthTrades.length === 0 && latestTradeDateHint ? `
<div class="glass rounded-xl p-3 mb-4 border border-amber-500/30 flex items-center justify-between gap-3">
<p class="text-sm text-amber-300">No trades found in this month. Your most recent trades are in <strong>${latestTradeDateHint}</strong>.</p>
<button onclick="jumpToTradesMonth()" class="px-3 py-1.5 rounded-lg bg-amber-500/20 text-amber-300 text-xs font-semibold hover:bg-amber-500/30 transition whitespace-nowrap">
Jump there →
</button>
</div>
` : ''}
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="glass rounded-xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-400 mb-1">Total Trades</p>
<p class="text-2xl font-bold text-white">${monthTrades.length}</p>
</div>
<div class="glass rounded-xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-400 mb-1">Win Rate</p>
<p class="text-2xl font-bold ${parseFloat(winRate) >= 50 ? 'text-green-400' : 'text-red-400'}">
${winRate}%
</p>
</div>
<div class="glass rounded-xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-400 mb-1">Net P&L</p>
<p class="text-2xl font-bold ${monthPnL >= 0 ? 'text-green-400' : 'text-red-400'}">
${monthPnL >= 0 ? '+' : ''}${formatCurrency(monthPnL)}
</p>
</div>
<div class="glass rounded-xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-400 mb-1">Trading Days</p>
<p class="text-2xl font-bold text-violet-400">${tradingDays}</p>
</div>
<div class="glass rounded-xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-400 mb-1">Avg/Day</p>
<p class="text-2xl font-bold text-white">
${tradingDays > 0 ? formatCurrency(monthPnL / tradingDays) : '$0'}
</p>
</div>
</div>
`;
}
// Render calendar grid
function renderCalendarGrid(year, month) {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
const today = new Date();
const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
// Get daily P&L data
const dailyData = calculateDailyPnL(year, month);
let calendarHTML = `
<div class="glass rounded-2xl p-4 border border-violet-500/20">
<!-- Day headers -->
<div class="grid grid-cols-7 gap-2 mb-2">
${['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => `
<div class="text-center text-xs font-semibold text-slate-400 py-2">
${day}
</div>
`).join('')}
</div>
<!-- Calendar days -->
<div class="grid grid-cols-7 gap-2">
`;
// Empty cells before month starts
for (let i = 0; i < startingDayOfWeek; i++) {
calendarHTML += `<div class="aspect-square"></div>`;
}
// Days of the month
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const dayData = dailyData[dateStr] || { pnl: 0, trades: 0 };
const isToday = isCurrentMonth && today.getDate() === day;
let bgColor = 'bg-slate-900/50 border-slate-700';
let textColor = 'text-slate-400';
if (dayData.trades > 0) {
if (dayData.pnl > 0) {
bgColor = 'bg-green-500/30 border-green-500/50';
textColor = 'text-green-400';
} else if (dayData.pnl < 0) {
bgColor = 'bg-red-500/30 border-red-500/50';
textColor = 'text-red-400';
} else {
bgColor = 'bg-slate-700 border-slate-600';
textColor = 'text-slate-300';
}
}
if (isToday) {
bgColor += ' ring-2 ring-blue-500';
}
calendarHTML += `
<div class="aspect-square ${bgColor} border rounded-xl p-2 flex flex-col justify-between hover:opacity-80 transition cursor-pointer"
onclick='showDayDetails("${dateStr}")'>
<div class="text-sm font-semibold ${textColor}">${day}</div>
${dayData.trades > 0 ? `
<div class="text-center">
<div class="text-xs font-bold ${textColor}">
${dayData.pnl >= 0 ? '+' : ''}${formatCurrency(dayData.pnl)}
</div>
<div class="text-xs text-slate-400">${dayData.trades} trade${dayData.trades > 1 ? 's' : ''}</div>
</div>
` : ''}
</div>
`;
}
calendarHTML += `
</div>
</div>
`;
return calendarHTML;
}
// Calculate daily P&L — timezone-safe, supports actual_pnl from imports
function calculateDailyPnL(year, month) {
const trades = state.trades || [];
const dailyData = {};
trades.forEach(trade => {
if (trade.status !== 'win' && trade.status !== 'loss') return;
const dp = tradeDateParts(trade);
if (!dp || dp.year !== year || dp.month !== month) return;
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(dp.day).padStart(2, '0')}`;
if (!dailyData[dateStr]) {
dailyData[dateStr] = { pnl: 0, trades: 0, tradeList: [] };
}
dailyData[dateStr].pnl += tradePnl(trade);
dailyData[dateStr].trades++;
dailyData[dateStr].tradeList.push(trade);
});
return dailyData;
}
// Show day details modal
function showDayDetails(dateStr) {
const [year, month, day] = dateStr.split('-').map(Number);
const trades = state.trades || [];
const dayTrades = trades.filter(t => {
const dp = tradeDateParts(t);
return dp && dp.year === year && dp.month === month - 1 && dp.day === day &&
(t.status === 'win' || t.status === 'loss');
});
if (dayTrades.length === 0) {
showToast('No trades on this day', 'info');
return;
}
const dayPnL = dayTrades.reduce((sum, t) => sum + tradePnl(t), 0);
const wins = dayTrades.filter(t => t.status === 'win').length;
const losses = dayTrades.filter(t => t.status === 'loss').length;
const date = new Date(year, month - 1, day);
const formattedDate = date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
});
const modalContent = `
<div class="p-6">
<h2 class="text-2xl font-bold text-white mb-2">${formattedDate}</h2>
<!-- Day Summary -->
<div class="grid grid-cols-3 gap-3 mb-6">
<div class="bg-slate-900/50 rounded-xl p-3 text-center border border-violet-500/20">
<p class="text-xs text-slate-400 mb-1">Total Trades</p>
<p class="text-2xl font-bold text-white">${dayTrades.length}</p>
</div>
<div class="bg-slate-900/50 rounded-xl p-3 text-center border border-violet-500/20">
<p class="text-xs text-slate-400 mb-1">W/L</p>
<p class="text-xl font-bold text-white">
<span class="text-green-400">${wins}</span>/<span class="text-red-400">${losses}</span>
</p>
</div>
<div class="bg-slate-900/50 rounded-xl p-3 text-center border border-violet-500/20">
<p class="text-xs text-slate-400 mb-1">Day P&L</p>
<p class="text-xl font-bold ${dayPnL >= 0 ? 'text-green-400' : 'text-red-400'}">
${dayPnL >= 0 ? '+' : ''}${formatCurrency(dayPnL)}
</p>
</div>
</div>
<!-- Trade List -->
<div class="space-y-2 max-h-96 overflow-y-auto mb-4">
${dayTrades.map(trade => `
<div class="bg-slate-900/50 rounded-xl p-4 border ${trade.status === 'win' ? 'border-green-500/30' : 'border-red-500/30'}">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-lg font-bold text-white">${trade.coin}</span>
<span class="text-xs px-2 py-1 rounded ${trade.status === 'win' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}">
${trade.status.toUpperCase()}
</span>
</div>
<span class="text-lg font-bold ${trade.status === 'win' ? 'text-green-400' : 'text-red-400'}">
${trade.status === 'win' ? '+' : '-'}${formatCurrency(trade.status === 'win' ? trade.profit_amount : trade.loss_amount)}
</span>
</div>
<div class="grid grid-cols-3 gap-2 text-xs text-slate-400">
<div>Entry: $${trade.entry_price}</div>
<div>Size: $${trade.position_size}</div>
<div>R:R: 1:${trade.reward_risk_ratio}</div>
</div>
${trade.trade_category ? `<p class="text-xs text-violet-400 mt-2">${trade.trade_category}</p>` : ''}
</div>
`).join('')}
</div>
<button onclick="closeAllModals()"
class="w-full gradient-bg text-white py-3 rounded-xl font-semibold active:scale-95 transition">
Close
</button>
</div>
`;
createModal(modalContent, 'max-w-2xl');
}
// Change month
function changeMonth(direction) {
const currentDate = new Date(getDefaultCalendarDate());
currentDate.setMonth(currentDate.getMonth() + direction);
setState({ calendarDate: new Date(currentDate) });
}
// Go to today
function goToToday() {
setState({ calendarDate: new Date() });
}
// Jump to the month containing the most recent trade
function jumpToTradesMonth() {
const trades = state.trades || [];
let latest = null;
for (const t of trades) {
const dp = tradeDateParts(t);
if (!dp) continue;
const d = new Date(dp.year, dp.month, dp.day);
if (!latest || d > latest) latest = d;
}
if (latest) {
setState({ calendarDate: new Date(latest.getFullYear(), latest.getMonth(), 1) });
}
}
// Initialize Calendar
function initCalendarCharts() {
// Calendar doesn't need charts, but placeholder for consistency
}
// Make functions global
window.renderCalendar = renderCalendar;
window.initCalendarCharts = initCalendarCharts;
window.changeMonth = changeMonth;
window.goToToday = goToToday;
window.jumpToTradesMonth = jumpToTradesMonth;
window.showDayDetails = showDayDetails;