Skill: popwho/etf-finance (version 1.1.0)

--- name: etf-finance description: ETF and fund portfolio manager with price alerts, profit/loss tracking, and position management. Track your ETF/fund holdings, calculate gains/losses, set price alerts, and monitor performance in real-time using 腾讯财经 (Tencent Finance) for Chinese A-shares and Yahoo Finance for US stocks. --- # ETF Finance Skill A comprehensive ETF and fund portfolio management tool with real-time price tracking, profit/loss calculations, and price alert functionality. ## 功能概述 - 持仓管理：添加、删除、查看 ETF/基金持仓 - 盈亏计算：实时计算持仓盈亏和收益率 - 价格提醒：设置目标价格，达到时自动提醒 - 行情监控：实时获取 ETF/基金价格数据 ## 使用方法 ### 持仓管理 #### 添加持仓 ```bash cd ~/.openclaw/workspace/skills/etf-investor python3 scripts/add_position.py SPY 150.50 100 "S&P 500 ETF" ``` 参数：代码 | 买入价 | 数量 | 名称 #### 删除持仓 ```bash python3 scripts/remove_position.py SPY ``` #### 查看持仓列表 ```bash python3 scripts/list_positions.py ``` #### 查看持仓详情（含盈亏） ```bash python3 scripts/portfolio_summary.py ``` ### 价格提醒 #### 设置价格提醒 ```bash python3 scripts/add_alert.py SPY 160.00 "price_up" # 或 python3 scripts/add_alert.py SPY 140.00 "price_down" ``` #### 查看所有提醒 ```bash python3 scripts/list_alerts.py ``` #### 删除提醒 ```bash python3 scripts/remove_alert.py SPY ``` #### 检查提醒状态 ```bash python3 scripts/check_alerts.py ``` ### 行情查询 #### 查询单个 ETF/基金价格 ```bash python3 scripts/get_price.py SPY ``` #### 批量查询持仓价格 ```bash python3 scripts/update_prices.py ``` ## 持仓数据格式 持仓存储在 `~/.clawdbot/etf_investor/positions.json`: ```json [ { "symbol": "SPY", "name": "S&P 500 ETF", "buy_price": 150.50, "quantity": 100, "buy_date": "2026-02-23", "current_price": 155.20, "last_updated": "2026-02-23T10:30:00Z" } ] ``` ## 价格提醒数据格式 提醒存储在 `~/.clawdbot/etf_investor/alerts.json`: ```json [ { "symbol": "SPY", "target_price": 160.00, "condition": "price_up", "created_at": "2026-02-23T10:00:00Z", "triggered": false } ] ``` ## 盈亏计算公式 - **持仓市值**: 当前价 × 数量 - **成本**: 买入价 × 数量 - **盈亏金额**: (当前价 - 买入价) × 数量 - **盈亏比例**: (当前价 - 买入价) / 买入价 × 100% ## 支持的代码格式 - **美股 ETF**: SPY, QQQ, VOO, VTI, IVV, IWM - **国际 ETF**: EFA, VEA, VWO, VXUS - **行业 ETF**: XLF, XLK, XLV, XLE, XLB - **基金**: 使用 Yahoo Finance 支持的任何基金代码 ## 安装与卸载 ### 安装 ```bash cd ~/.openclaw/workspace/skills/etf-investor bash scripts/install.sh ``` ### 卸载 ```bash bash scripts/uninstall.sh ``` ## 自动提醒 可以将 `check_alerts.py` 加入系统定时任务（如 cron）实现自动提醒： ```bash # 每小时检查一次提醒 0 * * * * cd ~/.openclaw/workspace/skills/etf-investor && python3 scripts/check_alerts.py ``` ## 注意事项 - 数据来源：Yahoo Finance (免费，无 API Key) - 价格可能有 15 分钟延迟（美股） - 建议定期备份数据文件 - 价格提醒仅在本地检查，不会推送通知 ## 脚本说明 - `add_position.py` - 添加持仓 - `remove_position.py` - 删除持仓 - `list_positions.py` - 列出所有持仓 - `portfolio_summary.py` - 持仓摘要（含盈亏） - `add_alert.py` - 添加价格提醒 - `remove_alert.py` - 删除价格提醒 - `list_alerts.py` - 列出所有提醒 - `check_alerts.py` - 检查提醒状态 - `get_price.py` - 获取单个价格 - `update_prices.py` - 批量更新价格 - `install.sh` - 安装脚本 - `uninstall.sh` - 卸载脚本

