Skill: salihgun/finclaw (version 1.0.0)

--- name: finclaw description: AI finance assistant — real-time quotes, charts, technical analysis, portfolio tracking, price alerts, watchlists, daily briefings, macro economics, and sentiment analysis for US stocks, BIST, crypto, and forex. metadata: { "openclaw": { "emoji": "📈", "requires": { "bins": ["python3"] }, "install": [{ "id": "setup", "kind": "uv", "package": "yfinance", "bins": ["python3"], "label": "Python 3 required" }] } } --- # FinClaw — AI Finance Assistant Your personal finance assistant covering **US stocks**, **BIST (Turkish market)**, **crypto**, and **forex**. Includes portfolio tracking, price alerts, charts, technical analysis, daily briefings, and more. ## First-Time Setup Run once after installation to create the Python venv and database: ```bash python3 {baseDir}/scripts/setup.py ``` Then add to `openclaw.json` under `skills.entries`: ```json "finclaw": { "env": { "FINNHUB_API_KEY": "", "FRED_API_KEY": "", "ALPHA_VANTAGE_API_KEY": "", "EXCHANGE_RATE_API_KEY": "" } } ``` API keys are optional — core features (prices, charts, TA, portfolio, alerts) work without any keys. ## Running Scripts All scripts use the skill's Python venv: ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/<script>.py [args] ``` --- ## Market Data ### quote.py — Real-Time Quotes Auto-detects asset type from symbol. Results cached for 60 seconds. ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py AAPL # US stock {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py THYAO.IS # BIST stock {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py BTC # Crypto {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py USD/TRY # Forex {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py AAPL MSFT BTC # Multiple {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py AAPL --force # Skip cache {baseDir}/venv/bin/python3 {baseDir}/scripts/quote.py AAPL --json # JSON output ``` **Symbol detection:** `.IS` → BIST | `BTC/ETH/SOL...` → Crypto | `USD/TRY` → Forex | else → US stock ### crypto.py — Crypto Market Data ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/crypto.py price BTC # Binance price {baseDir}/venv/bin/python3 {baseDir}/scripts/crypto.py top --limit 10 # Top gainers {baseDir}/venv/bin/python3 {baseDir}/scripts/crypto.py try BTC # Price in TRY ``` ### forex.py — Exchange Rates ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/forex.py rate USD TRY {baseDir}/venv/bin/python3 {baseDir}/scripts/forex.py convert USD TRY --amount 1000 {baseDir}/venv/bin/python3 {baseDir}/scripts/forex.py multi USD --targets TRY EUR GBP ``` ### chart.py — Price Charts Generates PNG charts. Send the saved file to the user. ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/chart.py AAPL # Candlestick {baseDir}/venv/bin/python3 {baseDir}/scripts/chart.py BTC --type line --period 1y # Line chart {baseDir}/venv/bin/python3 {baseDir}/scripts/chart.py AAPL --sma 20 50 200 # With SMAs ``` Periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max ### technical.py — Technical Analysis SMA, EMA, RSI, MACD, Bollinger Bands with buy/sell signals. ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/technical.py AAPL {baseDir}/venv/bin/python3 {baseDir}/scripts/technical.py BTC --period 1y --json ``` ### news.py — Financial News (requires FINNHUB_API_KEY) ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/news.py company --symbol AAPL {baseDir}/venv/bin/python3 {baseDir}/scripts/news.py market --category crypto ``` ### screener.py — Stock Screener ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/screener.py us # US gainers {baseDir}/venv/bin/python3 {baseDir}/scripts/screener.py bist --direction bottom # BIST losers {baseDir}/venv/bin/python3 {baseDir}/scripts/screener.py crypto --limit 15 # Crypto gainers ``` --- ## Portfolio & Alerts ### portfolio.py — Position Management ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/portfolio.py add --symbol AAPL --shares 10 --price 150 {baseDir}/venv/bin/python3 {baseDir}/scripts/portfolio.py sell --symbol AAPL --shares 5 --price 175 {baseDir}/venv/bin/python3 {baseDir}/scripts/portfolio.py remove --symbol AAPL {baseDir}/venv/bin/python3 {baseDir}/scripts/portfolio.py list {baseDir}/venv/bin/python3 {baseDir}/scripts/portfolio.py summary ``` Optional: `--fees 1.50`, `--date 2024-01-15`, `--name "Apple Inc"`, `--notes "Long hold"` ### alerts.py — Price Alerts ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/alerts.py create --symbol AAPL --condition above --target 200 {baseDir}/venv/bin/python3 {baseDir}/scripts/alerts.py create --symbol BTC --condition below --target 60000 --note "Buy signal" {baseDir}/venv/bin/python3 {baseDir}/scripts/alerts.py list {baseDir}/venv/bin/python3 {baseDir}/scripts/alerts.py delete --id 3 {baseDir}/venv/bin/python3 {baseDir}/scripts/alerts.py snooze --id 3 --hours 48 ``` Conditions: `above`, `below`, `change_pct`, `volume_above` ### check_alerts.py — Alert Checker (for cron) ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/check_alerts.py ``` ### pnl.py — Profit & Loss ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/pnl.py # All positions {baseDir}/venv/bin/python3 {baseDir}/scripts/pnl.py --symbol AAPL # Single symbol ``` ### watchlist.py — Watchlists ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/watchlist.py create --name "Tech" {baseDir}/venv/bin/python3 {baseDir}/scripts/watchlist.py add --name "Tech" --symbol AAPL {baseDir}/venv/bin/python3 {baseDir}/scripts/watchlist.py show --name "Tech" --prices {baseDir}/venv/bin/python3 {baseDir}/scripts/watchlist.py list ``` --- ## Intelligence ### briefing.py — Market Briefings ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/briefing.py morning # Full morning briefing {baseDir}/venv/bin/python3 {baseDir}/scripts/briefing.py close # End-of-day summary {baseDir}/venv/bin/python3 {baseDir}/scripts/briefing.py weekend # Weekend crypto + forex recap ``` ### macro.py — Macro Economics (requires FRED_API_KEY) ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/macro.py dashboard {baseDir}/venv/bin/python3 {baseDir}/scripts/macro.py indicator --name fed_rate {baseDir}/venv/bin/python3 {baseDir}/scripts/macro.py list ``` ### earnings.py — Earnings Calendar (requires FINNHUB_API_KEY) ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/earnings.py calendar {baseDir}/venv/bin/python3 {baseDir}/scripts/earnings.py symbol --symbol AAPL ``` ### sentiment.py — News Sentiment (requires ALPHA_VANTAGE_API_KEY) ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/sentiment.py --symbol AAPL {baseDir}/venv/bin/python3 {baseDir}/scripts/sentiment.py --topics technology ``` ### research.py — Deep Research ```bash {baseDir}/venv/bin/python3 {baseDir}/scripts/research.py AAPL ``` --- ## Data Sources - **US Stocks**: Finnhub (primary), yfinance (fallback) — no key needed - **BIST**: yfinance with .IS suffix — no key needed - **Crypto**: Binance API — no key needed - **Forex**: ExchangeRate-API — works without key - **Charts/TA**: matplotlib + mplfinance + pandas — local computation - **News**: Finnhub — needs FINNHUB_API_KEY - **Macro**: FRED — needs FRED_API_KEY - **Sentiment**: Alpha Vantage — needs ALPHA_VANTAGE_API_KEY

path: lib/__init__.py
content: # FinClaw — AI Finance Assistant for OpenClaw


path: lib/config.py
content: """Read API keys and skill config from openclaw.json."""

import json
import os

_CONFIG_PATH = os.path.expanduser("~/.openclaw/openclaw.json")
_SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_config_cache = None

_KEY_MAP = {
    "finnhubApiKey": "FINNHUB_API_KEY",
    "alphaVantageApiKey": "ALPHA_VANTAGE_API_KEY",
    "fredApiKey": "FRED_API_KEY",
    "fmpApiKey": "FMP_API_KEY",
    "exchangeRateApiKey": "EXCHANGE_RATE_API_KEY",
}


def _load_config():
    global _config_cache
    if _config_cache is None:
        with open(_CONFIG_PATH) as f:
            _config_cache = json.load(f)
    return _config_cache


def get_skill_config() -> dict:
    """Get config for finclaw from skills.entries.finclaw."""
    cfg = _load_config()
    return cfg.get("skills", {}).get("entries", {}).get("finclaw", {})


def get_api_key(key_name: str) -> str:
    """Get an API key from skill config env. Returns empty string if not set."""
    env = get_skill_config().get("env", {})
    env_name = _KEY_MAP.get(key_name, key_name)
    return env.get(env_name, "") or env.get(key_name, "")


def get_db_path() -> str:
    """Get the database path (inside skill directory)."""
    return os.path.join(_SKILL_DIR, "data", "finance.db")


def get_skill_dir() -> str:
    """Get the skill base directory."""
    return _SKILL_DIR


path: lib/db.py
content: """SQLite connection helper for FinClaw."""

import sqlite3
import os
from lib.config import get_db_path


def get_connection() -> sqlite3.Connection:
    """Get a SQLite connection with WAL mode and foreign keys enabled."""
    db_path = get_db_path()
    os.makedirs(os.path.dirname(db_path), exist_ok=True)
    conn = sqlite3.connect(db_path)
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA foreign_keys=ON")
    conn.row_factory = sqlite3.Row
    return conn


def execute(sql: str, params=None):
    """Execute a single SQL statement and return results."""
    conn = get_connection()
    try:
        cur = conn.execute(sql, params or ())
        conn.commit()
        return cur.fetchall()
    finally:
        conn.close()


path: lib/formatters.py
content: """Price and number formatting utilities for FinClaw."""


def fmt_price(value: float, currency: str = "USD") -> str:
    """Format a price with currency symbol."""
    symbols = {"USD": "$", "TRY": "₺", "EUR": "€", "GBP": "£"}
    sym = symbols.get(currency, currency + " ")
    if value == 0:
        return f"{sym}0.00"
    if abs(value) >= 1:
        return f"{sym}{value:,.2f}"
    return f"{sym}{value:.6f}"


def fmt_change(change: float, change_pct: float) -> str:
    arrow = "▲" if change >= 0 else "▼"
    sign = "+" if change >= 0 else ""
    return f"{arrow} {sign}{change:.2f} ({sign}{change_pct:.2f}%)"


def fmt_number(value: float) -> str:
    if abs(value) >= 1_000_000_000:
        return f"{value / 1_000_000_000:.2f}B"
    if abs(value) >= 1_000_000:
        return f"{value / 1_000_000:.2f}M"
    if abs(value) >= 1_000:
        return f"{value / 1_000:.2f}K"
    return f"{value:,.2f}"


def fmt_volume(volume: float) -> str:
    return f"Vol: {fmt_number(volume)}"


def detect_asset_type(symbol: str) -> str:
    """Detect asset type from symbol format."""
    s = symbol.upper().strip()
    forex_pairs = [
        "USD/TRY", "EUR/USD", "GBP/USD", "USD/JPY", "EUR/TRY",
        "USDTRY", "EURUSD", "GBPUSD", "USDJPY", "EURTRY",
    ]
    if s in forex_pairs or ("/" in s and len(s) <= 7):
        return "forex"
    if s.endswith(".IS"):
        return "stock_bist"
    crypto_symbols = {
        "BTC", "ETH", "BNB", "SOL", "XRP", "ADA", "DOGE", "DOT",
        "AVAX", "MATIC", "LINK", "UNI", "SHIB", "LTC", "ATOM",
    }
    if s in crypto_symbols or s.endswith("USDT") or s.endswith("BUSD"):
        return "crypto"
    return "stock_us"


def symbol_normalize(symbol: str) -> str:
    s = symbol.upper().strip()
    forex_map = {
        "USDTRY": "USD/TRY", "EURUSD": "EUR/USD", "GBPUSD": "GBP/USD",
        "USDJPY": "USD/JPY", "EURTRY": "EUR/TRY",
    }
    return forex_map.get(s, s)


