# پایتون و pandas

> API روشا در پایتون با requests و pandas؛ تابع کمکی با مدیریت 429، سابقهٔ روزانه در DataFrame، قیمت تعدیل‌شده، صفحه‌بندی کل سابقه، NAV صندوق و بازده.
> Source: https://poolick.ir/docs/python/

برای کار با روشا در پایتون به `requests` و `pandas` نیاز دارید و یک تابع `get` که کلید را از `ROSHA_KEY` می‌خواند و برای `429` به اندازهٔ `Retry-After` صبر می‌کند. هر نمونهٔ این صفحه یک برنامهٔ کامل است و جدا اجرا می‌شود.

## پیش‌نیاز

پایتون ۳٫۹ یا بالاتر، دو کتابخانه و کلید در متغیر محیطی:

```bash
pip install requests pandas
export ROSHA_KEY="rosha_xxxxxxxx_your-secret"
```

اگر کلید ندارید، [شروع سریع](/docs/quickstart/) را ببینید.

## تابع کمکی `get` چیست؟

`get` یک درخواست `GET` می‌فرستد، اگر `429` گرفت به اندازهٔ `Retry-After` صبر می‌کند و یک بار دیگر می‌فرستد، و برای هر خطای دیگر استثنا می‌دهد. همهٔ نمونه‌های پایتون این مستندات همین تابع را به کار می‌برند.

```python

BASE = "https://poolick.ir"
HEADERS = {"Authorization": f"Bearer {os.environ['ROSHA_KEY']}"}

def get(path, **kwargs):
    """GET with one retry on 429, waiting as long as Retry-After says."""
    for _ in range(2):
        r = requests.get(BASE + path, headers=HEADERS, timeout=30, **kwargs)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", "1")))
    r.raise_for_status()
    return r.json()

print(get("/v1/instruments", params={"q": "فولاد", "limit": 3}))
```

`requests` نماد فارسی را در مسیر و پارامترها خودش URL-encode می‌کند. معنای هر کد خطا در [خطاها](/docs/errors/) است.

## چطور سابقهٔ روزانه را در DataFrame بریزم؟

`rows` پاسخ را به `pd.DataFrame` بدهید و ستون `d` را ایندکس تاریخ کنید. این برنامه ۲۵۰ روز معاملاتی اخیر فولاد را با قیمت تعدیل‌شده می‌گیرد:

```python

BASE = "https://poolick.ir"
HEADERS = {"Authorization": f"Bearer {os.environ['ROSHA_KEY']}"}

def get(path, **kwargs):
    """GET with one retry on 429, waiting as long as Retry-After says."""
    for _ in range(2):
        r = requests.get(BASE + path, headers=HEADERS, timeout=30, **kwargs)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", "1")))
    r.raise_for_status()
    return r.json()

data = get(
    "/v1/instruments/فولاد/daily",
    params={"adjusted": "true", "limit": 250},
)
df = pd.DataFrame(data["rows"]).set_index("d")
df.index = pd.to_datetime(df.index)
print(data["ins_code"], data["ticker"])  # ins_code is a string: keep it that way
print(df[["close", "volume"]].tail())
```

ستون‌های DataFrame همان فیلدهای [سابقهٔ روزانه](/docs/api/daily/) هستند: `open`، `high`، `low`، `close`، `last`، `prev_close`، `volume`، `value`، `trades` و `jdate`. قیمت‌ها به ریال است.

## قیمت تعدیل‌شده چه فرقی با قیمت خام دارد؟

قیمت تعدیل‌شده اثر افزایش سرمایه و سود نقدی را از سری قیمت برمی‌دارد؛ قیمت خام همان عددی است که روی تابلو بوده است. این برنامه هر دو را برای یک بازه می‌گیرد و کنار هم می‌گذارد:

