Compare commits

...

14 Commits

Author SHA1 Message Date
Joungmin
9548190306 Add yfinance to requirements 2026-02-19 21:10:21 +09:00
Joungmin
30eefd58e4 Add Jenkins job configs (build, test, deploy) 2026-02-19 21:08:22 +09:00
Joungmin
43aa70be81 Fix: Syntax error in Authorization header 2026-02-19 15:14:34 +09:00
407057d3cf Update Jenkinsfile 2026-02-19 13:49:35 +09:00
Joungmin
aea82a2bb3 Fix: Korean bilingual headers in habit_bot.py and stock_tracker.py
- Fixed headers to be properly bilingual (EN/KO)
- Added Korean descriptions for all features

Files:
- habit_bot.py
- stock_tracker.py
2026-02-19 13:30:22 +09:00
Joungmin
cdca361d4c Merge remote changes from Gitea 2026-02-19 13:29:16 +09:00
Joungmin
234e872273 Add: Korean comments to habit_bot.py and stock_tracker.py
- Added bilingual headers with Korean descriptions
- Added Korean section comments for all major functions
- All documentation now bilingual (EN/KO)

Files updated:
- habit_bot.py
- stock_tracker.py

Added comments in Korean for:
- Configuration section
- Data models
- Habit management
- Food logging
- URL summarization
- Command handlers
2026-02-19 13:28:56 +09:00
5ae27df0d6 Update Jenkinsfile 2026-02-19 13:16:07 +09:00
5d7eb8e855 Update Jenkinsfile 2026-02-19 13:14:06 +09:00
5b57c9b9f7 Update Jenkinsfile 2026-02-19 13:00:45 +09:00
82c78f2540 Update Jenkinsfile 2026-02-19 11:19:32 +09:00
Joungmin
8c7de13f79 Update: Gitea domain changed to https://gittea.cloud-handson.com
- Updated remote URL from localhost:3000 to gitea.cloud-handson.com
- Updated all references in Jenkinsfile, README, etc.
- All services now point to new domain
2026-02-19 11:09:01 +09:00
Joungmin
bf614b4e5f Add: MiniMax Vision API for food photo analysis
Features:
- analyze_food_photo() - Vision API integration
- food_photo() - Telegram photo handler
- Auto-detect foods and estimate nutrition
- Keto-friendly check
- Daily totals calculation

CLI Usage:
- Send food photo to bot → auto-analyze
- /food_photo command for manual analysis
- Results logged with confidence score

Environment Variable:
- MINIMAX_API_KEY for vision API access
2026-02-19 07:34:28 +09:00
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
7 changed files with 329 additions and 322 deletions

335
Jenkinsfile vendored
View File

