172 lines
5.2 KiB
Python
172 lines
5.2 KiB
Python
"""
|
|
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]
|