.DS_Store // calendar.js - 处理日历和交易详情相关功能 import { allTrades, filteredTrades, TOTAL_ACCOUNT_VALUE, formatPnL, calculateDuration } from './data.js'; import { getDailyStats, getMonthlyStats, getWeeklyStats } from './stats.js'; import { addLogButtonToCalendarDay, displayLogInTradeModal } from './log-ui.js'; // 当前日期 export let currentDate = new Date(); // 保存交易弹窗的原始内容,用于详情视图切换后恢复 let originalTradeModalContent = ''; // 渲染日历 export function renderCalendar() { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(Date.UTC(year, month, 1)); const lastDay = new Date(Date.UTC(year, month + 1, 0)); document.getElementById('currentMonth').textContent = firstDay.toLocaleString('default', { month: 'long', year: 'numeric' }); const monthlyStats = getMonthlyStats(year, month); document.getElementById('monthlyPnL').innerHTML = formatPnL(monthlyStats.pnL); document.getElementById('tradingDays').textContent = `${monthlyStats.days} days`; const calendar = document.getElementById('calendar'); calendar.innerHTML = ''; // 检测是否为移动设备 const isMobile = window.innerWidth <= 767; // 添加表头 - 移动设备时不显示周日和Weekly const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Weekly']; days.forEach((day, index) => { // 在移动设备上跳过周六日和Weekly列 if (isMobile && (index === 0 || index === 6 || index === 7)) return; const header = document.createElement('div'); header.className = 'calendar-header'; header.textContent = day; calendar.appendChild(header); }); // 添加日期 let currentWeekPnL = 0; let currentWeekDays = 0; let weekStartDate = new Date(firstDay); // 填充月初空白 let i = isMobile?1:0; for (; i < firstDay.getDay(); i++) { calendar.appendChild(document.createElement('div')); } for (let date = new Date(firstDay); date <= lastDay; date.setDate(date.getDate() + 1)) { const dayDiv = document.createElement('div'); dayDiv.className = 'calendar-day'; dayDiv.dataset.date = date.toISOString().split('T')[0]; const day = date.getDate(); const stats = getDailyStats(date); if (stats) { dayDiv.className += stats.pnl >= 0 ? ' trading-day' : ' negative'; dayDiv.innerHTML = ` ${day}
${formatPnL(stats.pnl)}
${stats.trades} symbols
${stats.pnlPercentage.toFixed(1)}%
${stats.winRate.toFixed(1)}% WR
`; // 使用当次迭代的日期拷贝,避免闭包中引用被后续迭代修改 const dateCopy = new Date(date); dayDiv.addEventListener('click', () => showTradeDetails(dateCopy)); // 在日期单元格右上角添加日志按钮 addLogButtonToCalendarDay(dayDiv, date); currentWeekPnL += stats.pnl; currentWeekDays++; } else { dayDiv.textContent = date.getDate(); // 无交易的日期也需要日志按钮 addLogButtonToCalendarDay(dayDiv, date); } calendar.appendChild(dayDiv); // 处理周末或月末 if ((!isMobile) && (date.getDay() === 6 || date.getDate() === lastDay.getDate())) { const weekSummary = document.createElement('div'); weekSummary.className = 'week-summary'; if (currentWeekDays > 0) { // 添加正负数的CSS类 const pnlClass = currentWeekPnL >= 0 ? 'positive' : 'negative'; weekSummary.innerHTML = `
${formatPnL(currentWeekPnL)}
${currentWeekDays} days
`; } // 如果是月末,填充到周六的空白单元格 if (date.getDate() === lastDay.getDate()) { const lastDayOfWeek = date.getDay(); // 如果不是周六(6),则需要填充 if (lastDayOfWeek < 6) { // 计算需要填充的天数(从当前日期到周六) const fillCount = 6 - lastDayOfWeek; // 填充空白单元格 for (let i = 0; i < fillCount; i++) { const emptyDiv = document.createElement('div'); calendar.appendChild(emptyDiv); } } } calendar.appendChild(weekSummary); // 重置周数据 currentWeekPnL = 0; currentWeekDays = 0; weekStartDate = new Date(date); weekStartDate.setDate(date.getDate() + 1); } } } // 月份导航 export function navigateMonth(direction) { currentDate.setMonth(currentDate.getMonth() + direction); renderCalendar(); } // 显示交易详情 export function showTradeDetails(date) { const modal = document.getElementById('tradeModal'); const modalContent = modal ? modal.querySelector('.trade-modal-content') : null; const dateStr = date.toISOString().split('T')[0]; // 首次调用时记录原始内容,方便详情视图关闭后恢复 if (modalContent && !originalTradeModalContent) { originalTradeModalContent = modalContent.innerHTML; } // 获取当日已关闭的交易 const dayTrades = allTrades.filter(trade => trade.TradeDate === dateStr && trade['Open/CloseIndicator'] === 'C' ); // 按股票合并交易记录 const consolidatedTrades = new Map(); dayTrades.forEach(trade => { const symbol = trade.Symbol; const side = trade['Buy/Sell'].toLowerCase() === 'sell' ? 'LONG' : 'SHORT'; if (!consolidatedTrades.has(symbol)) { consolidatedTrades.set(symbol, { Symbol: symbol, Side: side, DateTime: trade.DateTime, // 使用第一笔交易的时间 FifoPnlRealized: 0, Quantity: 0, trades: [], TradeTimes: new Map() }); } const consolidated = consolidatedTrades.get(symbol); consolidated.FifoPnlRealized += parseFloat(trade.FifoPnlRealized) || 0; consolidated.Quantity += Math.abs(parseFloat(trade.Quantity) || 0); consolidated.trades.push(trade); // Count trades by DateTime const tradeTime = trade.DateTime; consolidated.TradeTimes.set(tradeTime, (consolidated.TradeTimes.get(tradeTime) || 0) + 1); }); consolidatedTrades.forEach(consolidated => { // Convert TradeTimes Map to total count of unique times consolidated.TradeTimes = consolidated.TradeTimes.size; }); // 设置模态框标题和统计信息 const dateEl = document.getElementById('modalDate'); dateEl.textContent = date.toLocaleDateString(); dateEl.dataset.date = dateStr; // 计算统计数据 const consolidatedArray = Array.from(consolidatedTrades.values()); const totalPnL = consolidatedArray.reduce((sum, trade) => sum + trade.FifoPnlRealized, 0); const winners = consolidatedArray.filter(trade => trade.FifoPnlRealized > 0).length; const winrate = consolidatedArray.length ? (winners / consolidatedArray.length * 100).toFixed(2) : '0.00'; const totalVolume = consolidatedArray.reduce((sum, trade) => sum + trade.Quantity, 0); const totalProfits = consolidatedArray.reduce((sum, t) => t.FifoPnlRealized > 0 ? sum + t.FifoPnlRealized : sum, 0); const totalLosses = consolidatedArray.reduce((sum, t) => t.FifoPnlRealized < 0 ? sum + Math.abs(t.FifoPnlRealized) : sum, 0); const profitFactor = totalLosses === 0 ? totalProfits : totalProfits / totalLosses; // 更新统计信息显示 const netPnLEl = document.getElementById('modalNetPnL'); if (netPnLEl) { netPnLEl.classList.remove('profit', 'fail'); netPnLEl.classList.add(totalPnL >= 0 ? 'profit' : 'fail'); const prefix = totalPnL >= 0 ? '+' : ''; netPnLEl.textContent = `Net P&L ${prefix}$${totalPnL.toFixed(2)}`; } document.getElementById('modalTotalTrades').textContent = consolidatedArray.length; document.getElementById('modalWinners').textContent = winners; document.getElementById('modalLosers').textContent = consolidatedArray.length - winners; document.getElementById('modalWinrate').textContent = `${winrate}%`; document.getElementById('modalVolume').textContent = totalVolume; document.getElementById('modalProfitFactor').textContent = profitFactor.toFixed(2); // 填充交易表格 const tableBody = document.getElementById('tradesTableBody'); tableBody.innerHTML = ''; consolidatedArray.forEach(trade => { const row = document.createElement('tr'); const pnl = trade.FifoPnlRealized; const roi = ((pnl / TOTAL_ACCOUNT_VALUE) * 100).toFixed(2); row.innerHTML = ` ${trade.DateTime} ${trade.Symbol} ${trade.Side} ${trade.Symbol} ${formatPnL(pnl)} ${roi}% ${trade.TradeTimes} -- `; tableBody.appendChild(row); }); // 在交易详情中渲染日志内容 displayLogInTradeModal(date); // 设置前后交易日导航 const tradeDates = Array.from(new Set(allTrades .filter(t => t['Open/CloseIndicator'] === 'C') .map(t => t.TradeDate) )).sort(); const currentIndex = tradeDates.indexOf(dateStr); const prevBtn = document.getElementById('prevTradeDay'); const nextBtn = document.getElementById('nextTradeDay'); if (prevBtn) { prevBtn.style.display = currentIndex > 0 ? 'inline-block' : 'none'; prevBtn.onclick = () => { if (currentIndex > 0) { showTradeDetails(new Date(tradeDates[currentIndex - 1])); } }; } if (nextBtn) { nextBtn.style.display = currentIndex < tradeDates.length - 1 ? 'inline-block' : 'none'; nextBtn.onclick = () => { if (currentIndex < tradeDates.length - 1) { showTradeDetails(new Date(tradeDates[currentIndex + 1])); } }; } if (modal) { modal.style.display = 'block'; // 确保按钮事件绑定(每次打开都重新绑定以避免丢失) const viewBtn = document.getElementById('viewDetailsBtn'); if (viewBtn) viewBtn.onclick = viewTradeDetails; const closeBtn = document.getElementById('closeTradeModalBtn'); if (closeBtn) closeBtn.onclick = closeTradeModal; // 添加点击外部关闭功能(使用onclick避免重复绑定) modal.onclick = (e) => { if (e.target === modal) { closeTradeModal(); } }; } } // 关闭交易详情弹窗 export function closeTradeModal() { const modal = document.getElementById('tradeModal'); if (modal) { // 如果详情视图替换了内容,先恢复原始结构 const modalContent = modal.querySelector('.trade-modal-content'); if (modalContent && originalTradeModalContent) { modalContent.innerHTML = originalTradeModalContent; } modal.style.display = 'none'; // 清空数据但保留结构,避免破坏已加载的内容 const dateEl = document.getElementById('modalDate'); if (dateEl) dateEl.textContent = ''; const netEl = document.getElementById('modalNetPnL'); if (netEl) { netEl.textContent = ''; netEl.classList.remove('profit', 'fail'); } const totalEl = document.getElementById('modalTotalTrades'); if (totalEl) totalEl.textContent = ''; const winEl = document.getElementById('modalWinners'); if (winEl) winEl.textContent = ''; const loseEl = document.getElementById('modalLosers'); if (loseEl) loseEl.textContent = ''; const winrateEl = document.getElementById('modalWinrate'); if (winrateEl) winrateEl.textContent = ''; const volumeEl = document.getElementById('modalVolume'); if (volumeEl) volumeEl.textContent = ''; const pfEl = document.getElementById('modalProfitFactor'); if (pfEl) pfEl.textContent = ''; const tableBody = document.getElementById('tradesTableBody'); if (tableBody) { tableBody.innerHTML = ''; } // 移除日志部分(如果存在) const logSection = modal.querySelector('.log-section'); if (logSection) { logSection.remove(); } } } // 查看详细交易信息 export function viewTradeDetails() { const dateEl = document.getElementById('modalDate'); const modalContent = document.querySelector('.trade-modal-content'); if (!dateEl || !modalContent) return; const displayDate = dateEl.textContent || ''; let isoDate = dateEl.dataset.date || ''; if (!isoDate && displayDate) { const parsedDate = new Date(displayDate); if (!isNaN(parsedDate)) { isoDate = parsedDate.toISOString().split('T')[0]; } } if (!isoDate) return; // 获取选定日期的详细交易 const detailedTrades = allTrades.filter(trade => trade.TradeDate === isoDate && trade['Open/CloseIndicator'] === 'C' ).sort((a, b) => { const timeA = new Date(a.DateTime).getTime(); const timeB = new Date(b.DateTime).getTime(); return timeA - timeB; }); // 创建详细视图 const detailedView = `
${detailedTrades.map(trade => { const pnl = parseFloat(trade.FifoPnlRealized); const roi = ((pnl / TOTAL_ACCOUNT_VALUE) * 100).toFixed(2); const duration = calculateDuration(trade.OpenDateTime, trade.DateTime); return ` `; }).join('')}
Time Symbol Side Qty Entry Exit Duration P&L ROI%
${new Date(trade.DateTime).toLocaleTimeString()} ${trade.Symbol} ${trade['Buy/Sell']} ${Math.abs(trade.Quantity)} ${trade.TradePrice} ${trade.ClosePrice} ${duration} ${formatPnL(pnl)} ${roi}%
`; modalContent.innerHTML = detailedView; // 添加关闭按钮事件监听 document.getElementById('closeDetailModalBtn').addEventListener('click', closeTradeModal); document.getElementById('closeDetailBtn').addEventListener('click', closeTradeModal); } // 显示日期选择器 // 切换日期选择器显示/隐藏 export function toggleDatePicker() { const datePicker = document.getElementById('dateRangePicker'); datePicker.classList.toggle('active'); // 添加点击外部关闭功能 if (datePicker.classList.contains('active')) { // 使用setTimeout确保当前点击事件不会立即触发关闭 setTimeout(() => { const closeOnClickOutside = (e) => { if (!datePicker.contains(e.target) && e.target.id !== 'showDateRangeBtn') { datePicker.classList.remove('active'); document.removeEventListener('click', closeOnClickOutside); } }; document.addEventListener('click', closeOnClickOutside); }, 0); } } // 更新日历中的交易数据 export function updateCalendarWithTrades() { allTrades.forEach(trade => { // 根据交易数据更新日历显示 if (trade.TradeDate) { const dayElement = document.querySelector(`[data-date="${trade.TradeDate}"]`); if (dayElement) { // 更新日历单元格的数据 updateDayCell(dayElement, trade); } } }); // 重新渲染日历 renderCalendar(); } // 更新日历单元格 function updateDayCell(cell, trade) { // 这里可以根据交易数据更新单元格的显示 // 例如添加交易信息、更改背景色等 const date = cell.getAttribute('data-date'); const stats = getDailyStats(new Date(date)); if (stats) { cell.className = 'calendar-day' + (stats.pnl >= 0 ? ' trading-day' : ' negative'); cell.innerHTML = ` ${new Date(date).getDate()}
${formatPnL(stats.pnl)}
${stats.trades} symbols
${stats.pnlPercentage.toFixed(1)}%
${stats.winRate.toFixed(1)}% WR
`; } } // data.js - 处理交易数据相关功能 import { R2Sync } from './r2-sync.js'; // 初始化 R2Sync export const r2Sync = new R2Sync(); // 全局状态 export let allTrades = []; export let filteredTrades = []; export const TOTAL_ACCOUNT_VALUE = 100000; const DATE_RANGE_STORAGE_KEY = 'pnlSelectedDateRange'; // 从本地存储加载交易数据 export function loadTradesFromStorage() { const storedTrades = localStorage.getItem('trades'); if (storedTrades) { const allTrades = JSON.parse(storedTrades); return Object.values( allTrades.reduce((acc, item) => { // 如果相同交易已存在,则合并 const existTrade = getSameDayTrades(item, acc); if (existTrade) { if (acc[existTrade.TransactionID]["Open/CloseIndicator"] === "C") { acc[existTrade.TransactionID].FifoPnlRealized = parseFloat(acc[existTrade.TransactionID].FifoPnlRealized) + parseFloat(item.FifoPnlRealized); } acc[existTrade.TransactionID].Quantity += item.Quantity; } else { // 如果相同交易不存在,则初始化 acc[item.TransactionID] = { ...item }; } return acc; }, {}) ); } return []; } // 从R2或本地存储加载交易数据 export async function loadTrades() { allTrades = loadTradesFromStorage(); if (allTrades && allTrades.length > 0) { filteredTrades = [...allTrades]; } setTimeout(async() => { const trades = await r2Sync.loadFromR2('trades'); if (trades && trades.length > 0) { allTrades = trades; filteredTrades = [...allTrades]; localStorage.setItem('trades', JSON.stringify(allTrades)); } }, 100); return true; } // 保存交易数据 export async function saveTrades() { localStorage.setItem('trades', JSON.stringify(allTrades)); await r2Sync.syncToR2(allTrades, 'trades'); } export function saveDateRangeSelection(startDate, endDate, range = null) { if (!(startDate instanceof Date) || Number.isNaN(startDate.getTime()) || !(endDate instanceof Date) || Number.isNaN(endDate.getTime())) { return; } try { const payload = { startDate: startDate.toISOString(), endDate: endDate.toISOString(), range: range }; localStorage.setItem(DATE_RANGE_STORAGE_KEY, JSON.stringify(payload)); } catch (error) { console.error('Failed to save date range to localStorage:', error); } } export function getSavedDateRangeSelection() { const storedRange = localStorage.getItem(DATE_RANGE_STORAGE_KEY); if (!storedRange) return null; try { const parsed = JSON.parse(storedRange); if (!parsed.startDate || !parsed.endDate) return null; const startDate = new Date(parsed.startDate); const endDate = new Date(parsed.endDate); if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) { return null; } return { startDate, endDate, range: parsed.range || null }; } catch (error) { console.error('Failed to parse saved date range from localStorage:', error); return null; } } // 清除数据 export function clearData() { if (confirm('Are you sure you want to clear all trade data?')) { localStorage.removeItem('trades'); allTrades = []; filteredTrades = []; return true; } return false; } // 处理文件选择 export function handleFileSelect(event) { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = function (e) { const text = e.target.result; const newTrades = parseCSV(text); mergeTrades(newTrades); allTrades = loadTradesFromStorage(); filteredTrades = [...allTrades]; return true; }; reader.readAsText(file); } // 解析CSV文件 export function parseCSV(text) { const lines = text.split('\n'); const headers = lines[0].split(','); const result = []; for (let i = 1; i < lines.length; i++) { if (!lines[i].trim()) continue; const values = lines[i].split(','); const trade = {}; headers.forEach((header, index) => { trade[header.trim().replace(/"/g, '')] = values[index]?.trim().replace(/"/g, '') || ''; }); result.push(trade); } return result; } // 获取相同日期的交易 function getSameDayTrades(cTrade, aTrades) { const existTrades = Object.values(aTrades); const sameTrades = existTrades.filter(trade => trade.Symbol === cTrade.Symbol && trade.TradeDate === cTrade.TradeDate && trade['Open/CloseIndicator'] === cTrade['Open/CloseIndicator']); if (sameTrades.length > 0) { return sameTrades[0]; } else { return null; } } // 获取交易开仓时间 function getTradeOpenTime(cTrade, aTrades) { const openTime = aTrades.filter(trade => trade.Symbol === cTrade.Symbol && trade.DateTime < cTrade.DateTime && trade['Open/CloseIndicator'] === 'O').reduce((max, item) => (item.DateTime > max ? item.DateTime : max), ""); return openTime; } // 合并交易数据 export function mergeTrades(newTrades) { const tradeMap = new Map(); // 先加载现有交易 allTrades.forEach(trade => { tradeMap.set(trade.TransactionID, trade); }); // 合并新交易 newTrades.forEach(trade => { if (trade['Open/CloseIndicator'] === 'C') { const openTime = getTradeOpenTime(trade, newTrades); trade.OpenDateTime = openTime; } tradeMap.set(trade.TransactionID, trade); }); allTrades = Array.from(tradeMap.values()); localStorage.setItem('trades', JSON.stringify(allTrades)); saveTrades(); } // 处理交易数据 export function processTradeData(csvData) { // 解析CSV数据 const rows = csvData.split('\n'); const headers = rows[0].split(','); const newTrades = []; for (let i = 1; i < rows.length; i++) { if (!rows[i].trim()) continue; const values = rows[i].split(','); const trade = {}; headers.forEach((header, index) => { trade[header.replace(/['"]+/g, '').trim()] = values[index]?.replace(/['"]+/g, '').trim(); }); newTrades.push(trade); } mergeTrades(newTrades); return true; } // 设置日期范围 export function setDateRange(range) { const today = new Date(); let start = new Date(); let end = new Date(); switch (range) { case 'today': start = today; end = today; break; case 'thisWeek': start = new Date(today); start.setDate(today.getDate() - today.getDay()); end = new Date(today); end.setDate(start.getDate() + 6); break; case 'thisMonth': start = new Date(today.getFullYear(), today.getMonth(), 1); end = new Date(today.getFullYear(), today.getMonth() + 1, 1); break; case 'thisQuarter': // 计算当前季度的起始月份 (0-2为第一季度,3-5为第二季度,以此类推) const quarterStartMonth = Math.floor(today.getMonth() / 3) * 3; // 设置当前季度的起始日期(季度第一个月的第一天) start = new Date(today.getFullYear(), quarterStartMonth, 1); // 设置当前季度的结束日期(季度最后一个月的最后一天) end = new Date(today.getFullYear(), quarterStartMonth + 3, 1); break; case 'last30Days': start = new Date(today.getFullYear(), today.getMonth(), today.getDate()-30); end = new Date(today.getFullYear(), today.getMonth(), today.getDate()+1); break; case 'lastMonth': start = new Date(today.getFullYear(), today.getMonth() - 1, 1); end = new Date(today.getFullYear(), today.getMonth(), 1); break; case 'thisYear': start = new Date(today.getFullYear(), 0, 1); end = new Date(today.getFullYear(), 11, 31); break; case 'lastYear': start = new Date(today.getFullYear() - 1, 0, 1); end = new Date(today.getFullYear() - 1, 11, 31); break; case 'ytd': start = new Date(today.getFullYear(), 0, 1); end = new Date(today.getFullYear(), 11, 31); break; case 'all': // 设置一个很早的开始日期和今天作为结束日期 start = new Date(2000, 0, 1); end = today; break; } document.getElementById('startDate').value = start.toISOString().split('T')[0]; document.getElementById('endDate').value = end.toISOString().split('T')[0]; filterTradesByDateRange(start, end); saveDateRangeSelection(start, end, range); } // 根据日期范围过滤交易 export function filterTradesByDateRange(startDate, endDate) { if (!startDate || !endDate) return; // 确保日期是UTC日期对象 const start = new Date(Date.UTC( startDate.getFullYear(), startDate.getMonth(), startDate.getDate() )); const end = new Date(Date.UTC( endDate.getFullYear(), endDate.getMonth(), endDate.getDate(), 23, 59, 59 )); filteredTrades = allTrades.filter(trade => { const tradeDate = new Date(trade.TradeDate); return tradeDate >= start && tradeDate <= end; }); return filteredTrades; } // 根据Symbol过滤交易 export function filterTradesBySymbol(symbol) { if (!symbol) return; // 检查是否是通配符模式 if (symbol.endsWith('*')) { const prefix = symbol.slice(0, -1); // 移除 * 符号 filteredTrades = allTrades.filter(trade => trade.Symbol.startsWith(prefix) ); } else { // 精确匹配 filteredTrades = allTrades.filter(trade => trade.Symbol === symbol ); } return filteredTrades; } // 计算交易持续时间 export function calculateDuration(startTime, endTime) { if (!startTime || !endTime) return '--'; const start = new Date(startTime); const end = new Date(endTime); const diffMinutes = Math.floor((end - start) / (1000 * 60)); if (diffMinutes < 60) { return `${diffMinutes}m`; } else { const hours = Math.floor(diffMinutes / 60); const minutes = diffMinutes % 60; return `${hours}h ${minutes}m`; } } // 格式化盈亏显示 export function formatPnL(pnl) { const prefix = pnl >= 0 ? '+' : ''; const className = pnl >= 0 ? 'profit' : 'fail'; return `${prefix}$${parseFloat(pnl).toFixed(2)}`; } ClientAccountID,AccountAlias,Model,CurrencyPrimary,FXRateToBase,AssetClass,SubCategory,Symbol,Description,Conid,SecurityID,SecurityIDType,CUSIP,ISIN,FIGI,ListingExchange,UnderlyingConid,UnderlyingSymbol,UnderlyingSecurityID,UnderlyingListingExchange,Issuer,IssuerCountryCode,TradeID,Multiplier,RelatedTradeID,Strike,ReportDate,Expiry,DateTime,Put/Call,TradeDate,PrincipalAdjustFactor,SettleDateTarget,TransactionType,Exchange,Quantity,TradePrice,TradeMoney,Proceeds,Taxes,IBCommission,IBCommissionCurrency,NetCash,ClosePrice,Open/CloseIndicator,Notes/Codes,CostBasis,FifoPnlRealized,MtmPnl,OrigTradePrice,OrigTradeDate,OrigTradeID,OrigOrderID,OrigTransactionID,Buy/Sell,ClearingFirmID,IBOrderID,TransactionID,IBExecID,RelatedTransactionID,RTN,BrokerageOrderID,OrderReference,VolatilityOrderLink,ExchOrderID,ExtExecID,OrderTime,OpenDateTime,HoldingPeriodDateTime,WhenRealized,WhenReopened,LevelOfDetail,ChangeInPrice,ChangeInQuantity,OrderType,TraderID,IsAPIOrder,AccruedInterest,InitialInvestment,SerialNumber,DeliveryType,CommodityType,Fineness,Weight ,,,USD,1,STK,COMMON,ACHR,ARCHER AVIATION INC-A,,US03945R1023,ISIN,,,,NYSE,,ACHR,,,,US,,1,,,2024/11/26,,2024/11/26 07:27,,2024/11/26,,2024/11/27,ExchTrade,ISLAND,343,7.3,2503.9,-2503.9,0,-1.731464,USD,-2505.631464,7.22,O,P,2505.631464,0,-27.44,0,,,0,0,BUY,,,30162563534,,,,,,,N/A,,2024/11/26 07:19,,,,,EXECUTION,0,0,LMT,,N,0,,,,,0,0 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Trading Calendar
Start Date End Date

Import Trades

Auto import your trades from your broker or platform.

Monthly stats:
×
Total Trades: Winners: Winrate: Losers: Volume: Profit Factor:
Open Time Ticker Side Instrument Net P&L Net ROI Trade Times Playbook
Net P&L
95
$16,574.07
Trade Win %
77.89%
74
0
21
Profit Factor
1.47
Day Win %
67.27%
37
12
18
Avg win/loss trade
67.27%
37
18

Daily Net Cumulative P&L

Net Daily P&L

Trade Duration Performance

Trade Time Performance

Drawdown

Stock Statistics

Top Profitable Stocks

Symbol Profit Loss Trades

Most Loss Stocks

Symbol Profit Loss Trades

Weekly Statistics

Weekly Win/Loss Trade Analysis

Creative Commons Attribution-NonCommercial-NoDerivs 4.0 International (CC BY-NC-ND 4.0) You are free to: - Share: Copy and redistribute the material in any medium, format, or channel for non-commercial purposes. Under the following terms: - Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. - NonCommercial: You may not use the material for commercial purposes. - NoDerivatives: If you remix, transform, or build upon the material, you may not distribute the modified material. Full license text: https://creativecommons.org/licenses/by-nc-nd/4.0/ // logs.js - 日志管理功能 import { r2Sync } from './data.js'; // 日志数据结构 export let allLogs = []; // 日志数据模式 export const LOG_TEMPLATE = { id: '', // 唯一标识符 date: '', // 日期 YYYY-MM-DD type: 'daily', // 'daily' 或 'weekly' quickReview: { tradesCount: 0, // 交易笔数 overallFeeling: 3 // 1-5分制 }, factRecord: '', // 记录事实 learningPoints: '', // 提炼学习点 improvementDirection: '', // 优化方向 selfAffirmation: '', // 自我肯定 associatedTrades: [], // 关联的交易ID列表 // 每周复盘专用字段 weeklyData: { totalTrades: 0, // 总交易笔数(自动计算) pnlResult: 0, // 盈亏结果(自动计算) maxWin: 0, // 最大单笔盈利(自动计算) maxLoss: 0, // 最大单笔亏损(自动计算) winRate: 0, // 胜率(自动计算) followsDailyLimit: true // 是否遵守"每日最多3笔交易"规则(自动计算) }, successExperiences: { plannedTrades: '', // 哪些交易符合计划,带来了预期结果? emotionalStability: '' // 哪些行为让我情绪稳定? }, mistakeSummary: { violatedPlans: [], // 哪些交易违背了计划?(多选) emotionalFactors: [] // 哪些情绪(贪婪/恐惧/犹豫)影响了操作?(多选) }, nextWeekOptimization: { goodHabitToKeep: '', // 我下周要保持的1个好习惯 mistakeToAvoid: '', // 我下周要避免的1个错误 specificActions: '' // 具体执行动作 }, weeklyAffirmation: '', // 本周最值得肯定的一件事 createdAt: '', // 创建时间戳 updatedAt: '' // 更新时间戳 }; // 从本地存储加载日志数据 export function loadLogsFromStorage() { const storedLogs = localStorage.getItem('logs'); if (storedLogs) { try { return JSON.parse(storedLogs); } catch (error) { console.error('Failed to parse logs from localStorage:', error); return []; } } return []; } // 保存日志数据到本地存储 export function saveLogsToStorage() { try { localStorage.setItem('logs', JSON.stringify(allLogs)); } catch (error) { console.error('Failed to save logs to localStorage:', error); } } // 从R2或本地存储加载日志数据 export async function loadLogs() { allLogs = loadLogsFromStorage(); // 异步从 R2 加载,使用 requestIdleCallback 避免阻塞其他请求 const fetchLogs = async () => { try { const logs = await r2Sync.loadFromR2('logs'); if (logs && logs.length > 0) { allLogs = logs; saveLogsToStorage(); } } catch (error) { console.error('Failed to load logs from R2:', error); } }; if (typeof requestIdleCallback === 'function') { requestIdleCallback(fetchLogs); } else { setTimeout(fetchLogs, 100); } return allLogs; } // 保存日志数据到R2和本地存储 export async function saveLogs() { try { saveLogsToStorage(); await r2Sync.syncToR2(allLogs, 'logs'); } catch (error) { console.error('Failed to save logs:', error); } } // 创建或更新日志 export function createOrUpdateLog(logData) { const now = new Date().toISOString(); if (logData.id) { // 更新现有日志 const index = allLogs.findIndex(log => log.id === logData.id); if (index !== -1) { allLogs[index] = { ...allLogs[index], ...logData, updatedAt: now }; } } else { // 创建新日志 const newLog = { ...LOG_TEMPLATE, ...logData, id: generateLogId(logData.date, logData.type), createdAt: now, updatedAt: now }; allLogs.push(newLog); } saveLogs(); return logData.id || generateLogId(logData.date, logData.type); } // 获取指定日期的日志 export function getLogByDate(date, type = 'daily') { return allLogs.find(log => log.date === date && log.type === type); } // 获取指定日期范围内的日志 export function getLogsByDateRange(startDate, endDate) { return allLogs.filter(log => { return log.date >= startDate && log.date <= endDate; }).sort((a, b) => new Date(b.date) - new Date(a.date)); } // 获取所有日志(按日期倒序) export function getAllLogs() { return allLogs.sort((a, b) => new Date(b.date) - new Date(a.date)); } // 删除日志 export function deleteLog(logId) { const index = allLogs.findIndex(log => log.id === logId); if (index !== -1) { allLogs.splice(index, 1); saveLogs(); return true; } return false; } // 生成日志ID function generateLogId(date, type) { return `${type}_${date}_${Date.now()}`; } // 清空所有日志数据 export function clearLogs() { allLogs = []; localStorage.removeItem('logs'); } // 获取指定周的日志(周复盘) export function getWeeklyLog(weekStartDate) { const weekEnd = new Date(weekStartDate); weekEnd.setDate(weekEnd.getDate() + 6); return allLogs.find(log => log.type === 'weekly' && log.date >= weekStartDate.toISOString().split('T')[0] && log.date <= weekEnd.toISOString().split('T')[0] ); } // 关联交易到日志 export function associateTradeToLog(logId, tradeId) { const log = allLogs.find(l => l.id === logId); if (log && !log.associatedTrades.includes(tradeId)) { log.associatedTrades.push(tradeId); saveLogs(); } } // 取消关联交易 export function disassociateTradeFromLog(logId, tradeId) { const log = allLogs.find(l => l.id === logId); if (log) { const index = log.associatedTrades.indexOf(tradeId); if (index !== -1) { log.associatedTrades.splice(index, 1); saveLogs(); } } } // 批量关联当日交易到日志 export function associateDailyTradesToLog(date, trades) { const log = getLogByDate(date, 'daily'); if (log && trades && trades.length > 0) { trades.forEach(trade => { if (trade.TransactionID && !log.associatedTrades.includes(trade.TransactionID)) { log.associatedTrades.push(trade.TransactionID); } }); saveLogs(); } } // 获取日志的预览文本(前三行) export function getLogPreviewText(log) { const texts = [ log.factRecord, log.learningPoints, log.improvementDirection, log.selfAffirmation ].filter(text => text && text.trim()); return texts.slice(0, 3).join(' ').substring(0, 100) + (texts.join(' ').length > 100 ? '...' : ''); } // log-ui.js - 日志UI交互处理 import { createOrUpdateLog, getLogByDate, getAllLogs, deleteLog, getLogPreviewText, LOG_TEMPLATE } from './logs.js'; import { allTrades } from './data.js'; import { getDailyStats } from './stats.js'; // 日志UI状态 let currentEditingLog = null; let logListPage = 0; const LOGS_PER_PAGE = 20; // 初始化日志UI事件 export function initLogUI() { // 日志按钮事件 const showLogSidebarBtn = document.getElementById('showLogSidebarBtn'); if (showLogSidebarBtn) { showLogSidebarBtn.addEventListener('click', openLogSidebar); } // 关闭侧边栏按钮 const closeLogSidebarBtn = document.getElementById('closeLogSidebar'); if (closeLogSidebarBtn) { closeLogSidebarBtn.addEventListener('click', closeLogSidebar); } // 日志模态框关闭按钮 const closeLogModalBtn = document.getElementById('closeLogModal'); if (closeLogModalBtn) { closeLogModalBtn.addEventListener('click', closeLogModal); } // 日志表单提交 const logForm = document.getElementById('logForm'); if (logForm) { logForm.addEventListener('submit', handleLogFormSubmit); } // 删除日志按钮 const deleteLogBtn = document.getElementById('deleteLogBtn'); if (deleteLogBtn) { deleteLogBtn.addEventListener('click', handleDeleteLog); } // 侧边栏滚动加载 const logSidebarList = document.getElementById('logSidebarList'); if (logSidebarList) { logSidebarList.addEventListener('scroll', handleLogListScroll); } // 点击模态框外部关闭 const logModal = document.getElementById('logModal'); if (logModal) { logModal.addEventListener('click', (e) => { if (e.target === logModal) { closeLogModal(); } }); } // 日志类型/日期变化时自动切换与计算 const typeSel = document.getElementById('logType'); const dateInput = document.getElementById('logDate'); if (typeSel && dateInput) { const recalc = () => { const date = dateInput.value; const type = typeSel.value; if (!date) return; // 自动填充对应类型数据 autoFillTradeInfo(date, type); // 互斥显示:weekly 显示周区块,daily 显示日区块 toggleWeeklyFields(type === 'weekly'); toggleDailyFields(type === 'daily'); }; typeSel.addEventListener('change', recalc); dateInput.addEventListener('change', recalc); } } // 打开日志录入模态框 export function openLogModal(date, logType = null) { const modal = document.getElementById('logModal'); const form = document.getElementById('logForm'); if (!modal || !form) return; // 自动判断日志类型(如果未指定) if (!logType) { const dateObj = new Date(date); const dayOfWeek = dateObj.getDay(); // 0=周日, 1=周一, ..., 6=周六 // 判断:周六为每周复盘,其他为每日复盘 logType = (dayOfWeek === 6) ? 'weekly' : 'daily'; } // 确保日期格式正确(YYYY-MM-DD) let formattedDate = date; if (typeof date === 'string' && date.length === 10) { formattedDate = date; // 已经是 YYYY-MM-DD 格式 } else if (date instanceof Date) { formattedDate = date.toISOString().split('T')[0]; } else { // 尝试解析字符串为日期 const tempDate = new Date(date); if (!isNaN(tempDate.getTime())) { formattedDate = tempDate.toISOString().split('T')[0]; } } // 设置日期和类型 document.getElementById('logDate').value = formattedDate; document.getElementById('logType').value = logType; // 检查是否已有日志 const existingLog = getLogByDate(formattedDate, logType); if (existingLog) { // 编辑现有日志 currentEditingLog = existingLog; populateLogForm(existingLog); document.getElementById('deleteLogBtn').classList.remove('hidden'); document.getElementById('logModalTitle').textContent = '编辑复盘日志'; } else { // 创建新日志 currentEditingLog = null; resetLogForm(); // 重新设置日期和类型(重置可能清空了) document.getElementById('logDate').value = formattedDate; document.getElementById('logType').value = logType; // 自动填充交易信息 autoFillTradeInfo(formattedDate, logType); // 根据类型显示相应区域 if (logType === 'weekly') { const weeklyData = computeWeeklyAutoStats(formattedDate); populateWeeklyAutoFields(weeklyData); toggleWeeklyFields(true); toggleDailyFields(false); } else { toggleWeeklyFields(false); toggleDailyFields(true); } document.getElementById('deleteLogBtn').classList.add('hidden'); document.getElementById('logModalTitle').textContent = '新增复盘日志'; } modal.classList.remove('hidden'); } // 获取指定日期及类型对应的交易信息 function getTradesForLog(date, logType) { if (!date) { return { trades: [], tradeIds: [] }; } if (logType === 'weekly') { const { weekStartStr, weekEndStr } = getWeekRange(date); const trades = allTrades.filter(trade => { const tradeDateStr = (new Date(trade.TradeDate)).toISOString().split('T')[0]; return tradeDateStr >= weekStartStr && tradeDateStr <= weekEndStr; }); const tradeIds = trades.map(trade => trade.TransactionID).filter(id => id); return { trades, tradeIds }; } const trades = allTrades.filter(trade => { const tradeDate = new Date(trade.TradeDate).toISOString().split('T')[0]; return tradeDate === date; }); const tradeIds = trades.map(trade => trade.TransactionID).filter(id => id); return { trades, tradeIds }; } // 自动填充交易信息 function autoFillTradeInfo(date, logType) { const { trades, tradeIds } = getTradesForLog(date, logType); if (logType === 'daily') { // 填充交易次数 document.getElementById('tradeCount').value = trades.length; // 填充关联交易ID document.getElementById('linkedTrades').value = tradeIds.join(','); toggleWeeklyFields(false); toggleDailyFields(true); } else if (logType === 'weekly') { // 填充交易次数 document.getElementById('tradeCount').value = trades.length; // 填充关联交易ID document.getElementById('linkedTrades').value = tradeIds.join(','); // 计算并填充每周自动统计 const weeklyData = computeWeeklyAutoStats(date); populateWeeklyAutoFields(weeklyData); toggleWeeklyFields(true); toggleDailyFields(false); } } // 计算给定日期所在周(周一~周五)的范围 function getWeekRange(dateStr) { const selectedDate = new Date(dateStr); const day = selectedDate.getDay(); // 0(日)~6(六) const offsetToMonday = (day + 6) % 7; // 将周日映射为6,周一为0 const monday = new Date(selectedDate); monday.setDate(selectedDate.getDate() - offsetToMonday); const friday = new Date(monday); friday.setDate(monday.getDate() + 4); const weekStartStr = monday.toISOString().split('T')[0]; const weekEndStr = friday.toISOString().split('T')[0]; return { monday, friday, weekStartStr, weekEndStr }; } // 计算每周自动统计数据 function computeWeeklyAutoStats(dateStr) { const { weekStartStr, weekEndStr } = getWeekRange(dateStr); const weekTrades = allTrades.filter(trade => { const tradeDateStr = (new Date(trade.TradeDate)).toISOString().split('T')[0]; return tradeDateStr >= weekStartStr && tradeDateStr <= weekEndStr; }); // 仅对已关闭交易用于盈亏统计 const closedTrades = weekTrades.filter(t => t['Open/CloseIndicator'] === 'C'); // 使用唯一的TransactionID统计交易笔数 const uniqueIds = new Set(closedTrades.map(t => t.TransactionID).filter(Boolean)); const totalTrades = uniqueIds.size || closedTrades.length; const pnls = closedTrades.map(t => parseFloat(t.FifoPnlRealized) || 0); const pnlResult = pnls.reduce((a, b) => a + b, 0); const maxWin = pnls.length ? Math.max(0, ...pnls) : 0; const maxLoss = pnls.length ? Math.min(0, ...pnls) : 0; // 负数或0 const winners = closedTrades.filter(t => parseFloat(t.FifoPnlRealized) > 0).length; const winRate = (closedTrades.length > 0) ? (winners / closedTrades.length) * 100 : 0; // 是否遵守“每日最多3笔交易”:对每个交易日统计唯一TransactionID数量 const perDayCounts = new Map(); closedTrades.forEach(t => { const d = (new Date(t.TradeDate)).toISOString().split('T')[0]; const key = d; const set = perDayCounts.get(key) || new Set(); if (t.TransactionID) set.add(t.TransactionID); else set.add(`${t.Symbol}-${t.DateTime || ''}-${t.Quantity || ''}`); perDayCounts.set(key, set); }); let followsDailyLimit = true; perDayCounts.forEach(set => { if (set.size > 3) followsDailyLimit = false; }); return { totalTrades, pnlResult, maxWin, maxLoss, winRate, followsDailyLimit }; } // 将周自动数据填充到只读字段 function populateWeeklyAutoFields(weeklyData) { if (!weeklyData) return; const { totalTrades, pnlResult, maxWin, maxLoss, winRate, followsDailyLimit } = weeklyData; const weeklyTotalTrades = document.getElementById('weeklyTotalTrades'); const weeklyPnlResult = document.getElementById('weeklyPnlResult'); const weeklyMaxWinLoss = document.getElementById('weeklyMaxWinLoss'); const weeklyWinRate = document.getElementById('weeklyWinRate'); const weeklyDailyLimit = document.getElementById('weeklyDailyLimit'); if (weeklyTotalTrades) weeklyTotalTrades.value = totalTrades; if (weeklyPnlResult) weeklyPnlResult.value = `${pnlResult >= 0 ? '+' : ''}$${pnlResult.toFixed(2)}`; if (weeklyMaxWinLoss) weeklyMaxWinLoss.value = `$${Math.max(0, maxWin).toFixed(2)} / $${Math.abs(Math.min(0, maxLoss)).toFixed(2)}`; if (weeklyWinRate) weeklyWinRate.value = `${winRate.toFixed(1)}%`; if (weeklyDailyLimit) weeklyDailyLimit.value = followsDailyLimit ? '是' : '否'; } function toggleWeeklyFields(show) { const weeklyFields = document.getElementById('weeklyFields'); if (!weeklyFields) return; if (show) weeklyFields.classList.remove('hidden'); else weeklyFields.classList.add('hidden'); } function toggleDailyFields(show) { const dailyFields = document.getElementById('dailyFields'); if (!dailyFields) return; if (show) dailyFields.classList.remove('hidden'); else dailyFields.classList.add('hidden'); } // 关闭日志模态框 export function closeLogModal() { const modal = document.getElementById('logModal'); if (modal) { modal.classList.add('hidden'); currentEditingLog = null; } } // 填充日志表单 function populateLogForm(log) { document.getElementById('logDate').value = log.date; document.getElementById('logType').value = log.type; document.getElementById('tradeCount').value = log.quickReview?.tradesCount || ''; document.getElementById('feelScore').value = log.quickReview?.overallFeeling || ''; document.getElementById('facts').value = log.factRecord || ''; document.getElementById('learnings').value = log.learningPoints || ''; document.getElementById('improvements').value = log.improvementDirection || ''; document.getElementById('affirmations').value = log.selfAffirmation || ''; const linkedTradesInput = document.getElementById('linkedTrades'); if (linkedTradesInput) { linkedTradesInput.value = log.associatedTrades?.join(',') || ''; } if (log.type === 'weekly') { // 周自动字段 populateWeeklyAutoFields(log.weeklyData || null); // 文本/多选字段 document.getElementById('plannedTrades').value = log.successExperiences?.plannedTrades || ''; document.getElementById('emotionalStability').value = log.successExperiences?.emotionalStability || ''; const violatedPlansEl = document.getElementById('violatedPlans'); const emotionalFactorsEl = document.getElementById('emotionalFactors'); if (violatedPlansEl) { Array.from(violatedPlansEl.options).forEach(opt => { opt.selected = (log.mistakeSummary?.violatedPlans || []).includes(opt.value); }); } if (emotionalFactorsEl) { Array.from(emotionalFactorsEl.options).forEach(opt => { opt.selected = (log.mistakeSummary?.emotionalFactors || []).includes(opt.value); }); } document.getElementById('goodHabitToKeep').value = log.nextWeekOptimization?.goodHabitToKeep || ''; document.getElementById('mistakeToAvoid').value = log.nextWeekOptimization?.mistakeToAvoid || ''; document.getElementById('specificActions').value = log.nextWeekOptimization?.specificActions || ''; document.getElementById('weeklyAffirmation').value = log.weeklyAffirmation || ''; toggleWeeklyFields(true); toggleDailyFields(false); } else { toggleWeeklyFields(false); toggleDailyFields(true); } // 如果日志中未记录关联交易ID,但交易数据已存在,则自动补全 if (linkedTradesInput && !linkedTradesInput.value.trim()) { const { tradeIds } = getTradesForLog(log.date, log.type); if (tradeIds.length > 0) { linkedTradesInput.value = tradeIds.join(','); } } } // 重置日志表单 function resetLogForm() { const form = document.getElementById('logForm'); if (form) { form.reset(); // 设置默认值 document.getElementById('feelScore').value = '3'; } toggleWeeklyFields(false); toggleDailyFields(true); } // 处理日志表单提交 function handleLogFormSubmit(event) { event.preventDefault(); const formData = new FormData(event.target); const type = formData.get('type'); const date = formData.get('date'); // 若为weekly,刷新一次自动字段,确保保存时为最新 let weeklyAuto = null; if (type === 'weekly') { weeklyAuto = computeWeeklyAutoStats(date); populateWeeklyAutoFields(weeklyAuto); } const logData = { id: currentEditingLog?.id, date: date, type: type, quickReview: { tradesCount: parseInt(formData.get('quickReview.count')) || 0, overallFeeling: parseInt(formData.get('quickReview.feel')) || 3 }, factRecord: formData.get('facts'), learningPoints: formData.get('learnings'), improvementDirection: formData.get('improvements'), selfAffirmation: formData.get('affirmations'), associatedTrades: formData.get('linkedTradeIds')?.split(',').map(id => id.trim()).filter(id => id) || [] }; if (type === 'weekly') { logData.weeklyData = weeklyAuto || { totalTrades: parseInt(document.getElementById('weeklyTotalTrades').value) || 0, pnlResult: 0, maxWin: 0, maxLoss: 0, winRate: 0, followsDailyLimit: true }; logData.successExperiences = { plannedTrades: document.getElementById('plannedTrades').value || '', emotionalStability: document.getElementById('emotionalStability').value || '' }; const violatedPlansSel = document.getElementById('violatedPlans'); const emotionalFactorsSel = document.getElementById('emotionalFactors'); logData.mistakeSummary = { violatedPlans: violatedPlansSel ? Array.from(violatedPlansSel.selectedOptions).map(o => o.value) : [], emotionalFactors: emotionalFactorsSel ? Array.from(emotionalFactorsSel.selectedOptions).map(o => o.value) : [] }; logData.nextWeekOptimization = { goodHabitToKeep: document.getElementById('goodHabitToKeep').value || '', mistakeToAvoid: document.getElementById('mistakeToAvoid').value || '', specificActions: document.getElementById('specificActions').value || '' }; logData.weeklyAffirmation = document.getElementById('weeklyAffirmation').value || ''; } try { createOrUpdateLog(logData); closeLogModal(); // 如果侧边栏打开,刷新列表 const sidebar = document.getElementById('logSidebar'); if (sidebar && !sidebar.classList.contains('hidden')) { refreshLogList(); } // 重新渲染日历以显示日志按钮 if (window.renderCalendar) { window.renderCalendar(); } showToast('日志保存成功'); } catch (error) { console.error('保存日志失败:', error); showToast('保存日志失败', 'error'); } } // 处理删除日志 function handleDeleteLog() { if (!currentEditingLog) return; if (confirm('确定要删除这条日志吗?')) { try { deleteLog(currentEditingLog.id); closeLogModal(); // 刷新侧边栏列表 const sidebar = document.getElementById('logSidebar'); if (sidebar && !sidebar.classList.contains('hidden')) { refreshLogList(); } // 重新渲染日历 if (window.renderCalendar) { window.renderCalendar(); } showToast('日志已删除'); } catch (error) { console.error('删除日志失败:', error); showToast('删除日志失败', 'error'); } } } // 打开日志侧边栏 export function openLogSidebar() { const sidebar = document.getElementById('logSidebar'); if (sidebar) { sidebar.classList.remove('hidden'); sidebar.classList.add('active'); logListPage = 0; loadLogList(); } } // 关闭日志侧边栏 export function closeLogSidebar() { const sidebar = document.getElementById('logSidebar'); if (sidebar) { sidebar.classList.add('hidden'); sidebar.classList.remove('active'); } } // 加载日志列表 function loadLogList() { const listContainer = document.getElementById('logSidebarList'); const loadingEl = document.getElementById('logSidebarLoading'); if (!listContainer) return; loadingEl?.classList.remove('hidden'); setTimeout(() => { const allLogs = getAllLogs(); const startIndex = logListPage * LOGS_PER_PAGE; const endIndex = startIndex + LOGS_PER_PAGE; const logsToShow = allLogs.slice(startIndex, endIndex); if (logListPage === 0) { listContainer.innerHTML = ''; } logsToShow.forEach(log => { const logItem = createLogListItem(log); listContainer.appendChild(logItem); }); loadingEl?.classList.add('hidden'); logListPage++; }, 200); } // 创建日志列表项 function createLogListItem(log) { const item = document.createElement('div'); item.className = 'log-item'; item.dataset.logId = log.id; item.dataset.logDate = log.date; const previewText = getLogPreviewText(log); const typeText = log.type === 'weekly' ? '周复盘' : '日复盘'; const dateObj = new Date(log.date); const weekdayMap = ['日','一','二','三','四','五','六']; const weekday = weekdayMap[dateObj.getDay()]; let pnlClass = ''; if (log.type === 'weekly') { const pnl = log.weeklyData?.pnlResult; if (typeof pnl === 'number') { pnlClass = pnl >= 0 ? 'profit' : 'fail'; } } else { const stats = getDailyStats(dateObj); if (stats) { pnlClass = stats.pnl >= 0 ? 'profit' : 'fail'; } } item.innerHTML = `
${log.date} 周${weekday}
${typeText}
${previewText}
快速回顾
交易笔数: ${log.quickReview?.tradesCount || 0}, 感觉评分: ${log.quickReview?.overallFeeling || 0}/5
${log.factRecord ? `
记录事实
${log.factRecord}
` : ''} ${log.learningPoints ? `
提炼学习点
${log.learningPoints}
` : ''} ${log.improvementDirection ? `
优化方向
${log.improvementDirection}
` : ''} ${log.selfAffirmation ? `
自我肯定
${log.selfAffirmation}
` : ''}
`; // 添加点击展开/折叠功能 const header = item.querySelector('.log-item-header'); header.addEventListener('click', () => { item.classList.toggle('expanded'); // 高亮对应日期 highlightCalendarDate(log.date); }); // 添加双击编辑功能 header.addEventListener('dblclick', () => { openLogModal(log.date, log.type); }); return item; } // 高亮日历日期 function highlightCalendarDate(date) { // 移除之前的高亮 document.querySelectorAll('.calendar-day.highlighted').forEach(day => { day.classList.remove('highlighted'); }); // 高亮指定日期 const calendarDays = document.querySelectorAll('.calendar-day'); calendarDays.forEach(day => { if (day.dataset.date === date) { day.classList.add('highlighted'); } }); } // 刷新日志列表 function refreshLogList() { logListPage = 0; loadLogList(); } // 处理列表滚动加载 function handleLogListScroll(event) { const container = event.target; if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) { const allLogs = getAllLogs(); const hasMore = logListPage * LOGS_PER_PAGE < allLogs.length; if (hasMore) { loadLogList(); } } } // 在日历中添加日志按钮 export function addLogButtonToCalendarDay(dayElement, date) { const dateStr = date.toISOString().split('T')[0]; const existingLog = getLogByDate(dateStr, 'daily'); // 避免重复添加按钮(例如重新渲染) const oldBtn = dayElement.querySelector('.log-button'); if (oldBtn) { oldBtn.remove(); } const logButton = document.createElement('button'); logButton.className = 'log-button'; logButton.title = existingLog ? '编辑日志' : '添加日志'; logButton.innerHTML = '📖'; logButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openLogModal(dateStr); }); // 如果已有日志,添加特殊样式 if (existingLog) { logButton.classList.add('has-log'); } dayElement.appendChild(logButton); } // 在交易详情中显示日志 export function displayLogInTradeModal(date) { const dateStr = date.toISOString().split('T')[0]; const log = getLogByDate(dateStr, 'daily'); // 查找交易详情模态框 const tradeModal = document.getElementById('tradeModal'); if (!tradeModal) return; // 查找或创建日志容器 let logContainer = tradeModal.querySelector('.log-section'); if (!logContainer) { logContainer = document.createElement('div'); logContainer.className = 'log-section'; // 插入到表格后面 const table = tradeModal.querySelector('.trades-table'); if (table && table.parentNode) { table.parentNode.insertBefore(logContainer, table.nextSibling); } } // 构建日志内容DOM,避免使用innerHTML覆盖表格的潜在事件 logContainer.innerHTML = ''; const header = document.createElement('div'); header.className = 'log-section-header'; const h3 = document.createElement('h3'); h3.textContent = '复盘日志'; header.appendChild(h3); const actionBtn = document.createElement('button'); actionBtn.className = log ? 'edit-log-btn' : 'add-log-btn'; actionBtn.textContent = log ? '编辑' : '添加日志'; actionBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openLogModal(dateStr); }); header.appendChild(actionBtn); logContainer.appendChild(header); const content = document.createElement('div'); content.className = 'log-section-content'; if (log) { const quick = document.createElement('div'); quick.className = 'log-quick-review'; quick.innerHTML = ` 交易笔数: ${log.quickReview?.tradesCount || 0} 感觉评分: ${log.quickReview?.overallFeeling || 0}/5 `; content.appendChild(quick); if (log.factRecord) { const f = document.createElement('div'); f.className = 'log-field'; f.innerHTML = `记录事实: ${log.factRecord}`; content.appendChild(f); } if (log.learningPoints) { const l = document.createElement('div'); l.className = 'log-field'; l.innerHTML = `提炼学习点: ${log.learningPoints}`; content.appendChild(l); } if (log.improvementDirection) { const i = document.createElement('div'); i.className = 'log-field'; i.innerHTML = `优化方向: ${log.improvementDirection}`; content.appendChild(i); } if (log.selfAffirmation) { const s = document.createElement('div'); s.className = 'log-field'; s.innerHTML = `自我肯定: ${log.selfAffirmation}`; content.appendChild(s); } } else { const no = document.createElement('p'); no.className = 'no-log'; no.textContent = '暂无复盘日志'; content.appendChild(no); } logContainer.appendChild(content); } // 显示提示消息 function showToast(message, type = 'success') { // 创建提示元素 const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; // 添加样式 toast.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translate(-50%, -20px); padding: 12px 24px; border-radius: 4px; color: white; z-index: 10000; font-size: 14px; background: ${type === 'error' ? '#e74c3c' : '#27ae60'}; box-shadow: 0 2px 8px rgba(0,0,0,0.2); opacity: 0; display: inline-block; transition: all 0.3s ease; `; document.body.appendChild(toast); // 显示动画 setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translate(-50%, 0)'; }, 100); // 自动隐藏 setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translate(-50%, -20px)'; setTimeout(() => { document.body.removeChild(toast); }, 300); }, 3000); } // 导出到全局作用域以便HTML中使用 window.logUI = { openLogModal, closeLogModal, openLogSidebar, closeLogSidebar }; import { allTrades, loadTrades, saveTrades, clearData, handleFileSelect, r2Sync, setDateRange, filterTradesByDateRange, filterTradesBySymbol, processTradeData, mergeTrades, saveDateRangeSelection, getSavedDateRangeSelection } from './data.js'; import { renderCalendar, navigateMonth, showTradeDetails, closeTradeModal, viewTradeDetails, toggleDatePicker, currentDate } from './calendar.js'; import { updateStatistics, chartInstances } from './stats.js'; import { loadLogs } from './logs.js'; import { initLogUI } from './log-ui.js'; // DOM Elements let showDateRangeBtn, clearDataBtn, handleImportBtn, showImportModalBtn, configR2Btn, csvFile; // Initialize async function init() { // 初始化DOM元素引用 initDOMElements(); await loadTrades(); await loadLogs(); // 初始化日志UI绑定 initLogUI(); applySavedDateRange(); renderCalendar(); setupEventListeners(); updateStatistics(); // 暴露给全局,供log-ui调用 window.renderCalendar = renderCalendar; } // 初始化DOM元素引用 function initDOMElements() { showDateRangeBtn = document.getElementById('showDateRangeBtn'); clearDataBtn = document.getElementById('clearDataBtn'); handleImportBtn = document.getElementById('handleImportForm'); showImportModalBtn = document.getElementById('showImportModalBtn'); configR2Btn = document.getElementById('configR2Btn'); csvFile = document.getElementById('csvFile'); } // Setup Event Listeners function setupEventListeners() { // 添加安全检查,确保DOM元素存在 if (configR2Btn) configR2Btn.addEventListener('click', handleConfigR2); if (showDateRangeBtn) showDateRangeBtn.addEventListener('click', toggleDatePicker); if (handleImportBtn) handleImportBtn.addEventListener('submit', handleImport); if (showImportModalBtn) showImportModalBtn.addEventListener('click', showImportModal); if (csvFile) csvFile.addEventListener('change', handleFileSelect); if (clearDataBtn) clearDataBtn.addEventListener('click', () => { if (clearData()) { renderCalendar(); updateStatistics(); } }); // 为IB导入表单添加事件监听器 const ibImportForm = document.querySelector('#importModal form'); if (ibImportForm) { ibImportForm.addEventListener('submit', handleIBImport); } // 为所有预设日期按钮添加事件监听器 document.querySelectorAll('.preset-dates button').forEach(button => { button.addEventListener('click', (e) => { const range = e.target.dataset.range; setDateRange(range); renderCalendar(); updateStatistics(); }); }); // 为月份导航按钮添加事件监听器 document.querySelectorAll('.month-navigation button').forEach(button => { button.addEventListener('click', (e) => { const direction = parseInt(e.target.dataset.direction); navigateMonth(direction); }); }); // 为查看详情按钮添加事件监听器 const viewDetailsBtn = document.getElementById('viewDetailsBtn'); if (viewDetailsBtn) { viewDetailsBtn.addEventListener('click', viewTradeDetails); } // 为关闭详情按钮添加事件监听器 const closeDetailsBtn = document.getElementById('closeTradeModalBtn'); if (closeDetailsBtn) { closeDetailsBtn.addEventListener('click', closeTradeModal); } // 日期范围过滤器 const startDateInput = document.getElementById('startDate'); const endDateInput = document.getElementById('endDate'); const applyDateRangeBtn = document.getElementById('applyDateRange'); if (applyDateRangeBtn && startDateInput && endDateInput) { applyDateRangeBtn.addEventListener('click', (e) => { e.stopPropagation(); // 阻止事件冒泡,防止触发外部点击事件 if (!startDateInput.value || !endDateInput.value) return; const startDate = new Date(startDateInput.value); const endDate = new Date(endDateInput.value); if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return; filterTradesByDateRange(startDate, endDate); saveDateRangeSelection(startDate, endDate); renderCalendar(); updateStatistics(); toggleDatePicker(); // 隐藏日期选择器 }); } const symbolFilter = document.getElementById('symbolFilter'); const symbolDropdown = document.getElementById('symbolDropdown'); const symbolDropdownBtn = document.getElementById('symbolDropdownBtn'); // Handle input changes symbolFilter.addEventListener('input', (e) => { const searchText = e.target.value.toLowerCase(); showSymbolDropdown(searchText); }); // Handle dropdown item selection symbolDropdown.addEventListener('click', (e) => { const option = e.target.closest('.symbol-option'); if (option) { const symbol = option.dataset.symbol; filterBySymbol(symbol); } }); // Handle dropdown button click symbolDropdownBtn.addEventListener('click', () => { showSymbolDropdown(); }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.symbol-filter-container')) { symbolDropdown.classList.remove('active'); } }); } function applySavedDateRange() { const savedRange = getSavedDateRangeSelection(); if (!savedRange) return; if (savedRange.range) { setDateRange(savedRange.range); return; } const { startDate, endDate } = savedRange; if (!startDate || !endDate) return; if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return; const startDateInput = document.getElementById('startDate'); const endDateInput = document.getElementById('endDate'); if (startDateInput) startDateInput.value = startDate.toISOString().split('T')[0]; if (endDateInput) endDateInput.value = endDate.toISOString().split('T')[0]; filterTradesByDateRange(startDate, endDate); } // Handle R2 Config function handleConfigR2() { r2Sync.createConfigDialog(() => { loadTrades(); loadLogs(); renderCalendar(); updateStatistics(); }); } // 处理导入 function handleImport(event) { event.preventDefault(); const formData = new FormData(event.target); const csvData = formData.get('csvData'); if (csvData) { processTradeData(csvData); renderCalendar(); updateStatistics(); closeImportModal(); } } // 显示导入模态框 function showImportModal() { const modal = document.getElementById('importModal'); if (modal) { modal.style.display = 'block'; // 添加点击外部关闭功能 modal.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; } }); } } // 关闭导入模态框 function closeImportModal() { const modal = document.getElementById('importModal'); if (modal) modal.style.display = 'none'; } // 页面加载时立即从 localStorage 加载数据 document.addEventListener('DOMContentLoaded', () => { init(); }); // 处理IB导入 async function handleIBImport(event) { event.preventDefault(); // 阻止表单默认提交行为 const token = document.getElementById('flexToken').value; const queryId = document.getElementById('reportId').value; // 显示加载状态 const connectButton = event.submitter; if (connectButton) { const originalText = connectButton.textContent; connectButton.textContent = '正在连接...'; connectButton.disabled = true; } try { // 第一步:获取 reference code const response = await fetchWithProxy( `https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/SendRequest?t=${token}&q=${queryId}&v=3` ); const xmlText = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); const status = xmlDoc.querySelector('Status')?.textContent; if (status === 'Success') { const referenceCode = xmlDoc.querySelector('ReferenceCode')?.textContent; // 第二步:使用 reference code 获取报告 // 注意这里 token 和 reference code 的顺序 const reportResponse = await fetchWithProxy( `https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/GetStatement?t=${token}&q=${referenceCode}&v=3` ); if (reportResponse.ok) { const csvData = await reportResponse.text(); if (csvData.startsWith('"ClientAccountID"')) { processTradeData(csvData); renderCalendar(); updateStatistics(); closeImportModal(); } else { alert(`获取CSV失败: ${csvData}`); } } else { console.error("获取报告失败:", reportResponse.statusText); alert("获取报告失败,请检查您的Token和ReportID"); } } else { const errorMessage = xmlDoc.querySelector('ErrorMessage')?.textContent; throw new Error(errorMessage || '导入失败'); } } catch (error) { alert(`导入失败: ${error.message}`); } finally { // 恢复按钮状态 if (connectButton) { connectButton.textContent = 'Connect'; connectButton.disabled = false; } } } // 添加代理函数处理CORS async function fetchWithProxy(url) { const proxyUrl = 'https://ib.broyustudio.com'; try { const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/plain, text/csv, */*' }, body: JSON.stringify({ url: url, method: 'GET' }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Proxy request failed: ${response.status} - ${errorText}`); } console.log("fetch response: " + response); return response; } catch (error) { console.error('Proxy request error:', error); throw new Error(`Failed to fetch through proxy: ${error.message}`); } } function showSymbolDropdown(searchText = '') { const symbolDropdown = document.getElementById('symbolDropdown'); const uniqueSymbols = [...new Set(allTrades.map(trade => trade.Symbol))]; // 过滤符合搜索文本的 symbols const filteredSymbols = uniqueSymbols.filter(symbol => symbol.toLowerCase().includes(searchText.toLowerCase()) ).sort(); // 生成前缀通配符选项 const wildcardPrefixes = new Set(); // 存储实际的前缀(不带*) if (searchText && searchText.length >= 2) { // 获取所有以搜索文本开头的 symbols const prefixMatches = uniqueSymbols.filter(symbol => symbol.toLowerCase().startsWith(searchText.toLowerCase()) ); // 按长度分组符号 const symbolsByLength = {}; prefixMatches.forEach(symbol => { const length = symbol.length; if (!symbolsByLength[length]) { symbolsByLength[length] = []; } symbolsByLength[length].push(symbol); }); // 对于每个长度,找出共同前缀 Object.keys(symbolsByLength).forEach(length => { const symbols = symbolsByLength[length]; if (symbols.length > 1) { // 找出这组符号的共同前缀 const commonPrefix = findCommonPrefix(symbols); if (commonPrefix.length > searchText.length) { wildcardPrefixes.add(commonPrefix); } } }); // 对于单个符号,检查是否有数字部分可以作为前缀 prefixMatches.forEach(symbol => { // 查找第一个数字的位置 const digitMatch = symbol.match(/\d/); if (digitMatch && digitMatch.index > searchText.length) { const prefix = symbol.substring(0, digitMatch.index); // 检查是否有多个符合此前缀的交易 const matchCount = prefixMatches.filter(s => s.startsWith(prefix)).length; if (matchCount > 1) { wildcardPrefixes.add(prefix); } } }); } // 转换前缀为通配符选项 const wildcardOptions = Array.from(wildcardPrefixes).map(prefix => `${prefix}*`).sort(); // 合并通配符选项和普通选项 const allOptions = [...wildcardOptions, ...filteredSymbols]; // 生成下拉框 HTML symbolDropdown.innerHTML = allOptions.map(option => { const isWildcard = option.endsWith('*'); return `
${option} ${isWildcard ? `(通配符)` : ''}
`; }).join(''); symbolDropdown.classList.add('active'); } // 辅助函数:查找一组字符串的共同前缀 function findCommonPrefix(strings) { if (!strings || strings.length === 0) return ''; if (strings.length === 1) return strings[0]; let prefix = strings[0]; for (let i = 1; i < strings.length; i++) { // 逐字符比较,找出共同前缀 let j = 0; while (j < prefix.length && j < strings[i].length && prefix[j] === strings[i][j]) { j++; } prefix = prefix.substring(0, j); if (prefix === '') break; } return prefix; } function filterBySymbol(symbol) { document.getElementById('symbolFilter').value = symbol; document.getElementById('symbolDropdown').classList.remove('active'); filterTradesBySymbol(symbol); renderCalendar(); updateStatistics(); } // 导出模块 export { init, handleConfigR2, handleImport, showImportModal, closeImportModal, handleIBImport }; // 初始化全屏功能 function initFullscreenButtons() { // 获取所有全屏按钮 const fullscreenBtns = document.querySelectorAll('.fullscreen-btn'); const statModal = document.getElementById('statModal'); const statModalTitle = document.getElementById('statModalTitle'); const statModalContent = document.getElementById('statModalContent'); const closeBtn = statModal.querySelector('.close-button'); // 为每个全屏按钮添加点击事件 fullscreenBtns.forEach(btn => { btn.addEventListener('click', function() { const cardType = this.getAttribute('data-card'); // 如果是图表全屏按钮,不处理(由initChartFullscreenButtons处理) if (this.getAttribute('data-chart')) { return; } // 查找父元素,增加错误处理 const card = this.closest('.stat-card'); if (!card) { console.warn('未找到统计卡片元素'); return; } // 查找标题元素,增加错误处理 const titleElement = card.querySelector('.stat-header span'); const title = titleElement ? titleElement.textContent : '统计详情'; // 设置模态框标题 statModalTitle.textContent = title; // 根据卡片类型生成内容 let content = ''; switch(cardType) { case 'net-pnl': const pnlValue = card.querySelector('.stat-value')?.textContent || '0'; content = `
${pnlValue}
`; break; case 'win-rate': case 'day-win-rate': const winRateValue = card.querySelector('.stat-value')?.textContent || '0%'; const winCount = card.querySelector('.win')?.textContent || '0'; const neutralCount = card.querySelector('.neutral')?.textContent || '0'; const lossCount = card.querySelector('.loss')?.textContent || '0'; content = `
${winRateValue}
${winCount}
${neutralCount !== '0' ? `
${neutralCount}
` : ''}
${lossCount}

