دریافت دیتای بورس با پایتون: از اسکرپینگ TSETMC تا API

تیم روشا · ۲۳ آبان ۱۴۰۵

برای دریافت دیتای بورس با پایتون دو راه دارید: خواندن مستقیم از TSETMC با اسکرپر خودتان، یا گرفتن دادهٔ آماده از یک API. با روشا، سابقهٔ روزانهٔ تعدیل‌شدهٔ یک نماد با یک درخواست HTTP و حدود ۲۰ خط کد در یک DataFrame قرار می‌گیرد.

این نوشته برای کسی است که اسکرپر دارد یا می‌خواهد بنویسد. اول می‌گوییم نگهداری اسکرپر چه کارهایی دارد، بعد همان کار را با API انجام می‌دهیم.

نگهداری اسکرپر TSETMC چه کارهایی دارد؟

نوشتن اولین نسخهٔ اسکرپر یک بعدازظهر وقت می‌گیرد. کار اصلی پس از آن شروع می‌شود:

  • تغییر منبع. اسکرپر به نشانی‌ها و قالب پاسخ سایت منبع وابسته است. هر تغییر در آن‌ها کد شما را از کار می‌اندازد و باید همان روز اصلاح شود.
  • یافتن insCode. TSETMC هر نماد را با یک کد عددی بلند می‌شناسد. نمادها تغییر نام می‌دهند و یک نماد ممکن است چند کد قدیمی و جدید داشته باشد.
  • تعدیل قیمت. سابقهٔ خام در افزایش سرمایه و سود نقدی می‌پرد. باید رویدادها را جدا بگیرید و ضریب‌ها را خودتان اعمال کنید.
  • یکسان‌سازی متن. نام‌ها با حروف «ی» و «ک» عربی می‌آیند و جست‌وجو با شکل فارسی این دو حرف چیزی پیدا نمی‌کند.
  • تاریخ و واحد. تاریخ‌ها عدد هشت‌رقمی هستند، ساعت‌ها عدد شش‌رقمی و قیمت‌ها ریال.
  • رفتار مؤدبانه با منبع. باید سرعت درخواست‌ها را محدود کنید، خطاها را دوباره امتحان کنید و در ساعت بازار فشار نیاورید.
  • صندوق‌ها. دادهٔ NAV از منبع دیگری (فیپیران) می‌آید و اسکرپر دوم می‌خواهد.

هیچ‌کدام از این‌ها سخت نیست. اما همه با هم یعنی یک سرویس کوچک که کسی باید همیشه مراقبش باشد.

کتابخانه‌های متن‌باز چطور؟

کتابخانه‌های متن‌باز پایتون مثل pytse-client و finpy-tse سال‌هاست کار با دادهٔ بازار ایران را برای جامعهٔ توسعه‌دهندگان ساده کرده‌اند. برای یادگیری، پروژهٔ شخصی و تحلیل یک‌باره انتخاب خوبی هستند و مستندات هر کدام در مخزن خودشان است.

روشا کار دیگری می‌کند. روشا یک سرویس میزبانی‌شده است: داده را خودمان هر روز جمع می‌کنیم، بررسی می‌کنیم و در پایگاه داده نگه می‌داریم. شما از سرور ما می‌خوانید، نه از سایت منبع. اگر منبع تغییر کند، اصلاح آن کار ماست و قرارداد /v1 برای شما ثابت می‌ماند.

همان کار با API روشا

یک کلید رایگان بگیرید و آن را در ROSHA_KEY بگذارید. بسته‌ها را نصب کنید:

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

کد زیر نماد را جست‌وجو می‌کند، کل سابقهٔ روزانهٔ تعدیل‌شده را می‌گیرد و به DataFrame تبدیل می‌کند:

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 daily(ins_code, adjusted=True):
"""Full daily history of one instrument as a DataFrame indexed by date."""
data = get(
f"/v1/instruments/{ins_code}/daily",
params={"adjusted": str(adjusted).lower(), "limit": 5000},
)
df = pd.DataFrame(data["rows"]).set_index("d")
df.index = pd.to_datetime(df.index)
return df
match = get("/v1/instruments", params={"q": "فولاد", "limit": 1})[0]
df = daily(match["ins_code"])
print(match["ticker"], match["name"], match["ins_code"])
print(df[["close", "volume"]].tail(3))

