Files
openclaw-workspace/stock_tracker.py
Joungmin 63e7a2ba32 Add: Real market data with yfinance
- stock_tracker.py now uses yfinance for real prices
- get_market_indices(): KOSPI, S&P 500, NASDAQ, DOW
- get_crypto_price(): BTC, ETH, SOL with 52W range
- CLI commands: 'python stock_tracker.py market' and 'crypto'

Features:
- Live prices from Yahoo Finance
- Market indices tracking
- Cryptocurrency prices
- 52-week high/low
- Daily change percentage

Example usage:
  python stock_tracker.py market   # Show indices
  python stock_tracker.py crypto --symbol BTC  # BTC price
2026-02-19 04:11:30 +09:00

505 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Stock & Crypto Portfolio Tracker
Features:
- Track stocks and crypto prices
- Calculate portfolio P&L
- Compare against market indices
- Generate investment recommendations based on guidelines
- Daily/weekly reports
"""
import os
import json
import datetime
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Optional, Tuple
from enum import Enum
import requests
# Try import yfinance for real market data
try:
import yfinance as yf
YFINANCE_AVAILABLE = True
except ImportError:
YFINANCE_AVAILABLE = False
print("⚠️ yfinance not installed. Run: pip install yfinance")
# Configuration
DATA_DIR = os.environ.get('PORTFOLIO_DATA_DIR', '/tmp/portfolio')
os.makedirs(DATA_DIR, exist_ok=True)
PORTFOLIO_FILE = os.path.join(DATA_DIR, 'portfolio.json')
PRICES_FILE = os.path.join(DATA_DIR, 'prices.json')
PERFORMANCE_FILE = os.path.join(DATA_DIR, 'performance.json')
# Market API (placeholder - would use real API)
YAHOO_API = "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
KRX_API = "https://api.ksicore.net/v1/stock/quote"
class AssetType(Enum):
STOCK = "stock"
CRYPTO = "crypto"
ETF = "etf"
@dataclass
class Position:
symbol: str
asset_type: str
quantity: float
avg_cost: float
entry_date: str = ""
notes: str = ""
@dataclass
class PriceData:
symbol: str
current_price: float
change_percent: float
high_52w: float = 0
low_52w: float = 0
volume: float = 0
updated_at: str = ""
@dataclass
class PortfolioSummary:
total_value: float
total_cost: float
total_pnl: float
total_pnl_percent: float
positions: List[Dict]
top_performer: Optional[Dict]
worst_performer: Optional[Dict]
market_comparison: Dict = field(default_factory=dict)
class StockTracker:
"""Stock & Crypto Portfolio Tracker"""
def __init__(self):
self.positions = self._load_positions()
self.prices = self._load_prices()
# ============== Data Management ==============
def _load_positions(self) -> Dict[str, Position]:
if os.path.exists(PORTFOLIO_FILE):
with open(PORTFOLIO_FILE, 'r') as f:
data = json.load(f)
return {k: Position(**v) for k, v in data.items()}
return {}
def _save_positions(self):
with open(PORTFOLIO_FILE, 'w') as f:
json.dump({k: asdict(v) for k, v in self.positions.items()}, f, indent=2)
def _load_prices(self) -> Dict[str, PriceData]:
if os.path.exists(PRICES_FILE):
with open(PRICES_FILE, 'r') as f:
return {k: PriceData(**v) for k, v in json.load(f).items()}
return {}
def _save_prices(self):
with open(PRICES_FILE, 'w') as f:
json.dump({k: asdict(v) for k, v in self.prices.items()}, f, indent=2)
# ============== Portfolio Management ==============
def add_position(self, symbol: str, asset_type: str, quantity: float,
avg_cost: float, entry_date: str = "", notes: str = "") -> bool:
"""Add a new position to portfolio"""
try:
key = f"{asset_type}_{symbol.upper()}"
self.positions[key] = Position(
symbol=symbol.upper(),
asset_type=asset_type,
quantity=quantity,
avg_cost=avg_cost,
entry_date=entry_date or datetime.datetime.now().strftime('%Y-%m-%d'),
notes=notes
)
self._save_positions()
return True
except Exception as e:
print(f"Error adding position: {e}")
return False
def remove_position(self, symbol: str, asset_type: str) -> bool:
"""Remove a position from portfolio"""
key = f"{asset_type}_{symbol.upper()}"
if key in self.positions:
del self.positions[key]
self._save_positions()
return True
return False
def get_positions(self) -> List[Position]:
return list(self.positions.values())
# ============== Price Fetching ==============
def fetch_price(self, symbol: str) -> Optional[PriceData]:
"""Fetch current price for a symbol using yfinance"""
if YFINANCE_AVAILABLE:
try:
# Add .KS for Korean stocks, normal for others
ticker = yf.Ticker(symbol)
info = ticker.info
current_price = info.get('currentPrice', info.get('regularMarketPrice', 0))
change_percent = info.get('regularMarketChangePercent', 0) * 100
high_52w = info.get('fiftyTwoWeekHigh', 0)
low_52w = info.get('fiftyTwoWeekLow', 0)
volume = info.get('volume', 0)
return PriceData(
symbol=symbol,
current_price=current_price,
change_percent=change_percent,
high_52w=high_52w,
low_52w=low_52w,
volume=volume,
updated_at=datetime.datetime.now().isoformat()
)
except Exception as e:
print(f"Error fetching {symbol}: {e}")
return None
else:
# Fallback to mock data if yfinance not available
import random
mock_price = random.uniform(10000, 500000)
mock_change = random.uniform(-5, 5)
return PriceData(
symbol=symbol,
current_price=mock_price,
change_percent=mock_change,
high_52w=mock_price * 1.2,
low_52w=mock_price * 0.8,
volume=random.uniform(100000, 10000000),
updated_at=datetime.datetime.now().isoformat()
)
def update_prices(self) -> Dict[str, PriceData]:
"""Update all prices"""
for key, pos in self.positions.items():
price = self.fetch_price(pos.symbol)
if price:
self.prices[key] = price
self._save_prices()
return self.prices
# ============== Performance Calculation ==============
def calculate_portfolio_summary(self) -> PortfolioSummary:
"""Calculate portfolio summary with P&L"""
total_value = 0
total_cost = 0
positions_data = []
for key, pos in self.positions.items():
price = self.prices.get(key)
if price:
current_value = pos.quantity * price.current_price
cost_basis = pos.quantity * pos.avg_cost
pnl = current_value - cost_basis
pnl_percent = (pnl / cost_basis) * 100 if cost_basis > 0 else 0
positions_data.append({
'symbol': pos.symbol,
'type': pos.asset_type,
'quantity': pos.quantity,
'avg_cost': pos.avg_cost,
'current_price': price.current_price,
'current_value': current_value,
'cost_basis': cost_basis,
'pnl': pnl,
'pnl_percent': pnl_percent,
'change_24h': price.change_percent,
'52w_high': price.high_52w,
'52w_low': price.low_52w,
})
total_value += current_value
total_cost += cost_basis
total_pnl = total_value - total_cost
total_pnl_percent = (total_pnl / total_cost) * 100 if total_cost > 0 else 0
# Top/Worst performers
sorted_positions = sorted(positions_data, key=lambda x: x['pnl_percent'], reverse=True)
top_performer = sorted_positions[0] if sorted_positions else None
worst_performer = sorted_positions[-1] if sorted_positions else None
return PortfolioSummary(
total_value=total_value,
total_cost=total_cost,
total_pnl=total_pnl,
total_pnl_percent=total_pnl_percent,
positions=positions_data,
top_performer=top_performer,
worst_performer=worst_performer
)
# ============== Investment Recommendations ==============
def check_investment_guidelines(self, symbol: str) -> Dict:
"""
Check if a stock meets investment guidelines
Reference: 주식 투자 원칙 가이드.md
"""
# Placeholder - would fetch real data
return {
'symbol': symbol,
'pbr': None, # Would fetch from data source
'roe': None,
'per': None,
'score': None,
'recommendation': None,
'checklist': {
'story_clear': False,
'earnings_uptrend': False,
'balance_sheet_healthy': False,
'capital_return_plan': False,
'governance_clean': False,
'market_liquidity': False,
'relative_strength': False,
}
}
def get_recommendation(self) -> List[Dict]:
"""Generate investment recommendations based on guidelines"""
# Filter positions that meet criteria
recommendations = []
for key, pos in self.positions.items():
if pos.asset_type == 'stock':
analysis = self.check_investment_guidelines(pos.symbol)
recommendations.append({
'symbol': pos.symbol,
'action': 'HOLD',
'reason': 'Review weekly',
'checklist_score': f"{sum(analysis['checklist'].values())}/7",
'pnl_percent': self._get_position_pnl(key)
})
return recommendations
def _get_position_pnl(self, key: str) -> float:
pos = self.positions.get(key)
price = self.prices.get(key)
if pos and price:
return ((price.current_price - pos.avg_cost) / pos.avg_cost) * 100
return 0
# ============== Crypto & Market Data ==============
def get_crypto_price(self, symbol: str = "BTC") -> Optional[PriceData]:
"""Fetch cryptocurrency price using yfinance"""
if not YFINANCE_AVAILABLE:
return None
try:
ticker = yf.Ticker(f"{symbol}-USD")
hist = ticker.history(period='1d')
hist_1d = ticker.history(period='2d') # Get 2 days for change
if hist.empty:
return None
current_price = hist['Close'].iloc[-1]
prev_close = hist_1d['Close'].iloc[0] if len(hist_1d) > 1 else current_price
change_percent = ((current_price - prev_close) / prev_close) * 100 if prev_close > 0 else 0
# Get 52-week data
hist_52w = ticker.history(period='1y')
high_52w = hist_52w['High'].max() if not hist_52w.empty else current_price * 1.2
low_52w = hist_52w['Low'].min() if not hist_52w.empty else current_price * 0.8
return PriceData(
symbol=symbol,
current_price=current_price,
change_percent=change_percent,
high_52w=high_52w,
low_52w=low_52w,
volume=hist['Volume'].iloc[-1] if 'Volume' in hist.columns else 0,
updated_at=datetime.datetime.now().isoformat()
)
except Exception as e:
print(f"Error fetching crypto {symbol}: {e}")
return None
def get_market_indices(self) -> Dict[str, Dict]:
"""Fetch major market indices using yfinance"""
indices = {
'KOSPI': '^KS11',
'KOSDAQ': '^KOSDAQ',
'S&P 500': '^GSPC',
'NASDAQ': '^IXIC',
'DOW': '^DJI'
}
result = {}
if YFINANCE_AVAILABLE:
for name, ticker in indices.items():
try:
t = yf.Ticker(ticker)
hist = t.history(period='1d')
hist_1d = t.history(period='2d') # Get 2 days for change calculation
if not hist.empty:
current = hist['Close'].iloc[-1]
prev_close = hist_1d['Close'].iloc[0] if len(hist_1d) > 1 else current
change = ((current - prev_close) / prev_close) * 100 if prev_close > 0 else 0
result[name] = {'price': current, 'change': change}
else:
result[name] = {'price': 0, 'change': 0}
except Exception as e:
print(f"Error fetching {name}: {e}")
result[name] = {'price': 0, 'change': 0}
return result
# ============== Reporting ==============
def generate_daily_report(self) -> str:
"""Generate daily portfolio report"""
summary = self.calculate_portfolio_summary()
report = f"""
📊 **일일 포트폴리오 리포트**
**Date:** {datetime.datetime.now().strftime('%Y-%m-%d')}
💰 **전체 현황**
- 현재 가치: ₩{summary.total_value:,.0f}
- 투자 원금: ₩{summary.total_cost:,.0f}
- 총 손익: ₩{summary.total_pnl:,.0f} ({summary.total_pnl_percent:+.1f}%)
📈 **상위 수익**
"""
for pos in sorted(summary.positions, key=lambda x: x['pnl_percent'], reverse=True)[:3]:
emoji = "🟢" if pos['pnl_percent'] > 0 else "🔴"
report += f"- {emoji} **{pos['symbol']}**: {pos['pnl_percent']:+.1f}% (₩{pos['pnl']:,.0f})\n"
report += "\n💡 **투자 원칙 체크**\n"
report += "- ⬜ 3년 실적 우상향 확인\n"
report += "- ⬜ PBR < 1 확인\n"
report += "- ⬜ 추적 손절 10% 설정\n"
report += "- ⬜ 주 1회 점검 예정\n"
return report
def generate_weekly_report(self) -> str:
"""Generate weekly portfolio report"""
summary = self.calculate_portfolio_summary()
report = f"""
📈 **주간 포트폴리오 리포트**
**Week:** {datetime.datetime.now().strftime('%Y-%W')}
🎯 **이번 주 목표**
- [ ] 시장·섹터 상대강도 Top/Bottom 5 확인
- [ ] 관찰목록 체크리스트 재적용
- [ ] 엔트리·손절·추적손절 가격 기입
- [ ] 트레이드 로그 작성
💰 **포트폴리오 현황**
| 항목 | 수치 |
|------|------|
| 총 가치 | ₩{summary.total_value:,.0f} |
| 총 수익률 | {summary.total_pnl_percent:+.1f}% |
| 베스트 | {summary.top_performer['symbol'] if summary.top_performer else 'N/A'} ({summary.top_performer['pnl_percent'] if summary.top_performer else 0:+.1f}%) |
| 워스트 | {summary.worst_performer['symbol'] if summary.worst_performer else 'N/A'} ({summary.worst_performer['pnl_percent'] if summary.worst_performer else 0:+.1f}%) |
📋 **체크리스트 이행**
- [ ] 가치 > 가격 확인
- [ ] 10% 손절 규칙 적용
- [ ] 핵심 2~5종목 집중 확인
"""
return report
# ============== CLI Interface ==============
def main():
import argparse
parser = argparse.ArgumentParser(description='Stock & Crypto Portfolio Tracker')
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Add position
add_parser = subparsers.add_parser('add', help='Add a position')
add_parser.add_argument('--symbol', required=True)
add_parser.add_argument('--type', required=True, choices=['stock', 'crypto', 'etf'])
add_parser.add_argument('--quantity', type=float, required=True)
add_parser.add_argument('--cost', type=float, required=True)
# Show portfolio
subparsers.add_parser('show', help='Show portfolio summary')
# Update prices
subparsers.add_parser('update', help='Update prices from market')
# Daily report
subparsers.add_parser('daily', help='Generate daily report')
# Weekly report
subparsers.add_parser('weekly', help='Generate weekly report')
# Crypto price
crypto_parser = subparsers.add_parser('crypto', help='Get crypto price')
crypto_parser.add_argument('--symbol', default='BTC', help='Crypto symbol (BTC, ETH, etc.)')
# Market indices
subparsers.add_parser('market', help='Show market indices')
args = parser.parse_args()
tracker = StockTracker()
if args.command == 'add':
tracker.add_position(args.symbol, args.type, args.quantity, args.cost)
print(f"✅ Added {args.quantity} {args.symbol} @ ₩{args.cost}")
elif args.command == 'show':
summary = tracker.calculate_portfolio_summary()
print(f"\n📊 Portfolio Summary")
print(f"Total Value: ₩{summary.total_value:,.0f}")
print(f"Total Cost: ₩{summary.total_cost:,.0f}")
print(f"P&L: ₩{summary.total_pnl:,.0f} ({summary.total_pnl_percent:+.1f}%)")
print(f"\nPositions ({len(summary.positions)}):")
for pos in summary.positions:
print(f" {pos['symbol']}: {pos['quantity']} @ ₩{pos['avg_cost']:,.0f} → ₩{pos['current_price']:,.0f} ({pos['pnl_percent']:+.1f}%)")
elif args.command == 'update':
prices = tracker.update_prices()
print(f"✅ Updated {len(prices)} prices")
elif args.command == 'daily':
print(tracker.generate_daily_report())
elif args.command == 'weekly':
print(tracker.generate_weekly_report())
elif args.command == 'crypto':
price = tracker.get_crypto_price(args.symbol)
if price:
emoji = "🟢" if price.change_percent > 0 else "🔴"
print(f"\n{emoji} {args.symbol}: ${price.current_price:,.2f} ({price.change_percent:+.2f}%)")
print(f" 52W Range: ${price.low_52w:,.2f} - ${price.high_52w:,.2f}")
else:
print("❌ yfinance not available. Install: pip install yfinance")
elif args.command == 'market':
indices = tracker.get_market_indices()
print("\n📈 Market Indices")
for name, data in indices.items():
emoji = "🟢" if data['change'] > 0 else "🔴"
print(f" {emoji} {name}: {data['price']:,.2f} ({data['change']:+.2f}%)")
else:
parser.print_help()
if __name__ == '__main__':
main()