path: lib/rate_limiter.py
content: """Per-API rate limiting for FinClaw."""

import time
import threading

_locks = {}
_call_times = {}
_global_lock = threading.Lock()

RATE_LIMITS = {
    "finnhub": 60, "alpha_vantage": 5, "fmp": 250,
    "fred": 120, "binance": 1200, "exchangerate": 60,
    "yfinance": 30, "coingecko": 10,
}


def wait_if_needed(api_name: str):
    limit = RATE_LIMITS.get(api_name, 60)
    window = 60.0
    with _global_lock:
        if api_name not in _locks:
            _locks[api_name] = threading.Lock()
            _call_times[api_name] = []
    with _locks[api_name]:
        now = time.time()
        _call_times[api_name] = [t for t in _call_times[api_name] if now - t < window]
        if len(_call_times[api_name]) >= limit:
            oldest = _call_times[api_name][0]
            sleep_time = window - (now - oldest) + 0.1
            if sleep_time > 0:
                time.sleep(sleep_time)
        _call_times[api_name].append(time.time())


path: references/api-endpoints.md
content: # API Endpoints Reference

## Finnhub (US Stocks)
- Quote: `GET https://finnhub.io/api/v1/quote?symbol={sym}&token={key}`
- Company News: `GET https://finnhub.io/api/v1/company-news?symbol={sym}&from={date}&to={date}&token={key}`
- Company Profile: `GET https://finnhub.io/api/v1/stock/profile2?symbol={sym}&token={key}`
- Rate limit: 60 calls/minute

## Binance (Crypto)
- 24hr Ticker: `GET https://api.binance.com/api/v3/ticker/24hr?symbol={pair}`
- Price: `GET https://api.binance.com/api/v3/ticker/price?symbol={pair}`
- Klines: `GET https://api.binance.com/api/v3/klines?symbol={pair}&interval={int}&limit={n}`
- Rate limit: 1200 requests/minute

## CoinGecko (Crypto TRY)
- Simple Price: `GET https://api.coingecko.com/api/v3/simple/price?ids={id}&vs_currencies=try,usd`
- Rate limit: 10-30 calls/minute (free tier)

## ExchangeRate-API (Forex)
- Pair: `GET https://v6.exchangerate-api.com/v6/{key}/pair/{base}/{target}`
- Latest: `GET https://v6.exchangerate-api.com/v6/{key}/latest/{base}`
- Free alt: `GET https://open.er-api.com/v6/latest/{base}`

## yfinance (Fallback)
- Python library, no direct HTTP endpoints
- BIST stocks use `.IS` suffix (e.g., THYAO.IS)

## FRED (Macro)
- Series: `GET https://api.stlouisfed.org/fred/series/observations?series_id={id}&[REDACTED_SECRET]`
- Rate limit: 120 requests/minute

## Alpha Vantage (Sentiment)
- News Sentiment: `GET https://www.alphavantage.co/query?function=NEWS_SENTIMENT&tickers={sym}&apikey={key}`
- Rate limit: 5 calls/minute

## FMP (Earnings)
- Earnings Calendar: `GET https://financialmodelingprep.com/api/v3/earning_calendar?apikey={key}`
- Rate limit: 250 calls/day


path: references/briefing-template.md
content: # Briefing Templates

## Morning Briefing (08:30 Weekdays)
1. **Market Overview**: S&P 500, Nasdaq, BIST 100, BTC, ETH, USD/TRY, EUR/USD, Gold
2. **Portfolio Summary**: Each position with current price and daily change
3. **Active Alerts**: Count of active price alerts
4. **Key Events**: Earnings reports today, economic data releases

## Market Close (23:00 Weekdays)
1. **Closing Prices**: All major indices and tracked assets
2. **Daily P&L**: Portfolio performance for the day
3. **Top Movers**: Biggest gainers/losers in portfolio

## Weekend Recap (Saturday 10:00)
1. **Crypto 24/7**: BTC, ETH, SOL, BNB with 7-day performance
2. **Forex**: USD/TRY, EUR/USD weekend rates
3. **Weekly Performance**: Portfolio weekly returns
4. **Upcoming Week**: Key events, earnings, economic releases

## Formatting Guidelines
- Use bold for section headers
- Use emoji for sentiment (green/red circles)
- Keep each briefing under 2000 characters for Telegram readability
- Include percentage changes for all prices
- Round to 2 decimal places for stocks, 4 for forex


path: references/macro-indicators.md
content: # FRED Macro Indicators Reference

## Key Indicators (Pre-configured)

| Name | FRED ID | Description | Frequency |
|------|---------|-------------|-----------|
| gdp | GDP | US Gross Domestic Product | Quarterly |
| inflation | CPIAUCSL | Consumer Price Index | Monthly |
| unemployment | UNRATE | Unemployment Rate | Monthly |
| fed_rate | FEDFUNDS | Federal Funds Rate | Monthly |
| 10y_yield | DGS10 | 10-Year Treasury Yield | Daily |
| 2y_yield | DGS2 | 2-Year Treasury Yield | Daily |
| m2 | M2SL | M2 Money Supply | Monthly |
| housing | HOUST | Housing Starts | Monthly |
| retail | RSAFS | Retail Sales | Monthly |
| pce | PCE | Personal Consumption Expenditures | Monthly |
| ism | MANEMP | Manufacturing Employment | Monthly |
| vix | VIXCLS | VIX Volatility Index | Daily |

## Other Useful FRED Series

| FRED ID | Description |
|---------|-------------|
| T10Y2Y | 10Y-2Y Treasury Spread (yield curve) |
| DEXUSEU | USD/EUR Exchange Rate |
| DCOILWTICO | Crude Oil WTI |
| GOLDAMGBD228NLBM | Gold Price |
| BAMLH0A0HYM2 | High Yield Bond Spread |
| MORTGAGE30US | 30-Year Fixed Mortgage Rate |
| UMCSENT | Consumer Sentiment |
| PAYEMS | Total Nonfarm Payrolls |
| INDPRO | Industrial Production Index |

## Interpreting Key Metrics

### Yield Curve (T10Y2Y)
- **Positive**: Normal economy
- **Near zero/negative**: Recession signal (inverted yield curve)

### VIX
- **<15**: Low volatility, complacent market
- **15-25**: Normal
- **25-35**: Elevated fear
- **>35**: Extreme fear / market stress

### Fed Funds Rate
- **Rising**: Tightening policy (bearish for stocks)
- **Falling**: Easing policy (bullish for stocks)


path: references/portfolio-commands.md
content: # Portfolio Commands Quick Reference

## Natural Language → Script Mapping

| User Says | Action |
|-----------|--------|
| "add 10 AAPL at 150" | portfolio.py add --symbol AAPL --shares 10 --price 150 |
| "buy 0.5 BTC at 65000" | portfolio.py add --symbol BTC --shares 0.5 --price 65000 |
| "sell 5 AAPL at 175" | portfolio.py sell --symbol AAPL --shares 5 --price 175 |
| "remove AAPL from portfolio" | portfolio.py remove --symbol AAPL |
| "show my portfolio" | portfolio.py list |
| "portfolio summary" | portfolio.py summary |
| "alert me when BTC hits 70000" | alerts.py create --symbol BTC --condition above --target 70000 |
| "alert if AAPL drops below 140" | alerts.py create --symbol AAPL --condition below --target 140 |
| "show my alerts" | alerts.py list |
| "delete alert 3" | alerts.py delete --id 3 |

## Asset Type Detection

The system auto-detects asset type from the symbol:
- **US Stock**: `AAPL`, `MSFT`, `GOOGL` (default)
- **BIST**: `THYAO.IS`, `GARAN.IS` (`.IS` suffix)
- **Crypto**: `BTC`, `ETH`, `SOL` (known crypto symbols)
- **Forex**: `USD/TRY`, `EUR/USD` (slash-separated pair)

## Currency Mapping
- US stocks → USD
- BIST stocks → TRY
- Crypto → USDT
- Forex → target currency


path: references/technical-indicators.md
content: # Technical Indicators Reference

## Available Indicators

### Moving Averages
- **SMA(20)**: Short-term trend
- **SMA(50)**: Medium-term trend
- **SMA(200)**: Long-term trend
- **EMA(12)**: Fast exponential average
- **EMA(26)**: Slow exponential average

### Oscillators
- **RSI(14)**: Relative Strength Index (0-100)
  - >70: Overbought (potential sell signal)
  - <30: Oversold (potential buy signal)
  - 30-70: Neutral zone

### MACD (Moving Average Convergence Divergence)
- **MACD Line**: EMA(12) - EMA(26)
- **Signal Line**: EMA(9) of MACD line
- **Histogram**: MACD - Signal (momentum strength)
- Bullish: MACD crosses above signal
- Bearish: MACD crosses below signal

### Bollinger Bands (20, 2)
- **Upper Band**: SMA(20) + 2 * StdDev
- **Middle Band**: SMA(20)
- **Lower Band**: SMA(20) - 2 * StdDev
- Price at lower band: Potential bounce/buy
- Price at upper band: Potential pullback/sell

## Signal Interpretation

### Bullish Signals
- Price above SMA(50)
- Golden Cross: SMA(50) crosses above SMA(200)
- RSI rising from below 30
- MACD bullish crossover
- Price bouncing off lower Bollinger Band

### Bearish Signals
- Price below SMA(50)
- Death Cross: SMA(50) crosses below SMA(200)
- RSI declining from above 70
- MACD bearish crossover
- Price rejected at upper Bollinger Band


path: references/ticker-symbols.md
content: # Ticker Symbols Reference

## BIST (Borsa Istanbul)
All BIST tickers use `.IS` suffix in yfinance.

### Major BIST Stocks
| Symbol | Company |
|--------|---------|
| THYAO.IS | Turkish Airlines |
| GARAN.IS | Garanti BBVA |
| AKBNK.IS | Akbank |
| ISCTR.IS | Is Bankasi |
| YKBNK.IS | Yapi Kredi |
| KCHOL.IS | Koc Holding |
| SAHOL.IS | Sabanci Holding |
| TUPRS.IS | Tupras |
| EREGL.IS | Eregli Demir Celik |
| BIMAS.IS | BIM |
| ASELS.IS | Aselsan |
| SISE.IS | Sisecam |
| TCELL.IS | Turkcell |
| TOASO.IS | Tofas Oto |
| PGSUS.IS | Pegasus Airlines |

### BIST Index
| Symbol | Index |
|--------|-------|
| XU100.IS | BIST 100 |
| XU030.IS | BIST 30 |

## US Stocks (Common)
Standard NYSE/NASDAQ tickers — no suffix needed.

## Crypto Pairs (Binance)
Append `USDT` for Binance API calls.
| Input | Binance Pair |
|-------|-------------|
| BTC | BTCUSDT |
| ETH | ETHUSDT |
| SOL | SOLUSDT |
| BNB | BNBUSDT |
| XRP | XRPUSDT |
| ADA | ADAUSDT |
| DOGE | DOGEUSDT |
| AVAX | AVAXUSDT |

## Forex Pairs
Use slash format (BASE/TARGET).
| Pair | Description |
|------|-------------|
| USD/TRY | US Dollar / Turkish Lira |
| EUR/USD | Euro / US Dollar |
| EUR/TRY | Euro / Turkish Lira |
| GBP/USD | British Pound / US Dollar |
| USD/JPY | US Dollar / Japanese Yen |


path: requirements.txt
content: yfinance
pandas
matplotlib
mplfinance
finnhub-python
requests
fredapi


