galaxis-po/backend/tests/e2e/test_rebalance_flow.py
zephyrdark eb06dfc48b
All checks were successful
Deploy to Production / deploy (push) Successful in 1m32s
feat: implement scenario gap analysis - core loop completion
Phase 1 (Critical):
- Add bulk rebalance apply API + UI with confirmation modal
- Add strategy results to portfolio targets flow (shared component)

Phase 2 (Important):
- Show current holdings in signal execute modal with auto-fill
- Add DC pension risk asset ratio warning (70% limit)
- Add KOSPI benchmark comparison to portfolio returns
- Track signal execution details (price, quantity, timestamp)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:18:15 +09:00

161 lines
5.2 KiB
Python

"""
E2E tests for rebalancing calculation flow.
"""
from fastapi.testclient import TestClient
def _setup_portfolio_with_holdings(client: TestClient, auth_headers: dict) -> int:
"""Helper: create portfolio with targets and holdings."""
# Create portfolio
resp = client.post(
"/api/portfolios",
json={"name": "Rebalance Test", "portfolio_type": "pension"},
headers=auth_headers,
)
pid = resp.json()["id"]
# Set targets (sum = 100)
client.put(
f"/api/portfolios/{pid}/targets",
json=[
{"ticker": "069500", "target_ratio": 50},
{"ticker": "148070", "target_ratio": 50},
],
headers=auth_headers,
)
# Set holdings
client.put(
f"/api/portfolios/{pid}/holdings",
json=[
{"ticker": "069500", "quantity": 10, "avg_price": 40000},
{"ticker": "148070", "quantity": 5, "avg_price": 100000},
],
headers=auth_headers,
)
return pid
def test_calculate_rebalance_with_manual_prices(client: TestClient, auth_headers):
"""Test rebalance calculation with manually provided prices."""
pid = _setup_portfolio_with_holdings(client, auth_headers)
response = client.post(
f"/api/portfolios/{pid}/rebalance/calculate",
json={
"strategy": "full_rebalance",
"prices": {"069500": 50000, "148070": 110000},
},
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["portfolio_id"] == pid
assert float(data["total_assets"]) > 0
assert len(data["items"]) == 2
# Verify items have required fields
item = data["items"][0]
assert "ticker" in item
assert "target_ratio" in item
assert "current_ratio" in item
assert "diff_quantity" in item
assert "action" in item
assert "change_vs_prev_month" in item
def test_calculate_additional_buy_strategy(client: TestClient, auth_headers):
"""Test additional buy strategy: buy-only, no sells."""
pid = _setup_portfolio_with_holdings(client, auth_headers)
response = client.post(
f"/api/portfolios/{pid}/rebalance/calculate",
json={
"strategy": "additional_buy",
"prices": {"069500": 50000, "148070": 110000},
"additional_amount": 1000000,
},
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert float(data["total_assets"]) > 0
assert float(data["available_to_buy"]) == 1000000
# Additional buy should never have "sell" actions
for item in data["items"]:
assert item["action"] in ("buy", "hold")
def test_additional_buy_requires_amount(client: TestClient, auth_headers):
"""Test that additional_buy strategy requires additional_amount."""
pid = _setup_portfolio_with_holdings(client, auth_headers)
response = client.post(
f"/api/portfolios/{pid}/rebalance/calculate",
json={
"strategy": "additional_buy",
"prices": {"069500": 50000, "148070": 110000},
},
headers=auth_headers,
)
assert response.status_code == 400
def test_calculate_rebalance_without_prices_fallback(client: TestClient, auth_headers):
"""Test rebalance calculation without manual prices falls back to DB."""
pid = _setup_portfolio_with_holdings(client, auth_headers)
# Without prices, should still work (may have 0 prices from DB in test env)
response = client.post(
f"/api/portfolios/{pid}/rebalance/calculate",
json={"strategy": "full_rebalance"},
headers=auth_headers,
)
assert response.status_code == 200
def test_apply_rebalance(client: TestClient, auth_headers):
"""리밸런싱 결과를 적용하면 거래가 일괄 생성된다."""
pid = _setup_portfolio_with_holdings(client, auth_headers)
response = client.post(
f"/api/portfolios/{pid}/rebalance/apply",
json={
"items": [
{"ticker": "069500", "action": "buy", "quantity": 5, "price": 50000},
{"ticker": "148070", "action": "sell", "quantity": 2, "price": 110000},
]
},
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert len(data["transactions"]) == 2
assert data["transactions"][0]["tx_type"] == "buy"
assert data["transactions"][1]["tx_type"] == "sell"
assert data["holdings_updated"] == 2
# Verify holdings were updated
holdings_resp = client.get(
f"/api/portfolios/{pid}/holdings",
headers=auth_headers,
)
holdings = {h["ticker"]: h for h in holdings_resp.json()}
assert holdings["069500"]["quantity"] == 15 # 10 + 5
assert holdings["148070"]["quantity"] == 3 # 5 - 2
def test_apply_rebalance_insufficient_quantity(client: TestClient, auth_headers):
"""매도 수량이 보유량을 초과하면 400 에러."""
pid = _setup_portfolio_with_holdings(client, auth_headers)
response = client.post(
f"/api/portfolios/{pid}/rebalance/apply",
json={
"items": [
{"ticker": "148070", "action": "sell", "quantity": 10, "price": 110000},
]
},
headers=auth_headers,
)
assert response.status_code == 400