From 930e6c26c5e08b992d78c6960b4e76ffccca9ee7 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 7 Aug 2026 14:49:51 +0800 Subject: [PATCH] Clear trade history when resetting sim equity. Deleting groups/fills/residuals and resetting strategy counters keeps funds reset a clean slate. Co-authored-by: Cursor --- backend/app/sim/ledger.py | 32 +++++++++- .../tests/test_reset_equity_clears_trades.py | 62 +++++++++++++++++++ frontend/src/pages/Settings.tsx | 4 +- 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 backend/tests/test_reset_equity_clears_trades.py diff --git a/backend/app/sim/ledger.py b/backend/app/sim/ledger.py index 136a1b0..43e60f9 100644 --- a/backend/app/sim/ledger.py +++ b/backend/app/sim/ledger.py @@ -64,12 +64,42 @@ class Ledger: pass return equity + def clear_trade_history(self) -> None: + """清空交易记录与持仓痕迹(组/成交/残留/账本流水),仓位置 flat。""" + now = int(time.time() * 1000) + with self.db._lock: + self.db._conn.execute("DELETE FROM fills") + self.db._conn.execute("DELETE FROM residual_options") + self.db._conn.execute("DELETE FROM groups") + self.db._conn.execute("DELETE FROM ledger_entries") + self.db._conn.execute( + """UPDATE positions SET + group_id=NULL, perp_side=NULL, perp_qty_eth=0, perp_entry_px=NULL, + option_inst_id=NULL, option_side=NULL, option_qty_eth=0, + option_qty_contracts=0, option_entry_px=NULL, entry_index_px=NULL, + initial_premium=0, exit_target_usdt=NULL, status='flat' + WHERE id=1""" + ) + self.db._conn.execute( + """UPDATE strategy_state SET + rounds_done=0, window_key=NULL, rest_until_ms=NULL, + last_error=NULL, phase=CASE WHEN running=1 THEN phase ELSE 'idle' END, + updated_at_ms=? + WHERE id=1""", + (now,), + ) + self.db._conn.execute( + "DELETE FROM settings WHERE key=?", ("risk_last_k",) + ) + self.db._conn.commit() + def reset_equity(self, amount: float, *, note: str = "重置模拟资金") -> float: - """将权益与可用资金重置为 amount(reserved 清零)。须在无持仓时调用。""" + """将权益与可用资金重置为 amount(reserved 清零),并清空交易记录。须在无持仓时调用。""" now = int(time.time() * 1000) amt = float(amount) if amt < 0: raise ValueError("模拟资金不能为负") + self.clear_trade_history() with self.db._lock: self.db._conn.execute( "UPDATE ledger_meta SET equity=?, available=?, reserved=0, updated_at_ms=? WHERE id=1", diff --git a/backend/tests/test_reset_equity_clears_trades.py b/backend/tests/test_reset_equity_clears_trades.py new file mode 100644 index 0000000..18d01ce --- /dev/null +++ b/backend/tests/test_reset_equity_clears_trades.py @@ -0,0 +1,62 @@ +"""模拟资金重置时同步清空交易记录。""" + +from __future__ import annotations + +from app.models.db import Database +from app.sim.ledger import Ledger + + +def test_reset_equity_clears_trade_history(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("MODE", "SIM") + db = Database(tmp_path / "reset.db") + with db._lock: + db._conn.execute( + """INSERT INTO groups(group_id, status, open_at_ms, close_at_ms, realized_pnl) + VALUES ('G1','closed',1,2,-5.0)""" + ) + db._conn.execute( + """INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, + fill_px, fee, slip, notional, ts_ms) + VALUES ('G1','perp','open','long','ETH-SWAP',1,2000,0.1,0,2000,1)""" + ) + db._conn.execute( + """INSERT INTO residual_options( + group_id, option_inst_id, option_side, option_qty_eth, + option_entry_px, status, created_at_ms + ) VALUES ('G1','OPT','call',2,10,'pending',1)""" + ) + db._conn.execute( + """INSERT INTO ledger_entries(group_id, kind, amount, balance_after, note, ts_ms) + VALUES ('G1','pnl',-5,9995,'t',2)""" + ) + db._conn.execute( + """UPDATE strategy_state SET rounds_done=3, window_key='w', rest_until_ms=99 + WHERE id=1""" + ) + db._conn.commit() + db.set_setting("risk_last_k", "1.5") + + Ledger(db).reset_equity(12000.0, note="test reset") + + assert db.fetchone("SELECT COUNT(*) AS c FROM groups")["c"] == 0 + assert db.fetchone("SELECT COUNT(*) AS c FROM fills")["c"] == 0 + assert db.fetchone("SELECT COUNT(*) AS c FROM residual_options")["c"] == 0 + entries = db.fetchall("SELECT kind, amount FROM ledger_entries") + assert len(entries) == 1 + assert entries[0]["kind"] == "reset" + assert float(entries[0]["amount"]) == 12000.0 + pos = db.fetchone("SELECT status, group_id FROM positions WHERE id=1") + assert pos["status"] == "flat" + assert pos["group_id"] is None + st = db.fetchone( + "SELECT rounds_done, window_key, rest_until_ms FROM strategy_state WHERE id=1" + ) + assert int(st["rounds_done"]) == 0 + assert st["window_key"] is None + assert st["rest_until_ms"] is None + assert db.get_setting("risk_last_k") is None + led = db.fetchone("SELECT equity, available, reserved FROM ledger_meta WHERE id=1") + assert float(led["equity"]) == 12000.0 + assert float(led["available"]) == 12000.0 + assert float(led["reserved"]) == 0.0 + db.close() diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index c757d79..8d899f8 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -419,7 +419,7 @@ export default function SettingsPage() { setStratOk( isLive ? "策略参数已保存(实盘模式不改模拟资金)" - : "策略参数已保存(切换交易所/改模拟资金需无持仓;切换后会重连行情)", + : "策略参数已保存(改模拟资金会重置权益并清空交易记录,须无持仓;切换交易所后会重连行情)", ); } catch (ex) { setErr(ex instanceof Error ? ex.message : String(ex)); @@ -1405,7 +1405,7 @@ export default function SettingsPage() {
  • 交易所:有持仓时不可切换。切换后套用该所合约并重连行情。
  • {mode !== "LIVE" ? (
  • - 模拟资金:仅 SIM 显示。保存后重置权益(须无持仓)。默认 + 模拟资金:仅 SIM 显示。保存且金额变更后重置权益,并清空交易记录(须无持仓)。默认 10000。
  • ) : null}