diff --git a/app.py b/app.py index 0155083..f105799 100644 --- a/app.py +++ b/app.py @@ -1620,6 +1620,12 @@ def stats(): return render_template("stats.html") +@app.route("/calendar") +@login_required +def trade_calendar(): + return render_template("calendar.html") + + @app.route("/api/stats") @login_required def api_stats(): diff --git a/static/js/calendar.js b/static/js/calendar.js new file mode 100644 index 0000000..f07a95f --- /dev/null +++ b/static/js/calendar.js @@ -0,0 +1,318 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 交易日历 — 按日汇总平仓与复盘 + */ +(function () { + var calYear = 0; + var calMonth = 0; + var selectedDate = ''; + + function pad2(n) { + return n < 10 ? '0' + n : String(n); + } + + function todayIso() { + var d = new Date(); + return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); + } + + function fmtNum(v) { + if (v === null || v === undefined || v === '') return '-'; + var n = Number(v); + if (isNaN(n)) return String(v); + return Number.isInteger(n) ? String(n) : n.toFixed(2); + } + + function fmtMoney(v) { + if (v === null || v === undefined) return '-'; + return fmtNum(v) + ' 元'; + } + + function pnlClass(v) { + if (v > 0) return 'is-profit'; + if (v < 0) return 'is-loss'; + return 'is-flat'; + } + + function fmtPnlShort(v) { + if (v === null || v === undefined) return '-'; + var n = Number(v); + if (isNaN(n)) return '-'; + var s = Number.isInteger(n) ? String(n) : n.toFixed(0); + return (n > 0 ? '+' : '') + s; + } + + function fmtTime(v) { + if (!v) return '-'; + return String(v).replace('T', ' ').slice(0, 16); + } + + function fmtTags(item) { + var tags = item.behavior_tags || ''; + if (item.is_emotion) { + return tags ? '情绪单 · ' + tags : '情绪单'; + } + return tags || ''; + } + + function setCalendarTitle() { + var title = document.getElementById('trade-cal-title'); + if (!title) return; + if (window.qihuoLunar && qihuoLunar.monthTitle) { + title.textContent = qihuoLunar.monthTitle(calYear, calMonth); + } else { + title.textContent = '公历 ' + calYear + '年' + calMonth + '月'; + } + } + + function lunarCellHtml(iso) { + if (!window.qihuoLunar) return ''; + var text = qihuoLunar.cellLunarText(iso); + var info = qihuoLunar.fromIso(iso); + var cls = 'trade-cal-day-lunar'; + if (info.lunarDay === 1) cls += ' is-month-start'; + return '' + text + ''; + } + + function renderCalendar(data) { + var grid = document.getElementById('trade-cal-grid'); + if (!grid) return; + setCalendarTitle(); + var html = ''; + var pad = data.weekday_start || 0; + var i; + for (i = 0; i < pad; i++) { + html += '
'; + } + (data.days || []).forEach(function (day) { + var dayNum = day.date.slice(8, 10).replace(/^0/, ''); + var classes = ['trade-cal-cell']; + if (day.count > 0) classes.push('is-clickable'); + if (day.date === todayIso()) classes.push('is-today'); + if (day.date === selectedDate) classes.push('is-selected'); + if (day.has_emotion) classes.push('is-emotion'); + html += '
'; + html += '
'; + html += '' + dayNum + ''; + html += lunarCellHtml(day.date); + html += '
'; + if (day.count > 0) { + html += '
' + day.count + ' 笔
'; + html += '
' + fmtPnlShort(day.total_net) + '
'; + if (day.has_emotion) { + html += '情绪' + (day.emotion_count > 1 ? '×' + day.emotion_count : '') + ''; + } + html += '
'; + } + html += '
'; + }); + grid.innerHTML = html; + } + + function loadCalendar() { + fetch('/api/stats/calendar?year=' + calYear + '&month=' + calMonth, { credentials: 'same-origin' }) + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (data) { + renderCalendar(data); + if (selectedDate && selectedDate.slice(0, 7) === calYear + '-' + pad2(calMonth)) { + loadDayDetail(selectedDate, false); + } else { + hideDayDetail(); + } + }) + .catch(function () { + var grid = document.getElementById('trade-cal-grid'); + if (grid) grid.innerHTML = '
日历加载失败
'; + }); + } + + function hideDayDetail() { + var panel = document.getElementById('trade-cal-day-detail'); + if (panel) panel.hidden = true; + } + + function renderDayDetail(data) { + var panel = document.getElementById('trade-cal-day-detail'); + var title = document.getElementById('trade-cal-day-detail-title'); + var summary = document.getElementById('trade-cal-day-summary'); + var list = document.getElementById('trade-cal-day-list'); + if (!panel || !list) return; + + var label = data.date.replace(/-/g, '/'); + var lunarPart = window.qihuoLunar ? '(' + qihuoLunar.daySubtitle(data.date) + ')' : ''; + if (title) title.textContent = label + lunarPart + ' 交易记录'; + if (summary) { + var parts = [data.count + ' 笔', '净盈亏 ' + fmtMoney(data.total_net)]; + if (data.emotion_count) parts.push('情绪单 ' + data.emotion_count); + summary.textContent = parts.join(' · '); + } + + list.innerHTML = ''; + if (!data.items || !data.items.length) { + list.innerHTML = '
当日无平仓记录
'; + panel.hidden = false; + return; + } + + data.items.forEach(function (item) { + var card = document.createElement('div'); + card.className = 'trade-cal-day-item' + (item.is_emotion ? ' is-emotion' : ''); + + var head = document.createElement('div'); + head.className = 'trade-cal-day-item-head'; + var sym = document.createElement('div'); + sym.className = 'trade-cal-day-item-symbol'; + sym.textContent = (item.symbol || item.symbol_code || '-') + ' · ' + (item.direction || '-'); + var pnl = document.createElement('div'); + pnl.className = 'trade-cal-day-item-pnl ' + pnlClass(item.pnl_net); + pnl.textContent = fmtMoney(item.pnl_net); + head.appendChild(sym); + head.appendChild(pnl); + + var meta = document.createElement('div'); + meta.className = 'trade-cal-day-item-meta'; + var badges = ''; + if (item.source === 'review') { + badges += '复盘'; + } + if (item.is_emotion) { + badges += '情绪单'; + } + var metaParts = [ + badges, + '平仓 ' + fmtTime(item.close_time), + item.lots != null ? item.lots + ' 手' : '', + item.entry_price != null ? '开 ' + item.entry_price : '', + item.close_price != null ? '平 ' + item.close_price : '', + ]; + if (item.source === 'review' && item.open_type) metaParts.push(item.open_type); + if (item.source === 'review' && item.exit_trigger) metaParts.push('出场: ' + item.exit_trigger); + if (item.result) metaParts.push(item.result); + meta.innerHTML = metaParts.filter(Boolean).join(' · '); + + card.appendChild(head); + card.appendChild(meta); + + var tags = fmtTags(item); + if (tags) { + var tagEl = document.createElement('div'); + tagEl.className = 'trade-cal-day-item-notes'; + tagEl.textContent = tags; + card.appendChild(tagEl); + } + if (item.notes) { + var notes = document.createElement('div'); + notes.className = 'trade-cal-day-item-notes'; + notes.textContent = item.notes; + card.appendChild(notes); + } + if (item.screenshot) { + var shot = document.createElement('div'); + shot.className = 'trade-cal-day-item-shot'; + shot.innerHTML = '复盘截图'; + card.appendChild(shot); + } + list.appendChild(card); + }); + panel.hidden = false; + } + + function loadDayDetail(dateStr, scroll) { + if (scroll === undefined) scroll = true; + selectedDate = dateStr; + document.querySelectorAll('.trade-cal-cell.is-selected').forEach(function (el) { + el.classList.remove('is-selected'); + }); + var cell = document.querySelector('.trade-cal-cell[data-date="' + dateStr + '"]'); + if (cell) cell.classList.add('is-selected'); + + fetch('/api/stats/calendar/day?date=' + encodeURIComponent(dateStr), { credentials: 'same-origin' }) + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (data) { + renderDayDetail(data); + if (scroll) { + var panel = document.getElementById('trade-cal-day-detail'); + if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }) + .catch(function () { + var panel = document.getElementById('trade-cal-day-detail'); + var list = document.getElementById('trade-cal-day-list'); + if (panel && list) { + list.innerHTML = '
加载失败
'; + panel.hidden = false; + } + }); + } + + function shiftCalendarMonth(delta) { + calMonth += delta; + if (calMonth > 12) { + calMonth = 1; + calYear += 1; + } else if (calMonth < 1) { + calMonth = 12; + calYear -= 1; + } + loadCalendar(); + } + + function bindCalendar() { + var grid = document.getElementById('trade-cal-grid'); + if (!grid || grid.dataset.tradeCalBound) return; + grid.dataset.tradeCalBound = '1'; + + grid.addEventListener('click', function (e) { + var cell = e.target.closest('.trade-cal-cell.is-clickable'); + if (!cell) return; + loadDayDetail(cell.getAttribute('data-date')); + }); + grid.addEventListener('keydown', function (e) { + if (e.key !== 'Enter' && e.key !== ' ') return; + var cell = e.target.closest('.trade-cal-cell.is-clickable'); + if (!cell) return; + e.preventDefault(); + loadDayDetail(cell.getAttribute('data-date')); + }); + + var prev = document.getElementById('trade-cal-prev'); + var next = document.getElementById('trade-cal-next'); + var todayBtn = document.getElementById('trade-cal-today'); + if (prev) prev.addEventListener('click', function () { shiftCalendarMonth(-1); }); + if (next) next.addEventListener('click', function () { shiftCalendarMonth(1); }); + if (todayBtn) todayBtn.addEventListener('click', function () { + var d = new Date(); + calYear = d.getFullYear(); + calMonth = d.getMonth() + 1; + loadCalendar(); + }); + } + + function bootCalendarPage() { + if (!document.getElementById('trade-cal-grid')) return; + var d = new Date(); + calYear = d.getFullYear(); + calMonth = d.getMonth() + 1; + bindCalendar(); + loadCalendar(); + + var params = new URLSearchParams(window.location.search); + var dateParam = params.get('date'); + if (dateParam && /^\d{4}-\d{2}-\d{2}$/.test(dateParam)) { + var parts = dateParam.split('-'); + calYear = parseInt(parts[0], 10); + calMonth = parseInt(parts[1], 10); + loadCalendar(); + setTimeout(function () { loadDayDetail(dateParam); }, 300); + } + } + + if (window.qihuoPageBoot) window.qihuoPageBoot(bootCalendarPage, '#trade-cal-grid'); + else if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootCalendarPage); + else bootCalendarPage(); +})(); diff --git a/static/js/lunar.js b/static/js/lunar.js new file mode 100644 index 0000000..c13568b --- /dev/null +++ b/static/js/lunar.js @@ -0,0 +1,195 @@ +/* Copyright (c) 2025-2026 马建军. All rights reserved. + * 公历 ↔ 农历(1900–2100,与国务院公布历法一致的数据表) + */ +(function (global) { + var LUNAR_INFO = [ + 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, + 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573, 0x052d0, 0x0a9a8, 0x0e950, 0x06aa0, + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6, + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, + 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, + 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06aa0, 0x1a6c4, 0x0aae0, + 0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, + 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, + 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, + 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, + 0x0d520, + ]; + + var TIAN_GAN = '甲乙丙丁戊己庚辛壬癸'; + var DI_ZHI = '子丑寅卯辰巳午未申酉戌亥'; + var LUNAR_MONTHS = '正二三四五六七八九十冬腊'; + var LUNAR_DAYS = [ + '初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十', + '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十', + '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十', + ]; + + function leapMonth(y) { + return LUNAR_INFO[y - 1900] & 0xf; + } + + function leapDays(y) { + if (leapMonth(y)) { + return (LUNAR_INFO[y - 1900] & 0x10000) ? 30 : 29; + } + return 0; + } + + function monthDays(y, m) { + return (LUNAR_INFO[y - 1900] & (0x10000 >> m)) ? 30 : 29; + } + + function lYearDays(y) { + var sum = 348; + var i; + for (i = 0x8000; i > 0x8; i >>= 1) { + sum += (LUNAR_INFO[y - 1900] & i) ? 1 : 0; + } + return sum + leapDays(y); + } + + function solarToDate(y, m, d) { + return new Date(y, m - 1, d); + } + + function solarToLunar(y, m, d) { + var base = new Date(1900, 0, 31); + var obj = solarToDate(y, m, d); + var offset = Math.floor((obj - base) / 86400000); + var i; + var temp = 0; + var lunarYear = 1900; + + for (i = 1900; i < 2101 && offset > 0; i++) { + temp = lYearDays(i); + offset -= temp; + lunarYear++; + } + if (offset < 0) { + offset += temp; + lunarYear--; + } + + var leap = leapMonth(lunarYear); + var isLeap = false; + var lunarMonth = 1; + + for (i = 1; i < 13 && offset > 0; i++) { + if (leap > 0 && i === leap + 1 && !isLeap) { + i--; + isLeap = true; + temp = leapDays(lunarYear); + } else { + temp = monthDays(lunarYear, i); + } + if (isLeap && i === leap + 1) isLeap = false; + offset -= temp; + if (!isLeap) lunarMonth++; + } + if (offset === 0 && leap > 0 && lunarMonth === leap + 1) { + if (isLeap) { + isLeap = false; + } else { + isLeap = true; + lunarMonth--; + } + } + if (offset < 0) { + offset += temp; + lunarMonth--; + } + + return { + year: lunarYear, + month: lunarMonth, + day: offset + 1, + isLeap: isLeap, + }; + } + + function lunarMonthName(month, isLeap) { + var idx = month - 1; + if (idx < 0 || idx >= LUNAR_MONTHS.length) return String(month) + '月'; + var name = LUNAR_MONTHS.charAt(idx); + if (name === '冬') name = '十一'; + if (name === '腊') name = '十二'; + return (isLeap ? '闰' : '') + name + '月'; + } + + function lunarDayName(day) { + return LUNAR_DAYS[day - 1] || String(day); + } + + function ganZhiYear(y) { + return TIAN_GAN.charAt((y - 4) % 10) + DI_ZHI.charAt((y - 4) % 12); + } + + function parseIso(iso) { + var p = iso.split('-'); + return { y: parseInt(p[0], 10), m: parseInt(p[1], 10), d: parseInt(p[2], 10) }; + } + + function fromIso(iso) { + var p = parseIso(iso); + var lunar = solarToLunar(p.y, p.m, p.d); + return { + solar: iso, + lunarYear: lunar.year, + lunarMonth: lunar.month, + lunarDay: lunar.day, + isLeap: lunar.isLeap, + lunarMonthText: lunarMonthName(lunar.month, lunar.isLeap), + lunarDayText: lunarDayName(lunar.day), + lunarFull: lunarMonthName(lunar.month, lunar.isLeap) + lunarDayName(lunar.day), + ganZhi: ganZhiYear(lunar.year), + }; + } + + /** 单元格内农历:初一显示月份,其余显示日名 */ + function cellLunarText(iso) { + var info = fromIso(iso); + if (info.lunarDay === 1) { + return info.lunarMonthText; + } + return info.lunarDayText; + } + + /** 标题:公历 YYYY年M月 · 农历干支年M月 */ + function monthTitle(solarYear, solarMonth) { + var mid = fromIso(solarYear + '-' + pad(solarMonth) + '-15'); + return ( + '公历 ' + solarYear + '年' + solarMonth + '月 · 农历 ' + + mid.ganZhi + '年' + mid.lunarMonthText + ); + } + + function pad(n) { + return n < 10 ? '0' + n : String(n); + } + + /** 详情标题后缀 */ + function daySubtitle(iso) { + var info = fromIso(iso); + return '农历' + info.ganZhi + '年' + info.lunarFull; + } + + global.qihuoLunar = { + fromIso: fromIso, + cellLunarText: cellLunarText, + monthTitle: monthTitle, + daySubtitle: daySubtitle, + lunarMonthName: lunarMonthName, + lunarDayName: lunarDayName, + }; +}(typeof window !== 'undefined' ? window : this)); diff --git a/static/js/stats.js b/static/js/stats.js index ce08876..be1cc45 100644 --- a/static/js/stats.js +++ b/static/js/stats.js @@ -150,278 +150,6 @@ }); } - var calYear = 0; - var calMonth = 0; - var calDays = []; - var selectedDate = ''; - - function pad2(n) { - return n < 10 ? '0' + n : String(n); - } - - function todayIso() { - var d = new Date(); - return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); - } - - function pnlClass(v) { - if (v > 0) return 'is-profit'; - if (v < 0) return 'is-loss'; - return 'is-flat'; - } - - function fmtPnlShort(v) { - if (v === null || v === undefined) return '-'; - var n = Number(v); - if (isNaN(n)) return '-'; - var s = Number.isInteger(n) ? String(n) : n.toFixed(0); - return (n > 0 ? '+' : '') + s; - } - - function fmtTime(v) { - if (!v) return '-'; - return String(v).replace('T', ' ').slice(0, 16); - } - - function fmtTags(item) { - var tags = item.behavior_tags || ''; - if (item.is_emotion) { - return tags ? '情绪单 · ' + tags : '情绪单'; - } - return tags || ''; - } - - function setCalendarTitle() { - var title = document.getElementById('stats-cal-title'); - if (title) title.textContent = calYear + '年' + calMonth + '月'; - } - - function renderCalendar(data) { - var grid = document.getElementById('stats-calendar-grid'); - if (!grid) return; - calDays = data.days || []; - setCalendarTitle(); - var html = ''; - var pad = data.weekday_start || 0; - var i; - for (i = 0; i < pad; i++) { - html += '
'; - } - calDays.forEach(function (day) { - var dayNum = day.date.slice(8, 10).replace(/^0/, ''); - var classes = ['stats-cal-cell']; - if (day.count > 0) classes.push('is-clickable'); - if (day.date === todayIso()) classes.push('is-today'); - if (day.date === selectedDate) classes.push('is-selected'); - if (day.has_emotion) classes.push('is-emotion'); - html += '
'; - html += '
' + dayNum + '
'; - if (day.count > 0) { - html += '
' + day.count + ' 笔
'; - html += '
' + fmtPnlShort(day.total_net) + '
'; - if (day.has_emotion) { - html += '情绪' + (day.emotion_count > 1 ? '×' + day.emotion_count : '') + ''; - } - html += '
'; - } - html += '
'; - }); - grid.innerHTML = html; - } - - function loadCalendar() { - fetch('/api/stats/calendar?year=' + calYear + '&month=' + calMonth, { credentials: 'same-origin' }) - .then(function (r) { - if (!r.ok) throw new Error('HTTP ' + r.status); - return r.json(); - }) - .then(function (data) { - renderCalendar(data); - if (selectedDate && selectedDate.slice(0, 7) === calYear + '-' + pad2(calMonth)) { - loadDayDetail(selectedDate, false); - } else { - hideDayDetail(); - } - }) - .catch(function () { - var grid = document.getElementById('stats-calendar-grid'); - if (grid) grid.innerHTML = '
日历加载失败
'; - }); - } - - function hideDayDetail() { - var panel = document.getElementById('stats-day-detail'); - if (panel) panel.hidden = true; - } - - function renderDayDetail(data) { - var panel = document.getElementById('stats-day-detail'); - var title = document.getElementById('stats-day-detail-title'); - var summary = document.getElementById('stats-day-summary'); - var list = document.getElementById('stats-day-list'); - if (!panel || !list) return; - - var label = data.date.replace(/-/g, '/'); - if (title) title.textContent = label + ' 交易记录'; - if (summary) { - var parts = [data.count + ' 笔', '净盈亏 ' + fmtMoney(data.total_net)]; - if (data.emotion_count) parts.push('情绪单 ' + data.emotion_count); - summary.textContent = parts.join(' · '); - } - - list.innerHTML = ''; - if (!data.items || !data.items.length) { - list.innerHTML = '
当日无平仓记录
'; - panel.hidden = false; - return; - } - - data.items.forEach(function (item) { - var card = document.createElement('div'); - card.className = 'stats-day-item' + (item.is_emotion ? ' is-emotion' : ''); - - var head = document.createElement('div'); - head.className = 'stats-day-item-head'; - var sym = document.createElement('div'); - sym.className = 'stats-day-item-symbol'; - sym.textContent = (item.symbol || item.symbol_code || '-') + ' · ' + (item.direction || '-'); - var pnl = document.createElement('div'); - pnl.className = 'stats-day-item-pnl ' + pnlClass(item.pnl_net); - pnl.textContent = fmtMoney(item.pnl_net); - head.appendChild(sym); - head.appendChild(pnl); - - var meta = document.createElement('div'); - meta.className = 'stats-day-item-meta'; - var badges = ''; - if (item.source === 'review') { - badges += '复盘'; - } - if (item.is_emotion) { - badges += '情绪单'; - } - var metaParts = [ - badges, - '平仓 ' + fmtTime(item.close_time), - item.lots != null ? item.lots + ' 手' : '', - item.entry_price != null ? '开 ' + item.entry_price : '', - item.close_price != null ? '平 ' + item.close_price : '', - ]; - if (item.source === 'review' && item.open_type) metaParts.push(item.open_type); - if (item.source === 'review' && item.exit_trigger) metaParts.push('出场: ' + item.exit_trigger); - if (item.result) metaParts.push(item.result); - meta.innerHTML = metaParts.filter(Boolean).join(' · '); - - card.appendChild(head); - card.appendChild(meta); - - var tags = fmtTags(item); - if (tags) { - var tagEl = document.createElement('div'); - tagEl.className = 'stats-day-item-notes'; - tagEl.textContent = tags; - card.appendChild(tagEl); - } - if (item.notes) { - var notes = document.createElement('div'); - notes.className = 'stats-day-item-notes'; - notes.textContent = item.notes; - card.appendChild(notes); - } - if (item.screenshot) { - var shot = document.createElement('div'); - shot.className = 'stats-day-item-shot'; - shot.innerHTML = '复盘截图'; - card.appendChild(shot); - } - list.appendChild(card); - }); - panel.hidden = false; - } - - function loadDayDetail(dateStr, scroll) { - if (scroll === undefined) scroll = true; - selectedDate = dateStr; - document.querySelectorAll('.stats-cal-cell.is-selected').forEach(function (el) { - el.classList.remove('is-selected'); - }); - var cell = document.querySelector('.stats-cal-cell[data-date="' + dateStr + '"]'); - if (cell) cell.classList.add('is-selected'); - - fetch('/api/stats/calendar/day?date=' + encodeURIComponent(dateStr), { credentials: 'same-origin' }) - .then(function (r) { - if (!r.ok) throw new Error('HTTP ' + r.status); - return r.json(); - }) - .then(function (data) { - renderDayDetail(data); - if (scroll) { - var panel = document.getElementById('stats-day-detail'); - if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - }) - .catch(function () { - var panel = document.getElementById('stats-day-detail'); - var list = document.getElementById('stats-day-list'); - if (panel && list) { - list.innerHTML = '
加载失败
'; - panel.hidden = false; - } - }); - } - - function shiftCalendarMonth(delta) { - calMonth += delta; - if (calMonth > 12) { - calMonth = 1; - calYear += 1; - } else if (calMonth < 1) { - calMonth = 12; - calYear -= 1; - } - loadCalendar(); - } - - function bindCalendar() { - var grid = document.getElementById('stats-calendar-grid'); - if (!grid || grid.dataset.statsCalBound) return; - grid.dataset.statsCalBound = '1'; - - grid.addEventListener('click', function (e) { - var cell = e.target.closest('.stats-cal-cell.is-clickable'); - if (!cell) return; - loadDayDetail(cell.getAttribute('data-date')); - }); - grid.addEventListener('keydown', function (e) { - if (e.key !== 'Enter' && e.key !== ' ') return; - var cell = e.target.closest('.stats-cal-cell.is-clickable'); - if (!cell) return; - e.preventDefault(); - loadDayDetail(cell.getAttribute('data-date')); - }); - - var prev = document.getElementById('stats-cal-prev'); - var next = document.getElementById('stats-cal-next'); - var todayBtn = document.getElementById('stats-cal-today'); - if (prev) prev.addEventListener('click', function () { shiftCalendarMonth(-1); }); - if (next) next.addEventListener('click', function () { shiftCalendarMonth(1); }); - if (todayBtn) todayBtn.addEventListener('click', function () { - var d = new Date(); - calYear = d.getFullYear(); - calMonth = d.getMonth() + 1; - loadCalendar(); - }); - } - - function initCalendar() { - if (!document.getElementById('stats-calendar-grid')) return; - var d = new Date(); - calYear = d.getFullYear(); - calMonth = d.getMonth() + 1; - bindCalendar(); - loadCalendar(); - } - function bootStatsPage() { if (!document.getElementById('stats-summary')) return; var viewSel = document.getElementById('stats-view-select'); @@ -432,7 +160,6 @@ }); } loadStats(); - initCalendar(); } if (window.qihuoPageBoot) window.qihuoPageBoot(bootStatsPage, '#stats-summary'); diff --git a/templates/base.html b/templates/base.html index 0ae12ac..fa75a69 100644 --- a/templates/base.html +++ b/templates/base.html @@ -89,6 +89,7 @@ {% if nav_items.ai %}AI 分析{% endif %} {% if nav_items.market %}行情K线{% endif %} 交易记录与复盘 + 交易日历 统计分析 {% if nav_items.fees %}手续费配置{% endif %} 系统设置 diff --git a/templates/calendar.html b/templates/calendar.html new file mode 100644 index 0000000..e52c5cd --- /dev/null +++ b/templates/calendar.html @@ -0,0 +1,144 @@ +{# Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt #} +{% extends "base.html" %} +{% block title %}交易日历 - 国内期货 · 交易复盘系统{% endblock %} +{% block extra_css %} + +{% endblock %} +{% block content %} + +
+
+
+

交易日历

+
+ + + + +
+
+

公历与农历对照;按平仓日汇总笔数与净盈亏。橙色日期含情绪单复盘,点击日期查看当日记录(已复盘优先)。

+
+ +
+
+
加载日历…
+
+ +
+
+ +{% endblock %} + +{% block extra_js %} + + +{% endblock %} diff --git a/templates/stats.html b/templates/stats.html index c81394e..0b6181c 100644 --- a/templates/stats.html +++ b/templates/stats.html @@ -28,93 +28,6 @@ .stats-card-head h2{margin-bottom:0} .stats-view-field{width:auto;min-width:200px} .stats-view-field select{width:100%;min-width:180px} - -/* 交易日历 */ -.stats-calendar-card{margin-bottom:1.25rem} -.stats-calendar-head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap;margin-bottom:.85rem} -.stats-calendar-head h2{margin:0} -.stats-calendar-nav{display:flex;align-items:center;gap:.5rem} -.stats-calendar-nav button{ - border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-title); - border-radius:8px;padding:.35rem .65rem;cursor:pointer;font:inherit;font-size:.85rem; -} -.stats-calendar-nav button:hover{border-color:var(--accent)} -.stats-calendar-title{font-size:.95rem;font-weight:600;color:var(--text-title);min-width:7rem;text-align:center} -.stats-calendar-weekdays{ - display:grid;grid-template-columns:repeat(7,1fr);gap:.35rem;margin-bottom:.35rem; -} -.stats-calendar-weekdays span{ - text-align:center;font-size:.68rem;color:var(--text-muted);padding:.15rem 0; -} -.stats-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:.35rem} -.stats-cal-cell{ - min-height:4.6rem;border:1px solid var(--card-border);border-radius:10px; - background:var(--card-inner);padding:.35rem .4rem;text-align:left;cursor:default; - transition:border-color .2s,box-shadow .2s; -} -.stats-cal-cell.is-empty{background:transparent;border-color:transparent} -.stats-cal-cell.is-clickable{cursor:pointer} -.stats-cal-cell.is-clickable:hover{border-color:var(--accent)} -.stats-cal-cell.is-selected{ - border-color:var(--accent);box-shadow:0 0 0 1px rgba(56,189,248,.25); -} -.stats-cal-cell.is-today .stats-cal-day-num{color:var(--accent);font-weight:700} -.stats-cal-cell.is-emotion{ - border-color:rgba(251,146,60,.65); - background:linear-gradient(145deg,rgba(251,146,60,.12),var(--card-inner)); -} -.stats-cal-cell.is-emotion.is-selected{ - border-color:rgba(251,146,60,.9); - box-shadow:0 0 0 1px rgba(251,146,60,.35); -} -.stats-cal-day-num{font-size:.78rem;font-weight:600;color:var(--text-title);line-height:1.2} -.stats-cal-meta{margin-top:.2rem;font-size:.62rem;line-height:1.35;color:var(--text-muted)} -.stats-cal-count{font-variant-numeric:tabular-nums} -.stats-cal-pnl{font-weight:600;font-variant-numeric:tabular-nums;margin-top:.08rem} -.stats-cal-pnl.is-profit{color:var(--profit)} -.stats-cal-pnl.is-loss{color:var(--loss)} -.stats-cal-emotion{ - display:inline-block;margin-top:.12rem;padding:.05rem .28rem;border-radius:4px; - font-size:.58rem;font-weight:600;color:#fb923c;background:rgba(251,146,60,.15); -} -.stats-day-detail{margin-top:1rem;padding-top:.85rem;border-top:1px solid var(--table-border)} -.stats-day-detail-head{ - display:flex;align-items:center;justify-content:space-between;gap:.75rem; - flex-wrap:wrap;margin-bottom:.65rem; -} -.stats-day-detail-head h3{margin:0;font-size:.95rem} -.stats-day-summary{font-size:.78rem;color:var(--text-muted)} -.stats-day-list{display:flex;flex-direction:column;gap:.55rem} -.stats-day-item{ - border:1px solid var(--card-border);border-radius:12px;background:var(--card-inner); - padding:.65rem .75rem; -} -.stats-day-item.is-emotion{ - border-color:rgba(251,146,60,.55); - background:linear-gradient(145deg,rgba(251,146,60,.1),var(--card-inner)); -} -.stats-day-item-head{ - display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin-bottom:.35rem; -} -.stats-day-item-symbol{font-weight:600;font-size:.88rem;color:var(--text-title)} -.stats-day-item-pnl{font-weight:600;font-size:.88rem;font-variant-numeric:tabular-nums} -.stats-day-item-pnl.is-profit{color:var(--profit)} -.stats-day-item-pnl.is-loss{color:var(--loss)} -.stats-day-item-meta{ - display:flex;flex-wrap:wrap;gap:.35rem .55rem;font-size:.72rem;color:var(--text-muted); -} -.stats-day-badge{ - display:inline-block;padding:.08rem .35rem;border-radius:4px;font-size:.65rem;font-weight:600; -} -.stats-day-badge.review{background:rgba(56,189,248,.15);color:var(--accent)} -.stats-day-badge.emotion{background:rgba(251,146,60,.18);color:#fb923c} -.stats-day-item-notes{ - margin-top:.4rem;font-size:.72rem;color:var(--text-muted);line-height:1.45; -} -.stats-day-item-shot{margin-top:.45rem} -.stats-day-item-shot img{ - max-width:100%;max-height:180px;border-radius:8px;border:1px solid var(--card-border); -} {% endblock %} {% block content %} @@ -141,31 +54,6 @@ -
-
-

交易日历

-
- - - - -
-
-
- -
-
-
加载日历…
-
- -
-

分项统计