```python

BASE = "https://poolick.ir"
HEADERS = {"Authorization": f"Bearer {os.environ['ROSHA_KEY']}"}

def get(path, **kwargs):
    """GET with one retry on 429, waiting as long as Retry-After says."""
    for _ in range(2):
        r = requests.get(BASE + path, headers=HEADERS, timeout=30, **kwargs)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", "1")))
    r.raise_for_status()
    return r.json()

def closes(ref, adjusted, **params):
    """Closing prices of one instrument as a Series indexed by date."""
    params["adjusted"] = "true" if adjusted else "false"
    rows = get(f"/v1/instruments/{ref}/daily", params=params)["rows"]
    s = pd.Series({r["d"]: r["close"] for r in rows}, dtype="float64")
    s.index = pd.to_datetime(s.index)
    return s

REF = "46348559193224090"  # فولاد
both = pd.DataFrame({
    "raw": closes(REF, False, start="2024-07-10", end="2024-07-31"),
    "adjusted": closes(REF, True, start="2024-07-10", end="2024-07-31"),
})
both["factor"] = (both["adjusted"] / both["raw"]).round(4)
print(both)
```

در خروجی، ستون `factor` تا خودِ روز رویداد `2024-07-21` برابر `0.8151` و از `2024-07-22` برابر `0.8830` است. نماد در روز رویداد متوقف بوده و هنوز قیمت قدیم را نشان می‌دهد، پس آن روز هم تعدیل می‌شود. برای محاسبهٔ بازده و اندیکاتور از قیمت تعدیل‌شده استفاده کنید و برای مقایسه با تابلوی همان روز از قیمت خام. قاعدهٔ تعدیل در [تعدیل قیمت](/docs/api/adjustments/) آمده است.

## چطور کل سابقهٔ یک نماد را بگیرم؟

هر پاسخ حداکثر ۵٬۰۰۰ ردیف دارد؛ برای سابقهٔ بلندتر با تاریخ صفحه‌بندی کنید. هر بار `end` را روزِ قبل از قدیمی‌ترین ردیف گرفته‌شده بگذارید و وقتی تعداد ردیف‌ها از اندازهٔ صفحه کمتر شد بایستید.

```python

from datetime import date, timedelta

BASE = "https://poolick.ir"
HEADERS = {"Authorization": f"Bearer {os.environ['ROSHA_KEY']}"}
PAGE = 5000  # the REST cap on `limit`

def get(path, **kwargs):
    """GET with one retry on 429, waiting as long as Retry-After says."""
    for _ in range(2):
        r = requests.get(BASE + path, headers=HEADERS, timeout=30, **kwargs)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", "1")))
    r.raise_for_status()
    return r.json()

def full_daily(ref, adjusted=True):
    """Every daily row of one instrument, oldest first, paged by date."""
    rows, end = [], None
    while True:
        params = {"limit": PAGE, "adjusted": "true" if adjusted else "false"}
        if end:
            params["end"] = end
        batch = get(f"/v1/instruments/{ref}/daily", params=params)["rows"]
        rows = batch + rows  # each page is older than the one before
        if len(batch) < PAGE:
            return rows
        oldest = date.fromisoformat(batch[0]["d"])
        end = (oldest - timedelta(days=1)).isoformat()

rows = full_daily("46348559193224090")  # فولاد
df = pd.DataFrame(rows).set_index("d")
df.index = pd.to_datetime(df.index)
print(len(df), "rows from", df.index[0].date(), "to", df.index[-1].date())
```

هر صفحه یک درخواست است. فولاد از اسفند ۱۳۸۵ تا آبان ۱۴۰۵ در مجموع ۴٬۶۸۰ ردیف دارد و با یک درخواست تمام می‌شود؛ حلقه برای نمادهایی است که بیش از ۵٬۰۰۰ روز سابقه دارند. همین الگو برای `/client-type`، `/v1/indices/{ref}/daily` و `/v1/funds/{ref}/history` هم کار می‌کند.

## چطور NAV یک صندوق را بگیرم؟

