fix: journal detail contrast and unify AI review journal format across four exchanges

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-05 13:09:59 +08:00
parent 934e48b9a8
commit 1b51f73ecd
11 changed files with 138 additions and 150 deletions
+52 -2
View File
@@ -1,9 +1,9 @@
"""AI 日复盘 / 周复盘:附图收集(各实例共用)。"""
"""AI 日复盘 / 周复盘:附图收集与 journal 文本格式化(四所共用)。"""
from __future__ import annotations
import os
import uuid
from typing import Callable, List, Optional, Sequence
from typing import Any, Callable, List, Mapping, Optional, Sequence
from journal_chart_lib import (
JOURNAL_CHART_ANCHOR_CLOSE,
@@ -14,6 +14,56 @@ from journal_chart_lib import (
)
def _journal_nz(v: Any, default: str = "") -> str:
if v is None:
return default
s = str(v).strip()
return s if s else default
def journal_row_lines_for_ai(
idx: int,
row: Mapping[str, Any],
*,
include_hold_duration: bool = True,
) -> str:
"""把 journal 字段拼成给 AI 的文本;四所日复盘/周复盘共用。"""
lines = [
(
f"{idx}. {_journal_nz(row.get('coin'))} {_journal_nz(row.get('tf'))} "
f"| 盈亏:{_journal_nz(row.get('pnl'))}U "
f"| 实际RR:{_journal_nz(row.get('real_rr'))} "
f"| 预期RR:{_journal_nz(row.get('expect_rr'))}"
),
f" 开仓逻辑:{_journal_nz(row.get('entry_reason'))}",
f" 平仓/离场(交易员自述):{_journal_nz(row.get('exit_reason'))}",
]
if include_hold_duration:
lines.append(f" 持仓时长:{_journal_nz(row.get('hold_duration'))}")
ee_bits = [
_journal_nz(row.get("early_exit")),
_journal_nz(row.get("early_exit_reason")),
_journal_nz(row.get("early_exit_trigger")),
_journal_nz(row.get("early_exit_note")),
]
if any(x != "" for x in ee_bits):
lines.append(
" 提前离场记录:"
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
)
mood_bits = f"心态标签:{_journal_nz(row.get('mood_issues'))}"
if row.get("mood_score") is not None:
mood_bits += f" | 自评心态分:{row.get('mood_score')}"
lines.append(f" {mood_bits}")
if _journal_nz(row.get("post_breakeven_stare")) != "":
lines.append(f" 保本后盯盘:{_journal_nz(row.get('post_breakeven_stare'))}")
if _journal_nz(row.get("new_trade_while_occupied")) != "":
lines.append(f" 占用时新开仓:{_journal_nz(row.get('new_trade_while_occupied'))}")
if _journal_nz(row.get("note")) != "":
lines.append(f" 备注:{_journal_nz(row.get('note'))}")
return "\n".join(lines) + "\n"
def collect_images_for_ai_review(
rows: Sequence,
upload_folder: str,
+7 -42
View File
@@ -35,7 +35,11 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
from ai_review_lib import (
build_journal_ai_chart_path,
collect_images_for_ai_review,
journal_row_lines_for_ai,
)
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
from fib_key_monitor_lib import (
FIB_KEY_MONITOR_TYPES,
@@ -562,45 +566,6 @@ def _extract_json_object(text):
return None
def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
"""把 journal 字段拼成给 AI 的文本;字段之外的事实不要指望模型自己猜。"""
def nz(v, default=""):
if v is None:
return default
s = str(v).strip()
return s if s else default
lines = [
f"{idx}. {nz(row['coin'])} {nz(row['tf'])} | 盈亏:{nz(row['pnl'])}U | 实际RR:{nz(row['real_rr'])} | 预期RR:{nz(row['expect_rr'])}",
f" 开仓逻辑:{nz(row['entry_reason'])}",
f" 平仓/离场(交易员自述):{nz(row['exit_reason'])}",
]
if include_hold_duration:
lines.append(f" 持仓时长:{nz(row['hold_duration'])}")
ee_bits = [
nz(row["early_exit"]),
nz(row["early_exit_reason"]),
nz(row["early_exit_trigger"]),
nz(row["early_exit_note"]),
]
if any(x != "" for x in ee_bits):
lines.append(
" 提前离场记录:"
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
)
mood_bits = f"心态标签:{nz(row['mood_issues'])}"
if row["mood_score"] is not None:
mood_bits += f" | 自评心态分:{row['mood_score']}"
lines.append(f" {mood_bits}")
if nz(row["post_breakeven_stare"]) != "":
lines.append(f" 保本后盯盘:{nz(row['post_breakeven_stare'])}")
if nz(row["new_trade_while_occupied"]) != "":
lines.append(f" 占用时新开仓:{nz(row['new_trade_while_occupied'])}")
if nz(row["note"]) != "":
lines.append(f" 备注:{nz(row['note'])}")
return "\n".join(lines) + "\n"
def _load_font(size):
if not ImageFont:
return None
@@ -8109,7 +8074,7 @@ def ai_daily_review():
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
text += _journal_row_lines_for_ai(idx, row)
text += journal_row_lines_for_ai(idx, row)
text += "\n"
image_paths = collect_images_for_ai_review(
@@ -8145,7 +8110,7 @@ def ai_weekly_review():
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
text += _journal_row_lines_for_ai(idx, row)
text += journal_row_lines_for_ai(idx, row)
text += "\n"
image_paths = collect_images_for_ai_review(
+1 -1
View File
@@ -231,7 +231,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
</head>
<body data-page="{{ page }}">
+7 -42
View File
@@ -35,7 +35,11 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
from ai_review_lib import (
build_journal_ai_chart_path,
collect_images_for_ai_review,
journal_row_lines_for_ai,
)
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
from fib_key_monitor_lib import (
FIB_KEY_MONITOR_TYPES,
@@ -552,45 +556,6 @@ def _extract_json_object(text):
return None
def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
"""把 journal 字段拼成给 AI 的文本;字段之外的事实不要指望模型自己猜。"""
def nz(v, default=""):
if v is None:
return default
s = str(v).strip()
return s if s else default
lines = [
f"{idx}. {nz(row['coin'])} {nz(row['tf'])} | 盈亏:{nz(row['pnl'])}U | 实际RR:{nz(row['real_rr'])} | 预期RR:{nz(row['expect_rr'])}",
f" 开仓逻辑:{nz(row['entry_reason'])}",
f" 平仓/离场(交易员自述):{nz(row['exit_reason'])}",
]
if include_hold_duration:
lines.append(f" 持仓时长:{nz(row['hold_duration'])}")
ee_bits = [
nz(row["early_exit"]),
nz(row["early_exit_reason"]),
nz(row["early_exit_trigger"]),
nz(row["early_exit_note"]),
]
if any(x != "" for x in ee_bits):
lines.append(
" 提前离场记录:"
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
)
mood_bits = f"心态标签:{nz(row['mood_issues'])}"
if row["mood_score"] is not None:
mood_bits += f" | 自评心态分:{row['mood_score']}"
lines.append(f" {mood_bits}")
if nz(row["post_breakeven_stare"]) != "":
lines.append(f" 保本后盯盘:{nz(row['post_breakeven_stare'])}")
if nz(row["new_trade_while_occupied"]) != "":
lines.append(f" 占用时新开仓:{nz(row['new_trade_while_occupied'])}")
if nz(row["note"]) != "":
lines.append(f" 备注:{nz(row['note'])}")
return "\n".join(lines) + "\n"
def _load_font(size):
if not ImageFont:
return None
@@ -8168,7 +8133,7 @@ def ai_daily_review():
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
text += _journal_row_lines_for_ai(idx, row)
text += journal_row_lines_for_ai(idx, row)
text += "\n"
image_paths = collect_images_for_ai_review(
@@ -8204,7 +8169,7 @@ def ai_weekly_review():
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
text += _journal_row_lines_for_ai(idx, row)
text += journal_row_lines_for_ai(idx, row)
text += "\n"
image_paths = collect_images_for_ai_review(
+1 -1
View File
@@ -231,7 +231,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
</head>
<body data-page="{{ page }}">
+7 -42
View File
@@ -35,7 +35,11 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
from ai_review_lib import (
build_journal_ai_chart_path,
collect_images_for_ai_review,
journal_row_lines_for_ai,
)
from position_sizing_lib import (
assert_open_source_allowed,
compute_full_margin_sizing,
@@ -522,45 +526,6 @@ def _extract_json_object(text):
return None
def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
"""把 journal 字段拼成给 AI 的文本;字段之外的事实不要指望模型自己猜。"""
def nz(v, default=""):
if v is None:
return default
s = str(v).strip()
return s if s else default
lines = [
f"{idx}. {nz(row['coin'])} {nz(row['tf'])} | 盈亏:{nz(row['pnl'])}U | 实际RR:{nz(row['real_rr'])} | 预期RR:{nz(row['expect_rr'])}",
f" 开仓逻辑:{nz(row['entry_reason'])}",
f" 平仓/离场(交易员自述):{nz(row['exit_reason'])}",
]
if include_hold_duration:
lines.append(f" 持仓时长:{nz(row['hold_duration'])}")
ee_bits = [
nz(row["early_exit"]),
nz(row["early_exit_reason"]),
nz(row["early_exit_trigger"]),
nz(row["early_exit_note"]),
]
if any(x != "" for x in ee_bits):
lines.append(
" 提前离场记录:"
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
)
mood_bits = f"心态标签:{nz(row['mood_issues'])}"
if row["mood_score"] is not None:
mood_bits += f" | 自评心态分:{row['mood_score']}"
lines.append(f" {mood_bits}")
if nz(row["post_breakeven_stare"]) != "":
lines.append(f" 保本后盯盘:{nz(row['post_breakeven_stare'])}")
if nz(row["new_trade_while_occupied"]) != "":
lines.append(f" 占用时新开仓:{nz(row['new_trade_while_occupied'])}")
if nz(row["note"]) != "":
lines.append(f" 备注:{nz(row['note'])}")
return "\n".join(lines) + "\n"
def _load_font(size):
if not ImageFont:
return None
@@ -8037,7 +8002,7 @@ def ai_daily_review():
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
text += _journal_row_lines_for_ai(idx, row)
text += journal_row_lines_for_ai(idx, row)
text += "\n"
image_paths = collect_images_for_ai_review(
@@ -8073,7 +8038,7 @@ def ai_weekly_review():
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
text += _journal_row_lines_for_ai(idx, row)
text += journal_row_lines_for_ai(idx, row)
text += "\n"
image_paths = collect_images_for_ai_review(
+1 -1
View File
@@ -268,7 +268,7 @@
.stats-split-row{grid-template-columns:1fr}
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
</head>
<body data-page="{{ page }}">
+7 -18
View File
@@ -35,7 +35,11 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
from ai_review_lib import (
build_journal_ai_chart_path,
collect_images_for_ai_review,
journal_row_lines_for_ai,
)
from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
from fib_key_monitor_lib import (
FIB_KEY_MONITOR_TYPES,
@@ -7752,15 +7756,7 @@ def ai_daily_review():
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
issues = row["mood_issues"] or ""
exit_one = (row["exit_reason"] or "").strip() or ""
text += (
f"{idx}. {row['coin']} {row['tf']} | 盈亏:{row['pnl']}U | RR:{row['real_rr']}\n"
f" 开仓类型:{row['entry_reason'] or ''}\n"
f" 心态标签:{issues}\n"
f" 平仓/离场:{exit_one}\n"
f" 问题:{issues}\n\n"
)
text += journal_row_lines_for_ai(idx, row)
image_paths = collect_images_for_ai_review(
rows,
@@ -7795,14 +7791,7 @@ def ai_weekly_review():
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
for idx, row in enumerate(rows, 1):
issues = row["mood_issues"] or ""
exit_one = (row["exit_reason"] or "").strip() or ""
text += (
f"{idx}. {row['coin']} {row['tf']} | 盈亏:{row['pnl']}U | RR:{row['real_rr']}\n"
f" 开仓类型:{row['entry_reason'] or ''}\n"
f" 平仓/离场:{exit_one}\n"
f" 心态标签:{issues} | 持仓:{row['hold_duration']}\n\n"
)
text += journal_row_lines_for_ai(idx, row)
image_paths = collect_images_for_ai_review(
rows,
+1 -1
View File
@@ -231,7 +231,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
<link rel="stylesheet" href="/static/instance_theme.css?v=5">
</head>
<body data-page="{{ page }}">
+13
View File
@@ -324,6 +324,19 @@ html[data-theme="light"] .detail-modal .panel-title {
color: #142232 !important;
}
/* 交易复盘详情:上方元数据(非 Markdown 区)浅色主题对比度 */
html[data-theme="light"] .detail-modal .panel-body:not(.md-review) {
color: #1a2838 !important;
}
html[data-theme="light"] .detail-modal .panel {
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .detail-modal .panel-image {
border-color: #b8c8d8 !important;
}
html[data-theme="light"] .journal-card .form-grid label,
html[data-theme="light"] .journal-card .sub {
color: #4a6078 !important;
+41
View File
@@ -0,0 +1,41 @@
"""AI 复盘 journal 文本格式化(四所共用)。"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from ai_review_lib import journal_row_lines_for_ai # noqa: E402
class TestAiReviewLib(unittest.TestCase):
def test_journal_row_includes_expect_and_actual_rr(self):
text = journal_row_lines_for_ai(
1,
{
"coin": "HYPE",
"tf": "5m",
"pnl": "10.73",
"real_rr": "2.1354",
"expect_rr": "-",
"entry_reason": "趋势回调",
"exit_reason": "移动止盈",
"hold_duration": "1天 3小时",
"mood_issues": "",
"post_breakeven_stare": "",
"new_trade_while_occupied": "",
"note": "测试备注",
},
)
self.assertIn("实际RR:2.1354", text)
self.assertIn("预期RR:-", text)
self.assertIn("开仓逻辑:趋势回调", text)
self.assertIn("备注:测试备注", text)
self.assertNotIn("开仓类型", text)
if __name__ == "__main__":
unittest.main()