Add shared records review pagination for all three exchanges.

Paginate trade records, journals, and AI history at 5 per page with soft in-card flips; hide the journal form until 填入复盘.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 11:52:02 +08:00
parent 57f8d6761c
commit d9b8069906
14 changed files with 999 additions and 361 deletions
+15
View File
@@ -9425,6 +9425,21 @@ def api_journal_upload_slot():
return jsonify(payload), code
from lib.instance.records_api_register import register_trade_records_api
register_trade_records_api(
app,
login_required=login_required,
get_db=get_db,
list_window_from_request=_list_window_from_request,
utc_window_to_bj_sql_strings=utc_window_to_bj_sql_strings,
sql_list_time_field=sql_list_time_field,
to_effective_trade_dict=to_effective_trade_dict,
filter_trade_records_excluding_miss=filter_trade_records_excluding_miss,
app_tz=APP_TZ,
)
@app.route("/api/journals")
@login_required
def api_journals():
+15
View File
@@ -9292,6 +9292,21 @@ def api_journal_upload_slot():
return jsonify(payload), code
from lib.instance.records_api_register import register_trade_records_api
register_trade_records_api(
app,
login_required=login_required,
get_db=get_db,
list_window_from_request=_list_window_from_request,
utc_window_to_bj_sql_strings=utc_window_to_bj_sql_strings,
sql_list_time_field=sql_list_time_field,
to_effective_trade_dict=to_effective_trade_dict,
filter_trade_records_excluding_miss=filter_trade_records_excluding_miss,
app_tz=APP_TZ,
)
@app.route("/api/journals")
@login_required
def api_journals():
+15
View File
@@ -8980,6 +8980,21 @@ def api_journal_upload_slot():
return jsonify(payload), code
from lib.instance.records_api_register import register_trade_records_api
register_trade_records_api(
app,
login_required=login_required,
get_db=get_db,
list_window_from_request=_list_window_from_request,
utc_window_to_bj_sql_strings=utc_window_to_bj_sql_strings,
sql_list_time_field=sql_list_time_field,
to_effective_trade_dict=to_effective_trade_dict,
filter_trade_records_excluding_miss=filter_trade_records_excluding_miss,
app_tz=APP_TZ,
)
@app.route("/api/journals")
@login_required
def api_journals():
+6 -2
View File
@@ -103,8 +103,12 @@
global.initStrategyRollForm();
}
if (tab === "records") {
if (!revisit && typeof global.loadJournals === "function") global.loadJournals();
if (!revisit && typeof global.loadReviews === "function") global.loadReviews();
if (global.RecordsReviewPage && typeof global.RecordsReviewPage.init === "function") {
global.RecordsReviewPage.init({ refresh: !!revisit });
} else {
if (!revisit && typeof global.loadJournals === "function") global.loadJournals();
if (!revisit && typeof global.loadReviews === "function") global.loadReviews();
}
if (global.InstanceTheme && typeof global.InstanceTheme.initReviewEditModeSync === "function") {
global.InstanceTheme.initReviewEditModeSync();
} else if (typeof global.toggleReviewMode === "function") {
+604
View File
@@ -0,0 +1,604 @@
/**
* 三所 /records:交易记录分页 + 复盘表单显隐 + 复盘/AI 列表分页(soft,每页5).
*/
(function (global) {
"use strict";
var PAGE_SIZE = 5;
var tradesPage = 0;
var tradesPages = 1;
var journalsAll = [];
var journalsPage = 0;
var journalsPages = 1;
var reviewsAll = [];
var reviewsPage = 0;
var reviewsPages = 1;
var tradesCache = {};
var booted = false;
function $(id) {
return document.getElementById(id);
}
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function listQs() {
if (typeof global.listWindowQueryString === "function") {
return global.listWindowQueryString() || "";
}
return "";
}
function fmtNum(v, digits) {
if (v == null || v === "") return "—";
var n = Number(v);
if (!Number.isFinite(n)) return esc(v);
return n.toFixed(digits == null ? 2 : digits);
}
function fmtTime(s) {
if (!s) return "—";
return esc(String(s).slice(0, 16));
}
function resultBadge(result) {
var er = String(result || "").trim();
if (["止盈", "保本止盈", "移动止盈"].indexOf(er) >= 0) {
return '<span class="badge profit">' + esc(er) + "</span>";
}
if (["止损", "强制清仓", "手动平仓"].indexOf(er) >= 0) {
return '<span class="badge loss">' + esc(er) + "</span>";
}
if (er === "时间平仓") return '<span class="badge miss">' + esc(er) + "</span>";
return '<span class="badge">' + esc(er || "-") + "</span>";
}
function pnlClass(v) {
var n = Number(v);
if (!Number.isFinite(n) || n === 0) return "";
return n > 0 ? "pnl-profit" : "pnl-loss";
}
function beginSoft(wrapId, soft) {
var wrap = $(wrapId);
if (!wrap) return null;
if (soft) {
if (!wrap.style.minHeight) {
wrap.style.minHeight = Math.max(wrap.offsetHeight, 1) + "px";
}
wrap.classList.add("rr-list-loading");
} else {
wrap.classList.remove("rr-list-loading");
wrap.style.minHeight = "";
}
return wrap;
}
function endSoft(wrap) {
if (!wrap) return;
wrap.classList.remove("rr-list-loading");
wrap.style.minHeight = "";
}
function updatePager(kind) {
var map = {
trades: {
page: tradesPage,
pages: tradesPages,
label: "rr-trades-page-label",
prev: "rr-trades-prev",
next: "rr-trades-next",
},
journals: {
page: journalsPage,
pages: journalsPages,
label: "rr-journals-page-label",
prev: "rr-journals-prev",
next: "rr-journals-next",
},
reviews: {
page: reviewsPage,
pages: reviewsPages,
label: "rr-reviews-page-label",
prev: "rr-reviews-prev",
next: "rr-reviews-next",
},
};
var m = map[kind];
if (!m) return;
var label = $(m.label);
var prev = $(m.prev);
var next = $(m.next);
if (label) label.textContent = "第 " + (m.page + 1) + " / " + m.pages + " 页";
if (prev) prev.disabled = m.page <= 0;
if (next) next.disabled = m.page + 1 >= m.pages;
}
function fillPayload(t) {
return {
symbol: t.symbol,
monitor_type: t.monitor_type,
key_signal_type: t.key_signal_type || "",
direction: t.direction,
trigger_price: t.trigger_price,
stop_loss: t.display_open_stop_loss || t.initial_stop_loss || t.stop_loss,
take_profit: t.effective_take_profit || t.take_profit,
opened_at: t.effective_opened_at,
closed_at: t.effective_closed_at,
pnl_amount: t.effective_pnl_amount,
result: t.effective_result,
risk_amount: t.risk_amount,
effective_entry_reason: t.effective_entry_reason || "",
};
}
function editPayload(t) {
return {
id: t.id,
opened_at: t.effective_opened_at,
closed_at: t.effective_closed_at,
stop_loss: t.effective_stop_loss || t.initial_stop_loss || t.stop_loss,
take_profit: t.effective_take_profit || t.take_profit,
pnl_amount: t.effective_pnl_amount,
result: t.effective_result,
miss_reason: t.effective_miss_reason,
effective_entry_reason: t.effective_entry_reason || "",
};
}
function renderTradesRows(rows) {
var tbody = $("rr-trades-tbody");
if (!tbody) return;
tradesCache = {};
if (!rows || !rows.length) {
tbody.innerHTML = '<tr><td colspan="15" class="muted">暂无交易记录</td></tr>';
return;
}
tbody.innerHTML = rows
.map(function (t) {
tradesCache[t.id] = t;
var mon = esc(t.monitor_type || "");
if (t.key_signal_type) mon += " · " + esc(t.key_signal_type);
var stopShow = t.display_open_stop_loss || t.initial_stop_loss || t.stop_loss;
var tpShow = t.effective_take_profit || t.take_profit;
var pnl = t.effective_pnl_amount;
var pnlSrc = "";
if (t.display_pnl_source === "exchange") {
pnlSrc = '<span style="font-size:.68rem;color:#6ab88a">所</span>';
} else if (t.display_pnl_source !== "reviewed") {
pnlSrc = '<span style="font-size:.68rem;color:#8892b0">估</span>';
}
var dirCls = t.direction === "long" ? "direction-long" : "direction-short";
var dirTxt = t.direction === "long" ? "做多" : "做空";
var margin =
t.margin_capital != null && t.margin_capital !== ""
? fmtNum(t.margin_capital, 2)
: "-";
return (
'<tr id="trade-row-' +
esc(t.id) +
'">' +
"<td>" +
esc(t.symbol) +
"</td>" +
"<td>" +
mon +
"</td>" +
"<td>" +
esc(t.effective_entry_reason || "-") +
"</td>" +
'<td><span class="badge ' +
dirCls +
'">' +
dirTxt +
"</span></td>" +
"<td>" +
fmtNum(t.trigger_price, 4) +
"</td>" +
"<td>" +
fmtNum(stopShow, 4) +
"</td>" +
"<td>" +
fmtNum(tpShow, 4) +
"</td>" +
"<td>" +
margin +
"</td>" +
"<td>" +
esc(t.leverage != null ? t.leverage : "-") +
"</td>" +
"<td>" +
esc(t.effective_hold_minutes || 0) +
"</td>" +
"<td>" +
fmtTime(t.effective_opened_at) +
"</td>" +
"<td>" +
fmtTime(t.effective_closed_at || t.created_at) +
"</td>" +
'<td><span class="' +
pnlClass(pnl) +
'">' +
fmtNum(pnl, 2) +
"</span>" +
pnlSrc +
"</td>" +
"<td>" +
resultBadge(t.effective_result) +
"</td>" +
"<td>" +
'<button type="button" class="table-del rr-fill-btn" style="background:#1f3a5a;color:#8fc8ff;margin-right:6px" data-id="' +
esc(t.id) +
'">填入复盘</button> ' +
'<button type="button" class="table-del review-edit-btn" style="background:#1f3a5a;color:#8fc8ff;margin-right:6px" data-id="' +
esc(t.id) +
'" disabled>核对修改</button> ' +
'<button type="button" class="table-del" onclick="deleteTradeRecord(' +
Number(t.id) +
')">删除</button>' +
"</td>" +
"</tr>"
);
})
.join("");
tbody.querySelectorAll(".rr-fill-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var id = btn.getAttribute("data-id");
var t = tradesCache[id];
if (!t) return;
showJournalCard();
if (typeof global.fillJournalFromTrade === "function") {
global.fillJournalFromTrade(fillPayload(t));
}
});
});
tbody.querySelectorAll(".review-edit-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var id = btn.getAttribute("data-id");
var t = tradesCache[id];
if (!t) return;
if (typeof global.editTradeRecordReview === "function") {
global.editTradeRecordReview(editPayload(t));
}
});
});
if (typeof global.toggleReviewMode === "function") {
global.toggleReviewMode();
}
}
function loadTradeRecords(opts) {
opts = opts || {};
var soft = !!opts.soft;
var tbody = $("rr-trades-tbody");
if (!tbody) return;
var wrap = beginSoft("rr-trades-wrap", soft);
if (!soft) {
tbody.innerHTML = '<tr><td colspan="15" class="muted">加载中…</td></tr>';
}
var qs = listQs();
var p = new URLSearchParams(qs || "");
p.set("limit", String(PAGE_SIZE));
p.set("offset", String(tradesPage * PAGE_SIZE));
fetch("/api/trade_records?" + p.toString(), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (!data || !data.ok) {
tbody.innerHTML = '<tr><td colspan="15" class="muted">加载失败</td></tr>';
endSoft(wrap);
return;
}
tradesPages = Math.max(1, Number(data.pages) || 1);
if (tradesPage >= tradesPages) {
tradesPage = Math.max(0, tradesPages - 1);
updatePager("trades");
if (Number(data.total || 0) > 0) {
loadTradeRecords(opts);
return;
}
}
updatePager("trades");
renderTradesRows(data.items || []);
endSoft(wrap);
})
.catch(function () {
tbody.innerHTML = '<tr><td colspan="15" class="muted">加载失败</td></tr>';
endSoft(wrap);
});
}
function renderJournalsPage(soft) {
var box = $("journal-list");
if (!box) return;
var wrap = beginSoft("journal-list-wrap", soft);
var total = journalsAll.length;
journalsPages = Math.max(1, Math.ceil(total / PAGE_SIZE) || 1);
if (journalsPage >= journalsPages) journalsPage = Math.max(0, journalsPages - 1);
updatePager("journals");
var slice = journalsAll.slice(
journalsPage * PAGE_SIZE,
journalsPage * PAGE_SIZE + PAGE_SIZE
);
if (global.InstanceUI && typeof InstanceUI.renderJournalListHtml === "function") {
var html = InstanceUI.renderJournalListHtml(slice);
box.innerHTML = html || "<div class='journal-empty-msg'>暂无数据</div>";
} else {
box.innerHTML = "<div class='journal-empty-msg'>暂无数据</div>";
}
endSoft(wrap);
}
function renderReviewsPage(soft) {
var box = $("review-list");
if (!box) return;
var wrap = beginSoft("review-list-wrap", soft);
var total = reviewsAll.length;
reviewsPages = Math.max(1, Math.ceil(total / PAGE_SIZE) || 1);
if (reviewsPage >= reviewsPages) reviewsPage = Math.max(0, reviewsPages - 1);
updatePager("reviews");
var slice = reviewsAll.slice(
reviewsPage * PAGE_SIZE,
reviewsPage * PAGE_SIZE + PAGE_SIZE
);
if (!slice.length) {
box.innerHTML = "<div class='entry'>暂无数据</div>";
endSoft(wrap);
return;
}
var html = "";
slice.forEach(function (r) {
if (global.reviewCache) global.reviewCache[r.id] = r;
var preview = (r.content || "").replace(/\s+/g, " ").trim();
var shortText = preview.length > 90 ? preview.slice(0, 90) + "..." : preview;
html +=
'<div class="entry">' +
"<div><strong>" +
(r.review_type === "daily" ? "日复盘" : "周复盘") +
"</strong> | " +
esc(r.target_date) +
"</div>" +
'<div style="font-size:12px;color:#9aa">' +
esc(r.created_at || "") +
"</div>" +
'<div style="margin-top:4px;color:#c9d2ff">' +
esc(shortText || "(空)") +
"</div>" +
'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">' +
'<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openReviewDetail(\'' +
esc(r.id) +
"', false)\">查看</button>" +
'<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openReviewDetail(\'' +
esc(r.id) +
"', true)\">全屏</button>" +
'<a class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff" href="/export/review_md/' +
esc(r.id) +
'">导出MD</a>' +
'<button type="button" class="btn-del" onclick="deleteReview(\'' +
esc(r.id) +
"')\">删除</button>" +
"</div></div>";
});
box.innerHTML = html;
endSoft(wrap);
}
function loadJournalsPaged() {
var qs = listQs();
fetch("/api/journals" + (qs ? "?" + qs : ""), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
journalsAll = Array.isArray(data) ? data : [];
if (global.journalCache) {
Object.keys(global.journalCache).forEach(function (k) {
delete global.journalCache[k];
});
journalsAll.forEach(function (o) {
global.journalCache[o.id] = o;
});
}
journalsPage = 0;
renderJournalsPage(false);
});
}
function loadReviewsPaged() {
var qs = listQs();
fetch("/api/reviews" + (qs ? "?" + qs : ""), { credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
reviewsAll = Array.isArray(data) ? data : [];
if (global.reviewCache) {
Object.keys(global.reviewCache).forEach(function (k) {
delete global.reviewCache[k];
});
} else {
global.reviewCache = {};
}
reviewsAll.forEach(function (r) {
global.reviewCache[r.id] = r;
});
reviewsPage = 0;
renderReviewsPage(false);
});
}
function showJournalCard() {
var card = $("journal-card");
if (card) card.classList.remove("hidden");
var hint = $("rr-journal-fill-hint");
if (hint) hint.style.display = "";
}
function hideJournalCard() {
var card = $("journal-card");
if (card) card.classList.add("hidden");
var hint = $("rr-journal-fill-hint");
if (hint) hint.style.display = "none";
}
function patchFillJournalFromTrade() {
var prev = global.fillJournalFromTrade;
if (typeof prev !== "function") return;
if (prev.__rrPatched) return;
global.fillJournalFromTrade = function (t) {
showJournalCard();
prev(t);
var hint = $("rr-journal-fill-hint");
if (hint) hint.style.display = "";
};
global.fillJournalFromTrade.__rrPatched = true;
}
function patchDeleteTradeRecord() {
var prev = global.deleteTradeRecord;
if (typeof prev !== "function") return;
if (prev.__rrPatched) return;
global.deleteTradeRecord = function (id) {
if (!confirm("确定删除这条交易记录?")) return;
fetch("/delete_trade_record/" + id, { method: "POST", credentials: "same-origin" })
.then(function (r) {
return r.json();
})
.then(function (data) {
if (data && data.ok) {
loadTradeRecords({ soft: true });
return;
}
if (typeof prev === "function") {
/* fallthrough reload */
}
global.location.href =
(global.location.pathname || "/records") + "?_ts=" + Date.now();
})
.catch(function () {
global.location.href =
(global.location.pathname || "/records") + "?_ts=" + Date.now();
});
};
global.deleteTradeRecord.__rrPatched = true;
}
function bindPagers() {
var tp = $("rr-trades-prev");
var tn = $("rr-trades-next");
var jp = $("rr-journals-prev");
var jn = $("rr-journals-next");
var rp = $("rr-reviews-prev");
var rn = $("rr-reviews-next");
var hideBtn = $("rr-journal-hide-btn");
if (tp) {
tp.addEventListener("click", function (ev) {
ev.preventDefault();
if (tradesPage <= 0) return;
tradesPage -= 1;
updatePager("trades");
loadTradeRecords({ soft: true });
});
}
if (tn) {
tn.addEventListener("click", function (ev) {
ev.preventDefault();
if (tradesPage + 1 >= tradesPages) return;
tradesPage += 1;
updatePager("trades");
loadTradeRecords({ soft: true });
});
}
if (jp) {
jp.addEventListener("click", function (ev) {
ev.preventDefault();
if (journalsPage <= 0) return;
journalsPage -= 1;
renderJournalsPage(true);
});
}
if (jn) {
jn.addEventListener("click", function (ev) {
ev.preventDefault();
if (journalsPage + 1 >= journalsPages) return;
journalsPage += 1;
renderJournalsPage(true);
});
}
if (rp) {
rp.addEventListener("click", function (ev) {
ev.preventDefault();
if (reviewsPage <= 0) return;
reviewsPage -= 1;
renderReviewsPage(true);
});
}
if (rn) {
rn.addEventListener("click", function (ev) {
ev.preventDefault();
if (reviewsPage + 1 >= reviewsPages) return;
reviewsPage += 1;
renderReviewsPage(true);
});
}
if (hideBtn) {
hideBtn.addEventListener("click", function (ev) {
ev.preventDefault();
hideJournalCard();
});
}
}
function init(opts) {
opts = opts || {};
if (!$("records-panel-root")) return;
if (booted) {
if (opts.refresh) {
loadTradeRecords({ soft: true });
loadJournalsPaged();
loadReviewsPaged();
}
patchFillJournalFromTrade();
patchDeleteTradeRecord();
return;
}
booted = true;
if (!global.journalCache) global.journalCache = {};
if (!global.reviewCache) global.reviewCache = {};
global.loadJournals = loadJournalsPaged;
global.loadReviews = loadReviewsPaged;
global.loadTradeRecords = loadTradeRecords;
patchFillJournalFromTrade();
patchDeleteTradeRecord();
bindPagers();
updatePager("trades");
updatePager("journals");
updatePager("reviews");
loadTradeRecords({ soft: false });
loadJournalsPaged();
loadReviewsPaged();
}
global.RecordsReviewPage = {
init: init,
loadTradeRecords: loadTradeRecords,
loadJournals: loadJournalsPaged,
loadReviews: loadReviewsPaged,
showJournalCard: showJournalCard,
hideJournalCard: hideJournalCard,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})(typeof window !== "undefined" ? window : globalThis);
+1
View File
@@ -58,6 +58,7 @@ def install_instance_theme_static(app) -> None:
"account_risk_badge.js": "application/javascript; charset=utf-8",
"instance_ui.js": "application/javascript; charset=utf-8",
"instance_records_mobile.js": "application/javascript; charset=utf-8",
"records_review_page.js": "application/javascript; charset=utf-8",
"ai_review_render.js": "application/javascript; charset=utf-8",
"form_submit_guard.js": "application/javascript; charset=utf-8",
"key_monitor_form.js": "application/javascript; charset=utf-8",
+54
View File
@@ -0,0 +1,54 @@
"""注册 /api/trade_records(三所共用)."""
from __future__ import annotations
from typing import Any, Callable
from flask import Flask, jsonify, request
def register_trade_records_api(
app: Flask,
*,
login_required: Callable,
get_db: Callable,
list_window_from_request: Callable[[], dict[str, Any]],
utc_window_to_bj_sql_strings: Callable[..., tuple[str, str]],
sql_list_time_field: Callable[..., str],
to_effective_trade_dict: Callable[[Any], dict[str, Any]],
filter_trade_records_excluding_miss: Callable[[list], list],
app_tz: Any,
) -> None:
from lib.instance.records_list_lib import list_trade_records_page
@app.route("/api/trade_records")
@login_required
def api_trade_records():
win = list_window_from_request()
start_bj, end_bj = utc_window_to_bj_sql_strings(
win["start_utc"], win["end_utc"], app_tz
)
tr_ts = sql_list_time_field("closed_at", "created_at", "opened_at")
try:
limit = int(request.args.get("limit") or 5)
except (TypeError, ValueError):
limit = 5
try:
offset = int(request.args.get("offset") or 0)
except (TypeError, ValueError):
offset = 0
conn = get_db()
try:
payload = list_trade_records_page(
conn,
start_bj,
end_bj,
tr_ts=tr_ts,
to_effective_fn=to_effective_trade_dict,
filter_fn=filter_trade_records_excluding_miss,
limit=limit,
offset=offset,
)
return jsonify(payload)
finally:
conn.close()
+44
View File
@@ -0,0 +1,44 @@
"""交易记录列表分页(三所 /records 共用)."""
from __future__ import annotations
from typing import Any, Callable
def list_trade_records_page(
conn: Any,
start_bj: str,
end_bj: str,
*,
tr_ts: str,
to_effective_fn: Callable[[Any], dict[str, Any]],
filter_fn: Callable[[list[dict[str, Any]]], list[dict[str, Any]]],
limit: int = 5,
offset: int = 0,
fetch_cap: int = 1000,
) -> dict[str, Any]:
"""按列表窗拉取、enrich、过滤「错过」后分页."""
limit = max(1, min(100, int(limit or 5)))
offset = max(0, int(offset or 0))
raw_records = conn.execute(
f"SELECT * FROM trade_records WHERE {tr_ts} >= ? AND {tr_ts} <= ? "
f"ORDER BY id DESC LIMIT ?",
(start_bj, end_bj, int(fetch_cap)),
).fetchall()
records = filter_fn([to_effective_fn(r) for r in raw_records])
total = len(records)
pages = max(1, (total + limit - 1) // limit) if total else 1
page = (offset // limit) + 1 if limit else 1
if page > pages:
page = pages
offset = (page - 1) * limit
items = records[offset : offset + limit]
return {
"ok": True,
"items": items,
"total": total,
"limit": limit,
"offset": offset,
"page": page,
"pages": pages,
}
@@ -95,6 +95,8 @@ function openAiInlineResultFullscreen(title, elementId){
const journalCache = {};
const reviewCache = {};
window.journalCache = journalCache;
window.reviewCache = reviewCache;
function formatJournalExitOneLine(o){
const t = (o.early_exit_trigger || "").trim();
@@ -579,7 +581,6 @@ function fillJournalFromTrade(t){
}
recomputeJournalRealRr();
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
alert("已填入下方复盘表单,请手动补充主观原因.");
}
function recomputeJournalRealRr(){
+2 -178
View File
@@ -302,185 +302,8 @@
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录</h2>
<div class="form-row" style="margin-bottom:10px;gap:8px">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input id="review-mode-toggle" type="checkbox">
修改/核对开关(开启后可编辑关键字段)
</label>
</div>
<div class="table-wrap">
<table>
<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>盈亏U</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td>{{ r.effective_entry_reason or '-' }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
{% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span>
{% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span>
{% else %}<span class="badge">{{ effective_result or '-' }}</span>{% endif %}
</td>
<td>
<button
type="button"
class="table-del"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='fillJournalFromTrade({{ {
"symbol": r.symbol,
"monitor_type": r.monitor_type,
"key_signal_type": r.key_signal_type or "",
"direction": r.direction,
"trigger_price": r.trigger_price,
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"risk_amount": r.risk_amount,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
>填入复盘</button>
<button
type="button"
class="table-del review-edit-btn"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='editTradeRecordReview({{ {
"id": r.id,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"miss_reason": r.effective_miss_reason,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
disabled
>核对修改</button>
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card full journal-card">
<h2>交易复盘记录上传(含截图)</h2>
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
{% from 'journal_form_fields.html' import journal_form_fields %}
{{ journal_form_fields(entry_reason_options, order_type_options) }}
{% from 'journal_upload_slots.html' import journal_upload_slots %}
{{ journal_upload_slots() }}
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true">
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线截止</label>
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
</select>
</div>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓,平仓与止损位</div>
<div class="mood-grid">
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
</div>
<textarea name="note" rows="2" placeholder="备注"></textarea>
<button type="submit" style="margin-top:8px">保存复盘记录</button>
</form>
</div>
<div class="card full review-card" id="review-card">
<div class="review-card-head">
<h2>AI复盘(按交易记录)</h2>
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button>
</div>
<div class="form-row">
<input type="date" id="day_date">
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button>
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
<input type="date" id="week_start">
<input type="date" id="week_end">
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button>
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
</div>
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none">
<div id="daily_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button>
</div>
</div>
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none">
<div id="weekly_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button>
</div>
</div>
<div class="panel-list" style="margin-top:10px">
<div class="panel-item">
<strong>交易复盘记录</strong>
<div id="journal-list"></div>
</div>
<div class="panel-item">
<strong>AI历史复盘</strong>
<div id="review-list"></div>
</div>
</div>
</div>
</div>
</div>
{% include 'records_panel.html' %}
{% endif %}
</div>
{% if page == 'env_config' %}
{% include 'env_config_panel.html' %}
{% endif %}
@@ -529,3 +352,4 @@
</div>
</div>
{% endif %}
</div>
+2 -1
View File
@@ -108,11 +108,12 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
<script src="/static/key_monitor_form.js?v=2"></script>
<script src="/static/instance_stats.js?v=4"></script>
{% include 'embed_boot_scripts.html' %}
<script src="/static/records_review_page.js?v=1"></script>
<script>
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
</script>
<script src="/static/instance_settings_prefs.js?v=13"></script>
<script src="/static/instance_live.js?v=5"></script>
<script src="/static/instance_embed.js?v=23"></script>
<script src="/static/instance_embed.js?v=24"></script>
</body>
</html>
+4 -179
View File
@@ -376,185 +376,8 @@
{% if page == 'records' %}
<div class="card full records-card">
<h2>交易记录</h2>
<div class="form-row" style="margin-bottom:10px;gap:8px">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input id="review-mode-toggle" type="checkbox">
修改/核对开关(开启后可编辑关键字段)
</label>
</div>
<div class="table-wrap">
<table>
<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>盈亏U</th><th>结果</th><th>操作</th></tr>
{% for r in record %}
<tr id="trade-row-{{ r.id }}">
{% set pnl_val = (r.pnl_amount or 0)|float %}
<td>{{ r.symbol }}</td>
<td>{{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}</td>
<td>{{ r.effective_entry_reason or '-' }}</td>
<td><span class="badge {{ 'direction-long' if r.direction == 'long' else 'direction-short' }}">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ price_fmt(r.symbol, r.trigger_price) }}</td>
{% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
{% set tp_show = r.effective_take_profit or r.take_profit %}
<td>{{ price_fmt(r.symbol, stop_show) }}</td>
<td>{{ price_fmt(r.symbol, tp_show) }}</td>
<td>{% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}</td>
<td>{{ r.leverage or '-' }}</td>
<td>{{ r.effective_hold_minutes or 0 }}</td>
<td>{{ (r.effective_opened_at or '-')[:16] }}</td>
<td>{{ (r.effective_closed_at or r.created_at or '-')[:16] }}</td>
{% set pnl_val = (r.effective_pnl_amount or 0)|float %}
<td><span class="{{ 'pnl-profit' if pnl_val > 0 else ('pnl-loss' if pnl_val < 0 else '') }}">{{ funds_fmt(r.effective_pnl_amount or 0) }}</span>{% if r.display_pnl_source == 'exchange' %}<span style="font-size:.68rem;color:#6ab88a"></span>{% elif r.display_pnl_source != 'reviewed' %}<span style="font-size:.68rem;color:#8892b0"></span>{% endif %}</td>
<td>
{% set effective_result = r.effective_result %}
{% if effective_result in ["止盈","保本止盈","移动止盈"] %}<span class="badge profit">{{ effective_result }}</span>
{% elif effective_result in ["止损","强制清仓","手动平仓"] %}<span class="badge loss">{{ effective_result }}</span>
{% elif effective_result == "时间平仓" %}<span class="badge miss">{{ effective_result }}</span>
{% else %}<span class="badge">{{ effective_result or '-' }}</span>{% endif %}
</td>
<td>
<button
type="button"
class="table-del"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='fillJournalFromTrade({{ {
"symbol": r.symbol,
"monitor_type": r.monitor_type,
"key_signal_type": r.key_signal_type or "",
"direction": r.direction,
"trigger_price": r.trigger_price,
"stop_loss": r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"risk_amount": r.risk_amount,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
>填入复盘</button>
<button
type="button"
class="table-del review-edit-btn"
style="background:#1f3a5a;color:#8fc8ff;margin-right:6px"
onclick='editTradeRecordReview({{ {
"id": r.id,
"opened_at": r.effective_opened_at,
"closed_at": r.effective_closed_at,
"stop_loss": r.effective_stop_loss or r.initial_stop_loss or r.stop_loss,
"take_profit": r.effective_take_profit or r.take_profit,
"pnl_amount": r.effective_pnl_amount,
"result": r.effective_result,
"miss_reason": r.effective_miss_reason,
"effective_entry_reason": r.effective_entry_reason or ""
}|tojson|safe }})'
disabled
>核对修改</button>
<button type="button" class="table-del" onclick="deleteTradeRecord({{ r.id }})">删除</button>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="card full journal-card">
<h2>交易复盘记录上传(含截图)</h2>
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
{% from 'journal_form_fields.html' import journal_form_fields %}
{{ journal_form_fields(entry_reason_options, order_type_options) }}
{% from 'journal_upload_slots.html' import journal_upload_slots %}
{{ journal_upload_slots() }}
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true">
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线截止</label>
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
</select>
</div>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓,平仓与止损位</div>
<div class="mood-grid">
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
</div>
<textarea name="note" rows="2" placeholder="备注"></textarea>
<button type="submit" style="margin-top:8px">保存复盘记录</button>
</form>
</div>
<div class="card full review-card" id="review-card">
<div class="review-card-head">
<h2>AI复盘(按交易记录)</h2>
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button>
</div>
<div class="form-row">
<input type="date" id="day_date">
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button>
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
<input type="date" id="week_start">
<input type="date" id="week_end">
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button>
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
</div>
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none">
<div id="daily_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button>
</div>
</div>
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none">
<div id="weekly_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button>
</div>
</div>
<div class="panel-list" style="margin-top:10px">
<div class="panel-item">
<strong>交易复盘记录</strong>
<div id="journal-list"></div>
</div>
<div class="panel-item">
<strong>AI历史复盘</strong>
<div id="review-list"></div>
</div>
</div>
</div>
</div>
</div>
{% include 'records_panel.html' %}
{% endif %}
</div>
{% if page == 'env_config' %}
{% include 'env_config_panel.html' %}
@@ -729,6 +552,8 @@ function openAiInlineResultFullscreen(title, elementId){
const journalCache = {};
const reviewCache = {};
window.journalCache = journalCache;
window.reviewCache = reviewCache;
function formatJournalExitOneLine(o){
const t = (o.early_exit_trigger || "").trim();
@@ -1213,7 +1038,6 @@ function fillJournalFromTrade(t){
}
recomputeJournalRealRr();
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
alert("已填入下方复盘表单,请手动补充主观原因.");
}
function recomputeJournalRealRr(){
@@ -2162,6 +1986,7 @@ setInterval(tickOrderHoldDurations, 1000);
tickOrderHoldDurations();
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
</script>
<script src="/static/records_review_page.js?v=1"></script>
<script>
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
</script>
+152
View File
@@ -0,0 +1,152 @@
{# 三所共用:交易记录(5/页) → 填入复盘出表单 → 交易复盘记录 / AI历史复盘 #}
<style>
.records-panel-wrap .rr-pager{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:.74rem}
.records-panel-wrap .rr-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease}
.records-panel-wrap .rr-trades-wrap{min-height:9.5rem}
.records-panel-wrap .journal-card.hidden{display:none!important}
.records-panel-wrap .rr-hint{font-size:.72rem;color:#8892b0;margin:0 0 8px}
</style>
<div class="records-panel-wrap" id="records-panel-root" style="grid-column:1/-1">
<div class="card full records-card">
<h2>交易记录</h2>
<p class="rr-hint">每页5条.点「填入复盘」打开下方复盘表单.</p>
<div class="form-row" style="margin-bottom:10px;gap:8px">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input id="review-mode-toggle" type="checkbox">
修改/核对开关(开启后可编辑关键字段)
</label>
</div>
<div class="table-wrap rr-trades-wrap" id="rr-trades-wrap">
<table id="rr-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>盈亏U</th><th>结果</th><th>操作</th>
</tr>
</thead>
<tbody id="rr-trades-tbody">
<tr><td colspan="15" class="muted">加载中…</td></tr>
</tbody>
</table>
</div>
<div class="rr-pager" id="rr-trades-pager">
<button type="button" class="btn-secondary" id="rr-trades-prev" style="font-size:.72rem;padding:2px 8px">上一页</button>
<span class="muted" id="rr-trades-page-label">第 1 / 1 页</span>
<button type="button" class="btn-secondary" id="rr-trades-next" style="font-size:.72rem;padding:2px 8px">下一页</button>
</div>
</div>
<div class="card full journal-card hidden" id="journal-card">
<div class="form-row" style="align-items:center;gap:8px;margin-bottom:6px">
<h2 style="margin:0;margin-right:auto">交易复盘记录上传(含截图)</h2>
<button type="button" class="btn-secondary" id="rr-journal-hide-btn" style="font-size:.76rem;padding:4px 10px">收起</button>
</div>
<p class="rr-hint" id="rr-journal-fill-hint" style="display:none">已从交易记录填入,请补充主观原因后保存.</p>
<form id="journal-form" action="/add_journal" method="post" enctype="multipart/form-data">
<input type="hidden" name="risk_amount_hint" id="risk-amount-hint">
<input type="hidden" name="entry_price_hint" id="entry-price-hint">
<input type="hidden" name="stop_loss_hint" id="stop-loss-hint">
<input type="hidden" name="exit_price_hint" id="exit-price-hint">
<input type="hidden" name="direction_hint" id="direction-hint">
{% from 'journal_form_fields.html' import journal_form_fields %}
{{ journal_form_fields(entry_reason_options, order_type_options) }}
{% from 'journal_upload_slots.html' import journal_upload_slots %}
{{ journal_upload_slots() }}
<div class="form-row journal-chart-options" style="flex-wrap:wrap;align-items:center">
<label style="display:flex;align-items:center;gap:6px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="journal_exchange_chart" value="true">
保存时自动生成 K 线图并作为截图
</label>
<label style="font-size:.82rem;color:#9aa">周期1</label>
<select name="journal_chart_tf1" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf1 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">周期2</label>
<select name="journal_chart_tf2" style="min-width:72px">
{% for tf in journal_chart_tf_choices %}
<option value="{{ tf }}" {% if tf == journal_chart_default_tf2 %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线数</label>
<select name="journal_chart_limit" style="min-width:72px">
{% for n in [100, 150, 200, 250, 300, 400, 500] %}
<option value="{{ n }}" {% if n == journal_chart_default_limit %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
<label style="font-size:.82rem;color:#9aa">K线截止</label>
<select name="journal_chart_anchor" id="journal-chart-anchor" style="min-width:96px" title="K线窗口右端对齐的时间">
<option value="close" {% if journal_chart_default_anchor == 'close' %}selected{% endif %}>平仓时间</option>
<option value="now" {% if journal_chart_default_anchor == 'now' %}selected{% endif %}>当前时间</option>
</select>
</div>
<div class="sub" id="journal-chart-anchor-hint" style="font-size:.72rem;color:#8892b0;margin-top:2px;margin-bottom:0">双周期上下排列;截止=平仓时间:开仓前背景至平仓;截止=当前时间:最近 N 根至此刻(可看平仓后走势);标注开仓,平仓与止损位</div>
<div class="mood-grid">
<label><input type="checkbox" name="mood_issues" value="怕踏空">怕踏空</label>
<label><input type="checkbox" name="mood_issues" value="报复开仓">报复开仓</label>
<label><input type="checkbox" name="mood_issues" value="盈利飘了">盈利飘了</label>
<label><input type="checkbox" name="mood_issues" value="拿不住单">拿不住单</label>
<label><input type="checkbox" name="mood_issues" value="扛单">扛单</label>
<label><input type="checkbox" name="mood_issues" value="重仓违规">重仓违规</label>
</div>
<textarea name="note" rows="2" placeholder="备注"></textarea>
<button type="submit" style="margin-top:8px">保存复盘记录</button>
</form>
</div>
<div class="card full review-card" id="review-card">
<div class="review-card-head">
<h2>AI复盘(按交易记录)</h2>
<button type="button" class="review-card-fs-btn" id="review-card-fs-btn" onclick="toggleReviewCardFullscreen()">全屏</button>
</div>
<div class="form-row">
<input type="date" id="day_date">
<button type="button" id="gen-daily-btn" onclick="genDaily()">生成日复盘</button>
<button type="button" onclick="exportDailyBundleMd()" style="background:#1f3a5a">导出当日日复盘MD</button>
<input type="date" id="week_start">
<input type="date" id="week_end">
<button type="button" id="gen-weekly-btn" onclick="genWeekly()">生成周复盘</button>
<button type="button" onclick="exportWeeklyBundleMd()" style="background:#1f3a5a">导出当周复盘MD</button>
</div>
<div class="ai-result-wrap" id="daily_result_wrap" style="display:none">
<div id="daily_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('日复盘结果', 'daily_result')">全屏查看</button>
</div>
</div>
<div class="ai-result-wrap" id="weekly_result_wrap" style="display:none">
<div id="weekly_result" class="ai-result"></div>
<div class="ai-result-toolbar">
<button type="button" class="btn-fs" onclick="openAiInlineResultFullscreen('周复盘结果', 'weekly_result')">全屏查看</button>
</div>
</div>
</div>
<div class="card full" id="rr-journals-card">
<h3 style="margin-top:0">交易复盘记录</h3>
<p class="rr-hint">已保存的复盘(每页5条).</p>
<div id="journal-list-wrap" class="rr-list-wrap">
<div id="journal-list"></div>
</div>
<div class="rr-pager" id="rr-journals-pager">
<button type="button" class="btn-secondary" id="rr-journals-prev" style="font-size:.72rem;padding:2px 8px">上一页</button>
<span class="muted" id="rr-journals-page-label">第 1 / 1 页</span>
<button type="button" class="btn-secondary" id="rr-journals-next" style="font-size:.72rem;padding:2px 8px">下一页</button>
</div>
</div>
<div class="card full" id="rr-ai-history-card">
<h3 style="margin-top:0">AI历史复盘</h3>
<p class="rr-hint">日/周 AI 复盘历史(每页5条).</p>
<div id="review-list-wrap" class="rr-list-wrap">
<div id="review-list"></div>
</div>
<div class="rr-pager" id="rr-reviews-pager">
<button type="button" class="btn-secondary" id="rr-reviews-prev" style="font-size:.72rem;padding:2px 8px">上一页</button>
<span class="muted" id="rr-reviews-page-label">第 1 / 1 页</span>
<button type="button" class="btn-secondary" id="rr-reviews-next" style="font-size:.72rem;padding:2px 8px">下一页</button>
</div>
</div>
</div>
+83
View File
@@ -0,0 +1,83 @@
"""records_list_lib pagination."""
from __future__ import annotations
import sqlite3
import unittest
from lib.instance.records_list_lib import list_trade_records_page
from lib.trade.trade_result_lib import filter_trade_records_excluding_miss
def _to_effective(row):
d = dict(row)
d["effective_result"] = d.get("result")
d["effective_pnl_amount"] = d.get("pnl_amount")
return d
class RecordsListLibTest(unittest.TestCase):
def setUp(self):
self.conn = sqlite3.connect(":memory:")
self.conn.row_factory = sqlite3.Row
self.conn.execute(
"""
CREATE TABLE trade_records (
id INTEGER PRIMARY KEY,
closed_at TEXT,
created_at TEXT,
opened_at TEXT,
result TEXT,
pnl_amount REAL
)
"""
)
for i in range(12):
self.conn.execute(
"INSERT INTO trade_records(id, closed_at, created_at, opened_at, result, pnl_amount) "
"VALUES (?,?,?,?,?,?)",
(i + 1, f"2026-07-1{i % 9}-10:00:00", None, None, "止盈", 1.0),
)
self.conn.execute(
"INSERT INTO trade_records(id, closed_at, created_at, opened_at, result, pnl_amount) "
"VALUES (?,?,?,?,?,?)",
(99, "2026-07-15-10:00:00", None, None, "错过", 0),
)
self.conn.commit()
def tearDown(self):
self.conn.close()
def test_pages_exclude_miss(self):
out = list_trade_records_page(
self.conn,
"2026-07-01",
"2026-07-31",
tr_ts="COALESCE(closed_at, created_at, opened_at)",
to_effective_fn=_to_effective,
filter_fn=filter_trade_records_excluding_miss,
limit=5,
offset=0,
)
self.assertTrue(out["ok"])
self.assertEqual(out["total"], 12)
self.assertEqual(out["pages"], 3)
self.assertEqual(len(out["items"]), 5)
def test_second_page(self):
out = list_trade_records_page(
self.conn,
"2026-07-01",
"2026-07-31",
tr_ts="COALESCE(closed_at, created_at, opened_at)",
to_effective_fn=_to_effective,
filter_fn=filter_trade_records_excluding_miss,
limit=5,
offset=5,
)
self.assertEqual(out["page"], 2)
self.assertEqual(len(out["items"]), 5)
if __name__ == "__main__":
unittest.main()