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:
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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, ">")
|
||||
.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 '<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);
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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(){
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user