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