diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py
index 5d1b334..25a25cc 100644
--- a/crypto_monitor_binance/app.py
+++ b/crypto_monitor_binance/app.py
@@ -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():
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py
index 476e8b6..735a0c1 100644
--- a/crypto_monitor_gate/app.py
+++ b/crypto_monitor_gate/app.py
@@ -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():
diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index b2e571e..3c114e1 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -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():
diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js
index b6f5c9e..13f378a 100644
--- a/lib/common/static/instance_embed.js
+++ b/lib/common/static/instance_embed.js
@@ -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") {
diff --git a/lib/common/static/records_review_page.js b/lib/common/static/records_review_page.js
new file mode 100644
index 0000000..689d91b
--- /dev/null
+++ b/lib/common/static/records_review_page.js
@@ -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, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+ }
+
+ 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 '' + esc(er) + " ";
+ }
+ if (["止损", "强制清仓", "手动平仓"].indexOf(er) >= 0) {
+ return '' + esc(er) + " ";
+ }
+ if (er === "时间平仓") return '' + esc(er) + " ";
+ return '' + esc(er || "-") + " ";
+ }
+
+ 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 = '
暂无交易记录 ';
+ 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 = '所 ';
+ } else if (t.display_pnl_source !== "reviewed") {
+ pnlSrc = '估 ';
+ }
+ 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 (
+ '' +
+ "" +
+ esc(t.symbol) +
+ " " +
+ "" +
+ mon +
+ " " +
+ "" +
+ esc(t.effective_entry_reason || "-") +
+ " " +
+ '' +
+ dirTxt +
+ " " +
+ "" +
+ fmtNum(t.trigger_price, 4) +
+ " " +
+ "" +
+ fmtNum(stopShow, 4) +
+ " " +
+ "" +
+ fmtNum(tpShow, 4) +
+ " " +
+ "" +
+ margin +
+ " " +
+ "" +
+ esc(t.leverage != null ? t.leverage : "-") +
+ " " +
+ "" +
+ esc(t.effective_hold_minutes || 0) +
+ " " +
+ "" +
+ fmtTime(t.effective_opened_at) +
+ " " +
+ "" +
+ fmtTime(t.effective_closed_at || t.created_at) +
+ " " +
+ '' +
+ fmtNum(pnl, 2) +
+ " " +
+ pnlSrc +
+ " " +
+ "" +
+ resultBadge(t.effective_result) +
+ " " +
+ "" +
+ '填入复盘 ' +
+ '核对修改 ' +
+ '删除 ' +
+ " " +
+ " "
+ );
+ })
+ .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 = '加载中… ';
+ }
+ 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 = '加载失败 ';
+ 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 = '加载失败 ';
+ 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 || "暂无数据
";
+ } else {
+ box.innerHTML = "暂无数据
";
+ }
+ 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 = "暂无数据
";
+ 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 +=
+ '' +
+ "
" +
+ (r.review_type === "daily" ? "日复盘" : "周复盘") +
+ " | " +
+ esc(r.target_date) +
+ "
" +
+ '
' +
+ esc(r.created_at || "") +
+ "
" +
+ '
' +
+ esc(shortText || "(空)") +
+ "
" +
+ '
' +
+ '
查看 " +
+ '
全屏 " +
+ '
导出MD ' +
+ '
删除 " +
+ "
";
+ });
+ 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);
diff --git a/lib/hub/hub_bridge.py b/lib/hub/hub_bridge.py
index ed4e817..ae62133 100644
--- a/lib/hub/hub_bridge.py
+++ b/lib/hub/hub_bridge.py
@@ -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",
diff --git a/lib/instance/records_api_register.py b/lib/instance/records_api_register.py
new file mode 100644
index 0000000..fb7d541
--- /dev/null
+++ b/lib/instance/records_api_register.py
@@ -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()
diff --git a/lib/instance/records_list_lib.py b/lib/instance/records_list_lib.py
new file mode 100644
index 0000000..dbda60d
--- /dev/null
+++ b/lib/instance/records_list_lib.py
@@ -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,
+ }
diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html
index 990312b..b8fd8f5 100644
--- a/lib/instance/templates/embed_boot_scripts.html
+++ b/lib/instance/templates/embed_boot_scripts.html
@@ -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(){
diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html
index 9ba7d14..2e135ea 100644
--- a/lib/instance/templates/embed_page_fragment.html
+++ b/lib/instance/templates/embed_page_fragment.html
@@ -302,185 +302,8 @@
{% if page == 'records' %}
-
-
交易记录
-
-
-
- 修改/核对开关(开启后可编辑关键字段)
-
-
-
-
- 品种 下单类型 开仓类型 方向 成交 止损(开仓) 止盈 基数 杠杆 持仓分钟 开仓时间(北京) 平仓时间(北京) 盈亏U 结果 操作
- {% for r in record %}
-
- {% set pnl_val = (r.pnl_amount or 0)|float %}
- {{ r.symbol }}
- {{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}
- {{ r.effective_entry_reason or '-' }}
- {{ '做多' if r.direction == 'long' else '做空' }}
- {{ price_fmt(r.symbol, r.trigger_price) }}
- {% 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 %}
- {{ price_fmt(r.symbol, stop_show) }}
- {{ price_fmt(r.symbol, tp_show) }}
- {% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}
- {{ r.leverage or '-' }}
- {{ r.effective_hold_minutes or 0 }}
- {{ (r.effective_opened_at or '-')[:16] }}
- {{ (r.effective_closed_at or r.created_at or '-')[:16] }}
- {% set pnl_val = (r.effective_pnl_amount or 0)|float %}
- {{ funds_fmt(r.effective_pnl_amount or 0) }} {% if r.display_pnl_source == 'exchange' %}所 {% elif r.display_pnl_source != 'reviewed' %}估 {% endif %}
-
- {% set effective_result = r.effective_result %}
- {% if effective_result in ["止盈","保本止盈","移动止盈"] %}{{ effective_result }}
- {% elif effective_result in ["止损","强制清仓","手动平仓"] %}{{ effective_result }}
- {% elif effective_result == "时间平仓" %}{{ effective_result }}
- {% else %}{{ effective_result or '-' }} {% endif %}
-
-
- 填入复盘
- 核对修改
- 删除
-
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
+ {% include 'records_panel.html' %}
{% endif %}
-
{% if page == 'env_config' %}
{% include 'env_config_panel.html' %}
{% endif %}
@@ -529,3 +352,4 @@
{% endif %}
+
diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html
index c9dbebe..b8af073 100644
--- a/lib/instance/templates/embed_shell.html
+++ b/lib/instance/templates/embed_shell.html
@@ -108,11 +108,12 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
{% include 'embed_boot_scripts.html' %}
+
-
+