胜利交易: ${winCount} 笔

${neutralCount !== '0' ? `

平局交易: ${neutralCount} 笔

` : ''}

亏损交易: ${lossCount} 笔

`; break; case 'profit-factor': const pfValue = card.querySelector('.stat-value')?.textContent || '0'; content = `
${pfValue}

盈利因子是总盈利除以总亏损的比率。高于1表示盈利,值越高越好。

`; break; case 'avg-win-loss': const avgValue = card.querySelector('.stat-value')?.textContent || '0%'; const avgWin = card.querySelector('.win')?.textContent || '0'; const avgLoss = card.querySelector('.loss')?.textContent || '0'; content = `
${avgValue}
${avgWin}
${avgLoss}

平均盈利: ${avgWin}

平均亏损: ${avgLoss}

`; break; default: content = '

无法显示此卡片的详细信息

'; } // 设置模态框内容 statModalContent.innerHTML = content; // 显示模态框 statModal.style.display = 'flex'; }); }); // 关闭按钮事件 if (closeBtn) { closeBtn.addEventListener('click', function() { statModal.style.display = 'none'; }); } else { console.warn('未找到关闭按钮'); } // 点击模态框背景关闭 statModal.addEventListener('click', function(e) { if (e.target === statModal) { statModal.style.display = 'none'; } }); } // 计算百分比 function calculatePercent(value, otherValue1, otherValue2) { const val = parseFloat(value) || 0; const other1 = parseFloat(otherValue1) || 0; const other2 = parseFloat(otherValue2) || 0; const total = val + other1 + other2; return total > 0 ? (val / total * 100) : 0; } // 初始化图表全屏功能 function initChartFullscreenButtons() { // 获取所有图表全屏按钮 const chartFullscreenBtns = document.querySelectorAll('.fullscreen-btn[data-chart]'); const chartModal = document.getElementById('chartModal'); const chartModalTitle = document.getElementById('chartModalTitle'); const chartModalContent = document.getElementById('chartModalContent'); const closeBtn = chartModal.querySelector('.close-button'); // 为每个全屏按钮添加点击事件 chartFullscreenBtns.forEach(btn => { btn.addEventListener('click', function() { const chartId = this.getAttribute('data-chart'); // 修复这里的错误,确保能找到标题元素 let chartTitle = ''; const headerElement = this.closest('.chart-header'); if (headerElement) { const titleElement = headerElement.querySelector('h3'); if (titleElement) { chartTitle = titleElement.textContent; } } // 设置模态框标题 chartModalTitle.textContent = chartTitle || '图表详情'; // 根据图表类型生成内容 let content = ''; if (chartId === 'stockStats') { // 股票统计表格 const stockStatsContainer = this.closest('.stock-stats-container'); if (stockStatsContainer) { const clonedContainer = stockStatsContainer.cloneNode(true); const header = clonedContainer.querySelector('.chart-header'); if (header) header.remove(); // 移除标题部分 content = `
${clonedContainer.innerHTML}
`; } else { content = '
无法加载股票统计数据
'; } } else { // 创建新的canvas元素 content = `
`; } // 设置模态框内容 chartModalContent.innerHTML = content; // 显示模态框 chartModal.style.display = 'flex'; // 如果是图表,复制原图表数据到全屏图表 if (chartId !== 'stockStats' && chartInstances && chartInstances[chartId]) { const originalChart = chartInstances[chartId]; if (originalChart) { try { // 获取原图表的配置 const config = originalChart.config; // 调整全屏图表的配置 if (config.options) { // 确保图表响应式 config.options.responsive = true; config.options.maintainAspectRatio = false; // 增大字体大小 if (config.options.plugins && config.options.plugins.title) { config.options.plugins.title.font = { size: 18 }; } // 增大刻度字体 if (config.options.scales) { Object.keys(config.options.scales).forEach(axis => { if (config.options.scales[axis].ticks) { config.options.scales[axis].ticks.font = { size: 14 }; } }); } // 显示图例 if (config.options.plugins && config.options.plugins.legend) { config.options.plugins.legend.display = true; } } // 创建全屏图表 const canvasElement = document.getElementById(`${chartId}_fullscreen`); if (canvasElement) { // 确保Canvas元素有正确的尺寸 canvasElement.style.width = '100%'; canvasElement.style.height = '100%'; // 延迟一帧再渲染图表,确保DOM已更新 setTimeout(() => { const ctx = canvasElement.getContext('2d'); chartInstances[`${chartId}_fullscreen`] = new Chart(ctx, config); }, 50); } } catch (error) { console.error('创建全屏图表时出错:', error); chartModalContent.innerHTML = '
加载图表时出错
'; } } } }); }); // 关闭按钮事件 if (closeBtn) { closeBtn.addEventListener('click', function() { chartModal.style.display = 'none'; }); } // 点击模态框背景关闭 chartModal.addEventListener('click', function(e) { if (e.target === chartModal) { chartModal.style.display = 'none'; } }); } // 在页面加载完成后初始化图表全屏按钮 document.addEventListener('DOMContentLoaded', function() { // 初始化全屏按钮 initFullscreenButtons(); // 初始化图表全屏按钮 initChartFullscreenButtons(); }); .config-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 1000; width: 95%; max-width: 400px; } .config-dialog label { display: block; margin: 10px 0; } .config-dialog input { width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box; } .config-dialog button { margin: 10px 5px; padding: 8px 15px; } @media (max-width: 576px) { .config-dialog { padding: 15px; } .config-dialog button { width: calc(50% - 10px); margin: 10px 0; } } // r2-sync.js export class R2Sync { constructor() { this.config = { app: '', url: '', token: '', enabled: false }; this.loadConfig(); } // 加载配置 loadConfig() { const stored = localStorage.getItem('r2Config'); if (stored) { this.config = JSON.parse(stored); } } // 保存配置 saveConfig() { localStorage.setItem('r2Config', JSON.stringify(this.config)); } // 更新配置 updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.saveConfig(); } // 同步数据到R2 async syncToR2(data, entity = 'todos') { if (!this.config.enabled) return; // Create a zip file const zip = new JSZip(); // Add files to zip const jsonString = JSON.stringify(data); zip.file(entity, jsonString); // Generate zip content const zipContent = await zip.generateAsync({type: "blob"}); try { await fetch(`${this.config.url}/${this.config.app}/${entity}.zip`, { method: 'PUT', headers: { 'X-Custom-Auth-Key': `${this.config.token}` }, body: zipContent }); } catch (error) { console.error('Upload failed:', error); } } // 从R2加载数据 async loadFromR2(entity = 'todos') { if (!this.config.enabled) return null; try { const response = await fetch(`${this.config.url}/${this.config.app}/${entity}.zip`, { headers: { 'X-Custom-Auth-Key': `${this.config.token}` } }); // 静默处理404错误,避免控制台报错 if (!response.ok) { if (response.status === 404) { // 文件不存在是正常情况,静默返回null return null; } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Get zip file blob const zipBlob = await response.blob(); // Load and parse zip const zip = new JSZip(); const contents = await zip.loadAsync(zipBlob); // Extract files const files = []; for (let filename in contents.files) { const content = await contents.files[filename].async("string"); files.push({ name: filename, content: content }); } return JSON.parse(files[0].content); } catch (error) { // 只在非404错误时输出日志 if (!error.message.includes('404')) { console.error('Download failed:', error); } return null; } } // 创建配置对话框 createConfigDialog(onSave) { const configDialog = document.createElement("div"); configDialog.className = "config-dialog"; configDialog.innerHTML = `