path: README.md
content: # ETF Finance Skill

A comprehensive ETF and fund portfolio management tool for OpenClaw.

## Features

- **Position Management**: Add, remove, and track your ETF/fund holdings
- **Profit/Loss Calculation**: Real-time calculation of gains/losses with percentage
- **Price Alerts**: Set target prices for alerts when prices go up or down
- **Real-time Data**: Fetches live prices from Yahoo Finance (free, no API key required)

## Installation

```bash
cd ~/.openclaw/workspace/skills/etf-investor
bash scripts/install.sh
```

The install script will:
- Create the data directory at `~/.clawdbot/etf_investor/`
- Set up a Python virtual environment with yfinance
- Initialize empty positions and alerts files

## Quick Start

### 1. Add a position
```bash
python3 scripts/add_position.py SPY 680.00 10 "S&P 500 ETF"
```

### 2. View your portfolio
```bash
python3 scripts/portfolio_summary.py
```

### 3. Set a price alert
```bash
python3 scripts/add_alert.py SPY 700.00 price_up
```

## Commands

### Position Management

| Command | Description |
|---------|-------------|
| `add_position.py` | Add a new position |
| `remove_position.py` | Remove a position |
| `list_positions.py` | List all positions |
| `portfolio_summary.py` | Show portfolio with P/L |

### Price Alerts

| Command | Description |
|---------|-------------|
| `add_alert.py` | Add a price alert |
| `remove_alert.py` | Remove alerts for a symbol |
| `list_alerts.py` | List all alerts |
| `check_alerts.py` | Check if any alerts triggered |

### Price Queries

| Command | Description |
|---------|-------------|
| `get_price.py` | Get current price for a symbol |
| `update_prices.py` | Update prices for all positions |

## Supported Symbols

- **US ETFs**: SPY, QQQ, VOO, VTI, IVV, IWM, etc.
- **International ETFs**: EFA, VEA, VWO, VXUS, etc.
- **Sector ETFs**: XLF, XLK, XLV, XLE, XLB, etc.
- **Funds**: Any fund symbol supported by Yahoo Finance

## Data Storage

All data is stored locally in `~/.clawdbot/etf_investor/`:
- `positions.json` - Your holdings
- `alerts.json` - Price alerts

## Uninstall

```bash
bash scripts/uninstall.sh
```

This will remove all data files and configurations.

## License

MIT License


path: _meta.json
content: ownerId: 887452f80776baf6ce7dd4e271f4b5c96db4f9fc9addfcd0b2bb5cb2a6fa0d71
slug: etf-finance
version: 1.0.0
publishedAt: 1740309120000

path: scripts/add_alert.py
content: #!/usr/bin/env python3
"""
Add price alert for a symbol.
Usage: python3 add_alert.py <symbol> <target_price> <condition>
Condition: 'price_up' (above) or 'price_down' (below)
"""
import sys
from datetime import datetime
from utils import load_alerts, save_alerts, get_price, find_alert


def add_alert(symbol, target_price, condition):
    """Add a price alert for a symbol."""
    alerts = load_alerts()

    # Validate condition
    if condition not in ['price_up', 'price_down']:
        print("Error: condition must be 'price_up' or 'price_down'")
        return False

    # Validate target price
    try:
        target_price = float(target_price)
    except ValueError:
        print("Error: target_price must be a number")
        return False

    # Check for existing alerts
    existing = find_alert(symbol, alerts)
    for alert in existing:
        if alert['target_price'] == target_price and alert['condition'] == condition and not alert.get('triggered', False):
            print(f"Alert already exists: {symbol} ${target_price:.2f} ({condition})")
            return False

    # Get current price for reference
    current_price = get_price(symbol)
    if current_price is None:
        print(f"Warning: Could not fetch current price for {symbol}")

    # Create alert
    alert = {
        'symbol': symbol.upper(),
        'target_price': target_price,
        'condition': condition,
        'created_at': datetime.now().isoformat() + 'Z',
        'triggered': False,
        'triggered_at': None
    }

    # Add to alerts
    alerts.append(alert)
    save_alerts(alerts)

    # Display result
    condition_text = "above" if condition == 'price_up' else "below"
    print(f"\nAdded alert for {symbol.upper()}")
    print(f"  Alert when price goes {condition_text} ${target_price:.2f}")
    if current_price:
        print(f"  Current price: ${current_price:.2f}")

    return True


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("Usage: python3 add_alert.py <symbol> <target_price> <condition>")
        print("\nCondition options:")
        print("  price_up - Alert when price goes above target")
        print("  price_down - Alert when price goes below target")
        print("\nExample:")
        print("  python3 add_alert.py SPY 160.00 price_up")
        print("  python3 add_alert.py SPY 140.00 price_down")
        sys.exit(1)

    symbol = sys.argv[1]
    target_price = sys.argv[2]
    condition = sys.argv[3]

    add_alert(symbol, target_price, condition)