خروجی واقعی در ۲۳ آبان ۱۴۰۵:

فولاد فولاد مبارکه اصفهان 46348559193224090
close volume
d
2026-11-09 4670.0 8.969032e+08
2026-11-10 4810.0 1.031670e+09
2026-11-11 4950.0 1.142422e+09

هر بخش اسکرپر به چه چیزی تبدیل می‌شود؟

کار در اسکرپر در روشا
یافتن insCode نماد GET /v1/instruments?q= یا خود نماد فارسی به‌جای ref
سابقهٔ قیمت روزانه GET /v1/instruments/{ref}/daily
تعدیل افزایش سرمایه و سود نقدی adjusted=true
فهرست رویدادهای تعدیل GET /v1/instruments/{ref}/adjustments
حقیقی و حقوقی GET /v1/instruments/{ref}/client-type
سابقهٔ شاخص GET /v1/indices/{ref}/daily
روزهای معاملاتی GET /v1/calendar?start=&end=
NAV صندوق از فیپیران GET /v1/funds/{ref}/history
ریز معاملات و کندل درون‌روزی GET /v1/instruments/{ref}/trades و /bars (پلن دارای intraday)

مرجع کامل در مستندات API است.

چند قرارداد که باید بدانید

  • ins_code رشته است. این کد از بازهٔ عدد صحیح امن جاوااسکریپت بزرگ‌تر است. در pandas هم آن را متن نگه دارید، وگرنه در تبدیل به float رقم از دست می‌دهد.
  • ردیف‌ها از قدیم به جدید هستند. limit جدیدترین N ردیف بازه را نگه می‌دارد. پس limit=1 آخرین روز است.
  • هر درخواست حداکثر ۵۰۰۰ ردیف. برای بازهٔ بلندتر با تاریخ صفحه‌بندی کنید: end را روز پیش از قدیمی‌ترین ردیف بگذارید.
  • تاریخ ورودی آزاد است. 2026-11-11، 20261111 و شمسی 1405-08-20 هر سه پذیرفته می‌شوند. تاریخ خروجی میلادی ISO است و ردیف‌های روزانهٔ سهام jdate شمسی هم دارند.
  • همه‌چیز ریال است. برای تومان بر ۱۰ تقسیم کنید.
  • close قیمت پایانی و last قیمت آخرین معامله است. این دو معمولاً برابر نیستند.
  • روز بدون معامله. open، high و low تهی هستند و volume صفر است.

جزئیات در قراردادها و خطاها آمده است.

چند نماد پشت سر هم

برای چند نماد، همان تابع را در حلقه صدا بزنید. تابع get در پاسخ 429 به اندازهٔ Retry-After صبر می‌کند، پس حلقه از سقف دقیقه‌ای پلن بیرون نمی‌زند. این تکه ادامهٔ کد بالاست:

tickers = ["فولاد", "فملی", "خودرو"]
closes = {}
for ticker in tickers:
match = get("/v1/instruments", params={"q": ticker, "limit": 1})[0]
closes[match["ticker"]] = daily(match["ins_code"])["close"]
panel = pd.DataFrame(closes).dropna()
print(panel.tail(3))

جست‌وجو با زیررشته کار می‌کند و اولین نتیجه همیشه نماد دلخواه شما نیست. در کد تولیدی، ins_code را یک بار پیدا و ذخیره کنید و بعد فقط با همان کار کنید.

چه چیزی را روشا ندارد؟

صورت‌های مالی و اطلاعیه‌های کدال، دادهٔ بنیادی شرکت‌ها و قیمت ارز و طلا در روشا نیست. ریز معاملات فقط برای روزهایی هست که ثبت شده‌اند. اگر اسکرپر شما این‌ها را می‌خواند، آن بخش را نگه دارید.

گام بعدی

Esc

نمونه: «فولاد»، «شاخص کل»، «عیار»، «rate limit»