R2 Config

`; document.body.appendChild(configDialog); document.getElementById("save-config").addEventListener("click", () => { const newConfig = { enabled: document.getElementById("r2-enabled").checked, app: document.getElementById("app").value, url: document.getElementById("url").value, token: document.getElementById("token").value, }; this.updateConfig(newConfig); if (onSave) onSave(); configDialog.remove(); }); document.getElementById("close-config").addEventListener("click", () => { configDialog.remove(); }); } } # Trade Calendar with PnL Visualization ([中文](readme_cn.md)) ![logo](favicon.png) ## Table of Contents - [Purpose](#purpose) - [Demo](#demo) - [Screenshots](#screenshots) - [New Feature](#new-feature) - [Features](#features) - [How to Use](#how-to-use) - [Additional Steps(Other brokers)](#additional-stepsother-brokers) - [Recommended Broker](#recommended-broker) ## Purpose ### TOTALLY RUNNING LOCALLY IN YOUR BROWSER WITHOUT ANY DATA UPLOADING This PnL Calendar Tool is designed for active traders and investors to manage and analyze their trading performance visually. By integrating daily profit and loss (PnL) data into an intuitive calendar format, it provides a powerful way to monitor, evaluate, and improve trading strategies. ## Demo Try this: [Demo](https://pnl.broyustudio.com/) ## Screenshots ![Screenshot1](./images/screenshot1.jpg) ![Screenshot2](./images/screenshot2.jpg) ## New Feature - Add two charts "Weekly Statistics" and "Weekly Win/Loss Trade Analysis" ![Screenshot7](./images/screenshot7.png) ![Screenshot8](./images/screenshot8.png) - Add "Top Profitable Stocks" and "Most Loss Stocks" Lists ![Screenshot6](./images/screenshot6.png) - CloudFlare R2 storage and sync ![Screenshot4](./images/screenshot4.png) - Zipped json file to 20% size ![Screenshot5](./images/screenshot5.png) ## Features ### 1. Interactive Calendar View - Displays daily and weekly PnL summaries with dynamic color-coded cells. - Identifies winning and losing days at a glance. ### 2. Detailed Trade Analysis - Clickable dates to view detailedtrade data, including individual trade metrics like ROI, PnL, and performance indicators. ### 3. Statistical Insights - Summarizes monthly performance statistics: net PnL, win rates,profit factor, and average trade outcomes. - Visualizes trends with charts fordaily PnL, cumulative PnL, trade durations, and drawdowns. ### 4. Data Import - Seamlessly import trading datafrom Interactive Brokers (IBKR) usingFlex Queries. - Supports CSV uploads for manual trade data input. ### 5. Customizable Date Ranges - Analyze specific time periods using the built-in date range picker. ## How to Use ### 1. Set Up - Open the main.html file in any modern browser. - Ensure your trading data is accessible in CSV format or via Interactive Brokers Flex Queries. - Default proxy(for CORS) is my personal CloudFlare worker, please replace it with yours for security. ### 2. Import Data: - Click “Import IB Data” in the toolbar. ![Import IB data](./images/screenshot3.jpg) - Enter your IBKR Flex Token and Report ID to fetch your trading data.[The how-to guide from tradezella document](https://intercom.help/tradezella-4066d388d93c/en/articles/6063403-interactive-broker-how-to-sync-your-interactive-broker-ibkr-account-with-tradezella) - Alternatively, upload a CSV file containing your trade history, which's still exported from IB flex queries. ### 3. Explore Your Performance: - Navigate through months to view daily and weekly summaries. - Click on any day to see detailed trade data. - Use the toolbar to filter by custom date ranges. ### 4. Analyze Trends: - Scroll down to view cumulative PnL, trade duration insights, andother advanced metrics through dynamic charts. ### 5. Save and Manage Data: - All data is stored locally inyour browser. Use the “Clear Data”button to reset when needed. ## Using a Proxy to Handle CORS Issues The tool integrates with Interactive Brokers’ Flex Queries API, which requires a server-side proxy to bypass CORS restrictions when fetching data. A proxyUrl is used for this purpose in the code. Setting Up a Proxy with Cloudflare Workers You can easily set up your own proxy using Cloudflare Workers([code example](./worker.js)). Deploying Your Proxy 1. Log in to your Cloudflare dashboard. 2. Navigate to Workers > Create a Service. 3. Copy the worker.js code into the script editor. 4. Deploy your worker and note the assigned URL (e.g., https://your-worker-url.workers.dev). 5. Replace the proxyUrl in the tool’s JavaScript code with your worker’s URL. const proxyUrl = 'https://your-worker-url.workers.dev'; By setting up your proxy, you ensure a secure and reliable way to fetch API data without being affected by CORS restrictions. ## Additional Steps(Other brokers) * if you want to try another broker, you might refer this: [IB CSV file](./example_ib.csv) * By now, there're a few fields necessary of the trade: 1. TransactionID: A unique identifier for each trade. 2. Symbol: The code of the traded instrument (e.g., stock or other assets). 3. TradeDate: The date when the trade occurred. 4. Open/CloseIndicator: Indicates whether the trade is an open or close position, with common values being O (Open) or C (Close). 5. DateTime: The timestamp of the trade. 6. Quantity: The number of shares/contracts traded. 7. FifoPnlRealized: The realized profit or loss of the trade. 8. Buy/Sell: Indicates whether the trade is a buy or sell operation. 9. OrderTime: The time when the trade order was placed. Usage Explanation: • These fields are used for daily, weekly, and monthly PnL calculations (e.g., FifoPnlRealized and TradeDate). • Used for summarizing trade statistics by instrument (e.g., Symbol and Quantity). • The Open/CloseIndicator distinguishes open and close trades and is used to categorize them in the calendar. • Key trade information (e.g., Buy/Sell, DateTime, and FifoPnlRealized) is displayed in the detailed trade view. • DateTime and OpenDateTime are used to calculate trade duration performance. If you need additional clarification or further details about any field, let me know! ## Recommended Broker To experience seamless trading data integration and low-cost trading services, we highly recommend using Interactive Brokers (IBKR). Start your journey with one of the most powerful trading platforms today. ## Sign up here: [Interactive Brokers Referral Link](https://ibkr.com/referral/yu950) Enjoy analyzing your trades and making better-informed decisions with this tool! Let us know if you have any feedback or suggestions. ## License This project is licensed under the Creative Commons Attribution-NonCommercial-NoDerivs 4.0 International License - see the [LICENSE](LICENSE) file for details. # 交易日历与盈亏可视化 ![logo](favicon.png) ## 目录 - [目的](#目的) - [演示](#演示) - [截图](#截图) - [新功能](#新功能) - [特性](#特性) - [使用方法](#使用方法) - [额外步骤(其他券商)](#额外步骤其他券商) - [推荐券商](#推荐券商) ## 目的 ### 完全在您的浏览器本地运行,不上传任何数据 这个盈亏日历工具专为活跃交易者和投资者设计,用于可视化管理和分析他们的交易表现。通过将每日盈亏(PnL)数据整合到直观的日历格式中,它提供了一种强大的方式来监控、评估和改进交易策略。 ## 演示 试用链接:[演示](https://pnl.broyustudio.com/) ## 截图 ![截图1](./images/screenshot1.jpg) ![截图2](./images/screenshot2.jpg) ## 新功能 - 添加了两个图表"每周统计"和"每周盈亏交易分析" ![截图7](./images/screenshot7.png) ![截图8](./images/screenshot8.png) - 添加了"最盈利股票"和"最亏损股票"列表 ![截图6](./images/screenshot6.png) - CloudFlare R2 存储和同步 ![截图4](./images/screenshot4.png) - 压缩 json 文件至原大小的 20% ![截图5](./images/screenshot5.png) ## 特性 ### 1. 交互式日历视图 - 显示每日和每周盈亏摘要,带有动态颜色编码的单元格。 - 一目了然地识别盈利和亏损日。 ### 2. 详细交易分析 - 可点击日期查看详细交易数据,包括个别交易指标如 ROI、盈亏和表现指标。 ### 3. 统计洞察 - 汇总月度表现统计:净盈亏、胜率、盈利因子和平均交易结果。 - 通过图表可视化趋势,包括每日盈亏、累计盈亏、交易持续时间和回撤。 ### 4. 数据导入 - 使用 Flex 查询无缝导入盈透证券(IBKR)的交易数据。 - 支持 CSV 上传,用于手动输入交易数据。 ### 5. 可自定义日期范围 - 使用内置日期范围选择器分析特定时间段。 ## 使用方法 ### 1. 设置 - 在任何现代浏览器中打开 main.html 文件。 - 确保您的交易数据可以通过 CSV 格式或盈透证券 Flex 查询访问。 - 默认代理(用于 CORS)是我个人的 CloudFlare worker,出于安全考虑,请替换为您自己的。 ### 2. 导入数据: - 点击工具栏中的"导入 IB 数据"。 ![导入 IB 数据](./images/screenshot3.jpg) - 输入您的 IBKR Flex Token 和 Report ID 以获取您的交易数据。[来自 tradezella 文档的操作指南](https://intercom.help/tradezella-4066d388d93c/en/articles/6063403-interactive-broker-how-to-sync-your-interactive-broker-ibkr-account-with-tradezella) - 或者,上传包含您的交易历史的 CSV 文件,该文件仍然从 IB flex 查询导出。 ### 3. 探索您的表现: - 浏览月份以查看每日和每周摘要。 - 点击任何一天查看详细的交易数据。 - 使用工具栏按自定义日期范围进行筛选。 ### 4. 分析趋势: - 向下滚动查看累计盈亏、交易持续时间洞察和通过动态图表展示的其他高级指标。 ### 5. 保存和管理数据: - 所有数据都存储在您的浏览器本地。需要时使用"清除数据"按钮重置。 ## 使用代理处理 CORS 问题 该工具与盈透证券的 Flex 查询 API 集成,在获取数据时需要服务器端代理来绕过 CORS 限制。代码中使用了 proxyUrl 来实现这一目的。 使用 Cloudflare Workers 设置代理 您可以使用 Cloudflare Workers 轻松设置自己的代理([代码示例](./worker.js))。 部署您的代理 1. 登录到您的 Cloudflare 控制面板。 2. 导航到 Workers > 创建服务。 3. 将 worker.js 代码复制到脚本编辑器中。 4. 部署您的 worker 并记下分配的 URL(例如,https://your-worker-url.workers.dev)。 5. 将工具的 JavaScript 代码中的 proxyUrl 替换为您的 worker 的 URL。 `const proxyUrl = ' https://your-worker-url.workers.dev ';` 通过设置您的代理,您可以确保一种安全可靠的方式来获取 API 数据,而不受 CORS 限制的影响。 ## 额外步骤(其他券商) * 如果您想尝试其他券商,可以参考:[IB CSV 文件](./example_ib.csv) * 目前,交易中有几个必要的字段: 1. TransactionID: 每笔交易的唯一标识符。 2. Symbol: 交易工具的代码(例如,股票或其他资产)。 3. TradeDate: 交易发生的日期。 4. Open/CloseIndicator: 表示交易是开仓还是平仓,常见值为 O(开仓)或 C(平仓)。 5. DateTime: 交易的时间戳。 6. Quantity: 交易的股票/合约数量。 7. FifoPnlRealized: 交易的已实现盈亏。 8. Buy/Sell: 表示交易是买入还是卖出操作。 9. OrderTime: 下单时间。 使用说明: * 这些字段用于每日、每周和每月盈亏计算(例如,FifoPnlRealized 和 TradeDate)。 * 用于按工具汇总交易统计(例如,Symbol 和 Quantity)。 * Open/CloseIndicator 区分开仓和平仓交易,并用于在日历中对它们进行分类。 * 关键交易信息(例如,Buy/Sell、DateTime 和 FifoPnlRealized)显示在详细交易视图中。 * DateTime 和 OpenDateTime 用于计算交易持续时间表现。 如果您需要任何字段的额外说明或更多详细信息,请告诉我! ## 推荐券商 为了体验无缝的交易数据集成和低成本交易服务,我们强烈推荐使用盈透证券(IBKR)。立即开始使用最强大的交易平台之一。 ## 在此注册:[盈透证券推荐链接](https://ibkr.com/referral/yu950) 享受分析您的交易并做出更明智决策的乐趣!如果您有任何反馈或建议,请告诉我们。 ## 许可证 本项目采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可证 - 详情请参阅 [LICENSE](LICENSE) 文件。 // stats.js - 处理统计数据和图表相关功能 import { allTrades, filteredTrades, TOTAL_ACCOUNT_VALUE, formatPnL } from './data.js'; // 全局存储Chart实例 export const chartInstances = {}; // 获取每日统计数据 export function getDailyStats(date) { const dateStr = date.toISOString().split('T')[0]; // 只统计已关闭的交易 const dayTrades = allTrades.filter(trade => trade.TradeDate === dateStr && trade['Open/CloseIndicator'] === 'C' ); if (dayTrades.length === 0) return null; // 按股票合并交易数量 const symbolTrades = new Map(); dayTrades.forEach(trade => { if (!symbolTrades.has(trade.Symbol)) { symbolTrades.set(trade.Symbol, { quantity: 0, pnl: 0 }); } const stats = symbolTrades.get(trade.Symbol); stats.quantity += Math.abs(parseFloat(trade.Quantity) || 0); stats.pnl += parseFloat(trade.FifoPnlRealized) || 0; }); const totalPnL = Array.from(symbolTrades.values()).reduce((sum, stat) => sum + stat.pnl, 0); const totalTrades = symbolTrades.size; const pnlPercentage = (totalPnL / TOTAL_ACCOUNT_VALUE) * 100; const winningTrades = dayTrades.filter(trade => parseFloat(trade.FifoPnlRealized) > 0).length; const winRate = (winningTrades / dayTrades.length) * 100; return { pnl: totalPnL, trades: totalTrades, winRate: winRate, pnlPercentage: pnlPercentage, symbols: Array.from(symbolTrades.entries()) }; } // 获取每周统计数据 export function getWeeklyStats(startDate, endDate) { let totalPnL = 0; let tradingDays = 0; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { const stats = getDailyStats(d); if (stats) { totalPnL += stats.pnl; tradingDays++; } } return { pnL: totalPnL, days: tradingDays }; } // 获取每月统计数据 export function getMonthlyStats(year, month) { const startDate = new Date(Date.UTC(year, month, 1)); const endDate = new Date(Date.UTC(year, month + 1, 0)); return getWeeklyStats(startDate, endDate); } // 计算统计数据 export function calculateStats() { // 只统计已关闭的交易 const dayTrades = filteredTrades.filter(trade => trade['Open/CloseIndicator'] === 'C' ); // 1. Net P&L 计算 const netPnL = dayTrades.reduce((sum, trade) => sum + parseFloat(trade.FifoPnlRealized), 0); // 2. Trade Win % 计算 const winningTrades = dayTrades.filter(trade => parseFloat(trade.FifoPnlRealized) > 0); const losingTrades = dayTrades.filter(trade => parseFloat(trade.FifoPnlRealized) < 0); const neutralTrades = dayTrades.filter(trade => parseFloat(trade.FifoPnlRealized) === 0); const winRate = (winningTrades.length / dayTrades.length) * 100; // 3. Profit Factor 计算 const totalProfits = winningTrades.reduce((sum, trade) => sum + parseFloat(trade.FifoPnlRealized), 0); const totalLosses = Math.abs(losingTrades.reduce((sum, trade) => sum + parseFloat(trade.FifoPnlRealized), 0)); const profitFactor = totalLosses === 0 ? totalProfits : totalProfits / totalLosses; // 4. Day Win % 计算 const tradingDays = new Map(); dayTrades.forEach(trade => { const date = new Date(trade.OrderTime).toDateString(); if (!tradingDays.has(date)) { tradingDays.set(date, { pnL: 0 }); } tradingDays.get(date).pnL += parseFloat(trade.FifoPnlRealized); }); const winningDays = Array.from(tradingDays.values()).filter(day => day.pnL > 0).length; const losingDays = Array.from(tradingDays.values()).filter(day => day.pnL < 0).length; const neutralDays = Array.from(tradingDays.values()).filter(day => day.pnL === 0).length; const dayWinRate = (winningDays / tradingDays.size) * 100; // 5. Average Win/Loss Trade 计算 const avgWin = winningTrades.length > 0 ? totalProfits / winningTrades.length : 0; const avgLoss = losingTrades.length > 0 ? totalLosses / losingTrades.length : 0; const avgRate = (avgWin / avgLoss) * 100; // 6. 生成每日累计P&L数据 const dailyCumulativePnL = []; let cumulative = 0; Array.from(tradingDays.entries()) .sort(([dateA], [dateB]) => new Date(dateA) - new Date(dateB)) .forEach(([date, data]) => { cumulative += data.pnL; dailyCumulativePnL.push({ date: date, value: cumulative }); }); // 7. 生成每日P&L数据 const dailyPnL = Array.from(tradingDays.entries()) .sort(([dateA], [dateB]) => new Date(dateA) - new Date(dateB)) .map(([date, data]) => ({ date: date, value: data.pnL })); return { netPnL: netPnL, tradeCount: dayTrades.length, winRate: { percentage: winRate, winning: winningTrades.length, neutral: neutralTrades.length, losing: losingTrades.length }, profitFactor: profitFactor, dayWinRate: { percentage: dayWinRate, winning: winningDays, neutral: neutralDays, losing: losingDays }, avgTrade: { avgRate: avgRate, avgWin: avgWin, avgLoss: avgLoss, winning: avgWin.toFixed(2), losing: avgLoss.toFixed(2) }, dailyCumulativePnL: dailyCumulativePnL, dailyPnL: dailyPnL }; } // 计算高级统计数据 export function calculateAdvancedStats() { const basicStats = calculateStats(); // 计算交易持续时间性能 const durationPerformance = filteredTrades.filter(trade => trade['Open/CloseIndicator'] === 'C').reduce((acc, trade) => { // 计算交易持续时间(分钟) const duration = (new Date(trade.DateTime) - new Date(trade.OpenDateTime)) / (1000 * 60); // 创建持续时间区间 let durationRange; if (duration <= 5) durationRange = '0-5m'; else if (duration <= 15) durationRange = '5-15m'; else if (duration <= 30) durationRange = '15-30m'; else if (duration <= 60) durationRange = '30-60m'; else if (duration <= 120) durationRange = '1-2h'; else if (duration <= 240) durationRange = '2-4h'; else durationRange = '4h+'; // 初始化区间数据 if (!acc[durationRange]) { acc[durationRange] = { totalPnL: 0, count: 0, winners: 0, losers: 0 }; } // 更新区间统计 acc[durationRange].totalPnL += parseFloat(trade.FifoPnlRealized); acc[durationRange].count += 1; if (parseFloat(trade.FifoPnlRealized) > 0) acc[durationRange].winners += 1; if (parseFloat(trade.FifoPnlRealized) < 0) acc[durationRange].losers += 1; return acc; }, {}); // 转换为图表数据格式 const durationPerformanceData = { labels: ['0-5m', '5-15m', '15-30m', '30-60m', '1-2h', '2-4h', '4h+'], avgPnL: [], winRate: [] }; durationPerformanceData.labels.forEach(label => { const data = durationPerformance[label] || { totalPnL: 0, count: 0, winners: 0, losers: 0 }; const avgPnL = data.count > 0 ? data.totalPnL / data.count : 0; const winRate = data.count > 0 ? (data.winners / data.count) * 100 : 0; durationPerformanceData.avgPnL.push(avgPnL); durationPerformanceData.winRate.push(winRate); }); // 计算交易时间性能 const timePerformance = filteredTrades.reduce((acc, trade) => { const hour = new Date(trade.DateTime).getHours(); if (!acc[hour]) { acc[hour] = { totalPnL: 0, count: 0, winners: 0, losers: 0 }; } acc[hour].totalPnL += parseFloat(trade.FifoPnlRealized); acc[hour].count += 1; if (parseFloat(trade.FifoPnlRealized) > 0) acc[hour].winners += 1; if (parseFloat(trade.FifoPnlRealized) < 0) acc[hour].losers += 1; return acc; }, {}); // 确保所有小时都有数据,并按照时间顺序排列 const timePerformanceData = { labels: Array.from({ length: 24 }, (_, i) => { const hour = i.toString().padStart(2, '0'); return `${hour}:00`; }), avgPnL: [], winRate: [] }; timePerformanceData.labels.forEach((_, i) => { const data = timePerformance[i] || { totalPnL: 0, count: 0, winners: 0, losers: 0 }; const avgPnL = data.count > 0 ? data.totalPnL / data.count : 0; const winRate = data.count > 0 ? (data.winners / data.count) * 100 : 0; timePerformanceData.avgPnL.push(avgPnL); timePerformanceData.winRate.push(winRate); }); // 计算回撤 const drawdown = calculateDrawdown(basicStats.dailyPnL); // 计算每支股票的统计数据 const stockStats = {}; filteredTrades.filter(trade => trade['Open/CloseIndicator'] === 'C').forEach(trade => { if (!stockStats[trade.Symbol]) { stockStats[trade.Symbol] = { symbol: trade.Symbol, totalProfit: 0, totalLoss: 0, tradeCount: 0 }; } const profit = parseFloat(trade.FifoPnlRealized); stockStats[trade.Symbol].tradeCount++; if (profit >= 0) { stockStats[trade.Symbol].totalProfit += profit; } else { stockStats[trade.Symbol].totalLoss += Math.abs(profit); } }); // 转换为数组并排序 const stockList = Object.values(stockStats); const sortedByProfit = [...stockList].sort((a, b) => b.totalProfit - a.totalProfit); const sortedByLoss = [...stockList].sort((a, b) => b.totalLoss - a.totalLoss); // 添加周统计计算 const weeklyStats = filteredTrades.reduce((acc, trade) => { const date = new Date(trade.TradeDate); const weekKey = `${date.getFullYear()}-W${getWeekNumber(date)}`; if (!acc[weekKey]) { acc[weekKey] = { winCount: 0, totalTrades: 0, totalPnL: 0, tradeAmount: 0 }; } if (trade['Open/CloseIndicator'] === 'C') { const pnl = parseFloat(trade.FifoPnlRealized); acc[weekKey].totalPnL += pnl; acc[weekKey].win = pnl >= 0 ? (acc[weekKey].win || 0) + 1 : (acc[weekKey].win || 0); acc[weekKey].winProfit = pnl >= 0 ? (acc[weekKey].winProfit || 0) + pnl : (acc[weekKey].winProfit || 0); acc[weekKey].loss = pnl < 0 ? (acc[weekKey].loss || 0) + 1 : (acc[weekKey].loss || 0); acc[weekKey].lossAmount = pnl < 0 ? (acc[weekKey].lossAmount || 0) + Math.abs(pnl) : (acc[weekKey].lossAmount || 0); acc[weekKey].totalTrades++; acc[weekKey].tradeAmount += Math.abs(parseFloat(trade.CostBasis)); if (pnl > 0) acc[weekKey].winCount++; } return acc; }, {}); // 转换为图表数据格式 const weeklyData = Object.entries(weeklyStats).map(([week, data]) => ({ week, winRate: (data.winCount / data.totalTrades) * 100, tradeCount: data.totalTrades, avgWinLoss: (data.winProfit / (data.win || 1)) / ((data.lossAmount / (data.loss || 1)) || 1) * 100, winProfit: data.winProfit || 0, lossAmount: data.lossAmount || 0, tradeAmount: data.tradeAmount })); return { ...basicStats, durationPerformance: durationPerformanceData, timePerformance: timePerformanceData, drawdown: drawdown, stockStats: { topProfitable: sortedByProfit.slice(0, 3), topLosses: sortedByLoss.slice(0, 3) }, weeklyData: weeklyData.sort((a, b) => a.week.localeCompare(b.week)) }; } // 计算回撤 function calculateDrawdown(dailyPnL) { let peak = 0; let currentDrawdown = 0; let maxDrawdown = 0; let drawdownStart = null; let drawdownEnd = null; let maxDrawdownStart = null; let maxDrawdownEnd = null; let equity = 0; const drawdownData = []; dailyPnL.forEach((day, index) => { equity += day.value; if (equity > peak) { peak = equity; // 如果有正在进行的回撤,则结束它 if (drawdownStart !== null) { drawdownEnd = index - 1; drawdownStart = null; } } else { currentDrawdown = peak - equity; // 如果这是新的回撤开始 if (drawdownStart === null) { drawdownStart = index; } // 如果这是最大回撤 if (currentDrawdown > maxDrawdown) { maxDrawdown = currentDrawdown; maxDrawdownStart = drawdownStart; maxDrawdownEnd = index; } } // 记录每日回撤数据 drawdownData.push({ date: day.date, equity: equity, drawdown: peak > 0 ? (peak - equity) : 0 }); }); return { maxDrawdown: maxDrawdown, maxDrawdownPercent: peak > 0 ? (maxDrawdown / peak) * 100 : 0, maxDrawdownPeriod: maxDrawdownStart !== null ? { start: dailyPnL[maxDrawdownStart]?.date, end: dailyPnL[maxDrawdownEnd]?.date } : null, drawdownData: drawdownData }; } // 获取周数 function getWeekNumber(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); } // 更新统计信息 export function updateStatistics() { const stats = calculateAdvancedStats(); // 更新统计卡片 - 添加安全检查 const tradeCountEl = document.querySelector('.stat-header .trade-count'); if (tradeCountEl) tradeCountEl.textContent = stats.tradeCount; const netPnLEl = document.querySelector('.stat-card .stat-value'); if (netPnLEl) netPnLEl.textContent = `$${stats.netPnL.toLocaleString(undefined, { minimumFractionDigits: 2 })}`; // 修改胜率统计卡片,使条形图宽度与实际数值成比例 const winRateEl = document.querySelector('#win-rate .stat-value'); if (winRateEl) winRateEl.textContent = `${stats.winRate.percentage.toFixed(2)}%`; const winEl = document.querySelector('#win-rate .win'); if (winEl) { winEl.textContent = stats.winRate.winning; // 计算胜率比例 const totalTrades = stats.winRate.winning + stats.winRate.neutral + stats.winRate.losing; const winPercent = totalTrades > 0 ? (stats.winRate.winning / totalTrades * 100) : 0; winEl.style.width = `${winPercent}%`; } const neutralEl = document.querySelector('#win-rate .neutral'); if (neutralEl) { neutralEl.textContent = stats.winRate.neutral; // 计算平局比例 const totalTrades = stats.winRate.winning + stats.winRate.neutral + stats.winRate.losing; const neutralPercent = totalTrades > 0 ? (stats.winRate.neutral / totalTrades * 100) : 0; neutralEl.style.width = `${neutralPercent}%`; } const lossEl = document.querySelector('#win-rate .loss'); if (lossEl) { lossEl.textContent = stats.winRate.losing; // 计算亏损比例 const totalTrades = stats.winRate.winning + stats.winRate.neutral + stats.winRate.losing; const lossPercent = totalTrades > 0 ? (stats.winRate.losing / totalTrades * 100) : 0; lossEl.style.width = `${lossPercent}%`; } const profitFactorEl = document.querySelector('#profit-factor .stat-value'); if (profitFactorEl) profitFactorEl.textContent = stats.profitFactor.toFixed(2); // 修改日胜率统计卡片,使条形图宽度与实际数值成比例 const dayWinRateEl = document.querySelector('#day-win-rate .stat-value'); if (dayWinRateEl) dayWinRateEl.textContent = `${stats.dayWinRate.percentage.toFixed(2)}%`; const dayWinEl = document.querySelector('#day-win-rate .win'); if (dayWinEl) { dayWinEl.textContent = stats.dayWinRate.winning; // 计算日胜率比例 const totalDays = stats.dayWinRate.winning + stats.dayWinRate.neutral + stats.dayWinRate.losing; const dayWinPercent = totalDays > 0 ? (stats.dayWinRate.winning / totalDays * 100) : 0; dayWinEl.style.width = `${dayWinPercent}%`; } const dayNeutralEl = document.querySelector('#day-win-rate .neutral'); if (dayNeutralEl) { dayNeutralEl.textContent = stats.dayWinRate.neutral; // 计算日平局比例 const totalDays = stats.dayWinRate.winning + stats.dayWinRate.neutral + stats.dayWinRate.losing; const dayNeutralPercent = totalDays > 0 ? (stats.dayWinRate.neutral / totalDays * 100) : 0; dayNeutralEl.style.width = `${dayNeutralPercent}%`; } const dayLossEl = document.querySelector('#day-win-rate .loss'); if (dayLossEl) { dayLossEl.textContent = stats.dayWinRate.losing; // 计算日亏损比例 const totalDays = stats.dayWinRate.winning + stats.dayWinRate.neutral + stats.dayWinRate.losing; const dayLossPercent = totalDays > 0 ? (stats.dayWinRate.losing / totalDays * 100) : 0; dayLossEl.style.width = `${dayLossPercent}%`; } // 修改平均盈亏统计卡片,使条形图宽度与实际数值成比例 const avgWinLossEl = document.querySelector('#avg-win-loss .stat-value'); if (avgWinLossEl) avgWinLossEl.textContent = `${stats.avgTrade.avgRate.toFixed(2)}%`; const avgWinEl = document.querySelector('#avg-win-loss .win'); if (avgWinEl) { avgWinEl.textContent = stats.avgTrade.winning; // 计算平均盈利比例 const totalAvg = parseFloat(stats.avgTrade.winning) + parseFloat(stats.avgTrade.losing); const avgWinPercent = totalAvg > 0 ? (parseFloat(stats.avgTrade.winning) / totalAvg * 100) : 0; avgWinEl.style.width = `${avgWinPercent}%`; } const avgLossEl = document.querySelector('#avg-win-loss .loss'); if (avgLossEl) { avgLossEl.textContent = stats.avgTrade.losing; // 计算平均亏损比例 const totalAvg = parseFloat(stats.avgTrade.winning) + parseFloat(stats.avgTrade.losing); const avgLossPercent = totalAvg > 0 ? (parseFloat(stats.avgTrade.losing) / totalAvg * 100) : 0; avgLossEl.style.width = `${avgLossPercent}%`; } // 更新图表 updateCharts(stats); updateAdvancedCharts(stats); } // 销毁已存在的Chart实例 export function destroyCharts() { Object.values(chartInstances).forEach(chart => { if (chart) { chart.destroy(); } }); } // 更新基础图表 export function updateCharts(stats) { // 在创建新图表前销毁已存在的图表 destroyCharts(); // 累计盈亏图表 const cumulativePnLChart = document.getElementById('cumulativePnLChart'); if (cumulativePnLChart) { chartInstances.cumulativePnLChart = new Chart( cumulativePnLChart.getContext('2d'), { type: 'line', data: { labels: stats.dailyCumulativePnL.map(d => d.date), datasets: [{ label: 'Cumulative P&L', data: stats.dailyCumulativePnL.map(d => d.value), fill: true, borderColor: '#2ecc71', backgroundColor: 'rgba(46, 204, 113, 0.1)', tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: value => '$' + value.toLocaleString() } }, x: { ticks: { maxRotation: 45, minRotation: 45, autoSkip: true, maxTicksLimit: 10 } } } } } ); } // 每日盈亏图表 const dailyPnLChart = document.getElementById('dailyPnLChart'); if (dailyPnLChart) { chartInstances.dailyPnLChart = new Chart( dailyPnLChart.getContext('2d'), { type: 'bar', data: { labels: stats.dailyPnL.map(d => d.date), datasets: [{ label: 'Daily P&L', data: stats.dailyPnL.map(d => d.value), backgroundColor: function (context) { const value = context.raw; return value >= 0 ? '#2ecc71' : '#e74c3c'; } }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { ticks: { callback: value => '$' + value.toLocaleString() } } } } } ); } } // 更新高级图表 export function updateAdvancedCharts(stats) { // 添加isMobile变量定义 const isMobile = window.innerWidth <= 767; // 通用图表配置 const commonChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: isMobile ? 'bottom' : 'top', labels: { boxWidth: isMobile ? 10 : 40, font: { size: isMobile ? 10 : 12 } } } }, layout: { padding: { left: 5, right: 5, top: 10, bottom: 10 } } }; // 持续时间性能图表 const durationPerformanceChart = document.getElementById('durationPerformanceChart'); if (durationPerformanceChart) { chartInstances.durationPerformanceChart = new Chart( durationPerformanceChart.getContext('2d'), { type: 'bar', data: { labels: stats.durationPerformance.labels, datasets: [ { label: 'Average P&L', data: stats.durationPerformance.avgPnL, backgroundColor: stats.durationPerformance.avgPnL.map(val => val >= 0 ? '#2ecc71' : '#e74c3c'), yAxisID: 'y' }, { label: 'Win Rate %', data: stats.durationPerformance.winRate, type: 'line', borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)', yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { position: 'left', title: { display: true, text: 'Average P&L ($)' }, ticks: { callback: value => '$' + value.toLocaleString() } }, y1: { position: 'right', title: { display: true, text: 'Win Rate (%)' }, min: 0, max: 100, grid: { drawOnChartArea: false } } } } } ); } // 交易时间性能图表 const timePerformanceChart = document.getElementById('timePerformanceChart'); if (timePerformanceChart) { chartInstances.timePerformanceChart = new Chart( timePerformanceChart.getContext('2d'), { type: 'bar', data: { labels: stats.timePerformance.labels, datasets: [ { label: 'Average P&L', data: stats.timePerformance.avgPnL, backgroundColor: stats.timePerformance.avgPnL.map(val => val >= 0 ? '#2ecc71' : '#e74c3c'), yAxisID: 'y' }, { label: 'Win Rate %', data: stats.timePerformance.winRate, type: 'line', borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)', yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { position: 'left', title: { display: true, text: 'Average P&L ($)' }, ticks: { callback: value => '$' + value.toLocaleString() } }, y1: { position: 'right', title: { display: true, text: 'Win Rate (%)' }, min: 0, max: 100, grid: { drawOnChartArea: false } } } } } ); } // 回撤图表 if (stats.drawdown && stats.drawdown.drawdownData.length > 0) { const drawdownChart = document.getElementById('drawdownChart'); if (drawdownChart) { chartInstances.drawdownChart = new Chart( drawdownChart.getContext('2d'), { type: 'line', data: { labels: stats.drawdown.drawdownData.map(d => d.date), datasets: [ { label: 'Drawdown', data: stats.drawdown.drawdownData.map(d => d.drawdown), fill: true, borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)', yAxisID: 'y' } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { position: 'left', title: { display: true, text: 'Drawdown ($)' }, ticks: { callback: value => value + '$' } } } } } ); } } // Update stock lists const formatMoney = (num) => `$${num.toFixed(2)}`; const topProfitList = document.getElementById('topProfitableStocks'); topProfitList.innerHTML = stats.stockStats.topProfitable.map(stock => ` ${stock.symbol} ${formatMoney(stock.totalProfit)} ${formatMoney(stock.totalLoss)} ${stock.tradeCount} `).join(''); const topLossList = document.getElementById('topLossStocks'); topLossList.innerHTML = stats.stockStats.topLosses.map(stock => ` ${stock.symbol} ${formatMoney(stock.totalProfit)} ${formatMoney(stock.totalLoss)} ${stock.tradeCount} `).join(''); // 周度胜率和交易次数图表 chartInstances.weeklyStatsChart = new Chart( document.getElementById('weeklyStatsChart').getContext('2d'), { type: 'bar', data: { labels: stats.weeklyData.map(d => d.week), datasets: [ { label: 'Trade Count', data: stats.weeklyData.map(d => d.tradeCount), type: 'bar', backgroundColor: 'rgba(46, 204, 113, 0.6)', yAxisID: 'y1' }, { label: 'Win Rate %', data: stats.weeklyData.map(d => d.winRate), type: 'line', borderColor: '#3498db', yAxisID: 'y2' } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y1: { position: 'left', beginAtZero: true, title: { display: true, text: 'Trade Count' } }, y2: { position: 'right', beginAtZero: true, max: 100, title: { display: true, text: 'Win Rate %' }, ticks: { callback: value => value + '%' } } } } } ); // 周度平均盈亏和交易金额图表 chartInstances.weeklyTradeAnalysisChart = new Chart( document.getElementById('weeklyTradeAnalysisChart').getContext('2d'), { type: 'bar', data: { labels: stats.weeklyData.map(d => d.week), datasets: [ { label: 'Win Profit', data: stats.weeklyData.map(d => d.winProfit), type: 'bar', backgroundColor: 'rgba(46, 204, 113, 0.6)', stack: 'amount' }, { label: 'Loss Amount', data: stats.weeklyData.map(d => d.lossAmount), type: 'bar', backgroundColor: 'rgba(231, 76, 60, 0.6)', stack: 'amount' }, { label: 'Avg Win/Loss', data: stats.weeklyData.map(d => d.avgWinLoss), type: 'line', borderColor: '#f1c40f', borderWidth: 2, fill: false, yAxisID: 'y2' } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y1: { position: 'left', stacked: true, beginAtZero: true, title: { display: true, text: 'Amount ($)' }, ticks: { callback: value => '$' + value.toFixed(2) } }, y2: { position: 'right', beginAtZero: true, title: { display: true, text: 'Avg Win/Loss (%)' }, ticks: { callback: value => value.toFixed(2) + '%' } } }, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': $' + context.raw.toFixed(2); } } } } } } ); // 股票列表表格响应式调整 if (isMobile) { const stockTables = document.querySelectorAll('.stock-stats-table'); stockTables.forEach(table => { table.style.fontSize = '0.8em'; table.style.width = '100%'; }); } } body { font-family: Arial, sans-serif; margin: 10px; background: #f5f5f5; } .container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 10px; box-sizing: border-box; } /* 响应式头部 */ .header { display: flex; flex-direction: column; gap: 15px; margin-bottom: 20px; } @media (min-width: 768px) { .header { flex-direction: row; justify-content: space-between; align-items: center; } } /* 响应式日历 */ .calendar { display: grid; grid-template-columns: repeat(1, 1fr); gap: 10px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow-x: auto; } /* 响应式统计卡片 */ .stats-cards { display: grid; grid-template-columns: repeat(1, 1fr); gap: 15px; margin-bottom: 20px; } @media (min-width: 576px) { .stats-cards { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 992px) { .stats-cards { grid-template-columns: repeat(5, 1fr); } } /* 图表容器样式 */ .charts-container, .charts-section { display: grid; grid-template-columns: 1fr; gap: 20px; margin-top: 20px; width: 100%; max-width: 100%; overflow-x: hidden; box-sizing: border-box; } @media (min-width: 768px) { .charts-container, .charts-section { grid-template-columns: repeat(2, 1fr); } } /* 图表项样式 */ .chart-item, .chart-container, .chart-large { background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); width: 100%; max-width: 100%; overflow: hidden; box-sizing: border-box; height: 400px; /* 固定高度 */ position: relative; /* 添加相对定位 */ } .chart-item h3, .chart-container h3, .chart-large h3 { margin: 0 0 15px 0; font-size: 16px; color: #333; } /* 图表包装器样式 */ .chart-wrapper { position: relative; height: 300px; /* 固定高度 */ width: 100%; max-width: 100%; overflow: hidden; box-sizing: border-box; } .chart-wrapper canvas, .chart-item canvas { max-width: 100% !important; height: auto !important; max-height: 350px !important; box-sizing: border-box; } /* 股票统计表格样式 */ .stock-stats-container { display: flex; flex-direction: column; gap: 20px; padding: 15px; background: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); width: 100%; max-width: 100%; overflow-x: hidden; } .stock-stats-tables { display: flex; flex-wrap: wrap; /* 允许在小屏幕上换行 */ gap: 20px; width: 100%; } .stock-stats-table { flex: 1; min-width: 250px; /* 设置最小宽度 */ max-width: 100%; /* 限制最大宽度 */ width: 100%; border-collapse: collapse; margin-top: 10px; } .stock-stats-table th, .stock-stats-table td { padding: 8px; text-align: left; border-bottom: 1px solid #eee; } .stock-stats-table th { background: #f5f5f5; font-weight: 600; color: #333; } .stock-stats-table tbody tr:hover { background: #f8f8f8; } .stock-stats-table .symbol { font-weight: 600; color: #2962ff; } .stock-stats-table .profit { color: #2e7d32; } .stock-stats-table .loss { background-color: white; color: #c62828; } /* 统计卡片样式 */ .stat-card { background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stat-header { display: flex; justify-content: space-between; align-items: center; color: #666; font-size: 14px; margin-bottom: 10px; } /* 添加头部控件容器样式 */ .header-controls { display: flex; align-items: center; gap: 8px; } .info-icon, .fullscreen-btn { cursor: pointer; } /* 响应式调整 */ @media (max-width: 767px) { .stat-modal-content { width: 95%; padding: 15px; } .stat-modal .chart-wrapper { height: 300px; } } .fullscreen-btn { cursor: pointer; color: #666; font-size: 16px; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; transition: background-color 0.2s; } .fullscreen-btn:hover { background-color: rgba(0,0,0,0.1); } /* 全屏模态框样式 */ .stat-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; } .stat-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); width: 80%; max-width: 800px; max-height: 80vh; overflow-y: auto; position: relative; } .stat-modal .close-button { position: absolute; top: 10px; right: 10px; background: transparent; border: none; font-size: 24px; cursor: pointer; color: #333; } /* 全屏图表样式 */ .stat-modal .chart-wrapper { height: 500px; } .stat-value { font-size: 24px; font-weight: bold; margin-bottom: 10px; } .stat-value.positive { color: #2ecc71; } .stat-gauge { display: flex; height: 20px; border-radius: 2px; overflow: hidden; width: 100%; } /* 修改子元素样式,不再使用flex属性固定比例 */ .win { background: #2ecc71; height: 100%; /* 宽度将通过JavaScript动态设置 */ } .neutral { background: #95a5a6; height: 100%; /* 宽度将通过JavaScript动态设置 */ } .loss { background: #e74c3c; height: 100%; /* 宽度将通过JavaScript动态设置 */ } /* 回撤部分样式 */ .drawdown-section { display: flex; gap: 20px; } .drawdown-chart { flex: 1; } /* 图表统计部分样式 */ .chart-item stats-section { margin-bottom: 20px; } .chart-item stats-section h4 { margin-bottom: 10px; color: #333; } .chart-item stats-list { list-style: none; padding: 0; } .chart-item stats-list li { padding: 8px 0; border-bottom: 1px solid #ddd; } .chart-item stats-list li:last-child { border-bottom: none; } /* GitHub链接样式 */ .github-link { position: fixed; color: #24292e; opacity: 0.7; transition: opacity 0.2s; z-index: 1000; display: inline-block; } .github-link:hover { opacity: 1; } /* 交易详情样式 */ .trades-details { max-height: 500px; overflow-y: auto; margin: 20px 0; } .trades-table th, .trades-table td { padding: 8px 12px; text-align: left; } .trades-table tr:hover { background-color: #f5f5f5; } /* 按钮样式 */ button { background: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 5px; font-size: 14px; transition: background 0.2s; } button:hover { background: #2980b9; } button svg { width: 16px; height: 16px; } /* 工具栏样式 */ .toolbar { display: flex; justify-content: space-between; align-items: center; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; flex-wrap: wrap; gap: 10px; } .toolbar-left, .toolbar-right { display: flex; gap: 10px; flex-wrap: wrap; } @media (max-width: 767px) { .toolbar { flex-direction: column; align-items: flex-start; } .toolbar-right { margin-top: 10px; width: 100%; justify-content: space-between; } button { padding: 6px 12px; font-size: 12px; } .stats-cards { grid-template-columns: repeat(2, 1fr); } .stat-card { padding: 10px; } .stat-header { font-size: 12px; } .stat-value { font-size: 18px; } } @media (max-width: 480px) { .stats-cards { grid-template-columns: 1fr; } } /* 日历样式 */ #calendar { display: grid; grid-template-columns: repeat(8, 1fr); gap: 10px; margin-top: 20px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .calendar-header { font-weight: bold; text-align: center; padding: 10px; background: #f8f9fa; border-radius: 4px; } .calendar-day { position: relative; } .log-button { position: absolute; top: 6px; right: 6px; background: transparent; border: none; padding: 10px; line-height: 1; border-radius: 6px; cursor: pointer; } .log-button:hover { background: rgba(0,0,0,0.06); } .log-button.has-log { color: #1e80ff; } .calendar-day { min-height: 100px; padding: 10px; background: #f8f9fa; border-radius: 4px; position: relative; cursor: pointer; transition: transform 0.2s; } .calendar-day:hover { transform: scale(1.02); } /* 交易弹窗中的日志区域样式 */ .log-section { margin-top: 20px; padding: 15px; background: #fff; border: 1px solid #eee; border-radius: 8px; } .log-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .log-section-header h3 { margin: 0; font-size: 16px; color: #333; } .add-log-btn, .edit-log-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; color: #fff; background: #1976d2; } .edit-log-btn { background: #ff9800; } .add-log-btn:hover { background: #145ca7; } .edit-log-btn:hover { background: #fb8c00; } .log-section-content { font-size: 14px; line-height: 1.6; color: #555; } .log-field { margin-bottom: 8px; } .no-log { color: #999; font-style: italic; } /* 日历头部样式 */ .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .chart-header h3 { margin: 0; font-size: 16px; color: #333; } /* 图表全屏模态框样式 */ .chart-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; } .chart-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); width: 90%; max-width: 1200px; height: 80vh; overflow-y: auto; position: relative; } .chart-modal .close-button { position: absolute; top: 10px; right: 10px; background: transparent; border: none; font-size: 24px; cursor: pointer; color: #333; } .chart-modal .chart-wrapper { width: 100%; height: 65vh; position: relative; } .chart-modal canvas { width: 100% !important; height: 100% !important; max-height: none !important; } /* 响应式调整 */ @media (max-width: 767px) { .chart-modal-content { width: 95%; padding: 15px; height: 90vh; } .chart-modal .chart-wrapper { height: 60vh; } } .month-navigation { display: flex; align-items: center; gap: 15px; } #currentMonth { font-size: 18px; font-weight: bold; } .month-stats { display: flex; flex-direction: column; align-items: flex-end; } .trading-day { background: rgba(46, 204, 113, 0.1); border-left: 3px solid #2ecc71; } .negative { background: rgba(231, 76, 60, 0.1); border-left: 3px solid #e74c3c; } .trade-info { margin-top: 5px; font-size: 0.9em; color: #555; } .week-summary { min-height: 100px; padding: 10px; background: #eaeaea; border-radius: 4px; display: flex; flex-direction: column; justify-content: center; align-items: center; font-weight: bold; text-align: center; } /* 添加weekly统计的正负数颜色 */ .week-summary .positive { color: #2ecc71; } .week-summary .negative { color: #e74c3c; } /* 日期选择器样式 */ .date-picker-container { position: relative; } .date-range-picker { display: none; position: absolute; top: 100%; left: 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 100; min-width: 300px; } .date-range-picker.active { display: block; } .date-picker-group { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } .date-picker-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } /* Go按钮样式 */ .go-button { background: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; } .go-button:hover { background: #2980b9; } .preset-dates { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; } .preset-dates button { width: 100%; justify-content: center; } /* 符号过滤器样式 */ .symbol-filter-container { position: relative; margin-left: 10px; } .combobox-wrapper { display: flex; align-items: center; background: white; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } .symbol-input { padding: 8px; border: none; outline: none; width: 150px; font-size: 14px; } .dropdown-toggle { padding: 8px; background: none; border: none; border-left: 1px solid #ddd; cursor: pointer; } .dropdown-toggle:hover { background: #f5f5f5; } .symbol-dropdown { display: none; position: absolute; top: 100%; left: 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 100; min-width: 300px; } .symbol-dropdown.active { display: block; } .symbol-option { padding: 8px; cursor: pointer; } .symbol-option:hover { background: #f5f5f5; } /* 弹框样式 - 默认隐藏 */ #tradeModal, #importModal, #r2ConfigModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色背景 */ z-index: 1000; justify-content: center; align-items: center; } .modal-content, .trade-modal-content, .import-modal-content, .r2-config-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; margin: auto; /* 确保居中 */ margin-top: 100px; width: 100%; max-width: 800px; /* 限制最大宽度 */ } .import-form { display: flex; flex-direction: column; gap: 20px; } .form-group { display: flex; flex-direction: column; gap: 8px; } .form-group label { color: #666; } .form-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .broker-select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white; } .connect-button { padding: 12px; background: #5D5FEF; color: white; border: none; border-radius: 4px; cursor: pointer; } /* 当弹框显示时的样式 */ #tradeModal.show, #importModal.show, #r2ConfigModal.show { display: flex; } /* 移动设备上的日期选择器 */ @media (max-width: 767px) { .date-range-picker { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 350px; } .date-picker-group { flex-direction: column; align-items: flex-start; } .date-picker-group input { width: 100%; } } /* 通配符选项样式 */ .wildcard-option { background-color: #f0f8ff; /* 浅蓝色背景 */ font-weight: 500; } .wildcard-hint { font-size: 0.8em; color: #666; margin-left: 5px; } /* 日志模态框使用完全居中的弹窗样式 */ #logModal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); z-index: 2000; } #logModal.hidden { display: none; } /* 日志弹窗内容样式 - 完全居中无偏移 */ #logModal .modal-content.log-modal { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; margin: 0; /* 移除上边距,完全居中 */ width: 100%; max-width: 600px; /* 适合日志表单的宽度 */ } /* 日志弹窗内部元素微调 */ #logModal .close { position: absolute; top: 10px; right: 14px; font-size: 22px; color: #666; cursor: pointer; } #logModal .close:hover { color: #333; } #logModal h2 { margin: 0 0 16px 0; font-size: 18px; } #logModal .form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } #logModal .form-row.two-cols { flex-direction: row; gap: 12px; } #logModal .form-row.two-cols > div { flex: 1; } #logModal .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; } #logModal .form-actions .primary { background: #1976d2; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; } #logModal .form-actions .primary:hover { background: #145ca7; } #logModal .form-actions .danger { background: #f44336; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; } #logModal .form-actions .danger:hover { background: #d32f2f; } .log-modal h2 { margin: 0 0 20px 0; } .log-modal .form-group { margin-bottom: 16px; } .log-modal label { display: block; font-weight: 600; color: #333; margin-bottom: 6px; } .log-modal input[type="date"], .log-modal select, .log-modal textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; } .log-modal textarea { min-height: 80px; resize: vertical; } /* Log button on calendar day */ .log-btn { position: absolute; right: 6px; top: 6px; width: 22px; height: 22px; border: none; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; cursor: pointer; } .log-btn:hover { background: #f3f3f3; } .log-btn svg { width: 14px; height: 14px; color: #1976d2; } /* Utility: hidden helper */ .hidden { display: none !important; } /* Logs: sidebar, list, buttons (restored) */ .log-sidebar-btn svg { color: #333; } .log-sidebar { position: fixed; left: -420px; top: 0; width: 380px; height: 100vh; background: #fff; box-shadow: 2px 0 10px rgba(0,0,0,0.1); z-index: 999; transition: left 0.3s ease; overflow-y: auto; display: flex; flex-direction: column; } .log-sidebar.active { left: 0; } .log-sidebar-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #eee; background: #f8f9fa; } .log-sidebar-header h3 { margin: 0; color: #333; font-size: 18px; } .log-list { flex: 1; padding: 20px; overflow-y: auto; } .log-item { border: 1px solid #eee; border-radius: 8px; margin-bottom: 15px; overflow: hidden; transition: box-shadow 0.2s; background: #fff; } .log-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .log-item-header { padding: 15px; cursor: pointer; background: #f8f9fa; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; } .log-item-date { font-weight: 600; color: #333; font-size: 16px; } .log-item-type { font-size: 12px; padding: 4px 8px; border-radius: 12px; background: #e3f2fd; color: #1976d2; } .log-item-type.weekly { background: #f3e5f5; color: #7b1fa2; } .log-item-preview { padding: 15px; color: #666; font-size: 14px; line-height: 1.4; max-height: 60px; overflow: hidden; } .log-item-content { display: none; padding: 15px; border-top: 1px solid #eee; background: #fff; } .log-item.expanded .log-item-content { display: block; } .log-item.expanded .log-item-header { background: #e8f5e8; } .log-content-section { margin-bottom: 15px; } .log-content-section h5 { margin: 0 0 5px 0; color: #333; font-size: 14px; font-weight: 600; } .log-content-text { color: #666; font-size: 14px; line-height: 1.4; margin-bottom: 10px; } .log-quick-review { display: flex; gap: 15px; margin-bottom: 10px; } .log-quick-item { display: flex; flex-direction: column; gap: 2px; } .log-quick-label { font-size: 12px; color: #666; } .log-quick-value { font-weight: 600; color: #333; } .log-loading { text-align: center; padding: 20px; color: #666; } .close-button { background: transparent; color: #333; font-size: 24px; position: absolute; right: 10px; top: 5px; padding: 0; width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; } .close-button:hover { background: rgba(0,0,0,0.1); } /* 工具栏样式 */ .toolbar { display: flex; justify-content: space-between; align-items: center; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; flex-wrap: wrap; gap: 10px; } .toolbar-left, .toolbar-right { display: flex; gap: 10px; flex-wrap: wrap; } @media (max-width: 767px) { .toolbar { flex-direction: column; align-items: flex-start; } .toolbar-right { margin-top: 10px; width: 100%; justify-content: space-between; } button { padding: 6px 12px; font-size: 12px; } .stats-cards { grid-template-columns: repeat(2, 1fr); } .stat-card { padding: 10px; } .stat-header { font-size: 12px; } .stat-value { font-size: 18px; } } @media (max-width: 480px) { .stats-cards { grid-template-columns: 1fr; } } /* 日历样式 */ #calendar { display: grid; grid-template-columns: repeat(8, 1fr); gap: 10px; margin-top: 20px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .calendar-header { font-weight: bold; text-align: center; padding: 10px; background: #f8f9fa; border-radius: 4px; } .calendar-day { position: relative; } .log-button { position: absolute; top: 6px; right: 6px; background: transparent; border: none; padding: 10px; line-height: 1; border-radius: 6px; cursor: pointer; } .log-button:hover { background: rgba(0,0,0,0.06); } .log-button.has-log { color: #1e80ff; } .calendar-day { min-height: 100px; padding: 10px; background: #f8f9fa; border-radius: 4px; position: relative; cursor: pointer; transition: transform 0.2s; } .calendar-day:hover { transform: scale(1.02); } /* 日历头部样式 */ .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .chart-header h3 { margin: 0; font-size: 16px; color: #333; } /* 图表全屏模态框样式 */ .chart-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; } .chart-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); width: 90%; max-width: 1200px; height: 80vh; overflow-y: auto; position: relative; } .chart-modal .close-button { position: absolute; top: 10px; right: 10px; background: transparent; border: none; font-size: 24px; cursor: pointer; color: #333; } .chart-modal .chart-wrapper { width: 100%; height: 65vh; position: relative; } .chart-modal canvas { width: 100% !important; height: 100% !important; max-height: none !important; } /* 响应式调整 */ @media (max-width: 767px) { .chart-modal-content { width: 95%; padding: 15px; height: 90vh; } .chart-modal .chart-wrapper { height: 60vh; } } .month-navigation { display: flex; align-items: center; gap: 15px; } #currentMonth { font-size: 18px; font-weight: bold; } .month-stats { display: flex; flex-direction: column; align-items: flex-end; } .trading-day { background: rgba(46, 204, 113, 0.1); border-left: 3px solid #2ecc71; } .negative { background: rgba(231, 76, 60, 0.1); border-left: 3px solid #e74c3c; } .trade-info { margin-top: 5px; font-size: 0.9em; color: #555; } .week-summary { min-height: 100px; padding: 10px; background: #eaeaea; border-radius: 4px; display: flex; flex-direction: column; justify-content: center; align-items: center; font-weight: bold; text-align: center; } /* 添加weekly统计的正负数颜色 */ .week-summary .positive { color: #2ecc71; } .week-summary .negative { color: #e74c3c; } /* 日期选择器样式 */ .date-picker-container { position: relative; } .date-range-picker { display: none; position: absolute; top: 100%; left: 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 100; min-width: 300px; } .date-range-picker.active { display: block; } .date-picker-group { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } .date-picker-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } /* Go按钮样式 */ .go-button { background: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; } .go-button:hover { background: #2980b9; } .preset-dates { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; } .preset-dates button { width: 100%; justify-content: center; } /* 符号过滤器样式 */ .symbol-filter-container { position: relative; margin-left: 10px; } .combobox-wrapper { display: flex; align-items: center; background: white; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } .symbol-input { padding: 8px; border: none; outline: none; width: 150px; font-size: 14px; } .dropdown-toggle { padding: 8px; background: none; border: none; border-left: 1px solid #ddd; cursor: pointer; } .dropdown-toggle:hover { background: #f5f5f5; } .symbol-dropdown { display: none; position: absolute; top: 100%; left: 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 100; min-width: 300px; } .symbol-dropdown.active { display: block; } .symbol-option { padding: 8px; cursor: pointer; } .symbol-option:hover { background: #f5f5f5; } /* 弹框样式 - 默认隐藏 */ #tradeModal, #importModal, #r2ConfigModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色背景 */ z-index: 1000; justify-content: center; align-items: center; } .modal-content, .trade-modal-content, .import-modal-content, .r2-config-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; margin: auto; /* 确保居中 */ margin-top: 100px; width: 100%; max-width: 800px; /* 限制最大宽度 */ } .import-form { display: flex; flex-direction: column; gap: 20px; } .form-group { display: flex; flex-direction: column; gap: 8px; } .form-group label { color: #666; } .form-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .broker-select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white; } .connect-button { padding: 12px; background: #5D5FEF; color: white; border: none; border-radius: 4px; cursor: pointer; } /* 当弹框显示时的样式 */ #tradeModal.show, #importModal.show, #r2ConfigModal.show { display: flex; } /* 移动设备上的日期选择器 */ @media (max-width: 767px) { .date-range-picker { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 350px; } .date-picker-group { flex-direction: column; align-items: flex-start; } .date-picker-group input { width: 100%; } } /* 通配符选项样式 */ .wildcard-option { background-color: #f0f8ff; /* 浅蓝色背景 */ font-weight: 500; } .wildcard-hint { font-size: 0.8em; color: #666; margin-left: 5px; } /* 日志模态框使用完全居中的弹窗样式 */ #logModal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); z-index: 2000; } #logModal.hidden { display: none; } /* 日志弹窗内容样式 - 完全居中无偏移 */ #logModal .modal-content.log-modal { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; margin: 0; /* 移除上边距,完全居中 */ width: 100%; max-width: 600px; /* 适合日志表单的宽度 */ } /* 日志弹窗内部元素微调 */ #logModal .close { position: absolute; top: 10px; right: 14px; font-size: 22px; color: #666; cursor: pointer; } #logModal .close:hover { color: #333; } #logModal h2 { margin: 0 0 16px 0; font-size: 18px; } #logModal .form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } #logModal .form-row.two-cols { flex-direction: row; gap: 12px; } #logModal .form-row.two-cols > div { flex: 1; } #logModal .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; } #logModal .form-actions .primary { background: #1976d2; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; } #logModal .form-actions .primary:hover { background: #145ca7; } #logModal .form-actions .danger { background: #f44336; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; } #logModal .form-actions .danger:hover { background: #d32f2f; } .log-modal h2 { margin: 0 0 20px 0; } .log-modal .form-group { margin-bottom: 16px; } .log-modal label { display: block; font-weight: 600; color: #333; margin-bottom: 6px; } .log-modal input[type="date"], .log-modal select, .log-modal textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; } .log-modal textarea { min-height: 80px; resize: vertical; } /* Log button on calendar day */ .log-btn { position: absolute; right: 6px; top: 6px; width: 22px; height: 22px; border: none; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; cursor: pointer; } .log-btn:hover { background: #f3f3f3; } .log-btn svg { width: 14px; height: 14px; color: #1976d2; } /* Utility */ .badge { display: inline-block; font-size: 12px; padding: 3px 8px; border-radius: 999px; background: #eee; color: #333; } .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #333; color: #fff; padding: 10px 14px; border-radius: 6px; opacity: 0.95; display: inline-block; z-index: 10000; } .close-button { background: transparent; color: #333; font-size: 24px; position: absolute; right: 10px; top: 5px; padding: 0; width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; } .close-button:hover { background: rgba(0,0,0,0.1); } /* 工具栏样式 */ .toolbar { display: flex; justify-content: space-between; align-items: center; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; flex-wrap: wrap; gap: 10px; } .toolbar-left, .toolbar-right { display: flex; gap: 10px; flex-wrap: wrap; } @media (max-width: 767px) { .toolbar { flex-direction: column; align-items: flex-start; } .toolbar-right { margin-top: 10px; width: 100%; justify-content: space-between; } button { padding: 6px 12px; font-size: 12px; } .stats-cards { grid-template-columns: repeat(2, 1fr); } .stat-card { padding: 10px; } .stat-header { font-size: 12px; } .stat-value { font-size: 18px; } } @media (max-width: 480px) { .stats-cards { grid-template-columns: 1fr; } } /* 日历样式 */ #calendar { display: grid; grid-template-columns: repeat(8, 1fr); gap: 10px; margin-top: 20px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .calendar-header { font-weight: bold; text-align: center; padding: 10px; background: #f8f9fa; border-radius: 4px; } .calendar-day { position: relative; } .log-button { position: absolute; top: 6px; right: 6px; background: transparent; border: none; padding: 10px; line-height: 1; border-radius: 6px; cursor: pointer; } .log-button:hover { background: rgba(0,0,0,0.06); } .log-button.has-log { color: #1e80ff; } .calendar-day { min-height: 100px; padding: 10px; background: #f8f9fa; border-radius: 4px; position: relative; cursor: pointer; transition: transform 0.2s; } .calendar-day:hover { transform: scale(1.02); } /* 日历头部样式 */ .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .chart-header h3 { margin: 0; font-size: 16px; color: #333; } /* 图表全屏模态框样式 */ .chart-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; } .chart-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); width: 90%; max-width: 1200px; height: 80vh; overflow-y: auto; position: relative; } .chart-modal .close-button { position: absolute; top: 10px; right: 10px; background: transparent; border: none; font-size: 24px; cursor: pointer; color: #333; } .chart-modal .chart-wrapper { width: 100%; height: 65vh; position: relative; } .chart-modal canvas { width: 100% !important; height: 100% !important; max-height: none !important; } /* 响应式调整 */ @media (max-width: 767px) { .chart-modal-content { width: 95%; padding: 15px; height: 90vh; } .chart-modal .chart-wrapper { height: 60vh; } } .month-navigation { display: flex; align-items: center; gap: 15px; } #currentMonth { font-size: 18px; font-weight: bold; } .month-stats { display: flex; flex-direction: column; align-items: flex-end; } .trading-day { background: rgba(46, 204, 113, 0.1); border-left: 3px solid #2ecc71; } .negative { background: rgba(231, 76, 60, 0.1); border-left: 3px solid #e74c3c; } .trade-info { margin-top: 5px; font-size: 0.9em; color: #555; } .week-summary { min-height: 100px; padding: 10px; background: #eaeaea; border-radius: 4px; display: flex; flex-direction: column; justify-content: center; align-items: center; font-weight: bold; text-align: center; } /* 添加weekly统计的正负数颜色 */ .week-summary .positive { color: #2ecc71; } .week-summary .negative { color: #e74c3c; } /* 日期选择器样式 */ .date-picker-container { position: relative; } .date-range-picker { display: none; position: absolute; top: 100%; left: 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 100; min-width: 300px; } .date-range-picker.active { display: block; } .date-picker-group { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } .date-picker-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } /* Go按钮样式 */ .go-button { background: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; } .go-button:hover { background: #2980b9; } .preset-dates { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; } .preset-dates button { width: 100%; justify-content: center; } /* 符号过滤器样式 */ .symbol-filter-container { position: relative; margin-left: 10px; } .combobox-wrapper { display: flex; align-items: center; background: white; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } .symbol-input { padding: 8px; border: none; outline: none; width: 150px; font-size: 14px; } .dropdown-toggle { padding: 8px; background: none; border: none; border-left: 1px solid #ddd; cursor: pointer; } .dropdown-toggle:hover { background: #f5f5f5; } .symbol-dropdown { display: none; position: absolute; top: 100%; left: 0; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 100; min-width: 300px; } .symbol-dropdown.active { display: block; } .symbol-option { padding: 8px; cursor: pointer; } .symbol-option:hover { background: #f5f5f5; } /* 弹框样式 - 默认隐藏 */ #tradeModal, #importModal, #r2ConfigModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色背景 */ z-index: 1000; justify-content: center; align-items: center; } .modal-content, .trade-modal-content, .import-modal-content, .r2-config-modal-content { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; margin: auto; /* 确保居中 */ margin-top: 100px; width: 100%; max-width: 800px; /* 限制最大宽度 */ } .import-form { display: flex; flex-direction: column; gap: 20px; } .form-group { display: flex; flex-direction: column; gap: 8px; } .form-group label { color: #666; } .form-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .broker-select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white; } .connect-button { padding: 12px; background: #5D5FEF; color: white; border: none; border-radius: 4px; cursor: pointer; } /* 当弹框显示时的样式 */ #tradeModal.show, #importModal.show, #r2ConfigModal.show { display: flex; } /* 移动设备上的日期选择器 */ @media (max-width: 767px) { .date-range-picker { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 350px; } .date-picker-group { flex-direction: column; align-items: flex-start; } .date-picker-group input { width: 100%; } } /* 通配符选项样式 */ .wildcard-option { background-color: #f0f8ff; /* 浅蓝色背景 */ font-weight: 500; } .wildcard-hint { font-size: 0.8em; color: #666; margin-left: 5px; } /* 日志模态框使用完全居中的弹窗样式 */ #logModal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); z-index: 2000; } #logModal.hidden { display: none; } /* 日志弹窗内容样式 - 完全居中无偏移 */ #logModal .modal-content.log-modal { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); max-width: 90%; max-height: 90%; overflow-y: auto; position: relative; margin: 0; /* 移除上边距,完全居中 */ width: 100%; max-width: 600px; /* 适合日志表单的宽度 */ } /* 日志弹窗内部元素微调 */ #logModal .close { position: absolute; top: 10px; right: 14px; font-size: 22px; color: #666; cursor: pointer; } #logModal .close:hover { color: #333; } #logModal h2 { margin: 0 0 16px 0; font-size: 18px; } #logModal .form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } #logModal .form-row.two-cols { flex-direction: row; gap: 12px; } #logModal .form-row.two-cols > div { flex: 1; } #logModal .form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; } #logModal .form-actions .primary { background: #1976d2; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; } #logModal .form-actions .primary:hover { background: #145ca7; } #logModal .form-actions .danger { background: #f44336; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; } #logModal .form-actions .danger:hover { background: #d32f2f; } .log-modal h2 { margin: 0 0 20px 0; } .log-modal .form-group { margin-bottom: 16px; } .log-modal label { display: block; font-weight: 600; color: #333; margin-bottom: 6px; } .log-modal input[type="date"], .log-modal select, .log-modal textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; } .log-modal textarea { min-height: 80px; resize: vertical; } /* Log button on calendar day */ .log-btn { position: absolute; right: 6px; top: 6px; width: 22px; height: 22px; border: none; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; cursor: pointer; } .log-btn:hover { background: #f3f3f3; } .log-btn svg { width: 14px; height: 14px; color: #1976d2; } /* Utility: hidden helper */ .hidden { display: none !important; } /* Logs: sidebar, list, buttons (restored) */ .log-sidebar-btn svg { color: #333; } .log-sidebar { position: fixed; left: -420px; top: 0; width: 380px; height: 100vh; background: #fff; box-shadow: 2px 0 10px rgba(0,0,0,0.1); z-index: 999; transition: left 0.3s ease; overflow-y: auto; display: flex; flex-direction: column; } .log-sidebar.active { left: 0; } .log-sidebar-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #eee; background: #f8f9fa; } .log-sidebar-header h3 { margin: 0; color: #333; font-size: 18px; } .log-list { flex: 1; padding: 20px; overflow-y: auto; } .log-item { border: 1px solid #eee; border-radius: 8px; margin-bottom: 15px; overflow: hidden; transition: box-shadow 0.2s; background: #fff; } .log-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .log-item-header { padding: 15px; cursor: pointer; background: #f8f9fa; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; } .log-item-date { font-weight: 600; color: #333; font-size: 16px; } .log-item-type { font-size: 12px; padding: 4px 8px; border-radius: 12px; background: #e3f2fd; color: #1976d2; } .log-item-type.weekly { background: #f3e5f5; color: #7b1fa2; } .log-item-preview { padding: 15px; color: #666; font-size: 14px; line-height: 1.4; max-height: 60px; overflow: hidden; } .log-item-content { display: none; padding: 15px; border-top: 1px solid #eee; background: #fff; } .log-item.expanded .log-item-content { display: block; } .log-item.expanded .log-item-header { background: #e8f5e8; } .log-content-section { margin-bottom: 15px; } .log-content-section h5 { margin: 0 0 5px 0; color: #333; font-size: 14px; font-weight: 600; } .log-content-text { color: #666; font-size: 14px; line-height: 1.4; margin-bottom: 10px; } .log-quick-review { display: flex; gap: 15px; margin-bottom: 10px; } .log-quick-item { display: flex; flex-direction: column; gap: 2px; } .log-quick-label { font-size: 12px; color: #666; } .log-quick-value { font-weight: 600; color: #333; } .log-loading { text-align: center; padding: 20px; color: #666; } .cancel-button { background: #95a5a6; } .cancel-button:hover { background: #7f8c8d; } .details-button { background: #3498db; } .details-button:hover { background: #2980b9; } .trade-modal-content { position: relative; } .modal-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; } .modal-header .date-nav { display: flex; align-items: center; gap: 5px; } .modal-date { font-size: 24px; font-weight: 600; color: #333; } .modal-pnl { font-size: 20px; } .profit { color: #2e7d32; } .fail { color: #c62828; } .trade-stats { margin: 10px 0 20px; } .trade-stats .stats-row { display: flex; flex-wrap: nowrap; gap: 15px; align-items: flex-end; } .trade-stats .stat-label { color: #666; margin-right: 4px; } .trade-stats .stat-value { font-weight: 600; color: #333; } .table-wrapper { margin-top: 10px; } .table-controls { display: flex; justify-content: flex-end; margin-bottom: 5px; } .trades-table { width: 100%; border-collapse: collapse; border: 1px solid #ddd; } .trades-table th, .trades-table td { border: 1px solid #ddd; padding: 8px; text-align: left; } /* Removed .trade-nav since navigation arrows moved to header */ .nav-arrow { background: transparent; border: none; font-size: 24px; cursor: pointer; } .close-button { background: transparent; color: #333; font-size: 24px; position: absolute; right: 10px; top: 5px; padding: 0; width: 30px; height: 30px; display: flex; justify-content: center; align-items: center; } .close-button:hover { background: rgba(0,0,0,0.1); } /* 工具栏样式 */ .toolbar { display: flex; justify-content: space-between; align-items: center; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; flex-wrap: wrap; gap: 10px; } .toolbar-left, .toolbar-right { display: flex; gap: 10px; flex-wrap: wrap; } @media (max-width: 767px) { .toolbar { flex-direction: column; align-items: flex-start; } .toolbar-right { margin-top: 10px; width: 100%; justify-content: space-between; } button { padding: 6px 12px; font-size: 12px; } .stats-cards { grid-template-columns: repeat(2, 1fr); } .stat-card { padding: 10px; } .stat-header { font-size: 12px; } .stat-value { font-size: 18px; } } @media (max-width: 480px) { .stats-cards { grid-template-columns: 1fr; } } /* 日历样式 */ #calendar { display: grid; grid-template-columns: repeat(8, 1fr); gap: 10px; margin-top: 20px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .calendar-header { font-weight: bold; text-align: center; padding: 10px; background: #f8f9fa; border-radius: 4px; } .calendar-day { position: relative; } .log-button { position: absolute; top: 6px; right: 6px; background: transparent; border: none; padding: 10px; line-height: 1; border-radius: 6px; cursor: pointer; } .log-button:hover { background: rgba(0,0,0,0.06); } .log-button.has-log { color: #1e80ff; } .calendar-day { min-height: 100px; padding: 10px; background: #f8f9fa; border-radius: 4px; position: relative; cursor: pointer; transition: transform 0.2s; } .calendar-day:hover { transform: scale(1.02); } addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { // 处理 CORS 预检请求 if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Accept', 'Access-Control-Max-Age': '86400', }, }); } // 只处理 POST 请求 if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); } try { // 解析客户端请求的数据 const requestData = await request.json(); const targetUrl = requestData.url; if (!targetUrl) { return new Response('Target URL is required', { status: 400 }); } // 定义请求头 const headers = new Headers({ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7,es;q=0.6,mn;q=0.5,ms;q=0.4,ko;q=0.3,la;q=0.2,th;q=0.1,is;q=0.1,ar;q=0.1,hmn;q=0.1,de;q=0.1,xh;q=0.1,eo;q=0.1,mi;q=0.1,pt;q=0.1,kk;q=0.1,bg;q=0.1,jv;q=0.1,zh-TW;q=0.1,nl;q=0.1', 'cache-control': 'max-age=0', 'cookie': requestData.cookie || '', 'dnt': '1', 'priority': 'u=0, i', 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', }); // 向目标 URL 发起请求 const response = await fetch(targetUrl, { method: 'GET', // 按 curl 的方式使用 GET 请求 headers, }); // 读取目标 URL 响应内容 const responseData = await response.text(); // 返回代理响应 return new Response(responseData, { headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': response.headers.get('Content-Type') || 'text/plain', }, status: response.status, }); } catch (error) { return new Response(`Proxy error: ${error.message}`, { status: 500, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain', }, }); } }