194 lines
7.3 KiB
Python
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
|