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
|
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")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
|||||||
@@ -9292,6 +9292,21 @@ def api_journal_upload_slot():
|
|||||||
return jsonify(payload), code
|
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")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
|||||||
@@ -8980,6 +8980,21 @@ def api_journal_upload_slot():
|
|||||||
return jsonify(payload), code
|
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")
|
@app.route("/api/journals")
|
||||||
@login_required
|
@login_required
|
||||||
def api_journals():
|
def api_journals():
|
||||||
|
|||||||
@@ -103,8 +103,12 @@
|
|||||||
global.initStrategyRollForm();
|
global.initStrategyRollForm();
|
||||||
}
|
}
|
||||||
if (tab === "records") {
|
if (tab === "records") {
|
||||||
if (!revisit && typeof global.loadJournals === "function") global.loadJournals();
|
if (global.RecordsReviewPage && typeof global.RecordsReviewPage.init === "function") {
|
||||||
if (!revisit && typeof global.loadReviews === "function") global.loadReviews();
|
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") {
|
if (global.InstanceTheme && typeof global.InstanceTheme.initReviewEditModeSync === "function") {
|
||||||
global.InstanceTheme.initReviewEditModeSync();
|
global.InstanceTheme.initReviewEditModeSync();
|
||||||
} else if (typeof global.toggleReviewMode === "function") {
|
} 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",
|
"account_risk_badge.js": "application/javascript; charset=utf-8",
|
||||||
"instance_ui.js": "application/javascript; charset=utf-8",
|
"instance_ui.js": "application/javascript; charset=utf-8",
|
||||||
"instance_records_mobile.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",
|
"ai_review_render.js": "application/javascript; charset=utf-8",
|
||||||
"form_submit_guard.js": "application/javascript; charset=utf-8",
|
"form_submit_guard.js": "application/javascript; charset=utf-8",
|
||||||
"key_monitor_form.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 journalCache = {};
|
||||||
const reviewCache = {};
|
const reviewCache = {};
|
||||||
|
window.journalCache = journalCache;
|
||||||
|
window.reviewCache = reviewCache;
|
||||||
|
|
||||||
function formatJournalExitOneLine(o){
|
function formatJournalExitOneLine(o){
|
||||||
const t = (o.early_exit_trigger || "").trim();
|
const t = (o.early_exit_trigger || "").trim();
|
||||||
@@ -579,7 +581,6 @@ function fillJournalFromTrade(t){
|
|||||||
}
|
}
|
||||||
recomputeJournalRealRr();
|
recomputeJournalRealRr();
|
||||||
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
|
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
|
||||||
alert("已填入下方复盘表单,请手动补充主观原因.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recomputeJournalRealRr(){
|
function recomputeJournalRealRr(){
|
||||||
|
|||||||
@@ -302,185 +302,8 @@
|
|||||||
|
|
||||||
|
|
||||||
{% if page == 'records' %}
|
{% if page == 'records' %}
|
||||||
<div class="card full records-card">
|
{% include 'records_panel.html' %}
|
||||||
<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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% if page == 'env_config' %}
|
{% if page == 'env_config' %}
|
||||||
{% include 'env_config_panel.html' %}
|
{% include 'env_config_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -529,3 +352,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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/key_monitor_form.js?v=2"></script>
|
||||||
<script src="/static/instance_stats.js?v=4"></script>
|
<script src="/static/instance_stats.js?v=4"></script>
|
||||||
{% include 'embed_boot_scripts.html' %}
|
{% include 'embed_boot_scripts.html' %}
|
||||||
|
<script src="/static/records_review_page.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/instance_settings_prefs.js?v=13"></script>
|
<script src="/static/instance_settings_prefs.js?v=13"></script>
|
||||||
<script src="/static/instance_live.js?v=5"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -376,185 +376,8 @@
|
|||||||
|
|
||||||
|
|
||||||
{% if page == 'records' %}
|
{% if page == 'records' %}
|
||||||
<div class="card full records-card">
|
{% include 'records_panel.html' %}
|
||||||
<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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if page == 'env_config' %}
|
{% if page == 'env_config' %}
|
||||||
{% include 'env_config_panel.html' %}
|
{% include 'env_config_panel.html' %}
|
||||||
@@ -729,6 +552,8 @@ function openAiInlineResultFullscreen(title, elementId){
|
|||||||
|
|
||||||
const journalCache = {};
|
const journalCache = {};
|
||||||
const reviewCache = {};
|
const reviewCache = {};
|
||||||
|
window.journalCache = journalCache;
|
||||||
|
window.reviewCache = reviewCache;
|
||||||
|
|
||||||
function formatJournalExitOneLine(o){
|
function formatJournalExitOneLine(o){
|
||||||
const t = (o.early_exit_trigger || "").trim();
|
const t = (o.early_exit_trigger || "").trim();
|
||||||
@@ -1213,7 +1038,6 @@ function fillJournalFromTrade(t){
|
|||||||
}
|
}
|
||||||
recomputeJournalRealRr();
|
recomputeJournalRealRr();
|
||||||
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
|
if(typeof syncEarlyExitNoteRequired === "function") syncEarlyExitNoteRequired();
|
||||||
alert("已填入下方复盘表单,请手动补充主观原因.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recomputeJournalRealRr(){
|
function recomputeJournalRealRr(){
|
||||||
@@ -2162,6 +1986,7 @@ setInterval(tickOrderHoldDurations, 1000);
|
|||||||
tickOrderHoldDurations();
|
tickOrderHoldDurations();
|
||||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/static/records_review_page.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
</script>
|
</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