galaxis-po/backend/tests/unit/test_journal.py

236 lines
8.8 KiB
Python
Raw Permalink Normal View History

"""
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