Split journal order type from entry reason and fix review edit toggle in embed shell.

Adds order_type to journal uploads, removes legacy swing/trend labels from entry reason options, and ensures review-edit buttons sync when the records tab loads dynamically.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-09 09:27:03 +08:00
parent 4abe8a6ca9
commit d3a44b23db
17 changed files with 325 additions and 85 deletions
+22 -11
View File
@@ -67,14 +67,16 @@ from lib.key_monitor.false_breakout_key_monitor_lib import (
storage_bounds_from_key_price,
)
from lib.strategy.strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS,
JOURNAL_ORDER_TYPE_OPTIONS,
apply_order_monitor_source_labels,
entry_reason_for_monitor_type,
handoff_trade_miss_reason,
normalize_journal_order_type,
order_monitor_source_type,
trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
)
from lib.instance.journal_form_lib import normalize_journal_entry_reason
from lib.instance.journal_images_lib import (
collect_journal_slot_images,
enrich_journal_api_item,
@@ -177,7 +179,7 @@ from lib.trade.position_sizing_lib import (
from lib.trade.trade_policy_lib import load_trade_policy
from lib.trade.entry_model_lib import (
build_intraday_entry_reason_options,
build_trend_div_entry_reason_options,
build_journal_entry_reason_options,
enrich_entry_model_display,
hub_meta_entry_context,
migrate_entry_model_columns,
@@ -1123,8 +1125,8 @@ EARLY_EXIT_TRIGGERS = (
"其他",
)
# 趋势户:大分歧A/B/小分歧 + 策略(关键位本实例关闭)
ENTRY_REASON_OPTIONS = build_trend_div_entry_reason_options(STRATEGY_ENTRY_REASON_OPTIONS)
# 趋势户:复盘开仓类型仅 entry model;策略/风格项已拆至下单类型
ENTRY_REASON_OPTIONS = build_journal_entry_reason_options()
STATS_SEGMENT_DEFS = (
("all", "全部交易", {"segment": "all"}),
@@ -1137,9 +1139,8 @@ STATS_SEGMENT_DEFS = (
("key_trigger", "关键位触价开仓", {"segment": "key_trigger"}),
)
def normalize_entry_reason(raw, custom_text=None):
from lib.trade.entry_model_lib import normalize_review_entry_reason
return normalize_review_entry_reason(raw, ENTRY_REASON_OPTIONS)
del custom_text
return normalize_journal_entry_reason(raw, ENTRY_REASON_OPTIONS, allow_legacy=True)
def entry_reason_valid_for_storage(s):
@@ -1429,6 +1430,9 @@ def init_db():
try:
c.execute("ALTER TABLE journal_entries ADD COLUMN images_json TEXT")
except: pass
try:
c.execute("ALTER TABLE journal_entries ADD COLUMN order_type TEXT")
except: pass
try:
c.execute("ALTER TABLE key_monitors ADD COLUMN direction TEXT DEFAULT 'long'")
except: pass
@@ -7326,6 +7330,7 @@ def render_main_page(page="trade", embed_mode=None):
trend_manual_count=trend_manual_entry_reason_count(TRADE_POLICY),
)
),
order_type_options=list(JOURNAL_ORDER_TYPE_OPTIONS),
key_auto_order_enabled=KEY_AUTO_ORDER_ENABLED,
journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES,
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
@@ -9256,7 +9261,13 @@ def del_order(id):
@login_required
def add_journal():
d = request.form
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"))
order_type_norm = normalize_journal_order_type(d.get("order_type"))
if not order_type_norm:
flash("请选择下单类型")
return _redirect_records()
entry_reason_norm = normalize_journal_entry_reason(
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
)
if not entry_reason_norm:
flash("请选择开仓类型")
return _redirect_records()
@@ -9349,11 +9360,11 @@ def add_journal():
conn = get_db()
conn.execute(
"""INSERT INTO journal_entries
(id, open_datetime, close_datetime, hold_duration, coin, tf, pnl, entry_reason, exit_reason,
(id, open_datetime, close_datetime, hold_duration, coin, tf, pnl, order_type, entry_reason, exit_reason,
expect_rr, real_rr, early_exit, early_exit_reason, early_exit_trigger, early_exit_note,
mood_score, mood_ai_score, mood_ai_comment, mood_issues, post_breakeven_stare,
new_trade_while_occupied, note, image, images_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
entry_id,
normalize_bj_datetime_storage(d.get("open_datetime")),
@@ -9361,7 +9372,7 @@ def add_journal():
hold_duration,
d.get("coin"),
d.get("tf"),
d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text,
d.get("pnl"), order_type_norm, entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text,
early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note,
None, None, None, mood_issues,
d.get("post_breakeven_stare"), None, d.get("note"), image_filename,
+19 -7
View File
@@ -68,14 +68,17 @@ from lib.key_monitor.false_breakout_key_monitor_lib import (
storage_bounds_from_key_price,
)
from lib.strategy.strategy_trade_labels import (
JOURNAL_ORDER_TYPE_OPTIONS,
STRATEGY_ENTRY_REASON_OPTIONS,
apply_order_monitor_source_labels,
entry_reason_for_monitor_type,
handoff_trade_miss_reason,
normalize_journal_order_type,
order_monitor_source_type,
trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
)
from lib.instance.journal_form_lib import normalize_journal_entry_reason
from lib.instance.journal_images_lib import (
collect_journal_slot_images,
enrich_journal_api_item,
@@ -1127,9 +1130,8 @@ STATS_SEGMENT_DEFS = (
("key_trigger", "关键位触价开仓", {"segment": "key_trigger"}),
)
def normalize_entry_reason(raw, custom_text=None):
from lib.trade.entry_model_lib import normalize_review_entry_reason
return normalize_review_entry_reason(raw, ENTRY_REASON_OPTIONS)
del custom_text
return normalize_journal_entry_reason(raw, ENTRY_REASON_OPTIONS, allow_legacy=True)
def entry_reason_valid_for_storage(s):
@@ -1424,6 +1426,9 @@ def init_db():
try:
c.execute("ALTER TABLE journal_entries ADD COLUMN images_json TEXT")
except: pass
try:
c.execute("ALTER TABLE journal_entries ADD COLUMN order_type TEXT")
except: pass
try:
c.execute("ALTER TABLE key_monitors ADD COLUMN direction TEXT DEFAULT 'long'")
except: pass
@@ -7122,6 +7127,7 @@ def render_main_page(page="trade", embed_mode=None):
trend_manual_count=trend_manual_entry_reason_count(TRADE_POLICY),
)
),
order_type_options=list(JOURNAL_ORDER_TYPE_OPTIONS),
key_auto_order_enabled=KEY_AUTO_ORDER_ENABLED,
journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES,
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
@@ -9122,7 +9128,13 @@ def del_order(id):
@login_required
def add_journal():
d = request.form
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"))
order_type_norm = normalize_journal_order_type(d.get("order_type"))
if not order_type_norm:
flash("请选择下单类型")
return _redirect_records()
entry_reason_norm = normalize_journal_entry_reason(
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
)
if not entry_reason_norm:
flash("请选择开仓类型")
return _redirect_records()
@@ -9215,11 +9227,11 @@ def add_journal():
conn = get_db()
conn.execute(
"""INSERT INTO journal_entries
(id, open_datetime, close_datetime, hold_duration, coin, tf, pnl, entry_reason, exit_reason,
(id, open_datetime, close_datetime, hold_duration, coin, tf, pnl, order_type, entry_reason, exit_reason,
expect_rr, real_rr, early_exit, early_exit_reason, early_exit_trigger, early_exit_note,
mood_score, mood_ai_score, mood_ai_comment, mood_issues, post_breakeven_stare,
new_trade_while_occupied, note, image, images_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
entry_id,
normalize_bj_datetime_storage(d.get("open_datetime")),
@@ -9227,7 +9239,7 @@ def add_journal():
hold_duration,
d.get("coin"),
d.get("tf"),
d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text,
d.get("pnl"), order_type_norm, entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text,
early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note,
None, None, None, mood_issues,
d.get("post_breakeven_stare"), None, d.get("note"), image_filename,
+22 -11
View File
@@ -67,14 +67,16 @@ from lib.key_monitor.false_breakout_key_monitor_lib import (
storage_bounds_from_key_price,
)
from lib.strategy.strategy_trade_labels import (
STRATEGY_ENTRY_REASON_OPTIONS,
JOURNAL_ORDER_TYPE_OPTIONS,
apply_order_monitor_source_labels,
entry_reason_for_monitor_type,
handoff_trade_miss_reason,
normalize_journal_order_type,
order_monitor_source_type,
trade_record_monitor_type as resolve_trade_record_monitor_type,
trend_plan_id_from_monitor_row,
)
from lib.instance.journal_form_lib import normalize_journal_entry_reason
from lib.exchange.okx_orders_lib import cancel_okx_all_open_orders, fetch_okx_all_open_orders
from lib.instance.journal_images_lib import (
collect_journal_slot_images,
@@ -175,7 +177,7 @@ from lib.trade.position_sizing_lib import (
from lib.trade.trade_policy_lib import load_trade_policy
from lib.trade.entry_model_lib import (
build_intraday_entry_reason_options,
build_trend_div_entry_reason_options,
build_journal_entry_reason_options,
enrich_entry_model_display,
hub_meta_entry_context,
migrate_entry_model_columns,
@@ -1134,8 +1136,8 @@ EARLY_EXIT_TRIGGERS = (
"其他",
)
# 趋势户:大分歧A/B/小分歧 + 策略(关键位本实例关闭)
ENTRY_REASON_OPTIONS = build_trend_div_entry_reason_options(STRATEGY_ENTRY_REASON_OPTIONS)
# 趋势户:复盘开仓类型仅 entry model;策略/风格项已拆至下单类型
ENTRY_REASON_OPTIONS = build_journal_entry_reason_options()
STATS_SEGMENT_DEFS = (
("all", "全部交易", {"segment": "all"}),
@@ -1148,9 +1150,8 @@ STATS_SEGMENT_DEFS = (
("key_trigger", "关键位触价开仓", {"segment": "key_trigger"}),
)
def normalize_entry_reason(raw, custom_text=None):
from lib.trade.entry_model_lib import normalize_review_entry_reason
return normalize_review_entry_reason(raw, ENTRY_REASON_OPTIONS)
del custom_text
return normalize_journal_entry_reason(raw, ENTRY_REASON_OPTIONS, allow_legacy=True)
def normalize_early_exit_trigger(raw):
@@ -1430,6 +1431,9 @@ def init_db():
try:
c.execute("ALTER TABLE journal_entries ADD COLUMN images_json TEXT")
except: pass
try:
c.execute("ALTER TABLE journal_entries ADD COLUMN order_type TEXT")
except: pass
try:
c.execute("ALTER TABLE key_monitors ADD COLUMN direction TEXT DEFAULT 'long'")
except: pass
@@ -6722,6 +6726,7 @@ def render_main_page(page="trade", embed_mode=None):
trend_manual_count=trend_manual_entry_reason_count(TRADE_POLICY),
)
),
order_type_options=list(JOURNAL_ORDER_TYPE_OPTIONS),
key_auto_order_enabled=KEY_AUTO_ORDER_ENABLED,
journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES,
journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1,
@@ -8776,7 +8781,13 @@ def del_order(id):
@login_required
def add_journal():
d = request.form
entry_reason_norm = normalize_entry_reason(d.get("entry_reason"))
order_type_norm = normalize_journal_order_type(d.get("order_type"))
if not order_type_norm:
flash("请选择下单类型")
return _redirect_records()
entry_reason_norm = normalize_journal_entry_reason(
d.get("entry_reason"), ENTRY_REASON_OPTIONS, allow_legacy=False
)
if not entry_reason_norm:
flash("请选择开仓类型")
return _redirect_records()
@@ -8869,11 +8880,11 @@ def add_journal():
conn = get_db()
conn.execute(
"""INSERT INTO journal_entries
(id, open_datetime, close_datetime, hold_duration, coin, tf, pnl, entry_reason, exit_reason,
(id, open_datetime, close_datetime, hold_duration, coin, tf, pnl, order_type, entry_reason, exit_reason,
expect_rr, real_rr, early_exit, early_exit_reason, early_exit_trigger, early_exit_note,
mood_score, mood_ai_score, mood_ai_comment, mood_issues, post_breakeven_stare,
new_trade_while_occupied, note, image, images_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
entry_id,
normalize_bj_datetime_storage(d.get("open_datetime")),
@@ -8881,7 +8892,7 @@ def add_journal():
hold_duration,
d.get("coin"),
d.get("tf"),
d.get("pnl"), entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text,
d.get("pnl"), order_type_norm, entry_reason_norm, exit_reason_stored, d.get("expect_rr"), real_rr_text,
early_exit_raw, early_exit_reason_saved, early_exit_trigger, early_exit_note,
None, None, None, mood_issues,
d.get("post_breakeven_stare"), None, d.get("note"), image_filename,
+8 -4
View File
@@ -100,10 +100,14 @@
if (!revisit && tab === "strategy" && typeof global.initStrategyRollForm === "function") {
global.initStrategyRollForm();
}
if (!revisit && tab === "records") {
if (typeof global.loadJournals === "function") global.loadJournals();
if (typeof global.loadReviews === "function") global.loadReviews();
if (typeof global.toggleReviewMode === "function") global.toggleReviewMode();
if (tab === "records") {
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") {
global.toggleReviewMode();
}
}
if (!revisit && tab === "stats") {
if (typeof global.initStatsSegmentFromUrl === "function") global.initStatsSegmentFromUrl();
+2
View File
@@ -1999,6 +1999,7 @@ html[data-theme="light"] .symbol-live-price--ok {
.journal-card .journal-form-row2 {
grid-template-columns:
minmax(6.5rem, 0.85fr)
minmax(7.5rem, 1.15fr)
minmax(7rem, 1fr)
minmax(0, 1.35fr)
@@ -2006,6 +2007,7 @@ html[data-theme="light"] .symbol-live-price--ok {
margin-bottom: 8px;
}
.journal-card .journal-form-row2 select[name="order_type"],
.journal-card .journal-form-row2 select[name="entry_reason"] {
font-size: 0.8rem;
line-height: 1.35;
+10 -6
View File
@@ -290,7 +290,7 @@
apply(data.theme, { skipStore: true });
}
/** 交易记录页:核对开关与按钮 disabled 保持同步(iframe 软导航/表单恢复后不触发 change) */
/** 交易记录页:核对开关与按钮 disabled 保持同步(iframe 软导航后动态挂载的 toggle) */
function syncReviewEditButtons() {
const toggle = document.getElementById("review-mode-toggle");
if (!toggle) return;
@@ -301,13 +301,17 @@
}
function initReviewEditModeSync() {
const toggle = document.getElementById("review-mode-toggle");
if (!toggle) return;
if (toggle.dataset.instReviewModeBound !== "1") {
toggle.dataset.instReviewModeBound = "1";
toggle.addEventListener("input", () => {
if (!global.__instReviewModeBound) {
global.__instReviewModeBound = true;
const onToggle = () => {
if (typeof global.toggleReviewMode === "function") global.toggleReviewMode();
else syncReviewEditButtons();
};
document.addEventListener("change", (ev) => {
if (ev.target && ev.target.id === "review-mode-toggle") onToggle();
});
document.addEventListener("input", (ev) => {
if (ev.target && ev.target.id === "review-mode-toggle") onToggle();
});
}
const run = () => {
+21 -18
View File
@@ -38,6 +38,7 @@
`平仓时间:${escapeHtml(o.close_datetime || "-")}`,
`持仓时长:${escapeHtml(o.hold_duration || "-")}`,
`盈亏:${formatPnlSpan(o.pnl)}`,
`下单类型:${escapeHtml(o.order_type || "无")}`,
`开仓类型:${escapeHtml(o.entry_reason || "无")}`,
`平仓/离场:${escapeHtml(exitText)}`,
`预期RR:${escapeHtml(o.expect_rr || "-")}`,
@@ -198,27 +199,28 @@
function parseTradeRecordRow(tr) {
const cells = tr.querySelectorAll("td");
if (cells.length < 14) return null;
const dirBadge = cells[2].querySelector(".badge");
if (cells.length < 15) return null;
const dirBadge = cells[3].querySelector(".badge");
return {
rowId: tr.id,
symbol: cells[0].textContent.trim(),
type: cells[1].textContent.trim(),
directionHtml: (dirBadge ? dirBadge.outerHTML : cells[2].innerHTML).trim(),
directionText: cells[2].textContent.trim(),
trigger: cells[3].textContent.trim(),
stopLoss: cells[4].textContent.trim(),
takeProfit: cells[5].textContent.trim(),
margin: cells[6].textContent.trim(),
leverage: cells[7].textContent.trim(),
holdMinutes: cells[8].textContent.trim(),
openedAt: cells[9].textContent.trim(),
closedAt: cells[10].textContent.trim(),
pnlHtml: cells[11].innerHTML.trim(),
pnlText: cells[11].textContent.trim(),
resultHtml: cells[12].innerHTML.trim(),
resultText: cells[12].textContent.trim(),
actionsHtml: cells[13].innerHTML,
entryReason: cells[2].textContent.trim(),
directionHtml: (dirBadge ? dirBadge.outerHTML : cells[3].innerHTML).trim(),
directionText: cells[3].textContent.trim(),
trigger: cells[4].textContent.trim(),
stopLoss: cells[5].textContent.trim(),
takeProfit: cells[6].textContent.trim(),
margin: cells[7].textContent.trim(),
leverage: cells[8].textContent.trim(),
holdMinutes: cells[9].textContent.trim(),
openedAt: cells[10].textContent.trim(),
closedAt: cells[11].textContent.trim(),
pnlHtml: cells[12].innerHTML.trim(),
pnlText: cells[12].textContent.trim(),
resultHtml: cells[13].innerHTML.trim(),
resultText: cells[13].textContent.trim(),
actionsHtml: cells[14].innerHTML,
};
}
@@ -240,7 +242,8 @@
function buildTradeRecordDetailHtml(row) {
return `<div class="trade-record-detail">${
tradeDetailRow("品种", escapeHtml(row.symbol)) +
tradeDetailRow("类型", escapeHtml(row.type)) +
tradeDetailRow("下单类型", escapeHtml(row.type)) +
tradeDetailRow("开仓类型", escapeHtml(row.entryReason || "-")) +
tradeDetailRow("方向", row.directionHtml) +
tradeDetailRow("成交价", escapeHtml(row.trigger)) +
tradeDetailRow("止损(开仓)", escapeHtml(row.stopLoss)) +
+44
View File
@@ -0,0 +1,44 @@
"""复盘表单:下单类型与开仓类型校验(三所共用)."""
from __future__ import annotations
from typing import Optional, Sequence, Tuple
from lib.strategy.strategy_trade_labels import (
JOURNAL_ORDER_TYPE_OPTIONS,
STRATEGY_ENTRY_REASON_OPTIONS,
normalize_journal_order_type,
)
from lib.trade.entry_model_lib import (
TRADE_STYLE_FALLBACK_ENTRY_REASONS,
normalize_review_entry_reason,
)
_LEGACY_JOURNAL_ENTRY_REASONS: Tuple[str, ...] = (
*TRADE_STYLE_FALLBACK_ENTRY_REASONS,
*STRATEGY_ENTRY_REASON_OPTIONS,
)
def normalize_journal_entry_reason(
raw: Optional[str],
allowed: Sequence[str],
*,
allow_legacy: bool = False,
) -> str:
s = normalize_review_entry_reason(raw, allowed)
if s:
return s
if not allow_legacy:
return ""
legacy = (raw or "").strip()
if legacy in _LEGACY_JOURNAL_ENTRY_REASONS:
return legacy
return ""
def journal_entry_reason_valid(raw: Optional[str], allowed: Sequence[str]) -> bool:
return bool(normalize_journal_entry_reason(raw, allowed, allow_legacy=False))
def journal_order_type_valid(raw: Optional[str]) -> bool:
return bool(normalize_journal_order_type(raw))
+27 -6
View File
@@ -1,5 +1,6 @@
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }};
function reloadInstancePage(){
if(document.body && document.body.getAttribute("data-embed-shell") === "1" && window.InstanceEmbed){
@@ -12,6 +13,11 @@ function reloadInstancePage(){
function validateJournalEntryReason(){
const form = document.getElementById("journal-form");
if(!form) return true;
const orderSel = form.querySelector('[name="order_type"]');
if(!orderSel || !orderSel.value){
alert("请选择下单类型");
return false;
}
const sel = form.querySelector('[name="entry_reason"]');
if(!sel || !sel.value){
alert("请选择开仓类型");
@@ -147,6 +153,10 @@ function normalizeBeijingDatetimeString(v){
}
function toggleReviewMode(){
if(window.InstanceTheme && typeof InstanceTheme.syncReviewEditButtons === "function"){
InstanceTheme.syncReviewEditButtons();
return;
}
const on = !!(document.getElementById("review-mode-toggle") || {}).checked;
document.querySelectorAll(".review-edit-btn").forEach(btn=>{
btn.disabled = !on;
@@ -537,11 +547,18 @@ function fillJournalFromTrade(t){
setJournalField("early_exit_note", "");
const kst = String(t.key_signal_type || "").trim();
const mt = String(t.monitor_type || "").trim();
if(mt === "趋势回调" && JOURNAL_ENTRY_REASON_OPTIONS.includes("趋势回调")){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("entry_reason", "顺势加仓");
} else if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
const orderType = (function(){
if(mt === "趋势回调" && JOURNAL_ORDER_TYPE_OPTIONS.includes("趋势回调")) return "趋势回调";
if(mt === "顺势加仓" && JOURNAL_ORDER_TYPE_OPTIONS.includes("顺势加仓")) return "顺势加仓";
if(mt === "关键位监控" || mt.includes("关键位")) return "关键位监控";
return "下单监控";
})();
if(JOURNAL_ORDER_TYPE_OPTIONS.includes(orderType)){
setJournalField("order_type", orderType);
} else {
setJournalField("order_type", "");
}
if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
@@ -554,7 +571,7 @@ function fillJournalFromTrade(t){
const er = String(t.result || "").trim();
const exitTrigMap = { 止盈: "止盈", 保本止盈: "保本止盈", 移动止盈: "移动止盈", 时间平仓: "时间平仓", 强制清仓: "强制清仓", 手动平仓: "手动平仓", 止损: "止损" };
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${entryPx || "-"} 止损:${slPx || "-"} 止盈:${tpPx || "-"} | 类型:${t.monitor_type || "-"}`;
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${entryPx || "-"} 止损:${slPx || "-"} 止盈:${tpPx || "-"} | 下单类型:${orderType || t.monitor_type || "-"}`;
setJournalField("note", note);
const form = document.getElementById("journal-form");
if(form && typeof form.scrollIntoView === "function"){
@@ -632,8 +649,12 @@ if(document.getElementById("review-list")) loadReviews();
const reviewToggle = document.getElementById("review-mode-toggle");
if(reviewToggle){
reviewToggle.addEventListener("change", toggleReviewMode);
reviewToggle.addEventListener("input", toggleReviewMode);
toggleReviewMode();
}
if(window.InstanceTheme && typeof InstanceTheme.initReviewEditModeSync === "function"){
InstanceTheme.initReviewEditModeSync();
}
const journalForm = document.getElementById("journal-form");
if(journalForm){
const pnlInput = journalForm.querySelector('[name="pnl"]');
@@ -227,7 +227,7 @@
</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>
<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 %}
@@ -309,7 +309,7 @@
<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) }}
{{ 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">
+4 -4
View File
@@ -3,11 +3,11 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=49"></script>
<script src="/static/instance_theme.js?v=50"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=4">
<link rel="stylesheet" href="/static/instance_theme.css?v=75">
<link rel="stylesheet" href="/static/instance_theme.css?v=76">
<script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
@@ -83,7 +83,7 @@
</div>
</div>
<script src="/static/instance_ui.js?v=7"></script>
<script src="/static/instance_ui.js?v=8"></script>
<script src="/static/journal_upload_slots.js?v=3"></script>
<script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=3"></script>
@@ -105,6 +105,6 @@ window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
</script>
<script src="/static/instance_settings_prefs.js?v=12"></script>
<script src="/static/instance_live.js?v=4"></script>
<script src="/static/instance_embed.js?v=21"></script>
<script src="/static/instance_embed.js?v=22"></script>
</body>
</html>
+32 -11
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/instance_theme.js?v=49"></script>
<script src="/static/instance_theme.js?v=50"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<script src="/static/account_risk_badge.js?v=4"></script>
@@ -17,7 +17,7 @@
<link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<link rel="stylesheet" href="/static/instance_page.css?v=1">
<link rel="stylesheet" href="/static/instance_theme.css?v=75">
<link rel="stylesheet" href="/static/instance_theme.css?v=76">
</head>
<body
@@ -309,7 +309,7 @@
</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>
<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 %}
@@ -391,7 +391,7 @@
<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) }}
{{ 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">
@@ -542,7 +542,7 @@
</div>
</div>
<script src="/static/instance_ui.js?v=7"></script>
<script src="/static/instance_ui.js?v=8"></script>
<script src="/static/journal_upload_slots.js?v=3"></script>
<script src="/static/instance_records_mobile.js?v=2"></script>
<script src="/static/time_close_ui.js?v=3"></script>
@@ -559,10 +559,16 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
<script src="/static/strategy_roll.js?v=6"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }};
function validateJournalEntryReason(){
const form = document.getElementById("journal-form");
if(!form) return true;
const orderSel = form.querySelector('[name="order_type"]');
if(!orderSel || !orderSel.value){
alert("请选择下单类型");
return false;
}
const sel = form.querySelector('[name="entry_reason"]');
if(!sel || !sel.value){
alert("请选择开仓类型");
@@ -698,6 +704,10 @@ function normalizeBeijingDatetimeString(v){
}
function toggleReviewMode(){
if(window.InstanceTheme && typeof InstanceTheme.syncReviewEditButtons === "function"){
InstanceTheme.syncReviewEditButtons();
return;
}
const on = !!(document.getElementById("review-mode-toggle") || {}).checked;
document.querySelectorAll(".review-edit-btn").forEach(btn=>{
btn.disabled = !on;
@@ -1088,11 +1098,18 @@ function fillJournalFromTrade(t){
setJournalField("early_exit_note", "");
const kst = String(t.key_signal_type || "").trim();
const mt = String(t.monitor_type || "").trim();
if(mt === "趋势回调" && JOURNAL_ENTRY_REASON_OPTIONS.includes("趋势回调")){
setJournalField("entry_reason", "趋势回调");
} else if(mt === "顺势加仓" && JOURNAL_ENTRY_REASON_OPTIONS.includes("顺势加仓")){
setJournalField("entry_reason", "顺势加仓");
} else if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
const orderType = (function(){
if(mt === "趋势回调" && JOURNAL_ORDER_TYPE_OPTIONS.includes("趋势回调")) return "趋势回调";
if(mt === "顺势加仓" && JOURNAL_ORDER_TYPE_OPTIONS.includes("顺势加仓")) return "顺势加仓";
if(mt === "关键位监控" || mt.includes("关键位")) return "关键位监控";
return "下单监控";
})();
if(JOURNAL_ORDER_TYPE_OPTIONS.includes(orderType)){
setJournalField("order_type", orderType);
} else {
setJournalField("order_type", "");
}
if(t.effective_entry_reason && JOURNAL_ENTRY_REASON_OPTIONS.includes(t.effective_entry_reason)){
setJournalField("entry_reason", t.effective_entry_reason);
} else {
const erFromKey = KEY_ENTRY_REASON_BY_SIGNAL[kst] || "";
@@ -1105,7 +1122,7 @@ function fillJournalFromTrade(t){
const er = String(t.result || "").trim();
const exitTrigMap = { 止盈: "止盈", 保本止盈: "保本止盈", 移动止盈: "移动止盈", 时间平仓: "时间平仓", 强制清仓: "强制清仓", 手动平仓: "手动平仓", 止损: "止损" };
if(exitTrigMap[er]) setJournalField("early_exit_trigger", exitTrigMap[er]);
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${entryPx || "-"} 止损:${slPx || "-"} 止盈:${tpPx || "-"} | 类型:${t.monitor_type || "-"}`;
const note = `来自交易记录自动填充:${t.symbol || "-"} ${t.direction || "-"} | 入场:${entryPx || "-"} 止损:${slPx || "-"} 止盈:${tpPx || "-"} | 下单类型:${orderType || t.monitor_type || "-"}`;
setJournalField("note", note);
const form = document.getElementById("journal-form");
if(form && typeof form.scrollIntoView === "function"){
@@ -1170,8 +1187,12 @@ if(document.getElementById("review-list")) loadReviews();
const reviewToggle = document.getElementById("review-mode-toggle");
if(reviewToggle){
reviewToggle.addEventListener("change", toggleReviewMode);
reviewToggle.addEventListener("input", toggleReviewMode);
toggleReviewMode();
}
if(window.InstanceTheme && typeof InstanceTheme.initReviewEditModeSync === "function"){
InstanceTheme.initReviewEditModeSync();
}
const journalForm = document.getElementById("journal-form");
if(journalForm){
const pnlInput = journalForm.querySelector('[name="pnl"]');
+33
View File
@@ -5,15 +5,48 @@ from typing import Optional
MONITOR_TYPE_TREND_PULLBACK = "趋势回调"
MONITOR_TYPE_ROLL = "顺势加仓"
ORDER_TYPE_MANUAL = "下单监控"
ORDER_TYPE_KEY = "关键位监控"
ENTRY_REASON_TREND_PULLBACK = "趋势回调"
ENTRY_REASON_ROLL = "顺势加仓"
JOURNAL_ORDER_TYPE_OPTIONS = (
ORDER_TYPE_MANUAL,
ORDER_TYPE_KEY,
MONITOR_TYPE_TREND_PULLBACK,
MONITOR_TYPE_ROLL,
)
STRATEGY_ENTRY_REASON_OPTIONS = (
ENTRY_REASON_TREND_PULLBACK,
ENTRY_REASON_ROLL,
)
def normalize_journal_order_type(raw: Optional[str]) -> str:
s = (raw or "").strip()
if s in JOURNAL_ORDER_TYPE_OPTIONS:
return s
if "关键位" in s:
return ORDER_TYPE_KEY
return ""
def order_type_from_monitor_type(
monitor_type: Optional[str],
key_signal_type: Optional[str] = None,
) -> str:
del key_signal_type
mt = (monitor_type or "").strip()
if mt == MONITOR_TYPE_TREND_PULLBACK:
return MONITOR_TYPE_TREND_PULLBACK
if mt == MONITOR_TYPE_ROLL:
return MONITOR_TYPE_ROLL
if mt == ORDER_TYPE_KEY or "关键位" in mt:
return ORDER_TYPE_KEY
return ORDER_TYPE_MANUAL
# 趋势回调保本移交下单监控:order_monitors.key_signal_type / 平仓备注
TREND_HANDOFF_KEY_SIGNAL = ENTRY_REASON_TREND_PULLBACK
TREND_HANDOFF_TRADE_NOTE = "趋势回调计划"
@@ -1,5 +1,5 @@
{# 复盘表单:首行按字段宽度比例;开仓类型与离场触发同一行 #}
{% macro journal_form_fields(entry_reason_options) -%}
{# 复盘表单:首行按字段宽度比例;下单类型/开仓类型与离场触发同一行 #}
{% macro journal_form_fields(entry_reason_options, order_type_options) -%}
<div class="form-grid journal-form-row1">
<input type="datetime-local" name="open_datetime" class="journal-field-datetime" required>
<input type="datetime-local" name="close_datetime" class="journal-field-datetime" required>
@@ -10,7 +10,13 @@
<input name="real_rr" class="journal-field-num" placeholder="实际RR">
</div>
<div class="form-grid journal-form-row2">
<select name="entry_reason" id="journal-entry-reason" class="journal-field-entry-reason" required title="日内:假破/结构突破/回调触价/突破触价;趋势户:反转/顺势/波段或策略项">
<select name="order_type" id="journal-order-type" class="journal-field-order-type" required title="下单来源:下单监控/关键位/趋势回调/顺势加仓">
<option value="">下单类型(必选)</option>
{% for ot in order_type_options %}
<option value="{{ ot }}">{{ ot }}</option>
{% endfor %}
</select>
<select name="entry_reason" id="journal-entry-reason" class="journal-field-entry-reason" required title="日内:假破/结构突破/回调触价/突破触价;趋势户:反转/顺势/波段小分歧等">
<option value="">开仓类型(必选)</option>
{% for er in entry_reason_options %}
<option value="{{ er }}">{{ er }}</option>
+6 -1
View File
@@ -312,7 +312,12 @@ _INTRADAY_JOURNAL_KEY_ENTRY_REASONS: Tuple[str, ...] = (
def trend_manual_entry_reason_count(policy: TradePolicy) -> int:
if is_intraday_trading_profile(policy):
return len(intraday_entry_reason_display_options())
return len(trend_div_entry_reason_display_options()) + len(TRADE_STYLE_FALLBACK_ENTRY_REASONS)
return len(trend_div_entry_reason_display_options())
def build_journal_entry_reason_options() -> Tuple[str, ...]:
"""复盘开仓类型:仅 entry model,不含 trade_style 兜底与策略下单类型."""
return trend_div_entry_reason_display_options()
def build_trend_div_entry_reason_options(
+1 -1
View File
@@ -46,7 +46,7 @@ class TestEntryModelLib(unittest.TestCase):
}
)
self.assertFalse(is_intraday_trading_profile(policy))
self.assertEqual(trend_manual_entry_reason_count(policy), 7)
self.assertEqual(trend_manual_entry_reason_count(policy), 5)
def test_entry_model_maps_trade_style(self):
self.assertEqual(trade_style_for_entry_model(ENTRY_MODEL_LAUNCH_A), "trend")
+63
View File
@@ -0,0 +1,63 @@
"""journal_form_lib / strategy_trade_labels 下单类型与开仓类型拆分."""
from __future__ import annotations
import unittest
from lib.instance.journal_form_lib import (
journal_entry_reason_valid,
normalize_journal_entry_reason,
)
from lib.strategy.strategy_trade_labels import (
JOURNAL_ORDER_TYPE_OPTIONS,
normalize_journal_order_type,
order_type_from_monitor_type,
)
from lib.trade.entry_model_lib import build_journal_entry_reason_options
class JournalFormLibTests(unittest.TestCase):
def test_journal_entry_reason_excludes_legacy_style_and_strategy(self):
opts = build_journal_entry_reason_options()
self.assertIn("反转/启动A", opts)
self.assertNotIn("趋势单", opts)
self.assertNotIn("波段单", opts)
self.assertNotIn("趋势回调", opts)
self.assertNotIn("顺势加仓", opts)
def test_normalize_journal_entry_reason_rejects_legacy_for_new_submit(self):
opts = build_journal_entry_reason_options()
self.assertEqual(
normalize_journal_entry_reason("趋势单", opts, allow_legacy=False),
"",
)
self.assertEqual(
normalize_journal_entry_reason("趋势回调", opts, allow_legacy=False),
"",
)
def test_normalize_journal_entry_reason_accepts_legacy_when_allowed(self):
opts = build_journal_entry_reason_options()
self.assertEqual(
normalize_journal_entry_reason("趋势单", opts, allow_legacy=True),
"趋势单",
)
def test_order_type_from_monitor_type(self):
self.assertEqual(order_type_from_monitor_type("下单监控"), "下单监控")
self.assertEqual(order_type_from_monitor_type("关键位监控"), "关键位监控")
self.assertEqual(order_type_from_monitor_type("趋势回调"), "趋势回调")
self.assertEqual(order_type_from_monitor_type("顺势加仓"), "顺势加仓")
def test_normalize_journal_order_type(self):
self.assertEqual(normalize_journal_order_type("顺势加仓"), "顺势加仓")
self.assertEqual(normalize_journal_order_type(""), "")
self.assertEqual(len(JOURNAL_ORDER_TYPE_OPTIONS), 4)
def test_journal_entry_reason_valid(self):
opts = build_journal_entry_reason_options()
self.assertTrue(journal_entry_reason_valid("顺势/大分歧A", opts))
self.assertFalse(journal_entry_reason_valid("趋势单", opts))
if __name__ == "__main__":
unittest.main()