Skill: temmyraharjo/yfinance (version 1.0.4)

--- name: yfinance description: > Fetch live and historical stock data from Yahoo Finance, optimised for Indonesia (IDX) stocks. Use this skill when the user asks about stock prices, historical charts, company fundamentals (P/E ratio, market cap, EPS, ROE), dividend history, or stock splits for any ticker — especially Indonesian stocks like BBCA, TLKM, BBRI, GOTO, or ASII. Tickers without a suffix are automatically treated as IDX stocks (appends .JK). Triggers: "harga saham", "stock price", "fundamental", "dividend", "PE ratio", "market cap", "historical data", "OHLCV", "IDX", "Bursa Efek Indonesia", ".JK". version: 1.0.0 homepage: https://github.com/your-username/openclaw-yfinance allowed-tools: ["bash", "exec"] metadata: openclaw: emoji: "📈" requires: bins: - curl - python3 env: [] install: - id: uv kind: uv package: yfinance bins: [] label: "Install yfinance via uv/pip" --- # YFinance Skill — Indonesia & Global Stock Data This skill fetches stock market data from Yahoo Finance via a local FastAPI server running at `http://localhost:8000`. The server must be running before using this skill. ## Starting the server If the server is not running yet, startup logic must: ```bash curl http://localhost:8000/ || (cd ~/.openclaw/workspace/skills/yfinance && uvicorn main:app --host 0.0.0.0 --port 8000 --reload &) ``` ``` pip install -r requirements.txt ``` Verify it is up: ```bash curl http://localhost:8000/ ``` ## Indonesia Stock Tickers Tickers without a dot suffix are **automatically** treated as IDX stocks by appending `.JK`. You do not need to add `.JK` yourself. | Short form | Resolved ticker | Company | |---|---|---| | BBCA | BBCA.JK | Bank Central Asia | | TLKM | TLKM.JK | Telkom Indonesia | | BBRI | BBRI.JK | Bank Rakyat Indonesia | | GOTO | GOTO.JK | GoTo Group | | ASII | ASII.JK | Astra International | For non-Indonesia stocks, always include the full suffix: `AAPL`, `MSFT`, `005930.KS`. Use `?auto_jk=false` to disable auto-append. ## Endpoints ### 1. Real-time Price ```bash curl "http://localhost:8000/price?ticker=BBCA" ``` --- ### 2. Historical OHLCV Data ```bash curl "http://localhost:8000/history?ticker=BBCA&period=1y&interval=1d" ``` --- ### 3. Company Fundamentals ```bash curl "http://localhost:8000/fundamentals?ticker=BBRI" ``` --- ### 4. Dividend History ```bash curl "http://localhost:8000/dividends?ticker=ASII" ``` --- ### 5. Stock Split History ```bash curl "http://localhost:8000/splits?ticker=BBCA" ``` --- ## Response Format Every endpoint returns JSON & formatted string. --- ## Rules - Always check API status before querying. - If server returns error, use startup logic above to launch. - Never fabricate or guess stock prices. Only report what the API returns. - If request in Bahasa Indonesia, respond in Bahasa.

path: README.md
content: # OpenClaw YFinance Connector

A FastAPI REST server that wraps `yfinance` to serve Yahoo Finance stock data,
**optimised for Indonesia (IDX) stocks** with full OpenClaw + ClawHub compatibility.

---

## Features

| Endpoint | Description |
|---|---|
| `GET /price` | Real-time / latest price, change %, day range, volume |
| `GET /history` | Historical OHLCV — daily, weekly, intraday |
| `GET /fundamentals` | P/E, P/B, EPS, Market Cap, ROE, ROA, Debt/Equity, … |
| `GET /dividends` | Full dividend payment history |
| `GET /splits` | Full stock split history |

Every endpoint returns **both** a structured `json` field and a human-readable `formatted` string.

---

## Indonesia Stock Support

Tickers without a dot suffix are **automatically** treated as IDX stocks by
appending `.JK`. You can disable this per-request with `?auto_jk=false`.

```
BBCA   →  BBCA.JK   (BCA)
TLKM   →  TLKM.JK   (Telkom Indonesia)
BBRI   →  BBRI.JK   (BRI)
GOTO   →  GOTO.JK   (GoTo Group)
ASII   →  ASII.JK   (Astra International)
```

