Skill: saebyeok-im/equity-analyst (version 1.0.7)

--- name: equity-analyst description: 전문 투자 분석가 AI로, 한국 주식 종목의 재무제표, 뉴스, 차트를 분석하여 Investment Attractiveness Score (0-100)와 BUY/HOLD/AVOID 추천을 제공합니다. 네이버 금융 데이터를 사용하며, 프롬프트에 명시된 엄격한 우선순위(Financial > News > Chart)와 가중치를 따릅니다. user-invocable: true disable-model-invocation: false metadata: {"openclaw":{"emoji":"📈"}} --- # Equity Analyst Skill This skill provides professional-grade equity analysis for Korean stocks listed on KRX. It follows a strict evaluation framework with Financial Fundamentals (50%), News & Outlook (25%), and Technical Chart (25%) priorities. ## When to Use This Skill - User requests stock analysis with specific ticker or company name (e.g., "삼성전자 분석해줘", "SK하이닉스 투자 매력도 알려줘") - User asks for investment recommendation (BUY/HOLD/AVOID) with supporting reasoning - Need systematic, conservative, logic-driven evaluation based on financial metrics **Do NOT use for**: Non-Korean stocks, cryptocurrency, or when user wants casual/opinion-based advice without rigorous framework. ## Quick Start 1. Identify the stock ticker (e.g., 005930 for 삼성전자, 000660 for SK하이닉스) 2. Use browser tool to navigate to Naver Finance page: `https://finance.naver.com/item/main.naver?code={ticker}` 3. Extract required data (see Data Requirements below) 4. Apply evaluation framework (see Framework section) 5. Generate structured report in specified format ## Data Requirements Collect the following data from Naver Finance main page: ### Financial Metrics - PER (Price Earnings Ratio) - PBR (Price Book-value Ratio) - ROE (Return on Equity) - 지배주주 기준 - Operating Margin (영업이익률) - Debt Ratio (부채비율) - Revenue Growth (매출 성장률) - recent multi-year trend (2024→2025 예상 사용) ### News & Outlook (summary) - Recent major news headlines (last few days) - Earnings outlook (컨센서스, 예상) - Industry tailwinds/headwinds - Analyst sentiment changes ### Technical/Chart Conditions (summary) - Trend direction (상승/횡보/하락) - Current price position relative to 52-week high/low - Volume characteristics (확장/수축/보통) - Any notable patterns (support/resistance, etc.) **Note**: Bollinger Band and other complex indicators are NOT required. Keep chart description simple: trend + current state. ## Evaluation Framework Follow these steps EXACTLY in order: ### STEP 1: FINANCIAL SCORE (50%) Score each sub-category 0-100: **A. Valuation (PER, PBR) - Weight 30%** - Low PER (< industry avg) and PBR (< 1) are positive - Extremely high PER (>40) is negative unless justified by exceptional growth - Output: ValuationScore **B. Profitability (ROE, Operating Margin) - Weight 30%** - ROE < 5%: critically weak - ROE 10%+: healthy - Stable operating margin above industry average: positive - Output: ProfitabilityScore **C. Growth (Revenue Growth) - Weight 25%** - Sustained growth (>10%) is positive - Stagnation (<3%) or decline: negative - Output: GrowthScore **D. Stability (Debt Ratio) - Weight 15%** - Low debt (<50%) is positive - High leverage (>100%) is negative - Output: StabilityScore **FinancialScore = Valuation×0.30 + Profitability×0.30 + Growth×0.25 + Stability×0.15** **Special Rule**: If BOTH ProfitabilityScore AND GrowthScore are below 30, cap FinancialScore at maximum 50 ( regardless of other scores ). ### STEP 2: NEWS & OUTLOOK SCORE (25%) Evaluate qualitative factors: - Earnings outlook strength - Product/service momentum - Analyst sentiment direction - Industry tailwinds vs headwinds **Rules**: - Strong positive catalysts (new contracts, regulatory approval, market expansion) raise score - Neutral or "wait-and-see" tone: 40-55 - Hype without financial backing: MUST NOT score high (max 60) - Negative catalysts ( lawsuits, customer loss, industry downturn) lower score Output: NewsScore (0-100) ### STEP 3: TECHNICAL / CHART SCORE (25%) Evaluate timing and market behavior: - Trend direction (up/sideways/down) - Volume expansion/contraction - Signs of accumulation or distribution - Current price position (near support/resistance) **Rules**: - Charts determine TIMING, not value - Strong fundamentals + weak charts = still low chart score - Technicals must NEVER override poor fundamentals - Focus on whether now is a good entry/exit timing based on chart alone Output: ChartScore (0-100) ### FINAL SCORE FinalScore = (FinancialScore × 0.50) + (NewsScore × 0.25) + (ChartScore × 0.25) ### Verdict Categories - **BUY**: 80–100 (strong conviction) - **BUY_LEAN**: 65–79 (cautious buy) - **HOLD**: 45–64 (wait/accumulate on dips) - **AVOID**: below 45 (too risky or unattractive) ## Output Format Return EXACTLY this structure: ``` 1. Financial Breakdown - ValuationScore: [0-100] - ProfitabilityScore: [0-100] - GrowthScore: [0-100] - StabilityScore: [0-100] - FinancialScore: [0-100] 2. NewsScore: [0-100] 3. ChartScore: [0-100] 4. Final Investment Attractiveness Score: XX / 100 5. Verdict: [BUY|BUY_LEAN|HOLD|AVOID] 6. Reasoning Summary: [One paragraph explaining why the score was assigned, respecting priority order: Financial > News > Chart. Be conservative, logic-driven. Do NOT give investment advice.] ``` ## Examples ### Example 1: SK하이닉스 (from real data) ``` 1. Financial Breakdown - ValuationScore: 70 - ProfitabilityScore: 95 - GrowthScore: 95 - StabilityScore: 75 - FinancialScore: 84.5 2. NewsScore: 70 3. ChartScore: 55 4. Final Investment Attractiveness Score: 73.5 / 100 5. Verdict: BUY_LEAN 6. Reasoning Summary: SK하이닉스는 재무제표가 매우 강력합니다. ROE 43.20%, 영업이익률 46.67%, 43.7%의 매출 성장률은 업계 최상위 수준이며, PER 17.11배는 상대적으로 저평가되어 있습니다. 부채비율 64.12%는 반도체 업체로서 적정범위 내에 있습니다. 뉴스 측면에서는 HBM4 공급과 AI memory 수요 증가가 주가에 긍정적이나, 외국인 매도세가 일부 부정적 영향을 미치고 있습니다. 기술적 측면에서는 장기 상승추세는 유지되고 있으나, 단기적으로 조정 국면에 있어 매수 타이밍에 신중을 기할 필요가 있습니다. 재무적 우수성과 성장성에도 불구, 단기 차트의 불확실성으로 인해 "buy with caution" 상태로 평가됩니다. ``` ### Example 2: Weak Fundamentals ``` ... (similar structure) ... ValuationScore: 25 (PER 150, PBR 8.5 - extremely overvalued) ProfitabilityScore: 20 (ROE 2%, margin negative) ... Verdict: AVOID ... ``` ## Scripts The skill includes these scripts: - `scripts/analyze.py` - Main analysis engine that takes extracted data and computes scores - `scripts/scrape_naver.py` - Optional: Data extraction from Naver Finance page Use these to automate repetitive tasks. ## References Detailed evaluation criteria and examples: `references/framework.md` ## Notes - This skill is for Korean stocks only (KRX) - Data source: Naver Finance (real-time snapshot, not delayed) - Scores are relative within KRX universe - Framework is conservative: hype without earnings does NOT get high scores - Technical score is about timing only, not quality ## Troubleshooting **Missing data**: If any metric is unavailable, treat as neutral (score 50) but note in reasoning. **Conflicting signals**: Follow priority order: Financial > News > Chart. Low financial score can NOT be compensated by good news or chart. **Extreme valuation**: PER > 50 or PBR > 5 should trigger heavy discount unless growth justifies.

