tradingview-mcp 26.3.1__py3-none-any.whl → 26.3.2__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.
@@ -49,8 +49,21 @@ def get_filter_example(type: str = "number") -> dict[str, Any]:
49
49
  """Get example filter format."""
50
50
  return get_filter_format(type)
51
51
 
52
+ from tradingview_mcp.utils import sanitize_market
53
+
52
54
  def check_market(market: str) -> dict[str, Any]:
53
55
  """Validate a market name."""
56
+ # Resolve alias first (e.g. 'tw' -> 'taiwan') for consistency with other tools
57
+ # We use strict=False because we want the default 'america' if fails? No, check_market should be explicit.
58
+ # sanitize_market returns default 'america' if invalid.
59
+ # We want to know if it's strictly valid or valid alias.
60
+
61
+ # Check if it's a known alias
62
+ clean = market.strip().lower()
63
+ from tradingview_mcp.utils import COUNTRY_ALIASES
64
+ if clean in COUNTRY_ALIASES:
65
+ return validate_market(COUNTRY_ALIASES[clean])
66
+
54
67
  return validate_market(market)
55
68
 
56
69
  def get_help() -> dict[str, Any]:
@@ -15,7 +15,7 @@ def screen_market(
15
15
  filters: Optional[list[dict[str, Any]]] = None,
16
16
  ) -> dict[str, Any]:
17
17
  """Run a custom market screening query."""
18
- market = sanitize_market(market)
18
+ market = sanitize_market(market, strict=True)
19
19
  limit = max(1, min(limit, 500))
20
20
 
21
21
  # Ensure description is always included
