From a2fb7b4dbb4e45a2dc9f698d3b7ccf8cc23407f6 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 25 Sep 2026 14:49:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(portfolio):=20=E5=91=A8=E6=8A=A5=E5=8F=8C?= =?UTF-8?q?=E4=BF=AE=E2=80=94=E2=80=94=E5=9B=9E=E6=B5=8B=E5=9F=BA=E5=87=86?= =?UTF-8?q?=E8=AE=A4=20equity=20=E5=88=97(=E7=94=9F=E4=BA=A7=E5=9B=9B?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=85=A8=E8=A2=AB=20balance-only=20=E6=8B=92?= =?UTF-8?q?)+daily=5Freturns=20=E5=90=8C=E6=97=A5=E5=A4=9A=E8=A1=8C?= =?UTF-8?q?=E5=8F=96=E6=9C=AB=E6=AC=A1(=E5=A4=9A=E5=86=99=E8=80=85?= =?UTF-8?q?=E9=98=B2=E8=A6=86=E5=86=99)=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- sanguo_portfolio/weekly_report.py | 19 ++++++++++++------- tests/portfolio/test_weekly_report.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/sanguo_portfolio/weekly_report.py b/sanguo_portfolio/weekly_report.py index 084db1ec..bc5bf291 100644 --- a/sanguo_portfolio/weekly_report.py +++ b/sanguo_portfolio/weekly_report.py @@ -31,13 +31,16 @@ def _week_start(d: date) -> date: def daily_returns(series: list[tuple[str, float]]) -> dict[str, float]: - """[(date, equity)] → {date: 当日收益率};首日无前值跳过,前值 0 防除零. - - 平盘日(收益 0.0)照记不剔除——剔除会低估 TE(M5 终审裁定). + """[(date, equity)] → {date: 当日收益率};同日多行取末次写入(多写者日末终值), + 首日无前值跳过,前值 0 防除零.平盘日(收益 0.0)照记不剔除——剔除会低估 TE(M5 终审裁定). """ + last: dict[str, float] = {} + for d, eq in series: + last[d] = eq out: dict[str, float] = {} prev: float | None = None - for d, eq in sorted(series): + for d in sorted(last): + eq = last[d] if prev is not None and prev != 0: out[d] = eq / prev - 1.0 prev = eq @@ -57,7 +60,7 @@ def load_actual_daily_returns(db_path: str, account_id: int, def load_backtest_daily_returns(db_path: str, task_id: str) -> dict[str, float] | None: - """回测基准:backtest_stats 最新行 → equity_path JSON(date/balance,orient=records).""" + """回测基准:backtest_stats 最新行 → equity_path JSON(date/balance|equity,orient=records).""" with sqlite3.connect(db_path) as conn: row = conn.execute( "SELECT equity_path FROM backtest_stats WHERE task_id=? " @@ -67,11 +70,13 @@ def load_backtest_daily_returns(db_path: str, try: import pandas as pd ec = pd.read_json(row[0], orient="records") - if "balance" not in ec.columns: + col = "balance" if "balance" in ec.columns else ( + "equity" if "equity" in ec.columns else None) + if col is None: return None dates = pd.to_datetime(ec["date"]).dt.strftime("%Y-%m-%d") series = list(zip(dates.tolist(), - pd.to_numeric(ec["balance"], errors="coerce").tolist())) + pd.to_numeric(ec[col], errors="coerce").tolist())) series = [(d, float(v)) for d, v in series if v == v] # NaN 剔除 return daily_returns(series) if series else None except Exception as exc: diff --git a/tests/portfolio/test_weekly_report.py b/tests/portfolio/test_weekly_report.py index 117dc3a5..8563c1ad 100644 --- a/tests/portfolio/test_weekly_report.py +++ b/tests/portfolio/test_weekly_report.py @@ -84,6 +84,15 @@ def test_daily_returns_keeps_flat_days(): assert r["c"] == pytest.approx(0.01) +def test_daily_returns_dedupes_same_date(): + # 同日多行(paper_daily_balance 每日 2-3 行多写者)取末次后再 diff; + # 不去重则排序后同日第二行覆写成日内 0.0,毒化跨日收益 + r = daily_returns([("2026-09-21", 100.0), ("2026-09-22", 110.0), + ("2026-09-22", 110.0), ("2026-09-23", 121.0)]) + assert r == {"2026-09-22": pytest.approx(0.1), + "2026-09-23": pytest.approx(0.1)} + + def test_tracking_error_aligned_and_none(): a = {"d1": 0.01, "d2": -0.01, "d3": 0.01} b = {"d1": 0.01, "d2": -0.01, "d3": 0.01} @@ -114,6 +123,21 @@ def test_load_backtest_daily_returns(main_db): assert load_backtest_daily_returns(main_db, "nope") is None +def test_backtest_loader_accepts_equity_column(main_db, tmp_path): + # 生产回测 equity JSON 用 equity 列(balance-only 校验把真文件全拒→te/vs 恒 None) + eq_path = str(tmp_path / "bt_equity.json") + with open(eq_path, "w", encoding="utf-8") as f: + json.dump([{"date": "2026-09-21", "equity": 100.0}, + {"date": "2026-09-22", "equity": 101.0}], f) + conn = sqlite3.connect(main_db) + conn.execute("INSERT INTO backtest_stats(task_id, equity_path) " + "VALUES('bt_run_equity',?)", (eq_path,)) + conn.commit() + conn.close() + got = load_backtest_daily_returns(main_db, "bt_run_equity") + assert got == {"2026-09-22": pytest.approx(0.01)} + + def test_load_actual_daily_returns_skips_null_equity(tmp_path): # M4:total_equity NULL 行剔除(不折算成 0 毒化收益链) p = str(tmp_path / "null_eq.db")