tradingview-mcp 26.0.0__py3-none-any.whl → 26.3.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.
@@ -1,47 +1,52 @@
1
- """Load packaged docs and reference datasets for MCP resources.
2
-
3
- This module provides access to:
4
- - Market metadata (markets.json)
5
- - Column/field display names (column_display_names.json)
6
- - Screener presets (screeners/*.json)
7
- - Market metainfo with field definitions (metainfo/*.json)
8
- - AI-friendly quick reference (extracted/ai_quick_reference.json)
9
- - Screener code examples (extracted/screener_code_examples.json)
10
- - Field definitions by market (extracted/fields_by_market.json)
11
-
12
- Data is loaded from packaged files first, with fallback to the reference path.
13
- Set TRADINGVIEW_SCREENER_DOCS_DATA env var to override the reference path.
14
- """
1
+ """Load packaged docs datasets for MCP resources - AI-friendly fast lookup."""
15
2
 
16
3
  from __future__ import annotations
17
4
 
18
5
  import json
19
- import os
20
6
  from functools import lru_cache
21
7
  from importlib.resources import files
22
- from pathlib import Path
23
8
  from typing import Any
24
9
 
25
10
 
26
- DEFAULT_REFERENCE_ROOT = Path(
27
- os.getenv(
28
- "TRADINGVIEW_SCREENER_DOCS_DATA",
29
- "/home/henrik/tradingview-mcp/referance/TradingView-Screener-docs/data",
30
- )
31
- )
32
-
33
- # Token/size limits for AI-friendly responses
34
- MAX_ITEMS_DEFAULT = 100
35
- MAX_ITEMS_LARGE = 500
36
- MAX_CHARS_DEFAULT = 50000 # ~12.5k tokens
37
- MAX_CHARS_LARGE = 200000 # ~50k tokens
38
-
39
- # Large data warnings
40
- LARGE_DATA_WARNING = (
41
- "⚠️ This data is large ({size} items). "
42
- "Use `limit` parameter to reduce output. "
43
- "Set `confirm_large=True` to retrieve full data."
44
- )
11
+ # Token/size limits
12
+ MAX_ITEMS_DEFAULT = 50
13
+ MAX_ITEMS_LARGE = 200
14
+
15
+ # Valid markets for quick reference
16
+ STOCK_MARKETS = [
17
+ "america", "argentina", "australia", "austria", "bahrain", "bangladesh",
18
+ "belgium", "brazil", "canada", "chile", "china", "colombia", "cyprus",
19
+ "czech", "denmark", "egypt", "estonia", "finland", "france", "germany",
20
+ "greece", "hongkong", "hungary", "iceland", "india", "indonesia", "ireland",
21
+ "israel", "italy", "japan", "kenya", "korea", "ksa", "kuwait", "latvia",
22
+ "lithuania", "luxembourg", "malaysia", "mexico", "morocco", "netherlands",
23
+ "newzealand", "nigeria", "norway", "pakistan", "peru", "philippines",
24
+ "poland", "portugal", "qatar", "romania", "rsa", "russia", "serbia",
25
+ "singapore", "slovakia", "spain", "srilanka", "sweden", "switzerland",
26
+ "taiwan", "thailand", "tunisia", "turkey", "uae", "uk", "venezuela", "vietnam",
27
+ ]
28
+ OTHER_MARKETS = ["bond", "bonds", "cfd", "coin", "crypto", "economics2", "forex", "futures", "options"]
29
+ ALL_MARKETS = STOCK_MARKETS + OTHER_MARKETS
30
+
31
+ # Common field categories for quick lookup
32
+ FIELD_CATEGORIES = {
33
+ "price": ["close", "open", "high", "low", "change", "change_abs", "gap"],
34
+ "volume": ["volume", "Value.Traded", "relative_volume_10d_calc", "average_volume_10d_calc"],
35
+ "market_cap": ["market_cap_basic", "market_cap_calc"],
36
+ "fundamental": ["price_earnings_ttm", "earnings_per_share_basic_ttm", "dividends_yield_current"],
37
+ "technical": ["RSI", "MACD.macd", "BB.upper", "BB.lower", "SMA50", "SMA200", "EMA50", "Recommend.All"],
38
+ "performance": ["Perf.W", "Perf.1M", "Perf.3M", "Perf.6M", "Perf.Y", "Perf.YTD"],
39
+ "metadata": ["name", "description", "sector", "exchange", "type", "currency"],
40
+ }
41
+
42
+ # Filter operations reference
43
+ FILTER_OPS = {
44
+ "comparison": ["greater", "less", "equal", "not_equal", "greater_or_equal", "less_or_equal"],
45
+ "range": ["in_range", "not_in_range"],
46
+ "list": ["in_list", "not_in_list"],
47
+ "text": ["match", "not_match", "has", "has_none_of"],
48
+ "null": ["empty", "not_empty"],
49
+ }
45
50
 
