#!/usr/bin/env python3 """ Unit tests for Stock Tracker Tests: Portfolio management, P&L calculation, price fetching """ import pytest import sys import os import json from datetime import datetime from unittest.mock import Mock, patch, MagicMock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) TEST_DATA_DIR = '/tmp/test_stock_tracker' os.makedirs(TEST_DATA_DIR, exist_ok=True) @pytest.fixture def mock_prices(): """Create mock price data""" return { 'stock_AAPL': { 'symbol': 'AAPL', 'current_price': 180.0, 'change_percent': 2.5, 'high_52w': 200.0, 'low_52w': 150.0, 'volume': 50000000, 'updated_at': datetime.now().isoformat() }, 'stock_MSFT': { 'symbol': 'MSFT', 'current_price': 380.0, 'change_percent': -1.2, 'high_52w': 420.0, 'low_52w': 310.0, 'volume': 25000000, 'updated_at': datetime.now().isoformat() }, 'crypto_BTC': { 'symbol': 'BTC', 'current_price': 45000.0, 'change_percent': 3.8, 'high_52w': 69000.0, 'low_52w': 35000.0, 'volume': 30000000000, 'updated_at': datetime.now().isoformat() } } @pytest.fixture def mock_positions(): """Create mock positions""" return { 'stock_AAPL': { 'symbol': 'AAPL', 'asset_type': 'stock', 'quantity': 10, 'avg_cost': 150.0, 'entry_date': '2025-01-15', 'streak': 0, 'is_active': True }, 'stock_MSFT': { 'symbol': 'MSFT', 'asset_type': 'stock', 'quantity': 5, 'avg_cost': 350.0, 'entry_date': '2025-02-01', 'streak': 0, 'is_active': True } } class TestPortfolioManagement: """Test portfolio management functionality""" def test_add_position(self, mock_positions): """Test adding a new position""" from stock_tracker import StockTracker, Position # Mock the file operations with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = {} # Add a position result = tracker.add_position( symbol='NVDA', asset_type='stock', quantity=10, avg_cost=800.0 ) assert result == True key = 'stock_NVDA' assert key in tracker.positions assert tracker.positions[key].symbol == 'NVDA' assert tracker.positions[key].quantity == 10 assert tracker.positions[key].avg_cost == 800.0 print(f"✅ Added position: NVDA 10 @ $800") def test_remove_position(self, mock_positions): """Test removing a position""" with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = mock_positions # Remove a position result = tracker.remove_position('AAPL', 'stock') assert result == True assert 'stock_AAPL' not in tracker.positions print("✅ Removed position: AAPL") def test_get_positions(self, mock_positions): """Test getting all positions""" with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = mock_positions positions = tracker.get_positions() assert len(positions) == 2 print(f"✅ Retrieved {len(positions)} positions") class TestPnLCalculation: """Test P&L calculation functionality""" def test_calculate_profit(self, mock_positions, mock_prices): """Test profit calculation for winning position""" with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = mock_positions tracker.prices = mock_prices summary = tracker.calculate_portfolio_summary() # AAPL: Bought @ $150, Current @ $180 = +20% profit assert summary.total_value > summary.total_cost assert summary.total_pnl_percent > 0 print(f"✅ Profit calculated: {summary.total_pnl_percent:.1f}%") def test_calculate_loss(self, mock_positions, mock_prices): """Test loss calculation for losing position""" # Modify MSFT to have a loss mock_positions['stock_MSFT']['avg_cost'] = 400.0 # Bought higher than current with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = mock_positions tracker.prices = mock_prices summary = tracker.calculate_portfolio_summary() # MSFT: Bought @ $400, Current @ $380 = -5% loss msft_pos = next((p for p in summary.positions if p['symbol'] == 'MSFT'), None) assert msft_pos['pnl_percent'] < 0 print(f"✅ Loss calculated: MSFT {msft_pos['pnl_percent']:.1f}%") def test_pnl_percentage(self, mock_positions): """Test P&L percentage calculation""" avg_cost = 100.0 current_price = 150.0 expected_pnl_percent = 50.0 pnl_percent = ((current_price - avg_cost) / avg_cost) * 100 assert pnl_percent == expected_pnl_percent print(f"✅ P&L % calculated: {pnl_percent}%") class TestInvestmentGuidelineChecks: """Test investment guideline compliance""" def test_checklist_score_calculation(self): """Test 7-item checklist scoring""" checklist = { 'story_clear': True, 'earnings_uptrend': True, 'balance_sheet_healthy': True, 'capital_return_plan': True, 'governance_clean': True, 'market_liquidity': True, 'relative_strength': False } score = sum(checklist.values()) max_score = len(checklist) assert score == 6 assert f"{score}/{max_score}" == "6/7" print(f"✅ Checklist score: {score}/{max_score}") def test_pbr_evaluation(self): """Test PBR evaluation logic""" # PBR < 1 is generally considered undervalued pbr_values = { 'AAPL': 0.85, # Undervalued 'MSFT': 1.5, # Fair value 'GOOGL': 2.1, # Premium 'NVDA': 25.0 # Expensive (but justified by growth) } for symbol, pbr in pbr_values.items(): if pbr < 1: status = "undervalued" elif pbr < 3: status = "fair value" else: status = "premium" print(f"✅ {symbol} PBR: {pbr}x ({status})") def test_stop_loss_calculation(self): """Test -10% stop loss calculation""" entry_price = 100000 # KRW # Hard stop loss stop_loss_price = entry_price * 0.9 # -10% assert stop_loss_price == 90000 # Trailing stop (from high) high_price = 120000 trailing_stop = high_price * 0.9 # -10% from high assert trailing_stop == 108000 print(f"✅ Stop loss: {stop_loss_price} (entry: {entry_price})") print(f"✅ Trailing stop: {trailing_stop} (high: {high_price})") class TestReportGeneration: """Test report generation functionality""" def test_daily_report_structure(self, mock_positions, mock_prices): """Test daily report has required sections""" with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = mock_positions tracker.prices = mock_prices report = tracker.generate_daily_report() # Check report contains key sections assert '일일 포트폴리오 리포트' in report or 'Daily' in report assert '총 가치' in report or 'Total Value' in report assert '손익' in report or 'P&L' in report print("✅ Daily report structure verified") def test_weekly_report_structure(self, mock_positions, mock_prices): """Test weekly report has required sections""" with patch('stock_tracker.load_json', return_value={}): with patch('stock_tracker.save_json'): tracker = StockTracker.__new__(StockTracker) tracker.positions = mock_positions tracker.prices = mock_prices report = tracker.generate_weekly_report() # Check report contains key sections assert '주간 포트폴리오 리포트' in report or 'Weekly' in report assert '목표' in report or 'Goal' in report assert '체크리스트' in report or 'Checklist' in report print("✅ Weekly report structure verified") class TestDataTypes: """Test data type validation""" def test_position_validation(self): """Test Position dataclass""" from stock_tracker import Position pos = Position( symbol='TEST', asset_type='stock', quantity=100, avg_cost=50.0, entry_date='2025-01-01' ) assert pos.symbol == 'TEST' assert pos.quantity == 100 assert pos.avg_cost == 50.0 assert pos.is_active == True print("✅ Position validation passed") def test_price_data_validation(self): """Test PriceData dataclass""" from stock_tracker import PriceData price = PriceData( symbol='TEST', current_price=100.0, change_percent=2.5, high_52w=120.0, low_52w=80.0, volume=1000000.0 ) assert price.symbol == 'TEST' assert price.current_price == 100.0 assert price.change_percent == 2.5 print("✅ PriceData validation passed") # Pytest configuration if __name__ == '__main__': pytest.main([__file__, '-v'])