Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions core/portfolio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,31 @@ def __init__(self, config: Config = None, account_key: str = "", initial_capital
if initial_capital is not None:
self.initial_capital = float(initial_capital)
else:
self.initial_capital = self.config.risk_params.get(
"position_sizing", {}
).get("initial_capital", 10000000)
# 바스켓 계정(basket_rebalance:<name>)은 baskets.yaml의 바스켓별 자본이
# 진실이다 — account_key만으로 이 계정을 여는 모든 경로(주문 리스크 가드
# 등)가 전역 자본(10M)으로 폴백하면 분모가 틀어져, 소액 바스켓(예: 30만
# kr_pocket)에서 MDD·일일 손실 한도가 사실상 무력화된다(#426이 평가기는
# 고쳤지만 이 폴백은 남아 있었다 — 6차 점검 E2E에서 실측). 중앙 해석으로
# 현재·미래 호출부를 일괄 커버한다. 해석 실패 시 기존 폴백 유지.
resolved = None
if (self.account_key or "").startswith("basket_rebalance:"):
try:
from core.basket_rebalancer import BasketRebalancer

_basket = self.account_key.split(":", 1)[1]
_cap = (
BasketRebalancer._load_baskets_config().get(_basket) or {}
).get("initial_capital")
resolved = float(_cap) if _cap is not None else None
except Exception as exc:
logger.debug("바스켓 자본 해석 실패(전역 폴백): {}", exc)
self.initial_capital = (
resolved
if resolved is not None
else self.config.risk_params.get(
"position_sizing", {}
).get("initial_capital", 10000000)
)
self._is_live = self.config.trading.get("mode", "paper") == "live"

# Peak value 복구: DB 스냅샷에서 이전 세션의 peak을 가져와 MDD 연속성 유지
Expand Down
48 changes: 48 additions & 0 deletions tests/test_cash_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,54 @@ def _pm(monkeypatch, account, initial=300_000, cash_delta=0.0, deposits=0.0,
return pm


class TestBasketCapitalResolution:
"""바스켓 계정 키만으로 PM을 열어도 baskets.yaml 자본이 분모여야 한다.

6차 점검 E2E 실측: 주문 리스크 가드(_drawdown_pre_order_check)가 account_key만으로
PM을 열어 전역 10M 폴백 → 30만 pocket의 손실 한도가 사실상 무력화되던 결함.
"""

def test_basket_key_resolves_basket_capital(self, monkeypatch):
import core.portfolio_manager as pm_mod

monkeypatch.setattr(pm_mod, "get_latest_peak_value", lambda account_key="": None)
from unittest.mock import patch
with patch(
"core.basket_rebalancer.BasketRebalancer._load_baskets_config",
return_value={"kr_pocket": {"initial_capital": 300_000}},
):
pm = PortfolioManager(account_key="basket_rebalance:kr_pocket")
assert pm.initial_capital == 300_000

def test_unknown_basket_falls_back_to_global(self, monkeypatch):
import core.portfolio_manager as pm_mod

monkeypatch.setattr(pm_mod, "get_latest_peak_value", lambda account_key="": None)
from unittest.mock import patch
with patch(
"core.basket_rebalancer.BasketRebalancer._load_baskets_config",
return_value={},
):
pm = PortfolioManager(account_key="basket_rebalance:no_such")
assert pm.initial_capital >= 1_000_000 # 전역 폴백(설정값)

def test_non_basket_key_unchanged(self, monkeypatch):
import core.portfolio_manager as pm_mod

monkeypatch.setattr(pm_mod, "get_latest_peak_value", lambda account_key="": None)
pm = PortfolioManager(account_key="scoring")
assert pm.initial_capital >= 1_000_000 # 기존 동작 그대로

def test_explicit_capital_still_wins(self, monkeypatch):
import core.portfolio_manager as pm_mod

monkeypatch.setattr(pm_mod, "get_latest_peak_value", lambda account_key="": None)
pm = PortfolioManager(
account_key="basket_rebalance:kr_pocket", initial_capital=777,
)
assert pm.initial_capital == 777


class TestSummaryWithDeposits:
def test_no_flows_keeps_legacy_formula(self, monkeypatch):
# 입금 0건: 총평가/초기자본 그대로 (하위 호환)
Expand Down
Loading