Files
openclaw-workspace/stock_tracker.py
Joungmin 6d9bc5980f Add: Stock tracker, Jenkins CI/CD pipeline, linting config
- stock_tracker.py: Portfolio tracking with P&L calculations
- Jenkinsfile: Full CI/CD with linting, testing, deployment
- test_requirements.txt: Testing dependencies
- .pylintrc: Linting configuration
- requirements.txt: Production dependencies

Features:
- Stock & crypto portfolio tracking
- Investment guideline checks
- Unit tests & linting pipeline
- Integration tests for Oracle/Telegram/Gitea
- Staging & Production deployment stages
2026-02-19 03:25:52 +09:00

382 lines
13 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
# 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"""
# Placeholder - would use real API
# For demo, generate mock data
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
# ============== 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')
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())
else:
parser.print_help()
if __name__ == '__main__':
main()