/**
* 交易日历组件:内照明心档案 + 四所统计分析共用。
*/
(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);