Files
sanguo_vnpy_v2/sanguo_trader/portfolio_paper.py
T
claude_dev f214e2f0e4
CI/CD / test (push) Failing after 12m2s
CI/CD / nas-deploy (push) Has been skipped
CI/CD / nas-verify (push) Has been skipped
fix(live): max_pool 全链默认 30→0(0=不限)——08-24巡检定罪MVP限流泄漏生产,选股池=代码序前30只失真 [vps]
根因(策略session 08-24 午休探针实证):runtime/live_strategy.py SANGUO_LIVE_MAX_POOL
默认30经env注入全部实盘+影子+paper实例,_stock_pool截断成份池为「代码序前30只」:
small_cap「全市场最小市值」实际在000001平安银行等30只固定代码里选(平安银行≈3800亿
出现在小市值买入=market_cap开盘NaN排序失效叠bug);momentum每行业RPS只在代码序前
30里排;value 0/30+零委托史同源。注释自曝「MVP验证用」=限流遗留泄漏生产,上线
首日起全部选股失真。

改动(9处默认位一致30→0;语义0=不限,与策略层max_pool>0才截断一致):
- sanguo_portfolio/live_strategy.py 适配器env默认+docstring
- sanguo_live/runner.py _portfolio_env_for(存量DB显式值不篡改,缺列/0→"0")
- sanguo_trader/shadow/supervisor.py 影子env默认
- sanguo_portfolio/runner_live.py live_env默认
- sanguo_trader/portfolio_paper.py + sanguo_api/routes_paper.py paper默认
- sanguo_api/routes_live.py create setdefault
- frontend live/paper New.vue 表单默认

测试:env mapping三态断言(缺列/0→"0",显式30不篡改)+live_env默认"0"
(RED→GREEN);CI范围642绿。存量实例DB仍存显式30,激活需配套DB迁移,必须与数据
session的get_security_info_batch SQL治本(101s→亚秒)同车部署——池放大×慢SQL=更糟。
2026-08-24 13:34:01 +08:00

