קריאת נתוני שוק מניות בחינם באמצעות Python ו-pandas
מדריך מעשי לשימוש ב-requests ו-pandas ב-Ubuntu לשליפת נתוני JSON מהבורסה. למדו כיצד להמיר נתונים ל-DataFrame ולחשב ממוצע ווליום לעשרים ימי מסחר ללא מפתח API.
requests
קריאה ל-API של נתוני שוק מניות בחינם באמצעות Python דורשת בקשת HTTP אחת ושתי ספריות שרוב האנשים כבר מחזיקים: requests כדי למשוך את ה-JSON ו-pandas כדי להפוך אותו ל-DataFrame. אין צורך במפתח API. מדריך זה רץ מתחילתו ועד סופו על קונטיינר Ubuntu נקי ומסתיים בהדפסת ממוצע הווליום ל-20 ימי מסחר עבור טיקר בודד. מה שה-endpoints מציעים מכוסה במדריך API של נתוני שוק מניות בחינם; דף זה הוא הצד של Python באותו ה-API.
הגדרת Python, requests ו-pandas בתוך קונטיינר Ubuntu חדש
דימוי Ubuntu בסיסי מגיע ללא המודול venv של Python, וגרסאות עדכניות מסרבות להתקין חבילות ישירות לתוך מפרש המערכת. סביבה וירטואלית עוקפת את שתי הבעיות הללו ומתנהגת באופן זהה בכל גרסת Ubuntu נוכחית. יש להריץ פקודות אלו כ-root, או להוסיף את הקידומת sudo לשורות ה-apt-get.
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y python3 python3-venv
python3 -m venv .venv
. .venv/bin/activate
pip install "requests>=2.31,<3" "pandas>=2.0,<4"
טווחי הגרסאות נבחרו במכוון. כל אחד מהם מקבל כל גרסה מתוחזקת של הספרייה ומוציא מהכלל רק גרסה ראשית עתידית שהתנהגותה אינה ידועה; הדבר מאפשר לאותה שורת התקנה לעבוד בצורה נקייה ככל ש-Ubuntu מעדכנת את גרסת ה-Python המוגדרת כברירת מחדל. שום דבר אינו מקובע לגרסת בנייה שקיימת היום. כל המפורט להלן מניח שהסביבה פעילה.
כיצד ניתן לקרוא ל-API של נתוני שוק מניות בחינם באמצעות Python?
נקודות הקצה (endpoints) להדגמה משיבות לבקשות GET פשוטות ללא צורך במפתח או בהרשמה. הסקריפט הראשון מבקש אחת מהשאילתות המוכנות מראש ומדפיס את המבנה של הנתונים המתקבלים.
import json
import requests
BASE = "https://ai.strasmore.com/api/demo"
resp = requests.get(BASE, params={"q": "dividend_yield_leaders"}, timeout=30)
resp.raise_for_status()
payload = resp.json()
print(list(payload.keys()))
print(payload["columns"])
print(json.dumps(payload["rows"][0], indent=2))
requests.get בונה את מחרוזת השאילתה מתוך params, raise_for_status() הופך כל סטטוס 4xx או 5xx לחריגה (exception), ו-.json() מנתח את גוף התשובה למילון. לאחר צמצום למבנה שלו, המילון נראה כך:
{
"key": "dividend_yield_leaders",
"label": "...",
"nl": "Which large-cap US stocks currently have the highest dividend yields?",
"sql": "SELECT ...",
"columns": ["as_of", "ticker", "dividend_yield_pct", "price", "market_cap_bn", "price_to_earnings"],
"rows": [{"as_of": "<date>", "ticker": "<symbol>", "dividend_yield_pct": <number>, ...}, ...],
"elapsed": "...",
"source": "Strasmore Research",
"more": "..."
}
שני מפתחות הם בעלי חשיבות עבור pandas. columns מגדיר את שמות השדות לפי סדר, ו-rows מכיל אובייקט JSON אחד לכל רשומה עם שמות אלו כמפתחות. המפתח sql הוא השאילתה המדויקת שהפיקה את המספרים, והוא מוחזר עם כל תשובה; זה מה שהופך את הנתונים לברי-ביקורת במקום ל"קופסה שחורה". בקשת GET ל-/api/demo/catalog מציגה רשימה של כל מפתח מוכן, בצירוף נקודת הקצה של ה-SQL המשמשת לאחר מכן.
כיצד טוענים JSON לתוך DataFrame של pandas?
השאילתות הערוכות עונות על שאלות קבועות. נתוני מסחר יומיים (Daily bars) עבור טיקר לבחירתכם מגיעים מנקודת הקצה של SQL ללא צורך בהרשמה ב-/api/demo/sql, המקבלת שאילתה לקריאה בלבד בפרמטר sql ומחזירה את אותו זוג columns ו-rows. המגבלות שלה, נכון לספטמבר 2026, מופיעות בכל תגובה: 500 שורות, 20 שניות, שנת היסטוריה אחת, ללא מפתח. ה-SQL API החינמי עם מפתח מעלה את התקרה למאה שאילתות ביום על פני היסטוריה עמוקה יותר; קוד הבקשה זהה.
שני מנגנוני הגנה ב-SQL שומרים על מהימנות הממוצע. טבלה שמתעדכנת במהלך הסשן יכולה להכיל יותר משורה אחת עבור היום הנוכחי, והווליום של אותו יום הוא חלקי כל עוד השוק פתוח. השאילתה עוצרת ביום האתמול באמצעות date < today() ומאחדת כל שורות כפולות עבור תאריך מסוים בעזרת GROUP BY date ו-max(volume). אם לא עיינתם באופן שבו נבנה בר יומי מתוך ה-tape, כיצד נבנים ברי OHLCV מסביר מהיכן מגיעות אותן שורות.
import time
import requests
import pandas as pd
SQL_URL = "https://ai.strasmore.com/api/demo/sql"
TICKER = "AAPL"
SQL = f"""
SELECT date, max(volume) AS volume
FROM stocks_daily_aggs
WHERE ticker = '{TICKER}'
AND date < today()
GROUP BY date
ORDER BY date DESC
LIMIT 20
"""
def fetch(sql, attempts=4):
"""GET the SQL endpoint. Back off on 429 and 5xx; stop on any other 4xx."""
delay = 2.0
for attempt in range(1, attempts + 1):
resp = requests.get(SQL_URL, params={"sql": " ".join(sql.split())}, timeout=30)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 429 or resp.status_code >= 500:
try:
wait = max(1.0, float(resp.headers.get("Retry-After")))
except (TypeError, ValueError):
wait = delay
print(f"HTTP {resp.status_code}; waiting {wait:.0f}s (attempt {attempt} of {attempts})")
time.sleep(wait)
delay *= 2
continue
raise SystemExit(f"HTTP {resp.status_code}: {resp.text[:400]}")
raise SystemExit("gave up after repeated 429 or 5xx responses")
payload = fetch(SQL)
df = pd.DataFrame(payload["rows"], columns=payload["columns"])
df["date"] = pd.to_datetime(df["date"])
df["volume"] = pd.to_numeric(df["volume"])
df = df.sort_values("date").reset_index(drop=True)
print(df.to_string(index=False))
avg_20 = df["volume"].mean()
first, last = df["date"].iloc[0], df["date"].iloc[-1]
print(f"{TICKER} 20-session average volume: {avg_20 / 1e6:.1f}M shares "
f"({first:%Y-%m-%d} to {last:%Y-%m-%d}, {len(df)} sessions)")
pd.DataFrame(rows, columns=columns) בונה את הטבלה ישירות מרשימת האובייקטים, והעברת columns שומרת על סדר השדות של ה-API. לאחר מכן מתבצעות שתי המרות: תאריכים מגיעים כמחרוזות והופכים ל-timestamps אמיתיים, ו-volume מומר למספר למקרה שתגובה כלשהי תסרל אותו כטקסט. מיון מהישן לחדש מסדר את ה-frame בסדר שגרף מצפה לו. df["volume"].mean() על פני בדיוק עשרים שורות הוא הממוצע ל-20 סשנים, והשורה האחרונה מדפיסה אותו במיליונים לצד טווח התאריכים שהוא מכסה.
מה מודד בפועל ממוצע נפח מסחר ל-20 ימים?
נפח מסחר ממוצע מחליק סדרה יומית תנודתית. מספר המניות הנסחרות ביום בודד משתנה בהתאם לאיזון מחדש של מדדים, פקיעות אופציות, תאריכי פרסום דוחות כספיים וכותרות חדשותיות; עשרים ימי מסחר הם בערך חודש קלנדרי אחד, פרק זמן ארוך מספיק כדי למתן את אותן קפיצות וקצר מספיק כדי לעקוב אחר שינוי ברמת הפעילות של מניה. הלוח להלן מחשב את אותו נתון סטטיסטי שמפיק הסקריפט, מתוך אותה טבלה יומית, לאורך החודשים האחרונים, ומציג את נתוני המסחר הגולמיים לצד הממוצע הנע.
| סשן | נפח (במיליונים) | ממוצע 20 יום (במיליונים) |
|---|---|---|
| 2026-08-17 | 38.2 | 51.9 |
| 2026-08-18 | 53.4 | 52.5 |
| 2026-08-19 | 50.5 | 53 |
| 2026-08-20 | 41 | 53.1 |
| 2026-08-21 | 46.9 | 53 |
| 2026-08-24 | 34.7 | 52.3 |
| 2026-08-25 | 25.9 | 51 |
| 2026-08-26 | 34 | 49.9 |
| 2026-08-27 | 32.4 | 47.8 |
| 2026-08-28 | 38.6 | 43.1 |
| 2026-08-31 | 41.2 | 41.4 |
| 2026-09-01 | 53.2 | 40.6 |
| 2026-09-02 | 33.8 | 39.8 |
| 2026-09-03 | 37.2 | 39.4 |
| 2026-09-04 | 39.6 | 39.7 |
| 2026-09-08 | 35.5 | 39.2 |
| 2026-09-09 | 65.6 | 40.6 |
| 2026-09-10 | 70 | 42 |
| 2026-09-11 | 50.7 | 42.5 |
| 2026-09-14 | 39.3 | 43.1 |
ה-SQL המדויק מאחורי כל מספר
SELECT
session,
volume_millions,
avg_20d_millions
FROM
(
SELECT
toString(d) AS session,
round(vol / 1e6, 1) AS volume_millions,
round(avg(vol) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) / 1e6, 1) AS avg_20d_millions,
count() OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS sessions_in_window
FROM
(
SELECT
date AS d,
toFloat64(max(volume)) AS vol
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'AAPL'
AND date >= today() - 75
AND date < today()
GROUP BY date
)
)
WHERE sessions_in_window = 20
ORDER BY sessionהסדרה היומית מציגה נקודה אחת לכל יום מסחר; הסדרה המוחלקת היא הממוצע של אותו יום ותשעה-עשר הימים שקדמו לו. הלוח משתרע מ-2026-08-17 ועד 2026-10-02, וכולל 34 ימי מסחר שלכל אחד מהם חלון מלא של עשרים ימים מאחוריו. ביום האחרון מביניהם, AAPL נסחרה בהיקף של 33.3 מיליון מניות, לעומת ממוצע של עשרים ימים שעמד על 42.2 מיליון. ממוצע זה הוא המספר שהסקריפט הציג ביום שבו נוצר דף זה. אם תריצו אותו היום, עשרים ימי המסחר יהיו שונים, ועימם גם הנתון.
החלפת ה-ticker
שנו את TICKER והסקריפט יעבוד עבור כל נייר ערך הנסחר בארה"ב המופיע בטבלה. ההשוואה להלן מריצה את אותו חישוב של עשרים ימי מסחר עבור ארבעה שמות מוכרים, כדי לספק הערכה מהירה של קנה המידה למה שמוגדר כ"נפח מסחר ממוצע" בקרן סל עוקבת מדד ובשלוש מניות ענק.
| ticker | ממוצע 20 יום (במיליונים) | סשן ראשון | סשן אחרון |
|---|---|---|---|
| NVDA | 110 | 2026-09-04 | 2026-10-02 |
| SPY | 46 | 2026-09-04 | 2026-10-02 |
| AAPL | 42.2 | 2026-09-04 | 2026-10-02 |
| MSFT | 21.3 | 2026-09-04 | 2026-10-02 |
ה-SQL המדויק מאחורי כל מספר
SELECT
ticker,
round(avg(vol) / 1e6, 1) AS avg_20d_millions,
toString(min(d)) AS first_session,
toString(max(d)) AS last_session
FROM
(
SELECT
ticker,
d,
vol,
row_number() OVER (PARTITION BY ticker ORDER BY d DESC) AS rn
FROM
(
SELECT
ticker,
date AS d,
toFloat64(max(volume)) AS vol
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('SPY', 'NVDA', 'AAPL', 'MSFT')
AND date >= today() - 45
AND date < today()
GROUP BY ticker, date
)
)
WHERE rn <= 20
GROUP BY ticker
HAVING count() = 20
ORDER BY avg_20d_millions DESCNVDA נושאת בממוצע עשרים ימי המסחר הגבוה ביותר מבין הארבעה, עם 110 מיליון מניות ליום מסחר, ו-MSFT היא הקלה ביותר עם 21.3 מיליון, הכל נמדד לאורך ימי מסחר שבין 2026-09-04 ל-2026-10-02. הנתון של AAPL כאן הוא אותו מספר שבו מסתיים המעקב לעיל: הגדרה אחת, מחושבת פעם אחת, בכל מקום שבו היא מופיעה.
כיצד נראית שגיאת 429, וכיצד ניתן לבצע ניסיון חוזר בצורה מנומסת?
מגבלת קצב (rate limit) מגיעה כקוד מצב HTTP ולא כנתונים. התגובה נושאת את הסטטוס 429 Too Many Requests ולעיתים קרובות כוללת כותרת Retry-After המציינת את מספר השניות להמתנה; גוף התגובה אינו מכיל את הנתונים שביקשת, וזו הסיבה שהפונקציה העוזרת fetch בודקת את status_code לפני שהיא מנסה לפענח דבר מה. הכללים שלה, לפי הסדר, הם:
200: פענוח והחזרת ה-JSON.429או כל5xx: המתנה שלRetry-Afterכאשר הוא קיים, ובמקרים אחרים נסיגה הדרגתית (שתיים, ארבע, שמונה, ולאחר מכן שש-עשרה שניות), וניסיון חוזר, עד ארבעה ניסיונות בסך הכל.- כל
4xxאחר: עצירה. גוף התגובה מפרט בדיוק מדוע השאילתה נדחתה, כגון טבלה שאינה קיימת או הצהרה ששער ה-read-only סירב לה, ושום כמות של ניסיונות חוזרים לא תשנה את התשובה. טבלה לא ידועה, למשל, חוזרת כ-400.
שני הרגלים יסייעו לך להישאר הרחק מהמגבלה. תוצאות מעובדות מתרעננות לכל היותר אחת לעשר דקות; לולאה המבצעת פולינג (polling) בתדירות גבוהה יותר מושכת את אותם הבתים בדיוק, ושמירת המידע ב-cache מקומית אינה עולה דבר. כמו כן, בקש רק את מה שאתה צריך: LIMIT 20 עבור ממוצע של עשרים סשנים, ולא שנה שלמה של שורות שיושלכו לפח. כשלים ברשת (חיבור שנותק, תקלת DNS) מופיעים כ-requests.RequestException, שהפונקציה העוזרת אינה תופסת; עטוף את הקריאה ב-try/except אם הסקריפט רץ ללא השגחה.
שאלות נפוצות
האם קיים API חינמי לנתוני שוק ההון עבור Python?
כן. נקודות הקצה (endpoints) להדגמה במדריך זה משיבות לבקשות GET ללא אימות מ-requests בפורמט JSON, הנטען ישירות לתוך pandas: שאילתות ערוכות ב-/api/demo?q=<key> ושאילתות SQL לקריאה בלבד שנכתבו ידנית ב-/api/demo/sql, מוגבלות ל-500 שורות ושנה אחת של היסטוריה. ללא מפתח, ללא הרשמה.
האם אני זקוק למפתח API כדי לקבל נתוני מניות ב-Python?
לא עבור שכבת ההדגמה המשמשת כאן. מפתח חינמי מעלה את התקרה למאה שאילתות ביום עם היסטוריה עמוקה יותר, וקוד ה-Python אינו משתנה. מפתחים העוטפים את אותן נקודות קצה ככלים עבור LLM יכולים להתחיל מ-מיומנויות נתוני שוק עבור סוכני בינה מלאכותית.
כיצד ממירים תגובת JSON מ-API ל-DataFrame של pandas?
יש לנתח את גוף התגובה באמצעות resp.json(), ולאחר מכן להעביר את רשימת אובייקטי השורות ל-pd.DataFrame(rows, columns=columns). יש להמיר מחרוזות תאריך באמצעות pd.to_datetime ומחרוזות מספריות באמצעות pd.to_numeric לפני ביצוע פעולות חשבוניות; ב-JSON אין טיפוס נתונים של תאריך, וחלק מה-APIs ממירים מספרים עשרוניים לטקסט.
מה משמעות שגיאת 429 בעת קריאה ל-API של מניות?
HTTP 429 משמעותו Too Many Requests: השרת מגביל את קצב הבקשות של הקורא. יש להמתין את מספר השניות המצוין בכותרת Retry-After כאשר הוא נשלח, ובמקרים אחרים לבצע נסיגה מעריכית (exponential backoff), ולשמור תשובות ב-cache במקום לבצע פולינג (polling) לתוצאה שמתרעננת רק אחת לעשר דקות.
כיצד מחשבים ממוצע נפח מסחר ב-pandas?
יש לטעון שורה אחת לכל סשן עם עמודת volume, לשמור את עשרים הסשנים המלאים האחרונים, ולקרוא ל-df["volume"].mean(). עבור גרסה מתגלגלת (rolling) על פני היסטוריה ארוכה יותר, יש להשתמש ב-df["volume"].rolling(20).mean(), שזהו המדד שמוצג בלוח הגרף לעיל.
כל לוח לעיל מגיע עם ה-SQL המדויק שמתחתיו, והסקריפט מבוסס על אותו רעיון עם קריאת requests בחזית. מפתח API חינמי. מאה שאילתות ביום, עשרים ושתיים שנים של היסטוריה, SQL על גבי ה-raw tape. ללא כרטיס אשראי. קבל מפתח API