path: scripts/alerts.py
content: #!/usr/bin/env python3
"""Price alerts: create, delete, list, snooze."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection
from lib.formatters import detect_asset_type


def create_alert(symbol, condition, target_value, current_price=None, note=None):
    symbol = symbol.upper()
    asset_type = detect_asset_type(symbol)
    conn = get_connection()
    try:
        aid = conn.execute("INSERT INTO alerts (symbol, asset_type, condition, target_value, current_value_at_creation, note) VALUES (?,?,?,?,?,?)",
                           (symbol, asset_type, condition, target_value, current_price, note)).lastrowid
        conn.commit()
        cond = {"above": f"goes above {target_value}", "below": f"drops below {target_value}",
                "change_pct": f"changes by {target_value}%", "volume_above": f"volume exceeds {target_value}"}
        return f"Alert #{aid} created: {symbol} {cond.get(condition, condition)}\nStatus: active"
    finally: conn.close()


def delete_alert(aid):
    conn = get_connection()
    try:
        row = conn.execute("SELECT symbol FROM alerts WHERE id=?", (aid,)).fetchone()
        if not row: return f"Error: Alert #{aid} not found"
        conn.execute("UPDATE alerts SET status='deleted' WHERE id=?", (aid,))
        conn.commit()
        return f"Alert #{aid} ({row['symbol']}) deleted."
    finally: conn.close()


def snooze_alert(aid, hours=24):
    conn = get_connection()
    try:
        conn.execute("UPDATE alerts SET status='snoozed', snoozed_until=datetime('now', ? || ' hours') WHERE id=?", (str(hours), aid))
        conn.commit()
        return f"Alert #{aid} snoozed for {hours} hours."
    finally: conn.close()


def list_alerts(status=None):
    conn = get_connection()
    try:
        if status:
            alerts = conn.execute("SELECT * FROM alerts WHERE status=? ORDER BY created_at DESC", (status,)).fetchall()
        else:
            alerts = conn.execute("SELECT * FROM alerts WHERE status != 'deleted' ORDER BY status, created_at DESC").fetchall()
        if not alerts: return "No alerts found."
        lines = ["**Price Alerts**\n"]
        for a in alerts:
            icon = {"active": "🟢", "triggered": "🔔", "snoozed": "💤"}.get(a["status"], "⚪")
            cond = {"above": f"> {a['target_value']}", "below": f"< {a['target_value']}",
                    "change_pct": f"±{a['target_value']}%", "volume_above": f"vol > {a['target_value']}"}.get(a["condition"], a["condition"])
            line = f"  {icon} #{a['id']} {a['symbol']} {cond} [{a['status']}]"
            if a["note"]: line += f" — {a['note']}"
            lines.append(line)
        return "\n".join(lines)
    finally: conn.close()


def main():
    parser = argparse.ArgumentParser(description="Price alerts")
    parser.add_argument("action", choices=["create", "delete", "list", "snooze"])
    parser.add_argument("--symbol", "-s"); parser.add_argument("--condition", "-c", choices=["above", "below", "change_pct", "volume_above"])
    parser.add_argument("--target", "-t", type=float); parser.add_argument("--current-price", type=float)
    parser.add_argument("--note"); parser.add_argument("--id", type=int)
    parser.add_argument("--status"); parser.add_argument("--hours", type=int, default=24)
    args = parser.parse_args()

    if args.action == "create":
        if not all([args.symbol, args.condition, args.target]): print("Error: --symbol, --condition, --target required"); sys.exit(1)
        print(create_alert(args.symbol, args.condition, args.target, args.current_price, args.note))
    elif args.action == "delete":
        if not args.id: print("Error: --id required"); sys.exit(1)
        print(delete_alert(args.id))
    elif args.action == "snooze":
        if not args.id: print("Error: --id required"); sys.exit(1)
        print(snooze_alert(args.id, args.hours))
    elif args.action == "list": print(list_alerts(args.status))


if __name__ == "__main__":
    main()


path: scripts/briefing.py
content: #!/usr/bin/env python3
"""Generate daily market briefings."""

import sys, os, hashlib, argparse
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection
from lib.formatters import fmt_price
from scripts.quote import get_quote


def market_snapshot():
    symbols = {"S&P 500": "SPY", "Nasdaq": "QQQ", "BIST 100": "XU100.IS",
               "BTC": "BTC", "ETH": "ETH", "USD/TRY": "USD/TRY", "EUR/USD": "EUR/USD", "Gold": "GLD"}
    snap = {}
    for label, sym in symbols.items():
        try:
            q = get_quote(sym, force=True)
            snap[label] = {"price": q["price"], "change_pct": q.get("change_pct"), "currency": q.get("currency", "USD")}
        except: snap[label] = {"error": True}
    return snap


def portfolio_snapshot():
    conn = get_connection()
    try:
        positions = conn.execute("SELECT * FROM positions").fetchall()
        result = []
        for p in positions:
            b = conn.execute("SELECT COALESCE(SUM(shares),0) as s FROM transactions WHERE position_id=? AND action='buy'", (p["id"],)).fetchone()["s"]
            s = conn.execute("SELECT COALESCE(SUM(shares),0) as s FROM transactions WHERE position_id=? AND action='sell'", (p["id"],)).fetchone()["s"]
            if b - s > 0: result.append({"symbol": p["symbol"], "shares": b - s})
        return result
    finally: conn.close()


def gen_morning():
    now = datetime.now()
    lines = [f"**Good morning! Market Briefing — {now.strftime('%A, %B %d, %Y')}**\n", "**Market Overview**"]
    for label, d in market_snapshot().items():
        if d.get("error"): lines.append(f"  {label}: _unavailable_"); continue
        c = d.get("currency", "USD")
        chg = f" {'▲' if d['change_pct'] >= 0 else '▼'} {'+' if d['change_pct'] >= 0 else ''}{d['change_pct']:.2f}%" if d.get("change_pct") is not None else ""
        lines.append(f"  {label}: {fmt_price(d['price'], c)}{chg}")
    portfolio = portfolio_snapshot()
    if portfolio:
        lines.append(f"\n**Your Portfolio** ({len(portfolio)} positions)")
        for p in portfolio:
            try:
                q = get_quote(p["symbol"])
                chg = f" ({'+' if q.get('change_pct', 0) >= 0 else ''}{q.get('change_pct', 0):.2f}%)" if q.get("change_pct") is not None else ""
                lines.append(f"  {p['symbol']}: {fmt_price(q['price'], q.get('currency', 'USD'))}{chg}")
            except: lines.append(f"  {p['symbol']}: _unavailable_")
    conn = get_connection()
    ac = conn.execute("SELECT COUNT(*) as c FROM alerts WHERE status='active'").fetchone()["c"]
    conn.close()
    if ac: lines.append(f"\n**Active Alerts**: {ac}")
    return "\n".join(lines)


def gen_close():
    lines = [f"**Market Close — {datetime.now().strftime('%B %d, %Y')}**\n"]
    for label, d in market_snapshot().items():
        if d.get("error"): continue
        c = d.get("currency", "USD")
        chg = f" {'▲' if d['change_pct'] >= 0 else '▼'} {'+' if d['change_pct'] >= 0 else ''}{d['change_pct']:.2f}%" if d.get("change_pct") is not None else ""
        lines.append(f"  {label}: {fmt_price(d['price'], c)}{chg}")
    return "\n".join(lines)


def gen_weekend():
    lines = ["**Weekend Market Recap**\n", "**Crypto (24/7)**"]
    for sym in ["BTC", "ETH", "SOL", "BNB"]:
        try:
            q = get_quote(sym, force=True)
            s = "+" if (q.get("change_pct") or 0) >= 0 else ""
            lines.append(f"  {sym}: {fmt_price(q['price'], 'USD')} ({s}{q.get('change_pct', 0):.2f}%)")
        except: pass
    lines.append("\n**Forex**")
    for pair in ["USD/TRY", "EUR/USD"]:
        try:
            q = get_quote(pair, force=True)
            lines.append(f"  {pair}: {q['price']:.4f}")
        except: pass
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Market briefings")
    parser.add_argument("type", choices=["morning", "close", "weekend"])
    args = parser.parse_args()
    content = {"morning": gen_morning, "close": gen_close, "weekend": gen_weekend}[args.type]()
    print(content)
    conn = get_connection()
    conn.execute("INSERT INTO briefing_log (briefing_type, content_hash) VALUES (?,?)",
                 (args.type, hashlib.md5(content.encode()).hexdigest()))
    conn.commit(); conn.close()


if __name__ == "__main__":
    main()


path: scripts/chart.py
content: #!/usr/bin/env python3
"""Generate price charts using mplfinance."""

import sys, os, argparse
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.formatters import detect_asset_type
from lib.rate_limiter import wait_if_needed
from lib.config import get_skill_dir

import pandas as pd
import matplotlib
matplotlib.use("Agg")
import mplfinance as mpf

CHART_DIR = os.path.join(get_skill_dir(), "data", "charts")


def fetch_history(symbol, period="3mo", interval="1d"):
    import yfinance as yf
    asset_type = detect_asset_type(symbol)
    ticker_sym = symbol
    if asset_type == "stock_bist" and not symbol.endswith(".IS"): ticker_sym = symbol + ".IS"
    if asset_type == "crypto": ticker_sym = symbol + "-USD"
    wait_if_needed("yfinance")
    df = yf.Ticker(ticker_sym).history(period=period, interval=interval)
    if df.empty: raise ValueError(f"No data for {ticker_sym}")
    return df


def generate_candlestick(symbol, period="3mo", interval="1d", volume=True, sma=None):
    df = fetch_history(symbol, period, interval)
    os.makedirs(CHART_DIR, exist_ok=True)
    filename = f"{symbol.replace('/', '_')}_{period}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
    filepath = os.path.join(CHART_DIR, filename)
    mc = mpf.make_marketcolors(up="green", down="red", inherit=True)
    style = mpf.make_mpf_style(marketcolors=mc, gridstyle=":", y_on_right=True)
    add_plots = []
    if sma:
        for p in sma:
            if len(df) >= p:
                add_plots.append(mpf.make_addplot(df["Close"].rolling(window=p).mean(), label=f"SMA{p}"))
    kwargs = {"type": "candle", "style": style, "volume": volume,
              "title": f"\n{symbol} — {period.upper()} ({interval})",
              "figsize": (12, 7), "savefig": filepath}
    if add_plots: kwargs["addplot"] = add_plots
    mpf.plot(df, **kwargs)
    return filepath


def generate_line(symbol, period="1y", interval="1d"):
    df = fetch_history(symbol, period, interval)
    os.makedirs(CHART_DIR, exist_ok=True)
    filename = f"{symbol.replace('/', '_')}_line_{period}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
    filepath = os.path.join(CHART_DIR, filename)
    mc = mpf.make_marketcolors(up="green", down="red", inherit=True)
    style = mpf.make_mpf_style(marketcolors=mc, gridstyle=":")
    mpf.plot(df, type="line", style=style, title=f"\n{symbol} — {period.upper()}",
             figsize=(12, 6), savefig=filepath)
    return filepath


def main():
    parser = argparse.ArgumentParser(description="Generate price charts")
    parser.add_argument("symbol")
    parser.add_argument("--type", choices=["candle", "line"], default="candle")
    parser.add_argument("--period", default="3mo")
    parser.add_argument("--interval", default="1d")
    parser.add_argument("--no-volume", action="store_true")
    parser.add_argument("--sma", nargs="+", type=int)
    args = parser.parse_args()
    try:
        if args.type == "candle":
            path = generate_candlestick(args.symbol, args.period, args.interval,
                                        not args.no_volume, args.sma)
        else:
            path = generate_line(args.symbol, args.period, args.interval)
        print(f"Chart saved: {path}")
    except Exception as e:
        print(f"Error: {e}"); sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/check_alerts.py
content: #!/usr/bin/env python3
"""Cron job: check all active alerts against current prices."""

import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection
from lib.config import get_api_key
from lib.rate_limiter import wait_if_needed
import requests


def fetch_price(symbol, asset_type):
    if asset_type == "crypto":
        pair = symbol if symbol.endswith("USDT") else symbol + "USDT"
        wait_if_needed("binance")
        return float(requests.get("https://api.binance.com/api/v3/ticker/price",
                                  params={"symbol": pair}, timeout=10).json()["price"])
    elif asset_type == "forex":
        parts = symbol.replace("/", "")
        base, target = parts[:3], parts[3:]
        [REDACTED_SECRET]"exchangeRateApiKey")
        if [REDACTED_SECRET]"exchangerate")
            return requests.get(f"https://v6.exchangerate-api.com/v6/{api_key}/pair/{base}/{target}",
                                timeout=10).json()["conversion_rate"]
        wait_if_needed("exchangerate")
        return requests.get(f"https://open.er-api.com/v6/latest/{base}", timeout=10).json()["rates"].get(target, 0)
    else:
        [REDACTED_SECRET]"finnhubApiKey")
        if asset_type == "stock_us" and [REDACTED_SECRET]"finnhub")
            data = requests.get("https://finnhub.io/api/v1/quote",
                                params={"symbol": symbol, "token": api_key}, timeout=10).json()
            if data.get("c", 0) > 0: return data["c"]
        import yfinance as yf
        wait_if_needed("yfinance")
        return yf.Ticker(symbol).fast_info.last_price


def check_alerts():
    conn = get_connection()
    try:
        conn.execute("UPDATE alerts SET status='active' WHERE status='snoozed' AND snoozed_until < datetime('now')")
        conn.commit()
        alerts = conn.execute("SELECT * FROM alerts WHERE status='active'").fetchall()
        if not alerts: return None
        triggered = []
        for a in alerts:
            try:
                price = fetch_price(a["symbol"], a["asset_type"])
                if price is None: continue
                hit = False
                if a["condition"] == "above" and price >= a["target_value"]: hit = True
                elif a["condition"] == "below" and price <= a["target_value"]: hit = True
                elif a["condition"] == "change_pct" and a["current_value_at_creation"]:
                    if abs((price - a["current_value_at_creation"]) / a["current_value_at_creation"] * 100) >= abs(a["target_value"]): hit = True
                if hit:
                    conn.execute("UPDATE alerts SET status='triggered', triggered_at=CURRENT_TIMESTAMP WHERE id=?", (a["id"],))
                    triggered.append({"id": a["id"], "symbol": a["symbol"], "condition": a["condition"],
                                      "target": a["target_value"], "current_price": price, "note": a["note"]})
            except Exception: continue
        conn.commit()
        return triggered if triggered else None
    finally: conn.close()


if __name__ == "__main__":
    triggered = check_alerts()
    if triggered:
        print("**Price Alert Triggered!**\n")
        for t in triggered:
            cond = {"above": f"above {t['target']}", "below": f"below {t['target']}",
                    "change_pct": f"changed by {t['target']}%"}.get(t["condition"], t["condition"])
            print(f"  #{t['id']} **{t['symbol']}** — now {t['current_price']:.4f} (target: {cond})")
            if t.get("note"): print(f"    Note: {t['note']}")
    else:
        print("No alerts triggered.")


path: scripts/crypto.py
content: #!/usr/bin/env python3
"""Crypto prices from Binance and CoinGecko."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.formatters import fmt_price, fmt_change, fmt_number
from lib.rate_limiter import wait_if_needed
import requests


