""" Tests for trading journal models and API endpoints. """ import pytest from datetime import date, datetime from decimal import Decimal # --- API endpoint tests --- class TestJournalAPI: def _create_journal(self, client, auth_headers, **overrides): payload = { "stock_code": "005930", "stock_name": "삼성전자", "trade_type": "buy", "entry_price": 72000, "target_price": 75600, "stop_loss_price": 69840, "entry_date": "2026-03-20", "quantity": 10, "entry_reason": "KJB 매수 신호 - 돌파 패턴", "scenario": "목표가 75,600 도달 시 전량 매도, 손절가 69,840 이탈 시 손절", **overrides, } return client.post("/api/journal", headers=auth_headers, json=payload) def test_create_journal(self, client, auth_headers): response = self._create_journal(client, auth_headers) assert response.status_code == 201 data = response.json() assert data["stock_code"] == "005930" assert data["stock_name"] == "삼성전자" assert data["trade_type"] == "buy" assert data["entry_price"] == 72000 assert data["status"] == "open" assert data["entry_reason"] == "KJB 매수 신호 - 돌파 패턴" assert data["scenario"] is not None def test_create_journal_minimal(self, client, auth_headers): response = client.post( "/api/journal", headers=auth_headers, json={ "stock_code": "035420", "trade_type": "sell", "entry_date": "2026-03-20", }, ) assert response.status_code == 201 data = response.json() assert data["stock_code"] == "035420" assert data["trade_type"] == "sell" assert data["status"] == "open" def test_list_journals(self, client, auth_headers): self._create_journal(client, auth_headers, stock_code="005930") self._create_journal(client, auth_headers, stock_code="035420", stock_name="NAVER") response = client.get("/api/journal", headers=auth_headers) assert response.status_code == 200 data = response.json() assert len(data) == 2 def test_list_journals_filter_by_status(self, client, auth_headers): self._create_journal(client, auth_headers) response = client.get("/api/journal?status=open", headers=auth_headers) assert response.status_code == 200 assert len(response.json()) == 1 response = client.get("/api/journal?status=closed", headers=auth_headers) assert response.status_code == 200 assert len(response.json()) == 0 def test_list_journals_filter_by_stock_code(self, client, auth_headers): self._create_journal(client, auth_headers, stock_code="005930") self._create_journal(client, auth_headers, stock_code="035420", stock_name="NAVER") response = client.get("/api/journal?stock_code=005930", headers=auth_headers) assert response.status_code == 200 assert len(response.json()) == 1 assert response.json()[0]["stock_code"] == "005930" def test_list_journals_filter_by_date_range(self, client, auth_headers): self._create_journal(client, auth_headers, entry_date="2026-03-15") self._create_journal(client, auth_headers, entry_date="2026-03-25") response = client.get( "/api/journal?start_date=2026-03-20&end_date=2026-03-31", headers=auth_headers, ) assert response.status_code == 200 assert len(response.json()) == 1 def test_get_journal(self, client, auth_headers): create_resp = self._create_journal(client, auth_headers) journal_id = create_resp.json()["id"] response = client.get(f"/api/journal/{journal_id}", headers=auth_headers) assert response.status_code == 200 assert response.json()["id"] == journal_id def test_get_journal_not_found(self, client, auth_headers): response = client.get("/api/journal/9999", headers=auth_headers) assert response.status_code == 404 def test_update_journal_add_exit(self, client, auth_headers): create_resp = self._create_journal(client, auth_headers) journal_id = create_resp.json()["id"] response = client.put( f"/api/journal/{journal_id}", headers=auth_headers, json={ "exit_price": 75600, "exit_date": "2026-03-28", "exit_reason": "목표가 도달", "lessons_learned": "시나리오대로 진행된 좋은 거래", }, ) assert response.status_code == 200 data = response.json() assert data["exit_price"] == 75600 assert data["status"] == "closed" assert data["profit_loss"] is not None assert data["profit_loss_pct"] is not None # Buy: (75600 - 72000) / 72000 * 100 = 5.0 assert abs(data["profit_loss_pct"] - 5.0) < 0.01 # Profit: (75600 - 72000) * 10 = 36000 assert abs(data["profit_loss"] - 36000) < 1 def test_update_journal_sell_pnl_calculation(self, client, auth_headers): create_resp = self._create_journal( client, auth_headers, trade_type="sell", entry_price=75000, quantity=5, ) journal_id = create_resp.json()["id"] response = client.put( f"/api/journal/{journal_id}", headers=auth_headers, json={ "exit_price": 72000, "exit_date": "2026-03-28", }, ) data = response.json() # Sell: (75000 - 72000) / 75000 * 100 = 4.0 assert abs(data["profit_loss_pct"] - 4.0) < 0.01 # Profit: (75000 - 72000) * 5 = 15000 assert abs(data["profit_loss"] - 15000) < 1 def test_update_journal_not_found(self, client, auth_headers): response = client.put( "/api/journal/9999", headers=auth_headers, json={"lessons_learned": "test"}, ) assert response.status_code == 404 def test_get_stats_empty(self, client, auth_headers): response = client.get("/api/journal/stats", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["total_trades"] == 0 assert data["win_rate"] is None def test_get_stats_with_data(self, client, auth_headers): # Create and close a winning trade r1 = self._create_journal(client, auth_headers, entry_price=72000, quantity=10) client.put( f"/api/journal/{r1.json()['id']}", headers=auth_headers, json={"exit_price": 75600, "exit_date": "2026-03-28"}, ) # Create and close a losing trade r2 = self._create_journal( client, auth_headers, stock_code="035420", stock_name="NAVER", entry_price=50000, quantity=5, ) client.put( f"/api/journal/{r2.json()['id']}", headers=auth_headers, json={"exit_price": 48000, "exit_date": "2026-03-28"}, ) # Create an open trade self._create_journal(client, auth_headers, stock_code="000660") response = client.get("/api/journal/stats", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["total_trades"] == 3 assert data["open_trades"] == 1 assert data["closed_trades"] == 2 assert data["win_count"] == 1 assert data["loss_count"] == 1 assert data["win_rate"] == 50.0 assert data["total_profit_loss"] is not None def test_unauthenticated_access(self, client): response = client.get("/api/journal") assert response.status_code == 401 def test_user_isolation(self, client, auth_headers, db): """User can only see their own journals.""" from app.models.user import User from app.core.security import get_password_hash, create_access_token # Create another user's journal other_user = User( username="otheruser", email="other@example.com", hashed_password=get_password_hash("password"), ) db.add(other_user) db.commit() db.refresh(other_user) other_token = create_access_token(data={"sub": other_user.username}) other_headers = {"Authorization": f"Bearer {other_token}"} self._create_journal(client, other_headers, stock_code="000660") # Current user should see 0 journals response = client.get("/api/journal", headers=auth_headers) assert response.status_code == 200 assert len(response.json()) == 0