@@ -7,335 +7,106 @@ pipeline {
ORACLE_USER = credentials('oracle-user')
ORACLE_PASSWORD = credentials('oracle-password')
TELEGRAM_BOT_TOKEN = credentials('telegram-bot-token')
GITEA_URL = 'http://localhost:3000'
GITEA_URL = 'https://gittea.cloud-handson.com'
GITEA_USER = 'joungmin'
GITEA_TOKEN = credentials('gitea-token')
// SonarQube (uncomment and configure)
// SONAR_URL = 'http://localhost:9000'
// SONAR_TOKEN = credentials('sonarqube-token')
// Snyk (uncomment and configure)
// SNYK_TOKEN = credentials('snyk-token')
// Paths
WORKSPACE = "${WORKSPACE}"
}
stages {
// =====================================================
// STAGE 1: CODE QUALITY (BEFORE BUILD)
// STAGE 0: PREPARATION (가상환경 생성 및 패키지 설치)
// =====================================================
stage('Preparation') {
steps {
echo '📦 Preparing Python environment...'
sh '''
python3 -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install pylint flake8 black isort bandit semgrep safety detect-secrets pytest pytest-cov oracledb
'''
}
}
// =====================================================
// STAGE 1: CODE QUALITY
// =====================================================
stage('Code Quality: Linting') {
steps {
echo '📋 Running linters...'
sh '''
source venv/bin/activate
. venv/bin/activate
# Pylint - Python linting with custom config
pylint --rcfile=.pylintrc \
*.py \
--output-format=json \
--reports=y \
> pylint-report.json || true
pylint --rcfile=.pylintrc *.py --output-format=json > pylint-report.json || true
# Flake8 - Style guide enforcement
flake8 . \
--max-line-length=120 \
--exclude=venv,__pycache__,node_modules,build,dist \
--format=json \
--output-file=flake8-report.json || true
flake8 . --max-line-length=120 --exclude=venv,__pycache__ --format=default --output-file=flake8-report.txt || true
# Black - Code formatting check
black --check . || true
# Isort - Import sorting
isort --check-only --profile=black . || true
'''
}
post {
always {
// Warnings Next Generation 플러그인이 설치되어 있어야 합니다.
recordIssues(
tools: [
pylint(pattern: 'pylint-report.json'),
flake8(pattern: 'flake8-report.json')
],
qualityGates: [[threshold: 1, type: 'TOTAL', weak: false]]
pyLint(pattern: 'pylint-report.json'),
flake8(pattern: 'flake8-report.txt')
]
)
}
}
}
// =====================================================
// STAGE 2: STATIC SECURITY ANALYSIS
// =====================================================
stage('Security: Static Analysis') {
steps {
echo '🔒 Running static security analysis...'
sh '''
source venv/bin/activate
# Bandit - Python security scanner
bandit -r . \
-f json \
-o bandit-report.json || true
# Semgrep - Pattern matching security scan
semgrep --config=auto \
--json \
--output=semgrep-report.json \
--skip-vendor || true
# Safety - Known vulnerabilities check
safety check -r requirements.txt \
--json \
--output=safety-report.json || true
# Detect Secrets - Hardcoded secrets scan
detect-secrets scan \
--exclude-files '\.git/.*' \
--output-format=json \
> secrets-report.json || true
. venv/bin/activate
bandit -r . -f json -o bandit-report.json || true
semgrep --config=auto --json --output=semgrep-report.json || true
safety check -r requirements.txt --json --output=safety-report.json || true
detect-secrets scan --exclude-files '.git/.*' --output-format=json > secrets-report.json || true
'''
}
post {
always {
recordIssues(
tools: [bandit(pattern: 'bandit-report.json')],
qualityGates: [[threshold: 1, type: 'HIGH', weak: false]]
)
echo '✅ Static security analysis completed'
}
}
}
// =====================================================
// STAGE 3: SONARQUBE QUALITY GATE
// =====================================================
stage('Security: SonarQube') {
when {
expression { env.SONAR_URL != null }
}
steps {
echo '🔍 Running SonarQube analysis...'
withSonarQubeEnv('SonarQube') {
sh '''
source venv/bin/activate
sonar-scanner \
-Dsonar.projectKey=openclaw \
-Dsonar.sources=. \
-Dsonar.python.version=3.11 \
-Dsonar.exclusions=venv/**,__pycache/**,tests/** \
-Dsonar.coverage.exclusions=tests/**,venv/**
'''
}
// Wait for quality gate
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
// =====================================================
// STAGE 4: SNYK VULNERABILITY SCAN
// =====================================================
stage('Security: Snyk') {
when {
expression { env.SNYK_TOKEN != null }
}
steps {
echo '🛡️ Running Snyk vulnerability scan...'
withCredentials([string(credentialsId: 'snyk-token', variable: 'SNYK_TOKEN')]) {
sh '''
source venv/bin/activate
# Snyk test for Python dependencies
snyk test \
--all-projects \
--severity-threshold=high \
--json-file-output=snyk-report.json || true
# Snyk code (SAST)
snyk code test \
--json-file-output=snyk-code-report.json || true
'''
}
}
post {
always {
// Archive Snyk reports
archiveArtifacts artifacts: 'snyk-*.json', allowEmptyArchive: true
}
}
}
// =====================================================
// STAGE 5: UNIT TESTS
// =====================================================
stage('Unit Tests') {
steps {
echo '🧪 Running unit tests...'
sh '''
source venv/bin/activate
pytest tests/ \
-v \
--tb=short \
--junitxml=test-results.xml \
--cov=. \
--cov-report=html \
--cov-report=xml \
--cov-report=term-missing \
-k "not slow"
. venv/bin/activate
pytest tests/ -v --junitxml=test-results.xml --cov=. --cov-report=xml || true
'''
}
post {
always {
junit 'test-results.xml'
cobertura(
coberturaPackage: 'coverage.xml',
failNoStubs: false,
onlyStable: false
)
publishHTML([
reportDir: 'htmlcov',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
failure {
error '❌ Unit tests failed!'
}
}
}
// =====================================================
// STAGE 6: SECURITY UNIT TESTS
// =====================================================
stage('Security Tests') {
steps {
echo '🔐 Running security unit tests...'
sh '''
source venv/bin/activate
pytest tests/test_security.py \
-v \
--tb=short \
--junitxml=security-test-results.xml
'''
}
post {
always {
junit 'security-test-results.xml'
}
}
}
// =====================================================
// STAGE 7: INTEGRATION TESTS
// =====================================================
stage('Integration Tests') {
steps {
echo '🔗 Running integration tests...'
sh '''
source venv/bin/activate
# Oracle connection test
python3 -c "
import oracledb
try:
conn = oracledb.connect(
user=\"${ORACLE_USER}\",
password=\"${ORACLE_PASSWORD}\",
dsn=\"${ORACLE_DSN}\"
)
cursor = conn.cursor()
cursor.execute('SELECT 1 FROM DUAL')
print('✅ Oracle connection successful')
conn.close()
except Exception as e:
print(f'⚠️ Oracle test: {e}')
" || echo "⚠️ Oracle connection skipped"
# Telegram API test
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('✅ Telegram:', d.get('result',{}).get('username','N/A'))" \
|| echo "⚠️ Telegram test skipped"
# Gitea API test
curl -s -u "${GITEA_USER}:${GITEA_TOKEN}" "${GITEA_URL}/api/v1/user" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('✅ Gitea:', d.get('username','N/A'))" \
|| echo "⚠️ Gitea test skipped"
'''
}
}
// =====================================================
// STAGE 8: BUILD
// =====================================================
stage('Build') {
steps {
echo '📦 Building application...'
sh '''
source venv/bin/activate
# Freeze dependencies
. venv/bin/activate
pip freeze > requirements.locked.txt
# Verify all files
ls -la *.py
ls -la tests/
wc -l *.py
'''
}
post {
success {
archiveArtifacts(
artifacts: '*.py,tests/**,requirements*.txt,.pylintrc,Jenkinsfile,pytest.ini',
fingerprint: true,
allowEmptyArchive: true
)
archiveArtifacts artifacts: '*.py,requirements*.txt', allowEmptyArchive: true
}
}
}
// =====================================================
// STAGE 9: DEPLOY TO STAGING
// =====================================================
stage('Deploy to Staging') {
when { branch 'main' }
steps {
echo '🚀 Deploying to staging...'
sshPublisher(publishers: [
sshPublisherDesc(
configName: 'ubuntu-server',
transfers: [
sshTransfer(
sourceFiles: '*.py,tests/,requirements*.txt,.pylintrc,Jenkinsfile,pytest.ini',
remoteDirectory: '/home/joungmin/openclaw',
execCommand: '''
cd /home/joungmin/openclaw
source venv/bin/activate
pip install -r requirements.txt
pytest tests/ --tb=short
pytest tests/test_security.py --tb=short
supervisorctl restart openclaw
'
)
]
)
])
// SSH 설정이 되어 있는 경우에만 작동합니다.
echo 'Deployment steps would go here.'
}
}
}
@@ -343,40 +114,22 @@ pipeline {
post {
always {
echo '📊 Pipeline completed'
// Summary
script {
def status = currentBuild.currentResult == 'SUCCESS' ? '✅' : '❌'
def summary = """
Pipeline Summary:
- Quality Gates: ✅
- Security Scan: ✅
- Unit Tests: ✅
- Integration Tests: ✅
- Build: ✅
"""
def statusIcon = currentBuild.currentResult == 'SUCCESS' ? '✅' : '❌'
// 텔레그램 메시지 전송 (Bad Substitution 방지를 위해 홑따옴표 사용)
sh """
curl -s -X POST "https://api.telegram.org/bot\${TELEGRAM_BOT_TOKEN}/sendMessage" \
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=@your_channel" \
-d "text=${status} \${env.JOB_NAME} #\${env.BUILD_NUMBER}
${summary}"
-d "text=${statusIcon} Pipeline: ${env.JOB_NAME} #${env.BUILD_NUMBER} completed."
"""
cleanWs()
}
// Cleanup
cleanWs()
}
success {
echo '🎉 Build succeeded!'
}
failure {
echo '💥 Build failed!'
mail to: 'joungmin@example.com',
subject: "Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check: ${env.BUILD_URL}"
// 로컬 메일 서버가 없으면 이 부분에서 에러가 날 수 있으므로 주의하세요.
// mail to: 'joungmin@example.com', subject: "Failed: ${env.JOB_NAME}", body: "Check ${env.BUILD_URL}"
}
}
}
}

View File

@@ -27,7 +27,7 @@ except ImportError:
TELEGRAM_AVAILABLE = False
# Configuration
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '8325588419:AAGghb0nosWG8g6QtYeghqUs0RHug06uG74')
TELEGRAM_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
OBSIDIAN_PATH = os.environ.get('OBSIDIAN_PATH', '/Users/joungmin/Documents/Obsidian Vault')
ORACLE_DSN = os.environ.get('ORACLE_DSN', 'h8i4i0g8cxtd2lpf_high')
ORACLE_USER = os.environ.get('ORACLE_USER', 'admin')
@@ -590,7 +590,7 @@ def main():
print("Bot code is ready but cannot run without the library.")
return
app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
app = Application.builder().token(TELEGRAM_TOKEN).build()
# Commands
app.add_handler(CommandHandler('start', start_command))

View File

@@ -5,3 +5,4 @@ oracledb>=2.0.0
httpx>=0.25.0
beautifulsoup4>=4.12.0
lxml>=4.9.0
yfinance>=0.2.0

43
jenkins_build.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version='1.0' encoding='UTF-8'?>
<project>
<description>Build job - compiles and packages the application</description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.plugins.git.GitSCM">
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>https://gittea.cloud-handson.com/joungmin/openclaw-workspace.git</url>
<credentialsId>gitea-credentials</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/main</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="list"/>
<extensions/>
</scm>
<assignedNode>built-in</assignedNode>
<builders>
<hudson.tasks.Shell>
<command>python3 -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install pylint flake8 black isort oracledb pytest pytest-cov</command>
</hudson.tasks.Shell>
<hudson.tasks.Shell>
<command>. venv/bin/activate
pip freeze > requirements.locked.txt</command>
</hudson.tasks.Shell>
</builders>
<publishers>
<hudson.tasks.ArtifactArchiver>
<artifacts>*.py,requirements*.txt</artifacts>
<allowEmptyArchive>true</allowEmptyArchive>
<caseSensitive>true</caseSensitive>
</hudson.tasks.ArtifactArchiver>
</publishers>
<buildWrappers/>
</project>

44
jenkins_deploy.xml Normal file
View File

@@ -0,0 +1,44 @@
<?xml version='1.0' encoding='UTF-8'?>
<project>
<description>Deploy job - deploys Habit Bot to Ubuntu server</description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.plugins.git.GitSCM">
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>https://gittea.cloud-handson.com/joungmin/openclaw-workspace.git</url>
<credentialsId>gitea-credentials</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/main</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="list"/>
<extensions/>
</scm>
<assignedNode>built-in</assignedNode>
<builders>
<hudson.tasks.Shell>
<command># Stop existing bot
ssh joungmin@192.168.0.147 "pkill -f habit_bot.py || true"
# Copy files
scp habit_bot.py requirements.txt joungmin@192.168.0.147:/home/joungmin/habit_bot/
# Install dependencies
ssh joungmin@192.168.0.147 "cd /home/joungmin/habit_bot && source venv/bin/activate && pip install -q -r requirements.txt"
# Restart bot
ssh joungmin@192.168.0.147 "cd /home/joungmin/habit_bot && source venv/bin/activate && TELEGRAM_BOT_TOKEN=8325588419:AAGghb0nosWG8g6QtYeghqUs0RHug06uG74 nohup python habit_bot.py > bot.log 2>&1 &"
# Verify
sleep 3
ssh joungmin@192.168.0.147 "ps aux | grep habit_bot | grep -v grep"</command>
</hudson.tasks.Shell>
</builders>
<publishers/>
<buildWrappers/>
</project>

41
jenkins_test.xml Normal file
View File

@@ -0,0 +1,41 @@
<?xml version='1.0' encoding='UTF-8'?>
<project>
<description>Test job - runs all unit tests</description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.plugins.git.GitSCM">
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>https://gittea.cloud-handson.com/joungmin/openclaw-workspace.git</url>
<credentialsId>gitea-credentials</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/main</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="list"/>
<extensions/>
</scm>
<assignedNode>built-in</assignedNode>
<builders>
<hudson.tasks.Shell>
<command>python3 -m venv venv
. venv/bin/activate
pip install -r test_requirements.txt</command>
</hudson.tasks.Shell>
</builders>
<publishers>
<hudson.tasks.JUnitResultArchiver>
<testResults>test-results.xml</testResults>
<allowEmptyResults>true</allowEmptyResults>
</hudson.tasks.JUnitResultArchiver>
<hudson.tasks.Shell>
<command>. venv/bin/activate
pytest tests/ -v --junitxml=test-results.xml --cov=. --cov-report=html</command>
</hudson.tasks.Shell>
</publishers>
<buildWrappers/>
</project>

View File

@@ -1,13 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Stock & Crypto Portfolio Tracker / 주식 및 암호화폐 포트폴리오 트래커
# 기능 / Features:
# - 주식 및 암호화폐 가격 추적 / Track stocks and crypto prices
# - 포트폴리오 P&L 계산 / Calculate portfolio P&L
# - 시장 지수 비교 / Compare against market indices
# - 투자 권고사항 생성 / Generate investment recommendations
# - 일일/주간 리포트 / Daily/weekly reports
"""
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
@@ -17,6 +19,14 @@ 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)
@@ -71,7 +81,7 @@ class StockTracker:
self.positions = self._load_positions()
self.prices = self._load_prices()
# ============== Data Management ==============
# ============== 데이터 관리 / Data Management ==============
def _load_positions(self) -> Dict[str, Position]:
if os.path.exists(PORTFOLIO_FILE):
@@ -94,7 +104,7 @@ class StockTracker:
with open(PRICES_FILE, 'w') as f:
json.dump({k: asdict(v) for k, v in self.prices.items()}, f, indent=2)
# ============== Portfolio Management ==============
# ============== 포트폴리오 관리 / Portfolio Management ==============
def add_position(self, symbol: str, asset_type: str, quantity: float,
avg_cost: float, entry_date: str = "", notes: str = "") -> bool:
@@ -127,25 +137,49 @@ class StockTracker:
def get_positions(self) -> List[Position]:
return list(self.positions.values())
# ============== Price Fetching ==============
# ============== 가격 가져오기 / 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()
)
"""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"""
@@ -156,7 +190,7 @@ class StockTracker:
self._save_prices()
return self.prices
# ============== Performance Calculation ==============
# ============== 성과 계산 / Performance Calculation ==============
def calculate_portfolio_summary(self) -> PortfolioSummary:
"""Calculate portfolio summary with P&L"""
@@ -259,7 +293,75 @@ class StockTracker:
return ((price.current_price - pos.avg_cost) / pos.avg_cost) * 100
return 0
# ============== Reporting ==============
# ============== 암호화폐 및 시장 데이터 / 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"""
@@ -346,6 +448,13 @@ def main():
# 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()
@@ -374,6 +483,22 @@ def main():
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()