رفتن به محتوا

پایتون و pandas

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

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

Terminal window
pip install requests pandas
export ROSHA_KEY="rosha_xxxxxxxx_your-secret"

اگر کلید ندارید، شروع سریع را ببینید.

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

import os
import time
import requests
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 می‌کند. معنای هر کد خطا در خطاها است.

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

Section titled “چطور سابقهٔ روزانه را در DataFrame بریزم؟”

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

import os
import time
import pandas as pd
import requests
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 همان فیلدهای سابقهٔ روزانه هستند: open، high، low، close، last، prev_close، volume، value، trades و jdate. قیمت‌ها به ریال است.

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

Section titled “قیمت تعدیل‌شده چه فرقی با قیمت خام دارد؟”

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

import os
import time
import pandas as pd
import requests
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 است. نماد در روز رویداد متوقف بوده و هنوز قیمت قدیم را نشان می‌دهد، پس آن روز هم تعدیل می‌شود. برای محاسبهٔ بازده و اندیکاتور از قیمت تعدیل‌شده استفاده کنید و برای مقایسه با تابلوی همان روز از قیمت خام. قاعدهٔ تعدیل در تعدیل قیمت آمده است.

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

Section titled “چطور کل سابقهٔ یک نماد را بگیرم؟”

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

import os
import time
from datetime import date, timedelta
import pandas as pd
import requests
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 یک صندوق را بگیرم؟

Section titled “چطور NAV یک صندوق را بگیرم؟”

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

import os
import time
import pandas as pd
import requests
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 هیچ زیرصندوقی نیست؛ صندوق‌ها را ببینید.

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

Section titled “چطور بازده را حساب کنم؟”

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

import os
import time
import pandas as pd
import requests
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%}")

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

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

صفحهٔ اصلی · قیمت‌ها · وضعیت · پنل · پشتیبانی
داده‌ها توصیهٔ سرمایه‌گذاری نیست. سلب مسئولیت