149 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""组合策略模拟盘实走(E1,spec §13.2 前后端 session 职责)。
设计:每晚 20:30scheduler 全局 job)对每个 strategy_type=portfolio 的 live
账户,用回测引擎从 start_date **全量重放到今天**,取末日持仓/当日成交/净值落
paper 表。不做增量引擎——回测引擎是单一真相源,避免增量状态与回测口径漂移;
1 年区间全量重放约 2-3 分钟(NAS 26G 库),20:30 后台跑可接受。区间拉长到
数年后可优化为 checkpoint 续跑(见 update_checkpoint)。
"""
from __future__ import annotations
import json
import logging
from datetime import datetime, timedelta
from typing import Any
logger = logging.getLogger(__name__)
def _bj_today() -> str:
"""北京时间今天(NAS 容器 TZ 可能为 UTC)。"""
return (datetime.utcnow() + timedelta(hours=8)).strftime("%Y-%m-%d")
def _nas_provider_config() -> dict[str, Any]:
"""NAS 容器 unified provider 配置(与 portfolio_worker NAS 分支一致)。"""
return {
"db_path": "/volume1/stock/sanguo_vnpy_v2/data_backup/quant_trading.db",
"data_dir": "/volume1/stock/sanguo_vnpy_v2/data",
}
def _provider_config() -> dict[str, Any]:
import os
if os.path.isdir("/app"):
return _nas_provider_config()
# 开发机兜底:默认 data 目录(Mac 无全量数据,step 会因数据缺失跳过)
return {"mode": "backtest"}
def run_portfolio_live_step(db_path: str, account_id: int, today: str | None = None) -> dict[str, Any]:
"""组合实走单日 step:全量重放 → 当日成交/末日持仓/净值 落库。
幂等:当日已结算(checkpoint_date == today 或最新净值日期 == today)则跳过。
"""
import sqlite3
from sanguo_trader.persistence import (
load_checkpoint, load_last_balance, save_daily_balance, save_positions,
save_trade, update_checkpoint,
)
today = today or _bj_today()
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
row = conn.execute("SELECT * FROM paper_accounts WHERE id=?", (account_id,)).fetchone()
if not row:
raise ValueError(f"paper account {account_id} not found")
acc = dict(row)
if (acc.get("strategy_type") or "cta") != "portfolio":
raise ValueError(f"account {account_id} 不是 portfolio 类型")
if acc["status"] != "running":
return {"account_id": account_id, "skipped": f"status={acc['status']}"}
# 幂等:今天已结算过
ck = load_checkpoint(db_path, account_id)
last_bal = load_last_balance(db_path, account_id)
if ck == today or (last_bal and last_bal.get("date") == today):
return {"account_id": account_id, "skipped": "already stepped today"}
strategies = json.loads(acc["strategies"] or "[]")
if not strategies:
raise ValueError("portfolio 账户缺少策略配置")
strategy_name = strategies[0].get("name", "all_weather")
pool = (json.loads(acc["symbols"] or "[]") or ["hs300_subset"])[0]
max_pool = int(strategies[0].get("params", {}).get("max_pool", 0))
benchmark = strategies[0].get("params", {}).get("benchmark", "000300.XSHG")
from sanguo_portfolio.runner_backtest import run_backtest_json
result = run_backtest_json({
"strategy": strategy_name,
"pool": pool,
"max_pool": max_pool,
"start_date": acc["start_date"],
"end_date": today,
"initial_cash": acc["initial_capital"],
"benchmark": benchmark,
"commission_rate": acc.get("rate") or 0.0003,
"stamp_duty_rate": acc.get("stamp_duty_rate") or 0.001,
"min_commission": acc.get("min_commission") or 5.0,
"slippage": acc.get("slippage") or 0.0,
"provider": "unified",
"provider_config": json.dumps(_provider_config()),
})
equity_curve = result.get("equity_curve") or []
if not equity_curve:
return {"account_id": account_id, "skipped": "no equity point (数据未到?)"}
last_point = equity_curve[-1]
if last_point["date"] <= _acc_last_date(last_bal):
return {"account_id": account_id, "skipped": "no new trading day"}
settle_date = last_point["date"]
# 当日成交(回放里 settle_date 发生的全部交易)
n_trades = 0
for t in result.get("trades") or []:
d = str(t.get("datetime") or t.get("date") or "")
if not d.startswith(settle_date):
continue
side = str(t.get("side") or t.get("action") or "").lower()
save_trade(db_path, account_id, {
"strategy_id": strategy_name,
"datetime": d,
"symbol": str(t.get("code", "")),
"direction": "long" if side in ("buy", "open", "") else "short",
"offset": "open" if side in ("buy", "open") else "close",
"match_session": "current_close",
"price": float(t.get("filled_price") or t.get("price") or 0),
"volume": int(float(t.get("filled_amount") or t.get("amount") or 0)),
"commission": float(t.get("commission") or 0),
"stamp_duty": 0.0,
"bar_date": settle_date,
})
n_trades += 1
# 末日持仓 + 净值
stocks = result.get("stocks_selected") or []
positions_value = sum(float(s.get("value") or 0) for s in stocks)
equity = float(last_point["equity"])
save_positions(db_path, account_id, "account", {
str(s["code"]): {"volume": int(float(s.get("amount", 0))),
"frozen": 0,
"avg_price": float(s.get("avg_cost", 0) or 0)}
for s in stocks
}, date=settle_date)
save_daily_balance(db_path, account_id, settle_date,
cash=equity - positions_value,
market_value=positions_value,
total_equity=equity)
update_checkpoint(db_path, account_id, settle_date)
logger.info("portfolio live step aid=%s date=%s trades=%s equity=%.2f",
account_id, settle_date, n_trades, equity)
return {"account_id": account_id, "date": settle_date, "trades": n_trades, "equity": equity}
def _acc_last_date(last_bal: dict | None) -> str:
"""账户已有净值的最新日期(无则空串,任何新数据都算新)。"""
return (last_bal or {}).get("date") or ""