diff --git a/ai_review_lib.py b/ai_review_lib.py index 1b4cb0d..d4704d6 100644 --- a/ai_review_lib.py +++ b/ai_review_lib.py @@ -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, diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 54e1c19..cd3cd80 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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( diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 902152c..955e12d 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -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} - + diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index d494878..9285e0d 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -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( diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 902152c..955e12d 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -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} - + diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 3700579..05e6c72 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -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( diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 33c7496..39a7511 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -268,7 +268,7 @@ .stats-split-row{grid-template-columns:1fr} } - + diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index c699059..aad37f2 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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, diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 70976cf..e33102d 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -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} - + diff --git a/static/instance_theme.css b/static/instance_theme.css index 8f0b4b3..4c00341 100644 --- a/static/instance_theme.css +++ b/static/instance_theme.css @@ -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; diff --git a/tests/test_ai_review_lib.py b/tests/test_ai_review_lib.py new file mode 100644 index 0000000..717139b --- /dev/null +++ b/tests/test_ai_review_lib.py @@ -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()