tradingview-mcp 26.0.0__py3-none-any.whl → 26.2.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
  # ============================================================================
@@ -132,39 +171,27 @@ def paginate_data(
132
171
  limit: int = MAX_ITEMS_DEFAULT,
133
172
  confirm_large: bool = False,
134
173
  ) -> 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
- """
174
+ """Paginate data with offset and limit."""
146
175
  if isinstance(data, list):
147
176
  total = len(data)
148
- items = data[offset:offset + limit]
177
+ items = data if confirm_large else data[offset:offset + limit]
149
178
  else:
150
179
  keys = list(data.keys())
151
180
  total = len(keys)
152
- selected_keys = keys[offset:offset + limit]
153
- items = {k: data[k] for k in selected_keys}
181
+ if confirm_large:
182
+ items = data
183
+ else:
184
+ selected_keys = keys[offset:offset + limit]
185
+ items = {k: data[k] for k in selected_keys}
154
186
 
155
187
  result: dict[str, Any] = {
156
188
  "data": items,
157
189
  "total": total,
158
190
  "offset": offset,
159
- "limit": limit,
160
- "has_more": (offset + limit) < total,
191
+ "limit": total if confirm_large else limit,
192
+ "has_more": False if confirm_large else (offset + limit) < total,
193
+ "next_offset": None if confirm_large else ((offset + limit) if (offset + limit) < total else None),
161
194
  }
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
195
  return result
169
196
 
170
197
 
@@ -173,16 +200,7 @@ def search_fields(
173
200
  market: str | None = None,
174
201
  limit: int = 20,
175
202
  ) -> 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
- """
203
+ """Search fields by name or display name."""
186
204
  query_lower = query.lower()
187
205
 
188
206
  if market:
