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
236 lines
8.8 KiB
Python
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
|