feat: 内照明心交易日历与交易所口径成交额/手续费统计
新增按 08:00 切日的月历(盈亏、笔数、犯病日高亮与点击筛选);平仓时从交易所 fill 写入双边成交额与手续费,统计表与明细同步展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6703,6 +6703,107 @@ body.funds-fullscreen-open {
|
||||
.archive-trades-table td.neg {
|
||||
color: #ef4444;
|
||||
}
|
||||
.archive-calendar-wrap {
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-soft);
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
.archive-calendar-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.archive-cal-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.archive-cal-weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.archive-cal-wd {
|
||||
text-align: center;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted, #9aa);
|
||||
}
|
||||
.archive-cal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
.archive-cal-cell {
|
||||
min-height: 62px;
|
||||
padding: 4px 3px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(30, 41, 59, 0.45);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
.archive-cal-cell.has-trade {
|
||||
cursor: pointer;
|
||||
}
|
||||
.archive-cal-cell.has-trade:hover {
|
||||
border-color: rgba(99, 102, 241, 0.45);
|
||||
background: rgba(49, 46, 129, 0.25);
|
||||
}
|
||||
.archive-cal-cell.is-selected {
|
||||
border-color: rgba(99, 102, 241, 0.75);
|
||||
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
|
||||
}
|
||||
.archive-cal-cell.is-sick-day {
|
||||
border-color: rgba(239, 68, 68, 0.55);
|
||||
background: rgba(127, 29, 29, 0.22);
|
||||
}
|
||||
.archive-cal-cell.is-sick-day.is-selected {
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.45);
|
||||
}
|
||||
.archive-cal-cell.pnl-pos .archive-cal-pnl {
|
||||
color: #22c55e;
|
||||
}
|
||||
.archive-cal-cell.pnl-neg .archive-cal-pnl {
|
||||
color: #ef4444;
|
||||
}
|
||||
.archive-cal-day-num {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.archive-cal-pnl {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.archive-cal-cnt {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted, #9aa);
|
||||
}
|
||||
.archive-cal-sick-tag {
|
||||
font-size: 0.62rem;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.25);
|
||||
color: #fca5a5;
|
||||
font-weight: 600;
|
||||
}
|
||||
.archive-cal-pad {
|
||||
background: transparent;
|
||||
border: none;
|
||||
min-height: 0;
|
||||
}
|
||||
.archive-del-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
const elBtnSync = document.getElementById("archive-btn-sync");
|
||||
const elStatus = document.getElementById("archive-status");
|
||||
const elStats = document.getElementById("archive-stats");
|
||||
const elCalendarWrap = document.getElementById("archive-calendar-wrap");
|
||||
const elCalendar = document.getElementById("archive-calendar");
|
||||
const elCalTitle = document.getElementById("archive-cal-title");
|
||||
const elCalPrev = document.getElementById("archive-cal-prev");
|
||||
const elCalNext = document.getElementById("archive-cal-next");
|
||||
const elQuotesList = document.getElementById("archive-quotes-list");
|
||||
const elQuotesCount = document.getElementById("archive-quotes-count");
|
||||
const elQuoteForm = document.getElementById("archive-quote-form");
|
||||
@@ -72,6 +77,10 @@
|
||||
let chartExchangeSymbol = "";
|
||||
let chartMarketType = "swap";
|
||||
let searchTimer = null;
|
||||
let calendarYear = 0;
|
||||
let calendarMonth = 0;
|
||||
let calendarDays = {};
|
||||
let selectedCalendarDay = "";
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? "" : s)
|
||||
@@ -401,6 +410,19 @@
|
||||
return q.toString();
|
||||
}
|
||||
|
||||
function fmtVolStat(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) return "—";
|
||||
if (n >= 10000) return (n / 1000).toFixed(1) + "k";
|
||||
return n.toFixed(0) + "U";
|
||||
}
|
||||
|
||||
function fmtFeeStat(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n <= 0) return "—";
|
||||
return n.toFixed(2) + "U";
|
||||
}
|
||||
|
||||
function fmtPnlStat(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
@@ -475,11 +497,146 @@
|
||||
"%</td><td>" +
|
||||
fmtPnlStat(e.pnl_total) +
|
||||
"</td><td>" +
|
||||
fmtPnlStat(e.pnl_total) +
|
||||
"</td><td>" +
|
||||
fmtPnlStat(e.pnl_ex_sick) +
|
||||
"</td><td>" +
|
||||
fmtVolStat(e.turnover_total) +
|
||||
"</td><td>" +
|
||||
fmtFeeStat(e.commission_total) +
|
||||
"</td></tr>"
|
||||
);
|
||||
}
|
||||
|
||||
function calendarMonthLabel(y, m) {
|
||||
return y + " 年 " + m + " 月";
|
||||
}
|
||||
|
||||
function ensureCalendarMonthFromUI() {
|
||||
if (calendarYear > 0 && calendarMonth > 0) return;
|
||||
let ref = tradingDay || (elTradingDay && elTradingDay.value) || "";
|
||||
if (!ref && dateFrom) ref = dateFrom;
|
||||
if (!ref) {
|
||||
const now = new Date();
|
||||
calendarYear = now.getFullYear();
|
||||
calendarMonth = now.getMonth() + 1;
|
||||
return;
|
||||
}
|
||||
const p = String(ref).slice(0, 10).split("-");
|
||||
calendarYear = parseInt(p[0], 10) || new Date().getFullYear();
|
||||
calendarMonth = parseInt(p[1], 10) || new Date().getMonth() + 1;
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
if (!elCalendar || !elCalTitle) return;
|
||||
ensureCalendarMonthFromUI();
|
||||
elCalTitle.textContent = calendarMonthLabel(calendarYear, calendarMonth);
|
||||
const first = new Date(calendarYear, calendarMonth - 1, 1);
|
||||
const lastDay = new Date(calendarYear, calendarMonth, 0).getDate();
|
||||
const startWd = first.getDay();
|
||||
const weekdays = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
let html =
|
||||
'<div class="archive-cal-weekdays">' +
|
||||
weekdays.map(function (w) {
|
||||
return '<span class="archive-cal-wd">' + w + "</span>";
|
||||
}).join("") +
|
||||
"</div><div class=\"archive-cal-grid\">";
|
||||
for (let i = 0; i < startWd; i++) {
|
||||
html += '<span class="archive-cal-cell archive-cal-pad"></span>';
|
||||
}
|
||||
for (let d = 1; d <= lastDay; d++) {
|
||||
const dayStr =
|
||||
calendarYear +
|
||||
"-" +
|
||||
String(calendarMonth).padStart(2, "0") +
|
||||
"-" +
|
||||
String(d).padStart(2, "0");
|
||||
const info = calendarDays[dayStr];
|
||||
const hasTrade = info && info.open_count > 0;
|
||||
const sick = info && info.has_sick;
|
||||
const pnl = hasTrade ? Number(info.pnl_total) : null;
|
||||
const cnt = hasTrade ? info.open_count : 0;
|
||||
const cls =
|
||||
"archive-cal-cell" +
|
||||
(hasTrade ? " has-trade" : "") +
|
||||
(sick ? " is-sick-day" : "") +
|
||||
(selectedCalendarDay === dayStr ? " is-selected" : "") +
|
||||
(pnl != null && pnl > 0.0001 ? " pnl-pos" : pnl != null && pnl < -0.0001 ? " pnl-neg" : "");
|
||||
let body = '<span class="archive-cal-day-num">' + d + "</span>";
|
||||
if (hasTrade) {
|
||||
const pnlTxt = (pnl >= 0 ? "+" : "") + pnl.toFixed(1);
|
||||
body +=
|
||||
'<span class="archive-cal-pnl">' +
|
||||
esc(pnlTxt) +
|
||||
"</span>" +
|
||||
'<span class="archive-cal-cnt">' +
|
||||
cnt +
|
||||
"笔</span>";
|
||||
if (sick) body += '<span class="archive-cal-sick-tag">犯病</span>';
|
||||
}
|
||||
html +=
|
||||
'<button type="button" class="' +
|
||||
cls +
|
||||
'" data-day="' +
|
||||
dayStr +
|
||||
'" data-sick="' +
|
||||
(sick ? "1" : "0") +
|
||||
'"' +
|
||||
(hasTrade ? "" : " disabled") +
|
||||
">" +
|
||||
body +
|
||||
"</button>";
|
||||
}
|
||||
html += "</div>";
|
||||
elCalendar.innerHTML = html;
|
||||
elCalendar.querySelectorAll(".archive-cal-cell[data-day]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const day = btn.getAttribute("data-day");
|
||||
if (!day) return;
|
||||
selectedCalendarDay = day;
|
||||
setPeriodMode("today");
|
||||
if (elTradingDay) elTradingDay.value = day;
|
||||
if (elFilterSick) {
|
||||
elFilterSick.checked = btn.getAttribute("data-sick") === "1";
|
||||
}
|
||||
syncPeriodUI();
|
||||
void loadDailyTrades();
|
||||
renderCalendar();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCalendar() {
|
||||
ensureCalendarMonthFromUI();
|
||||
const q = new URLSearchParams();
|
||||
q.set("year", String(calendarYear));
|
||||
q.set("month", String(calendarMonth));
|
||||
const ex = (elExchange && elExchange.value) || "";
|
||||
if (ex) q.set("exchange_key", ex);
|
||||
try {
|
||||
const r = await apiFetch("/api/archive/calendar?" + q.toString());
|
||||
const data = await r.json();
|
||||
if (!data.ok) return;
|
||||
calendarDays = data.days || {};
|
||||
renderCalendar();
|
||||
} catch (e) {
|
||||
console.warn("[archive calendar]", e);
|
||||
}
|
||||
}
|
||||
|
||||
function shiftCalendarMonth(delta) {
|
||||
ensureCalendarMonthFromUI();
|
||||
calendarMonth += delta;
|
||||
if (calendarMonth > 12) {
|
||||
calendarMonth = 1;
|
||||
calendarYear += 1;
|
||||
} else if (calendarMonth < 1) {
|
||||
calendarMonth = 12;
|
||||
calendarYear -= 1;
|
||||
}
|
||||
void loadCalendar();
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
if (!elStats) return;
|
||||
const st = dailyStats || { open_count: 0, by_exchange: {} };
|
||||
@@ -503,6 +660,8 @@
|
||||
profit_loss_ratio: st.profit_loss_ratio,
|
||||
max_win: st.max_win,
|
||||
max_loss: st.max_loss,
|
||||
turnover_total: st.turnover_total,
|
||||
commission_total: st.commission_total,
|
||||
},
|
||||
true
|
||||
) +
|
||||
@@ -513,7 +672,7 @@
|
||||
.join("");
|
||||
elStats.innerHTML =
|
||||
'<table class="archive-stats-table"><thead><tr>' +
|
||||
"<th>范围</th><th>开仓</th><th>盈利单</th><th>亏损单</th><th>胜率</th><th>平均盈利</th><th>平均亏损</th><th>盈亏比</th><th>最大盈利</th><th>最大亏损</th><th>犯病</th><th>犯病占比</th><th>盈亏</th><th>剔除犯病盈亏</th>" +
|
||||
"<th>范围</th><th>开仓</th><th>盈利单</th><th>亏损单</th><th>胜率</th><th>平均盈利</th><th>平均亏损</th><th>盈亏比</th><th>最大盈利</th><th>最大亏损</th><th>犯病</th><th>犯病占比</th><th>盈亏</th><th>剔除犯病盈亏</th><th>成交额</th><th>手续费</th>" +
|
||||
"</tr></thead><tbody>" +
|
||||
rows +
|
||||
"</tbody></table>";
|
||||
@@ -1147,7 +1306,7 @@
|
||||
elTrades.innerHTML =
|
||||
'<table class="archive-trades-table"><thead><tr>' +
|
||||
"<th>交易所</th><th>合约</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
|
||||
"<th>方向</th><th>结果</th><th>盈亏</th><th>标签</th><th>备注</th><th>操作</th>" +
|
||||
"<th>方向</th><th>结果</th><th>盈亏</th><th>成交额</th><th>手续费</th><th>标签</th><th>备注</th><th>操作</th>" +
|
||||
"</tr></thead><tbody>" +
|
||||
dailyTrades
|
||||
.map(function (t) {
|
||||
@@ -1201,6 +1360,12 @@
|
||||
'">' +
|
||||
fmtPnl(t.pnl_amount) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmtVolStat(t.exchange_turnover_usdt) +
|
||||
"</td>" +
|
||||
"<td>" +
|
||||
fmtFeeStat(t.exchange_commission_usdt) +
|
||||
"</td>" +
|
||||
'<td><select class="archive-tag-select" data-id="' +
|
||||
tid +
|
||||
'" data-ex="' +
|
||||
@@ -1362,8 +1527,10 @@
|
||||
syncPeriodUI();
|
||||
dailyTrades = j.trades || [];
|
||||
dailyStats = j.stats || { open_count: 0, by_exchange: {} };
|
||||
if (periodMode === "today" && tradingDay) selectedCalendarDay = tradingDay;
|
||||
renderStats();
|
||||
renderTrades();
|
||||
void loadCalendar();
|
||||
setStatus(
|
||||
(periodLabel || tradingDay || "当日") +
|
||||
" · 列表 " +
|
||||
@@ -1414,6 +1581,7 @@
|
||||
const j = await r.json();
|
||||
setStatus(formatSyncSummary(j));
|
||||
await loadDailyTrades();
|
||||
await loadCalendar();
|
||||
await loadQuotes();
|
||||
if (isChartOpen() && selected) await loadChart();
|
||||
} catch (e) {
|
||||
@@ -1426,7 +1594,14 @@
|
||||
function bindEvents() {
|
||||
if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadDailyTrades);
|
||||
if (elBtnSync) elBtnSync.addEventListener("click", syncAll);
|
||||
if (elExchange) elExchange.addEventListener("change", loadDailyTrades);
|
||||
if (elExchange) {
|
||||
elExchange.addEventListener("change", function () {
|
||||
void loadDailyTrades();
|
||||
void loadCalendar();
|
||||
});
|
||||
}
|
||||
if (elCalPrev) elCalPrev.addEventListener("click", function () { shiftCalendarMonth(-1); });
|
||||
if (elCalNext) elCalNext.addEventListener("click", function () { shiftCalendarMonth(1); });
|
||||
if (elPeriodTabs) {
|
||||
elPeriodTabs.addEventListener("click", function (ev) {
|
||||
const btn = ev.target.closest(".archive-period-btn");
|
||||
|
||||
@@ -488,6 +488,14 @@
|
||||
<h2>数据总览</h2>
|
||||
</div>
|
||||
<div id="archive-stats" class="archive-stats-bar"></div>
|
||||
<div id="archive-calendar-wrap" class="archive-calendar-wrap">
|
||||
<div class="archive-calendar-head">
|
||||
<button type="button" id="archive-cal-prev" class="ghost" title="上一月">‹</button>
|
||||
<span id="archive-cal-title" class="archive-cal-title"></span>
|
||||
<button type="button" id="archive-cal-next" class="ghost" title="下一月">›</button>
|
||||
</div>
|
||||
<div id="archive-calendar" class="archive-calendar" role="grid" aria-label="交易日历"></div>
|
||||
</div>
|
||||
</section>
|
||||
<details id="archive-chart-section" class="archive-acc-section archive-chart-section archive-panel-desktop">
|
||||
<summary class="archive-acc-summary">K 线图表 <span id="archive-chart-title" class="archive-acc-sub">—</span></summary>
|
||||
|
||||
Reference in New Issue
Block a user