diff --git a/backend/app/api/hold_timing.py b/backend/app/api/hold_timing.py index bc6f527..fb487e9 100644 --- a/backend/app/api/hold_timing.py +++ b/backend/app/api/hold_timing.py @@ -18,10 +18,20 @@ def _ts(v: Any) -> int | None: return n if n > 0 else None -def first_perp_close_ts_ms(fills: Sequence[Mapping[str, Any]]) -> int | None: +def _as_map(row: Mapping[str, Any] | Any) -> Mapping[str, Any]: + if isinstance(row, Mapping): + return row + try: + return dict(row) + except Exception: + return {} + + +def first_perp_close_ts_ms(fills: Sequence[Mapping[str, Any] | Any]) -> int | None: """永续平仓成交时间(目标只平永续时作为持仓结束时刻)。""" best: int | None = None - for f in fills: + for raw in fills: + f = _as_map(raw) if str(f.get("leg") or "") != "perp": continue if str(f.get("action") or "") != "close": @@ -35,7 +45,7 @@ def first_perp_close_ts_ms(fills: Sequence[Mapping[str, Any]]) -> int | None: def hold_timing( - group: Mapping[str, Any], fills: Sequence[Mapping[str, Any]] + group: Mapping[str, Any] | Any, fills: Sequence[Mapping[str, Any] | Any] ) -> dict[str, Any]: """ 返回展示用开仓/平仓/持仓时长。 @@ -45,10 +55,11 @@ def hold_timing( - `target_perp_only` / `option_residual`:永续平仓 fill 时间 - 其它已平:groups.close_at_ms(缺则回退成交) """ - open_ms = _ts(group.get("open_at_ms")) - status = str(group.get("status") or "") - reason = str(group.get("close_reason") or "") - group_close = _ts(group.get("close_at_ms")) + g = _as_map(group) + open_ms = _ts(g.get("open_at_ms")) + status = str(g.get("status") or "") + reason = str(g.get("close_reason") or "") + group_close = _ts(g.get("close_at_ms")) perp_close = first_perp_close_ts_ms(fills) use_perp = reason == "target_perp_only" or status == "option_residual" diff --git a/backend/tests/test_hold_timing.py b/backend/tests/test_hold_timing.py index 79b9c4d..141817a 100644 --- a/backend/tests/test_hold_timing.py +++ b/backend/tests/test_hold_timing.py @@ -55,9 +55,32 @@ def test_hold_dual_leg_uses_group_close(): assert h["hold_basis"] == "group" -def test_hold_open_no_close(): - g = {"open_at_ms": 100, "close_at_ms": None, "status": "open", "close_reason": None} - h = hold_timing(g, []) - assert h["hold_close_at_ms"] is None - assert h["hold_ms"] is None - assert h["hold_basis"] == "open" +def test_hold_sqlite_row_like_without_get(): + """sqlite3.Row 无 .get,需能转 dict。""" + + class Row: + def __init__(self, d): + self._d = d + + def keys(self): + return self._d.keys() + + def __getitem__(self, k): + return self._d[k] + + def __iter__(self): + return iter(self._d) + + g = Row( + { + "open_at_ms": 100, + "close_at_ms": None, + "status": "option_residual", + "close_reason": "target_perp_only", + } + ) + fills = [Row({"leg": "perp", "action": "close", "ts_ms": 400})] + h = hold_timing(g, fills) + assert h["hold_close_at_ms"] == 400 + assert h["hold_ms"] == 300 + diff --git a/docs/更新说明.md b/docs/更新说明.md index dd9154d..ea74477 100644 --- a/docs/更新说明.md +++ b/docs/更新说明.md @@ -5,6 +5,15 @@ --- +## 2026-07-27 — 修复交易记录 500(记录未丢) + +### 变更 + +1. `/api/trades/groups` 因 `sqlite3.Row` 无 `.get` 抛错导致「Internal Server Error / 暂无成交组」假象。 +2. 持仓周期计算改为兼容 Row;库内成交组仍在。 + +--- + ## 2026-07-27 — 交易记录:开仓/平仓时间与持仓周期 ### 变更