Rotate to portrait

Vizual Stocks works best vertically on mobile.

VizualStocks API

PYTHON TUTORIAL · SYSTEMATIC STOCK RESEARCH

Historical analyst data for backtesting with Python

Build a dated stock-ranking dataset from EPS and revenue revisions, analyst price targets, buy/hold/sell consensus, and company guidance. Combine the histories in Python and export signals for your backtesting workflow.

01 · OUTPUT

Turn historical analyst data into stock-ranking inputs

A systematic stock-ranking strategy needs more than today’s consensus. It needs dated observations you can compare consistently: were earnings expectations improving, price targets rising, and recommendations becoming more positive at the time?

ONE DATE · THREE ENDPOINTSWhich companies’ expectations improved?

90-day changes · decision date September 30, 2026

Example AScore 100
Revenue revision
+10%
Target revision
+10%
Buy-share change
+20 pp
Consensus vs guidance
Inside range
Example BScore 50
Revenue revision
+4%
Target revision
+2%
Buy-share change
+10 pp
Consensus vs guidance
Below range
Example CScore 0
Revenue revision
−2%
Target revision
−5%
Buy-share change
−10 pp
Consensus vs guidance
Above range
Synthetic teaching example, not reported company data. The downloadable script reproduces these values with --sample. Scores rank the three analyst signals; guidance adds context.

Build one row per company and decision date, with EPS and revenue revisions, price-target changes, recommendation counts and company guidance. The output includes a visual report and CSV files for your own research model. We’ll use three tickers to keep the example easy to inspect.

02 · SETUP

Run the Python example

You’ll need Python 3.10 or newer. Save analyst_stock_ranking.py in your working folder, then install the libraries:

Terminal · install dependencies
python -m pip install requests

Log in to get your API key. In the dashboard, click Create API key → Create key → Copy, then return here and replace YOUR_API_KEY below.

Windows PowerShell
$env:VIZUALSTOCKS_API_KEY = "YOUR_API_KEY"
macOS / Linux shell
export VIZUALSTOCKS_API_KEY="YOUR_API_KEY"

Choose the command for your terminal. The key is sent in the X-API-Key header and is not saved in your research export.

To see the finished output before making API requests, run the synthetic example. This command needs no key:

Terminal · reproduce the example
python analyst_stock_ranking.py --sample --output-dir analyst-example

Open analyst-example/report.html. The same folder contains ranking.csv, coverage.csv, the input responses and run settings. Real-data mode uses the same calculations.

03 · HISTORY

How far back does each company’s history go?

Start by choosing one forecast target. This historical example uses Q2'26 and a June 1, 2026 decision date. List the available periods for each company; the API’s periodEndDate identifies the actual reporting-period end. Normalized chart labels do not replace the issuer’s fiscal calendar.

Python · inspect available target periods
from analyst_stock_ranking import BASE_URL, make_session

with make_session() as session:
    response = session.get(
        BASE_URL + "/companies/AAPL/estimate-revisions",
        params={"frequency": "quarterly", "order": "asc", "limit": 1},
        timeout=(10, 60), allow_redirects=False,
    )
    if 300 <= response.status_code < 400:
        raise ValueError("Unexpected API redirect")
    response.raise_for_status()
    for group in response.json()["data"]:
        for period in group["periods"]:
            print(period["normalizedPeriod"], period.get("periodEndDate"))
Terminal · fetch the three company-history endpoints
python analyst_stock_ranking.py --tickers AAPL MSFT PLTR --frequency quarterly --period "Q2'26" --dates 2026-06-01 --output-dir analyst-june

The live run retrieves estimate revisions, unified analyst consensus and company guidance, plus guidance states at the current and baseline dates: five read-only requests per company for this example, before retries. The unified consensus response contains both recommendation and price-target fields. Guidance uses the API’s own date selection so withdrawn or unresolved forecasts do not accidentally revive older values. If a company or endpoint has no usable history, its fields remain blank and the reason stays in the exports.