def binance_price(symbol):
    pair = symbol.upper()
    if not pair.endswith("USDT") and not pair.endswith("BUSD"):
        pair = pair + "USDT"
    wait_if_needed("binance")
    d = requests.get("https://api.binance.com/api/v3/ticker/24hr",
                     params={"symbol": pair}, timeout=10).json()
    return {"symbol": symbol.upper(), "pair": pair, "price": float(d["lastPrice"]),
            "change": float(d["priceChange"]), "change_pct": float(d["priceChangePercent"]),
            "high_24h": float(d["highPrice"]), "low_24h": float(d["lowPrice"]),
            "volume_24h": float(d["volume"]), "quote_volume_24h": float(d["quoteVolume"]),
            "source": "Binance"}


def binance_top_gainers(limit=10):
    wait_if_needed("binance")
    data = requests.get("https://api.binance.com/api/v3/ticker/24hr", timeout=15).json()
    usdt = [d for d in data if d["symbol"].endswith("USDT") and float(d["quoteVolume"]) > 1_000_000]
    usdt.sort(key=lambda x: float(x["priceChangePercent"]), reverse=True)
    return [{"symbol": d["symbol"].replace("USDT", ""), "price": float(d["lastPrice"]),
             "change_pct": float(d["priceChangePercent"]),
             "volume_usdt": float(d["quoteVolume"])} for d in usdt[:limit]]


COIN_MAP = {"BTC": "bitcoin", "ETH": "ethereum", "BNB": "binancecoin", "SOL": "solana",
            "XRP": "ripple", "ADA": "cardano", "DOGE": "dogecoin", "DOT": "polkadot",
            "AVAX": "avalanche-2", "MATIC": "matic-network", "LINK": "chainlink",
            "UNI": "uniswap", "LTC": "litecoin", "ATOM": "cosmos", "SHIB": "shiba-inu"}


def coingecko_price_try(symbol):
    coin_id = COIN_MAP.get(symbol.upper())
    if not coin_id:
        return {"error": f"Unknown coin: {symbol}. Supported: {', '.join(COIN_MAP.keys())}"}
    wait_if_needed("coingecko")
    data = requests.get("https://api.coingecko.com/api/v3/simple/price",
                        params={"ids": coin_id, "vs_currencies": "try,usd",
                                "include_24hr_change": "true"}, timeout=10).json().get(coin_id, {})
    return {"symbol": symbol.upper(), "price_try": data.get("try", 0),
            "price_usd": data.get("usd", 0), "change_pct_24h": data.get("usd_24h_change", 0),
            "source": "CoinGecko"}


def main():
    parser = argparse.ArgumentParser(description="Crypto prices")
    parser.add_argument("action", choices=["price", "top", "try"])
    parser.add_argument("symbol", nargs="?", default="BTC")
    parser.add_argument("--limit", type=int, default=10)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if args.action == "price":
        r = binance_price(args.symbol)
        if args.json: print(json.dumps(r, indent=2))
        else:
            print(f"**{r['symbol']}** — {fmt_price(r['price'], 'USD')}")
            print(fmt_change(r["change"], r["change_pct"]))
            print(f"24h Range: {fmt_price(r['low_24h'], 'USD')} — {fmt_price(r['high_24h'], 'USD')}")
            print(f"24h Volume: {fmt_number(r['volume_24h'])} coins ({fmt_number(r['quote_volume_24h'])} USDT)")
    elif args.action == "top":
        results = binance_top_gainers(args.limit)
        if args.json: print(json.dumps(results, indent=2))
        else:
            print(f"**Top {args.limit} Crypto Gainers (24h)**\n")
            for i, r in enumerate(results, 1):
                s = "+" if r["change_pct"] >= 0 else ""
                print(f"{i}. **{r['symbol']}** — {fmt_price(r['price'], 'USD')} ({s}{r['change_pct']:.2f}%)")
    elif args.action == "try":
        r = coingecko_price_try(args.symbol)
        if args.json: print(json.dumps(r, indent=2))
        elif "error" in r: print(r["error"])
        else:
            print(f"**{r['symbol']}** — {fmt_price(r['price_try'], 'TRY')} / {fmt_price(r['price_usd'], 'USD')}")
            if r.get("change_pct_24h"):
                s = "+" if r["change_pct_24h"] >= 0 else ""
                print(f"24h: {s}{r['change_pct_24h']:.2f}%")


if __name__ == "__main__":
    main()


path: scripts/earnings.py
content: #!/usr/bin/env python3
"""Earnings calendar from Finnhub."""

import sys, os, json, argparse
from datetime import datetime, timedelta
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.rate_limiter import wait_if_needed
import requests


def get_earnings_calendar(from_date=None, to_date=None):
    [REDACTED_SECRET]"finnhubApiKey")
    if not [REDACTED_SECRET] [{"error": "Finnhub API key not configured"}]
    if not from_date: from_date = datetime.now().strftime("%Y-%m-%d")
    if not to_date: to_date = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
    wait_if_needed("finnhub")
    data = requests.get("https://finnhub.io/api/v1/calendar/earnings",
        params={"from": from_date, "to": to_date, "token": api_key}, timeout=10).json()
    return [{"symbol": e.get("symbol", ""), "date": e.get("date", ""), "hour": e.get("hour", ""),
             "eps_estimate": e.get("epsEstimate")} for e in data.get("earningsCalendar", [])[:50]]


def get_earnings_for_symbol(symbol):
    [REDACTED_SECRET]"finnhubApiKey")
    if not [REDACTED_SECRET] [{"error": "Finnhub API key not configured"}]
    wait_if_needed("finnhub")
    return requests.get("https://finnhub.io/api/v1/stock/earnings",
        params={"symbol": symbol.upper(), "token": api_key}, timeout=10).json()[:10]


def main():
    parser = argparse.ArgumentParser(description="Earnings calendar")
    parser.add_argument("action", choices=["calendar", "symbol"])
    parser.add_argument("--symbol", "-s"); parser.add_argument("--from-date"); parser.add_argument("--to-date")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if args.action == "calendar":
        data = get_earnings_calendar(args.from_date, args.to_date)
        if args.json: print(json.dumps(data, indent=2)); return
        if data and data[0].get("error"): print(f"Error: {data[0]['error']}"); return
        print("**Upcoming Earnings**\n")
        cur = ""
        for e in data:
            if e["date"] != cur: cur = e["date"]; print(f"\n**{cur}**")
            hour = {"bmo": "Before Open", "amc": "After Close"}.get(e.get("hour", ""), "")
            eps = f" (est: {e['eps_estimate']})" if e.get("eps_estimate") else ""
            print(f"  {e['symbol']} {hour}{eps}")
    elif args.action == "symbol":
        if not args.symbol: print("Error: --symbol required"); sys.exit(1)
        data = get_earnings_for_symbol(args.symbol)
        if args.json: print(json.dumps(data, indent=2)); return
        print(f"**Earnings History: {args.symbol.upper()}**\n")
        for e in data:
            print(f"  {e.get('period','?')}: EPS {e.get('actual','N/A')} (est: {e.get('estimate','N/A')}, surprise: {e.get('surprisePercent','N/A')}%)")


if __name__ == "__main__":
    main()


