Files
links/tests/test_invest_api.py
T

236 lines
8.6 KiB
Python

from datetime import date
from decimal import Decimal
import json
import pytest
from django.utils import timezone
from invest.models import BenchmarkPrice, CashFlow, Portfolio, PortfolioSnapshot, Stock, Transaction
from invest.services import get_performance_chart_data
from links.models import Post, Tag
@pytest.mark.django_db
def test_transaction_price_fields_are_optional(api_client):
portfolio = Portfolio.objects.create(name="Agent Test")
response = api_client.post(
"/api/invest/transactions/",
{
"portfolio": portfolio.id,
"action": "BUY",
"stock_code": "NVDA",
"quantity": "2",
"date": "2026-06-13",
},
format="json",
)
assert response.status_code == 201
tx = Transaction.objects.get()
assert tx.price_per_share is None
assert tx.currency == "USD"
assert tx.fee is None
assert response.data["price_per_share"] is None
@pytest.mark.django_db
def test_transaction_accepts_optional_price_currency_and_fee(api_client):
portfolio = Portfolio.objects.create(name="Agent Test")
response = api_client.post(
"/api/invest/transactions/",
{
"portfolio": portfolio.id,
"action": "BUY",
"stock_code": "NVDA",
"quantity": "2",
"price_per_share": "100.25",
"currency": "USD",
"fee": "1.50",
"date": "2026-06-13",
},
format="json",
)
assert response.status_code == 201
tx = Transaction.objects.get()
assert tx.price_per_share == Decimal("100.250000")
assert tx.fee == Decimal("1.500000")
assert response.data["price_per_share"] == "100.250000"
assert response.data["fee"] == "1.500000"
@pytest.mark.django_db
def test_cashflow_api_records_external_deposits(api_client):
portfolio = Portfolio.objects.create(name="Agent Test")
response = api_client.post(
"/api/invest/cashflows/",
{
"portfolio": portfolio.id,
"flow_type": "DEPOSIT",
"amount": "2500.00",
"currency": "USD",
"date": "2026-06-13",
"source": "salary",
"note": "monthly contribution",
},
format="json",
)
assert response.status_code == 201
flow = CashFlow.objects.get()
assert flow.signed_amount == Decimal("2500.00")
assert response.data["signed_amount"] == "2500.00"
@pytest.mark.django_db
def test_agent_summary_reports_cash_adjusted_return_and_concentration(api_client, monkeypatch):
portfolio = Portfolio.objects.create(name="Agent Test")
Stock.objects.create(portfolio=portfolio, stock_code="AAA", quantity=Decimal("10"))
Stock.objects.create(portfolio=portfolio, stock_code="BBB", quantity=Decimal("5"))
CashFlow.objects.create(
portfolio=portfolio,
flow_type=CashFlow.FLOW_DEPOSIT,
amount=Decimal("1000.00"),
currency="USD",
date=date(2026, 6, 1),
)
prices = {"AAA": 100.0, "BBB": 20.0}
monkeypatch.setattr("invest.services.get_current_price", lambda ticker: prices[ticker])
response = api_client.get("/api/invest/agent/summary/")
assert response.status_code == 200
payload = response.json()
assert payload["total_value"] == 1100.0
assert payload["performance"]["net_external_cash_flow"] == 1000.0
assert payload["performance"]["cash_adjusted_gain"] == 100.0
assert payload["risk"]["top_1_weight"] == pytest.approx(0.9091, rel=1e-3)
assert payload["risk"]["max_position"]["stock_code"] == "AAA"
assert payload["risk"]["concentration_level"] == "HIGH"
@pytest.mark.django_db
def test_performance_endpoint_excludes_deposits_from_gain(api_client):
portfolio = Portfolio.objects.create(name="Agent Test")
PortfolioSnapshot.objects.create(
portfolio=portfolio,
captured_at=timezone.make_aware(timezone.datetime(2026, 6, 1, 8, 0)),
total_value=Decimal("1000.00"),
)
CashFlow.objects.create(
portfolio=portfolio,
flow_type=CashFlow.FLOW_DEPOSIT,
amount=Decimal("500.00"),
currency="USD",
date=date(2026, 6, 8),
)
PortfolioSnapshot.objects.create(
portfolio=portfolio,
captured_at=timezone.make_aware(timezone.datetime(2026, 6, 15, 8, 0)),
total_value=Decimal("1700.00"),
)
BenchmarkPrice.objects.create(ticker="QQQ", date=date(2026, 6, 1), close=Decimal("100.00"))
BenchmarkPrice.objects.create(ticker="QQQ", date=date(2026, 6, 8), close=Decimal("110.00"))
BenchmarkPrice.objects.create(ticker="QQQ", date=date(2026, 6, 15), close=Decimal("120.00"))
response = api_client.get("/api/invest/performance/?start=2026-06-01&end=2026-06-15")
assert response.status_code == 200
payload = response.json()
assert payload["start_value"] == 1000.0
assert payload["end_value"] == 1700.0
assert payload["net_external_cash_flow"] == 500.0
assert payload["cash_adjusted_gain"] == 200.0
assert payload["simple_return"] == pytest.approx(0.1333, rel=1e-3)
assert payload["benchmarks"]["QQQ"]["end_value"] == pytest.approx(1745.45, rel=1e-3)
@pytest.mark.django_db
def test_dashboard_shows_agent_first_metrics(client, monkeypatch):
portfolio = Portfolio.objects.create(name="Agent Test")
Stock.objects.create(portfolio=portfolio, stock_code="AAA", quantity=Decimal("10"))
CashFlow.objects.create(
portfolio=portfolio,
flow_type=CashFlow.FLOW_DEPOSIT,
amount=Decimal("1000.00"),
currency="USD",
date=date(2026, 6, 1),
)
monkeypatch.setattr("invest.services.get_current_price", lambda ticker: 110.0)
response = client.get("/invest/")
assert response.status_code == 200
content = response.content.decode()
assert "Net Contributions" in content
assert "Investment Gain" in content
assert "Top 5 Concentration" in content
assert "Same-cashflow Benchmark" in content
@pytest.mark.django_db
def test_performance_chart_data_has_percentage_and_value_modes():
portfolio = Portfolio.objects.create(name="Agent Test")
PortfolioSnapshot.objects.create(
portfolio=portfolio,
captured_at=timezone.make_aware(timezone.datetime(2026, 1, 1, 8, 0)),
total_value=Decimal("1000.00"),
)
PortfolioSnapshot.objects.create(
portfolio=portfolio,
captured_at=timezone.make_aware(timezone.datetime(2026, 1, 8, 8, 0)),
total_value=Decimal("1100.00"),
)
BenchmarkPrice.objects.create(ticker="SPY", date=date(2026, 1, 1), close=Decimal("100.00"))
BenchmarkPrice.objects.create(ticker="SPY", date=date(2026, 1, 8), close=Decimal("110.00"))
BenchmarkPrice.objects.create(ticker="QQQ", date=date(2026, 1, 1), close=Decimal("200.00"))
BenchmarkPrice.objects.create(ticker="QQQ", date=date(2026, 1, 8), close=Decimal("220.00"))
payload = json.loads(get_performance_chart_data())
assert payload["baseline_date"] == "2026-01-01"
assert set(payload["modes"]) == {"percentage", "value"}
assert payload["modes"]["percentage"]["unit"] == "percent"
assert payload["modes"]["value"]["unit"] == "currency"
value_datasets = {dataset["label"]: dataset["data"] for dataset in payload["modes"]["value"]["datasets"]}
assert value_datasets["All Portfolios"] == [1000.0, 1100.0]
assert value_datasets["S&P 500 benchmark"] == [1000.0, 1100.0]
assert value_datasets["QQQ benchmark"] == [1000.0, 1100.0]
@pytest.mark.django_db
def test_dashboard_links_posts_tagged_invest_or_investment(client):
invest_tag = Tag.objects.create(name="invest", slug="invest")
investment_tag = Tag.objects.create(name="investment", slug="investment")
other_tag = Tag.objects.create(name="life", slug="life")
invest_post = Post.objects.create(
title="Weekly investment report",
summary="AI generated market and portfolio notes",
content="details",
)
invest_post.tags.add(invest_tag)
legacy_post = Post.objects.create(
title="Legacy investment report",
summary="Saved before the invest tag standard existed",
content="details",
)
legacy_post.tags.add(investment_tag)
other_post = Post.objects.create(title="Cooking note", summary="not shown", content="details")
other_post.tags.add(other_tag)
response = client.get("/invest/")
assert response.status_code == 200
content = response.content.decode()
assert "Investment Reports" in content
assert "Weekly investment report" in content
assert "Legacy investment report" in content
assert invest_post.get_absolute_url() in content
assert legacy_post.get_absolute_url() in content
assert "Cooking note" not in content
assert "View all posts tagged invest" in content