penti/backend/tests/test_api_rebalancing.py

172 lines
5.2 KiB
Python
Raw Normal View History

2026-01-31 23:30:51 +09:00
"""
Rebalancing API integration tests
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
@pytest.mark.integration
class TestRebalancingAPI:
"""Rebalancing API endpoint tests"""
def test_calculate_rebalancing_success(
self,
client: TestClient,
sample_portfolio,
sample_assets
):
"""Test successful rebalancing calculation"""
request_data = {
"portfolio_id": str(sample_portfolio.id),
"current_holdings": [
{"ticker": "005930", "quantity": 100},
{"ticker": "000660", "quantity": 50},
{"ticker": "035420", "quantity": 30},
],
"cash": 5000000
}
response = client.post("/api/v1/rebalancing/calculate", json=request_data)
assert response.status_code == 200
data = response.json()
# Check response structure
assert "portfolio" in data
assert "total_value" in data
assert "cash" in data
assert "recommendations" in data
assert "summary" in data
# Check summary
summary = data["summary"]
assert "buy" in summary
assert "sell" in summary
assert "hold" in summary
# Check recommendations
recommendations = data["recommendations"]
assert len(recommendations) == 3
for rec in recommendations:
assert "ticker" in rec
assert "name" in rec
assert "current_price" in rec
assert "current_quantity" in rec
assert "current_value" in rec
assert "current_ratio" in rec
assert "target_ratio" in rec
assert "target_value" in rec
assert "delta_value" in rec
assert "delta_quantity" in rec
assert "action" in rec
assert rec["action"] in ["buy", "sell", "hold"]
def test_calculate_rebalancing_portfolio_not_found(
self,
client: TestClient
):
"""Test rebalancing with non-existent portfolio"""
import uuid
fake_id = str(uuid.uuid4())
request_data = {
"portfolio_id": fake_id,
"current_holdings": [],
"cash": 1000000
}
response = client.post("/api/v1/rebalancing/calculate", json=request_data)
assert response.status_code == 404
def test_calculate_rebalancing_no_cash_no_holdings(
self,
client: TestClient,
sample_portfolio
):
"""Test rebalancing with no cash and no holdings"""
request_data = {
"portfolio_id": str(sample_portfolio.id),
"current_holdings": [
{"ticker": "005930", "quantity": 0},
{"ticker": "000660", "quantity": 0},
{"ticker": "035420", "quantity": 0},
],
"cash": 0
}
response = client.post("/api/v1/rebalancing/calculate", json=request_data)
# Should handle gracefully
if response.status_code == 200:
data = response.json()
assert data["total_value"] == 0
def test_calculate_rebalancing_only_cash(
self,
client: TestClient,
sample_portfolio,
sample_assets
):
"""Test rebalancing with only cash (no holdings)"""
request_data = {
"portfolio_id": str(sample_portfolio.id),
"current_holdings": [
{"ticker": "005930", "quantity": 0},
{"ticker": "000660", "quantity": 0},
{"ticker": "035420", "quantity": 0},
],
"cash": 10000000
}
response = client.post("/api/v1/rebalancing/calculate", json=request_data)
assert response.status_code == 200
data = response.json()
# All should be buy recommendations
recommendations = data["recommendations"]
buy_count = sum(1 for r in recommendations if r["action"] == "buy")
assert buy_count > 0
def test_calculate_rebalancing_missing_holdings(
self,
client: TestClient,
sample_portfolio
):
"""Test rebalancing with incomplete holdings list"""
request_data = {
"portfolio_id": str(sample_portfolio.id),
"current_holdings": [
{"ticker": "005930", "quantity": 100},
# Missing other tickers
],
"cash": 1000000
}
response = client.post("/api/v1/rebalancing/calculate", json=request_data)
# Should handle missing tickers (treat as 0 quantity)
assert response.status_code == 200
def test_calculate_rebalancing_invalid_ticker(
self,
client: TestClient,
sample_portfolio
):
"""Test rebalancing with invalid ticker in holdings"""
request_data = {
"portfolio_id": str(sample_portfolio.id),
"current_holdings": [
{"ticker": "999999", "quantity": 100},
],
"cash": 1000000
}
response = client.post("/api/v1/rebalancing/calculate", json=request_data)
# Should fail validation or ignore invalid ticker
assert response.status_code in [200, 400, 404]