path: data.json
content: ��{  
     " t i c k e r " :   " 0 0 5 9 3 0 " ,  
     " c o m p a n y _ n a m e " :   " ��1� Ȑ�  :   N p a y   �Ɍ�" ,  
     " f i n a n c i a l s " :   {  
         " P E R " :   2 3 . 8 7 ,  
         " P B R " :   3 2 . 9 3 ,  
         " R O E " :   0 . 4 4 ,  
         " o p e r a t i n g _ m a r g i n " :   0 . 4 4 ,  
         " d e b t _ r a t i o " :   0 . 4 4 ,  
         " r e v e n u e _ g r o w t h " :   0 . 0  
     } ,  
     " n e w s _ s u m m a r y " :   " R e c e n t   n e w s :   [ D��ɠ�l�< ��> ]   ��ĳ8�   ���   � �  5 ��  ��̸& �����. . . ;     1 0 ̹�� �   �X��      ��  8 ̹��t��     �  ����  ��. . . ;   \ " 6 0 p�  $���	�  ��ଔ�  W�x�  �¤�\�   ���  ����  ��\ " " ,  
     " c h a r t _ d e s c r i p t i o n " :   " C h a r t   s h o w s :   t�����  (̸�"  
 }  
 

path: data_utf8.json
content: {"ticker": "005930", "company_name": "삼성전자 : Npay 증권", "financials": {"PER": 23.87, "PBR": 32.93, "ROE": 0.44, "operating_margin": 0.44, "debt_ratio": 0.44, "revenue_growth": 0.0}, "news_summary": "Recent news: [비즈토크<상>] 양도세 중과 유예 5월 종료…다주택...; “10만원대 팔았다” “난 8만원이다” 판 사람 어...; \"60조 오지급 사고는 빗썸 시스템 관리 부실 탓\"", "chart_description": "Chart shows: 이미지 차트"}

path: references/framework.md
content: # Equity Analysis Framework

This document details the strict evaluation framework used by the equity-analyst skill.

## Priority Order (Read First)

1. **Financial Fundamentals** (highest priority - has veto power)
2. **News & Outlook**
3. **Technical Chart Indicators** (lowest priority - timing only)

Financial score can NEVER be overridden by News or Technical factors.

---

## STEP 1: FINANCIAL SCORE (50% of final)

### Scoring Tables

#### A. Valuation (PER, PBR) - 30% weight

| PER Level | Score | Note |
|-----------|-------|------|
| < 10 | 90-100 | Exceptionally cheap |
| 10-15 | 80-89 | Very attractive |
| 15-20 | 70-79 | Fair value |
| 20-30 | 50-69 | Slightly expensive |
| 30-40 | 30-49 | Expensive |
| > 40 | 0-29 | Very expensive (unless >30% growth) |

| PBR Level | Score | Note |
|-----------|-------|------|
| < 0.8 | 90-100 | Deep discount to book |
| 0.8-1.2 | 80-89 | Near or below book |
| 1.2-2.0 | 70-79 | Slight premium |
| 2.0-3.0 | 50-69 | Moderate premium |
| 3.0-5.0 | 30-49 | High premium |
| > 5.0 | 0-29 | Very high premium |

**Combine PER and PBR**: Average the two scores for final ValuationScore.

#### B. Profitability (ROE, Operating Margin) - 30% weight

| ROE Level | Score | Note |
|-----------|-------|------|
| > 20% | 90-100 | Exceptional |
| 15-20% | 80-89 | Excellent |
| 10-15% | 70-79 | Good |
| 5-10% | 50-69 | Moderate |
| 3-5% | 30-49 | Weak |
| < 3% | 0-29 | Critically weak |

| Operating Margin Level | Score | Note |
|------------------------|-------|------|
| > 20% | 90-100 | High margin business |
| 15-20% | 80-89 | Good margins |
| 10-15% | 70-79 | Decent margins |
| 5-10% | 50-69 | Average |
| 0-5% | 30-49 | Low margins |
| < 0% | 0-29 | Negative (loss-making) |

**Combine**: Average ROE score and Operating Margin score.

#### C. Growth (Revenue Growth) - 25% weight

| Revenue Growth (YoY) | Score | Note |
|---------------------|-------|------|
| > 30% | 90-100 | Explosive growth |
| 20-30% | 80-89 | Strong growth |
| 10-20% | 70-79 | Good growth |
| 5-10% | 50-69 | Modest growth |
| 0-5% | 30-49 | Stagnation |
| < 0% | 0-29 | Decline |

**Use multi-year trend**: Prere consistent 3-year trend. If volatile, use average.

#### D. Stability (Debt Ratio) - 15% weight

| Debt Ratio | Score | Note |
|------------|-------|------|
| < 30% | 90-100 | Very low leverage |
| 30-50% | 80-89 | Conservative |
| 50-80% | 70-79 | Moderate |
| 80-100% | 50-69 | High |
| 100-150% | 30-49 | Very high |
| > 150% | 0-29 | Extreme leverage |

**Note**: Some industries (financial, utilities) naturally have higher debt. Adjust slightly if industry context is clear (max +10 points).

#### Financial Score Calculation

```
FinancialScore =
    ValuationScore × 0.30 +
    ProfitabilityScore × 0.30 +
    GrowthScore × 0.25 +
    StabilityScore × 0.15
```

**Special Rule (Veto Cap)**:
If **both** ProfitabilityScore < 30 **and** GrowthScore < 30, then MAXIMUM FinancialScore = 50.
This prevents low-quality companies from scoring high on valuation/stability alone.

---

## STEP 2: NEWS & OUTLOOK SCORE (25%)

### Evaluation Criteria

**Positive Catalysts** (raise score):
- Strong earnings beat/revenue upside
- New major contracts or partnerships
- Regulatory approvals, market expansion
- Industry tailwinds (e.g., AI boom, policy support)
- Analyst upgrades, positive sentiment shift

**Negative Catalysts** (lower score):
- Earnings miss, guidance cut
- Customer losses, litigation
- Regulatory setbacks, industry headwinds
- Analyst downgrades, negative sentiment

**Neutral/Uncertain**:
- Mixed signals, waiting for next earnings
- No major recent news
- Industry uncertain

### Scoring Guide

| News Tone | Score Range | Rationale |
|-----------|-------------|-----------|
| Very positive, multiple strong catalysts | 80-100 | High conviction |
| Positive, clear tailwinds | 65-79 | Favorable outlook |
| Neutral/mixed | 40-64 | Wait-and-see |
| Some concerns but not critical | 30-39 | Cautious |
| Very negative, major red flags | 0-29 | Avoid |