path: scripts/add_position.py
content: #!/usr/bin/env python3
"""
Add position to portfolio.
Usage: python3 add_position.py <symbol> <buy_price> <quantity> [name]
"""
import sys
from datetime import datetime
from utils import load_positions, save_positions, find_position, get_price


def add_position(symbol, buy_price, quantity, name=None):
    """Add a new position to the portfolio."""
    # Load existing positions
    positions = load_positions()

    # Check if position already exists
    existing = find_position(symbol, positions)
    if existing:
        print(f"Position for {symbol} already exists:")
        print(f"  Buy Price: ${existing['buy_price']:.2f}")
        print(f"  Quantity: {existing['quantity']}")
        print(f"Use update_position.py to modify existing positions.")
        return False

    # Get current price
    current_price = get_price(symbol)
    if current_price is None:
        print(f"Warning: Could not fetch current price for {symbol}")

    # Validate inputs
    try:
        buy_price = float(buy_price)
        quantity = float(quantity)
    except ValueError:
        print("Error: buy_price and quantity must be numbers")
        return False

    if buy_price <= 0 or quantity <= 0:
        print("Error: buy_price and quantity must be positive")
        return False

    # If name not provided, use the symbol
    if not name:
        name = symbol

    # Create position
    position = {
        'symbol': symbol.upper(),
        'name': name,
        'buy_price': buy_price,
        'quantity': quantity,
        'buy_date': datetime.now().strftime('%Y-%m-%d'),
        'current_price': current_price,
        'last_updated': datetime.now().isoformat() + 'Z'
    }

    # Add to positions
    positions.append(position)
    save_positions(positions)

    # Display result
    print(f"\nAdded position: {symbol.upper()} - {name}")
    print(f"  Buy Price: ${buy_price:.2f}")
    print(f"  Quantity: {quantity}")
    print(f"  Cost: ${buy_price * quantity:,.2f}")
    if current_price:
        print(f"  Current Price: ${current_price:.2f}")

    return True


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("Usage: python3 add_position.py <symbol> <buy_price> <quantity> [name]")
        print("\nExample:")
        print("  python3 add_position.py SPY 150.50 100 \"S&P 500 ETF\"")
        sys.exit(1)

    symbol = sys.argv[1]
    buy_price = sys.argv[2]
    quantity = sys.argv[3]
    name = sys.argv[4] if len(sys.argv) > 4 else None

    add_position(symbol, buy_price, quantity, name)


path: scripts/check_alerts.py
content: #!/usr/bin/env python3
"""
Check price alerts and notify if triggered.
Usage: python3 check_alerts.py
"""
from datetime import datetime
from utils import load_alerts, save_alerts, get_price