Decision date: June 1, 2026
Inputs use May 31 or earlier. The previous-calendar-day cutoff avoids assuming a date-only announcement was available at the market open that same day.
Comparison date: 90 days before the cutoff
Use the latest observation on or before each boundary, never the next observation after it. Actual dates and their ages are included in every ranking row.
coverage.csv
First date, last date and observation count for each requested dataset through the latest cutoff. Estimate coverage is for the selected target; guidance coverage is for the requested revenue/frequency history.

For a single-date request, analyst consensus and guidance accept asOfDate. The consensus endpoint joins ratings and targets by asOfDate; fields for either side can be null when only the other side has an observation. Guidance records keep their actual announcementDate. Omitting the filter returns histories suitable for comparing multiple dates.

04 · REVISIONS

Calculate historical EPS and revenue revisions

Compare forecasts for the same quarter or year at both dates. A change from next quarter’s estimate to the following quarter’s estimate is not a revision to one forecast.

Python · build one dated feature row
from analyst_stock_ranking import make_session, fetch_histories, build_row

with make_session() as session:
    histories, errors = fetch_histories(
        session, "PLTR", "quarterly", "Q2'26", ["2026-06-01"],
    )

row = build_row("PLTR", histories, "2026-06-01", "quarterly", "Q2'26")
print(row["estimateDate"], row["estimateBaselineDate"])
print(row["revenueRevisionPct"], row["epsChange"])
print(errors)  # Keep unavailable endpoints visible.

revenueRevisionPct is the percentage change from a positive starting estimate. epsChange is an absolute change in normalized EPS, so moving from −0.20 to −0.10 correctly shows +0.10 without a misleading percentage. Missing estimates remain missing.

For the single-company charting workflow, see revenue and EPS estimate revisions with Python. This guide adds cross-company rankings using the same estimate-revisions endpoint.

05 · TARGETS

Are analyst price targets rising or falling?

Python · inspect target changes and dispersion
# Continue with row from the previous section.
print(row["targetMean"], row["targetCurrency"])
print(row["targetRevisionPct"], row["targetSpreadPct"])
print(row["targetDate"], row["targetAnalystCount"])

targetRevisionPct compares the mean target at the two boundaries in the same currency. targetSpreadPct is (high − low) / mean × 100: a simple measure of the width of the target range. The median and contributing analyst count are also exported.

The downloadable script reads consensusPriceTarget, medianPriceTarget, lowPriceTarget, highPriceTarget and priceTargetContributors from the unified consensus history. It selects the latest target-bearing row at each boundary, so a ratings-only snapshot does not erase an earlier valid target.

A target change is not a stock return or implied upside. To calculate upside, you would additionally need the stock price on the corresponding date. These histories are calculated consensus aggregates; changes can reflect both new targets and changes in the contributing set.

Unified rating and price-target fields →

06 · RATINGS

Track historical buy, hold and sell consensus

Calculate the Buy/Strong Buy share of all five recommendation categories, then compare that share across dates. Going from 50% to 70% is a 20-percentage-point increase.

Python · inspect recommendation consensus
# Buy share includes Strong Buy and Buy.
print(row["buySharePct"], row["buyShareChangePp"])
print(row["ratingDate"], row["ratingBaselineDate"])

The denominator includes Strong Buy, Buy, Hold, Sell and Strong Sell. A missing category does not become zero; an incomplete or empty distribution leaves the calculation blank. This measures a change in aggregate sentiment, not the number of broker upgrades.

The same unified response includes consensusRating, consensusRatingValue and ratingsContributors. The script calculates Buy share from the five category counts and selects rating-bearing rows independently from target-bearing rows.

Unified rating and price-target fields →

07 · GUIDANCE

Compare company guidance with analyst consensus

