""" 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