For non-Indonesia stocks, pass the full ticker: `AAPL`, `MSFT`, `005930.KS`

---

## File Structure

```
yfinance/
├── SKILL.md          ← OpenClaw skill definition (required for ClawHub)
├── main.py           ← FastAPI server
├── openapi.json      ← OpenClaw connector spec
├── requirements.txt  ← Python dependencies
└── README.md         ← This file
```

---

## Quick Start

### 1. Install dependencies

```bash
pip install -r requirements.txt
```

### 2. Run the server

```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```

### 3. Interactive docs

Open `http://localhost:8000/docs` for Swagger UI.

---

## OpenClaw Setup (Skill)

### Install as a local skill

```bash
# Copy the skill folder to your OpenClaw workspace
cp -r ./yfinance ~/.openclaw/workspace/skills/yfinance

# Sync with ClawHub (if logged in)
clawhub sync
```

### Install from ClawHub (once published)

```bash
clawhub install yfinance
```

### Verify skill is loaded

In your OpenClaw Telegram bot, ask:
```
What is the current price of BBCA?
```

---

## OpenClaw Setup (Connector / Tool)

If you prefer to register this as an OpenClaw **connector** via `openapi.json`:

1. Update the server URL in `openapi.json`:
```json
"servers": [
  {
    "url": "http://YOUR_VM_PUBLIC_IP:8000"
  }
]
```

2. Import `openapi.json` in the OpenClaw web UI under **Connectors → Add**

3. Make sure **port 8000** is open in your Azure VM Network Security Group (NSG)

---

## Example Requests

### Current price — BBCA (BCA)
```
GET /price?ticker=BBCA
```

### 1-year daily history — Telkom
```
GET /history?ticker=TLKM&period=1y&interval=1d
```

### Custom date range — GOTO
```
GET /history?ticker=GOTO&start=2024-01-01&end=2024-12-31
```

### Fundamentals — BBRI
```
GET /fundamentals?ticker=BBRI
```

### Dividends — ASII
```
GET /dividends?ticker=ASII
```

### Global stock (no .JK auto-append)
```
GET /price?ticker=AAPL&auto_jk=false
```

---

## Response Shape

Every endpoint returns:

```json
{
  "json": { "ticker": "BBCA.JK", "price": 9500.0, "change_pct": 1.23 },
  "formatted": "─────────────────────────────\n  Bank Central Asia (BBCA.JK)\n  IDR  9,500.00  ▲ +1.23%\n  ..."
}
```

---

## Publishing to ClawHub

```bash
# Login with GitHub account (at least 1 week old)
clawhub login

# Publish the skill
clawhub publish ~/.openclaw/workspace/skills/yfinance \
  --slug yfinance \
  --name "YFinance — Indonesia & Global Stock Data" \
  --version 1.0.0 \
  --tags latest
```

> The `--slug` must be unique across the entire ClawHub registry.

---

## Deployment (Azure VM)

### Run as background process

```bash
uvicorn main:app --host 0.0.0.0 --port 8000 &
curl http://localhost:8000/
```

### Keep alive with systemd

```bash
sudo nano /etc/systemd/system/yfinance.service
```

```ini
[Unit]
Description=YFinance FastAPI Server
After=network.target

[Service]
User=azureuser
WorkingDirectory=/home/azureuser/yfinance-connector
ExecStart=uvicorn main:app --host 0.0.0.0 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl enable yfinance
sudo systemctl start yfinance
sudo systemctl status yfinance
```