46
51
 
47
52
  def _data_path(*parts: str):
@@ -49,22 +54,12 @@ def _data_path(*parts: str):
49
54
  return files("tradingview_mcp.data").joinpath(*parts)
50
55
 
51
56
 
52
- def _reference_path(*parts: str) -> Path:
53
- """Get path to reference data file."""
54
- return DEFAULT_REFERENCE_ROOT.joinpath(*parts)
55
-
56
-
57
57
  @lru_cache(maxsize=128)
58
58
  def load_json(*parts: str) -> Any:
59
- """Load JSON file from package data or reference fallback."""
60
- try:
61
- path = _data_path(*parts)
62
- with path.open("r", encoding="utf-8") as handle:
63
- return json.load(handle)
64
- except (FileNotFoundError, TypeError):
65
- fallback = _reference_path(*parts)
66
- with fallback.open("r", encoding="utf-8") as handle:
67
- return json.load(handle)
59
+ """Load JSON file from packaged data."""
60
+ path = _data_path(*parts)
61
+ with path.open("r", encoding="utf-8") as handle:
62
+ return json.load(handle)
68
63
 
69
64
 
70
65
  def get_markets_data() -> dict[str, list[str]]:
@@ -98,6 +93,50 @@ def get_screeners_data(name: str) -> Any:
98
93
  return load_json("screeners", name)
99
94
 
100
95
 
96
+ def get_stock_screeners() -> list[dict[str, Any]]:
97
+ """Get stock screeners metadata."""
98
+ return load_json("screeners", "stocks.json")
99
+
100
+
101
+ def get_stock_screeners_failed() -> list[dict[str, Any]]:
102
+ """Get failed stock screeners metadata."""
103
+ return load_json("screeners", "stocks_failed.json")
104
+
105
+
106
+ def get_stock_screener_presets(market: str | None = None) -> dict[str, Any]:
107
+ """Get stock screener presets by market."""
108
+ data = load_json("extracted", "stock_screener_presets.json")
109
+ if market:
110
+ return data.get(market, {})
111
+ return data
112
+
113
+
114
+ def get_valid_fields_for_market(market: str) -> list[str]:
115
+ """Get list of valid field names for a market."""
116
+ fields_data = get_fields_by_market(market)
117
+ if isinstance(fields_data, list):
118
+ return [f.get("name", "") for f in fields_data if f.get("name")]
119
+ return []
120
+
121
+
122
+ def suggest_fields_for_error(error_message: str, market: str = "stocks") -> dict[str, Any]:
123
+ """Suggest valid fields when an error occurs."""
124
+ valid_fields = get_valid_fields_for_market(market)
125
+ common = get_common_fields()
126
+
127
+ # Extract commonly used fields
128
+ common_names = list(common.keys())[:50]
129
+
130
+ return {
131
+ "error": error_message,
132
+ "market": market,
133
+ "suggestion": "Use fields from the valid_fields list",
134
+ "common_fields": common_names,
135
+ "total_valid_fields": len(valid_fields),
136
+ "hint": "Call list_fields_for_market(market) for full field list"
137
+ }
138
+
139
+
101
140
  # ============================================================================
102
141
  # AI-Friendly Data Access with Output Limits
103
142
  # ============================================================================
