Finance and markets

tradingview.com

TradingView - Scraping & Data Extraction

Notes an AI agent reads before it works on tradingview.com: what to open, what to call, what breaks. Written by browser-harness contributors after real runs, and kept on GitHub under the MIT license.

Give it to your agent

Any coding agent

Start the task with this line. The Markdown version is the same notes without the page around them.

Read https://browser-use.com/skills/tradingview.md, then <your task>

Browser Harness

Save the files into your workspace and turn domain skills on. The code below calls its helpers, such as http_get and js. What Browser Harness is.

d="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}/domain-skills/tradingview"
mkdir -p "$d" && cd "$d"
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/tradingview/scraping.md
export BH_DOMAIN_SKILLS=1

https://www.tradingview.com - charting platform with multiple internal REST APIs. Stock/crypto/forex screener and symbol search work without auth. Use http_get or raw urllib for all workflows except JS-rendered chart pages.

Do this first

Use the scanner API for bulk screener data - one POST, no browser, full column control.

import json, urllib.request
 
def tv_scan(payload, market="america"):
    data = json.dumps(payload).encode()
    req = urllib.request.Request(
        f"https://scanner.tradingview.com/{market}/scan",
        data=data,
        headers={"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"}
    )
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.loads(r.read())

No auth, no Referer, no cookies required for the scanner. Responses arrive in ~200ms.

Common workflows

Top stocks by market cap (screener)

import json, urllib.request
 
payload = {
    "filter": [],
    "options": {"lang": "en"},
    "columns": ["name", "close", "change", "volume", "market_cap_basic"],
    "sort": {"sortBy": "market_cap_basic", "sortOrder": "desc"},
    "range": [0, 10]   # [start, end] — half-open, so this returns rows 0–9
}
 
data = json.dumps(payload).encode()
req = urllib.request.Request(
    "https://scanner.tradingview.com/america/scan",
    data=data,
    headers={"Content-Type": "application/json", "User-Agent": "Mozilla/5.0"}
)
with urllib.request.urlopen(req, timeout=20) as r:
    resp = json.loads(r.read())
 
# resp["totalCount"] = 19549 (all US-listed instruments)
# resp["data"] is a list of {"s": "NASDAQ:NVDA", "d": [col0, col1, ...]}
# "d" values align positionally with "columns" in the payload
 
cols = payload["columns"]
for item in resp["data"]:
    row = dict(zip(cols, item["d"]))
    symbol = item["s"]   # e.g. "NASDAQ:AAPL"
    print(symbol, row["close"], row["change"], row["market_cap_basic"])
# NASDAQ:NVDA  201.68  1.68  4900823822021.0
# NASDAQ:AAPL  270.23  2.59  3967284528489.0
# ...

Critical: "d" is a plain positional array - index 0 = columns[0], index 1 = columns[1], etc. There are no keys in the row data itself.

Pagination

# Page 1: range [0, 20]
# Page 2: range [20, 40]
payload["range"] = [20, 40]

Filtering stocks

payload = {
    "filter": [
        {"left": "market_cap_basic", "operation": "greater", "right": 10_000_000_000},
        {"left": "volume",           "operation": "greater", "right": 5_000_000},
        {"left": "change",           "operation": "in_range", "right": [2, 10]},
        {"left": "exchange",         "operation": "equal",   "right": "NASDAQ"},
        {"left": "sector",           "operation": "equal",   "right": "Electronic Technology"},
    ],
    "columns": ["name", "close", "change", "volume", "market_cap_basic",
                "description", "sector", "industry"],
    "sort": {"sortBy": "market_cap_basic", "sortOrder": "desc"},
    "range": [0, 20]
}

Valid filter operations: greater, less, equal, in_range (right = [min, max]), match (substring on name).

Sector names use TradingView taxonomy (not GICS). Confirmed working values:

  • "Electronic Technology" - NVDA, AAPL, TSM
  • "Technology Services" - MSFT, GOOGL, META
  • "Finance", "Health Technology", "Consumer Non-Durables"

Full list of tested valid column names