def check_alerts():
    """Check all alerts and trigger if conditions are met."""
    alerts = load_alerts()
    positions = load_alerts()  # Reusing for alert storage

    if not alerts:
        print("No alerts to check.")
        return

    active_alerts = [a for a in alerts if not a.get('triggered', False)]

    if not active_alerts:
        print("No active alerts to check.")
        return

    print(f"\nChecking {len(active_alerts)} active alert(s)...")
    print("-" * 80)

    triggered_count = 0

    for alert in active_alerts:
        symbol = alert['symbol'].upper()
        target_price = alert['target_price']
        condition = alert['condition']

        # Get current price
        current_price = get_price(symbol)

        if current_price is None:
            print(f"  {symbol}: Could not fetch price")
            continue

        # Check if alert should trigger
        should_trigger = False
        condition_text = ""

        if condition == 'price_up' and current_price >= target_price:
            should_trigger = True
            condition_text = f"Price went above ${target_price:.2f}"
        elif condition == 'price_down' and current_price <= target_price:
            should_trigger = True
            condition_text = f"Price went below ${target_price:.2f}"

        if should_trigger:
            # Mark as triggered
            alert['triggered'] = True
            alert['triggered_at'] = datetime.now().isoformat() + 'Z'
            alert['triggered_price'] = current_price

            print(f"\n  ALERT TRIGGERED: {symbol}")
            print(f"  {condition_text}")
            print(f"  Current price: ${current_price:.2f}")
            print(f"  Target price: ${target_price:.2f}")

            triggered_count += 1
        else:
            condition_text = "above" if condition == 'price_up' else "below"
            diff = abs(current_price - target_price)
            print(f"  {symbol}: ${current_price:.2f} (target: ${target_price:.2f} {condition_text}, diff: ${diff:.2f})")

    # Save updated alerts
    if triggered_count > 0:
        save_alerts(alerts)
        print("\n" + "-" * 80)
        print(f"\n{triggered_count} alert(s) triggered!")
    else:
        print("\n" + "-" * 80)
        print("\nNo alerts triggered.")


if __name__ == "__main__":
    check_alerts()


path: scripts/config.py
content: #!/usr/bin/env python3
"""
Configuration for etf-investor skill.
Centralized configuration to avoid path confusion.
"""
import os
import sys

# Add venv to path for yfinance import
VENV_PATH = os.path.expanduser("~/.clawdbot/etf_investor/venv")
if os.path.exists(VENV_PATH):
    sys.path.insert(0, os.path.join(VENV_PATH, "lib", f"python3.{sys.version_info.minor}", "site-packages"))

# Data directory
DATA_DIR = os.path.expanduser("~/.clawdbot/etf_investor")

# Positions file
POSITIONS_FILE = os.path.join(DATA_DIR, "positions.json")

# Alerts file
ALERTS_FILE = os.path.join(DATA_DIR, "alerts.json")

# Price cache file (optional, for storing latest prices)
PRICES_FILE = os.path.join(DATA_DIR, "prices.json")

# Ensure directory exists
os.makedirs(DATA_DIR, exist_ok=True)

# Price update interval in seconds (default: 300 = 5 minutes)
PRICE_UPDATE_INTERVAL = 300


path: scripts/get_price.py
content: #!/usr/bin/env python3
"""
Get current price for a symbol.
Usage: python3 get_price.py <symbol>
"""
import sys
from utils import get_price


def get_price_cli(symbol):
    """Get and display current price for a symbol."""
    price = get_price(symbol)

    if price is None:
        print(f"Could not fetch price for {symbol}")
        return False

    print(f"{symbol.upper()}: ${price:.2f}")
    return True


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 get_price.py <symbol>")
        print("\nExample:")
        print("  python3 get_price.py SPY")
        sys.exit(1)

    symbol = sys.argv[1]
    get_price_cli(symbol)


path: scripts/install.sh
content: #!/bin/bash
# Install etf-investor skill

set -e

echo "Installing etf-investor skill..."

# Create the data directory
mkdir -p ~/.clawdbot/etf_investor

# Create empty files if they don't exist
DATA_DIR="$HOME/.clawdbot/etf_investor"

if [ ! -f "$DATA_DIR/positions.json" ]; then
    echo "[]" > "$DATA_DIR/positions.json"
    echo "Created empty positions file: $DATA_DIR/positions.json"
fi

if [ ! -f "$DATA_DIR/alerts.json" ]; then
    echo "[]" > "$DATA_DIR/alerts.json"
    echo "Created empty alerts file: $DATA_DIR/alerts.json"
fi

# Check if yfinance is installed
if ! python3 -c "import yfinance" 2>/dev/null; then
    echo ""
    echo "Installing yfinance package..."
    pip3 install --user yfinance || pip3 install --break-system-packages yfinance
fi

