Files
sanguo_vnpy_v2/sanguo_portfolio/live_portfolio_proxy.py
T
claude_dev 4a9208db9d
CI/CD / test (push) Successful in 12s
CI/CD / nas-deploy (push) Successful in 34s
CI/CD / nas-verify (push) Successful in 11s
feat(portfolio): B2定寸虚拟化InstancePortfolio代理——策略决策层脱离全账户污染(spec§B2+A2清单issue#29)——①live_portfolio_proxy:InstanceContextProxy(portfolio→账本视图,其余属性透传真context,property+__getattr__实现)+InstancePortfolio(每次访问现算)②覆盖A2清单全集:available_cash/cash=账本现金(168万/只定寸污染根治点,.cash fallback同源);positions=账本jq对象(total_amount/amount/closeable_amount(T+1)/avg_cost(台账口径=成交价)/cost_basis/security)+price/last_sale_price从真portfolio同名标的透传(市场数据非所有权,缺→None止损跳过);value/market_value/total_value=现价×量(缺价→0偏保守不误卖);total_value/positions_value=账本equity(现价优先成本兜底);locked_cash=0③清单外属性AttributeError fail-fast(真值=全账户数字,静默透传=污染复发,宁可崩=A1同款house style)④wrap_scheduler包装facade的run_daily/run_monthly(发生在策略initialize注册任务之前,策略代码零改动);functools.wraps保任务名;无账本原样返回=回测/影子零改动⑤live_strategy._setup接线(_ledger None→wrap直通);引擎内部撮合/风控仍看真实账户;+13测试(定寸现金/T+1/现价透传/缺价保守/equity兜底/fail-fast/调度代理注入/无账本零改动/_setup接线两态);portfolio 373绿 [vps]
2026-08-19 22:37:16 +08:00

194 lines
7.3 KiB
Python

"""B2 定寸虚拟化:InstancePortfolio 代理(spec §multi-strategy-instance-budget §B2)。
共享 QMT 账户下,策略读 ``context.portfolio`` 会看到全账户(995万)→ 定寸被污染
(channel_test 168万/只 vs 影子 16.8万)。本模块把**策略决策层**看到的 portfolio
换成实例账本视图;引擎内部(撮合/风控/下单)仍看真实账户。
覆盖范围(A2 属性清单,策略 session issue#29 核对):
- ``available_cash`` / ``cash``:实例账本现金(定寸污染点,核心)
- ``positions``:账本视图的 jq 风格对象——``total_amount``/``amount``/
``closeable_amount``(T+1)/``avg_cost``/``cost_basis``/``security`` +
``price``/``last_sale_price``(现价从真 portfolio 同名标的透传=市场数据非所有权,
缺则 None)/``value``/``market_value``/``total_value``(现价缺失时 0,策略当 0
处理偏保守不误卖)
- ``total_value``/``positions_value``:账本 equity(现金+Σ市值,现价缺则成本价)
- ``locked_cash``:0(账本无锁现语义)
**未覆盖属性 fail-fast**(AttributeError):A2 确认策略不读;真值是全账户数字,
静默透传=污染复发,宁可崩(house style 同 A1 的 TypeError fail-fast)。
非 portfolio 属性(current_dt/previous_date 等)由 context 代理透传真 context。
"""
from __future__ import annotations
import functools
import logging
from typing import Any, Callable, Dict, Optional
logger = logging.getLogger(__name__)
class InstancePosition:
"""实例持仓的 jq 风格对象(账本数量/成本 + 真实账户现价透传)。"""
def __init__(self, security: str, total_amount: int,
closeable_amount: int, avg_cost: float,
price: Optional[float]) -> None:
self.security = security
self.total_amount = int(total_amount)
self.amount = self.total_amount
self.closeable_amount = int(closeable_amount)
self.avg_cost = float(avg_cost)
self.cost_basis = self.avg_cost
self.price = float(price) if price else None
self.last_sale_price = self.price
@property
def value(self) -> float:
"""现价缺失 → 0(channel_test _safe_value 语义:取不到当 0,偏保守)。"""
return self.price * self.total_amount if self.price else 0.0
@property
def market_value(self) -> float:
return self.value
@property
def total_value(self) -> float:
return self.value
class InstancePortfolio:
"""每次访问现算的实例账本 portfolio 视图。"""
def __init__(self, ledger: Any,
real_portfolio: Callable[[], Any]) -> None:
self._ledger = ledger
self._real_portfolio = real_portfolio
@property
def available_cash(self) -> float:
return float(self._ledger.cash)
@property
def cash(self) -> float:
# 策略族 _available_cash 先读 available_cash 再 fallback .cash——两口径同源
return float(self._ledger.cash)
@property
def positions(self) -> Dict[str, InstancePosition]:
real = self._real_positions()
real_by_sym = _index_real_positions(real)
out: Dict[str, InstancePosition] = {}
for sym, info in self._ledger.positions_view(_today()).items():
rp = real_by_sym.get(sym)
price = None
if rp is not None:
price = (getattr(rp, "price", None)
or getattr(rp, "last_sale_price", None))
out[sym] = InstancePosition(
security=sym,
total_amount=int(info.get("amount", 0)),
closeable_amount=int(info.get("closeable_amount", 0)),
avg_cost=float(info.get("avg_cost", 0.0)),
price=price,
)
return out
@property
def total_value(self) -> float:
real_by_sym = _index_real_positions(self._real_positions())
prices: Dict[str, float] = {}
for sym, rp in real_by_sym.items():
p = getattr(rp, "price", None) or getattr(rp, "last_sale_price", None)
if p:
prices[sym] = float(p)
_cash, _mv, total = self._ledger.equity(prices)
return float(total)
@property
def positions_value(self) -> float:
return self.total_value - float(self._ledger.cash)
@property
def locked_cash(self) -> float:
return 0.0 # 账本无锁现语义(A2:策略不读;给安全值)
# 其余属性(AttributeError fail-fast):真值是全账户数字,透传=污染复发
def __getattr__(self, name: str) -> Any:
raise AttributeError(
f"InstancePortfolio 未覆盖 portfolio.{name}(A2 清单外;"
f"真值=全账户数字,拒绝静默透传——如策略确需,请扩本代理)")
def _real_positions(self) -> Any:
try:
rp = self._real_portfolio()
except Exception: # noqa: BLE001 - 真 portfolio 拿不到 → 无现价可用
return {}
if rp is None:
return {}
try:
return getattr(rp, "positions", None) or {}
except Exception: # noqa: BLE001
return {}
def _index_real_positions(real_positions: Any) -> Dict[str, Any]:
"""真 portfolio.positions(dict 或 list)→ {symbol: position_obj}。"""
out: Dict[str, Any] = {}
if isinstance(real_positions, dict):
for k, v in real_positions.items():
out[str(k)] = v
else:
for p in real_positions or []:
sec = getattr(p, "security", None) or getattr(p, "symbol", None)
if sec:
out[str(sec)] = p
return out
def _today() -> str:
from datetime import date as _date
return _date.today().isoformat()
class InstanceContextProxy:
"""context 代理:``portfolio`` → InstancePortfolio,其余透传真 context。"""
def __init__(self, real_context: Any, ledger: Any) -> None:
object.__setattr__(self, "_real", real_context)
object.__setattr__(self, "_ledger", ledger)
@property
def portfolio(self) -> InstancePortfolio:
real = object.__getattribute__(self, "_real")
ledger = object.__getattribute__(self, "_ledger")
return InstancePortfolio(
ledger, lambda: getattr(real, "portfolio", None))
def __getattr__(self, name: str) -> Any:
return getattr(object.__getattribute__(self, "_real"), name)
def make_proxy_context(context: Any, ledger: Optional[Any]) -> Any:
"""有账本 → 代理 context;无(回测/影子/测试) → 原 context 原样。"""
if ledger is None:
return context
return InstanceContextProxy(context, ledger)
def wrap_scheduler(bt_sched: Callable, ledger: Optional[Any]) -> Callable:
"""包装 run_daily/run_monthly:经其注册的回调收到的 context 换成代理。
策略代码零改动——虚拟化发生在 bullet_trade 调度器 → 策略函数之间。
"""
if ledger is None:
return bt_sched
def sched(func: Callable, *args: Any, **kw: Any) -> Any:
@functools.wraps(func)
def wrapped(context: Any, *cb_args: Any, **cb_kw: Any) -> Any:
return func(make_proxy_context(context, ledger),
*cb_args, **cb_kw)
return bt_sched(wrapped, *args, **kw)
logger.info("定寸虚拟化已启用: run_daily/run_monthly 回调注入实例账本视图")
return sched