# Price & volume
"name"                     # ticker (e.g. "AAPL")
"description"              # full name ("Apple Inc.")
"close"                    # last price
"open", "high", "low"
"volume"
"change"                   # % change today
"change_abs"               # absolute price change
"change|1M"                # 1-month % change (also: |6M, |1Y)
"High.1M", "High.6M"       # period high
"High.All", "Low.All"      # all-time high/low
"price_52_week_high"       # confirmed works
"price_52_week_low"        # confirmed works
"premarket_change"         # pre-market %
"postmarket_change"        # after-hours %
"gap"                      # overnight gap %
"change_from_open_abs"     # intraday move from open
"average_volume_10d_calc"  # 10-day avg volume
"relative_volume_10d_calc" # relative volume vs 10-day avg
"relative_volume_intraday|5"  # intraday relative vol (5m bars)
 
# Fundamentals
"market_cap_basic"          # market cap in USD
"earnings_per_share_diluted_ttm"  # EPS TTM
"price_earnings_ttm"        # P/E TTM
"P/E"                       # P/E (snapshot)
"dividends_yield"           # dividend yield %
"beta_1_year"               # beta
"float_shares_outstanding"  # float shares
 
# Technical ratings & indicators
"Recommend.All"   # composite rating: -1 (strong sell) to +1 (strong buy)
"RSI"             # RSI 14
"MACD.macd"       # MACD line
 
# Classification
"sector", "industry", "country", "exchange"
"type"    # "stock", "fund", "dr" (depository receipt), etc.
 
# NOTE: "52_week_high" / "52_week_low" are INVALID — use "price_52_week_high" / "price_52_week_low"
# NOTE: "EPS_diluted_net" is INVALID — use "earnings_per_share_diluted_ttm"

Bad columns return HTTP 400 with {"error": "Unknown field \"X\""}.

Other scanner markets

# market argument options (confirmed working):
# "america"  — US equities (19,549 instruments)
# "crypto"   — crypto across exchanges (56,455 instruments)
# "forex"    — FX pairs (6,401 instruments)
# "futures"  — futures (53,947 instruments)
 
# Crypto example
payload = {
    "filter": [],
    "columns": ["name", "close", "change", "volume", "market_cap_calc"],
    "sort": {"sortBy": "market_cap_calc", "sortOrder": "desc"},
    "range": [0, 10]
}
resp = tv_scan(payload, market="crypto")
# Returns BTC, ETH, etc. across Binance, Bybit, OKX...

Symbol search (requires Origin header)

import json, urllib.request
 
def symbol_search(query, exchange="", type_filter="", limit=50):
    url = (
        f"https://symbol-search.tradingview.com/symbol_search/v3/"
        f"?text={query}&hl=1&exchange={exchange}&lang=en"
        f"&search_type={type_filter or 'undefined'}&domain=production"
    )
    req = urllib.request.Request(url, headers={
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "Origin": "https://www.tradingview.com",   # REQUIRED — 403 without this
    })
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())
 
result = symbol_search("AAPL")
# result["symbols_remaining"] = 137
# result["symbols"] = list of up to 50 matches
# result["symbols"][0] keys:
#   symbol, description, type, exchange, country, currency_code,
#   cusip, isin, cik_code, logoid, provider_id, source_id,
#   is_primary_listing, typespecs

Gotcha: symbol-search.tradingview.com requires Origin: https://www.tradingview.com. Referer alone is not enough. The scanner API does NOT need Origin or Referer.

Filter by exchange and type:

# Exact match on NASDAQ:AAPL
result = symbol_search("AAPL", exchange="NASDAQ", type_filter="stock")
# Returns 1 result — exact symbol only when exchange is specified

News headlines for a symbol

import json, urllib.request
 
def get_news(symbol, limit=20):
    # symbol format: "NASDAQ:AAPL", "NYSE:TSLA"
    url = (
        f"https://news-headlines.tradingview.com/v2/view/headlines/symbol"
        f"?symbol={symbol}&client=web&streaming=false&lang=en&limit={limit}"
    )
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=15) as r:
        data = json.loads(r.read())
    return data["items"]  # list of news items
 
items = get_news("NASDAQ:AAPL", limit=10)
# item keys: id, title, provider, sourceLogoId, published (unix ts),
#            source, urgency, link, permission, relatedSymbols, storyPath
# example:
# items[0]["title"]     = "Apple Clears Major Legal Hurdle..."
# items[0]["published"] = 1776472317  (unix timestamp)
# items[0]["link"]      = "https://stocktwits.com/..."
# items[0]["relatedSymbols"] = [{"symbol": "NASDAQ:AAPL", "logoid": "apple"}]

No auth or special headers needed. Returns up to 200 items per request.

Published trading ideas feed

import json, urllib.request
 