**Critical Rule**: Hype without financial backing (e.g., "metaverse", "AI buzzword" without revenue) MUST NOT score above 60. Score based on tangible impact.

---

## STEP 3: TECHNICAL / CHART SCORE (25%)

### What to Evaluate (Simple Only)

**Trend Direction**:
- Up: Higher highs and higher lows
- Sideways: Range-bound
- Down: Lower highs and lower lows

**Current Price Position** (relative to 52-week range):
- Near 52w low: potential accumulation
- Near 52w high: possible distribution
- Mid-range: neutral

**Volume**:
- Expanding volume on up moves: bullish
- Expanding volume on down moves: bearish
- Contracting volume: consolidation
- Average volume: neutral

**Timing Implication**:
- Good entry/exit points based on current chart

### Scoring Guide

| Chart Condition | Score | Rationale |
|-----------------|-------|-----------|
| Strong uptrend, good entry point | 70-100 | Buy on pullback |
| Emerging uptrend, early stage | 60-79 | Accumulating |
| Sideways consolidation | 40-59 | Wait for breakout |
| Downtrend but near support | 30-39 | Avoid or short |
| Strong downtrend, distribution | 0-29 | High risk |

**Critical Rule**: Technicals are TIMING-ONLY. A stock with terrible fundamentals (FinancialScore < 40) can NOT get high ChartScore. Maximum justified score for poor fundamentals is 40 (waiting for better entry is irrelevant if business is broken).

---

## FINAL SCORE & VERDICT

```
FinalScore = (FinancialScore × 0.50) + (NewsScore × 0.25) + (ChartScore × 0.25)
```

### Verdict Mapping

| FinalScore | Verdict | Action |
|------------|---------|--------|
| 80-100 | BUY | Strong conviction, consider significant allocation |
| 65-79 | BUY_LEAN | Cautious buy, small position or accumulate on dips |
| 45-64 | HOLD | Neutral, wait for better entry or exit |
| < 45 | AVOID | Too risky or unattractive |

---

## Output Format Checklist

- [ ] Financial Breakdown section with 5 lines (Valuation, Profitability, Growth, Stability, Financial)
- [ ] NewsScore line
- [ ] ChartScore line
- [ ] Final Investment Attractiveness Score: XX / 100
- [ ] Verdict: one of BUY, BUY_LEAN, HOLD, AVOID
- [ ] Reasoning Summary: ONE paragraph only, respecting priority order
- [ ] No investment advice (no "you should buy", use "score suggests")
- [ ] Conservative, logic-driven language
- [ ] No exaggeration

---

## Common Pitfalls to Avoid

1. **Reversing priority**: Never let News or Technicals override Financials.
2. **Hype traction**: Don't give high NewsScore for unproven stories.
3. **Chart over-analysis**: Keep it simple - trend + timing.
4. **Valuation ignoring growth**: High PER may be justified by >30% growth.
5. **Sector ignorance**: Adjust Stability for capital-intensive vs asset-light businesses (but only slight adjustment, max +10).

---

## Framework Philosophy

This framework is **conservative by design**:
- Financials are 50% weight and have veto power (special rule)
- News requires tangible catalysts, not speculation
- Technicals only matter for timing, not intrinsic value
- Low financial scores cap overall attractiveness regardless of hype

The goal: filter out hype, find fundamentally sound companies at reasonable valuations with positive catalysts.


path: scripts/analyze.py
content: #!/usr/bin/env python3
"""
Equity Analyst - Investment Attractiveness Score Calculator

This script implements the strict evaluation framework described in references/framework.md.
It takes extracted financial data, news summary, and chart description, then outputs
a structured analysis report with scores and verdict.

Usage:
    python analyze.py --input data.json --output report.txt

Input JSON format:
{
  "ticker": "000660",
  "company_name": "SK하이닉스",
  "financials": {
    "PER": 17.11,
    "PBR": 5.79,
    "ROE": 43.20,
    "operating_margin": 46.67,
    "debt_ratio": 64.12,
    "revenue_growth": 43.7
  },
  "news_summary": "Brief news summary...",
  "chart_description": "Brief chart description..."
}

Output format: See SKILL.md for exact structure.
"""

import json
import sys
import argparse
from typing import Dict, Any, Tuple

def score_valuation(per: float, pbr: float, industry_per_avg: float = 23.87) -> Tuple[int, str]:
    """Score valuation based on PER and PBR levels."""
    # PER scoring
    if per < 10:
        per_score = 95
    elif per < 15:
        per_score = 85
    elif per < 20:
        per_score = 75
    elif per < 30:
        per_score = 60
    elif per < 40:
        per_score = 40
    else:
        # PER > 40: very expensive unless growth justifies (handled elsewhere)
        per_score = 20

    # PBR scoring
    if pbr < 0.8:
        pbr_score = 95
    elif pbr < 1.2:
        pbr_score = 85
    elif pbr < 2.0:
        pbr_score = 75
    elif pbr < 3.0:
        pbr_score = 60
    elif pbr < 5.0:
        pbr_score = 40
    else:
        pbr_score = 20

    # Average
    final_score = (per_score + pbr_score) // 2

    reasoning = f"PER {per:.1f}x (score {per_score}), PBR {pbr:.1f}x (score {pbr_score})"
    return final_score, reasoning

def score_profitability(roe: float, operating_margin: float) -> Tuple[int, str]:
    """Score profitability based on ROE and operating margin."""
    # ROE scoring
    if roe > 20:
        roe_score = 95
    elif roe > 15:
        roe_score = 85
    elif roe > 10:
        roe_score = 75
    elif roe > 5:
        roe_score = 60
    elif roe > 3:
        roe_score = 40
    else:
        roe_score = 20

    # Operating margin scoring
    if operating_margin > 20:
        margin_score = 95
    elif operating_margin > 15:
        margin_score = 85
    elif operating_margin > 10:
        margin_score = 75
    elif operating_margin > 5:
        margin_score = 60
    elif operating_margin > 0:
        margin_score = 40
    else:
        margin_score = 20

    final_score = (roe_score + margin_score) // 2
    reasoning = f"ROE {roe:.1f}% (score {roe_score}), OpMargin {operating_margin:.1f}% (score {margin_score})"
    return final_score, reasoning

def score_growth(revenue_growth: float) -> Tuple[int, str]:
    """Score growth based on revenue growth rate."""
    if revenue_growth > 30:
        score = 95
    elif revenue_growth > 20:
        score = 85
    elif revenue_growth > 10:
        score = 75
    elif revenue_growth > 5:
        score = 60
    elif revenue_growth > 0:
        score = 40
    else:
        score = 20

    return score, f"Revenue growth {revenue_growth:.1f}%"

def score_stability(debt_ratio: float, industry_avg: float = 80) -> Tuple[int, str]:
    """Score stability based on debt ratio."""
    # Adjust for industry (semis typically have higher debt than software)
    # But only slight adjustment: max +10 if debt_ratio is reasonable for industry
    if debt_ratio < 30:
        score = 95
    elif debt_ratio < 50:
        score = 85
    elif debt_ratio < 80:
        score = 75
    elif debt_ratio < 100:
        score = 60
    elif debt_ratio < 150:
        score = 40
    else:
        score = 20

    return score, f"Debt ratio {debt_ratio:.1f}%"

