/** * 交易日历组件:内照明心档案 + 四所统计分析共用。 */ (function (global) { "use strict"; var WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"]; function esc(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function monthLabel(y, m) { return y + "年" + m + "月"; } function formatCalPnl(pnl) { var n = Number(pnl); if (!Number.isFinite(n)) n = 0; return (n >= 0 ? "+" : "") + n.toFixed(1) + "U"; } function dayHasTrade(info) { if (!info) return false; var cnt = Number(info.open_count); if (Number.isFinite(cnt) && cnt > 0) return true; var pnl = Number(info.pnl_total); return Number.isFinite(pnl) && Math.abs(pnl) > 0.0001; } function dayOpenCount(info) { var cnt = Number(info && info.open_count); return Number.isFinite(cnt) && cnt > 0 ? cnt : 0; } function dayPnl(info) { return Number(info && info.pnl_total) || 0; } function TradeStatsCalendar(config) { this.gridEl = config.gridEl; this.titleEl = config.titleEl; this.prevBtn = config.prevBtn || null; this.nextBtn = config.nextBtn || null; this.apiUrl = config.apiUrl || "/api/stats/calendar"; this.buildQuery = config.buildQuery || function (year, month) { var q = new URLSearchParams(); q.set("year", String(year)); q.set("month", String(month)); return q; }; this.parseResponse = config.parseResponse || function (data) { if (data && data.ok === false) return {}; return (data && data.days) || {}; }; this.fetchFn = config.fetchFn || null; this.showSick = config.showSick !== false; this.selectedDay = config.selectedDay || ""; this.onDayClick = config.onDayClick || null; this.onMonthChange = config.onMonthChange || null; this.year = config.year || 0; this.month = config.month || 0; this.days = {}; this.monthPnlTotal = 0; this.monthOpenCount = 0; this._navBound = false; this._bindNav(); } TradeStatsCalendar.prototype.ensureMonth = function (ref) { if (this.year > 0 && this.month > 0) return; var d; if (ref instanceof Date) d = ref; else if (typeof ref === "string" && ref.length >= 7) { var p = ref.slice(0, 10).split("-"); this.year = parseInt(p[0], 10) || new Date().getFullYear(); this.month = parseInt(p[1], 10) || new Date().getMonth() + 1; return; } else d = new Date(); this.year = d.getFullYear(); this.month = d.getMonth() + 1; }; TradeStatsCalendar.prototype.setSelectedDay = function (day) { this.selectedDay = day || ""; this.render(); }; TradeStatsCalendar.prototype.render = function () { if (!this.gridEl || !this.titleEl) return; if (this.year <= 0 || this.month <= 0) this.ensureMonth(new Date()); var title = monthLabel(this.year, this.month); if (this.monthOpenCount > 0) { title += " · " + formatCalPnl(this.monthPnlTotal) + " · " + this.monthOpenCount + "笔"; } this.titleEl.textContent = title; var first = new Date(this.year, this.month - 1, 1); var lastDay = new Date(this.year, this.month, 0).getDate(); var startWd = first.getDay(); var html = '
' + WEEKDAYS.map(function (w) { return '' + w + ""; }).join("") + '
'; var i; for (i = 0; i < startWd; i++) { html += ''; } for (var d = 1; d <= lastDay; d++) { var dayStr = this.year + "-" + String(this.month).padStart(2, "0") + "-" + String(d).padStart(2, "0"); var info = this.days[dayStr]; var hasTrade = dayHasTrade(info); var sick = this.showSick && info && info.has_sick; var pnl = hasTrade ? dayPnl(info) : null; var cnt = hasTrade ? dayOpenCount(info) : 0; var cls = "trade-cal-cell" + (hasTrade ? " has-trade" : "") + (sick ? " is-sick-day" : "") + (this.selectedDay === dayStr ? " is-selected" : "") + (pnl != null && pnl > 0.0001 ? " pnl-pos" : pnl != null && pnl < -0.0001 ? " pnl-neg" : ""); var body = '' + d + ""; if (hasTrade) { body += '' + esc(formatCalPnl(pnl)) + "" + '' + cnt + "笔"; if (sick) body += '犯病'; } html += '"; } html += "
"; this.gridEl.innerHTML = html; var self = this; this.gridEl.querySelectorAll(".trade-cal-cell[data-day]").forEach(function (btn) { btn.addEventListener("click", function () { var day = btn.getAttribute("data-day"); if (!day || !self.onDayClick) return; self.selectedDay = day; self.render(); self.onDayClick(day, btn.getAttribute("data-sick") === "1", self.days[day] || null); }); }); }; TradeStatsCalendar.prototype.load = async function () { this.ensureMonth(new Date()); this.render(); var q = this.buildQuery(this.year, this.month); if (!q.has("year")) q.set("year", String(this.year)); if (!q.has("month")) q.set("month", String(this.month)); try { var data; if (this.fetchFn) { data = await this.fetchFn(q); } else { var resp = await fetch(this.apiUrl + "?" + q.toString(), { credentials: "same-origin", }); if (!resp.ok) { console.warn("[trade calendar] api", resp.status); return; } data = await resp.json(); } this.days = this.parseResponse(data) || {}; this.monthPnlTotal = Number(data && data.month_pnl_total) || 0; this.monthOpenCount = Number(data && data.month_open_count) || 0; if (!this.monthOpenCount) { var self = this; Object.keys(this.days).forEach(function (k) { if (dayHasTrade(self.days[k])) { self.monthOpenCount += dayOpenCount(self.days[k]); self.monthPnlTotal += dayPnl(self.days[k]); } }); this.monthPnlTotal = Math.round(this.monthPnlTotal * 10000) / 10000; } this.render(); if (this.onMonthChange) this.onMonthChange(this.year, this.month, this.days); } catch (e) { console.warn("[trade calendar]", e); this.render(); } }; TradeStatsCalendar.prototype.shiftMonth = function (delta) { this.ensureMonth(new Date()); this.month += delta; if (this.month > 12) { this.month = 1; this.year += 1; } else if (this.month < 1) { this.month = 12; this.year -= 1; } void this.load(); }; TradeStatsCalendar.prototype._bindNav = function () { if (this._navBound) return; var self = this; if (this.prevBtn) { this.prevBtn.addEventListener("click", function () { self.shiftMonth(-1); }); } if (this.nextBtn) { this.nextBtn.addEventListener("click", function () { self.shiftMonth(1); }); } this._navBound = true; }; global.TradeStatsCalendar = TradeStatsCalendar; global.statsCalendarWidget = null; global.initInstanceStatsCalendar = function () { var grid = document.getElementById("stats-calendar"); if (!grid || !global.TradeStatsCalendar) return null; global.statsCalendarWidget = new TradeStatsCalendar({ gridEl: grid, titleEl: document.getElementById("stats-cal-title"), prevBtn: document.getElementById("stats-cal-prev"), nextBtn: document.getElementById("stats-cal-next"), apiUrl: "/api/stats/calendar", showSick: false, buildQuery: function (year, month) { var q = new URLSearchParams(); q.set("year", String(year)); q.set("month", String(month)); var sel = document.getElementById("stats-segment-select"); if (sel) q.set("segment", sel.value || "all"); return q; }, parseResponse: function (data) { if (data && data.ok === false) return {}; return (data && data.days) || {}; }, }); global.statsCalendarWidget.render(); void global.statsCalendarWidget.load(); return global.statsCalendarWidget; }; global.initStatsCalendarWidget = global.initInstanceStatsCalendar; })(window);