Files
sanguo_vnpy_v2/sanguo_trader/portfolio_paper.py
T

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", 30))
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 ""