Management’s outlook adds a different perspective: do analysts expect revenue below, inside or above the company’s range? Start with consolidated revenue and match the actual period end, annual/quarterly frequency, currency and accounting basis.

Python · review the source and compare like with like
# Inspect the returned definitions and original source first.
for guide in histories.get("guidance", {}).get("data", []):
    print(guide["announcementDate"], guide["periodEndDate"],
          guide["scope"], guide["basis"], guide["unit"],
          guide["status"], guide["sourceUrl"])

# The default row shows ranges without assuming an accounting basis.
print(row["guidanceLow"], row["guidanceHigh"])
print(row["consensusVsGuidance"])  # review_basis until verified

The script retains the latest eligible guidance for the chosen forecast, including its status and source. A withdrawn forecast stays withdrawn. A missing guidance series leaves the analyst ranking usable.

By default, comparable period/currency ranges are shown with review_basis rather than an automatic above/below conclusion. After checking the issuer source and consensus definition, provide the exact verified basis with --guidance-basis GAAP (or the appropriate returned label). Unspecified or mismatched definitions stay unclassified.

guidanceMidpointDerived and its lookback change are calculated locally from complete ranges; they are not issuer-reported point forecasts. Qualitative and one-sided guidance remain in responses.json, without invented bounds.

Company guidance response, statuses and source fields →

08 · TARGET UPSIDE

Calculate historical price-target upside with Python

How far above or below the stock price was the analyst consensus at your research cutoff? Historical implied upside is (mean target / stock price − 1) × 100. A $150 target against a $120 closing price implies 25% upside.

Use the target already selected for row and supply a CSV of historical closing prices from your own price source. The example selects the latest trading close on or before the same cutoff, including across weekends. Match the currency and split basis of the target and price; do not substitute a dividend-adjusted total-return price.

Python · compare historical consensus with the stock price
import csv
from analyst_stock_ranking import day, latest, percent_change

# Continue with row from the earlier build_row example.
# Your price file: ticker,date,close,currency (one daily close per ticker).
with open("historical-closes.csv", newline="", encoding="utf-8-sig") as file:
    prices = [p for p in csv.DictReader(file)
              if p["ticker"].strip().upper() == row["ticker"].upper()]

quote = latest(prices, "date", day(row["cutoffDate"]))
close = float(quote["close"]) if quote and quote["close"] else None
upside = None
if (quote and row["targetCurrency"]
        and quote["currency"].strip().upper() == row["targetCurrency"].upper()):
    upside = percent_change(row["targetMean"], close)

print({
    "ticker": row["ticker"], "cutoffDate": row["cutoffDate"],
    "targetDate": row["targetDate"], "targetMean": row["targetMean"],
    "priceDate": quote["date"] if quote else None, "close": close,
    "impliedUpsidePct": upside,
})

The output preserves both observation dates. Missing targets or prices, nonpositive closing prices, and mismatched currencies leave upside unset. This optional comparison does not change the tutorial’s three-signal ranking.

Did analysts raise their targets, or did the stock fall?

If the target stays at $150 while the stock falls from $120 to $100, implied upside rises from 25% to 50% without any analyst target increase. Read it alongside targetRevisionPct to distinguish a widening price gap from improving analyst expectations.

09 · RANKING

Combine the analyst signals into a stock-ranking table

The default example ranks revenue-estimate revisions, mean price-target revisions and changes in Buy share. Each gets equal weight. Within each decision date, the lowest value receives 0 and the highest 100; ties share their average position.

A company needs all selected signals to receive a score, and at least two companies must qualify. Missing companies stay visible as unranked rows. Guidance is context alongside the score—not a penalty for companies that do not issue it.

Python · rank rows already built for your companies
import json
from pathlib import Path
from analyst_stock_ranking import build_row, rank_rows

# Reuse the saved inputs; no additional API requests.
saved = json.loads(Path("analyst-june/responses.json").read_text())
rows = [build_row(ticker, data, "2026-06-01", "quarterly", "Q2'26")
        for ticker, data in saved.items()]
