tradingview-mcp 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,154 @@
1
+ """
2
+ Type definitions and models for TradingView Screener.
3
+
4
+ Provides TypedDicts for API request/response structures.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Literal, TypedDict
10
+
11
+
12
+ class FilterOperationDict(TypedDict):
13
+ """Filter operation for WHERE clauses."""
14
+
15
+ left: str
16
+ operation: Literal[
17
+ "greater",
18
+ "egreater",
19
+ "less",
20
+ "eless",
21
+ "equal",
22
+ "nequal",
23
+ "in_range",
24
+ "not_in_range",
25
+ "empty",
26
+ "nempty",
27
+ "crosses",
28
+ "crosses_above",
29
+ "crosses_below",
30
+ "match",
31
+ "nmatch",
32
+ "smatch",
33
+ "has",
34
+ "has_none_of",
35
+ "above%",
36
+ "below%",
37
+ "in_range%",
38
+ "not_in_range%",
39
+ "in_day_range",
40
+ "in_week_range",
41
+ "in_month_range",
42
+ ]
43
+ right: Any
44
+
45
+
46
+ class SortByDict(TypedDict, total=False):
47
+ """Sort configuration."""
48
+
49
+ sortBy: str
50
+ sortOrder: Literal["asc", "desc"]
51
+ nullsFirst: bool
52
+
53
+
54
+ class SymbolsDict(TypedDict, total=False):
55
+ """Symbols configuration for queries."""
56
+
57
+ query: dict[Literal["types"], list[str]]
58
+ tickers: list[str]
59
+ symbolset: list[str]
60
+ watchlist: dict[Literal["id"], int]
61
+ groups: list[dict[Literal["type", "values"], str]]
62
+
63
+
64
+ class ExpressionDict(TypedDict):
65
+ """Single expression wrapper."""
66
+
67
+ expression: FilterOperationDict
68
+
69
+
70
+ class OperationComparisonDict(TypedDict):
71
+ """AND/OR operation for complex filters."""
72
+
73
+ operator: Literal["and", "or"]
74
+ operands: list[Any] # Can be OperationDict or ExpressionDict
75
+
76
+
77
+ class OperationDict(TypedDict):
78
+ """Operation wrapper for complex filters."""
79
+
80
+ operation: OperationComparisonDict
81
+
82
+
83
+ class QueryDict(TypedDict, total=False):
84
+ """Complete query structure for TradingView API."""
85
+
86
+ markets: list[str]
87
+ symbols: SymbolsDict
88
+ options: dict[str, Any]
89
+ columns: list[str]
90
+ filter: list[FilterOperationDict]
91
+ filter2: OperationComparisonDict
92
+ sort: SortByDict
93
+ range: list[int]
94
+ ignore_unknown_fields: bool
95
+ preset: str
96
+ price_conversion: dict[str, Any]
97
+
98
+
99
+ class ScreenerRowDict(TypedDict):
100
+ """Single row in screener response."""
101
+
102
+ s: str # symbol (NASDAQ:AAPL)
103
+ d: list[Any] # data values
104
+
105
+
106
+ class ScreenerDict(TypedDict):
107
+ """Screener API response structure."""
108
+
109
+ totalCount: int
110
+ data: list[ScreenerRowDict]
111
+
112
+
113
+ class IndicatorMap(TypedDict, total=False):
114
+ """Common technical indicators."""
115
+
116
+ open: float | None
117
+ high: float | None
118
+ low: float | None
119
+ close: float | None
120
+ volume: float | None
121
+ change: float | None
122
+ SMA20: float | None
123
+ SMA50: float | None
124
+ SMA200: float | None
125
+ EMA20: float | None
126
+ EMA50: float | None
127
+ EMA200: float | None
128
+ RSI: float | None
129
+ RSI7: float | None
130
+ MACD_macd: float | None
131
+ MACD_signal: float | None
132
+ BB_upper: float | None
133
+ BB_lower: float | None
134
+ ADX: float | None
135
+ ATR: float | None
136
+ Stoch_K: float | None
137
+ Stoch_D: float | None
138
+ VWAP: float | None
139
+
140
+
141
+ class AnalysisResult(TypedDict, total=False):
142
+ """Complete analysis result for a symbol."""
143
+
144
+ symbol: str
145
+ exchange: str
146
+ timeframe: str
147
+ timestamp: str
148
+ price: float
149
+ change_percent: float
150
+ indicators: IndicatorMap
151
+ bollinger: dict[str, Any]
152
+ signals: list[str]
153
+ rating: int
154
+ recommendation: str
@@ -0,0 +1,367 @@
1
+ """
2
+ Query class for building and executing TradingView screener queries.
3
+
4
+ Provides a SQL-like interface for market screening.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import pprint
11
+ from typing import TYPE_CHECKING, Any, Literal
12
+
13
+ import pandas as pd
14
+ import requests
15
+
16
+ from tradingview_mcp.column import Column
17
+ from tradingview_mcp.constants import (
18
+ COLUMNS,
19
+ DEFAULT_RANGE,
20
+ HEADERS,
21
+ MARKETS,
22
+ URL,
23
+ get_column_name,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from tradingview_mcp.models import (
28
+ FilterOperationDict,
29
+ OperationDict,
30
+ QueryDict,
31
+ SortByDict,
32
+ )
33
+
34
+
35
+ def _impl_and_or_chaining(
36
+ expressions: tuple[FilterOperationDict | OperationDict, ...], operator: Literal["and", "or"]
37
+ ) -> OperationDict:
38
+ """Internal helper for AND/OR expression chaining."""
39
+ lst = []
40
+ for expr in expressions:
41
+ if "left" in expr: # FilterOperationDict
42
+ lst.append({"expression": expr})
43
+ else:
44
+ lst.append(expr)
45
+ return {"operation": {"operator": operator, "operands": lst}}
46
+
47
+
48
+ def And(*expressions: FilterOperationDict | OperationDict) -> OperationDict:
49
+ """
50
+ Combine filter expressions with AND logic.
51
+
52
+ Examples:
53
+ >>> And(Column('close') > 10, Column('volume') > 1000000)
54
+ """
55
+ return _impl_and_or_chaining(expressions, operator="and")
56
+
57
+
58
+ def Or(*expressions: FilterOperationDict | OperationDict) -> OperationDict:
59
+ """
60
+ Combine filter expressions with OR logic.
61
+
62
+ Examples:
63
+ >>> Or(Column('type') == 'stock', Column('type') == 'fund')
64
+ """
65
+ return _impl_and_or_chaining(expressions, operator="or")
66
+
67
+
68
+ class Query:
69
+ """
70
+ Build and execute TradingView screener queries.
71
+
72
+ Provides a SQL-like interface for market screening with support for:
73
+ - Column selection (SELECT)
74
+ - Filtering (WHERE)
75
+ - Sorting (ORDER BY)
76
+ - Pagination (OFFSET, LIMIT)
77
+ - Multiple markets
78
+
79
+ Examples:
80
+ Basic query:
81
+ >>> Query().get_scanner_data()
82
+
83
+ Custom columns:
84
+ >>> Query().select('name', 'close', 'RSI', 'MACD.macd').get_scanner_data()
85
+
86
+ With filters:
87
+ >>> Query().select('name', 'close').where(Column('RSI') < 30).get_scanner_data()
88
+
89
+ Multiple conditions:
90
+ >>> Query().where(
91
+ ... Column('close') > 10,
92
+ ... Column('volume') > 1000000,
93
+ ... Column('RSI').between(30, 70)
94
+ ... ).get_scanner_data()
95
+
96
+ Crypto market:
97
+ >>> Query().set_markets('crypto').get_scanner_data()
98
+ """
99
+
100
+ def __init__(self) -> None:
101
+ """Initialize a new Query with default settings."""
102
+ self.query: QueryDict = {
103
+ "markets": ["america"],
104
+ "symbols": {"query": {"types": []}, "tickers": []},
105
+ "options": {"lang": "en"},
106
+ "columns": ["name", "close", "volume", "market_cap_basic"],
107
+ "sort": {"sortBy": "Value.Traded", "sortOrder": "desc"},
108
+ "range": DEFAULT_RANGE.copy(),
109
+ }
110
+ self.url = "https://scanner.tradingview.com/america/scan"
111
+
112
+ def set_markets(self, *markets: str) -> "Query":
113
+ """
114
+ Set the market(s) to query.
115
+
116
+ Available markets include:
117
+ - Crypto: 'crypto', 'coin'
118
+ - Countries: 'america', 'uk', 'germany', etc. (67 countries)
119
+ - Other: 'forex', 'futures', 'bonds', 'cfd', 'options'
120
+
121
+ Examples:
122
+ >>> Query().set_markets('crypto')
123
+ >>> Query().set_markets('america', 'uk', 'germany')
124
+
125
+ Args:
126
+ markets: One or more market identifiers
127
+
128
+ Returns:
129
+ Self for method chaining
130
+ """
131
+ if len(markets) == 1:
132
+ market = markets[0]
133
+ if market not in MARKETS:
134
+ raise ValueError(f"Unknown market: {market}. Available: {sorted(MARKETS)}")
135
+ self.url = URL.format(market=market)
136
+ self.query["markets"] = [market]
137
+ elif len(markets) > 1:
138
+ for m in markets:
139
+ if m not in MARKETS:
140
+ raise ValueError(f"Unknown market: {m}. Available: {sorted(MARKETS)}")
141
+ self.url = URL.format(market="global")
142
+ self.query["markets"] = list(markets)
143
+ return self
144
+
145
+ def set_tickers(self, *tickers: str) -> "Query":
146
+ """
147
+ Set specific tickers to query.
148
+
149
+ Args:
150
+ tickers: One or more tickers in format 'EXCHANGE:SYMBOL'
151
+
152
+ Examples:
153
+ >>> Query().set_tickers('NASDAQ:TSLA', 'NYSE:GME', 'BINANCE:BTCUSDT')
154
+
155
+ Returns:
156
+ Self for method chaining
157
+ """
158
+ self.query.pop("markets", None)
159
+ self.query["symbols"] = {"tickers": list(tickers)}
160
+ self.url = "https://scanner.tradingview.com/global/scan"
161
+ return self
162
+
163
+ def select(self, *columns: Column | str) -> "Query":
164
+ """
165
+ Set columns to return in results.
166
+
167
+ Accepts both Column objects and string names.
168
+ Human-readable names are automatically converted to API names.
169
+
170
+ Examples:
171
+ >>> Query().select('name', 'close', 'RSI', 'MACD.macd')
172
+ >>> Query().select(Column('name'), 'close', 'Relative Strength Index (14)')
173
+
174
+ Args:
175
+ columns: Column objects or column names
176
+
177
+ Returns:
178
+ Self for method chaining
179
+ """
180
+ self.query["columns"] = [
181
+ col.name if isinstance(col, Column) else get_column_name(col) for col in columns
182
+ ]
183
+ return self
184
+
185
+ def where(self, *expressions: FilterOperationDict) -> "Query":
186
+ """
187
+ Add filter conditions (combined with AND).
188
+
189
+ Examples:
190
+ >>> Query().where(Column('RSI') < 30)
191
+ >>> Query().where(
192
+ ... Column('close') > 10,
193
+ ... Column('volume') > 1000000
194
+ ... )
195
+
196
+ Args:
197
+ expressions: Filter expressions created by Column comparisons
198
+
199
+ Returns:
200
+ Self for method chaining
201
+ """
202
+ self.query["filter"] = list(expressions)
203
+ return self
204
+
205
+ def where2(self, operation: OperationDict) -> "Query":
206
+ """
207
+ Add complex filter with AND/OR logic.
208
+
209
+ Use And() and Or() functions to build complex expressions.
210
+
211
+ Examples:
212
+ >>> Query().where2(
213
+ ... Or(
214
+ ... And(Column('type') == 'stock', Column('RSI') < 30),
215
+ ... And(Column('type') == 'fund', Column('change') > 5)
216
+ ... )
217
+ ... )
218
+
219
+ Args:
220
+ operation: Operation created by And() or Or()
221
+
222
+ Returns:
223
+ Self for method chaining
224
+ """
225
+ self.query["filter2"] = operation["operation"]
226
+ return self
227
+
228
+ def order_by(self, column: Column | str, ascending: bool = True) -> "Query":
229
+ """
230
+ Set sort order for results.
231
+
232
+ Examples:
233
+ >>> Query().order_by('volume', ascending=False) # Highest volume first
234
+ >>> Query().order_by('RSI', ascending=True) # Lowest RSI first
235
+
236
+ Args:
237
+ column: Column to sort by
238
+ ascending: True for ascending, False for descending
239
+
240
+ Returns:
241
+ Self for method chaining
242
+ """
243
+ column_name = column.name if isinstance(column, Column) else get_column_name(column)
244
+ sort_order: Literal["asc", "desc"] = "asc" if ascending else "desc"
245
+ self.query["sort"] = {"sortBy": column_name, "sortOrder": sort_order}
246
+ return self
247
+
248
+ def offset(self, offset: int) -> "Query":
249
+ """
250
+ Set starting position for results (for pagination).
251
+
252
+ Examples:
253
+ >>> Query().offset(50).limit(50) # Get results 50-100
254
+
255
+ Args:
256
+ offset: Starting index (0-based)
257
+
258
+ Returns:
259
+ Self for method chaining
260
+ """
261
+ self.query["range"][0] = offset
262
+ return self
263
+
264
+ def limit(self, limit: int) -> "Query":
265
+ """
266
+ Set maximum number of results to return.
267
+
268
+ Examples:
269
+ >>> Query().limit(100)
270
+
271
+ Args:
272
+ limit: Maximum results (default 50)
273
+
274
+ Returns:
275
+ Self for method chaining
276
+ """
277
+ self.query["range"][1] = limit
278
+ return self
279
+
280
+ def get_scanner_data(
281
+ self, *, timeout: int = 20, **kwargs: Any
282
+ ) -> tuple[int, pd.DataFrame]:
283
+ """
284
+ Execute the query and return results.
285
+
286
+ Args:
287
+ timeout: Request timeout in seconds
288
+ **kwargs: Additional arguments passed to requests.post()
289
+
290
+ Returns:
291
+ Tuple of (total_count, DataFrame)
292
+ - total_count: Total matching records (may be more than returned)
293
+ - DataFrame: Results with columns as specified in select()
294
+
295
+ Raises:
296
+ requests.HTTPError: If the API request fails
297
+ """
298
+ kwargs.setdefault("headers", HEADERS)
299
+ kwargs.setdefault("timeout", timeout)
300
+
301
+ r = requests.post(self.url, json=self.query, **kwargs)
302
+
303
+ if r.status_code >= 400:
304
+ r.reason += f"\nBody: {r.text}\n"
305
+ r.raise_for_status()
306
+
307
+ json_obj = r.json()
308
+ rows_count = json_obj.get("totalCount", 0)
309
+ data = json_obj.get("data", [])
310
+
311
+ df = pd.DataFrame(
312
+ data=([row["s"], *row["d"]] for row in data),
313
+ columns=["ticker", *self.query.get("columns", [])],
314
+ )
315
+ return rows_count, df
316
+
317
+ def get_scanner_data_raw(self, *, timeout: int = 20, **kwargs: Any) -> dict[str, Any]:
318
+ """
319
+ Execute the query and return raw JSON response.
320
+
321
+ Useful when you don't need DataFrame conversion.
322
+
323
+ Returns:
324
+ Raw JSON response dict
325
+ """
326
+ kwargs.setdefault("headers", HEADERS)
327
+ kwargs.setdefault("timeout", timeout)
328
+
329
+ r = requests.post(self.url, json=self.query, **kwargs)
330
+ r.raise_for_status()
331
+ return r.json()
332
+
333
+ def copy(self) -> "Query":
334
+ """Create a deep copy of this query."""
335
+ new = Query()
336
+ new.query = copy.deepcopy(self.query)
337
+ new.url = self.url
338
+ return new
339
+
340
+ def __repr__(self) -> str:
341
+ return f"<Query {pprint.pformat(self.query)}>"
342
+
343
+ def __eq__(self, other: object) -> bool:
344
+ return isinstance(other, Query) and self.query == other.query
345
+
346
+
347
+ def get_all_symbols(market: str = "america") -> list[str]:
348
+ """
349
+ Get all symbols for a given market.
350
+
351
+ Examples:
352
+ >>> get_all_symbols('america')
353
+ ['NASDAQ:AAPL', 'NYSE:GME', ...]
354
+
355
+ >>> get_all_symbols('crypto')
356
+ ['BINANCE:BTCUSDT', 'COINBASE:ETHUSD', ...]
357
+
358
+ Args:
359
+ market: Market identifier (default 'america')
360
+
361
+ Returns:
362
+ List of ticker strings in 'EXCHANGE:SYMBOL' format
363
+ """
364
+ r = requests.get(URL.format(market=market), headers=HEADERS, timeout=20)
365
+ r.raise_for_status()
366
+ data = r.json().get("data", [])
367
+ return [dct["s"] for dct in data]