path: scripts/forex.py
content: #!/usr/bin/env python3
"""Exchange rates via ExchangeRate-API."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.formatters import fmt_price
from lib.rate_limiter import wait_if_needed
import requests


def get_rate(base, target):
    base, target = base.upper(), target.upper()
    [REDACTED_SECRET]"exchangeRateApiKey")
    if [REDACTED_SECRET]"exchangerate")
        data = requests.get(f"https://v6.exchangerate-api.com/v6/{api_key}/pair/{base}/{target}", timeout=10).json()
        return {"base": base, "target": target, "rate": data["conversion_rate"],
                "updated": data.get("time_last_update_utc", ""), "source": "ExchangeRate-API"}
    wait_if_needed("exchangerate")
    data = requests.get(f"https://open.er-api.com/v6/latest/{base}", timeout=10).json()
    return {"base": base, "target": target, "rate": data["rates"].get(target, 0),
            "updated": data.get("time_last_update_utc", ""), "source": "open.er-api.com"}


def convert(amount, base, target):
    r = get_rate(base, target)
    return {"from": base.upper(), "to": target.upper(), "amount": amount,
            "rate": r["rate"], "result": round(amount * r["rate"], 2)}


def get_multiple_rates(base, targets):
    base = base.upper()
    [REDACTED_SECRET]"exchangeRateApiKey")
    if [REDACTED_SECRET]"exchangerate")
        data = requests.get(f"https://v6.exchangerate-api.com/v6/{api_key}/latest/{base}", timeout=10).json()
    else:
        wait_if_needed("exchangerate")
        data = requests.get(f"https://open.er-api.com/v6/latest/{base}", timeout=10).json()
    rates = data.get("conversion_rates", data.get("rates", {}))
    return {"base": base, "rates": {t.upper(): rates.get(t.upper(), 0) for t in targets},
            "updated": data.get("time_last_update_utc", "")}


def main():
    parser = argparse.ArgumentParser(description="Foreign exchange rates")
    parser.add_argument("action", choices=["rate", "convert", "multi"])
    parser.add_argument("base", help="Base currency")
    parser.add_argument("target", nargs="?", default="TRY")
    parser.add_argument("--amount", type=float)
    parser.add_argument("--targets", nargs="+", default=["TRY", "EUR", "GBP"])
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if args.action == "rate":
        r = get_rate(args.base, args.target)
        if args.json: print(json.dumps(r, indent=2))
        else: print(f"**{r['base']}/{r['target']}** — {r['rate']:.4f}\n1 {r['base']} = {fmt_price(r['rate'], r['target'])}")
    elif args.action == "convert":
        if not args.amount: print("Error: --amount required"); sys.exit(1)
        r = convert(args.amount, args.base, args.target)
        if args.json: print(json.dumps(r, indent=2))
        else: print(f"{fmt_price(r['amount'], r['from'])} = {fmt_price(r['result'], r['to'])}\nRate: {r['rate']:.4f}")
    elif args.action == "multi":
        r = get_multiple_rates(args.base, args.targets)
        if args.json: print(json.dumps(r, indent=2))
        else:
            print(f"**{r['base']} Exchange Rates**\n")
            for c, rate in r["rates"].items(): print(f"  {r['base']}/{c}: {rate:.4f}")


if __name__ == "__main__":
    main()


path: scripts/init_db.py
content: #!/usr/bin/env python3
"""Initialize the FinClaw SQLite database schema."""

import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection

SCHEMA = """
CREATE TABLE IF NOT EXISTS positions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    symbol TEXT NOT NULL,
    name TEXT,
    asset_type TEXT NOT NULL CHECK(asset_type IN ('stock_us','stock_bist','crypto','forex')),
    currency TEXT NOT NULL DEFAULT 'USD',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    notes TEXT
);