`/v1/funds/{ref}/history` سابقهٔ روزانهٔ صندوق را می‌دهد. `ref` می‌تواند شمارهٔ ثبت یا نماد صندوق باشد. این برنامه NAV ابطال و خالص دارایی ۹۰ روز اخیر یک صندوق را می‌گیرد:

```python

BASE = "https://poolick.ir"
HEADERS = {"Authorization": f"Bearer {os.environ['ROSHA_KEY']}"}

def get(path, **kwargs):
    """GET with one retry on 429, waiting as long as Retry-After says."""
    for _ in range(2):
        r = requests.get(BASE + path, headers=HEADERS, timeout=30, **kwargs)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", "1")))
    r.raise_for_status()
    return r.json()

data = get("/v1/funds/10589/history", params={"limit": 90})
nav = pd.DataFrame(data["rows"]).set_index("d")
nav.index = pd.to_datetime(nav.index)
print(data["reg_no"])
print(nav[["cancel_nav", "issue_nav", "net_asset", "source"]].tail())
```

`cancel_nav`، `issue_nav` و `net_asset` به ریال است. برای صندوق چتری این سابقه در سطح شمارهٔ ثبت است و NAV هیچ زیرصندوقی نیست؛ [صندوق‌ها](/docs/api/funds/) را ببینید.

## چطور بازده را حساب کنم؟

بازده را از قیمت پایانی **تعدیل‌شده** حساب کنید؛ با قیمت خام، روز افزایش سرمایه مثل افت قیمت دیده می‌شود. این برنامه بازده روزانه، بازده کل بازه و بیشترین افت از قله را برای ۲۵۰ روز معاملاتی اخیر حساب می‌کند:

```python

BASE = "https://poolick.ir"
HEADERS = {"Authorization": f"Bearer {os.environ['ROSHA_KEY']}"}

def get(path, **kwargs):
    """GET with one retry on 429, waiting as long as Retry-After says."""
    for _ in range(2):
        r = requests.get(BASE + path, headers=HEADERS, timeout=30, **kwargs)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", "1")))
    r.raise_for_status()
    return r.json()

rows = get(
    "/v1/instruments/46348559193224090/daily",  # فولاد
    params={"adjusted": "true", "limit": 250},
)["rows"]
close = pd.Series({r["d"]: r["close"] for r in rows}, dtype="float64")
close.index = pd.to_datetime(close.index)

daily_return = close.pct_change().dropna()
total_return = close.iloc[-1] / close.iloc[0] - 1
drawdown = close / close.cummax() - 1

print(f"period: {close.index[0].date()} to {close.index[-1].date()}")
print(f"total return: {total_return:.2%}")
print(f"mean daily return: {daily_return.mean():.3%}")
print(f"daily volatility: {daily_return.std():.3%}")
print(f"max drawdown: {drawdown.min():.2%}")
```

این اعداد توصیف گذشتهٔ قیمت‌اند و توصیهٔ سرمایه‌گذاری نیستند.

## نکته‌های pandas

- **`ins_code`:** رشته است. اگر آن را در ستون می‌گذارید، نوع ستون را `str` نگه دارید و به `int64` یا `float` تبدیل نکنید.
- **`null`:** در روزهای بدون معامله `open`، `high` و `low` برابر `None` است و در DataFrame به `NaN` تبدیل می‌شود.
- **ترتیب:** ردیف‌ها از قدیم به جدید می‌آیند؛ نیازی به `sort_index` نیست.
- **سهمیه:** هر فراخوانی `get` یک درخواست از سهمیهٔ روزانه کم می‌کند. با پلن رایگان (تا آبان ۱۴۰۵: ۱٬۰۰۰ درخواست در روز) برای حلقه روی نمادهای زیاد، نتیجه را روی دیسک نگه دارید. روش در [همگام‌سازی روزانه](/docs/guides/daily-sync/).