@@ -212,14 +230,7 @@ def search_fields(
212
230
 
213
231
 
214
232
  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
- """
233
+ """Get summary info for a field."""
223
234
  # Check common fields first
224
235
  common = get_common_fields()
225
236
  if field_name in common:
@@ -252,15 +263,7 @@ def truncate_for_tokens(
252
263
  data: Any,
253
264
  max_tokens: int = 10000,
254
265
  ) -> 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
- """
266
+ """Truncate data to fit within a token limit."""
264
267
  estimated = estimate_tokens(data)
265
268
  if estimated <= max_tokens:
266
269
  return data, False
@@ -295,3 +298,214 @@ def truncate_for_tokens(
295
298
  if len(json_str) > max_chars:
296
299
  return json_str[:max_chars] + "... (truncated)", True
297
300
  return data, False
301
+
302
+
303
+ # ============================================================================
304
+ # Fast Lookup Functions - Returns compact data, not full dumps
305
+ # ============================================================================
306
+
307
+
308
+ def get_quick_reference() -> dict[str, Any]:
309
+ """Get compact quick reference - markets, field categories, filter ops."""
310
+ return {
311
+ "stock_markets": STOCK_MARKETS,
312
+ "other_markets": OTHER_MARKETS,
313
+ "field_categories": FIELD_CATEGORIES,
314
+ "filter_operations": FILTER_OPS,
315
+ "usage": {
316
+ "lookup_field": "lookup_field('RSI') - get field info",
317
+ "search_fields": "search_fields('volume') - search by keyword",
318
+ "get_filter_format": "get_filter_format('price') - get filter example",
319
+ },
320
+ }
321
+
322
+
323
+ def lookup_field(field_name: str) -> dict[str, Any]:
324
+ """Lookup a single field by exact name."""
325
+ # Check common fields
326
+ common = get_common_fields()
327
+ if field_name in common:
328
+ info = common[field_name]
329
+ return {
330
+ "found": True,
331
+ "name": field_name,
332
+ "display_name": info.get("display_name"),
333
+ "type": info.get("type"),
334
+ "variants": info.get("variants"),
335
+ }
336
+
337
+ # Check display names
338
+ display_names = get_column_display_names()
339
+ if field_name in display_names:
340
+ return {
341
+ "found": True,
342
+ "name": field_name,
343
+ "display_name": display_names[field_name],
344
+ }
345
+
346
+ # Not found - suggest similar
347
+ query_lower = field_name.lower()
348
+ suggestions = [
349
+ name for name in list(common.keys())[:200]
350
+ if query_lower in name.lower()
351
+ ][:5]
352
+
353
+ return {
354
+ "found": False,
355
+ "name": field_name,
356
+ "suggestions": suggestions,
357
+ "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.",
358
+ }
359
+
360
+
361
+ def get_filter_format(field_type: str = "number") -> dict[str, Any]:
362
+ """Get correct filter format for a field type."""
363
+ formats = {
364
+ "number": {
365
+ "example": {"column": "RSI", "operation": "lt", "value": 30},
366
+ "operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between"],
367
+ "between_example": {"column": "RSI", "operation": "between", "value": [20, 80]},
368
+ },
369
+ "price": {
370
+ "example": {"column": "close", "operation": "gt", "value": 100},
371
+ "operations": ["gt", "gte", "lt", "lte", "eq", "between"],
372
+ },
373
+ "percent": {
374
+ "example": {"column": "change", "operation": "gt", "value": 5},
375
+ "operations": ["gt", "gte", "lt", "lte", "between"],
376
+ "note": "Values are percentages, e.g., 5 = 5%",
377
+ },
378
+ "text": {
379
+ "example": {"column": "sector", "operation": "eq", "value": "Technology"},
380
+ "operations": ["eq", "neq", "isin"],
381
+ "isin_example": {"column": "exchange", "operation": "isin", "value": ["NASDAQ", "NYSE"]},
382
+ },
383
+ "bool": {
384
+ "example": {"column": "is_primary", "operation": "eq", "value": True},
385
+ "operations": ["eq"],
386
+ },
387
+ }
388
+
389
+ if field_type in formats:
390
+ return {"type": field_type, **formats[field_type]}
391
+
392
+ return {
393
+ "error": f"Unknown type '{field_type}'",
394
+ "available_types": list(formats.keys()),
395
+ "default": formats["number"],
396
+ }
397
+
398
+
399
+ def get_market_fields_summary(market: str) -> dict[str, Any]:
400
+ """Get summary of available fields for a market (not full list)."""
401
+ if market not in ALL_MARKETS:
402
+ return {
403
+ "error": f"Invalid market '{market}'",
404
+ "valid_markets": ALL_MARKETS,
405
+ }
406
+
407
+ # Try metainfo first
408
+ try:
409
+ metainfo = get_metainfo(market if market not in STOCK_MARKETS else "stocks")
410
+ if isinstance(metainfo, list):
411
+ field_count = len(metainfo)
412
+ # Get field types distribution
413
+ types = {}
414
+ for f in metainfo[:500]:
415
+ t = f.get("t", "unknown")
416
+ types[t] = types.get(t, 0) + 1
417
+
418
+ return {
419
+ "market": market,
420
+ "total_fields": field_count,
421
+ "field_types": types,
422
+ "sample_fields": [f.get("n") for f in metainfo[:20]],
423
+ "hint": "Use search_fields(query, market) to find specific fields",
424
+ }
425
+ except FileNotFoundError:
426
+ pass
427
+
428
+ # Fallback to fields_by_market
429
+ fields = get_fields_by_market(market)
430
+ if isinstance(fields, list):
431
+ return {
432
+ "market": market,
433
+ "total_fields": len(fields),
434
+ "sample_fields": [f.get("name") for f in fields[:20]],
435
+ }
436
+
437
+ return {"market": market, "note": "Use get_common_fields() for this market"}
438
+
439
+
440
+ def get_screener_preset_names() -> list[str]:
441
+ """Get list of available screener preset names only."""
442
+ presets = get_screener_presets()
443
+ return [p.get("name", "") for p in presets if p.get("name")]
444
+
445
+
446
+ def get_code_example_names() -> list[str]:
447
+ """Get list of available code example names."""
448
+ examples = get_screener_code_examples()
449
+ return list(examples.keys())
450
+
451
+
452
+ def validate_market(market: str) -> dict[str, Any]:
453
+ """Validate market name and return correct format if invalid."""
454
+ if market in ALL_MARKETS:
455
+ return {"valid": True, "market": market}
456
+
457
+ # Try case-insensitive match
458
+ market_lower = market.lower()
459
+ for m in ALL_MARKETS:
460
+ if m.lower() == market_lower:
461
+ return {"valid": True, "market": m, "normalized": True}
462
+
463
+ # Suggest similar
464
+ suggestions = [m for m in ALL_MARKETS if market_lower in m.lower()][:5]
465
+
466
+ return {
467
+ "valid": False,
468
+ "input": market,
469
+ "suggestions": suggestions if suggestions else ALL_MARKETS[:10],
470
+ "hint": f"Invalid market '{market}'. Use one of the valid markets.",
471
+ }
472
+
473
+
474
+ def build_error_with_format(
475
+ error_type: str,
476
+ context: dict[str, Any] | None = None,
477
+ ) -> dict[str, Any]:
478
+ """Build helpful error response with correct format examples."""
479
+ error_formats = {
480
+ "invalid_market": {
481
+ "error": "Invalid market",
482
+ "valid_markets": ALL_MARKETS,
483
+ "example": "market='america'",
484
+ },
485
+ "invalid_field": {
486
+ "error": "Invalid field",
487
+ "common_fields": list(FIELD_CATEGORIES.get("price", [])) + list(FIELD_CATEGORIES.get("volume", [])),
488
+ "example": "columns=['close', 'volume', 'change']",
489
+ "hint": "Use search_fields(query) to find fields",
490
+ },
491
+ "invalid_filter": {
492
+ "error": "Invalid filter format",
493
+ "correct_format": {"column": "field_name", "operation": "op", "value": "value"},
494
+ "operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
495
+ "examples": [
496
+ {"column": "change", "operation": "gt", "value": 5},
497
+ {"column": "volume", "operation": "gt", "value": 1000000},
498
+ {"column": "RSI", "operation": "between", "value": [20, 30]},
499
+ ],
500
+ },
501
+ "invalid_sort": {
502
+ "error": "Invalid sort field",
503
+ "common_sort_fields": ["volume", "change", "market_cap_basic", "close", "RSI"],
504
+ "example": "sort_by='volume', ascending=False",
505
+ },
506
+ }
507
+
508
+ result = error_formats.get(error_type, {"error": error_type})
509
+ if context:
510
+ result["context"] = context
511
+ return result