CREATE TABLE IF NOT EXISTS transactions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    position_id INTEGER NOT NULL,
    symbol TEXT NOT NULL,
    action TEXT NOT NULL CHECK(action IN ('buy','sell')),
    shares REAL NOT NULL,
    price REAL NOT NULL,
    fees REAL DEFAULT 0,
    transaction_date DATE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (position_id) REFERENCES positions(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS alerts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    symbol TEXT NOT NULL,
    asset_type TEXT NOT NULL,
    condition TEXT NOT NULL CHECK(condition IN ('above','below','change_pct','volume_above')),
    target_value REAL NOT NULL,
    current_value_at_creation REAL,
    status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','triggered','snoozed','deleted')),
    triggered_at TIMESTAMP,
    snoozed_until TIMESTAMP,
    note TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS watchlists (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS watchlist_items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    watchlist_id INTEGER NOT NULL,
    symbol TEXT NOT NULL,
    asset_type TEXT NOT NULL,
    FOREIGN KEY (watchlist_id) REFERENCES watchlists(id) ON DELETE CASCADE,
    UNIQUE(watchlist_id, symbol)
);

CREATE TABLE IF NOT EXISTS price_cache (
    symbol TEXT NOT NULL,
    asset_type TEXT NOT NULL,
    price REAL NOT NULL,
    change REAL,
    change_pct REAL,
    volume REAL,
    fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (symbol, asset_type)
);

CREATE TABLE IF NOT EXISTS briefing_log (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    briefing_type TEXT NOT NULL,
    content_hash TEXT,
    sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""


if __name__ == "__main__":
    conn = get_connection()
    conn.executescript(SCHEMA)
    conn.commit()
    tables = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    ).fetchall()
    print(f"Database initialized. Tables: {', '.join(r['name'] for r in tables)}")
    conn.close()


path: scripts/macro.py
content: #!/usr/bin/env python3
"""Macro economics data from FRED API."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.rate_limiter import wait_if_needed
import requests

SERIES = {
    "gdp": ("GDP", "US GDP (Quarterly)", "$B"), "inflation": ("CPIAUCSL", "CPI", "Index"),
    "unemployment": ("UNRATE", "Unemployment Rate", "%"), "fed_rate": ("FEDFUNDS", "Fed Funds Rate", "%"),
    "10y_yield": ("DGS10", "10-Year Treasury", "%"), "2y_yield": ("DGS2", "2-Year Treasury", "%"),
    "m2": ("M2SL", "M2 Money Supply", "$B"), "housing": ("HOUST", "Housing Starts", "K"),
    "retail": ("RSAFS", "Retail Sales", "$M"), "vix": ("VIXCLS", "VIX", "Index"),
}


def get_fred_series(series_id, limit=10):
    [REDACTED_SECRET]"fredApiKey")
    if not [REDACTED_SECRET] {"error": "FRED API key not configured. Add FRED_API_KEY to finclaw env config."}
    wait_if_needed("fred")
    data = requests.get("https://api.stlouisfed.org/fred/series/observations",
        params={"series_id": series_id, "api_key": api_key, "file_type": "json",
                "sort_order": "desc", "limit": limit}, timeout=10).json()
    return {"series_id": series_id,
            "observations": [{"date": o["date"], "value": float(o["value"])} for o in data.get("observations", []) if o["value"] != "."]}


def main():
    parser = argparse.ArgumentParser(description="Macro economics data")
    parser.add_argument("action", choices=["indicator", "dashboard", "series", "list"])
    parser.add_argument("--name", "-n"); parser.add_argument("--series-id")
    parser.add_argument("--limit", type=int, default=5); parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if args.action == "list":
        print("**Available Macro Indicators**\n")
        for k, (sid, name, unit) in SERIES.items(): print(f"  `{k}` — {name} ({sid})")
    elif args.action == "dashboard":
        [REDACTED_SECRET]"fredApiKey")
        if not [REDACTED_SECRET]"Error: FRED API key not configured"); sys.exit(1)
        print("**Macro Dashboard**\n")
        for name in ["fed_rate", "10y_yield", "unemployment", "inflation", "vix"]:
            sid, label, unit = SERIES[name]
            try:
                d = get_fred_series(sid, 1)
                if d.get("observations"): print(f"  {label}: {d['observations'][0]['value']}{unit} ({d['observations'][0]['date']})")
            except: print(f"  {label}: _error_")
    elif args.action == "indicator":
        if not args.name: print("Error: --name required"); sys.exit(1)
        if args.name not in SERIES: print(f"Unknown: {args.name}. Use: {', '.join(SERIES.keys())}"); sys.exit(1)
        sid, label, unit = SERIES[args.name]
        d = get_fred_series(sid, args.limit)
        if args.json: print(json.dumps(d, indent=2))
        elif "error" in d: print(f"Error: {d['error']}")
        else:
            print(f"**{label}** ({sid})\n")
            for o in d["observations"]: print(f"  {o['date']}: {o['value']} {unit}")
    elif args.action == "series":
        if not args.series_id: print("Error: --series-id required"); sys.exit(1)
        d = get_fred_series(args.series_id, args.limit)
        if args.json: print(json.dumps(d, indent=2))
        elif "error" in d: print(f"Error: {d['error']}")
        else:
            print(f"**{d['series_id']}**\n")
            for o in d["observations"]: print(f"  {o['date']}: {o['value']}")


if __name__ == "__main__":
    main()


path: scripts/news.py
content: #!/usr/bin/env python3
"""Financial news from Finnhub."""

import sys, os, json, argparse
from datetime import datetime, timedelta
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.rate_limiter import wait_if_needed
import requests


def get_company_news(symbol, days=7):
    [REDACTED_SECRET]"finnhubApiKey")
    if not [REDACTED_SECRET] [{"error": "Finnhub API key not configured. Add FINNHUB_API_KEY to finclaw env config."}]
    end = datetime.now()
    start = end - timedelta(days=days)
    wait_if_needed("finnhub")
    articles = requests.get("https://finnhub.io/api/v1/company-news",
        params={"symbol": symbol.upper(), "from": start.strftime("%Y-%m-%d"),
                "to": end.strftime("%Y-%m-%d"), "token": api_key}, timeout=10).json()
    return [{"headline": a.get("headline", ""), "summary": a.get("summary", ""),
             "source": a.get("source", ""), "url": a.get("url", ""),
             "datetime": datetime.fromtimestamp(a["datetime"]).strftime("%Y-%m-%d %H:%M") if a.get("datetime") else ""}
            for a in articles[:20]]


def get_market_news(category="general"):
    [REDACTED_SECRET]"finnhubApiKey")
    if not [REDACTED_SECRET] [{"error": "Finnhub API key not configured"}]
    wait_if_needed("finnhub")
    articles = requests.get("https://finnhub.io/api/v1/news",
        params={"category": category, "token": api_key}, timeout=10).json()
    return [{"headline": a.get("headline", ""), "source": a.get("source", ""),
             "url": a.get("url", ""),
             "datetime": datetime.fromtimestamp(a["datetime"]).strftime("%Y-%m-%d %H:%M") if a.get("datetime") else ""}
            for a in articles[:15]]


def format_news(articles, title="News"):
    if not articles: return "No news found."
    if articles[0].get("error"): return f"Error: {articles[0]['error']}"
    lines = [f"**{title}**\n"]
    for i, a in enumerate(articles, 1):
        lines.append(f"{i}. **{a['headline']}**")
        if a.get("summary"):
            lines.append(f"   {a['summary'][:200]}...")
        meta = [x for x in [a.get("source"), a.get("datetime")] if x]
        if meta: lines.append(f"   _{'  |  '.join(meta)}_")
        lines.append("")
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Financial news")
    parser.add_argument("action", choices=["company", "market"])
    parser.add_argument("--symbol", "-s")
    parser.add_argument("--days", type=int, default=7)
    parser.add_argument("--category", default="general")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    if args.action == "company":
        if not args.symbol: print("Error: --symbol required"); sys.exit(1)
        articles = get_company_news(args.symbol, args.days)
        title = f"{args.symbol.upper()} News"
    else:
        articles = get_market_news(args.category)
        title = f"Market News ({args.category})"
    print(json.dumps(articles, indent=2) if args.json else format_news(articles, title))


if __name__ == "__main__":
    main()


path: scripts/pnl.py
content: #!/usr/bin/env python3
"""Profit & Loss calculations with live market prices."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection
from lib.formatters import fmt_price
from scripts.quote import get_quote


def calculate_pnl(symbol=None):
    conn = get_connection()
    try:
        if symbol:
            positions = conn.execute("SELECT * FROM positions WHERE symbol=?", (symbol.upper(),)).fetchall()
        else:
            positions = conn.execute("SELECT * FROM positions ORDER BY symbol").fetchall()
        results = []
        for p in positions:
            buys = conn.execute("SELECT COALESCE(SUM(shares),0) as s, COALESCE(SUM(shares*price),0) as c, COALESCE(SUM(fees),0) as f FROM transactions WHERE position_id=? AND action='buy'", (p["id"],)).fetchone()
            sells = conn.execute("SELECT COALESCE(SUM(shares),0) as s, COALESCE(SUM(shares*price),0) as c, COALESCE(SUM(fees),0) as f FROM transactions WHERE position_id=? AND action='sell'", (p["id"],)).fetchone()
            holding = buys["s"] - sells["s"]
            if holding <= 0: continue
            cost = buys["c"] + buys["f"] - sells["c"] + sells["f"]
            avg = cost / holding if holding else 0
            try:
                q = get_quote(p["symbol"])
                cp = q["price"]; mv = holding * cp; pnl = mv - cost
                pct = (pnl / cost * 100) if cost else 0
            except Exception:
                cp = mv = pnl = pct = None
            results.append({"symbol": p["symbol"], "asset_type": p["asset_type"], "currency": p["currency"],
                            "shares": holding, "avg_cost": round(avg, 2), "cost_basis": round(cost, 2),
                            "current_price": round(cp, 2) if cp else None, "market_value": round(mv, 2) if mv else None,
                            "unrealized_pnl": round(pnl, 2) if pnl is not None else None,
                            "unrealized_pct": round(pct, 2) if pct is not None else None})
        return results
    finally: conn.close()


def format_pnl(results):
    if not results: return "No open positions."
    lines = ["**Portfolio P&L**\n"]
    tc = tv = tp = 0
    for r in results:
        c = r["currency"]
        lines.append(f"**{r['symbol']}** — {r['shares']} shares @ avg {fmt_price(r['avg_cost'], c)}")
        if r["current_price"] is not None:
            e = "🟢" if r["unrealized_pnl"] >= 0 else "🔴"
            s = "+" if r["unrealized_pnl"] >= 0 else ""
            lines.append(f"  Now: {fmt_price(r['current_price'], c)} | Value: {fmt_price(r['market_value'], c)}")
            lines.append(f"  {e} P&L: {s}{fmt_price(r['unrealized_pnl'], c)} ({s}{r['unrealized_pct']:.2f}%)")
            tc += r["cost_basis"]; tv += r["market_value"]; tp += r["unrealized_pnl"]
        lines.append("")
    if tc > 0:
        e = "🟢" if tp >= 0 else "🔴"; s = "+" if tp >= 0 else ""
        lines += ["---", f"**Total**: {fmt_price(tv, 'USD')} | {e} {s}{fmt_price(tp, 'USD')} ({s}{tp/tc*100:.2f}%)"]
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Portfolio P&L")
    parser.add_argument("--symbol", "-s")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    results = calculate_pnl(args.symbol)
    print(json.dumps(results, indent=2, default=str) if args.json else format_pnl(results))


if __name__ == "__main__":
    main()


path: scripts/portfolio.py
content: #!/usr/bin/env python3
"""Portfolio management: add, sell, remove, list, summary."""

import sys, os, json, argparse
from datetime import date
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection
from lib.formatters import detect_asset_type, fmt_price


def add_position(symbol, shares, price, name=None, fees=0, transaction_date=None, notes=None):
    symbol = symbol.upper()
    asset_type = detect_asset_type(symbol)
    currency = "TRY" if asset_type == "stock_bist" else "USD"
    tx_date = transaction_date or date.today().isoformat()
    conn = get_connection()
    try:
        existing = conn.execute("SELECT id FROM positions WHERE symbol=?", (symbol,)).fetchone()
        if existing:
            pos_id = existing["id"]
        else:
            pos_id = conn.execute("INSERT INTO positions (symbol, name, asset_type, currency, notes) VALUES (?, ?, ?, ?, ?)",
                                  (symbol, name or symbol, asset_type, currency, notes)).lastrowid
        conn.execute("INSERT INTO transactions (position_id, symbol, action, shares, price, fees, transaction_date) VALUES (?, ?, 'buy', ?, ?, ?, ?)",
                     (pos_id, symbol, shares, price, fees, tx_date))
        conn.execute("UPDATE positions SET updated_at=CURRENT_TIMESTAMP WHERE id=?", (pos_id,))
        conn.commit()
        return f"Bought {shares} {symbol} @ {fmt_price(price, currency)}\nTotal: {fmt_price(shares * price + fees, currency)} (fees: {fmt_price(fees, currency)})"
    finally: conn.close()


def sell_position(symbol, shares, price, fees=0, transaction_date=None):
    symbol = symbol.upper()
    tx_date = transaction_date or date.today().isoformat()
    conn = get_connection()
    try:
        pos = conn.execute("SELECT id, currency FROM positions WHERE symbol=?", (symbol,)).fetchone()
        if not pos: return f"Error: No position for {symbol}"
        bought = conn.execute("SELECT COALESCE(SUM(shares),0) as t FROM transactions WHERE position_id=? AND action='buy'", (pos["id"],)).fetchone()["t"]
        sold = conn.execute("SELECT COALESCE(SUM(shares),0) as t FROM transactions WHERE position_id=? AND action='sell'", (pos["id"],)).fetchone()["t"]
        if shares > bought - sold: return f"Error: Only {bought - sold} shares available"
        conn.execute("INSERT INTO transactions (position_id, symbol, action, shares, price, fees, transaction_date) VALUES (?, ?, 'sell', ?, ?, ?, ?)",
                     (pos["id"], symbol, shares, price, fees, tx_date))
        conn.commit()
        return f"Sold {shares} {symbol} @ {fmt_price(price, pos['currency'])}\nProceeds: {fmt_price(shares * price - fees, pos['currency'])}"
    finally: conn.close()


def remove_position(symbol):
    symbol = symbol.upper()
    conn = get_connection()
    try:
        pos = conn.execute("SELECT id FROM positions WHERE symbol=?", (symbol,)).fetchone()
        if not pos: return f"Error: No position for {symbol}"
        conn.execute("DELETE FROM transactions WHERE position_id=?", (pos["id"],))
        conn.execute("DELETE FROM positions WHERE id=?", (pos["id"],))
        conn.commit()
        return f"Removed {symbol} and all transactions."
    finally: conn.close()


def list_positions():
    conn = get_connection()
    try:
        positions = conn.execute(
            "SELECT p.*, COALESCE((SELECT SUM(shares) FROM transactions WHERE position_id=p.id AND action='buy'),0) as bought, "
            "COALESCE((SELECT SUM(shares) FROM transactions WHERE position_id=p.id AND action='sell'),0) as sold "
            "FROM positions p ORDER BY p.symbol").fetchall()
        if not positions: return "No positions in portfolio."
        lines = ["**Portfolio Positions**\n"]
        for p in positions:
            holding = p["bought"] - p["sold"]
            if holding <= 0: continue
            avg = conn.execute("SELECT COALESCE(SUM(shares*price)/NULLIF(SUM(shares),0),0) as avg FROM transactions WHERE position_id=? AND action='buy'",
                               (p["id"],)).fetchone()["avg"]
            lines.append(f"  {p['symbol']} — {holding} shares @ avg {fmt_price(avg, p['currency'])} ({p['asset_type']})")
        return "\n".join(lines) if len(lines) > 1 else "No open positions."
    finally: conn.close()


def portfolio_summary():
    conn = get_connection()
    try:
        positions = conn.execute("SELECT * FROM positions ORDER BY symbol").fetchall()
        if not positions: return "Portfolio is empty."
        lines = ["**Portfolio Summary**\n"]
        total = 0
        for p in positions:
            buys = conn.execute("SELECT COALESCE(SUM(shares),0) as s, COALESCE(SUM(shares*price),0) as c FROM transactions WHERE position_id=? AND action='buy'", (p["id"],)).fetchone()
            sells = conn.execute("SELECT COALESCE(SUM(shares),0) as s, COALESCE(SUM(shares*price),0) as c FROM transactions WHERE position_id=? AND action='sell'", (p["id"],)).fetchone()
            holding = buys["s"] - sells["s"]
            if holding <= 0: continue
            cost = buys["c"] - sells["c"]
            avg = cost / holding if holding else 0
            total += cost
            lines.append(f"  **{p['symbol']}**: {holding} shares, cost: {fmt_price(cost, p['currency'])}, avg: {fmt_price(avg, p['currency'])}")
        lines.append(f"\n**Total Invested**: {fmt_price(total, 'USD')}")
        return "\n".join(lines)
    finally: conn.close()


def main():
    parser = argparse.ArgumentParser(description="Portfolio management")
    parser.add_argument("action", choices=["add", "sell", "remove", "list", "summary"])
    parser.add_argument("--symbol", "-s"); parser.add_argument("--shares", "-n", type=float)
    parser.add_argument("--price", "-p", type=float); parser.add_argument("--name")
    parser.add_argument("--fees", type=float, default=0); parser.add_argument("--date")
    parser.add_argument("--notes")
    args = parser.parse_args()

    if args.action == "add":
        if not all([args.symbol, args.shares, args.price]): print("Error: --symbol, --shares, --price required"); sys.exit(1)
        print(add_position(args.symbol, args.shares, args.price, args.name, args.fees, args.date, args.notes))
    elif args.action == "sell":
        if not all([args.symbol, args.shares, args.price]): print("Error: --symbol, --shares, --price required"); sys.exit(1)
        print(sell_position(args.symbol, args.shares, args.price, args.fees, args.date))
    elif args.action == "remove":
        if not args.symbol: print("Error: --symbol required"); sys.exit(1)
        print(remove_position(args.symbol))
    elif args.action == "list": print(list_positions())
    elif args.action == "summary": print(portfolio_summary())


if __name__ == "__main__":
    main()


path: scripts/quote.py
content: #!/usr/bin/env python3
"""Fetch real-time quotes for US stocks, BIST, crypto, and forex."""

import sys
import os
import json
import argparse

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.formatters import fmt_price, fmt_change, fmt_volume, detect_asset_type, symbol_normalize
from lib.rate_limiter import wait_if_needed
from lib.db import get_connection


def get_cached_price(symbol, asset_type, max_age_seconds=60):
    conn = get_connection()
    try:
        row = conn.execute(
            "SELECT * FROM price_cache WHERE symbol=? AND asset_type=? "
            "AND fetched_at > datetime('now', ?)",
            (symbol, asset_type, f"-{max_age_seconds} seconds")
        ).fetchone()
        return dict(row) if row else None
    finally:
        conn.close()


def update_cache(symbol, asset_type, price, change=None, change_pct=None, volume=None):
    conn = get_connection()
    try:
        conn.execute(
            "INSERT OR REPLACE INTO price_cache "
            "(symbol, asset_type, price, change, change_pct, volume, fetched_at) "
            "VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)",
            (symbol, asset_type, price, change, change_pct, volume)
        )
        conn.commit()
    finally:
        conn.close()


def quote_us_stock(symbol):
    import requests
    [REDACTED_SECRET]"finnhubApiKey")
    if not [REDACTED_SECRET] quote_stock_yf(symbol)
    wait_if_needed("finnhub")
    resp = requests.get("https://finnhub.io/api/v1/quote",
                        params={"symbol": symbol, "token": api_key}, timeout=10)
    resp.raise_for_status()
    data = resp.json()
    if data.get("c", 0) == 0:
        return quote_stock_yf(symbol)
    return {"symbol": symbol, "price": data["c"], "change": data["d"],
            "change_pct": data["dp"], "high": data["h"], "low": data["l"],
            "open": data["o"], "prev_close": data["pc"], "currency": "USD", "source": "Finnhub"}


def quote_stock_yf(symbol):
    import yfinance as yf
    wait_if_needed("yfinance")
    ticker = yf.Ticker(symbol)
    info = ticker.fast_info
    price = info.last_price
    prev = info.previous_close
    change = price - prev if prev else 0
    change_pct = (change / prev * 100) if prev else 0
    return {"symbol": symbol, "price": price, "change": round(change, 2),
            "change_pct": round(change_pct, 2), "high": getattr(info, 'day_high', None),
            "low": getattr(info, 'day_low', None), "open": getattr(info, 'open', None),
            "prev_close": prev, "currency": "USD", "source": "yfinance"}


