diff --git a/hub_symbol_archive_lib.py b/hub_symbol_archive_lib.py index 245aac1..f48a597 100644 --- a/hub_symbol_archive_lib.py +++ b/hub_symbol_archive_lib.py @@ -15,7 +15,11 @@ from hub_ohlcv_lib import ( aggregate_ohlcv_bars, normalize_chart_timeframe, ) -from hub_trades_lib import effective_entry_type, effective_hold_minutes, format_hold_minutes +from hub_trades_lib import ( + display_entry_type_label, + effective_hold_minutes, + format_hold_minutes, +) ARCHIVE_TIMEFRAMES = frozenset({"5m", "15m", "1h", "4h"}) ARCHIVE_DEFAULT_TIMEFRAME = "15m" @@ -160,18 +164,114 @@ def _parse_dt_ms(raw: Any) -> int | None: return None +def _trade_entry_reason_for_cache(t: dict[str, Any]) -> str: + for key in ("entry_type", "entry_reason", "reviewed_entry_reason"): + raw = t.get(key) + if raw is not None and str(raw).strip(): + return str(raw).strip() + return display_entry_type_label(t) if isinstance(t, dict) else "" + + +def purge_stale_trades_cache( + exchange_key: str, + active_trade_ids: list[int] | set[int], + *, + db_path: Path | None = None, +) -> int: + """删除该所缓存中已不在复盘/交易记录里的条目。""" + ex_k = (exchange_key or "").strip().lower() + if not ex_k: + return 0 + ids: list[int] = [] + for raw in active_trade_ids or []: + try: + ids.append(int(raw)) + except (TypeError, ValueError): + continue + conn = _connect(db_path) + try: + if not ids: + rows = conn.execute( + "SELECT trade_id FROM archive_trade_cache WHERE exchange_key=?", + (ex_k,), + ).fetchall() + stale_ids = [int(r["trade_id"]) for r in rows] + cur = conn.execute( + "DELETE FROM archive_trade_cache WHERE exchange_key=?", + (ex_k,), + ) + else: + placeholders = ",".join("?" * len(ids)) + rows = conn.execute( + f""" + SELECT trade_id FROM archive_trade_cache + WHERE exchange_key=? AND trade_id NOT IN ({placeholders}) + """, + (ex_k, *ids), + ).fetchall() + stale_ids = [int(r["trade_id"]) for r in rows] + cur = conn.execute( + f""" + DELETE FROM archive_trade_cache + WHERE exchange_key=? AND trade_id NOT IN ({placeholders}) + """, + (ex_k, *ids), + ) + removed = int(cur.rowcount or 0) + if stale_ids: + ph2 = ",".join("?" * len(stale_ids)) + conn.execute( + f""" + DELETE FROM trade_overlay + WHERE exchange_key=? AND trade_id IN ({ph2}) + """, + (ex_k, *stale_ids), + ) + return removed + finally: + conn.close() + + +def delete_trade_from_archive( + exchange_key: str, + trade_id: int, + *, + db_path: Path | None = None, +) -> bool: + ex_k = (exchange_key or "").strip().lower() + tid = int(trade_id) + conn = _connect(db_path) + try: + cur = conn.execute( + """ + DELETE FROM archive_trade_cache + WHERE exchange_key=? AND trade_id=? + """, + (ex_k, tid), + ) + conn.execute( + "DELETE FROM trade_overlay WHERE exchange_key=? AND trade_id=?", + (ex_k, tid), + ) + return int(cur.rowcount or 0) > 0 + finally: + conn.close() + + def upsert_trades_cache( exchange_key: str, trades: list[dict[str, Any]], *, db_path: Path | None = None, -) -> int: + prune_missing: bool = True, +) -> dict[str, int]: init_db(db_path) ex_k = (exchange_key or "").strip().lower() if not ex_k: - return 0 + return {"upserted": 0, "removed": 0} now = _now_ms() n = 0 + active_ids: list[int] = [] conn = _connect(db_path) try: for t in trades or []: @@ -182,7 +282,9 @@ def upsert_trades_cache( sym = (t.get("symbol") or "").strip().upper() if not sym: continue + active_ids.append(tid) payload = {k: t.get(k) for k in t.keys()} + entry_label = _trade_entry_reason_for_cache(t) conn.execute( """ INSERT INTO archive_trade_cache ( @@ -216,7 +318,7 @@ def upsert_trades_cache( t.get("opened_at_ms") or _parse_dt_ms(t.get("opened_at")), t.get("closed_at_ms") or _parse_dt_ms(t.get("closed_at")), t.get("monitor_type"), - t.get("entry_reason"), + entry_label, json.dumps(payload, ensure_ascii=False, default=str), now, ), @@ -224,7 +326,10 @@ def upsert_trades_cache( n += 1 finally: conn.close() - return n + removed = 0 + if prune_missing: + removed = purge_stale_trades_cache(ex_k, active_ids, db_path=db_path) + return {"upserted": n, "removed": removed} def _enrich_trade_display_fields(out: dict[str, Any]) -> dict[str, Any]: @@ -243,8 +348,8 @@ def _enrich_trade_display_fields(out: dict[str, Any]) -> dict[str, Any]: out["closed_at"] = datetime.fromtimestamp(int(closed_ms) / 1000).strftime( "%Y-%m-%d %H:%M:%S" ) - entry_type = (out.get("entry_type") or effective_entry_type(out) or "").strip() - if entry_type: + entry_type = display_entry_type_label(out) + if entry_type and entry_type != "—": out["entry_type"] = entry_type out["entry_reason"] = entry_type hold_m = out.get("hold_minutes") @@ -961,7 +1066,7 @@ def sync_exchange_symbol_archives( ) -> dict[str, Any]: """同步单所:交易缓存 + 各币种 K 线种子/增量。""" ex_k = (exchange_key or "").strip().lower() - upsert_trades_cache(ex_k, trades, db_path=db_path) + cache_stats = upsert_trades_cache(ex_k, trades, db_path=db_path, prune_missing=True) by_sym: dict[str, int] = {} for t in trades or []: @@ -997,6 +1102,8 @@ def sync_exchange_symbol_archives( "ok": True, "exchange_key": ex_k, "symbols": len(by_sym), + "trades_upserted": int(cache_stats.get("upserted") or 0), + "trades_removed": int(cache_stats.get("removed") or 0), "seed_bars": seeded, "appended_bars": appended, "trades": len(trades or []), diff --git a/hub_trades_lib.py b/hub_trades_lib.py index 65102ba..8e68cd9 100644 --- a/hub_trades_lib.py +++ b/hub_trades_lib.py @@ -119,9 +119,20 @@ def effective_entry_type(d: dict) -> str: kst = str(d.get("key_signal_type") or "").strip() if kst: return kst + legacy = str(d.get("entry_type") or "").strip() + if legacy and legacy not in ("trend_pullback", "roll", "trend"): + return _normalize_monitor_type_label(legacy) or legacy return mt +def display_entry_type_label(d: dict) -> str: + """档案/列表展示用开仓类型(不回落为「下单监控」若已有复盘或建档类型)。""" + label = effective_entry_type(d).strip() + if not label: + return "—" + return _normalize_monitor_type_label(label) or label + + def effective_hold_minutes( d: dict, *, @@ -281,7 +292,7 @@ def _normalize_archive_trade_row( opened_ms_i = int(opened_ms) if opened_ms else None closed_ms_i = int(closed_ms) if closed_ms else None hold_m = effective_hold_minutes(d, opened_ms=opened_ms_i, closed_ms=closed_ms_i) - entry_type = effective_entry_type(d) + entry_type = display_entry_type_label(d) reviewed = bool( d.get("reviewed_at") or d.get("reviewed_result") diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index ef783c4..48d6292 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -1835,6 +1835,20 @@ def api_archive_trade_overlay( return {"ok": True, "overlay": out} +@app.delete("/api/archive/trade/{exchange_key}/{trade_id}") +def api_archive_trade_delete(exchange_key: str, trade_id: int): + from hub_symbol_archive_lib import delete_trade_from_archive + + ex_k = (exchange_key or "").strip().lower() + if not ex_k: + raise HTTPException(status_code=400, detail="缺少 exchange_key") + init_archive_db() + removed = delete_trade_from_archive(ex_k, int(trade_id)) + if not removed: + raise HTTPException(status_code=404, detail="档案中无该笔交易") + return {"ok": True, "exchange_key": ex_k, "trade_id": int(trade_id)} + + @app.post("/api/archive/sync") async def api_archive_sync(): body = await _run_archive_sync_once() diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 6e98f66..188f35b 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -4063,6 +4063,18 @@ body.hub-page-ai #page-ai { .archive-trades-table td.neg { color: #ef4444; } +.archive-del-btn { + padding: 3px 8px; + font-size: 0.72rem; + border-radius: 6px; + border: 1px solid rgba(239, 68, 68, 0.35); + background: rgba(239, 68, 68, 0.08); + color: #f87171; + cursor: pointer; +} +.archive-del-btn:hover { + background: rgba(239, 68, 68, 0.16); +} .archive-tag-select, .archive-note-input { width: 100%; diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index 0743f40..d5f47fd 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -109,14 +109,25 @@ return mins + "分钟"; } + const ENTRY_TYPE_LABELS = { + trend_pullback: "趋势回调", + roll: "顺势加仓", + trend: "趋势回调", + }; + function fmtEntryType(tr) { if (!tr) return "—"; - return ( - tr.entry_type || - tr.entry_reason || - tr.monitor_type || - "—" - ); + const raw = String( + tr.entry_type || tr.entry_reason || tr.reviewed_entry_reason || "" + ).trim(); + if (raw) { + return ENTRY_TYPE_LABELS[raw] || raw; + } + const mt = String(tr.monitor_type || "").trim(); + if (mt && mt !== "下单监控") { + return ENTRY_TYPE_LABELS[mt] || mt; + } + return "—"; } function reviewMark(tr) { @@ -551,7 +562,7 @@ elTrades.innerHTML = '
| 开仓类型 | 开仓时间 | 平仓时间 | 持仓时长 | " + - "方向 | 结果 | 盈亏 | 标签 | 备注 | " + + "方向 | 结果 | 盈亏 | 标签 | 备注 | 操作 | " + "' + "" ); }) .join("") + " |
|---|