def score_news(news_summary: str) -> int:
    """
    Score news and outlook based on qualitative assessment.
    This is a simplified heuristic - in practice would use NLP.
    """
    news_lower = news_summary.lower()

    # Positive keywords
    positive_words = [
        'hbm4', '공급', '기대', 'ai', '수요', '증가', '성장', '호조',
        '상승', '매수', '목표가', '상향', '계약', '협력', '승인',
        'tailwind', 'bullish', 'upgrade', 'beat', 'expansion'
    ]

    # Negative keywords
    negative_words = [
        '매도', '하락', '부진', '위기', '소송', '규제', '하향',
        ' cuts', 'loss', 'lawsuit', 'downgrade', 'bearish', 'headwind',
        'foreign selling', '외국인 매도'
    ]

    positive_count = sum(1 for word in positive_words if word in news_lower)
    negative_count = sum(1 for word in negative_words if word in news_lower)

    # Base score
    if positive_count > negative_count + 2:
        score = 80  # Strong positive
    elif positive_count > negative_count:
        score = 70  # Moderate positive
    elif positive_count == negative_count:
        score = 50  # Neutral
    elif negative_count > positive_count + 2:
        score = 30  # Strong negative
    else:
        score = 40  # Moderate negative

    # Cap hype: if many positive words but no concrete numbers, cap at 60
    # Simple heuristic: if score > 60 but no percentage/numbers in summary, reduce
    if score > 60 and not any(c.isdigit() for c in news_summary):
        score = 60

    return score

def score_chart(chart_description: str) -> int:
    """
    Score technical/chart conditions for timing.
    """
    desc_lower = chart_description.lower()

    # Uptrend keywords
    uptrend_words = ['상승', 'uptrend', ' rising', 'higher high', '상승추세', '강세']
    # Downtrend keywords
    downtrend_words = ['하락', 'downtrend', ' falling', 'lower low', '하락추세', '약세']
    # Consolidation/sideways
    sideways_words = ['조정', '횡보', 'consolidation', 'sideways', '밴드', '봉']

    # High52w reference
    near_high = any(word in desc_lower for word in ['최고', '52주 최고', 'high', ' resistance'])
    near_low = any(word in desc_lower for word in ['최저', '52주 최저', 'low', ' support'])

    # Determine trend score
    if any(word in desc_lower for word in uptrend_words):
        trend_score = 70
    elif any(word in desc_lower for word in downtrend_words):
        trend_score = 30
    else:
        trend_score = 50  # Neutral/unknown

    # Adjust for position
    if near_high and trend_score > 50:
        trend_score -= 10  # Extended from high
    if near_low and trend_score < 50:
        trend_score += 10  # Potential bounce

    # Ensure within range
    trend_score = max(0, min(100, trend_score))

    return trend_score