# Make scripts executable
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
chmod +x "$SCRIPT_DIR"/*.py

echo ""
echo "ETF Investor skill installed successfully!"
echo ""
echo "Data directory: $DATA_DIR"
echo ""
echo "Quick start:"
echo "  Add a position:   python3 scripts/add_position.py SPY 150.50 100"
echo "  View portfolio:   python3 scripts/portfolio_summary.py"
echo "  Add alert:        python3 scripts/add_alert.py SPY 160.00 price_up"
echo ""
echo "For more commands, see SKILL.md"


path: scripts/list_alerts.py
content: #!/usr/bin/env python3
"""
List all price alerts.
Usage: python3 list_alerts.py
"""
from utils import load_alerts, get_price


def list_alerts():
    """Display all price alerts."""
    alerts = load_alerts()

    if not alerts:
        print("No alerts found. Add alerts using add_alert.py")
        return

    # Separate active and triggered alerts
    active_alerts = [a for a in alerts if not a.get('triggered', False)]
    triggered_alerts = [a for a in alerts if a.get('triggered', False)]

    print("\n" + "=" * 80)

    if active_alerts:
        print("ACTIVE ALERTS:")
        print("-" * 80)
        for alert in active_alerts:
            symbol = alert['symbol'].upper()
            condition_text = ">" if alert['condition'] == 'price_up' else "<"
            target = f"${alert['target_price']:.2f}"
            created = alert['created_at'][:10]

            # Get current price
            current = get_price(symbol)
            current_str = f"${current:.2f}" if current else "N/A"

            print(f"  {symbol} {condition_text} {target} | Current: {current_str} | Created: {created}")

    if triggered_alerts:
        print("\nTRIGGERED ALERTS:")
        print("-" * 80)
        for alert in triggered_alerts:
            symbol = alert['symbol'].upper()
            condition_text = ">" if alert['condition'] == 'price_up' else "<"
            target = f"${alert['target_price']:.2f}"
            triggered = alert.get('triggered_at', 'N/A')[:10]

            print(f"  {symbol} {condition_text} {target} | Triggered: {triggered}")

    print("=" * 80)
    print(f"Active alerts: {len(active_alerts)}")
    print(f"Triggered alerts: {len(triggered_alerts)}")


if __name__ == "__main__":
    list_alerts()


path: scripts/list_positions.py
content: #!/usr/bin/env python3
"""
List all positions in the portfolio.
Usage: python3 list_positions.py
"""
from utils import load_positions, format_currency


def list_positions():
    """Display all positions in the portfolio."""
    positions = load_positions()

    if not positions:
        print("No positions found. Add positions using add_position.py")
        return

    print("\n" + "=" * 80)
    print(f"{'Symbol':<10} {'Name':<25} {'Buy Price':<12} {'Quantity':<10} {'Cost':<15}")
    print("=" * 80)

    total_cost = 0

    for pos in positions:
        cost = pos['buy_price'] * pos['quantity']
        total_cost += cost

        symbol = pos['symbol'].upper()
        name = pos['name'][:24] if len(pos['name']) > 24 else pos['name']
        buy_price = f"${pos['buy_price']:,.2f}"
        quantity = f"{pos['quantity']:.2f}"
        cost_str = f"${cost:,.2f}"

        print(f"{symbol:<10} {name:<25} {buy_price:<12} {quantity:<10} {cost_str:<15}")

    print("=" * 80)
    print(f"{'':<47} {'Total:':<10} ${total_cost:,.2f}")
    print("=" * 80)
    print(f"\nTotal positions: {len(positions)}")


if __name__ == "__main__":
    list_positions()


path: scripts/portfolio_summary.py
content: #!/usr/bin/env python3
"""
Show portfolio summary with profit/loss calculation.
Usage: python3 portfolio_summary.py [--update]
"""
import sys
from datetime import datetime
from utils import (
    load_positions, load_alerts, get_price, format_currency,
    format_percentage, calculate_pnl, save_positions
)


def portfolio_summary(update_prices=False):
    """Display portfolio summary with profit/loss."""
    positions = load_positions()

    if not positions:
        print("No positions found. Add positions using add_position.py")
        return

    print("\n" + "=" * 100)
    print(f"{'Symbol':<10} {'Name':<25} {'Buy Price':<12} {'Current':<12} {'Quantity':<10}")
    print(f"{'Cost':<15} {'Value':<15} {'P/L ($)':<12} {'P/L (%)':<12}")
    print("=" * 100)

    total_cost = 0
    total_value = 0
    total_pnl = 0

    for pos in positions:
        symbol = pos['symbol'].upper()
        name = pos['name'][:24] if len(pos['name']) > 24 else pos['name']

        # Get current price
        if update_prices:
            current_price = get_price(symbol)
            if current_price:
                pos['current_price'] = current_price
                pos['last_updated'] = datetime.now().isoformat() + 'Z'
        else:
            current_price = pos.get('current_price', pos.get('buy_price'))

        # Calculate P/L
        pnl = calculate_pnl(pos, current_price)

        cost = pnl['cost']
        value = pnl['value']
        pnl_amount = pnl['pnl']
        pnl_pct = pnl['pnl_pct']

        # Update totals
        total_cost += cost
        total_value += value
        total_pnl += pnl_amount

        # Format output
        buy_price = f"${pos['buy_price']:,.2f}"
        current_str = f"${current_price:,.2f}" if current_price else "N/A"
        quantity = f"{pos['quantity']:.2f}"
        cost_str = f"${cost:,.2f}"
        value_str = f"${value:,.2f}" if value else "N/A"
        pnl_str = format_currency(pnl_amount)
        pnl_pct_str = format_percentage(pnl_pct)

        # Color P/L (positive=green-ish, negative=red-ish)
        if pnl_amount and pnl_amount > 0:
            pnl_pct_str = f"+{pnl_pct:.2f}%"
        elif pnl_amount and pnl_amount < 0:
            pnl_pct_str = f"{pnl_pct:.2f}%"

        print(f"{symbol:<10} {name:<25} {buy_price:<12} {current_str:<12} {quantity:<10}")
        print(f"{cost_str:<15} {value_str:<15} {pnl_str:<12} {pnl_pct_str:<12}")
        print("-" * 100)

    # Save updated positions if requested
    if update_prices:
        save_positions(positions)

    # Show totals
    print("=" * 100)
    print(f"{'':<67} {'Total:':<15} ${total_cost:,.2f}")
    print(f"{'':<67} {'Value:':<15} ${total_value:,.2f}")
    print(f"{'':<67} {'P/L ($):':<15} {format_currency(total_pnl)}")
    total_pct = (total_pnl / total_cost * 100) if total_cost > 0 else 0
    total_pct_str = f"+{total_pct:.2f}%" if total_pct >= 0 else f"{total_pct:.2f}%"
    print(f"{'':<67} {'P/L (%):':<15} {total_pct_str}")
    print("=" * 100)
    print(f"\nTotal positions: {len(positions)}")

    # Show active alerts
    alerts = load_alerts()
    active_alerts = [a for a in alerts if not a.get('triggered', False)]
    if active_alerts:
        print(f"\nActive alerts: {len(active_alerts)}")
        for alert in active_alerts:
            print(f"  - {alert['symbol']}: ${alert['target_price']:.2f} ({alert['condition']})")


if __name__ == "__main__":
    update = '--update' in sys.argv or '-u' in sys.argv
    portfolio_summary(update_prices=update)


path: scripts/remove_alert.py
content: #!/usr/bin/env python3
"""
Remove alerts for a symbol.
Usage: python3 remove_alert.py <symbol>
"""
import sys
from utils import load_alerts, save_alerts, find_alert


def remove_alert(symbol):
    """Remove alerts for a symbol."""
    alerts = load_alerts()

    # Find alerts for this symbol
    symbol_alerts = find_alert(symbol, alerts)

    if not symbol_alerts:
        print(f"No alerts found for {symbol}")
        return False

    # Display alerts
    print(f"\nFound {len(symbol_alerts)} alert(s) for {symbol}:")
    for i, alert in enumerate(symbol_alerts, 1):
        condition_text = "above" if alert['condition'] == 'price_up' else "below"
        status = "Triggered" if alert.get('triggered', False) else "Active"
        print(f"  {i}. {condition_text} ${alert['target_price']:.2f} - {status}")

    # Confirm removal
    confirm = input("\nRemove all alerts for this symbol? (y/n): ").strip().lower()
    if confirm != 'y':
        print("Cancelled.")
        return False

    # Remove alerts
    alerts = [a for a in alerts if a['symbol'].upper() != symbol.upper()]
    save_alerts(alerts)

    print(f"Removed {len(symbol_alerts)} alert(s) for {symbol}")
    return True


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 remove_alert.py <symbol>")
        print("\nExample:")
        print("  python3 remove_alert.py SPY")
        sys.exit(1)

    symbol = sys.argv[1]
    remove_alert(symbol)


path: scripts/remove_position.py
content: #!/usr/bin/env python3
"""
Remove position from portfolio.
Usage: python3 remove_position.py <symbol>
"""
import sys
from utils import load_positions, save_positions, find_position


def remove_position(symbol):
    """Remove a position from the portfolio."""
    positions = load_positions()

    # Find the position
    existing = find_position(symbol, positions)
    if not existing:
        print(f"No position found for {symbol}")
        return False

    # Confirm removal
    print(f"Position found: {existing['symbol']} - {existing['name']}")
    print(f"  Buy Price: ${existing['buy_price']:.2f}")
    print(f"  Quantity: {existing['quantity']}")
    print(f"  Cost: ${existing['buy_price'] * existing['quantity']:,.2f}")

    confirm = input("\nRemove this position? (y/n): ").strip().lower()
    if confirm != 'y':
        print("Cancelled.")
        return False

    # Remove the position
    positions = [p for p in positions if p['symbol'].upper() != symbol.upper()]
    save_positions(positions)

    print(f"Removed position: {symbol}")
    return True


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 remove_position.py <symbol>")
        print("\nExample:")
        print("  python3 remove_position.py SPY")
        sys.exit(1)

    symbol = sys.argv[1]
    remove_position(symbol)


path: scripts/uninstall.sh
content: #!/bin/bash
# Uninstall etf-investor skill

set -e

echo "Uninstalling etf-investor skill..."

DATA_DIR="$HOME/.clawdbot/etf_investor"

# Ask for confirmation
read -p "This will delete all your positions and alerts. Are you sure? (y/n): " confirm
if [ "$confirm" != "y" ]; then
    echo "Uninstall cancelled."
    exit 0
fi

# Remove data directory
if [ -d "$DATA_DIR" ]; then
    rm -rf "$DATA_DIR"
    echo "Removed data directory: $DATA_DIR"
else
    echo "Data directory not found: $DATA_DIR"
fi

echo "ETF Investor skill uninstalled successfully."


path: scripts/update_prices.py
content: #!/usr/bin/env python3
"""
Update current prices for all positions.
Usage: python3 update_prices.py
"""
from datetime import datetime
from utils import load_positions, save_positions, get_price


def update_prices():
    """Update current prices for all positions."""
    positions = load_positions()

    if not positions:
        print("No positions to update.")
        return

    print(f"\nUpdating prices for {len(positions)} position(s)...")
    print("-" * 80)

    updated_count = 0
    failed_count = 0

    for pos in positions:
        symbol = pos['symbol'].upper()

        # Get current price
        current_price = get_price(symbol)

        if current_price is None:
            print(f"  {symbol}: Failed to fetch price")
            failed_count += 1
            continue

        old_price = pos.get('current_price', 'N/A')
        pos['current_price'] = current_price
        pos['last_updated'] = datetime.now().isoformat() + 'Z'

        # Calculate change if we have old price
        if old_price != 'N/A':
            change = current_price - old_price
            change_pct = (change / old_price) * 100 if old_price > 0 else 0
            change_sign = "+" if change >= 0 else ""
            print(f"  {symbol}: ${current_price:.2f} (was ${old_price:.2f}, {change_sign}{change:.2f}, {change_sign}{change_pct:.2f}%)")
        else:
            print(f"  {symbol}: ${current_price:.2f}")

        updated_count += 1

    # Save updated positions
    save_positions(positions)

    print("-" * 80)
    print(f"\nUpdated: {updated_count} position(s)")
    if failed_count > 0:
        print(f"Failed: {failed_count} position(s)")


if __name__ == "__main__":
    update_prices()


path: scripts/utils.py
content: #!/usr/bin/env python3
"""
Utility functions for etf-investor skill.
"""
import json
import os
from datetime import datetime
from config import POSITIONS_FILE, ALERTS_FILE, PRICES_FILE


def load_positions():
    """Load positions from JSON file."""
    if not os.path.exists(POSITIONS_FILE):
        return []
    try:
        with open(POSITIONS_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return []


def save_positions(positions):
    """Save positions to JSON file."""
    with open(POSITIONS_FILE, 'w', encoding='utf-8') as f:
        json.dump(positions, f, indent=2, ensure_ascii=False)


def load_alerts():
    """Load alerts from JSON file."""
    if not os.path.exists(ALERTS_FILE):
        return []
    try:
        with open(ALERTS_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return []


def save_alerts(alerts):
    """Save alerts to JSON file."""
    with open(ALERTS_FILE, 'w', encoding='utf-8') as f:
        json.dump(alerts, f, indent=2, ensure_ascii=False)


def get_price(symbol):
    """Get current price for a symbol using 腾讯财经 (Tencent Finance) for Chinese stocks, yfinance for US stocks."""
    # 判断是中国股票还是美股
    # 中国A股: 6位数字代码 (如 512400, 513050)
    # 美股: 字母代码 (如 SPY, QQQ)
    
    if symbol.isdigit() and len(symbol) == 6:
        # 中国A股 - 使用腾讯财经
        return get_price_tencent(symbol)
    else:
        # 美股 - 使用 yfinance
        return get_price_yahoo(symbol)


def get_price_tencent(symbol):
    """Get current price for Chinese A-shares using 腾讯财经 API."""
    import urllib.request
    import json
    
    # 添加市场前缀: 上海 sh, 深圳 sz
    if symbol.startswith(('6', '5')):
        market = 'sh' + symbol
    else:
        market = 'sz' + symbol
    
    url = f'https://qt.gtimg.cn/q={market}'
    
    try:
        req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
        with urllib.request.urlopen(req, timeout=10) as response:
            data = response.read().decode('gbk', errors='ignore')
            
            # 解析返回数据格式: v_sh512400="1~512400~512400~2.264~..."
            if '~' in data:
                parts = data.split('~')
                if len(parts) > 3:
                    price = float(parts[3])
                    return price
            return None
    except Exception as e:
        print(f"Error getting price from 腾讯财经 for {symbol}: {e}")
        return None


def get_price_yahoo(symbol):
    """Get current price for US stocks using yfinance."""
    try:
        import yfinance as yf
        ticker = yf.Ticker(symbol)
        info = ticker.info
        if info and 'regularMarketPrice' in info:
            return info['regularMarketPrice']
        elif info and 'currentPrice' in info:
            return info['currentPrice']
        elif info and 'previousClose' in info:
            return info['previousClose']
        else:
            # Try fast_info as fallback
            fast_info = ticker.fast_info
            if fast_info and 'last_price' in fast_info:
                return fast_info['last_price']
            return None
    except Exception as e:
        print(f"Error getting price from Yahoo for {symbol}: {e}")
        return None


def format_currency(value):
    """Format value as currency string."""
    if value is None:
        return "N/A"
    return f"${value:,.2f}"


def format_percentage(value):
    """Format value as percentage string."""
    if value is None:
        return "N/A"
    sign = "+" if value >= 0 else ""
    return f"{sign}{value:.2f}%"


def calculate_pnl(position, current_price):
    """Calculate profit/loss for a position."""
    if not position or current_price is None:
        return {
            'cost': None,
            'value': None,
            'pnl': None,
            'pnl_pct': None
        }

    cost = position['buy_price'] * position['quantity']
    value = current_price * position['quantity']
    pnl = value - cost
    pnl_pct = ((current_price - position['buy_price']) / position['buy_price']) * 100

    return {
        'cost': cost,
        'value': value,
        'pnl': pnl,
        'pnl_pct': pnl_pct
    }


def find_position(symbol, positions):
    """Find a position by symbol."""
    for pos in positions:
        if pos['symbol'].upper() == symbol.upper():
            return pos
    return None


def find_alert(symbol, alerts):
    """Find alerts by symbol."""
    found = []
    for alert in alerts:
        if alert['symbol'].upper() == symbol.upper():
            found.append(alert)
    return found


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