def quote_bist(symbol):
    import yfinance as yf
    if not symbol.endswith(".IS"):
        symbol = symbol + ".IS"
    wait_if_needed("yfinance")
    ticker = yf.Ticker(symbol)
    info = ticker.fast_info
    price = info.last_price
    prev = info.previous_close
    change = price - prev if prev else 0
    change_pct = (change / prev * 100) if prev else 0
    return {"symbol": symbol, "price": price, "change": round(change, 2),
            "change_pct": round(change_pct, 2), "high": getattr(info, 'day_high', None),
            "low": getattr(info, 'day_low', None), "open": getattr(info, 'open', None),
            "prev_close": prev, "currency": "TRY", "source": "yfinance"}


def quote_crypto(symbol):
    import requests
    pair = symbol.upper()
    if not pair.endswith("USDT") and not pair.endswith("BUSD"):
        pair = pair + "USDT"
    wait_if_needed("binance")
    resp = requests.get("https://api.binance.com/api/v3/ticker/24hr",
                        params={"symbol": pair}, timeout=10)
    resp.raise_for_status()
    d = resp.json()
    return {"symbol": symbol.upper(), "pair": pair, "price": float(d["lastPrice"]),
            "change": float(d["priceChange"]), "change_pct": float(d["priceChangePercent"]),
            "high": float(d["highPrice"]), "low": float(d["lowPrice"]),
            "volume": float(d["volume"]), "currency": "USDT", "source": "Binance"}


def quote_forex(symbol):
    import requests
    [REDACTED_SECRET]"exchangeRateApiKey")
    parts = symbol.replace("/", "").upper()
    base, target = parts[:3], parts[3:]
    if [REDACTED_SECRET]"exchangerate")
        resp = requests.get(f"https://v6.exchangerate-api.com/v6/{api_key}/pair/{base}/{target}", timeout=10)
        resp.raise_for_status()
        rate = resp.json()["conversion_rate"]
    else:
        wait_if_needed("exchangerate")
        resp = requests.get(f"https://open.er-api.com/v6/latest/{base}", timeout=10)
        resp.raise_for_status()
        rate = resp.json()["rates"].get(target, 0)
    return {"symbol": f"{base}/{target}", "price": rate, "change": None,
            "change_pct": None, "currency": target, "source": "ExchangeRate-API"}


def get_quote(symbol, force=False):
    symbol = symbol_normalize(symbol)
    asset_type = detect_asset_type(symbol)
    if not force:
        cached = get_cached_price(symbol, asset_type)
        if cached:
            return {"symbol": symbol, "price": cached["price"], "change": cached["change"],
                    "change_pct": cached["change_pct"], "volume": cached["volume"],
                    "asset_type": asset_type, "cached": True, "source": "cache"}

    fetchers = {"stock_us": quote_us_stock, "stock_bist": quote_bist,
                "crypto": quote_crypto, "forex": quote_forex}
    result = fetchers[asset_type](symbol)
    result["asset_type"] = asset_type
    update_cache(symbol, asset_type, result["price"],
                 result.get("change"), result.get("change_pct"), result.get("volume"))
    return result


def format_quote(q):
    if "error" in q:
        return f"Error: {q['error']}"
    currency = q.get("currency", "USD")
    lines = [f"**{q['symbol']}** — {fmt_price(q['price'], currency)}"]
    if q.get("change") is not None and q.get("change_pct") is not None:
        lines.append(fmt_change(q["change"], q["change_pct"]))
    details = []
    if q.get("open"): details.append(f"Open: {fmt_price(q['open'], currency)}")
    if q.get("high"): details.append(f"High: {fmt_price(q['high'], currency)}")
    if q.get("low"): details.append(f"Low: {fmt_price(q['low'], currency)}")
    if q.get("volume"): details.append(fmt_volume(q["volume"]))
    if details: lines.append(" | ".join(details))
    src = "cached" if q.get("cached") else q.get("source", "")
    lines.append(f"_Source: {src}_")
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Get financial quotes")
    parser.add_argument("symbols", nargs="+", help="Ticker symbol(s)")
    parser.add_argument("--force", action="store_true", help="Skip cache")
    parser.add_argument("--json", action="store_true", help="Output raw JSON")
    args = parser.parse_args()
    results = [get_quote(sym, force=args.force) for sym in args.symbols]
    if args.json:
        print(json.dumps(results, indent=2, default=str))
    else:
        for q in results:
            print(format_quote(q))
            print()


if __name__ == "__main__":
    main()


path: scripts/research.py
content: #!/usr/bin/env python3
"""Deep-dive ticker research."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.formatters import fmt_price, fmt_number, detect_asset_type
from lib.rate_limiter import wait_if_needed
from scripts.quote import get_quote
import requests


def research(symbol):
    symbol = symbol.upper()
    asset_type = detect_asset_type(symbol)
    result = {"symbol": symbol, "asset_type": asset_type}
    try: result["quote"] = get_quote(symbol, force=True)
    except: result["quote"] = {"error": "unavailable"}

    if asset_type in ("stock_us", "stock_bist"):
        import yfinance as yf
        ticker_sym = symbol if asset_type == "stock_us" else (symbol if symbol.endswith(".IS") else symbol + ".IS")
        wait_if_needed("yfinance")
        try:
            info = yf.Ticker(ticker_sym).info
            result["info"] = {k: info.get(k) for k in ["longName", "sector", "industry", "marketCap",
                "trailingPE", "forwardPE", "trailingEps", "dividendYield", "beta",
                "fiftyTwoWeekHigh", "fiftyTwoWeekLow", "averageVolume", "longBusinessSummary"]}
            if result["info"].get("longBusinessSummary"):
                result["info"]["longBusinessSummary"] = result["info"]["longBusinessSummary"][:500]
        except: result["info"] = {}
    return result


def format_research(d):
    lines = [f"**Research: {d['symbol']}** ({d['asset_type']})\n"]
    q = d.get("quote", {})
    if "error" not in q:
        c = q.get("currency", "USD")
        lines.append(f"**Price**: {fmt_price(q.get('price', 0), c)}")
        if q.get("change_pct") is not None:
            lines.append(f"Change: {'+' if q['change_pct'] >= 0 else ''}{q['change_pct']:.2f}%")
    info = d.get("info", {})
    if info:
        if info.get("longName"): lines.append(f"\n**{info['longName']}**")
        if info.get("sector"): lines.append(f"Sector: {info['sector']} | Industry: {info.get('industry', 'N/A')}")
        if info.get("marketCap"): lines.append(f"Market Cap: {fmt_number(info['marketCap'])}")
        if info.get("trailingPE"): lines.append(f"P/E: {info['trailingPE']:.2f}")
        if info.get("trailingEps"): lines.append(f"EPS: {info['trailingEps']:.2f}")
        if info.get("dividendYield"): lines.append(f"Div Yield: {info['dividendYield']*100:.2f}%")
        if info.get("beta"): lines.append(f"Beta: {info['beta']:.2f}")
        if info.get("fiftyTwoWeekHigh") and info.get("fiftyTwoWeekLow"):
            lines.append(f"52W Range: {info['fiftyTwoWeekLow']:.2f} — {info['fiftyTwoWeekHigh']:.2f}")
        if info.get("longBusinessSummary"): lines.append(f"\n**About**: {info['longBusinessSummary']}")
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Ticker research")
    parser.add_argument("symbol"); parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    d = research(args.symbol)
    print(json.dumps(d, indent=2, default=str) if args.json else format_research(d))


if __name__ == "__main__":
    main()


path: scripts/screener.py
content: #!/usr/bin/env python3
"""Stock screener for US, BIST, and crypto markets."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.formatters import fmt_price
from lib.rate_limiter import wait_if_needed

POPULAR_US = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "BRK.B",
              "JPM", "V", "UNH", "MA", "HD", "PG", "XOM"]
POPULAR_BIST = ["THYAO.IS", "GARAN.IS", "AKBNK.IS", "ISCTR.IS", "YKBNK.IS",
                "KCHOL.IS", "SAHOL.IS", "TUPRS.IS", "EREGL.IS", "BIMAS.IS",
                "ASELS.IS", "SISE.IS", "TCELL.IS", "TOASO.IS", "PGSUS.IS"]


def screen_by_change(symbols, direction="top"):
    import yfinance as yf
    results = []
    for sym in symbols:
        try:
            wait_if_needed("yfinance")
            info = yf.Ticker(sym).fast_info
            price, prev = info.last_price, info.previous_close
            if not prev: continue
            results.append({"symbol": sym, "price": round(price, 2),
                            "change_pct": round((price - prev) / prev * 100, 2)})
        except Exception: continue
    results.sort(key=lambda x: x["change_pct"], reverse=(direction == "top"))
    return results


def main():
    parser = argparse.ArgumentParser(description="Stock screener")
    parser.add_argument("market", choices=["us", "bist", "crypto"])
    parser.add_argument("--direction", choices=["top", "bottom"], default="top")
    parser.add_argument("--limit", type=int, default=10)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if args.market == "crypto":
        from crypto import binance_top_gainers
        results = binance_top_gainers(args.limit)
        if args.json: print(json.dumps(results, indent=2)); return
        print(f"**Top {args.limit} Crypto Gainers (24h)**\n")
        for i, r in enumerate(results, 1):
            s = "+" if r["change_pct"] >= 0 else ""
            print(f"{i}. **{r['symbol']}** — ${r['price']:.2f} ({s}{r['change_pct']:.2f}%)")
        return

    symbols = POPULAR_US if args.market == "us" else POPULAR_BIST
    results = screen_by_change(symbols, args.direction)[:args.limit]
    if args.json: print(json.dumps(results, indent=2)); return
    currency_sym = "$" if args.market == "us" else "₺"
    label = "US" if args.market == "us" else "BIST"
    d = "Gainers" if args.direction == "top" else "Losers"
    print(f"**Top {label} {d}**\n")
    for i, r in enumerate(results, 1):
        s = "+" if r["change_pct"] >= 0 else ""
        a = "▲" if r["change_pct"] >= 0 else "▼"
        print(f"{i}. **{r['symbol']}** — {currency_sym}{r['price']:.2f} {a} {s}{r['change_pct']:.2f}%")


if __name__ == "__main__":
    main()