def analyze_stock(data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Main analysis function implementing the framework.
    """
    financials = data['financials']

    # Step 1: Financial scores
    valuation_score, val_reason = score_valuation(
        financials['PER'],
        financials['PBR']
    )
    profitability_score, profit_reason = score_profitability(
        financials['ROE'],
        financials['operating_margin']
    )
    growth_score, growth_reason = score_growth(
        financials['revenue_growth']
    )
    stability_score, stabil_reason = score_stability(
        financials['debt_ratio']
    )

    # Calculate FinancialScore
    financial_score = (
        valuation_score * 0.30 +
        profitability_score * 0.30 +
        growth_score * 0.25 +
        stability_score * 0.15
    )

    # Special rule: if both Profitability and Growth below 30, cap FinancialScore at 50
    if profitability_score < 30 and growth_score < 30:
        financial_score = min(financial_score, 50)

    # Step 2: News score
    news_score = score_news(data['news_summary'])

    # Step 3: Chart score
    chart_score = score_chart(data['chart_description'])

    # Final score
    final_score = (
        financial_score * 0.50 +
        news_score * 0.25 +
        chart_score * 0.25
    )

    # Verdict
    if final_score >= 80:
        verdict = "BUY"
    elif final_score >= 65:
        verdict = "BUY_LEAN"
    elif final_score >= 45:
        verdict = "HOLD"
    else:
        verdict = "AVOID"

    # Build reasoning parts separately to avoid nested f-string issues
    financial_strength = 'strong' if financial_score >= 70 else 'moderate' if financial_score >= 50 else 'weak'
    valuation_desc = 'reasonable' if valuation_score >= 60 else 'expensive'
    news_sentiment = 'positive' if news_score >= 60 else 'neutral' if news_score >= 40 else 'negative'
    chart_timing = 'favorable timing' if chart_score >= 60 else 'neutral' if chart_score >= 40 else 'unfavorable timing'

    # Profit/growth message
    if profitability_score >= 70 and growth_score >= 70:
        profit_msg = f'Profitability is excellent (ROE {financials["ROE"]:.1f}%) with solid growth (Revenue growth {financials["revenue_growth"]:.1f}%).'
    else:
        profit_msg = 'Profitability and growth are mixed.'

    reasoning = f"""The stock scores {final_score:.1f}/100 ({verdict}). Financial strength is {financial_strength} (FinancialScore {financial_score:.1f}). {profit_msg} Valuation appears {valuation_desc} (PER {financials['PER']:.1f}x). News sentiment is {news_sentiment} (Score {news_score}). Technical chart shows {chart_timing} (Score {chart_score})."""

    # Build output
    result = {
        "ticker": data['ticker'],
        "company_name": data.get('company_name', ''),
        "financial_breakdown": {
            "ValuationScore": round(valuation_score),
            "ProfitabilityScore": round(profitability_score),
            "GrowthScore": round(growth_score),
            "StabilityScore": round(stability_score),
            "FinancialScore": round(financial_score, 1)
        },
        "NewsScore": round(news_score),
        "ChartScore": round(chart_score),
        "Final Investment Attractiveness Score": round(final_score, 1),
        "Verdict": verdict,
        "Reasoning Summary": reasoning.strip(),
        "detailed_reasoning": {
            "valuation": val_reason,
            "profitability": profit_reason,
            "growth": growth_reason,
            "stability": stabil_reason
        }
    }

    return result

def format_output(result: Dict[str, Any]) -> str:
    """Format result as plain text report."""
    lines = []
    lines.append("1. Financial Breakdown")
    fb = result['financial_breakdown']
    lines.append(f"- ValuationScore: {fb['ValuationScore']}")
    lines.append(f"- ProfitabilityScore: {fb['ProfitabilityScore']}")
    lines.append(f"- GrowthScore: {fb['GrowthScore']}")
    lines.append(f"- StabilityScore: {fb['StabilityScore']}")
    lines.append(f"- FinancialScore: {fb['FinancialScore']}")
    lines.append("")
    lines.append(f"2. NewsScore: {result['NewsScore']}")
    lines.append("")
    lines.append(f"3. ChartScore: {result['ChartScore']}")
    lines.append("")
    lines.append(f"4. Final Investment Attractiveness Score: {result['Final Investment Attractiveness Score']} / 100")
    lines.append("")
    lines.append(f"5. Verdict: {result['Verdict']}")
    lines.append("")
    lines.append("6. Reasoning Summary:")
    lines.append(result['Reasoning Summary'])

    return "\n".join(lines)

def main():
    parser = argparse.ArgumentParser(description='Equity Analyst - Stock Analysis Score Calculator')
    parser.add_argument('--input', required=True, help='Input JSON file with stock data')
    parser.add_argument('--output', required=False, help='Output file (defaults to stdout)')

    args = parser.parse_args()

    # Load input data
    with open(args.input, 'r', encoding='utf-8') as f:
        data = json.load(f)

    # Analyze
    result = analyze_stock(data)

    # Format output
    output_text = format_output(result)

    # Output
    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            f.write(output_text)
    else:
        print(output_text)

    # Also return JSON for programmatic use
    return result

if __name__ == '__main__':
    result = main()
    # Print JSON to stderr for debugging/capturing
    import json
    print("\n--- JSON OUTPUT ---", file=sys.stderr)
    print(json.dumps(result, ensure_ascii=False, indent=2), file=sys.stderr)


path: scripts/daily_popular_report.py
content: #!/usr/bin/env python3
"""
Daily Popular Stocks Report
- Opens Naver Finance main page via openclaw browser
- Extracts top 5 popular search stocks
- Visits each stock page, scrapes financial data
- Runs equity analysis
- Generates report and sends to LINE
"""

import subprocess, json, re, sys, os, time
from datetime import datetime

def run_cmd(cmd, timeout=30):
    """Run shell command and return output."""
    result = subprocess.run(
        cmd,
        shell=True,
        capture_output=True,
        text=True,
        timeout=timeout
    )
    return result.stdout, result.stderr, result.returncode

def browser_open(url):
    """Open URL in openclaw browser."""
    cmd = f'openclaw browser --browser-profile openclaw open "{url}"'
    out, err, rc = run_cmd(cmd)
    return rc == 0

def browser_snapshot(save_path=None):
    """Get snapshot of current page. Optionally save to file."""
    if save_path:
        cmd = f'openclaw browser --browser-profile openclaw snapshot > "{save_path}" 2>&1'
        out, err, rc = run_cmd(cmd, timeout=20)
        if rc == 0:
            # Read file with cp949 encoding (Windows default)
            try:
                with open(save_path, 'r', encoding='cp949') as f:
                    return f.read()
            except UnicodeDecodeError:
                # Fallback to utf-8
                with open(save_path, 'r', encoding='utf-8', errors='ignore') as f:
                    return f.read()
        else:
            print(f"Snapshot error: {err}", file=sys.stderr)
            return None
    else:
        cmd = 'openclaw browser --browser-profile openclaw snapshot'
        out, err, rc = run_cmd(cmd, timeout=20)
        if rc == 0:
            return out
        else:
            print(f"Snapshot error: {err}", file=sys.stderr)
            return None

def parse_popular_stocks_from_snapshot(snapshot_text):
    """
    Parse the snapshot output to extract top 5 popular search stocks.
    Expected format from earlier snapshot: "인기 검색 종목" table with rows like:
    '1.삼성전자 158,600 하락 700' and link with code 005930.
    We'll extract name and code from the rowheader links.
    """
    stocks = []
    lines = snapshot_text.split('\n')
    in_popular_section = False

    # Patterns for popular stocks table
    # Row pattern: rowheader with link: 'link "삼성전자" ... /code=005930'
    code_pattern = re.compile(r'/item/main\.naver\?code=(\d{6})')
    name_pattern = re.compile(r'link "([^"]+)" \[ref=')

    for line in lines:
        # Detect start of popular stocks section
        if '인기 검색 종목' in line:
            in_popular_section = True
            continue
        if in_popular_section and '더보기' in line:
            break  # End of table

        if in_popular_section:
            # Look for rows with stock codes
            if '/item/main.naver?code=' in line:
                # Extract code
                code_match = code_pattern.search(line)
                if code_match:
                    code = code_match.group(1)
                    # Extract name: look for the link text just before the URL
                    # The pattern typically: link "NAME" ... /url...
                    name_match = name_pattern.search(line)
                    if name_match:
                        name = name_match.group(1)
                    else:
                        # Fallback: maybe the line contains name in quotes earlier
                        # Try another approach: find the text between quotes before the URL
                        alt_match = re.search(r'"([^"]+)"\s+\[ref=', line)
                        if alt_match:
                            name = alt_match.group(1)
                        else:
                            name = "Unknown"

                    # Avoid duplicates
                    if not any(s['code'] == code for s in stocks):
                        stocks.append({"name": name, "code": code})

                    if len(stocks) >= 5:
                        break

    return stocks[:5]

def scrape_stock_data(ticker):
    """
    Visit stock page and scrape required data.
    Returns dict with ticker, company_name, financials, news_summary, chart_description.
    """
    # Use mobile version to avoid bot detection
    url = f"https://m.stock.naver.com/item/main.nhn?code={ticker}"
    if not browser_open(url):
        print(f"Failed to open {url}", file=sys.stderr)
        return None

    time.sleep(4)  # Wait longer for page load (mobile page might be slower)

    snapshot = browser_snapshot()
    if not snapshot:
        return None

    # Parse data from snapshot (simplified)
    # We'll extract PER, PBR, ROE, etc. from the snapshot text
    data = {
        "ticker": ticker,
        "company_name": "",
        "financials": {
            "PER": 0.0,
            "PBR": 0.0,
            "ROE": 0.0,
            "operating_margin": 0.0,
            "debt_ratio": 0.0,
            "revenue_growth": 0.0
        },
        "news_summary": "",
        "chart_description": "neutral"
    }

    # Extract company name from title or heading
    name_match = re.search(r'<strong[^>]*>([^<]+)</strong>', snapshot)
    if name_match:
        data["company_name"] = name_match.group(1).strip()
    else:
        data["company_name"] = ticker  # fallback

    # Extract financial metrics from the snapshot (look for table rows)
    # In the snapshot we saw: 'ROE(지배주주) 8.37' etc.
    # Pattern: label + number + unit (% or 배)
    # We'll search for these labels

    # PER (배), PBR (배), ROE (%), 영업이익률 (%), 부채비율 (%), 매출성장률 (%)
    finance_labels = {
        'PER': (r'PER\(배\)\s*([\d\.]+)', 1.0),
        'PBR': (r'PBR\(배\)\s*([\d\.]+)', 1.0),
        'ROE': (r'ROE\(지배주주\)\s*([\d\.]+)', 1.0),
        'operating_margin': (r'영업이익률\s*([\d\.]+)', 1.0),
        'debt_ratio': (r'부채비율\s*([\d\.]+)', 1.0),
        'revenue_growth': (r'매출성장률\s*([\d\.]+)', 1.0)
    }

    for key, (pattern, scale) in finance_labels.items():
        match = re.search(pattern, snapshot)
        if match:
            try:
                data["financials"][key] = float(match.group(1)) * scale
            except:
                pass

    # Extract news summary (grab a few recent news headlines)
    # Look for news section links or titles
    news_headlines = []
    news_matches = re.findall(r'<a[^>]*>([^<]+)</a>', snapshot)
    for match in news_matches:
        title = match.strip()
        if len(title) > 10 and ('[' in title or '전망' in title or '공급' in title or 'AI' in title):
            news_headlines.append(title)
        if len(news_headlines) >= 3:
            break
    data["news_summary"] = "; ".join(news_headlines) if news_headlines else "No major news."

    # Extract chart description (simple: look for trend keywords in snapshot)
    if '상승' in snapshot and '하락' not in snapshot[-500:]:
        data["chart_description"] = "uptrend"
    elif '하락' in snapshot[-500:]:
        data["chart_description"] = "downtrend"
    else:
        data["chart_description"] = "neutral"

    return data

def analyze_stock_with_script(data):
    """Call analyze.py to compute scores."""
    import tempfile, os

    # Write input to temp JSON
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        input_path = f.name

    script_path = os.path.join(os.path.dirname(__file__), 'analyze.py')
    python_exe = "C:/Users/IM/AppData/Local/Programs/Python/Python310/python.exe"

    try:
        result = subprocess.run(
            [python_exe, script_path, '--input', input_path],
            capture_output=True,
            text=True,
            timeout=15
        )
        # Parse JSON output from stderr
        if result.stderr:
            json_start = result.stderr.find('{')
            if json_start != -1:
                json_str = result.stderr[json_start:]
                return json.loads(json_str)
        return {"error": "No JSON output", "raw": result.stderr}
    except Exception as e:
        return {"error": str(e)}
    finally:
        try:
            os.unlink(input_path)
        except:
            pass

def generate_report(results, date_str):
    """Format the final report."""
    lines = [f"[{date_str}] 유력종목 아침 리포트 (인기검색 TOP 5)\n"]
    lines.append("순위 | 종목 | 코드 | 점수 | 추천 | 핵심근거")
    lines.append("-" * 60)

    sorted_results = sorted(results, key=lambda x: x.get('final_score', 0), reverse=True)

    for idx, r in enumerate(sorted_results, 1):
        name = r.get('company_name', 'N/A')[:6]
        ticker = r.get('ticker', 'N/A')
        score = r.get('final_score', 0)
        verdict = r.get('verdict', 'N/A')
        reasoning = r.get('reasoning', '')[:30] + "..." if r.get('reasoning') else ''
        lines.append(f"{idx:<4} | {name:<6} | {ticker} | {score:<5.1f} | {verdict:<8} | {reasoning}")

    lines.append("\n=== 상세 analyze 결과는 상위 3개만 표시 ===\n")
    for idx, r in enumerate(sorted_results[:3], 1):
        lines.append(f"{idx}. {r.get('company_name')} ({r.get('ticker')})")
        lines.append(f"   Final Score: {r.get('final_score', 0):.1f} / 100")
        lines.append(f"   Verdict: {r.get('verdict')}")
        fb = r.get('financial_breakdown', {})
        lines.append(f"   재무: Valuation {fb.get('ValuationScore')}, Profit {fb.get('ProfitabilityScore')}, Growth {fb.get('GrowthScore')}, Stability {fb.get('StabilityScore')}")
        lines.append(f"   뉴스: {r.get('news_score')}, 차트: {r.get('chart_score')}")
        lines.append("")

    return "\n".join(lines)

def main():
    print("=== Daily Popular Report Started ===")

    # Step 1: Open Naver Finance main page
    print("1. Opening Naver Finance main page...")
    if not browser_open("https://finance.naver.com/"):
        print("Failed to open browser.", file=sys.stderr)
        return

    time.sleep(3)
    snapshot = browser_snapshot()
    if not snapshot:
        print("Failed to get snapshot.", file=sys.stderr)
        return

    # Step 2: Extract top 5 popular stocks
    print("2. Extracting top 5 popular stocks...")
    top_stocks = parse_popular_stocks_from_snapshot(snapshot)
    if not top_stocks:
        print("No stocks extracted. Using fallback list.", file=sys.stderr)
        top_stocks = [
            {"name": "삼성전자", "code": "005930"},
            {"name": "SK하이닉스", "code": "000660"},
            {"name": "현대차", "code": "005380"},
            {"name": "한화솔루션", "code": "009830"},
            {"name": "두산에너빌리", "code": "034020"}
        ]

    print(f"   Found: {[s['name'] for s in top_stocks]}")

    # Step 3: Scrape each stock and analyze
    results = []
    for stock in top_stocks:
        print(f"3. Analyzing {stock['name']} ({stock['code']})...")
        data = scrape_stock_data(stock['code'])
        if not data:
            print(f"   Skipping {stock['name']} due to scrape failure.")
            continue

        analysis = analyze_stock_with_script(data)
        if 'error' not in analysis:
            # Flatten keys for easier report generation
            analysis['final_score'] = analysis.get('Final Investment Attractiveness Score', 0)
            analysis['verdict'] = analysis.get('Verdict', 'N/A')
            analysis['financial_breakdown'] = analysis.get('financial_breakdown', {})
            analysis['news_score'] = analysis.get('NewsScore', 0)
            analysis['chart_score'] = analysis.get('ChartScore', 0)
            analysis['reasoning'] = analysis.get('Reasoning Summary', '')
            results.append(analysis)
        else:
            print(f"   Analysis error: {analysis.get('error')}")

    # Step 4: Generate report
    today = datetime.now().strftime('%Y-%m-%d')
    report = generate_report(results, today)
    print("\n=== REPORT PREVIEW ===")
    print(report)

    # Save report
    report_path = 'C:/Users/IM/.openclaw/workspace/daily_popular_report.txt'
    with open(report_path, 'w', encoding='utf-8') as f:
        f.write(report)
    print(f"\nReport saved to {report_path}")

    # Step 5: Send to LINE via cron delivery (will be handled by cron job systemEvent -> isolated agentTurn)
    # For manual run, we could also send immediately:
    # message send --channel line --to <user_id> --message "$(cat report_path)"
    print("\n=== Report Generation Complete ===")

if __name__ == '__main__':
    main()

path: scripts/morning_report.py
content: #!/usr/bin/env python3
"""
Morning Report Generator
Analyzes top 5 KRX stocks and sends LINE report.
"""

import json
import subprocess
import sys
from datetime import datetime

# Top 5 influential stocks (hardcoded for now)
TICKERS = [
    {"code": "005930", "name": "삼성전자"},
    {"code": "000660", "name": "SK하이닉스"},
    {"code": "035720", "name": "카카오"},
    {"code": "035420", "name": "네이버"},
    {"code": "005380", "name": "현대차"}
]

def fetch_naver_finance(ticker: str) -> dict:
    """Scrape data from Naver Finance (simplified)."""
    try:
        import requests
        from bs4 import BeautifulSoup
        import re

        url = f"https://finance.naver.com/item/main.naver?code={ticker}"
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        html = response.text
        soup = BeautifulSoup(html, 'html.parser')

        # Extract financial metrics
        data = {}
        tables = soup.find_all('table')
        for table in tables:
            text = table.get_text()
            if 'PER' in text and 'PBR' in text and 'ROE' in text:
                rows = table.find_all('tr')
                for row in rows:
                    cols = row.find_all(['th', 'td'])
                    if len(cols) >= 2:
                        label = cols[0].get_text(strip=True)
                        value = cols[1].get_text(strip=True)
                        try:
                            value_clean = re.sub(r'[^\d\.\-]', '', value)
                            if value_clean:
                                float_val = float(value_clean)
                                if 'PER' in label and '배' in label:
                                    data['PER'] = float_val
                                elif 'PBR' in label and '배' in label:
                                    data['PBR'] = float_val
                                elif 'ROE' in label and '%' in label:
                                    data['ROE'] = float_val
                                elif '영업이익률' in label and '%' in label:
                                    data['operating_margin'] = float_val
                                elif '부채비율' in label and '%' in label:
                                    data['debt_ratio'] = float_val
                                elif '매출성장률' in label and '%' in label:
                                    data['revenue_growth'] = float_val
                        except (ValueError, IndexError):
                            continue
                break

        # Fallback extraction from entire page
        defaults = {'PER': 0.0, 'PBR': 0.0, 'ROE': 0.0, 'operating_margin': 0.0, 'debt_ratio': 0.0, 'revenue_growth': 0.0}
        for key, default in defaults.items():
            data.setdefault(key, default)

        # News summary (simple)
        news_section = soup.find('div', {'class': re.compile(r'news|press')})
        news_list = []
        if news_section:
            links = news_section.find_all('a', limit=3)
            for link in links:
                title = link.get_text(strip=True)
                if title and len(title) > 10:
                    news_list.append(title)
        news_summary = '; '.join(news_list) if news_list else "No recent news."

        # Chart description (simple trend from price movement)
        price_section = soup.find('div', {'class': re.compile(r'price|current')})
        chart_desc = "neutral"
        if price_section:
            txt = price_section.get_text()
            if '상승' in txt:
                chart_desc = "uptrend"
            elif '하락' in txt:
                chart_desc = "downtrend"
        else:
            chart_desc = "no explicit trend"

        return {
            "ticker": ticker,
            "financials": data,
            "news_summary": news_summary,
            "chart_description": chart_desc
        }

    except Exception as e:
        print(f"Error scraping {ticker}: {e}", file=sys.stderr)
        return {
            "ticker": ticker,
            "financials": {"PER": 0, "PBR": 0, "ROE": 0, "operating_margin": 0, "debt_ratio": 0, "revenue_growth": 0},
            "news_summary": "Error fetching data",
            "chart_description": "unknown"
        }

def analyze_with_script(data: dict) -> dict:
    """Call analyze.py to compute score."""
    import tempfile
    import os

    # Write input JSON to temp file
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        input_path = f.name

    # Output to temp file
    output_path = input_path.replace('.json', '_out.txt')

    # Call analyzer
    script_path = os.path.join(os.path.dirname(__file__), 'analyze.py')
    python_exe = "C:/Users/IM/AppData/Local/Programs/Python/Python310/python.exe"

    try:
        result = subprocess.run(
            [python_exe, script_path, '--input', input_path],
            capture_output=True,
            text=True,
            timeout=15
        )
        # Parse JSON output from stderr (analyze.py prints JSON to stderr)
        if result.stderr:
            json_start = result.stderr.find('{')
            if json_start != -1:
                json_str = result.stderr[json_start:]
                return json.loads(json_str)
        return {"error": "No JSON output", "raw": result.stderr}
    except Exception as e:
        return {"error": str(e)}
    finally:
        # Cleanup
        try:
            os.unlink(input_path)
            if os.path.exists(output_path):
                os.unlink(output_path)
        except:
            pass

def generate_report(results: list) -> str:
    """Create formatted report string."""
    today = datetime.now().strftime('%Y-%m-%d')
    lines = [f"[{today}] 유력종목 아침 리포트\n"]
    lines.append("순위 | 종목 | 코드 | 점수 | 추천 | 주요이유")
    lines.append("-" * 50)

    # Sort by final score descending
    sorted_results = sorted(results, key=lambda x: x.get('Final Investment Attractiveness Score', 0), reverse=True)

    for idx, r in enumerate(sorted_results, 1):
        name = r.get('company_name', 'N/A')
        ticker = r.get('ticker', 'N/A')
        score = r.get('Final Investment Attractiveness Score', 0)
        verdict = r.get('Verdict', 'N/A')
        reasoning = r.get('Reasoning Summary', '')[:40] + "..." if r.get('Reasoning Summary') else ''
        lines.append(f"{idx:<4} | {name:<6} | {ticker} | {score:<5} | {verdict:<8} | {reasoning}")

    return "\n".join(lines)

def main():
    all_results = []

    print("[Morning Report] Starting analysis...")
    for stock in TICKERS:
        print(f"  Analyzing {stock['name']} ({stock['code']})...")
        # Scrape fresh data (would work if network allowed)
        # data = fetch_naver_finance(stock['code'])
        # For now, use previously scraped data or mock if fails
        try:
            data = fetch_naver_finance(stock['code'])
        except Exception as e:
            print(f"    Skipping {stock['name']} due to fetch error: {e}")
            continue

        # Analyze
        result = analyze_with_script(data)
        if 'error' not in result:
            all_results.append(result)
        else:
            print(f"    Analysis error: {result.get('error')}")

    if all_results:
        report = generate_report(all_results)
        print("\n=== REPORT PREVIEW ===")
        print(report)
        # Also write to file for cron delivery
        with open('morning_report_output.txt', 'w', encoding='utf-8') as f:
            f.write(report)
        print("\n✅ Report generated and saved to morning_report_output.txt")
    else:
        print("❌ No results to report.")

if __name__ == '__main__':
    main()

path: scripts/quick_report.py
content: #!/usr/bin/env python3
"""Quick test of morning report pipeline using pre-made JSON data."""
import json, sys, os
from datetime import datetime

# Add skill scripts to path
skill_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, skill_dir)

from scripts.analyze import analyze_stock, format_output

# Load test data
with open('C:/Users/IM/.openclaw/workspace/test_morning_report.json', 'r', encoding='utf-8') as f:
    stocks = json.load(f)

results = []
for stock in stocks:
    try:
        result = analyze_stock(stock)
        results.append(result)
        print(f"Analyzed {stock['company_name']}: {result['Final Investment Attractiveness Score']} ({result['Verdict']})")
    except Exception as e:
        print(f"Failed {stock['company_name']}: {e}")

# Generate report
today = datetime.now().strftime('%Y-%m-%d')
report_lines = [f"[{today}] 유력종목 아침 리포트\n"]
report_lines.append("순위 | 종목 | 코드 | 점수 | 추천")
report_lines.append("-" * 40)

sorted_results = sorted(results, key=lambda x: x.get('Final Investment Attractiveness Score', 0), reverse=True)

for idx, r in enumerate(sorted_results, 1):
    name = r.get('company_name', 'N/A')
    ticker = r.get('ticker', 'N/A')
    score = r.get('Final Investment Attractiveness Score', 0)
    verdict = r.get('Verdict', 'N/A')
    report_lines.append(f"{idx:<4} | {name:<6} | {ticker} | {score:<5.1f} | {verdict}")

report = "\n".join(report_lines)
print("\n" + report)

with open('C:/Users/IM/.openclaw/workspace/morning_report_output.txt', 'w', encoding='utf-8') as f:
    f.write(report)

print("\nSaved to morning_report_output.txt")

path: scripts/scrape_naver.py
content: #!/usr/bin/env python3
"""
Scrape Naver Finance page for stock data.

Usage:
    python scrape_naver.py <ticker> [--output data.json]

Example:
    python scrape_naver.py 251270 --output netmarble_data.json
"""

import argparse
import json
import sys
import re
from typing import Dict, Any, Optional

try:
    import requests
    from bs4 import BeautifulSoup
except ImportError:
    print("Error: requests and beautifulsoup4 are required. Install with: pip install requests beautifulsoup4")
    sys.exit(1)


def fetch_naver_finance(ticker: str) -> str:
    """Fetch the HTML content from Naver Finance."""
    url = f"https://finance.naver.com/item/main.naver?code={ticker}"
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.text


def extract_financial_data(html: str) -> Dict[str, float]:
    """Extract financial metrics from Naver Finance HTML."""
    soup = BeautifulSoup(html, 'html.parser')

    data = {}

    # Find the table with financial data (typically "재무정보" table)
    # Naver Finance structure: there are multiple tables, we need to find the right one
    tables = soup.find_all('table')

    # Look for table containing PER, PBR, ROE etc.
    for table in tables:
        text = table.get_text()
        if 'PER' in text and 'PBR' in text and 'ROE' in text:
            # Found candidate table
            rows = table.find_all('tr')
            for row in rows:
                cols = row.find_all(['th', 'td'])
                if len(cols) >= 2:
                    label = cols[0].get_text(strip=True)
                    value = cols[1].get_text(strip=True)
                    # Clean value (remove commas, convert to float)
                    try:
                        value_clean = re.sub(r'[^\d\.\-]', '', value)
                        if value_clean:
                            float_val = float(value_clean)
                            if 'PER' in label and '배' in label:
                                data['PER'] = float_val
                            elif 'PBR' in label and '배' in label:
                                data['PBR'] = float_val
                            elif 'ROE' in label and '%' in label:
                                data['ROE'] = float_val
                            elif '영업이익률' in label and '%' in label:
                                data['operating_margin'] = float_val
                            elif '부채비율' in label and '%' in label:
                                data['debt_ratio'] = float_val
                            elif '매출성장률' in label and '%' in label:
                                data['revenue_growth'] = float_val
                    except (ValueError, IndexError):
                        continue
            break

    # If some data is missing, try alternative parsing (e.g., from summary section)
    if 'PER' not in data or 'PBR' not in data or 'ROE' not in data:
        # Look for '시가총액' section or other areas
        # Sometimes data is in a different format with spans/classes
        # This is a fallback heuristics
        for element in soup.find_all(['span', 'div', 'p']):
            text = element.get_text()
            if 'PER' in text:
                match = re.search(r'(\d+\.?\d*)\s*배', text)
                if match:
                    data['PER'] = float(match.group(1))
            if 'PBR' in text:
                match = re.search(r'(\d+\.?\d*)\s*배', text)
                if match:
                    data['PBR'] = float(match.group(1))
            if 'ROE' in text:
                match = re.search(r'(\d+\.?\d*)\s*%', text)
                if match:
                    data['ROE'] = float(match.group(1))
            if '영업이익률' in text:
                match = re.search(r'(\d+\.?\d*)\s*%', text)
                if match:
                    data['operating_margin'] = float(match.group(1))
            if '부채비율' in text:
                match = re.search(r'(\d+\.?\d*)\s*%', text)
                if match:
                    data['debt_ratio'] = float(match.group(1))
            if '매출성장률' in text:
                match = re.search(r'(\d+\.?\d*)\s*%', text)
                if match:
                    data['revenue_growth'] = float(match.group(1))

    # Ensure all fields exist (set to 0 or None if missing)
    defaults = {
        'PER': 0.0,
        'PBR': 0.0,
        'ROE': 0.0,
        'operating_margin': 0.0,
        'debt_ratio': 0.0,
        'revenue_growth': 0.0
    }
    for key, default in defaults.items():
        data.setdefault(key, default)

    return data


def extract_news_summary(soup: BeautifulSoup) -> str:
    """Extract recent news headlines or summary from the page."""
    # Look for news section - often in a div with class 'news_section' or similar
    news_list = []
    news_section = soup.find('div', {'class': re.compile(r'news|press|article')})
    if news_section:
        links = news_section.find_all('a', limit=5)
        for link in links:
            title = link.get_text(strip=True)
            if title and len(title) > 10:
                news_list.append(title)
    if news_list:
        return "Recent news: " + "; ".join(news_list[:3])
    return "No recent news found."


def extract_chart_description(soup: BeautifulSoup) -> str:
    """Extract simple chart/trend information."""
    # Look for price trend indicators, maybe from a chart img alt text or summary
    # Naver Finance often shows a small chart image, we can look for alt text
    chart_img = soup.find('img', {'alt': re.compile(r'차트|추세|일간|주간|월간')})
    if chart_img:
        alt_text = chart_img.get('alt', '')
        return f"Chart shows: {alt_text}" if alt_text else "Chart: trend not specified"
    # Fallback: check if current price is above/below moving averages mentioned
    price_section = soup.find('div', {'class': re.compile(r'price|current')})
    if price_section:
        price_text = price_section.get_text()
        if '상승' in price_text:
            return "Current trend: 상승 (rising)"
        elif '하락' in price_text:
            return "Current trend: 하락 (falling)"
    return "Chart: neutral/no explicit trend info"


def scrape_stock(ticker: str) -> Dict[str, Any]:
    """Main function to scrape all data for a given ticker."""
    try:
        html = fetch_naver_finance(ticker)
    except requests.RequestException as e:
        print(f"Error fetching data for {ticker}: {e}", file=sys.stderr)
        sys.exit(1)

    soup = BeautifulSoup(html, 'html.parser')

    financials = extract_financial_data(html)
    news_summary = extract_news_summary(soup)
    chart_description = extract_chart_description(soup)

    # Get company name
    title_tag = soup.find('title')
    company_name = title_tag.get_text().split('-')[0].strip() if title_tag else "Unknown"

    return {
        "ticker": ticker,
        "company_name": company_name,
        "financials": financials,
        "news_summary": news_summary,
        "chart_description": chart_description
    }


def main():
    parser = argparse.ArgumentParser(description='Scrape Naver Finance for stock data')
    parser.add_argument('ticker', help='Stock ticker (e.g., 251270)')
    parser.add_argument('--output', help='Output JSON file path', default=None)

    args = parser.parse_args()

    data = scrape_stock(args.ticker)

    output_json = json.dumps(data, ensure_ascii=False, indent=2)
    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            f.write(output_json)
        print(f"Data saved to {args.output}")
    else:
        print(output_json)

    return data


if __name__ == '__main__':
    main()


path: skill.yaml
content: name: equity-analyst
version: 1.0.4
bins:
  - "C:/Users/IM/AppData/Local/Programs/Python/Python310/python.exe"

path: _meta.json
content: ownerId: kn73mkxnbwe4r37kxmbx40tba980hfcq
slug: equity-analyst
version: 1.0.5
publishedAt: 1770464588572

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)