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 += '
';
+ html += '
';
+ html += '' + dayNum + '';
+ html += lunarCellHtml(day.date);
+ html += '
';
+ if (day.count > 0) {
+ 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 = '
+
+
+
交易日历
+
+
+ —
+
+
+
+
+
公历与农历对照;按平仓日汇总笔数与净盈亏。橙色日期含情绪单复盘,点击日期查看当日记录(已复盘优先)。
+
+ 一二三四五六日
+
+
+
+
+
+
+{% 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 @@
-