@@ -71,17 +71,17 @@ def screen_market(
71
71
 
72
72
  def get_top_gainers(market: str = "america", limit: int = 25) -> dict[str, Any]:
73
73
  """Get top gainers for a market."""
74
- market = sanitize_market(market)
74
+ market = sanitize_market(market, strict=True)
75
75
  return screen_market(market, sort_by="change", ascending=False, limit=limit)
76
76
 
77
77
 
78
78
  def get_top_losers(market: str = "america", limit: int = 25) -> dict[str, Any]:
79
79
  """Get top losers for a market."""
80
- market = sanitize_market(market)
80
+ market = sanitize_market(market, strict=True)
81
81
  return screen_market(market, sort_by="change", ascending=True, limit=limit)
82
82
 
83
83
 
84
84
  def get_most_active(market: str = "america", limit: int = 25) -> dict[str, Any]:
85
85
  """Get most active symbols by volume."""
86
- market = sanitize_market(market)
86
+ market = sanitize_market(market, strict=True)
87
87
  return screen_market(market, sort_by="volume", ascending=False, limit=limit)
@@ -55,13 +55,92 @@ def search_symbols(
55
55
 
56
56
  try:
57
57
  # Standard search in requested market
58
- results = _execute_search(queries, market, limit, cols, sort_col)
58
+ # We start this async to allow parallel peeking
59
+ with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
60
+ future_main = executor.submit(_execute_search, queries, market, limit, cols, sort_col)
61
+
62
+ # Smart Peek: Always check CFD/Forex/Crypto for EXACT matches or high relevance
63
+ # This solves the "Silver" problem: "Silver" exists in America (Stocks), but user might want "SILVER" (CFD)
64
+ # We don't want to wait for main search to fail (lazy fallback), we want to augment results.
65
+ peek_markets = ["cfd", "crypto", "forex"]
66
+ if market in peek_markets:
67
+ peek_markets.remove(market)
68
+
69
+ future_peeks = []
70
+ for pm in peek_markets:
71
+ # For peeking, we only care about high relevance, so limit is small
72
+ p_cols = get_default_columns_for_market(pm)
73
+ p_sort = "name" # Sort by name match
74
+ future_peeks.append(executor.submit(_execute_search, queries, pm, 5, p_cols, p_sort))
75
+
76
+ # Gather Main Results
77
+ results = future_main.result()
78
+
79
+ # Gather Peek Results
80
+ extra_matches = []
81
+ for f in future_peeks:
82
+ try:
83
+ res = f.result()
84
+ if res.get("results"):
85
+ extra_matches.extend(res["results"])
86
+ except:
87
+ pass
59
88
 
60
- # Smart Fallback: If default market ('america') yielded no results, try others
89
+ # Merge Logic
90
+ # 1. Start with Main Results
91
+ final_list = results.get("results", [])
92
+
93
+ # 2. Inject Exact Matches from Peeks at the TOP
94
+ # (e.g. if query="Silver" and we found "SILVER" in CFD, put it first)
95
+ high_priority = []
96
+ low_priority = []
97
+
98
+ query_upper = query.upper()
99
+
100
+ for m in extra_matches:
101
+ # Check for exact ticker/name match
102
+ n = m.get("name", "").upper()
103
+ tick = m.get("ticker", "").split(":")[-1].upper()
104
+
105
+ if n == query_upper or tick == query_upper or f"{query_upper}USD" in n:
106
+ high_priority.append(m)
107
+ else:
108
+ low_priority.append(m)
109
+
110
+ # specific uniqueness check
111
+ seen = {f"{r.get('exchange')}:{r.get('name')}" for r in final_list}
112
+
113
+ merged = []
114
+ # Add High Priority Peeks (if new)
115
+ for r in high_priority:
116
+ k = f"{r.get('exchange')}:{r.get('name')}"
117
+ if k not in seen:
118
+ merged.append(r)
119
+ seen.add(k)
120
+
121
+ # Add Main Results
122
+ merged.extend(final_list)
123
+
124
+ # Add Low Priority Peeks (only if we have space or main list is empty)
125
+ # Actually, let's append them if main list is small, or specialized matching
126
+ if not final_list:
127
+ for r in low_priority:
128
+ k = f"{r.get('exchange')}:{r.get('name')}"
129
+ if k not in seen:
130
+ merged.append(r)
131
+ seen.add(k)
132
+
133
+ # Update results
134
+ results["results"] = merged
135
+ results["total_found"] = len(merged)
136
+ results["returned"] = len(merged)
137
+
138
+ # Fallback Logic (Only if truly empty)
61
139
  if not results["results"] and market == "america":
62
140
 
63
141
  # 1. Asian Numeric Heuristic
64
142
  if query.isdigit() and len(query) in [4, 5, 6]:
143
+ # ... existing heuristic code ...
65
144
  asian_markets = ["taiwan", "hongkong", "japan", "china", "korea"]
66
145
  for asian_market in asian_markets:
67
146
  asian_cols = get_default_columns_for_market(asian_market)
@@ -98,7 +177,7 @@ def search_symbols(
98
177
  # Add locators for global results
99
178
  records = df.to_dict("records")
100
179
  for r in records:
101
- _enrich_locator(r, "stock", query) # We can't know exact market easily from batch query, assume global context usage
180
+ _enrich_locator(r, "stock", query)
102
181
 
103
182
  return {
104
183
  "query": query,
@@ -111,18 +190,6 @@ def search_symbols(
111
190
  except Exception:
112
191
  pass
113
192
 
114
- # 3. Check Crypto/Forex/CFD
115
- for other in ["crypto", "forex", "cfd"]:
116
- if other == market: continue
117
-
118
- cols = get_default_columns_for_market(other)
119
- sort = "market_cap_basic" if "market_cap_basic" in cols else "name"
120
- if other == "forex" or other == "cfd": sort = "name"
121
-
122
- res = _execute_search(queries, other, 5, cols, sort)
123
- if res["results"]:
124
- return {**res, "note": f"Found matches in '{other}'."}
125
-
126
193
  return results
127
194
 
128
195
  except Exception as e:
@@ -25,7 +25,7 @@ def get_technical_analysis(symbol: str, market: str) -> dict[str, Any]:
25
25
  # Parse strictly formatted symbol
26
26
  exchange, ticker = symbol.split(":", 1)
27
27
 
28
- market = sanitize_market(market)
28
+ market = sanitize_market(market, strict=True)
29
29
 
30
30
  # Use standard technical columns
31
31
  cols = list(TECHNICAL_COLUMNS)
@@ -60,7 +60,7 @@ def get_technical_analysis(symbol: str, market: str) -> dict[str, Any]:
60
60
 
61
61
  def scan_rsi_extremes(market: str = "america", limit: int = 25) -> dict[str, Any]:
62
62
  """Find symbols with extreme RSI values (<30 or >70)."""
63
- market = sanitize_market(market)
63
+ market = sanitize_market(market, strict=True)
64
64
 
65
65
  q = (
66
66
  Query()
@@ -100,7 +100,7 @@ def scan_rsi_extremes(market: str = "america", limit: int = 25) -> dict[str, Any
100
100
 
101
101
  def scan_bollinger_bands(market: str = "america", limit: int = 25) -> dict[str, Any]:
102
102
  """Find symbols trading outside Bollinger Bands."""
103
- market = sanitize_market(market)
103
+ market = sanitize_market(market, strict=True)
104
104
  # This is complex to do with simple filters.
105
105
  # We'll just return basic BB values for top volume stocks.
106
106
 
@@ -120,7 +120,7 @@ def scan_bollinger_bands(market: str = "america", limit: int = 25) -> dict[str,
120
120
 
121
121
  def scan_macd_crossover(market: str = "america", limit: int = 25) -> dict[str, Any]:
122
122
  """scan for MACD crossovers."""
123
- market = sanitize_market(market)
123
+ market = sanitize_market(market, strict=True)
124
124
  q = (
125
125
  Query()
126
126
  .set_markets(market)
tradingview_mcp/utils.py CHANGED
@@ -99,33 +99,42 @@ COUNTRY_ALIASES = {
99
99
  "za": "rsa", # South Africa
100
100
  }
101
101
 
102
- def sanitize_market(market: str | None, default: str = "america") -> str:
102
+ def sanitize_market(market: str | None, default: str = "america", strict: bool = False) -> str:
103
103
  """
104
104
  Validate and sanitize a market name.
105
105
 
106
106
  Args:
107
107
  market: Market to validate (e.g. 'america', 'tw', 'crypto')
108
- default: Default value if invalid
108
+ default: Default value if invalid (only used if strict=False)
109
+ strict: If True, raise ValueError for invalid markets instead of returning default.
109
110
 
110
111
  Returns:
111
112
  Valid market string
113
+
114
+ Raises:
115
+ ValueError: If strict=True and market is invalid.
112
116
  """
113
117
  if not market:
118
+ if strict:
119
+ raise ValueError("Market parameter is required (e.g. 'america', 'taiwan').")
114
120
  return default
115
121
 
116
- market = market.strip().lower()
122
+ market_clean = market.strip().lower()
117
123
 
118
124
  # Check exact match
119
- if market in MARKETS:
120
- return market
125
+ if market_clean in MARKETS:
126
+ return market_clean
121
127
 
122
128
  # Check alias match
123
- if market in COUNTRY_ALIASES:
124
- return COUNTRY_ALIASES[market]
129
+ if market_clean in COUNTRY_ALIASES:
130
+ return COUNTRY_ALIASES[market_clean]
125
131
 
126
132
  # Check if user passed exchange (e.g. 'nasdaq') by mistake
127
- if market in EXCHANGE_SCREENER:
128
- return EXCHANGE_SCREENER[market]
133
+ if market_clean in EXCHANGE_SCREENER:
134
+ return EXCHANGE_SCREENER[market_clean]
135
+
136
+ if strict:
137
+ raise ValueError(f"Invalid market '{market}'. Please use a valid country code or name (e.g. 'america', 'taiwan', 'crypto').")
129
138
 
130
139
  return default
131
140
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradingview-mcp
3
- Version: 26.3.1
3
+ Version: 26.3.2
4
4
  Summary: A comprehensive MCP server for TradingView market screening with integrated screener functionality
5
5
  Project-URL: Homepage, https://github.com/k73a/tradingview-mcp
6
6
  Project-URL: Documentation, https://github.com/k73a/tradingview-mcp#readme
@@ -7,7 +7,7 @@ tradingview_mcp/query.py,sha256=gkC4t-2_jtuUK3KgzU62wrIcGoaOmY--1EwAshGQb0Q,1087
7
7
  tradingview_mcp/resources.py,sha256=6cbsPhXsq6SFh3bmmSnMtrVkyCx1_xpWWiyqNLCCULk,1910
8
8
  tradingview_mcp/scanner.py,sha256=oEfMzaYhQ7ggBdLlPLqKZCMxHkd6MBMaUMcm3p-YvPA,7649
9
9
  tradingview_mcp/server.py,sha256=opC61fKfchYn3-XL5ZlPGvsPRZZoLXe-VqGSulhrJHA,7165
10
- tradingview_mcp/utils.py,sha256=5W0vLcrguwZv8q-ohZfQCqrkxVF1OCWPPVyCJ-bxhOo,11486
10
+ tradingview_mcp/utils.py,sha256=My7SdEz89xfAW8aDsxoK311PGCd6kMFmg2DW2udM098,12007
11
11
  tradingview_mcp/data/__init__.py,sha256=3kot_ZG4a6jbZgyksE9z56n3skI407lsd4y2ThHotKY,298
12
12
  tradingview_mcp/data/column_display_names.json,sha256=AFdo6Q2n-wSXWHi2wEExOd2pTItyjocRbrnxqYpCa94,30137
13
13
  tradingview_mcp/data/markets.json,sha256=CehqCeVdQhWeCkMX1HWOrgCRjOomWa3VH1v2ZvPnIck,1482
@@ -33,12 +33,12 @@ tradingview_mcp/data/screeners/markets.json,sha256=vpdrAKg4E_lgg4FxeX2GHNbnSsKQD
33
33
  tradingview_mcp/data/screeners/stocks.json,sha256=gCnntHTJKJ7n7IqY72fZ4EuBfthtnWUpfHvPqNOvnXo,8843
34
34
  tradingview_mcp/data/screeners/stocks_failed.json,sha256=RqBp9XCZ3xvdWZMwuVGysEE2Er_XOQvBZoUHo9uyj6k,1084574
35
35
  tradingview_mcp/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- tradingview_mcp/tools/reference.py,sha256=BPip9ntzyRwIyE0SoD8TWqSGLYj7Evt-OM1SQ1uPG8o,2160
37
- tradingview_mcp/tools/screener.py,sha256=K6uOUFq4GzPeCE4w2oa1LkAHbodQdQcmexw0W3sofeo,3188
38
- tradingview_mcp/tools/search.py,sha256=_BNdmf4qzUUG5A6orODzm3R2evDcdgBQhY6aRdg09-o,13791
39
- tradingview_mcp/tools/technical.py,sha256=x5raTT-l-Kn6uGUI_U7MLZRGjHXdgPB8Vss9nox3m5A,4500
40
- tradingview_mcp-26.3.1.dist-info/METADATA,sha256=XNaWsK_-K7ei3R8sLivRp4VPEYD4BewD_iCSePjOsXw,4818
41
- tradingview_mcp-26.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
- tradingview_mcp-26.3.1.dist-info/entry_points.txt,sha256=GZxjGqgVbUlWDp5OzFQoCN_g1UBLyOmfVqCR5uzscnU,57
43
- tradingview_mcp-26.3.1.dist-info/licenses/LICENSE,sha256=1Hdpp7qGWCXVw1BP6vpdAPO4KrgO0T_c0N3ipkYHKAo,1070
44
- tradingview_mcp-26.3.1.dist-info/RECORD,,
36
+ tradingview_mcp/tools/reference.py,sha256=Oyc-l2ogjk2Evvqwk4SzgpGFV0vKKkdKJwQJmJbLJrM,2751
37
+ tradingview_mcp/tools/screener.py,sha256=5n-p-yXSljm9uqEfQmeg3EpDq2alDywrptvaxJaYMz8,3240
38
+ tradingview_mcp/tools/search.py,sha256=3uSGKfmKpI6ysc8v4hOLin3XAAKcQHeTBzzFSudt1hw,16328
39
+ tradingview_mcp/tools/technical.py,sha256=8oIrag7qnGltdsbIOkLkKtAUOHRYrp7RMV7Er5X_jAY,4552
40
+ tradingview_mcp-26.3.2.dist-info/METADATA,sha256=OcI-yg571T3ToHOe0Zwf4KW09xAclsPqJab3c6hoy1g,4818
41
+ tradingview_mcp-26.3.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
+ tradingview_mcp-26.3.2.dist-info/entry_points.txt,sha256=GZxjGqgVbUlWDp5OzFQoCN_g1UBLyOmfVqCR5uzscnU,57
43
+ tradingview_mcp-26.3.2.dist-info/licenses/LICENSE,sha256=1Hdpp7qGWCXVw1BP6vpdAPO4KrgO0T_c0N3ipkYHKAo,1070
44
+ tradingview_mcp-26.3.2.dist-info/RECORD,,