@@ -126,45 +165,74 @@ def get_fields_by_market(market: str | None = None) -> dict[str, Any]:
126
165
  return data
127
166
 
128
167
 
168
+ def get_default_columns_for_market(market: str) -> list[str]:
169
+ """Get smart default columns for a market to avoid 400 errors."""
170
+ # Base columns safe for all
171
+ cols = ["name", "description", "close", "change", "change_abs", "exchange", "type"]
172
+
173
+ # Check if market has specific fields in data
174
+ fields = get_fields_by_market(market)
175
+ if not fields:
176
+ # Fallback defaults if market data missing
177
+ if market == "crypto":
178
+ return cols + ["volume", "market_cap_basic", "24h_vol|5"]
179
+ if market == "forex":
180
+ return cols + ["bid", "ask"]
181
+ return cols + ["volume", "market_cap_basic", "price_earnings_ttm", "sector", "industry"]
182
+
183
+ # Extract available field names
184
+ valid_names = {f.get("name") for f in fields if f.get("name")}
185
+
186
+ # Add common extra columns if they exist in this market
187
+ extras = [
188
+ "open", "high", "low", "volume", "market_cap_basic",
189
+ "24h_vol|5", "bid", "ask", "price_earnings_ttm",
190
+ "earnings_per_share_basic_ttm", "dividend_yield_recent",
191
+ "sector", "industry"
192
+ ]
193
+
194
+ for extra in extras:
195
+ if extra in valid_names:
196
+ cols.append(extra)
197
+
198
+ # Deduplicate while preserving order
199
+ seen = set()
200
+ unique_cols = []
201
+ for c in cols:
202
+ if c not in seen:
203
+ seen.add(c)
204
+ unique_cols.append(c)
205
+
206
+ return unique_cols
207
+
208
+
129
209
  def paginate_data(
130
210
  data: list | dict,
131
211
  offset: int = 0,
132
212
  limit: int = MAX_ITEMS_DEFAULT,
133
213
  confirm_large: bool = False,
134
214
  ) -> dict[str, Any]:
135
- """Paginate large data with size warnings.
136
-
137
- Args:
138
- data: List or dict to paginate
139
- offset: Starting index (for lists) or skip count (for dicts)
140
- limit: Maximum items to return
141
- confirm_large: If True, allow large outputs without warning
142
-
143
- Returns:
144
- Dict with 'data', 'total', 'offset', 'limit', and optional 'warning'
145
- """
215
+ """Paginate data with offset and limit."""
146
216
  if isinstance(data, list):
147
217
  total = len(data)
148
- items = data[offset:offset + limit]
218
+ items = data if confirm_large else data[offset:offset + limit]
149
219
  else:
150
220
  keys = list(data.keys())
151
221
  total = len(keys)
152
- selected_keys = keys[offset:offset + limit]
153
- items = {k: data[k] for k in selected_keys}
222
+ if confirm_large:
223
+ items = data
224
+ else:
225
+ selected_keys = keys[offset:offset + limit]
226
+ items = {k: data[k] for k in selected_keys}
154
227
 
155
228
  result: dict[str, Any] = {
156
229
  "data": items,
157
230
  "total": total,
158
231
  "offset": offset,
159
- "limit": limit,
160
- "has_more": (offset + limit) < total,
232
+ "limit": total if confirm_large else limit,
233
+ "has_more": False if confirm_large else (offset + limit) < total,
234
+ "next_offset": None if confirm_large else ((offset + limit) if (offset + limit) < total else None),
161
235
  }
162
-
163
- # Add warning for large data if not confirmed
164
- if total > MAX_ITEMS_DEFAULT and not confirm_large and limit >= total:
165
- result["warning"] = LARGE_DATA_WARNING.format(size=total)
166
- result["hint"] = f"Use limit={MAX_ITEMS_DEFAULT} or set confirm_large=True"
167
-
168
236
  return result
169
237
 
170
238
 
@@ -173,16 +241,7 @@ def search_fields(
173
241
  market: str | None = None,
174
242
  limit: int = 20,
175
243
  ) -> list[dict[str, Any]]:
