feat(paper/live): 生命周期管理补全: paper停止/恢复(实走20:30 step跳过stopped)/删除(连带净值成交持仓挂单)/编辑(名称标的资金); live删除(运行中拒绝)/编辑(stopped才可改); 前端列表+结果页按钮/编辑弹窗/删除确认 [vps]
This commit is contained in:
@@ -176,3 +176,69 @@ def get_status(aid: int):
|
||||
"strategy_name": acc["strategy_name"], "updated_at": acc["updated_at"],
|
||||
"error_msg": acc.get("error_msg", ""),
|
||||
}
|
||||
|
||||
|
||||
# ===== 生命周期管理补全:删除/编辑(停止/启动已有)=====
|
||||
|
||||
class LiveUpdateRequest(BaseModel):
|
||||
"""可编辑字段。仅 stopped 状态可改(运行中改配置会与 engine 失配)。"""
|
||||
name: str | None = None
|
||||
account: str | None = None
|
||||
vt_symbol: str | None = None
|
||||
strategy_class: str | None = None
|
||||
strategy_name: str | None = None
|
||||
setting: dict | None = None
|
||||
interval: str | None = None
|
||||
|
||||
|
||||
@router.put("/live/{aid}", dependencies=[Depends(verify_token)])
|
||||
def update_live(aid: int, req: LiveUpdateRequest):
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
from sanguo_live.persistence import get_account
|
||||
|
||||
db = _db_path["path"]
|
||||
acc = get_account(db, aid)
|
||||
if not acc:
|
||||
raise HTTPException(404, "account not found")
|
||||
if acc["status"] == "running":
|
||||
raise HTTPException(400, "运行中不可编辑,请先停止实例")
|
||||
sets, args = [], []
|
||||
field_map = {
|
||||
"name": req.name, "account": req.account, "vt_symbol": req.vt_symbol,
|
||||
"strategy_class": req.strategy_class, "strategy_name": req.strategy_name,
|
||||
"interval": req.interval,
|
||||
}
|
||||
for col, v in field_map.items():
|
||||
if v is not None:
|
||||
sets.append(f"{col}=?"); args.append(v)
|
||||
if req.setting is not None:
|
||||
sets.append("setting=?"); args.append(json.dumps(req.setting))
|
||||
if not sets:
|
||||
return {"account_id": aid, "updated": False}
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute(f"UPDATE live_accounts SET {', '.join(sets)} WHERE id=?", (*args, aid))
|
||||
conn.commit()
|
||||
return {"account_id": aid, "updated": True}
|
||||
|
||||
|
||||
@router.delete("/live/{aid}", dependencies=[Depends(verify_token)])
|
||||
def delete_live(aid: int):
|
||||
"""删除实盘实例及其数据。运行中拒绝删除(先 stop)。"""
|
||||
import sqlite3
|
||||
|
||||
from sanguo_live.persistence import get_account
|
||||
|
||||
db = _db_path["path"]
|
||||
acc = get_account(db, aid)
|
||||
if not acc:
|
||||
raise HTTPException(404, "account not found")
|
||||
if acc["status"] == "running":
|
||||
raise HTTPException(400, "运行中不可删除,请先停止实例")
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute("DELETE FROM live_accounts WHERE id=?", (aid,))
|
||||
for t in ("live_trades", "live_positions", "live_balance"):
|
||||
conn.execute(f"DELETE FROM {t} WHERE account_id=?", (aid,))
|
||||
conn.commit()
|
||||
return {"account_id": aid, "deleted": True}
|
||||
|
||||
@@ -4,6 +4,7 @@ create 建 paper_account(持久化配置);GET 查询净值/成交/状态
|
||||
回放执行(engine.run)由 orchestrator 异步触发或容器内同步跑,端到端冒烟在容器
|
||||
(本机无 NAS parquet + vnpy 完整依赖),本模块只做 account 管理 + 查询。
|
||||
"""
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException
|
||||
@@ -164,6 +165,90 @@ def get_pending(aid: int):
|
||||
return load_pending_orders(_db_path["path"], aid)
|
||||
|
||||
|
||||
# ===== 生命周期管理(spec §10 补全:停止/恢复/删除/编辑)=====
|
||||
|
||||
def _get_account_row(db, aid: int) -> dict:
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute("SELECT * FROM paper_accounts WHERE id=?", (aid,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "account not found")
|
||||
return dict(row)
|
||||
|
||||
|
||||
@router.post("/paper/{aid}/stop", dependencies=[Depends(verify_token)])
|
||||
def stop_paper(aid: int):
|
||||
"""停止实走:置 status=stopped,每日 20:30 全局 step 只选 running → 自动跳过。"""
|
||||
from sanguo_trader.persistence import update_account_status
|
||||
|
||||
db = _db_path["path"]
|
||||
acc = _get_account_row(db, aid)
|
||||
if acc.get("mode") != "live":
|
||||
raise HTTPException(400, "仅实走(live)账户支持停止;回放账户为一次性任务")
|
||||
update_account_status(db, aid, "stopped")
|
||||
return {"account_id": aid, "status": "stopped"}
|
||||
|
||||
|
||||
@router.post("/paper/{aid}/resume", dependencies=[Depends(verify_token)])
|
||||
def resume_paper(aid: int):
|
||||
"""恢复实走:次日 20:30 起继续 step。"""
|
||||
from sanguo_trader.persistence import update_account_status
|
||||
|
||||
db = _db_path["path"]
|
||||
acc = _get_account_row(db, aid)
|
||||
if acc.get("mode") != "live":
|
||||
raise HTTPException(400, "仅实走(live)账户支持恢复")
|
||||
update_account_status(db, aid, "running")
|
||||
return {"account_id": aid, "status": "running"}
|
||||
|
||||
|
||||
@router.delete("/paper/{aid}", dependencies=[Depends(verify_token)])
|
||||
def delete_paper(aid: int):
|
||||
"""删除模拟盘账户及其全部数据(净值/成交/持仓/挂单,不可恢复)。"""
|
||||
db = _db_path["path"]
|
||||
_get_account_row(db, aid)
|
||||
tables = ("paper_accounts", "paper_daily_balance", "paper_trades",
|
||||
"paper_positions", "paper_pending_orders", "paper_shadow_orders")
|
||||
with sqlite3.connect(db) as conn:
|
||||
for t in tables:
|
||||
if t == "paper_accounts":
|
||||
conn.execute("DELETE FROM paper_accounts WHERE id=?", (aid,))
|
||||
else:
|
||||
conn.execute(f"DELETE FROM {t} WHERE account_id=?", (aid,))
|
||||
conn.commit()
|
||||
return {"account_id": aid, "deleted": True}
|
||||
|
||||
|
||||
class PaperUpdateRequest(BaseModel):
|
||||
"""可编辑字段(其余字段沿用原值;策略参数/标的改动自下次 step 生效)。"""
|
||||
name: str | None = None
|
||||
symbols: list[str] | None = None
|
||||
strategies: list[StrategyCfg] | None = None
|
||||
initial_capital: float | None = None
|
||||
|
||||
|
||||
@router.put("/paper/{aid}", dependencies=[Depends(verify_token)])
|
||||
def update_paper(aid: int, req: PaperUpdateRequest):
|
||||
db = _db_path["path"]
|
||||
_get_account_row(db, aid)
|
||||
sets, args = [], []
|
||||
if req.name is not None:
|
||||
sets.append("name=?"); args.append(req.name)
|
||||
if req.symbols is not None:
|
||||
sets.append("symbols=?"); args.append(json.dumps(req.symbols))
|
||||
if req.strategies is not None:
|
||||
sets.append("strategies=?")
|
||||
args.append(json.dumps([s.model_dump() for s in req.strategies]))
|
||||
if req.initial_capital is not None:
|
||||
sets.append("initial_capital=?"); args.append(req.initial_capital)
|
||||
if not sets:
|
||||
return {"account_id": aid, "updated": False}
|
||||
with sqlite3.connect(db) as conn:
|
||||
conn.execute(f"UPDATE paper_accounts SET {', '.join(sets)} WHERE id=?", (*args, aid))
|
||||
conn.commit()
|
||||
return {"account_id": aid, "updated": True}
|
||||
|
||||
|
||||
class _DataSourceWrapper:
|
||||
"""包装 iter_bars/fetch_day 给 PaperEngine/live_orchestrator。"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user