yfinance alternative: a watchlist's quotes, P/E and financials in a spreadsheet
27 Oct 2026 · benthepythondev · Last checked 27 Oct 2026 with live runs
To get quotes, valuation ratios and financials for a list of tickers into CSV or Google Sheets on a schedule, run the Yahoo Finance Scraper on Apify with your symbols. Save that input as a Task, schedule the Task, and read its last run as CSV from Python or with IMPORTDATA in Sheets.
Why not just run yfinance?
You can, and for a few tickers on your laptop it is the simplest option. The trouble starts when the script has to run unattended. The yfinance issue tracker has a long thread of people hitting YFRateLimitError('Too Many Requests'); one wrote "I keep getting the following error even from different IPs". Then the cron job fails quietly and the sheet goes stale.
To be clear about what this is: the Actor uses the yfinance library itself, inside its own container on Apify. What changes is where it runs and who keeps it working. You don't install, pin or patch anything, the requests don't leave from your machine, and you get one JSON record per ticker that exports straight to CSV.
The input
Tickers go in one string, comma-separated or one per line. dataTypes picks the blocks per ticker. For a fundamentals watchlist, quote and info are enough:
{
"tickers": "MSFT, KO, NVDA, JNJ, XOM",
"dataTypes": ["quote", "info"]
}
Add "financials" for income statement, balance sheet and cash flow, "history" with historyPeriod and historyInterval for candles, or "holders", "recommendations", "earnings", "dividends", "splits" and "news". The price per ticker is the same whichever blocks you pick.
A real output row
This is the MSFT record from a run on 27 Oct 2026, with the long description and the address fields cut:
{
"symbol": "MSFT",
"scraped_at": "2026-10-27T21:18:21.736299",
"quote": {
"price": 531.3300170898438,
"previous_close": 532.4412,
"day_high": 534.7000122070312,
"day_low": 528.3309936523438,
"volume": 19419705,
"market_cap": 3945415212634.4424,
"fifty_day_average": 509.1716009521484,
"two_hundred_day_average": 436.78714935302736,
"year_high": 553.719970703125,
"year_low": 349.20001220703125
},
"info": {
"name": "Microsoft Corporation",
"exchange": "NMS",
"currency": "USD",
"sector": "Technology",
"industry": "Software - Infrastructure",
"employees": 223000,
"enterprise_value": 4006815465472,
"pe_ratio": 29.600557,
"forward_pe": 22.428717,
"peg_ratio": 1.73,
"price_to_book": 8.920172,
"price_to_sales": 11.889546,
"dividend_yield": 0.74,
"dividend_rate": 3.92,
"beta": 1.099,
"eps": 17.95,
"forward_eps": 23.68972,
"revenue": 331839012864,
"ebitda": 194237005824,
"net_income": 133748998144,
"profit_margin": 0.40305,
"operating_margin": 0.45111,
"return_on_equity": 0.34039003,
"debt_to_equity": 29.118,
"current_ratio": 1.23,
"free_cash_flow": 16545500160,
"target_mean_price": 605.1243,
"target_high_price": 870,
"target_low_price": 440,
"recommendation": "strong_buy",
"analyst_count": 53
}
}
Note the units: dividend_yield came back as a percent (0.74 means 0.74%), while profit_margin and return_on_equity are fractions (0.40305 means 40.3%). These are the values Yahoo returns. Label your columns so nobody reads them as the same unit.
Into a CSV with Python
Install the client with pip install apify-client (tested with version 3.4.0) and set APIFY_TOKEN to the token from Apify Console → Settings → API & Integrations. The script runs the Actor, waits for it and writes one row per ticker with the columns you name:
import csv
import os
from apify_client import ApifyClient
WATCHLIST = "MSFT, KO, NVDA, JNJ, XOM"
COLUMNS = ["price", "market_cap", "pe_ratio", "forward_pe", "price_to_book",
"dividend_yield", "profit_margin", "debt_to_equity", "target_mean_price"]
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("benthepythondev/yahoo-finance-scraper").call(
run_input={"tickers": WATCHLIST, "dataTypes": ["quote", "info"]},
logger=None,
)
items = client.dataset(run.default_dataset_id).list_items().items
with open("watchlist.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["symbol", "name"] + COLUMNS)
for item in items:
values = {**item.get("info", {}), **item.get("quote", {})}
writer.writerow([item["symbol"], values.get("name")] + [values.get(c) for c in COLUMNS])
The Actor run itself took about six seconds. This is the file the script wrote, unedited:
symbol,name,price,market_cap,pe_ratio,forward_pe,price_to_book,dividend_yield,profit_margin,debt_to_equity,target_mean_price
MSFT,Microsoft Corporation,531.3300170898438,3945415212634.4424,29.600557,22.428717,8.920172,0.74,0.40305,29.118,605.1243
KO,The Coca-Cola Company,91.18000030517578,392306441289.7738,27.381382,25.872097,10.85347,2.29,0.28558,115.519,95.625
NVDA,NVIDIA Corporation,217.94000244140625,5262597238952.637,27.552465,13.694344,22.98218,0.46,0.63663,16.971,328.95346
JNJ,Johnson & Johnson,268.7300109863281,646916514229.6804,40.778454,23.130926,7.9341607,2.01,0.16203,54.767,279.4091
XOM,ExxonMobil Holdings Corporation,179.91000366210938,739774095781.8713,23.15444,14.735453,2.8520925,2.23,0.09072,15.921,179.40909
With apify-client 2.x the run is a dict, so write run["defaultDatasetId"] instead of run.default_dataset_id.
On a schedule, into Google Sheets
- Open the Actor, enter your tickers and data types, and save the input as a new Task named, for example,
my-watchlist(Apify docs on Tasks). - In Apify Console go to Schedules, create a schedule with a cron expression such as
30 21 * * 1-5(weekdays at 21:30 UTC, after the US close) and add your Task to it (Apify docs on schedules). - In Google Sheets, put this in cell A1, with your username, Task name and token filled in:
=IMPORTDATA("https://api.apify.com/v2/actor-tasks/YOUR_USERNAME~my-watchlist/runs/last/dataset/items?status=SUCCEEDED&format=csv&fields=symbol,quote,info&omit=info.description&token=YOUR_TOKEN")
That URL always points at the Task's latest successful run, so the sheet follows the schedule without extra code. Nested blocks are flattened into columns named quote/price, info/pe_ratio and so on, and omit=info.description blanks out the long company description. Google refreshes IMPORTDATA on its own timing, not the moment a run ends.
Anyone who can open the sheet can read the token in that formula, so use a token you can revoke and keep the sheet private. If that is a problem, run the Python script on a schedule and upload the CSV instead.
What it costs
You pay one record charge per ticker plus one Actor Start charge per run. On the Free plan a record is $0.005. From 4 Nov 2026 the start charge is $0.00005 for a default run, so the five-ticker watchlist above costs about $0.025 per run. Until then the start charge on the Free plan is $0.02. Higher Apify plans pay less per record, and the Store page always shows the price in force.
Open the actor See a megacap example
Honest limits
- Check your symbols. An unknown or delisted symbol still comes back as a row, with an empty
quoteblock and everyinfofield set to null, and today that row is charged like the others. The test symbolZZZZQXdid exactly that on 27 Oct 2026. - The data is Yahoo's. Fields are what Yahoo returns at run time. Some can be missing for a given company (a fund has no P/E, a stock without a dividend has no yield), so handle nulls.
- It is still yfinance underneath. When Yahoo changes something that breaks the library, the fix lands as a new build of the Actor, not as an upgrade you make. Runs can still fail until that build ships.
- Big lists run longer. Tickers are read one after another, so hundreds of tickers with
financialsor longhistoryneed more than the default 600-second run timeout. - Terms. Yahoo's terms of service govern how the data may be used. Read them for your use case; this page makes no claim beyond that.
Affiliate if you still need an Apify account: apify.com/?fpr=qolupv. I may earn a commission; the price you pay does not change.