176
- """Search fields by name or display name.
177
-
178
- Args:
179
- query: Search term (case-insensitive)
180
- market: Optional market to search in (stocks, crypto, forex, etc.)
181
- limit: Maximum results to return
182
-
183
- Returns:
184
- List of matching field definitions
185
- """
244
+ """Search fields by name or display name."""
186
245
  query_lower = query.lower()
187
246
 
188
247
  if market:
@@ -212,14 +271,7 @@ def search_fields(
212
271
 
213
272
 
214
273
  def get_field_summary(field_name: str) -> dict[str, Any] | None:
215
- """Get summary info for a specific field.
216
-
217
- Args:
218
- field_name: Field name to look up
219
-
220
- Returns:
221
- Field info dict or None if not found
222
- """
274
+ """Get summary info for a field."""
223
275
  # Check common fields first
224
276
  common = get_common_fields()
225
277
  if field_name in common:
@@ -252,15 +304,7 @@ def truncate_for_tokens(
252
304
  data: Any,
253
305
  max_tokens: int = 10000,
254
306
  ) -> tuple[Any, bool]:
255
- """Truncate data to fit within token limit.
256
-
257
- Args:
258
- data: Data to potentially truncate
259
- max_tokens: Maximum tokens allowed
260
-
261
- Returns:
262
- Tuple of (truncated_data, was_truncated)
263
- """
307
+ """Truncate data to fit within a token limit."""
264
308
  estimated = estimate_tokens(data)
265
309
  if estimated <= max_tokens:
266
310
  return data, False
@@ -295,3 +339,214 @@ def truncate_for_tokens(
295
339
  if len(json_str) > max_chars:
296
340
  return json_str[:max_chars] + "... (truncated)", True
297
341
  return data, False
342
+
343
+
344
+ # ============================================================================
345
+ # Fast Lookup Functions - Returns compact data, not full dumps
346
+ # ============================================================================
347
+
348
+
349
+ def get_quick_reference() -> dict[str, Any]:
350
+ """Get compact quick reference - markets, field categories, filter ops."""
351
+ return {
352
+ "stock_markets": STOCK_MARKETS,
353
+ "other_markets": OTHER_MARKETS,
354
+ "field_categories": FIELD_CATEGORIES,
355
+ "filter_operations": FILTER_OPS,
356
+ "usage": {
357
+ "lookup_field": "lookup_field('RSI') - get field info",
358
+ "search_fields": "search_fields('volume') - search by keyword",
359
+ "get_filter_format": "get_filter_format('price') - get filter example",
360
+ },
361
+ }
362
+
363
+
364
+ def lookup_field(field_name: str) -> dict[str, Any]:
365
+ """Lookup a single field by exact name."""
366
+ # Check common fields
367
+ common = get_common_fields()
368
+ if field_name in common:
369
+ info = common[field_name]
370
+ return {
371
+ "found": True,
372
+ "name": field_name,
373
+ "display_name": info.get("display_name"),
374
+ "type": info.get("type"),
375
+ "variants": info.get("variants"),
376
+ }
377
+
378
+ # Check display names
379
+ display_names = get_column_display_names()
380
+ if field_name in display_names:
381
+ return {
382
+ "found": True,
383
+ "name": field_name,
384
+ "display_name": display_names[field_name],
385
+ }
386
+
387
+ # Not found - suggest similar
388
+ query_lower = field_name.lower()
389
+ suggestions = [
390
+ name for name in list(common.keys())[:200]
391
+ if query_lower in name.lower()
392
+ ][:5]
393
+
394
+ return {
395
+ "found": False,
396
+ "name": field_name,
397
+ "suggestions": suggestions,
398
+ "hint": f"Field '{field_name}' not found. Try one of: {suggestions}" if suggestions else f"Field '{field_name}' not found. Use search_fields() to find available fields.",
399
+ }
400
+
401
+
402
+ def get_filter_format(field_type: str = "number") -> dict[str, Any]:
403
+ """Get correct filter format for a field type."""
404
+ formats = {
405
+ "number": {
406
+ "example": {"column": "RSI", "operation": "lt", "value": 30},
407
+ "operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between"],
408
+ "between_example": {"column": "RSI", "operation": "between", "value": [20, 80]},
409
+ },
410
+ "price": {
411
+ "example": {"column": "close", "operation": "gt", "value": 100},
412
+ "operations": ["gt", "gte", "lt", "lte", "eq", "between"],
413
+ },
414
+ "percent": {
415
+ "example": {"column": "change", "operation": "gt", "value": 5},
416
+ "operations": ["gt", "gte", "lt", "lte", "between"],
417
+ "note": "Values are percentages, e.g., 5 = 5%",
418
+ },
419
+ "text": {
420
+ "example": {"column": "sector", "operation": "eq", "value": "Technology"},
421
+ "operations": ["eq", "neq", "isin"],
422
+ "isin_example": {"column": "exchange", "operation": "isin", "value": ["NASDAQ", "NYSE"]},
423
+ },
424
+ "bool": {
425
+ "example": {"column": "is_primary", "operation": "eq", "value": True},
426
+ "operations": ["eq"],
427
+ },
428
+ }
429
+
430
+ if field_type in formats:
431
+ return {"type": field_type, **formats[field_type]}
432
+
433
+ return {
434
+ "error": f"Unknown type '{field_type}'",
435
+ "available_types": list(formats.keys()),
436
+ "default": formats["number"],
437
+ }
438
+
439
+
440
+ def get_market_fields_summary(market: str) -> dict[str, Any]:
441
+ """Get summary of available fields for a market (not full list)."""
442
+ if market not in ALL_MARKETS:
443
+ return {
444
+ "error": f"Invalid market '{market}'",
445
+ "valid_markets": ALL_MARKETS,
446
+ }
447
+
448
+ # Try metainfo first
449
+ try:
450
+ metainfo = get_metainfo(market if market not in STOCK_MARKETS else "stocks")
451
+ if isinstance(metainfo, list):
452
+ field_count = len(metainfo)
453
+ # Get field types distribution
454
+ types = {}
455
+ for f in metainfo[:500]:
456
+ t = f.get("t", "unknown")
457
+ types[t] = types.get(t, 0) + 1
458
+
459
+ return {
460
+ "market": market,
461
+ "total_fields": field_count,
462
+ "field_types": types,
463
+ "sample_fields": [f.get("n") for f in metainfo[:20]],
464
+ "hint": "Use search_fields(query, market) to find specific fields",
465
+ }
466
+ except FileNotFoundError:
467
+ pass
468
+
469
+ # Fallback to fields_by_market
470
+ fields = get_fields_by_market(market)
471
+ if isinstance(fields, list):
472
+ return {
473
+ "market": market,
474
+ "total_fields": len(fields),
475
+ "sample_fields": [f.get("name") for f in fields[:20]],
476
+ }
477
+
478
+ return {"market": market, "note": "Use get_common_fields() for this market"}
479
+
480
+
481
+ def get_screener_preset_names() -> list[str]:
482
+ """Get list of available screener preset names only."""
483
+ presets = get_screener_presets()
484
+ return [p.get("name", "") for p in presets if p.get("name")]
485
+
486
+
487
+ def get_code_example_names() -> list[str]:
488
+ """Get list of available code example names."""
489
+ examples = get_screener_code_examples()
490
+ return list(examples.keys())
491
+
492
+
493
+ def validate_market(market: str) -> dict[str, Any]:
494
+ """Validate market name and return correct format if invalid."""
495
+ if market in ALL_MARKETS:
496
+ return {"valid": True, "market": market}
497
+
498
+ # Try case-insensitive match
499
+ market_lower = market.lower()
500
+ for m in ALL_MARKETS:
501
+ if m.lower() == market_lower:
502
+ return {"valid": True, "market": m, "normalized": True}
503
+
504
+ # Suggest similar
505
+ suggestions = [m for m in ALL_MARKETS if market_lower in m.lower()][:5]
506
+
507
+ return {
508
+ "valid": False,
509
+ "input": market,
510
+ "suggestions": suggestions if suggestions else ALL_MARKETS[:10],
511
+ "hint": f"Invalid market '{market}'. Use one of the valid markets.",
512
+ }
513
+
514
+
515
+ def build_error_with_format(
516
+ error_type: str,
517
+ context: dict[str, Any] | None = None,
518
+ ) -> dict[str, Any]:
519
+ """Build helpful error response with correct format examples."""
520
+ error_formats = {
521
+ "invalid_market": {
522
+ "error": "Invalid market",
523
+ "valid_markets": ALL_MARKETS,
524
+ "example": "market='america'",
525
+ },
526
+ "invalid_field": {
527
+ "error": "Invalid field",
528
+ "common_fields": list(FIELD_CATEGORIES.get("price", [])) + list(FIELD_CATEGORIES.get("volume", [])),
529
+ "example": "columns=['close', 'volume', 'change']",
530
+ "hint": "Use search_fields(query) to find fields",
531
+ },
532
+ "invalid_filter": {
533
+ "error": "Invalid filter format",
534
+ "correct_format": {"column": "field_name", "operation": "op", "value": "value"},
535
+ "operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
536
+ "examples": [
537
+ {"column": "change", "operation": "gt", "value": 5},
538
+ {"column": "volume", "operation": "gt", "value": 1000000},
539
+ {"column": "RSI", "operation": "between", "value": [20, 30]},
540
+ ],
541
+ },
542
+ "invalid_sort": {
543
+ "error": "Invalid sort field",
544
+ "common_sort_fields": ["volume", "change", "market_cap_basic", "close", "RSI"],
545
+ "example": "sort_by='volume', ascending=False",
546
+ },
547
+ }
548
+
549
+ result = error_formats.get(error_type, {"error": error_type})
550
+ if context:
551
+ result["context"] = context
552
+ return result
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+ from typing import Any
3
+
4
+ from tradingview_mcp.docs_data import (
5
+ get_markets_data,
6
+ get_column_display_names,
7
+ get_screener_presets,
8
+ get_screener_markets,
9
+ get_stock_screeners,
10
+ get_stock_screeners_failed,
11
+ get_fields_by_market,
12
+ get_ai_quick_reference,
13
+ get_screener_code_examples,
14
+ )
15
+ from tradingview_mcp.constants import EXCHANGE_SCREENER
16
+
17
+ def list_markets() -> dict[str, Any]:
18
+ """List all available markets."""
19
+ return get_markets_data()
20
+
21
+ def list_columns() -> dict[str, str]:
22
+ """List all available columns."""
23
+ return get_column_display_names()
24
+
25
+ def list_crypto_exchanges() -> list[str]:
26
+ """List all crypto exchanges."""
27
+ return [k for k, v in EXCHANGE_SCREENER.items() if v == "crypto"]
28
+
29
+ def list_scanner_presets() -> list[dict[str, Any]]:
30
+ """List scanner presets."""
31
+ return get_screener_presets()
32
+
33
+ def docs_params() -> dict[str, Any]:
34
+ """Documentation for parameters."""
35
+ return {"limit": "Max 500", "range": "[0, 100]"}
36
+
37
+ def docs_screeners() -> list[str]:
38
+ """Documentation for screeners."""
39
+ return get_screener_markets()
40
+
41
+ def docs_stock_screeners() -> list[dict[str, Any]]:
42
+ """Documentation for stock screeners."""
43
+ return get_stock_screeners()
44
+
45
+ def docs_stock_screeners_failed() -> list[dict[str, Any]]:
46
+ """Documentation for failed stock screeners."""
47
+ return get_stock_screeners_failed()
48
+
49
+ def docs_fields() -> dict[str, Any]:
50
+ """Documentation for fields."""
51
+ return get_fields_by_market("stocks") # Default to stocks
52
+
53
+ def docs_markets() -> dict[str, Any]:
54
+ """Documentation for markets."""
55
+ return get_markets_data()
56
+
57
+ def docs_ai_reference() -> dict[str, Any]:
58
+ """Documentation for AI reference."""
59
+ return get_ai_quick_reference()
60
+
61
+ def docs_code_examples() -> dict[str, Any]:
62
+ """Documentation for code examples."""
63
+ return get_screener_code_examples()