diff --git a/lib/hub/hub_symbol_archive_lib.py b/lib/hub/hub_symbol_archive_lib.py index c7573e1..1c1a24c 100644 --- a/lib/hub/hub_symbol_archive_lib.py +++ b/lib/hub/hub_symbol_archive_lib.py @@ -468,7 +468,23 @@ def _trade_row_to_dict(row: sqlite3.Row, overlay: dict | None = None) -> dict[st if key in d and d[key] not in (None, ""): out[key] = d[key] ov = overlay or {} - out["behavior_tag"] = ov.get("behavior_tag") or "" + from lib.trade.account_risk_lib import parse_mood_issues + + journal_issues = parse_mood_issues( + out.get("journal_mood_issues") or payload.get("journal_mood_issues") + ) + tag_from_journal = bool( + out.get("journal_mood_sick") + or payload.get("journal_mood_sick") + or journal_issues + ) + tag = (ov.get("behavior_tag") or "").strip().lower() + if tag_from_journal: + tag = "sick" + out["behavior_tag"] = tag + out["behavior_tag_from_journal"] = tag_from_journal + if journal_issues: + out["journal_mood_issues"] = journal_issues out["note"] = ov.get("note") or "" out["trade_id"] = out.get("trade_id") or out.get("id") ex_col = str(d.get("exchange_key") or "").strip().lower() @@ -551,6 +567,31 @@ def upsert_trade_overlay( return {"exchange_key": ex_k, "trade_id": tid, "behavior_tag": tag, "note": note_text} +def apply_journal_behavior_overlays( + exchange_key: str, + trades: list[dict[str, Any]], + *, + db_path: Path | None = None, +) -> int: + """复盘情绪标签 → 写入 trade_overlay.behavior_tag=sick(仅正 trade_id)。""" + ex_k = (exchange_key or "").strip().lower() + if not ex_k: + return 0 + n = 0 + for t in trades or []: + if not isinstance(t, dict) or not t.get("journal_mood_sick"): + continue + try: + tid = int(t.get("id")) + except (TypeError, ValueError): + continue + if tid <= 0: + continue + upsert_trade_overlay(ex_k, tid, behavior_tag="sick", db_path=db_path) + n += 1 + return n + + def list_symbol_rows( *, exchange_key: str = "", @@ -1154,6 +1195,7 @@ def sync_exchange_symbol_archives( """同步单所:交易缓存 + 各币种 K 线种子/增量。""" ex_k = (exchange_key or "").strip().lower() cache_stats = upsert_trades_cache(ex_k, trades, db_path=db_path, prune_missing=True) + apply_journal_behavior_overlays(ex_k, trades, db_path=db_path) by_sym: dict[str, int] = {} for t in trades or []: diff --git a/lib/hub/hub_trades_lib.py b/lib/hub/hub_trades_lib.py index 94ea521..cbb2209 100644 --- a/lib/hub/hub_trades_lib.py +++ b/lib/hub/hub_trades_lib.py @@ -617,7 +617,98 @@ def fetch_trades_for_archive( key=lambda x: int(x.get("closed_at_ms") or 0), reverse=True, ) - return merged[:lim] + merged = merged[:lim] + attach_journal_mood_tags(conn, merged, cutoff_s=cutoff_s) + return merged + + +def _symbol_coin_base(symbol: str) -> str: + s = (symbol or "").strip().upper() + if "/" in s: + return s.split("/")[0] + return s + + +def _datetime_minute_key(raw: Any) -> str: + if raw is None: + return "" + s = str(raw).strip().replace("T", " ").replace("Z", "") + if len(s) >= 16: + return s[:16] + return s[:10] if len(s) >= 10 else s + + +def journal_trade_match_key(symbol: str, opened_at: Any, closed_at: Any) -> tuple[str, str, str]: + return ( + _symbol_coin_base(symbol), + _datetime_minute_key(opened_at), + _datetime_minute_key(closed_at), + ) + + +def load_journal_mood_match_index( + conn, + *, + cutoff_s: str, +) -> dict[tuple[str, str, str], list[str]]: + """复盘 mood_issues → 交易匹配键(币种 + 开/平仓分钟)。""" + from lib.trade.account_risk_lib import parse_mood_issues + + cols = _table_columns(conn, "journal_entries") + if not cols or "mood_issues" not in cols: + return {} + close_expr = "REPLACE(COALESCE(close_datetime, open_datetime, created_at), 'T', ' ')" + open_expr = "REPLACE(COALESCE(open_datetime, created_at), 'T', ' ')" + rows = conn.execute( + f""" + SELECT coin, open_datetime, close_datetime, mood_issues + FROM journal_entries + WHERE {close_expr} >= ? OR {open_expr} >= ? + """, + (cutoff_s, cutoff_s), + ).fetchall() + out: dict[tuple[str, str, str], list[str]] = {} + for row in rows: + d = _row_dict(row) + issues = parse_mood_issues(d.get("mood_issues")) + if not issues: + continue + coin = str(d.get("coin") or "").strip().upper() + sym = coin if "/" in coin else (f"{coin}/USDT" if coin else "") + key = journal_trade_match_key(sym, d.get("open_datetime"), d.get("close_datetime")) + if not key[0]: + continue + out[key] = issues + return out + + +def attach_journal_mood_tags( + conn, + trades: list[dict[str, Any]], + *, + cutoff_s: str, +) -> None: + """实例复盘勾选情绪标签 → 档案交易自动标犯病(hub 同步用)。""" + if not trades: + return + mood_index = load_journal_mood_match_index(conn, cutoff_s=cutoff_s) + if not mood_index: + return + for t in trades: + if not isinstance(t, dict): + continue + key = journal_trade_match_key( + str(t.get("symbol") or ""), + t.get("opened_at"), + t.get("closed_at"), + ) + issues = mood_index.get(key) + if not issues: + continue + t["journal_mood_issues"] = issues + t["journal_mood_sick"] = True + t["behavior_tag_from_journal"] = True + t["behavior_tag"] = "sick" def summarize_trades(trades: list[dict]) -> dict[str, Any]: diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 46e7ad2..ce3eea4 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -7126,6 +7126,18 @@ body.funds-fullscreen-open { color: var(--text); font-size: 0.75rem; } +.archive-tag-fixed { + display: inline-block; + padding: 4px 8px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; +} +.archive-tag-fixed.is-tag-sick { + color: var(--red); + border: 1px solid color-mix(in srgb, var(--red) 45%, var(--border-soft)); + background: color-mix(in srgb, var(--red) 14%, var(--inset-surface)); +} .archive-tag-select.is-tag-sick { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, var(--border-soft)); diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index 3135b93..8cfc275 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -1230,7 +1230,8 @@ const tid = t.trade_id || t.id; const exKey = String(t.exchange_key || "").toLowerCase(); const rowKey = tradeRowKey(t); - const tag = t.behavior_tag || ""; + const journalSick = !!t.behavior_tag_from_journal; + const tag = journalSick ? "sick" : (t.behavior_tag || ""); const sick = tag === "sick"; const active = rowKey && rowKey === selectedTradeKey ? " is-active" : ""; const rev = reviewMark(t); @@ -1283,21 +1284,23 @@ "