def get_ideas(sort="trending", page=1, symbol=None):
    # Valid sort values (others return 400):
    # "trending", "recent", "latest_popular", "week_popular",
    # "suggested", "recent_extended", "picked_time"
    url = f"https://www.tradingview.com/api/v1/ideas/?lang=en&sort={sort}&page={page}"
    if symbol:
        url += f"&symbol={symbol}"  # e.g. "NASDAQ:AAPL"
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.loads(r.read())
 
data = get_ideas("trending")
# data["count"]      = 1000 (always 1000 — soft cap)
# data["page_size"]  = 20
# data["page_count"] = 50
# data["next"]       = "https://www.tradingview.com/api/v1/ideas/?page=2"
# data["results"]    = list of idea objects
 
idea = data["results"][0]
# idea keys: id, name, description, created_at, chart_url, views_count,
#            likes_count, comments_count, is_video, is_education, is_hot,
#            symbol (dict with name/exchange/type/interval/direction),
#            user (dict with username/is_pro/badges), image (big/middle URLs)
# idea["symbol"]["direction"]: 1=long, 2=short, 0=neutral
 
# Filter by symbol:
aapl_ideas = get_ideas(symbol="NASDAQ:AAPL")

API summary table

EndpointAuthHeaders neededSpeed
scanner.tradingview.com/{market}/scanNoneNone~200ms
symbol-search.tradingview.com/symbol_search/v3/NoneOrigin: https://www.tradingview.com~150ms
symbol-search.tradingview.com/symbol_search/ (v1)NoneOrigin: https://www.tradingview.com~100ms
news-headlines.tradingview.com/v2/view/headlines/symbolNoneNone~400ms
www.tradingview.com/api/v1/ideas/NoneNone~300ms
data.tradingview.com/quotes/NoneNoneDead - connection refused
economic-calendar.tradingview.com/eventsYes-HTTP 403

Gotchas

Scanner range is half-open: [0, 10] returns rows 0–9 (10 rows total). [10, 20] for the next page.

Column order is critical: The "d" array in each result row is positional - it exactly mirrors your "columns" array. Always zip them: dict(zip(columns, item["d"])).

data.tradingview.com/quotes/ is dead: The URL https://data.tradingview.com/quotes/?symbols=NASDAQ:AAPL closes the connection without a response. Use the scanner API instead for real-time quotes.

Scanner needs no Referer: scanner.tradingview.com works with just User-Agent. The symbol-search subdomain checks Origin (CORS enforcement on the server side).

Symbol search highlights: The v3 endpoint wraps matched text in <em> tags (e.g. "<em>AAPL</em>"). Strip them: re.sub(r'</?em>', '', symbol["symbol"]).

Ideas sort validation: Only specific values work. "sort=popular" returns 400. Use "trending", "recent", "latest_popular", "week_popular", "suggested".

Ideas count cap: The API always reports count=1000 regardless of actual corpus size. With page_size=20, max pages is 50.

Scanner server is AWS CloudFront (X-Amz-Cf-Pop header) with a custom Server: tv - no Cloudflare. No anti-bot on the scanner subdomain. Main www.tradingview.com is a React SPA with window.initData = {} (empty - no embedded data). All data is loaded via API calls after hydration.

Rate limits: No 429s observed in testing. 5 concurrent scanner calls complete in ~1s. Symbol search returns symbols_remaining in the response (counts against some quota - varies 90–180 across calls but never blocks). Observed no blocking after 15 rapid calls in a row.

Sector names: Use TradingView's own taxonomy, not GICS. "Technology" does not exist - use "Electronic Technology" (hardware/semis) or "Technology Services" (software/internet).

When to use the browser

The charting UI (/chart/), symbol detail pages (/symbols/NASDAQ-AAPL/), and the ideas page (/ideas/) are React SPAs - their visible data comes from the APIs above, not embedded HTML. Use browser + JS extraction only if you need visual chart screenshots or data from auth-gated pages (watchlists, portfolio, paper trading).

# Only if you need a chart screenshot:
goto_url("https://www.tradingview.com/chart/?symbol=NASDAQ:AAPL")
wait_for_load()
wait(3)   # chart renders asynchronously after readyState
capture_screenshot("/tmp/aapl_chart.png", full=False)

scraping.md · updated Apr 28, 2026 · 1,444 words

Sites change. These notes describe what worked when they were last updated, not a guarantee they still do. If something broke, the fix is a pull request to browser-harness.