tradingview-mcp 26.3.0__py3-none-any.whl → 26.3.1__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.
- tradingview_mcp/tools/search.py +172 -148
- tradingview_mcp/tools/technical.py +27 -4
- tradingview_mcp/utils.py +62 -3
- {tradingview_mcp-26.3.0.dist-info → tradingview_mcp-26.3.1.dist-info}/METADATA +1 -1
- {tradingview_mcp-26.3.0.dist-info → tradingview_mcp-26.3.1.dist-info}/RECORD +8 -8
- {tradingview_mcp-26.3.0.dist-info → tradingview_mcp-26.3.1.dist-info}/WHEEL +0 -0
- {tradingview_mcp-26.3.0.dist-info → tradingview_mcp-26.3.1.dist-info}/entry_points.txt +0 -0
- {tradingview_mcp-26.3.0.dist-info → tradingview_mcp-26.3.1.dist-info}/licenses/LICENSE +0 -0
tradingview_mcp/tools/search.py
CHANGED
|
@@ -10,14 +10,34 @@ from tradingview_mcp.docs_data import get_default_columns_for_market, STOCK_MARK
|
|
|
10
10
|
from tradingview_mcp.query import Query
|
|
11
11
|
from tradingview_mcp.utils import sanitize_market
|
|
12
12
|
|
|
13
|
+
# Common aliases for commodities/forex that don't match TV names
|
|
14
|
+
SYMBOL_ALIASES = {
|
|
15
|
+
# Precious Metals
|
|
16
|
+
"XAG": ["SILVER", "XAGUSD"],
|
|
17
|
+
"XAU": ["GOLD", "XAUUSD"],
|
|
18
|
+
"XPT": ["PLATINUM", "XPTUSD"],
|
|
19
|
+
"XPD": ["PALLADIUM", "XPDUSD"],
|
|
20
|
+
|
|
21
|
+
# Energy
|
|
22
|
+
"OIL": ["USOIL", "UKOIL", "WTI", "BRENT"],
|
|
23
|
+
"WTI": ["USOIL"],
|
|
24
|
+
"BRENT": ["UKOIL"],
|
|
25
|
+
"NATGAS": ["NG1!", "NATURALGAS"],
|
|
26
|
+
|
|
27
|
+
# Others
|
|
28
|
+
"COPPER": ["HG1!", "XCUUSD"],
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
def search_symbols(
|
|
14
32
|
query: str,
|
|
15
33
|
market: str = "america",
|
|
16
34
|
limit: int = 25,
|
|
17
35
|
) -> dict[str, Any]:
|
|
18
36
|
"""
|
|
19
|
-
Search symbols by name or ticker.
|
|
20
|
-
|
|
37
|
+
Search symbols by name or ticker. Returns strict locator format for downstream tools.
|
|
38
|
+
|
|
39
|
+
Output format includes 'locator': 'EXCHANGE:SYMBOL, market_name'.
|
|
40
|
+
Use this locator to call get_technical_analysis() or other tools precisely.
|
|
21
41
|
"""
|
|
22
42
|
market = sanitize_market(market)
|
|
23
43
|
limit = max(1, min(limit, 100))
|
|
@@ -28,29 +48,31 @@ def search_symbols(
|
|
|
28
48
|
# Determine sort column
|
|
29
49
|
sort_col = "market_cap_basic" if "market_cap_basic" in cols else "name"
|
|
30
50
|
|
|
51
|
+
# Expand query with aliases if available
|
|
52
|
+
queries = [query]
|
|
53
|
+
if query.upper() in SYMBOL_ALIASES:
|
|
54
|
+
queries.extend(SYMBOL_ALIASES[query.upper()])
|
|
55
|
+
|
|
31
56
|
try:
|
|
32
57
|
# Standard search in requested market
|
|
33
|
-
results = _execute_search(
|
|
58
|
+
results = _execute_search(queries, market, limit, cols, sort_col)
|
|
34
59
|
|
|
35
60
|
# Smart Fallback: If default market ('america') yielded no results, try others
|
|
36
61
|
if not results["results"] and market == "america":
|
|
37
62
|
|
|
38
|
-
# 1. Asian Numeric Heuristic
|
|
63
|
+
# 1. Asian Numeric Heuristic
|
|
39
64
|
if query.isdigit() and len(query) in [4, 5, 6]:
|
|
40
65
|
asian_markets = ["taiwan", "hongkong", "japan", "china", "korea"]
|
|
41
66
|
for asian_market in asian_markets:
|
|
42
67
|
asian_cols = get_default_columns_for_market(asian_market)
|
|
43
|
-
asian_sort = "volume"
|
|
44
|
-
asian_res = _execute_search(
|
|
68
|
+
asian_sort = "volume"
|
|
69
|
+
asian_res = _execute_search(queries, asian_market, 5, asian_cols, asian_sort)
|
|
45
70
|
if asian_res["results"]:
|
|
46
|
-
return {**asian_res, "note": f"No results in 'america',
|
|
71
|
+
return {**asian_res, "note": f"No results in 'america', using numeric heuristic for '{asian_market}'."}
|
|
47
72
|
|
|
48
|
-
# 2. True Global Search
|
|
73
|
+
# 2. True Global Search
|
|
49
74
|
try:
|
|
50
|
-
# Use standard global columns safe for all
|
|
51
75
|
global_cols = ["name", "description", "close", "change", "volume", "market_cap_basic", "exchange", "type"]
|
|
52
|
-
|
|
53
|
-
# Check Description First (often better for "Maybank")
|
|
54
76
|
q = (
|
|
55
77
|
Query()
|
|
56
78
|
.set_markets(*STOCK_MARKETS)
|
|
@@ -62,7 +84,6 @@ def search_symbols(
|
|
|
62
84
|
_, df = q.get_scanner_data()
|
|
63
85
|
|
|
64
86
|
if df.empty:
|
|
65
|
-
# Check Name (Ticker)
|
|
66
87
|
q = (
|
|
67
88
|
Query()
|
|
68
89
|
.set_markets(*STOCK_MARKETS)
|
|
@@ -74,31 +95,33 @@ def search_symbols(
|
|
|
74
95
|
_, df = q.get_scanner_data()
|
|
75
96
|
|
|
76
97
|
if not df.empty:
|
|
77
|
-
#
|
|
78
|
-
|
|
98
|
+
# Add locators for global results
|
|
99
|
+
records = df.to_dict("records")
|
|
100
|
+
for r in records:
|
|
101
|
+
_enrich_locator(r, "stock", query) # We can't know exact market easily from batch query, assume global context usage
|
|
102
|
+
|
|
79
103
|
return {
|
|
80
104
|
"query": query,
|
|
81
105
|
"market": "global",
|
|
82
|
-
"total_found":
|
|
106
|
+
"total_found": len(df),
|
|
83
107
|
"returned": len(df),
|
|
84
|
-
"results":
|
|
85
|
-
"note": "
|
|
108
|
+
"results": records,
|
|
109
|
+
"note": "Found matches in global stock markets."
|
|
86
110
|
}
|
|
87
|
-
|
|
88
111
|
except Exception:
|
|
89
|
-
pass
|
|
112
|
+
pass
|
|
90
113
|
|
|
91
|
-
# 3. Check Crypto/Forex
|
|
92
|
-
for other in ["crypto", "forex"]:
|
|
114
|
+
# 3. Check Crypto/Forex/CFD
|
|
115
|
+
for other in ["crypto", "forex", "cfd"]:
|
|
93
116
|
if other == market: continue
|
|
94
117
|
|
|
95
118
|
cols = get_default_columns_for_market(other)
|
|
96
119
|
sort = "market_cap_basic" if "market_cap_basic" in cols else "name"
|
|
97
|
-
if other == "forex": sort = "name"
|
|
120
|
+
if other == "forex" or other == "cfd": sort = "name"
|
|
98
121
|
|
|
99
|
-
res = _execute_search(
|
|
122
|
+
res = _execute_search(queries, other, 5, cols, sort)
|
|
100
123
|
if res["results"]:
|
|
101
|
-
return {**res, "note": f"
|
|
124
|
+
return {**res, "note": f"Found matches in '{other}'."}
|
|
102
125
|
|
|
103
126
|
return results
|
|
104
127
|
|
|
@@ -106,74 +129,86 @@ def search_symbols(
|
|
|
106
129
|
return _search_symbols_fallback(query, market, limit, cols, str(e))
|
|
107
130
|
|
|
108
131
|
|
|
109
|
-
def
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
def _enrich_locator(record: dict, market: str, category: str = "stock"):
|
|
133
|
+
"""Add standard locator string to a record."""
|
|
134
|
+
ex = record.get("exchange", "UNKNOWN")
|
|
135
|
+
name = record.get("name", "UNKNOWN")
|
|
136
|
+
|
|
137
|
+
# Try to deduce market if 'stock' is generic
|
|
138
|
+
# (This is hard without a map of Exchange->Country, but we do our best)
|
|
139
|
+
|
|
140
|
+
# Construct Locator
|
|
141
|
+
# Format: EXCHANGE:SYMBOL, Market
|
|
142
|
+
record["locator"] = f"{ex}:{name}, {market}"
|
|
143
|
+
record["market"] = market
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _execute_search(queries: list[str] | str, market: str, limit: int, cols: list[str], sort_col: str) -> dict[str, Any]:
|
|
147
|
+
"""Execute search, supporting multiple query terms (aliases)."""
|
|
148
|
+
if isinstance(queries, str):
|
|
149
|
+
queries = [queries]
|
|
150
|
+
|
|
151
|
+
all_results = []
|
|
152
|
+
|
|
153
|
+
# Try each query term until we get enough results
|
|
154
|
+
for q_term in queries:
|
|
155
|
+
if len(all_results) >= limit: break
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
try:
|
|
158
|
+
# 1. Search description
|
|
159
|
+
q = (
|
|
127
160
|
Query()
|
|
128
161
|
.set_markets(market)
|
|
129
162
|
.select(*cols)
|
|
130
|
-
.where(Column("
|
|
163
|
+
.where(Column("description").like(q_term))
|
|
131
164
|
.order_by(sort_col, ascending=False)
|
|
132
165
|
.limit(limit)
|
|
133
166
|
)
|
|
134
|
-
|
|
135
|
-
|
|
167
|
+
_, df = q.get_scanner_data()
|
|
168
|
+
if not df.empty:
|
|
169
|
+
all_results.extend(df.to_dict("records"))
|
|
136
170
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
# 2. Search name
|
|
172
|
+
if len(all_results) < limit:
|
|
173
|
+
q2 = (
|
|
174
|
+
Query()
|
|
175
|
+
.set_markets(market)
|
|
176
|
+
.select(*cols)
|
|
177
|
+
.where(Column("name").like(q_term))
|
|
178
|
+
.order_by(sort_col, ascending=False)
|
|
179
|
+
.limit(limit)
|
|
180
|
+
)
|
|
181
|
+
_, df2 = q2.get_scanner_data()
|
|
182
|
+
if not df2.empty:
|
|
183
|
+
all_results.extend(df2.to_dict("records"))
|
|
184
|
+
except:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Dedup
|
|
188
|
+
unique = {r.get("name")+r.get("exchange"): r for r in all_results}
|
|
189
|
+
results = list(unique.values())[:limit]
|
|
190
|
+
|
|
191
|
+
# Enhance with locator
|
|
192
|
+
for r in results:
|
|
193
|
+
_enrich_locator(r, market)
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
"query": queries[0],
|
|
197
|
+
"market": market,
|
|
198
|
+
"total_found": len(results),
|
|
199
|
+
"returned": len(results),
|
|
200
|
+
"results": results,
|
|
201
|
+
}
|
|
147
202
|
|
|
148
203
|
|
|
149
204
|
def _search_symbols_fallback(query: str, market: str, limit: int, cols: list[str], error: str) -> dict[str, Any]:
|
|
150
|
-
"""
|
|
151
|
-
try:
|
|
152
|
-
q = Query().set_markets(market).select(*cols).limit(100)
|
|
153
|
-
_, df = q.get_scanner_data()
|
|
154
|
-
q_lower = query.lower()
|
|
155
|
-
mask = df["name"].str.lower().str.contains(q_lower) | df["description"].str.lower().str.contains(q_lower)
|
|
156
|
-
df_filtered = df[mask].head(limit)
|
|
157
|
-
return {
|
|
158
|
-
"query": query,
|
|
159
|
-
"market": market,
|
|
160
|
-
"returned": len(df_filtered),
|
|
161
|
-
"results": df_filtered.to_dict("records"),
|
|
162
|
-
"note": "Used fallback local search.",
|
|
163
|
-
"original_error": error,
|
|
164
|
-
}
|
|
165
|
-
except Exception as e:
|
|
166
|
-
return {"error": f"Search failed: {str(e)}", "original_error": error}
|
|
205
|
+
return {"error": f"Search failed: {str(e)}", "original_error": error}
|
|
167
206
|
|
|
168
207
|
|
|
169
208
|
def get_symbol_info(symbol: str, include_technical: bool = False) -> dict[str, Any]:
|
|
170
209
|
"""
|
|
171
210
|
Get detailed information for a symbol.
|
|
172
|
-
|
|
173
|
-
Modes:
|
|
174
|
-
1. Strict: 'EXCHANGE:SYMBOL' (e.g. 'NASDAQ:AAPL') - searches only that exchange/market.
|
|
175
|
-
2. Universal: 'SYMBOL' (e.g. 'AAA') - searches ALL global markets, Crypto, and Forex.
|
|
176
|
-
Returns ALL matches found to verify ambiguity.
|
|
211
|
+
Returns matches with strict locators: 'EXCHANGE:SYMBOL, Market'.
|
|
177
212
|
"""
|
|
178
213
|
technical_cols = [
|
|
179
214
|
"RSI", "RSI7", "MACD.macd", "MACD.signal", "SMA20", "SMA50", "SMA200",
|
|
@@ -187,12 +222,13 @@ def get_symbol_info(symbol: str, include_technical: bool = False) -> dict[str, A
|
|
|
187
222
|
exchange, ticker = symbol.split(":", 1)
|
|
188
223
|
market = EXCHANGE_SCREENER.get(exchange.lower()) or "america"
|
|
189
224
|
|
|
190
|
-
#
|
|
225
|
+
# Allow override via sanitized lookup? No, specific exchange implies specific market logic usually.
|
|
226
|
+
# But let's be safe.
|
|
227
|
+
|
|
191
228
|
cols = get_default_columns_for_market(market)
|
|
192
229
|
if include_technical: cols.extend(technical_cols)
|
|
193
230
|
cols = list(dict.fromkeys(cols))
|
|
194
231
|
|
|
195
|
-
# Query
|
|
196
232
|
q = (
|
|
197
233
|
Query()
|
|
198
234
|
.set_markets(market)
|
|
@@ -204,6 +240,10 @@ def get_symbol_info(symbol: str, include_technical: bool = False) -> dict[str, A
|
|
|
204
240
|
results = df.to_dict("records")
|
|
205
241
|
|
|
206
242
|
if results:
|
|
243
|
+
# Add strict locators
|
|
244
|
+
for r in results:
|
|
245
|
+
_enrich_locator(r, market)
|
|
246
|
+
|
|
207
247
|
if len(results) == 1:
|
|
208
248
|
return {"symbol": symbol, "found": True, "market": market, "data": results[0]}
|
|
209
249
|
return {"symbol": symbol, "found": True, "market": market, "matches": results}
|
|
@@ -211,92 +251,76 @@ def get_symbol_info(symbol: str, include_technical: bool = False) -> dict[str, A
|
|
|
211
251
|
return {"symbol": symbol, "found": False, "hint": f"Symbol not found in {market} ({exchange}). Check format."}
|
|
212
252
|
|
|
213
253
|
# --- Universal Mode (Implicit Market) ---
|
|
214
|
-
# We search Stocks (Global), Crypto, vs Forex in parallel using ThreadPoolExecutor
|
|
215
254
|
all_matches = []
|
|
216
255
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
256
|
+
# Targets
|
|
257
|
+
targets = [symbol, symbol.upper(), f"{symbol}USDT", f"{symbol}USD"]
|
|
258
|
+
if symbol.upper() in SYMBOL_ALIASES:
|
|
259
|
+
targets.extend(SYMBOL_ALIASES[symbol.upper()])
|
|
260
|
+
|
|
261
|
+
# Helper
|
|
262
|
+
def run_search(market, col_getter):
|
|
263
|
+
try:
|
|
264
|
+
cols = col_getter(market)
|
|
265
|
+
if include_technical: cols.extend(technical_cols)
|
|
266
|
+
cols = list(dict.fromkeys(cols))
|
|
222
267
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
.
|
|
229
|
-
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
268
|
+
# We need to set markets properly
|
|
269
|
+
q = Query()
|
|
270
|
+
if market == "global_stocks":
|
|
271
|
+
q.set_markets(*STOCK_MARKETS)
|
|
272
|
+
else:
|
|
273
|
+
q.set_markets(market)
|
|
274
|
+
|
|
275
|
+
q.select(*cols).where(Column("name").isin(targets))
|
|
276
|
+
|
|
277
|
+
if market == "global_stocks":
|
|
278
|
+
q.order_by("market_cap_basic", ascending=False).limit(10)
|
|
279
|
+
else:
|
|
280
|
+
q.limit(5)
|
|
281
|
+
|
|
282
|
+
_, df = q.get_scanner_data()
|
|
283
|
+
if not df.empty:
|
|
284
|
+
matches = df.to_dict("records")
|
|
285
|
+
cat = "stock" if market == "global_stocks" else market
|
|
286
|
+
|
|
287
|
+
# Fixup locator logic
|
|
288
|
+
for m in matches:
|
|
289
|
+
m["_category"] = cat
|
|
290
|
+
# If global stock, we don't know exact country easily without map.
|
|
291
|
+
# But for now we pass 'global_stocks' or try to guess?
|
|
292
|
+
# It's better to tell AI "taiwan" if possible.
|
|
293
|
+
# But we queried ALL markets.
|
|
294
|
+
# HACK: We should query 'taiwan' explicitly if heuristics match?
|
|
295
|
+
# No, just return EXCHANGE:SYMBOL and let AI use 'search_symbols' if it needs precise market.
|
|
296
|
+
# Actually user wants "TWSE:0050, taiwan".
|
|
297
|
+
# If we used set_markets(*ALL), we lose the origin market info in the response unless we select 'market' column?
|
|
298
|
+
# TradingView API doesn't usually return 'market' column. It returns 'exchange'.
|
|
299
|
+
# We can map Exchange -> Market via EXCHANGE_SCREENER?
|
|
300
|
+
rec_market = EXCHANGE_SCREENER.get(m.get("exchange", "").lower(), cat)
|
|
301
|
+
_enrich_locator(m, rec_market)
|
|
302
|
+
|
|
257
303
|
return matches
|
|
258
|
-
|
|
304
|
+
except:
|
|
259
305
|
pass
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def check_forex():
|
|
263
|
-
try:
|
|
264
|
-
forex_cols = get_default_columns_for_market("forex")
|
|
265
|
-
if include_technical: forex_cols.extend(technical_cols)
|
|
266
|
-
forex_cols = list(dict.fromkeys(forex_cols))
|
|
267
|
-
|
|
268
|
-
q_forex = (
|
|
269
|
-
Query()
|
|
270
|
-
.set_markets("forex")
|
|
271
|
-
.select(*forex_cols)
|
|
272
|
-
.where(Column("name").isin([symbol, symbol.upper()]))
|
|
273
|
-
.limit(5)
|
|
274
|
-
)
|
|
275
|
-
_, df_forex = q_forex.get_scanner_data()
|
|
276
|
-
if not df_forex.empty:
|
|
277
|
-
matches = df_forex.to_dict("records")
|
|
278
|
-
for m in matches: m["_category"] = "forex"
|
|
279
|
-
return matches
|
|
280
|
-
except Exception:
|
|
281
|
-
pass
|
|
282
|
-
return []
|
|
306
|
+
return []
|
|
283
307
|
|
|
284
308
|
# Execute in parallel
|
|
285
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=
|
|
309
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
|
286
310
|
futures = [
|
|
287
|
-
executor.submit(
|
|
288
|
-
executor.submit(
|
|
289
|
-
executor.submit(
|
|
311
|
+
executor.submit(run_search, "global_stocks", lambda m: ["name", "description", "close", "change", "volume", "market_cap_basic", "exchange", "type"]),
|
|
312
|
+
executor.submit(run_search, "crypto", get_default_columns_for_market),
|
|
313
|
+
executor.submit(run_search, "forex", get_default_columns_for_market),
|
|
314
|
+
executor.submit(run_search, "cfd", get_default_columns_for_market)
|
|
290
315
|
]
|
|
291
316
|
for future in concurrent.futures.as_completed(futures):
|
|
292
317
|
all_matches.extend(future.result())
|
|
293
318
|
|
|
294
319
|
# --- Aggregate Results ---
|
|
295
320
|
if not all_matches:
|
|
296
|
-
# Try search_symbols as last resort (fuzzy search)
|
|
297
321
|
return search_symbols(symbol, "america", 5)
|
|
298
322
|
|
|
299
|
-
# Remove duplicates
|
|
323
|
+
# Remove duplicates
|
|
300
324
|
unique_matches = {}
|
|
301
325
|
for m in all_matches:
|
|
302
326
|
key = m.get("ticker", m.get("name"))
|
|
@@ -6,8 +6,25 @@ from tradingview_mcp.constants import TECHNICAL_COLUMNS
|
|
|
6
6
|
from tradingview_mcp.query import Query
|
|
7
7
|
from tradingview_mcp.utils import sanitize_market
|
|
8
8
|
|
|
9
|
-
def get_technical_analysis(symbol: str, market: str
|
|
10
|
-
"""
|
|
9
|
+
def get_technical_analysis(symbol: str, market: str) -> dict[str, Any]:
|
|
10
|
+
"""
|
|
11
|
+
Get technical indicators for a symbol.
|
|
12
|
+
|
|
13
|
+
IMPORTANT: You must use a strictly formatted symbol and market.
|
|
14
|
+
1. Call `get_symbol_info(symbol)` first.
|
|
15
|
+
2. Use the `locator` field from the response (e.g. "EXCHANGE:SYMBOL") and the exact `market`.
|
|
16
|
+
3. Do NOT guess the market.
|
|
17
|
+
"""
|
|
18
|
+
# Strict validation
|
|
19
|
+
if ":" not in symbol:
|
|
20
|
+
return {
|
|
21
|
+
"error": "Ambiguous symbol format. Missing exchange prefix.",
|
|
22
|
+
"hint": f"Please run get_symbol_info('{symbol}') first to find the correct 'EXCHANGE:{symbol}' locator and 'market'."
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Parse strictly formatted symbol
|
|
26
|
+
exchange, ticker = symbol.split(":", 1)
|
|
27
|
+
|
|
11
28
|
market = sanitize_market(market)
|
|
12
29
|
|
|
13
30
|
# Use standard technical columns
|
|
@@ -18,13 +35,19 @@ def get_technical_analysis(symbol: str, market: str = "america") -> dict[str, An
|
|
|
18
35
|
Query()
|
|
19
36
|
.set_markets(market)
|
|
20
37
|
.select(*cols)
|
|
21
|
-
.where(Column("name").isin([
|
|
38
|
+
.where(Column("name").isin([ticker, ticker.upper()])) # Use ticker part for name lookup
|
|
22
39
|
.limit(1)
|
|
23
40
|
)
|
|
41
|
+
|
|
24
42
|
_, df = q.get_scanner_data()
|
|
25
43
|
|
|
26
44
|
if df.empty:
|
|
27
|
-
|
|
45
|
+
# Check if user maybe used wrong market?
|
|
46
|
+
# But we want to be strict.
|
|
47
|
+
return {
|
|
48
|
+
"error": f"Symbol '{symbol}' not found in market '{market}'.",
|
|
49
|
+
"hint": "Ensure you are using the correct market from get_symbol_info()."
|
|
50
|
+
}
|
|
28
51
|
|
|
29
52
|
data = df.iloc[0].to_dict()
|
|
30
53
|
return {
|
tradingview_mcp/utils.py
CHANGED
|
@@ -54,12 +54,57 @@ def sanitize_exchange(ex: str | None, default: str = "america") -> str:
|
|
|
54
54
|
return default
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
# Country Code mappings
|
|
58
|
+
COUNTRY_ALIASES = {
|
|
59
|
+
"us": "america",
|
|
60
|
+
"usa": "america",
|
|
61
|
+
"uk": "uk",
|
|
62
|
+
"gb": "uk",
|
|
63
|
+
"de": "germany",
|
|
64
|
+
"fr": "france",
|
|
65
|
+
"it": "italy",
|
|
66
|
+
"es": "spain",
|
|
67
|
+
"pt": "portugal",
|
|
68
|
+
"ch": "switzerland",
|
|
69
|
+
"se": "sweden",
|
|
70
|
+
"no": "norway",
|
|
71
|
+
"fi": "finland",
|
|
72
|
+
"nl": "netherlands",
|
|
73
|
+
"be": "belgium",
|
|
74
|
+
"at": "austria",
|
|
75
|
+
"ie": "ireland",
|
|
76
|
+
"ru": "russia",
|
|
77
|
+
"cn": "china",
|
|
78
|
+
"jp": "japan",
|
|
79
|
+
"kr": "korea",
|
|
80
|
+
"in": "india",
|
|
81
|
+
"id": "indonesia",
|
|
82
|
+
"my": "malaysia",
|
|
83
|
+
"th": "thailand",
|
|
84
|
+
"vn": "vietnam",
|
|
85
|
+
"tw": "taiwan",
|
|
86
|
+
"sg": "singapore",
|
|
87
|
+
"hk": "hongkong",
|
|
88
|
+
"au": "australia",
|
|
89
|
+
"nz": "newzealand",
|
|
90
|
+
"ca": "canada",
|
|
91
|
+
"br": "brazil",
|
|
92
|
+
"mx": "mexico",
|
|
93
|
+
"ar": "argentina",
|
|
94
|
+
"cl": "chile",
|
|
95
|
+
"co": "colombia",
|
|
96
|
+
"pe": "peru",
|
|
97
|
+
"eg": "egypt",
|
|
98
|
+
"tr": "turkey",
|
|
99
|
+
"za": "rsa", # South Africa
|
|
100
|
+
}
|
|
101
|
+
|
|
57
102
|
def sanitize_market(market: str | None, default: str = "america") -> str:
|
|
58
103
|
"""
|
|
59
104
|
Validate and sanitize a market name.
|
|
60
|
-
|
|
105
|
+
|
|
61
106
|
Args:
|
|
62
|
-
market: Market to validate
|
|
107
|
+
market: Market to validate (e.g. 'america', 'tw', 'crypto')
|
|
63
108
|
default: Default value if invalid
|
|
64
109
|
|
|
65
110
|
Returns:
|
|
@@ -67,8 +112,22 @@ def sanitize_market(market: str | None, default: str = "america") -> str:
|
|
|
67
112
|
"""
|
|
68
113
|
if not market:
|
|
69
114
|
return default
|
|
115
|
+
|
|
70
116
|
market = market.strip().lower()
|
|
71
|
-
|
|
117
|
+
|
|
118
|
+
# Check exact match
|
|
119
|
+
if market in MARKETS:
|
|
120
|
+
return market
|
|
121
|
+
|
|
122
|
+
# Check alias match
|
|
123
|
+
if market in COUNTRY_ALIASES:
|
|
124
|
+
return COUNTRY_ALIASES[market]
|
|
125
|
+
|
|
126
|
+
# Check if user passed exchange (e.g. 'nasdaq') by mistake
|
|
127
|
+
if market in EXCHANGE_SCREENER:
|
|
128
|
+
return EXCHANGE_SCREENER[market]
|
|
129
|
+
|
|
130
|
+
return default
|
|
72
131
|
|
|
73
132
|
|
|
74
133
|
def timeframe_to_resolution(tf: str) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tradingview-mcp
|
|
3
|
-
Version: 26.3.
|
|
3
|
+
Version: 26.3.1
|
|
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=
|
|
10
|
+
tradingview_mcp/utils.py,sha256=5W0vLcrguwZv8q-ohZfQCqrkxVF1OCWPPVyCJ-bxhOo,11486
|
|
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
|
|
@@ -35,10 +35,10 @@ tradingview_mcp/data/screeners/stocks_failed.json,sha256=RqBp9XCZ3xvdWZMwuVGysEE
|
|
|
35
35
|
tradingview_mcp/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
tradingview_mcp/tools/reference.py,sha256=BPip9ntzyRwIyE0SoD8TWqSGLYj7Evt-OM1SQ1uPG8o,2160
|
|
37
37
|
tradingview_mcp/tools/screener.py,sha256=K6uOUFq4GzPeCE4w2oa1LkAHbodQdQcmexw0W3sofeo,3188
|
|
38
|
-
tradingview_mcp/tools/search.py,sha256=
|
|
39
|
-
tradingview_mcp/tools/technical.py,sha256=
|
|
40
|
-
tradingview_mcp-26.3.
|
|
41
|
-
tradingview_mcp-26.3.
|
|
42
|
-
tradingview_mcp-26.3.
|
|
43
|
-
tradingview_mcp-26.3.
|
|
44
|
-
tradingview_mcp-26.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|