tradingview-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tradingview_sdk-0.1.0/.github/workflows/ci.yml +28 -0
- tradingview_sdk-0.1.0/.github/workflows/release.yml +57 -0
- tradingview_sdk-0.1.0/.gitignore +9 -0
- tradingview_sdk-0.1.0/LICENSE +21 -0
- tradingview_sdk-0.1.0/PKG-INFO +267 -0
- tradingview_sdk-0.1.0/README.md +252 -0
- tradingview_sdk-0.1.0/examples/historical_bars.py +124 -0
- tradingview_sdk-0.1.0/examples/run_screener.py +165 -0
- tradingview_sdk-0.1.0/examples/search_and_quote.py +100 -0
- tradingview_sdk-0.1.0/examples/strategies.py +118 -0
- tradingview_sdk-0.1.0/examples/stream_bars.py +116 -0
- tradingview_sdk-0.1.0/examples/stream_quotes.py +108 -0
- tradingview_sdk-0.1.0/pyproject.toml +42 -0
- tradingview_sdk-0.1.0/scripts/generate_fields.py +491 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/__init__.py +117 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/_chart.py +118 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/_http.py +118 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/_protocol.py +78 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/_stream.py +305 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/_sync.py +37 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/auth.py +136 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/bar_stream.py +167 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/bars.py +230 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/client.py +337 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/errors.py +51 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/fields.py +2337 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/models.py +298 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/quotes.py +50 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/screener.py +269 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/scripts.py +307 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/search.py +59 -0
- tradingview_sdk-0.1.0/src/tradingview_sdk/ws.py +128 -0
- tradingview_sdk-0.1.0/tests/conftest.py +22 -0
- tradingview_sdk-0.1.0/tests/fixtures/bars_btcusdt.json +134 -0
- tradingview_sdk-0.1.0/tests/fixtures/listing.html +1611 -0
- tradingview_sdk-0.1.0/tests/fixtures/metainfo.json +1 -0
- tradingview_sdk-0.1.0/tests/fixtures/pine_source.json +1 -0
- tradingview_sdk-0.1.0/tests/fixtures/quote_aapl.json +1 -0
- tradingview_sdk-0.1.0/tests/fixtures/screener_scan.json +1 -0
- tradingview_sdk-0.1.0/tests/fixtures/script_page.html +656 -0
- tradingview_sdk-0.1.0/tests/fixtures/search_aapl.json +1 -0
- tradingview_sdk-0.1.0/tests/live/test_live_rest.py +66 -0
- tradingview_sdk-0.1.0/tests/live/test_live_scripts.py +39 -0
- tradingview_sdk-0.1.0/tests/live/test_live_ws.py +74 -0
- tradingview_sdk-0.1.0/tests/test_bars.py +189 -0
- tradingview_sdk-0.1.0/tests/test_bars_session.py +171 -0
- tradingview_sdk-0.1.0/tests/test_fields.py +108 -0
- tradingview_sdk-0.1.0/tests/test_http.py +40 -0
- tradingview_sdk-0.1.0/tests/test_parsers.py +181 -0
- tradingview_sdk-0.1.0/tests/test_protocol.py +88 -0
- tradingview_sdk-0.1.0/tests/test_quote_stream.py +238 -0
- tradingview_sdk-0.1.0/tests/test_screener_query.py +61 -0
- tradingview_sdk-0.1.0/uv.lock +599 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ['3.11', '3.12']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
25
|
+
run: uv python install ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Run offline tests
|
|
28
|
+
run: uv run --python ${{ matrix.python-version }} pytest
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Tag a version (vX.Y.Z) -> build, publish to PyPI, then create the GitHub Release.
|
|
4
|
+
#
|
|
5
|
+
# Ordered for atomicity: every cheap check runs first as a gate; PyPI (the one
|
|
6
|
+
# irreversible step) publishes only after they pass and is made idempotent with
|
|
7
|
+
# --check-url; the reversible GitHub Release is created last, so its existence
|
|
8
|
+
# means the whole release succeeded. The single job is safe to re-run.
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
tags:
|
|
12
|
+
- 'v*.*.*'
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
release:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: write # needed to create the GitHub Release
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v6
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync
|
|
29
|
+
|
|
30
|
+
# -- gate: nothing external happens until all of these pass --------------
|
|
31
|
+
- name: Verify tag matches package version
|
|
32
|
+
run: |
|
|
33
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
34
|
+
PKG="$(grep -m1 -E '^version[[:space:]]*=' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/')"
|
|
35
|
+
echo "tag=$TAG package=$PKG"
|
|
36
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
37
|
+
echo "::error::Tag v$TAG does not match pyproject version $PKG"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
- name: Run offline tests
|
|
42
|
+
run: uv run pytest
|
|
43
|
+
|
|
44
|
+
- name: Build sdist and wheel
|
|
45
|
+
run: uv build
|
|
46
|
+
|
|
47
|
+
# -- irreversible commit: PyPI (idempotent via --check-url) --------------
|
|
48
|
+
- name: Publish to PyPI
|
|
49
|
+
run: uv publish --check-url https://pypi.org/simple/ --token "${{ secrets.PYPI_API_TOKEN }}"
|
|
50
|
+
|
|
51
|
+
# -- reversible marker: GitHub Release, created last ---------------------
|
|
52
|
+
- name: Create GitHub Release
|
|
53
|
+
uses: softprops/action-gh-release@v2
|
|
54
|
+
with:
|
|
55
|
+
files: dist/*
|
|
56
|
+
generate_release_notes: true
|
|
57
|
+
fail_on_unmatched_files: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 greatricky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tradingview-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial Python SDK for TradingView: quotes, streaming prices, historical bars, screeners, and community strategies
|
|
5
|
+
Author-email: greatricky <greatricky@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: selectolax>=0.3
|
|
11
|
+
Requires-Dist: websockets>=12.0
|
|
12
|
+
Provides-Extra: pandas
|
|
13
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# tradingview-sdk
|
|
17
|
+
|
|
18
|
+
Unofficial Python SDK for TradingView's web APIs:
|
|
19
|
+
|
|
20
|
+
- **Instrument information** — symbol search with exchange, type, ISIN/CUSIP, currency
|
|
21
|
+
- **Latest quotes** — one REST call for price, change, volume, bid/ask, fundamentals
|
|
22
|
+
- **Streaming prices** — websocket client with dynamic subscribe/unsubscribe of many tickers
|
|
23
|
+
- **Historical bars (OHLCV)** — daily/intraday candles over any date range as typed `BarSet`/`Bar` objects (optional pandas `.to_dataframe()`), plus a live bar stream
|
|
24
|
+
- **Stock & ETF screeners** — programmatic equivalents of [tradingview.com/screener](https://www.tradingview.com/screener/) and [tradingview.com/etf-screener](https://www.tradingview.com/etf-screener/) with a fluent query builder
|
|
25
|
+
- **Community strategies** — list open-source strategies, fetch the published backtest **strategy report** (net profit, profit factor, drawdown, trade list, equity curves) and the full **Pine source code**
|
|
26
|
+
|
|
27
|
+
> ⚠️ This SDK uses TradingView's private web endpoints, which are undocumented and may change without notice. It is not affiliated with or endorsed by TradingView. Use responsibly and respect their terms of service.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv sync # from this repo (dev)
|
|
33
|
+
# or
|
|
34
|
+
pip install .
|
|
35
|
+
pip install ".[pandas]" # adds pandas, for BarSet.to_dataframe()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.11+. Dependencies: `httpx`, `websockets`, `selectolax`. `pandas` is an optional extra — everything works without it except `BarSet.to_dataframe()`, which imports it lazily.
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
### Instrument info & quotes (sync)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from tradingview_sdk import TradingView
|
|
46
|
+
|
|
47
|
+
with TradingView() as tv:
|
|
48
|
+
# 1. Instrument information
|
|
49
|
+
for info in tv.search_symbols("AAPL"):
|
|
50
|
+
print(info.full_symbol, info.type, info.isin, info.description)
|
|
51
|
+
|
|
52
|
+
# 2. Latest quote — accepts "NASDAQ:AAPL" or a bare "AAPL"
|
|
53
|
+
quote = tv.get_quote("NASDAQ:AAPL")
|
|
54
|
+
print(quote.last, quote.change, quote.volume, quote["market_cap_basic"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Every method also exists on `AsyncTradingView` with an identical signature:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from tradingview_sdk import AsyncTradingView
|
|
61
|
+
|
|
62
|
+
async with AsyncTradingView() as tv:
|
|
63
|
+
quote = await tv.get_quote("NASDAQ:AAPL")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Streaming prices (websocket)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from tradingview_sdk import QuoteStream
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with QuoteStream() as stream:
|
|
74
|
+
await stream.subscribe("NASDAQ:AAPL", "BINANCE:BTCUSDT")
|
|
75
|
+
|
|
76
|
+
async for update in stream.updates():
|
|
77
|
+
print(update.symbol, update.last_price, update.changes)
|
|
78
|
+
|
|
79
|
+
# add/remove tickers at any time, mid-stream
|
|
80
|
+
await stream.unsubscribe("NASDAQ:AAPL")
|
|
81
|
+
await stream.subscribe("NASDAQ:TSLA", "BINANCE:ETHUSDT")
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `stream.updates("NASDAQ:TSLA")` — filtered iterator for specific symbols; multiple concurrent iterators are fine.
|
|
87
|
+
- `stream.on_update(callback)` — sync or async callback alternative; returns an unregister function.
|
|
88
|
+
- `stream.snapshot("NASDAQ:AAPL")` — latest merged field values.
|
|
89
|
+
- Reconnects automatically with exponential backoff and **resubscribes everything** after a drop.
|
|
90
|
+
|
|
91
|
+
### Historical bars (OHLCV)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from datetime import date, datetime, timezone
|
|
95
|
+
from tradingview_sdk import TradingView, Interval
|
|
96
|
+
|
|
97
|
+
with TradingView() as tv:
|
|
98
|
+
# Index — most-recent N candles (accepts "EXCHANGE:TICKER" or a bare ticker)
|
|
99
|
+
spx = tv.get_bars("SP:SPX", Interval.DAY, bars=300)
|
|
100
|
+
print(spx.symbol, spx.interval, len(spx))
|
|
101
|
+
print(spx.last.close, spx.closes[-5:])
|
|
102
|
+
|
|
103
|
+
# Stock — a daily range; a plain date is the natural input here
|
|
104
|
+
# (no time-of-day / tz needed). `start` wins over `bars`, paging back to reach it.
|
|
105
|
+
aapl = tv.get_bars("NASDAQ:AAPL", Interval.DAY, start=date(2026, 1, 1))
|
|
106
|
+
|
|
107
|
+
# Crypto — an intraday, timezone-aware window. The tz matters because these
|
|
108
|
+
# bars are intraday, and BTCUSDT trades 24/7 so every hour is populated.
|
|
109
|
+
btc = tv.get_bars("BINANCE:BTCUSDT", "60", # "60" = 60-minute bars
|
|
110
|
+
start=datetime(2026, 8, 1, 8, 0, tzinfo=timezone.utc),
|
|
111
|
+
end=datetime(2026, 8, 8, tzinfo=timezone.utc))
|
|
112
|
+
|
|
113
|
+
df = btc.to_dataframe() # optional: pandas DataFrame indexed by UTC time
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `bars=N` returns the N most-recent candles; pass `start`/`end` to fetch a range instead — the SDK paginates automatically. Use a timezone-aware `datetime` for intraday precision, or a plain `date` (or epoch seconds) for daily ranges.
|
|
117
|
+
- `interval` accepts the `Interval` enum (`Interval.MIN_5`, `Interval.HOUR_1`, `Interval.DAY`, `Interval.WEEK`, …) or any raw TradingView resolution string (`"1"`, `"5"`, `"60"`, `"240"`, `"1D"`, `"1W"`, `"1M"`).
|
|
118
|
+
- `adjustment` mirrors TradingView's single chart "ADJ" toggle (plain strings `"splits"`/`"dividends"` work too):
|
|
119
|
+
- `Adjustment.SPLITS` → split-adjusted only (dividends left in) — the default.
|
|
120
|
+
- `Adjustment.DIVIDENDS` → split-adjusted and dividend-adjusted.
|
|
121
|
+
|
|
122
|
+
Splits are always applied (there is no split-off mode), and the latest bar is identical either way — only historical bars change.
|
|
123
|
+
- Each `Bar` has `time` (epoch seconds, UTC), `open`/`high`/`low`/`close`/`volume`, and a `.datetime` (aware UTC). `BarSet` is iterable/indexable with `.last`, `.closes`, `.times`, … and a lazy `.to_dataframe()` (needs pandas only if you call it). Bar times are always UTC, and a naive `start`/`end` `datetime` is interpreted as UTC.
|
|
124
|
+
- Bars load over a websocket chart session; anonymous access returns delayed data (log in for realtime — see [Authentication](#authentication-optional)).
|
|
125
|
+
|
|
126
|
+
**Streaming bars** — `BarStream` mirrors `QuoteStream` for live, updating candles:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import asyncio
|
|
130
|
+
from tradingview_sdk import BarStream
|
|
131
|
+
|
|
132
|
+
async def main():
|
|
133
|
+
async with BarStream() as stream:
|
|
134
|
+
await stream.subscribe("BINANCE:BTCUSDT", "1") # 1-minute bars
|
|
135
|
+
async for u in stream.updates():
|
|
136
|
+
print(u.symbol, u.interval, u.bar.close, "closed" if u.closed else "forming")
|
|
137
|
+
|
|
138
|
+
asyncio.run(main())
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Each `BarUpdate` carries the latest `bar` and `closed` — `False` while the bar is still forming, `True` once a newer bar has started.
|
|
142
|
+
|
|
143
|
+
### Screeners
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from tradingview_sdk import TradingView, ScreenerQuery, Filter
|
|
147
|
+
|
|
148
|
+
with TradingView() as tv:
|
|
149
|
+
stocks = tv.screen_stocks() # defaults mirror the web stock screener
|
|
150
|
+
etfs = tv.screen_etfs() # defaults mirror the web ETF screener
|
|
151
|
+
print(stocks.total_count, etfs.total_count)
|
|
152
|
+
|
|
153
|
+
# Custom query
|
|
154
|
+
query = (
|
|
155
|
+
ScreenerQuery()
|
|
156
|
+
.where(
|
|
157
|
+
Filter.gt("market_cap_basic", 10e9),
|
|
158
|
+
Filter.between("price_earnings_ttm", 0, 15),
|
|
159
|
+
Filter.gt("volume", 2_000_000),
|
|
160
|
+
)
|
|
161
|
+
.select("name", "close", "change", "market_cap_basic", "price_earnings_ttm", "sector")
|
|
162
|
+
.order_by("market_cap_basic")
|
|
163
|
+
.limit(100)
|
|
164
|
+
)
|
|
165
|
+
for row in tv.screen(query):
|
|
166
|
+
print(row.symbol, row["price_earnings_ttm"])
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Any column/filter field the web screener supports works here. Use `.market("germany")`, `.market("crypto")`, etc. for other markets.
|
|
170
|
+
|
|
171
|
+
#### Field catalog
|
|
172
|
+
|
|
173
|
+
All ~1,100 screener fields are available as a typed enum with metadata (generated from TradingView's scanner `metainfo` endpoint — regenerate with `uv run python scripts/generate_fields.py`):
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from tradingview_sdk import Field, FIELDS, field_info, search_fields
|
|
177
|
+
|
|
178
|
+
Field.MARKET_CAP_BASIC # == "market_cap_basic", usable anywhere a field name goes
|
|
179
|
+
Field.RSI.info.description # "Relative Strength Index (14). Range 0-100; <30 oversold, >70 overbought."
|
|
180
|
+
FIELDS["sector"].values # allowed values: ("Commercial Services", ..., "Utilities")
|
|
181
|
+
FIELDS["change"].type # FieldType.PERCENT (12.5 means 12.5%)
|
|
182
|
+
FIELDS["change"].timeframes # ("1", "5", "15", "30", "60", "120", "240", "1W", "1M")
|
|
183
|
+
Field.CHANGE.tf("60") # "change|60" — change on the 60-minute chart
|
|
184
|
+
search_fields("dividend yield") # find fields by name/description substring
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Each `FieldInfo` carries: `name`, `type` (semantics: `percent` = percentage points, `price`/`fundamental_price` = monetary, `time` = UNIX timestamp, `num_slice` = per-period history array), a human-readable `description` (hand-curated for the ~120 most-used fields, auto-derived otherwise), `values` (the allowed values for enumerated text fields like `sector`, `industry`, `exchange`, `type`, `typespecs`), and `timeframes` (fields that exist per chart timeframe via the `"name|tf"` suffix). Some fund classification fields (`asset_class`, `focus`, `niche`, `weighting_scheme`) return internal ids — select the `.tr`-suffixed twin column (e.g. `"asset_class.tr"`) for readable labels.
|
|
188
|
+
|
|
189
|
+
### Community strategies
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from tradingview_sdk import TradingView
|
|
193
|
+
|
|
194
|
+
with TradingView() as tv:
|
|
195
|
+
# List open-source strategies (paginated; also: tv.iter_strategies())
|
|
196
|
+
page = tv.list_strategies()
|
|
197
|
+
for card in page:
|
|
198
|
+
print(card.title, card.author, card.likes, card.url)
|
|
199
|
+
|
|
200
|
+
# Full detail: metadata + published strategy report + Pine source
|
|
201
|
+
s = tv.get_strategy("eUCT3oSF-WW-Pro-Flow-Zones-Miracle-V4") # slug or full URL
|
|
202
|
+
print(s.title, s.chart_symbol, s.chart_interval)
|
|
203
|
+
|
|
204
|
+
r = s.report # the "Strategy report" from the script page
|
|
205
|
+
print(r.all.net_profit, r.all.total_trades, r.all.profit_factor)
|
|
206
|
+
print(r.max_drawdown, r.sharpe_ratio, r.sortino_ratio)
|
|
207
|
+
print(r.trades[:2]) # individual trades
|
|
208
|
+
print(r.buy_hold_curve[:5]) # buy & hold equity curve
|
|
209
|
+
|
|
210
|
+
print(s.source.source_text) # full Pine source (open-source scripts)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Protected/invite-only scripts raise `AuthRequiredError` for the source; the report and metadata still parse when the page publishes them.
|
|
214
|
+
|
|
215
|
+
## Authentication (optional)
|
|
216
|
+
|
|
217
|
+
Everything works anonymously. Logging in upgrades websocket data from delayed to realtime for exchanges you have data entitlements for, per your TradingView account.
|
|
218
|
+
|
|
219
|
+
Copy the `sessionid` and `sessionid_sign` cookies from a logged-in browser (DevTools → Application → Cookies → tradingview.com), then either:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
export TV_SESSION_ID="..."
|
|
223
|
+
export TV_SESSION_ID_SIGN="..."
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
(picked up automatically), or pass explicitly:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from tradingview_sdk import Credentials, QuoteStream, TradingView
|
|
230
|
+
|
|
231
|
+
creds = Credentials(session_id="...", session_sign="...")
|
|
232
|
+
tv = TradingView(credentials=creds)
|
|
233
|
+
stream = QuoteStream(credentials=creds)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
uv sync
|
|
240
|
+
uv run pytest # offline tests (protocol codec, parsers vs recorded fixtures, fake WS server)
|
|
241
|
+
uv run pytest -m live # live smoke tests against real endpoints
|
|
242
|
+
uv run python examples/historical_bars.py
|
|
243
|
+
uv run python examples/stream_quotes.py
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Package layout: pure request-builders/parsers per endpoint (`search.py`, `quotes.py`, `screener.py`, `scripts.py`, `_chart.py`), thin sync/async facades (`client.py`), and async websocket clients (`ws.py` for quotes, `bars.py`/`bar_stream.py` for OHLCV) over the `~m~`-framed TradingView protocol (`_protocol.py`).
|
|
247
|
+
|
|
248
|
+
### Releasing to PyPI
|
|
249
|
+
|
|
250
|
+
Tagging a version runs `.github/workflows/release.yml`, which builds and publishes to PyPI, then creates the matching GitHub Release. To cut a release:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# bump `version` in pyproject.toml (the single source of truth), then:
|
|
254
|
+
git commit -am "Release v0.2.0"
|
|
255
|
+
git tag v0.2.0
|
|
256
|
+
git push origin main --tags
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The workflow guards that the tag matches the package version, runs the offline tests, publishes to PyPI (idempotently), and attaches the built artifacts to the GitHub Release.
|
|
260
|
+
|
|
261
|
+
## Error handling
|
|
262
|
+
|
|
263
|
+
All errors derive from `TradingViewError`: `HTTPStatusError` (with `RateLimitError` for 429 and `AuthRequiredError` for 401/403), `SymbolNotFoundError`, `ParseError` (markup drift), `ProtocolError`, and `StreamClosedError`. Every model keeps the raw payload on `.raw` so new upstream fields remain accessible.
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# tradingview-sdk
|
|
2
|
+
|
|
3
|
+
Unofficial Python SDK for TradingView's web APIs:
|
|
4
|
+
|
|
5
|
+
- **Instrument information** — symbol search with exchange, type, ISIN/CUSIP, currency
|
|
6
|
+
- **Latest quotes** — one REST call for price, change, volume, bid/ask, fundamentals
|
|
7
|
+
- **Streaming prices** — websocket client with dynamic subscribe/unsubscribe of many tickers
|
|
8
|
+
- **Historical bars (OHLCV)** — daily/intraday candles over any date range as typed `BarSet`/`Bar` objects (optional pandas `.to_dataframe()`), plus a live bar stream
|
|
9
|
+
- **Stock & ETF screeners** — programmatic equivalents of [tradingview.com/screener](https://www.tradingview.com/screener/) and [tradingview.com/etf-screener](https://www.tradingview.com/etf-screener/) with a fluent query builder
|
|
10
|
+
- **Community strategies** — list open-source strategies, fetch the published backtest **strategy report** (net profit, profit factor, drawdown, trade list, equity curves) and the full **Pine source code**
|
|
11
|
+
|
|
12
|
+
> ⚠️ This SDK uses TradingView's private web endpoints, which are undocumented and may change without notice. It is not affiliated with or endorsed by TradingView. Use responsibly and respect their terms of service.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync # from this repo (dev)
|
|
18
|
+
# or
|
|
19
|
+
pip install .
|
|
20
|
+
pip install ".[pandas]" # adds pandas, for BarSet.to_dataframe()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.11+. Dependencies: `httpx`, `websockets`, `selectolax`. `pandas` is an optional extra — everything works without it except `BarSet.to_dataframe()`, which imports it lazily.
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
### Instrument info & quotes (sync)
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from tradingview_sdk import TradingView
|
|
31
|
+
|
|
32
|
+
with TradingView() as tv:
|
|
33
|
+
# 1. Instrument information
|
|
34
|
+
for info in tv.search_symbols("AAPL"):
|
|
35
|
+
print(info.full_symbol, info.type, info.isin, info.description)
|
|
36
|
+
|
|
37
|
+
# 2. Latest quote — accepts "NASDAQ:AAPL" or a bare "AAPL"
|
|
38
|
+
quote = tv.get_quote("NASDAQ:AAPL")
|
|
39
|
+
print(quote.last, quote.change, quote.volume, quote["market_cap_basic"])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Every method also exists on `AsyncTradingView` with an identical signature:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from tradingview_sdk import AsyncTradingView
|
|
46
|
+
|
|
47
|
+
async with AsyncTradingView() as tv:
|
|
48
|
+
quote = await tv.get_quote("NASDAQ:AAPL")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Streaming prices (websocket)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import asyncio
|
|
55
|
+
from tradingview_sdk import QuoteStream
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
async with QuoteStream() as stream:
|
|
59
|
+
await stream.subscribe("NASDAQ:AAPL", "BINANCE:BTCUSDT")
|
|
60
|
+
|
|
61
|
+
async for update in stream.updates():
|
|
62
|
+
print(update.symbol, update.last_price, update.changes)
|
|
63
|
+
|
|
64
|
+
# add/remove tickers at any time, mid-stream
|
|
65
|
+
await stream.unsubscribe("NASDAQ:AAPL")
|
|
66
|
+
await stream.subscribe("NASDAQ:TSLA", "BINANCE:ETHUSDT")
|
|
67
|
+
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `stream.updates("NASDAQ:TSLA")` — filtered iterator for specific symbols; multiple concurrent iterators are fine.
|
|
72
|
+
- `stream.on_update(callback)` — sync or async callback alternative; returns an unregister function.
|
|
73
|
+
- `stream.snapshot("NASDAQ:AAPL")` — latest merged field values.
|
|
74
|
+
- Reconnects automatically with exponential backoff and **resubscribes everything** after a drop.
|
|
75
|
+
|
|
76
|
+
### Historical bars (OHLCV)
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from datetime import date, datetime, timezone
|
|
80
|
+
from tradingview_sdk import TradingView, Interval
|
|
81
|
+
|
|
82
|
+
with TradingView() as tv:
|
|
83
|
+
# Index — most-recent N candles (accepts "EXCHANGE:TICKER" or a bare ticker)
|
|
84
|
+
spx = tv.get_bars("SP:SPX", Interval.DAY, bars=300)
|
|
85
|
+
print(spx.symbol, spx.interval, len(spx))
|
|
86
|
+
print(spx.last.close, spx.closes[-5:])
|
|
87
|
+
|
|
88
|
+
# Stock — a daily range; a plain date is the natural input here
|
|
89
|
+
# (no time-of-day / tz needed). `start` wins over `bars`, paging back to reach it.
|
|
90
|
+
aapl = tv.get_bars("NASDAQ:AAPL", Interval.DAY, start=date(2026, 1, 1))
|
|
91
|
+
|
|
92
|
+
# Crypto — an intraday, timezone-aware window. The tz matters because these
|
|
93
|
+
# bars are intraday, and BTCUSDT trades 24/7 so every hour is populated.
|
|
94
|
+
btc = tv.get_bars("BINANCE:BTCUSDT", "60", # "60" = 60-minute bars
|
|
95
|
+
start=datetime(2026, 8, 1, 8, 0, tzinfo=timezone.utc),
|
|
96
|
+
end=datetime(2026, 8, 8, tzinfo=timezone.utc))
|
|
97
|
+
|
|
98
|
+
df = btc.to_dataframe() # optional: pandas DataFrame indexed by UTC time
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `bars=N` returns the N most-recent candles; pass `start`/`end` to fetch a range instead — the SDK paginates automatically. Use a timezone-aware `datetime` for intraday precision, or a plain `date` (or epoch seconds) for daily ranges.
|
|
102
|
+
- `interval` accepts the `Interval` enum (`Interval.MIN_5`, `Interval.HOUR_1`, `Interval.DAY`, `Interval.WEEK`, …) or any raw TradingView resolution string (`"1"`, `"5"`, `"60"`, `"240"`, `"1D"`, `"1W"`, `"1M"`).
|
|
103
|
+
- `adjustment` mirrors TradingView's single chart "ADJ" toggle (plain strings `"splits"`/`"dividends"` work too):
|
|
104
|
+
- `Adjustment.SPLITS` → split-adjusted only (dividends left in) — the default.
|
|
105
|
+
- `Adjustment.DIVIDENDS` → split-adjusted and dividend-adjusted.
|
|
106
|
+
|
|
107
|
+
Splits are always applied (there is no split-off mode), and the latest bar is identical either way — only historical bars change.
|
|
108
|
+
- Each `Bar` has `time` (epoch seconds, UTC), `open`/`high`/`low`/`close`/`volume`, and a `.datetime` (aware UTC). `BarSet` is iterable/indexable with `.last`, `.closes`, `.times`, … and a lazy `.to_dataframe()` (needs pandas only if you call it). Bar times are always UTC, and a naive `start`/`end` `datetime` is interpreted as UTC.
|
|
109
|
+
- Bars load over a websocket chart session; anonymous access returns delayed data (log in for realtime — see [Authentication](#authentication-optional)).
|
|
110
|
+
|
|
111
|
+
**Streaming bars** — `BarStream` mirrors `QuoteStream` for live, updating candles:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
from tradingview_sdk import BarStream
|
|
116
|
+
|
|
117
|
+
async def main():
|
|
118
|
+
async with BarStream() as stream:
|
|
119
|
+
await stream.subscribe("BINANCE:BTCUSDT", "1") # 1-minute bars
|
|
120
|
+
async for u in stream.updates():
|
|
121
|
+
print(u.symbol, u.interval, u.bar.close, "closed" if u.closed else "forming")
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Each `BarUpdate` carries the latest `bar` and `closed` — `False` while the bar is still forming, `True` once a newer bar has started.
|
|
127
|
+
|
|
128
|
+
### Screeners
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from tradingview_sdk import TradingView, ScreenerQuery, Filter
|
|
132
|
+
|
|
133
|
+
with TradingView() as tv:
|
|
134
|
+
stocks = tv.screen_stocks() # defaults mirror the web stock screener
|
|
135
|
+
etfs = tv.screen_etfs() # defaults mirror the web ETF screener
|
|
136
|
+
print(stocks.total_count, etfs.total_count)
|
|
137
|
+
|
|
138
|
+
# Custom query
|
|
139
|
+
query = (
|
|
140
|
+
ScreenerQuery()
|
|
141
|
+
.where(
|
|
142
|
+
Filter.gt("market_cap_basic", 10e9),
|
|
143
|
+
Filter.between("price_earnings_ttm", 0, 15),
|
|
144
|
+
Filter.gt("volume", 2_000_000),
|
|
145
|
+
)
|
|
146
|
+
.select("name", "close", "change", "market_cap_basic", "price_earnings_ttm", "sector")
|
|
147
|
+
.order_by("market_cap_basic")
|
|
148
|
+
.limit(100)
|
|
149
|
+
)
|
|
150
|
+
for row in tv.screen(query):
|
|
151
|
+
print(row.symbol, row["price_earnings_ttm"])
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Any column/filter field the web screener supports works here. Use `.market("germany")`, `.market("crypto")`, etc. for other markets.
|
|
155
|
+
|
|
156
|
+
#### Field catalog
|
|
157
|
+
|
|
158
|
+
All ~1,100 screener fields are available as a typed enum with metadata (generated from TradingView's scanner `metainfo` endpoint — regenerate with `uv run python scripts/generate_fields.py`):
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from tradingview_sdk import Field, FIELDS, field_info, search_fields
|
|
162
|
+
|
|
163
|
+
Field.MARKET_CAP_BASIC # == "market_cap_basic", usable anywhere a field name goes
|
|
164
|
+
Field.RSI.info.description # "Relative Strength Index (14). Range 0-100; <30 oversold, >70 overbought."
|
|
165
|
+
FIELDS["sector"].values # allowed values: ("Commercial Services", ..., "Utilities")
|
|
166
|
+
FIELDS["change"].type # FieldType.PERCENT (12.5 means 12.5%)
|
|
167
|
+
FIELDS["change"].timeframes # ("1", "5", "15", "30", "60", "120", "240", "1W", "1M")
|
|
168
|
+
Field.CHANGE.tf("60") # "change|60" — change on the 60-minute chart
|
|
169
|
+
search_fields("dividend yield") # find fields by name/description substring
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Each `FieldInfo` carries: `name`, `type` (semantics: `percent` = percentage points, `price`/`fundamental_price` = monetary, `time` = UNIX timestamp, `num_slice` = per-period history array), a human-readable `description` (hand-curated for the ~120 most-used fields, auto-derived otherwise), `values` (the allowed values for enumerated text fields like `sector`, `industry`, `exchange`, `type`, `typespecs`), and `timeframes` (fields that exist per chart timeframe via the `"name|tf"` suffix). Some fund classification fields (`asset_class`, `focus`, `niche`, `weighting_scheme`) return internal ids — select the `.tr`-suffixed twin column (e.g. `"asset_class.tr"`) for readable labels.
|
|
173
|
+
|
|
174
|
+
### Community strategies
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from tradingview_sdk import TradingView
|
|
178
|
+
|
|
179
|
+
with TradingView() as tv:
|
|
180
|
+
# List open-source strategies (paginated; also: tv.iter_strategies())
|
|
181
|
+
page = tv.list_strategies()
|
|
182
|
+
for card in page:
|
|
183
|
+
print(card.title, card.author, card.likes, card.url)
|
|
184
|
+
|
|
185
|
+
# Full detail: metadata + published strategy report + Pine source
|
|
186
|
+
s = tv.get_strategy("eUCT3oSF-WW-Pro-Flow-Zones-Miracle-V4") # slug or full URL
|
|
187
|
+
print(s.title, s.chart_symbol, s.chart_interval)
|
|
188
|
+
|
|
189
|
+
r = s.report # the "Strategy report" from the script page
|
|
190
|
+
print(r.all.net_profit, r.all.total_trades, r.all.profit_factor)
|
|
191
|
+
print(r.max_drawdown, r.sharpe_ratio, r.sortino_ratio)
|
|
192
|
+
print(r.trades[:2]) # individual trades
|
|
193
|
+
print(r.buy_hold_curve[:5]) # buy & hold equity curve
|
|
194
|
+
|
|
195
|
+
print(s.source.source_text) # full Pine source (open-source scripts)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Protected/invite-only scripts raise `AuthRequiredError` for the source; the report and metadata still parse when the page publishes them.
|
|
199
|
+
|
|
200
|
+
## Authentication (optional)
|
|
201
|
+
|
|
202
|
+
Everything works anonymously. Logging in upgrades websocket data from delayed to realtime for exchanges you have data entitlements for, per your TradingView account.
|
|
203
|
+
|
|
204
|
+
Copy the `sessionid` and `sessionid_sign` cookies from a logged-in browser (DevTools → Application → Cookies → tradingview.com), then either:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
export TV_SESSION_ID="..."
|
|
208
|
+
export TV_SESSION_ID_SIGN="..."
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
(picked up automatically), or pass explicitly:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from tradingview_sdk import Credentials, QuoteStream, TradingView
|
|
215
|
+
|
|
216
|
+
creds = Credentials(session_id="...", session_sign="...")
|
|
217
|
+
tv = TradingView(credentials=creds)
|
|
218
|
+
stream = QuoteStream(credentials=creds)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Development
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uv sync
|
|
225
|
+
uv run pytest # offline tests (protocol codec, parsers vs recorded fixtures, fake WS server)
|
|
226
|
+
uv run pytest -m live # live smoke tests against real endpoints
|
|
227
|
+
uv run python examples/historical_bars.py
|
|
228
|
+
uv run python examples/stream_quotes.py
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Package layout: pure request-builders/parsers per endpoint (`search.py`, `quotes.py`, `screener.py`, `scripts.py`, `_chart.py`), thin sync/async facades (`client.py`), and async websocket clients (`ws.py` for quotes, `bars.py`/`bar_stream.py` for OHLCV) over the `~m~`-framed TradingView protocol (`_protocol.py`).
|
|
232
|
+
|
|
233
|
+
### Releasing to PyPI
|
|
234
|
+
|
|
235
|
+
Tagging a version runs `.github/workflows/release.yml`, which builds and publishes to PyPI, then creates the matching GitHub Release. To cut a release:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# bump `version` in pyproject.toml (the single source of truth), then:
|
|
239
|
+
git commit -am "Release v0.2.0"
|
|
240
|
+
git tag v0.2.0
|
|
241
|
+
git push origin main --tags
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The workflow guards that the tag matches the package version, runs the offline tests, publishes to PyPI (idempotently), and attaches the built artifacts to the GitHub Release.
|
|
245
|
+
|
|
246
|
+
## Error handling
|
|
247
|
+
|
|
248
|
+
All errors derive from `TradingViewError`: `HTTPStatusError` (with `RateLimitError` for 429 and `AuthRequiredError` for 401/403), `SymbolNotFoundError`, `ParseError` (markup drift), `ProtocolError`, and `StreamClosedError`. Every model keeps the raw payload on `.raw` so new upstream fields remain accessible.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT — see [LICENSE](LICENSE).
|