mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
236 lines
8.6 KiB
Python
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
|