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:
+52
-2
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user