ranked = rank_rows(rows, signals=(
    "revenueRevisionPct", "targetRevisionPct", "buyShareChangePp",
))
for item in ranked:
    print(item["decisionDate"], item["ticker"],
          item["rank"], item["score"], item["rankingStatus"])

Use --signals revenueRevisionPct buyShareChangePp, for example, to rank only those two inputs. The score is relative to the eligible companies on that date; 100 does not mean a 100% return or a probability of success.

ranking.csv
Signals, score, guidance context, actual comparison dates, observation gaps and request notes for every requested company/date.
coverage.csv
Available history and endpoint status. Check this before choosing your backtest window.
responses.json · metadata.json
The fetched inputs, retrieval time and exact settings needed to inspect or reproduce the calculation. No API key is saved.
report.html
A compact visual summary that opens offline, without external assets or credentials.

10 · BACKTESTING

Use dated signals in a Python backtesting workflow

Terminal · repeat across decision dates
python analyst_stock_ranking.py --tickers AAPL MSFT PLTR --frequency quarterly --period "Q2'26" --dates 2026-04-01 2026-05-01 2026-06-01 --lookback-days 30 --output-dir analyst-monthly

The script downloads each endpoint’s full history once, retrieves guidance states for each unique comparison boundary, and calculates a separate cross-company ranking for each date. Ratings and price targets share one download. This example switches to a 30-day comparison; keep the target period fixed within each comparison. Across longer windows, deliberately choose the next forecast period instead of silently splicing different targets together.

Join signals to subsequent stock returns

Supply your own historical prices and investment universe. Join on ticker and decision date, then evaluate returns after that decision. Keep those future returns outside the ranking calculation.

Python · optional price-file integration
import pandas as pd

# Optional: python -m pip install pandas
signals = pd.read_csv("analyst-monthly/ranking.csv")
prices = pd.read_csv("historical-prices.csv")
# Your file: ticker, decisionDate, entryPrice, exitPrice.
# Entry is at/after the decision time; exit is after entry.
keys = ["ticker", "decisionDate"]
evaluation = signals.merge(prices, on=keys, how="left", validate="one_to_one")
evaluation["forwardReturnPct"] = (
    evaluation["exitPrice"] / evaluation["entryPrice"] - 1
) * 100
evaluation.to_csv("ranking-evaluation.csv", index=False)
# Returns are evaluation labels, never inputs to rank_rows.
What the date controls do—and do not doThe example excludes observations after its cutoff and preserves the dates used. Estimate collection dates are not original analyst publication timestamps, and analyst histories are calculated aggregates. A dated response is not a guarantee of an immutable original-data vintage. For a tradable backtest, also use the historical stock universe, appropriately adjusted prices, transaction costs and an explicit execution time.

Keep each run in a new folder. Historical revisions to source data can then be compared against the inputs you previously saved.

11 · TROUBLESHOOTING

Troubleshooting

Why are some scores or comparisons blank?

Check the endpoint statuses in coverage.csv and the signal statuses in ranking.csv. A baseline may predate available history, a number may be missing, or the selected forecast period may already have ended. An optional --max-age-days 120 rejects comparison observations more than 120 days before their boundaries; it measures observation gaps, not underlying source freshness.

Why is company guidance missing or marked for review?

The script only compares consolidated revenue for the matching period and currency. Unavailable guidance, unmatched definitions or multiple accounting bases do not get silently substituted. Review the returned source and basis before enabling the comparison; an unready endpoint does not stop the remaining datasets.

Why do I get a 401, 403 or 404?

A 401 stops the run so you can correct the key. Other request failures stay attached to their company and dataset while the remaining requests continue. Check company access and the error reference. Temporary failures receive bounded retries.

Can I overwrite an existing report?

Choose a new --output-dir. Existing reports are intentionally preserved, so the inputs behind an earlier research result remain available.