### Run with Docker

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```

---

## Troubleshooting

| Problem | Fix |
|---|---|
| `Connection refused` on port 8000 | Start the uvicorn server |
| OpenClaw can't reach the API | Use VM public IP, not `localhost`, in `openapi.json` |
| Ticker returns no data | Verify ticker on finance.yahoo.com |
| Stale/wrong price | Yahoo Finance delays IDX data ~15 minutes |
| `Permission denied` on skill folder | `chmod -R 755 ~/.openclaw/workspace/skills/yfinance` |
| Skill not picked up by OpenClaw | Restart OpenClaw — skills load at session start |

---

## Notes

- Yahoo Finance data is **delayed ~15 minutes** for most markets including IDX.
- Intraday intervals (`1m`, `5m`, etc.) are only available for the last 7–60 days.
- `yfinance` availability depends on Yahoo Finance's servers; rate limiting may apply.
- OpenClaw picks up workspace skills on the **next session** after install.


path: _meta.json
content: ownerId: kn7fcm45fxfv7qx5fjvdztwaqh83x6we
slug: yfinance
version: 1.0.3
publishedAt: 1777134841326

path: main.py
content: """
OpenClaw YFinance Connector
FastAPI REST server to fetch Yahoo Finance stock data,
optimised for Indonesia stocks (suffix: .JK)
"""

from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import yfinance as yf
from datetime import datetime, date
from typing import Optional
import pandas as pd

app = FastAPI(
    title="OpenClaw YFinance Connector",
    description="Fetch Yahoo Finance stock data for Indonesia (IDX) and global markets.",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# ─── Helpers ─────────────────────────────────────────────────────────────────

def normalize_ticker(ticker: str) -> str:
    """Auto-append .JK for Indonesia tickers if no suffix present."""
    t = ticker.strip().upper()
    if "." not in t:
        t = f"{t}.JK"
    return t

def df_to_records(df: pd.DataFrame) -> list:
    """Convert DataFrame to list of dicts with ISO date strings."""
    df = df.copy()
    df.index = df.index.strftime("%Y-%m-%d")
    return df.reset_index().rename(columns={"index": "date", "Date": "date"}).to_dict(orient="records")

def format_number(value) -> str:
    """Human-readable large number formatting."""
    if value is None or (isinstance(value, float) and pd.isna(value)):
        return "N/A"
    v = float(value)
    if abs(v) >= 1e12:
        return f"{v/1e12:.2f}T"
    if abs(v) >= 1e9:
        return f"{v/1e9:.2f}B"
    if abs(v) >= 1e6:
        return f"{v/1e6:.2f}M"
    return f"{v:,.2f}"

def safe(value, default=None):
    try:
        if pd.isna(value):
            return default
    except Exception:
        pass
    return value


# ─── Routes ──────────────────────────────────────────────────────────────────

@app.get("/", tags=["Health"])
def root():
    return {
        "service": "OpenClaw YFinance Connector",
        "status": "ok",
        "note": "Append .JK for Indonesia stocks, e.g. BBCA.JK",
        "endpoints": ["/price", "/history", "/fundamentals", "/dividends", "/splits"],
    }


@app.get("/price", tags=["Price"], summary="Real-time / latest stock price")
def get_price(
    ticker: str = Query(..., description="Stock ticker. Omit .JK for IDX stocks, e.g. BBCA"),
    auto_jk: bool = Query(True, description="Auto-append .JK if no suffix"),
):
    """
    Returns the latest market price, day range, volume and change %.
    Both JSON and formatted text are returned.
    """
    sym = normalize_ticker(ticker) if auto_jk else ticker.upper()
    try:
        stock = yf.Ticker(sym)
        info = stock.info
        fast = stock.fast_info

        price = safe(fast.last_price) or safe(info.get("currentPrice")) or safe(info.get("regularMarketPrice"))
        prev_close = safe(fast.previous_close) or safe(info.get("previousClose"))
        change = round(price - prev_close, 4) if price and prev_close else None
        change_pct = round((change / prev_close) * 100, 2) if change and prev_close else None

        data = {
            "ticker": sym,
            "name": info.get("longName") or info.get("shortName", sym),
            "currency": info.get("currency", "IDR"),
            "exchange": info.get("exchange", "N/A"),
            "price": price,
            "previous_close": prev_close,
            "change": change,
            "change_pct": change_pct,
            "day_high": safe(fast.day_high),
            "day_low": safe(fast.day_low),
            "volume": safe(fast.last_volume),
            "market_cap": safe(fast.market_cap),
            "timestamp": datetime.utcnow().isoformat() + "Z",
        }

        arrow = "▲" if (change_pct or 0) >= 0 else "▼"
        formatted = (
            f"{'─'*45}\n"
            f"  {data['name']} ({sym})\n"
            f"  {data['currency']}  {price:,.2f}  {arrow} {change_pct:+.2f}%\n"
            f"  Day: {safe(fast.day_low, 'N/A'):,} – {safe(fast.day_high, 'N/A'):,}\n"
            f"  Prev Close: {prev_close:,.2f}   Vol: {format_number(safe(fast.last_volume))}\n"
            f"{'─'*45}"
        )

        return {"json": data, "formatted": formatted}

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/history", tags=["History"], summary="Historical OHLCV price data")
def get_history(
    ticker: str = Query(..., description="Stock ticker"),
    period: Optional[str] = Query("1mo", description="Period: 1d 5d 1mo 3mo 6mo 1y 2y 5y 10y ytd max"),
    start: Optional[date] = Query(None, description="Start date YYYY-MM-DD (overrides period)"),
    end: Optional[date] = Query(None, description="End date YYYY-MM-DD"),
    interval: str = Query("1d", description="Interval: 1m 5m 15m 30m 1h 1d 1wk 1mo"),
    auto_jk: bool = Query(True),
):
    """
    Returns OHLCV history. Use `period` OR `start`/`end` pair.
    """
    sym = normalize_ticker(ticker) if auto_jk else ticker.upper()
    try:
        stock = yf.Ticker(sym)
        if start:
            df = stock.history(start=str(start), end=str(end) if end else None, interval=interval)
        else:
            df = stock.history(period=period, interval=interval)

        if df.empty:
            raise HTTPException(status_code=404, detail=f"No data found for {sym}")

        df = df[["Open", "High", "Low", "Close", "Volume"]]
        records = df_to_records(df)

        # Simple text table (last 5 rows preview)
        preview = df.tail(5)
        lines = [f"{'─'*65}", f"  {sym} — Last {len(df)} candles ({interval})", f"{'─'*65}",
                 f"  {'Date':<12} {'Open':>10} {'High':>10} {'Low':>10} {'Close':>10} {'Volume':>12}",
                 f"  {'─'*63}"]
        for idx, row in preview.iterrows():
            d = idx.strftime("%Y-%m-%d") if hasattr(idx, "strftime") else str(idx)[:10]
            lines.append(f"  {d:<12} {row['Open']:>10,.2f} {row['High']:>10,.2f} {row['Low']:>10,.2f} {row['Close']:>10,.2f} {format_number(row['Volume']):>12}")
        lines.append(f"{'─'*65}")
        formatted = "\n".join(lines)

        return {"json": {"ticker": sym, "interval": interval, "count": len(records), "records": records},
                "formatted": formatted}

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/fundamentals", tags=["Fundamentals"], summary="Company fundamentals & ratios")
def get_fundamentals(
    ticker: str = Query(..., description="Stock ticker"),
    auto_jk: bool = Query(True),
):
    """
    Returns key financial fundamentals: P/E, P/B, EPS, market cap, revenue, etc.
    """
    sym = normalize_ticker(ticker) if auto_jk else ticker.upper()
    try:
        info = yf.Ticker(sym).info

        fields = {
            "longName": "name",
            "exchange": "exchange",
            "currency": "currency",
            "sector": "sector",
            "industry": "industry",
            "marketCap": "market_cap",
            "trailingPE": "pe_ratio_ttm",
            "forwardPE": "pe_ratio_forward",
            "priceToBook": "pb_ratio",
            "trailingEps": "eps_ttm",
            "forwardEps": "eps_forward",
            "dividendYield": "dividend_yield",
            "payoutRatio": "payout_ratio",
            "returnOnEquity": "roe",
            "returnOnAssets": "roa",
            "debtToEquity": "debt_to_equity",
            "totalRevenue": "revenue",
            "grossProfits": "gross_profit",
            "ebitda": "ebitda",
            "freeCashflow": "free_cashflow",
            "52WeekChange": "52w_change",
            "fiftyTwoWeekHigh": "52w_high",
            "fiftyTwoWeekLow": "52w_low",
            "beta": "beta",
            "sharesOutstanding": "shares_outstanding",
            "bookValue": "book_value_per_share",
        }

        data = {"ticker": sym}
        for src, dst in fields.items():
            data[dst] = safe(info.get(src))

        # Percent formatting
        for pct_key in ["dividend_yield", "payout_ratio", "roe", "roa", "52w_change"]:
            v = data.get(pct_key)
            if v is not None:
                data[pct_key] = round(v * 100, 2)

        # Formatted text
        lines = [
            f"{'═'*50}",
            f"  {data.get('name', sym)} ({sym})",
            f"  {data.get('sector','N/A')} › {data.get('industry','N/A')}",
            f"{'─'*50}",
            f"  Market Cap        : {format_number(data.get('market_cap'))} {data.get('currency','')}",
            f"  P/E (TTM)         : {data.get('pe_ratio_ttm','N/A')}",
            f"  P/E (Forward)     : {data.get('pe_ratio_forward','N/A')}",
            f"  P/B               : {data.get('pb_ratio','N/A')}",
            f"  EPS (TTM)         : {data.get('eps_ttm','N/A')}",
            f"  Dividend Yield    : {data.get('dividend_yield','N/A')}%",
            f"  ROE               : {data.get('roe','N/A')}%",
            f"  ROA               : {data.get('roa','N/A')}%",
            f"  Debt/Equity       : {data.get('debt_to_equity','N/A')}",
            f"  Revenue           : {format_number(data.get('revenue'))} {data.get('currency','')}",
            f"  Free Cash Flow    : {format_number(data.get('free_cashflow'))} {data.get('currency','')}",
            f"  Beta              : {data.get('beta','N/A')}",
            f"  52W High/Low      : {data.get('52w_high','N/A')} / {data.get('52w_low','N/A')}",
            f"{'═'*50}",
        ]
        formatted = "\n".join(lines)

        return {"json": data, "formatted": formatted}

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/dividends", tags=["Corporate Actions"], summary="Dividend history")
def get_dividends(
    ticker: str = Query(..., description="Stock ticker"),
    auto_jk: bool = Query(True),
):
    """Returns full dividend payment history."""
    sym = normalize_ticker(ticker) if auto_jk else ticker.upper()
    try:
        stock = yf.Ticker(sym)
        divs = stock.dividends

        if divs.empty:
            return {"json": {"ticker": sym, "count": 0, "records": []},
                    "formatted": f"No dividend history found for {sym}."}

        divs.index = divs.index.strftime("%Y-%m-%d")
        records = [{"date": d, "dividend": round(v, 6)} for d, v in divs.items()]

        lines = [f"{'─'*35}", f"  {sym} — Dividend History ({len(records)} payments)", f"{'─'*35}"]
        for r in records[-10:]:
            lines.append(f"  {r['date']}    {r['dividend']:>12,.4f}")
        if len(records) > 10:
            lines.append(f"  ... and {len(records)-10} more earlier payments")
        lines.append(f"{'─'*35}")

        return {"json": {"ticker": sym, "count": len(records), "records": records},
                "formatted": "\n".join(lines)}

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.get("/splits", tags=["Corporate Actions"], summary="Stock split history")
def get_splits(
    ticker: str = Query(..., description="Stock ticker"),
    auto_jk: bool = Query(True),
):
    """Returns full stock split history."""
    sym = normalize_ticker(ticker) if auto_jk else ticker.upper()
    try:
        stock = yf.Ticker(sym)
        splits = stock.splits

        if splits.empty:
            return {"json": {"ticker": sym, "count": 0, "records": []},
                    "formatted": f"No split history found for {sym}."}

        splits.index = splits.index.strftime("%Y-%m-%d")
        records = [{"date": d, "ratio": v} for d, v in splits.items()]

        lines = [f"{'─'*35}", f"  {sym} — Split History", f"{'─'*35}"]
        for r in records:
            lines.append(f"  {r['date']}    {r['ratio']:.2f}:1 split")
        lines.append(f"{'─'*35}")

        return {"json": {"ticker": sym, "count": len(records), "records": records},
                "formatted": "\n".join(lines)}

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


path: openapi.json
content: {
  "openapi": "3.1.0",
  "info": {
    "title": "OpenClaw YFinance Connector",
    "description": "Fetch Yahoo Finance stock data for Indonesia (IDX .JK) and global markets. All tickers without a suffix are automatically treated as IDX stocks by appending .JK.",
    "version": "1.0.0",
    "contact": {
      "name": "OpenClaw YFinance Connector"
    }
  },
  "servers": [
    {
      "url": "http://localhost:8000",
      "description": "Local development server"
    }
  ],
  "paths": {
    "/price": {
      "get": {
        "operationId": "getStockPrice",
        "summary": "Get real-time / latest stock price",
        "description": "Returns the latest market price, change %, day range, and volume for a given ticker.",
        "tags": ["Price"],
        "parameters": [
          {
            "name": "ticker",
            "in": "query",
            "required": true,
            "description": "Stock ticker symbol. For IDX stocks omit .JK suffix, e.g. BBCA",
            "schema": { "type": "string", "example": "BBCA" }
          },
          {
            "name": "auto_jk",
            "in": "query",
            "required": false,
            "description": "Auto-append .JK for Indonesia stocks if no dot suffix present",
            "schema": { "type": "boolean", "default": true }
          }
        ],
        "responses": {
          "200": {
            "description": "Price data returned",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DualResponse" }
              }
            }
          }
        }
      }
    },
    "/history": {
      "get": {
        "operationId": "getStockHistory",
        "summary": "Get historical OHLCV data",
        "description": "Returns historical open/high/low/close/volume data. Use period OR start+end.",
        "tags": ["History"],
        "parameters": [
          {
            "name": "ticker",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "example": "BBCA" }
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "One of: 1d 5d 1mo 3mo 6mo 1y 2y 5y 10y ytd max",
            "schema": { "type": "string", "default": "1mo" }
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "description": "Start date YYYY-MM-DD (overrides period)",
            "schema": { "type": "string", "format": "date" }
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "description": "End date YYYY-MM-DD",
            "schema": { "type": "string", "format": "date" }
          },
          {
            "name": "interval",
            "in": "query",
            "required": false,
            "description": "Data interval: 1m 5m 15m 30m 1h 1d 1wk 1mo",
            "schema": { "type": "string", "default": "1d" }
          },
          {
            "name": "auto_jk",
            "in": "query",
            "required": false,
            "schema": { "type": "boolean", "default": true }
          }
        ],
        "responses": {
          "200": {
            "description": "Historical OHLCV data",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DualResponse" }
              }
            }
          }
        }
      }
    },
    "/fundamentals": {
      "get": {
        "operationId": "getStockFundamentals",
        "summary": "Get company fundamentals and financial ratios",
        "description": "Returns P/E, P/B, EPS, market cap, ROE, ROA, dividends, and more.",
        "tags": ["Fundamentals"],
        "parameters": [
          {
            "name": "ticker",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "example": "BBCA" }
          },
          {
            "name": "auto_jk",
            "in": "query",
            "required": false,
            "schema": { "type": "boolean", "default": true }
          }
        ],
        "responses": {
          "200": {
            "description": "Fundamentals returned",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DualResponse" }
              }
            }
          }
        }
      }
    },
    "/dividends": {
      "get": {
        "operationId": "getDividendHistory",
        "summary": "Get dividend payment history",
        "tags": ["Corporate Actions"],
        "parameters": [
          {
            "name": "ticker",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "example": "BBCA" }
          },
          {
            "name": "auto_jk",
            "in": "query",
            "required": false,
            "schema": { "type": "boolean", "default": true }
          }
        ],
        "responses": {
          "200": {
            "description": "Dividend history returned",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DualResponse" }
              }
            }
          }
        }
      }
    },
    "/splits": {
      "get": {
        "operationId": "getSplitHistory",
        "summary": "Get stock split history",
        "tags": ["Corporate Actions"],
        "parameters": [
          {
            "name": "ticker",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "example": "BBCA" }
          },
          {
            "name": "auto_jk",
            "in": "query",
            "required": false,
            "schema": { "type": "boolean", "default": true }
          }
        ],
        "responses": {
          "200": {
            "description": "Split history returned",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DualResponse" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DualResponse": {
        "type": "object",
        "description": "Every endpoint returns both structured JSON data and a human-readable formatted string.",
        "properties": {
          "json": {
            "type": "object",
            "description": "Structured data for programmatic use"
          },
          "formatted": {
            "type": "string",
            "description": "Human-readable text/table representation"
          }
        }
      }
    }
  }
}


path: requirements.txt
content: fastapi>=0.110.0
uvicorn[standard]>=0.29.0
yfinance>=0.2.40
pandas>=2.0.0


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