path: scripts/sentiment.py
content: #!/usr/bin/env python3
"""News sentiment via Alpha Vantage."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.config import get_api_key
from lib.rate_limiter import wait_if_needed
import requests


def get_sentiment(symbol=None, topics=None):
    [REDACTED_SECRET]"alphaVantageApiKey")
    if not [REDACTED_SECRET] {"error": "Alpha Vantage API key not configured. Add ALPHA_VANTAGE_API_KEY to finclaw env config."}
    params = {"function": "NEWS_SENTIMENT", "apikey": api_key, "limit": 20}
    if symbol: params["tickers"] = symbol.upper()
    if topics: params["topics"] = topics
    wait_if_needed("alpha_vantage")
    data = requests.get("https://www.alphavantage.co/query", params=params, timeout=15).json()
    if "feed" not in data: return {"error": data.get("Note", "No data")}
    articles = [{"title": a.get("title", ""), "source": a.get("source", ""),
                 "score": float(a.get("overall_sentiment_score", 0)),
                 "label": a.get("overall_sentiment_label", "")} for a in data["feed"][:15]]
    avg = sum(a["score"] for a in articles) / len(articles) if articles else 0
    label = "Bullish" if avg > 0.35 else "Somewhat Bullish" if avg > 0.15 else "Neutral" if avg > -0.15 else "Somewhat Bearish" if avg > -0.35 else "Bearish"
    return {"query": symbol or topics or "general", "count": len(articles), "avg_sentiment": round(avg, 4),
            "label": label, "articles": articles}


def main():
    parser = argparse.ArgumentParser(description="News sentiment")
    parser.add_argument("--symbol", "-s"); parser.add_argument("--topics"); parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    d = get_sentiment(args.symbol, args.topics)
    if args.json: print(json.dumps(d, indent=2)); return
    if "error" in d: print(f"Error: {d['error']}"); return
    e = {"Bullish": "🟢", "Somewhat Bullish": "🟡", "Neutral": "⚪", "Somewhat Bearish": "🟡", "Bearish": "🔴"}.get(d["label"], "⚪")
    print(f"**Sentiment: {d['query']}**\n\n{e} {d['label']} ({d['avg_sentiment']:.4f}) — {d['count']} articles\n")
    for a in d["articles"][:10]:
        ae = "🟢" if a["score"] > 0.15 else "🔴" if a["score"] < -0.15 else "⚪"
        print(f"{ae} {a['title']}\n   _{a['source']} | {a['label']}_")


if __name__ == "__main__":
    main()


path: scripts/setup.py
content: #!/usr/bin/env python3
"""FinClaw setup: create venv, install dependencies, initialize database."""

import subprocess
import sys
import os

SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
VENV_DIR = os.path.join(SKILL_DIR, "venv")
VENV_PYTHON = os.path.join(VENV_DIR, "bin", "python3")
VENV_PIP = os.path.join(VENV_DIR, "bin", "pip")
REQ_FILE = os.path.join(SKILL_DIR, "requirements.txt")


def create_venv():
    if os.path.exists(VENV_PYTHON):
        print(f"Venv already exists at {VENV_DIR}")
        return
    print(f"Creating venv at {VENV_DIR}...")
    subprocess.check_call([sys.executable, "-m", "venv", VENV_DIR])
    print("Venv created.")


def install_deps():
    print("Installing dependencies...")
    subprocess.check_call([VENV_PIP, "install", "--upgrade", "pip"],
                          stdout=subprocess.DEVNULL)
    subprocess.check_call([VENV_PIP, "install", "-r", REQ_FILE])
    print("Dependencies installed.")


def init_db():
    print("Initializing database...")
    subprocess.check_call([VENV_PYTHON, os.path.join(SKILL_DIR, "scripts", "init_db.py")])


def main():
    print("=== FinClaw Setup ===\n")
    create_venv()
    install_deps()
    init_db()
    print("\n=== Setup complete! ===")
    print(f"Python: {VENV_PYTHON}")
    print(f"Database: {os.path.join(SKILL_DIR, 'data', 'finance.db')}")


if __name__ == "__main__":
    main()


path: scripts/technical.py
content: #!/usr/bin/env python3
"""Technical analysis: SMA, EMA, RSI, MACD, Bollinger Bands."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.formatters import detect_asset_type
from lib.rate_limiter import wait_if_needed
import pandas as pd
import numpy as np


def fetch_history(symbol, period="6mo"):
    import yfinance as yf
    asset_type = detect_asset_type(symbol)
    ticker_sym = symbol
    if asset_type == "stock_bist" and not symbol.endswith(".IS"): ticker_sym = symbol + ".IS"
    if asset_type == "crypto": ticker_sym = symbol + "-USD"
    wait_if_needed("yfinance")
    df = yf.Ticker(ticker_sym).history(period=period)
    if df.empty: raise ValueError(f"No data for {ticker_sym}")
    return df


def calc_sma(s, p): return s.rolling(window=p).mean()
def calc_ema(s, p): return s.ewm(span=p, adjust=False).mean()

def calc_rsi(s, p=14):
    delta = s.diff()
    gain = delta.where(delta > 0, 0).rolling(window=p).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=p).mean()
    return 100 - (100 / (1 + gain / loss))

def calc_macd(s, fast=12, slow=26, signal=9):
    ml = calc_ema(s, fast) - calc_ema(s, slow)
    sl = calc_ema(ml, signal)
    return ml, sl, ml - sl

def calc_bollinger(s, p=20, std=2):
    sma = calc_sma(s, p)
    sd = s.rolling(window=p).std()
    return sma + sd * std, sma, sma - sd * std


def analyze(symbol, period="6mo"):
    df = fetch_history(symbol, period)
    close = df["Close"]
    last = close.iloc[-1]
    sma20 = calc_sma(close, 20).iloc[-1]
    sma50 = calc_sma(close, 50).iloc[-1]
    sma200 = calc_sma(close, 200).iloc[-1] if len(close) >= 200 else None
    rsi = calc_rsi(close).iloc[-1]
    macd_l, sig_l, hist = calc_macd(close)
    bb_u, bb_m, bb_l = calc_bollinger(close)

    signals = []
    signals.append(f"Price {'above' if last > sma50 else 'below'} SMA50 ({'bullish' if last > sma50 else 'bearish'})")
    if sma200 is not None:
        signals.append(f"{'Golden' if sma50 > sma200 else 'Death'} cross (SMA50 {'>' if sma50 > sma200 else '<'} SMA200)")
    if rsi > 70: signals.append(f"RSI {rsi:.1f} — Overbought")
    elif rsi < 30: signals.append(f"RSI {rsi:.1f} — Oversold")
    else: signals.append(f"RSI {rsi:.1f} — Neutral")
    signals.append(f"MACD {'bullish' if macd_l.iloc[-1] > sig_l.iloc[-1] else 'bearish'} crossover")

    return {"symbol": symbol, "price": round(last, 2),
            "sma_20": round(sma20, 2), "sma_50": round(sma50, 2),
            "sma_200": round(sma200, 2) if sma200 else None,
            "ema_12": round(calc_ema(close, 12).iloc[-1], 2),
            "ema_26": round(calc_ema(close, 26).iloc[-1], 2),
            "rsi_14": round(rsi, 2), "macd": round(macd_l.iloc[-1], 4),
            "macd_signal": round(sig_l.iloc[-1], 4), "macd_histogram": round(hist.iloc[-1], 4),
            "bb_upper": round(bb_u.iloc[-1], 2), "bb_middle": round(bb_m.iloc[-1], 2),
            "bb_lower": round(bb_l.iloc[-1], 2), "signals": signals}


def format_analysis(d):
    lines = [f"**Technical Analysis: {d['symbol']}**\n", f"Price: {d['price']}",
             f"\n**Moving Averages**", f"  SMA(20): {d['sma_20']}  |  SMA(50): {d['sma_50']}"]
    if d['sma_200']: lines.append(f"  SMA(200): {d['sma_200']}")
    lines += [f"  EMA(12): {d['ema_12']}  |  EMA(26): {d['ema_26']}",
              f"\n**Oscillators**", f"  RSI(14): {d['rsi_14']}",
              f"  MACD: {d['macd']}  |  Signal: {d['macd_signal']}  |  Hist: {d['macd_histogram']}",
              f"\n**Bollinger Bands** (20,2)",
              f"  Upper: {d['bb_upper']}  |  Middle: {d['bb_middle']}  |  Lower: {d['bb_lower']}",
              f"\n**Signals**"] + [f"  • {s}" for s in d["signals"]]
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Technical analysis")
    parser.add_argument("symbol")
    parser.add_argument("--period", default="6mo")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    try:
        r = analyze(args.symbol, args.period)
        print(json.dumps(r, indent=2) if args.json else format_analysis(r))
    except Exception as e:
        print(f"Error: {e}"); sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/watchlist.py
content: #!/usr/bin/env python3
"""Watchlist management."""

import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from lib.db import get_connection
from lib.formatters import detect_asset_type, fmt_price
from scripts.quote import get_quote


def create_watchlist(name):
    conn = get_connection()
    try:
        conn.execute("INSERT INTO watchlists (name) VALUES (?)", (name,)); conn.commit()
        return f"Watchlist '{name}' created."
    except Exception as e:
        return f"Watchlist '{name}' already exists." if "UNIQUE" in str(e) else f"Error: {e}"
    finally: conn.close()


def delete_watchlist(name):
    conn = get_connection()
    try:
        wl = conn.execute("SELECT id FROM watchlists WHERE name=?", (name,)).fetchone()
        if not wl: return f"Watchlist '{name}' not found."
        conn.execute("DELETE FROM watchlist_items WHERE watchlist_id=?", (wl["id"],))
        conn.execute("DELETE FROM watchlists WHERE id=?", (wl["id"],)); conn.commit()
        return f"Watchlist '{name}' deleted."
    finally: conn.close()


def add_to_watchlist(name, symbol):
    symbol = symbol.upper()
    conn = get_connection()
    try:
        wl = conn.execute("SELECT id FROM watchlists WHERE name=?", (name,)).fetchone()
        if not wl:
            wl_id = conn.execute("INSERT INTO watchlists (name) VALUES (?)", (name,)).lastrowid
        else: wl_id = wl["id"]
        conn.execute("INSERT OR IGNORE INTO watchlist_items (watchlist_id, symbol, asset_type) VALUES (?,?,?)",
                     (wl_id, symbol, detect_asset_type(symbol))); conn.commit()
        return f"Added {symbol} to '{name}'."
    finally: conn.close()


def remove_from_watchlist(name, symbol):
    conn = get_connection()
    try:
        wl = conn.execute("SELECT id FROM watchlists WHERE name=?", (name,)).fetchone()
        if not wl: return f"Watchlist '{name}' not found."
        conn.execute("DELETE FROM watchlist_items WHERE watchlist_id=? AND symbol=?", (wl["id"], symbol.upper())); conn.commit()
        return f"Removed {symbol.upper()} from '{name}'."
    finally: conn.close()


def list_watchlists():
    conn = get_connection()
    try:
        wls = conn.execute("SELECT w.*, COUNT(wi.id) as count FROM watchlists w LEFT JOIN watchlist_items wi ON w.id=wi.watchlist_id GROUP BY w.id ORDER BY w.name").fetchall()
        if not wls: return "No watchlists."
        return "**Watchlists**\n\n" + "\n".join(f"  • {w['name']} ({w['count']} symbols)" for w in wls)
    finally: conn.close()


def show_watchlist(name, with_prices=False):
    conn = get_connection()
    try:
        wl = conn.execute("SELECT id FROM watchlists WHERE name=?", (name,)).fetchone()
        if not wl: return f"Watchlist '{name}' not found."
        items = conn.execute("SELECT * FROM watchlist_items WHERE watchlist_id=? ORDER BY symbol", (wl["id"],)).fetchall()
        if not items: return f"Watchlist '{name}' is empty."
        lines = [f"**Watchlist: {name}**\n"]
        for item in items:
            if with_prices:
                try:
                    q = get_quote(item["symbol"])
                    c = q.get("currency", "USD")
                    chg = f" ({'+' if q.get('change_pct', 0) >= 0 else ''}{q.get('change_pct', 0):.2f}%)" if q.get("change_pct") is not None else ""
                    lines.append(f"  {item['symbol']} — {fmt_price(q['price'], c)}{chg}")
                except: lines.append(f"  {item['symbol']} — _unavailable_")
            else: lines.append(f"  {item['symbol']} ({item['asset_type']})")
        return "\n".join(lines)
    finally: conn.close()


def main():
    parser = argparse.ArgumentParser(description="Watchlist management")
    parser.add_argument("action", choices=["create", "delete", "add", "remove", "list", "show"])
    parser.add_argument("--name", "-n"); parser.add_argument("--symbol", "-s")
    parser.add_argument("--prices", action="store_true")
    args = parser.parse_args()

    if args.action == "create":
        if not args.name: print("Error: --name required"); sys.exit(1)
        print(create_watchlist(args.name))
    elif args.action == "delete":
        if not args.name: print("Error: --name required"); sys.exit(1)
        print(delete_watchlist(args.name))
    elif args.action == "add":
        if not args.name or not args.symbol: print("Error: --name and --symbol required"); sys.exit(1)
        print(add_to_watchlist(args.name, args.symbol))
    elif args.action == "remove":
        if not args.name or not args.symbol: print("Error: --name and --symbol required"); sys.exit(1)
        print(remove_from_watchlist(args.name, args.symbol))
    elif args.action == "list": print(list_watchlists())
    elif args.action == "show":
        if not args.name: print("Error: --name required"); sys.exit(1)
        print(show_watchlist(args.name, args.prices))


if __name__ == "__main__":
    main()


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