galaxis-po/backend/tests/unit/test_journal.py
머니페니 12d235a1f1 feat: add 9 new modules - notification alerts, trading journal, position sizing, pension allocation, drawdown monitoring, benchmark dashboard, tax simulation, correlation analysis, parameter optimizer
Phase 1:
- Real-time signal alerts (Discord/Telegram webhook)
- Trading journal with entry/exit tracking
- Position sizing calculator (Fixed/Kelly/ATR)

Phase 2:
- Pension asset allocation (DC/IRP 70% risk limit)
- Drawdown monitoring with SVG gauge
- Benchmark dashboard (portfolio vs KOSPI vs deposit)

Phase 3:
- Tax benefit simulation (Korean pension tax rules)
- Correlation matrix heatmap
- Parameter optimizer with grid search + overfit detection
2026-03-29 10:03:08 +09:00

236 lines
8.8 KiB
Python

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