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.
- tradingview_mcp/data/extracted/ai_quick_reference.json +130 -131
- tradingview_mcp/data/extracted/common_fields.json +5185 -1450
- tradingview_mcp/data/extracted/fields_by_market.json +23704 -3429
- tradingview_mcp/data/extracted/stock_screener_presets.json +1871 -0
- tradingview_mcp/docs_data.py +312 -98
- tradingview_mcp/server.py +266 -800
- tradingview_mcp-26.2.0.dist-info/METADATA +141 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/RECORD +11 -10
- tradingview_mcp-26.0.0.dist-info/METADATA +0 -333
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/WHEEL +0 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/entry_points.txt +0 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/licenses/LICENSE +0 -0
tradingview_mcp/server.py
CHANGED
|
@@ -52,39 +52,55 @@ from tradingview_mcp.docs_data import (
|
|
|
52
52
|
get_screener_code_examples,
|
|
53
53
|
get_screener_markets,
|
|
54
54
|
get_screener_presets,
|
|
55
|
+
get_stock_screeners,
|
|
56
|
+
get_stock_screeners_failed,
|
|
57
|
+
get_stock_screener_presets,
|
|
58
|
+
get_valid_fields_for_market,
|
|
59
|
+
suggest_fields_for_error,
|
|
55
60
|
paginate_data,
|
|
56
61
|
search_fields,
|
|
57
62
|
get_field_summary,
|
|
58
63
|
estimate_tokens,
|
|
59
64
|
truncate_for_tokens,
|
|
60
65
|
MAX_ITEMS_DEFAULT,
|
|
66
|
+
# New fast lookup functions
|
|
67
|
+
get_quick_reference,
|
|
68
|
+
lookup_field,
|
|
69
|
+
get_filter_format,
|
|
70
|
+
get_market_fields_summary,
|
|
71
|
+
get_screener_preset_names,
|
|
72
|
+
get_code_example_names,
|
|
73
|
+
validate_market,
|
|
74
|
+
build_error_with_format,
|
|
75
|
+
ALL_MARKETS,
|
|
76
|
+
FIELD_CATEGORIES,
|
|
77
|
+
FILTER_OPS,
|
|
61
78
|
)
|
|
62
79
|
|
|
63
80
|
# Initialize MCP Server
|
|
64
81
|
mcp = FastMCP(
|
|
65
82
|
name="TradingView Screener MCP",
|
|
66
|
-
instructions="""TradingView Market Screener MCP
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
1. Call `get_help()` for
|
|
70
|
-
2. Call `ai_get_reference()` for markets
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- `screen_market(market, limit)`
|
|
74
|
-
- `get_top_gainers(market, limit)`
|
|
75
|
-
- `get_top_losers(market, limit)`
|
|
76
|
-
- `search_symbols(query, market)`
|
|
77
|
-
- `get_symbol_info(symbol)`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
- `limit`
|
|
81
|
-
- Results include
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- columns://list - Available fields
|
|
83
|
+
instructions="""TradingView Market Screener MCP
|
|
84
|
+
|
|
85
|
+
Quick start:
|
|
86
|
+
1. Call `get_help()` for usage
|
|
87
|
+
2. Call `ai_get_reference()` for markets and fields
|
|
88
|
+
|
|
89
|
+
Common tools:
|
|
90
|
+
- `screen_market(market, limit)`
|
|
91
|
+
- `get_top_gainers(market, limit)`
|
|
92
|
+
- `get_top_losers(market, limit)`
|
|
93
|
+
- `search_symbols(query, market)`
|
|
94
|
+
- `get_symbol_info(symbol)`
|
|
95
|
+
|
|
96
|
+
Notes:
|
|
97
|
+
- `limit` default 25, max 500
|
|
98
|
+
- Results include `description`
|
|
99
|
+
|
|
100
|
+
Resources:
|
|
101
|
+
- docs://ai-reference
|
|
102
|
+
- markets://list
|
|
103
|
+
- columns://list
|
|
88
104
|
""",
|
|
89
105
|
)
|
|
90
106
|
|
|
@@ -98,14 +114,34 @@ def _safe_json(value: Any) -> Any:
|
|
|
98
114
|
return str(value)
|
|
99
115
|
|
|
100
116
|
|
|
101
|
-
def _debug_hint(exc: Exception) -> str | None:
|
|
117
|
+
def _debug_hint(exc: Exception, context: dict[str, Any] | None = None) -> dict[str, Any] | str | None:
|
|
118
|
+
"""Generate helpful hints based on error type."""
|
|
102
119
|
message = str(exc).lower()
|
|
120
|
+
|
|
121
|
+
# Extract market from context if available
|
|
122
|
+
market = None
|
|
123
|
+
if context:
|
|
124
|
+
market = context.get("kwargs", {}).get("market") or context.get("market")
|
|
125
|
+
|
|
103
126
|
if "market" in message and "invalid" in message:
|
|
104
127
|
return "Check the market name using the markets://list or docs://screeners resources."
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
|
|
129
|
+
if "column" in message or "unknown" in message or "field" in message:
|
|
130
|
+
# Return structured field suggestions
|
|
131
|
+
market_type = market or "stocks"
|
|
132
|
+
if market_type in ["crypto", "coin"]:
|
|
133
|
+
market_type = market_type
|
|
134
|
+
elif market_type in ["forex"]:
|
|
135
|
+
market_type = "forex"
|
|
136
|
+
else:
|
|
137
|
+
# Default to stocks for field suggestions
|
|
138
|
+
market_type = "stocks"
|
|
139
|
+
|
|
140
|
+
return suggest_fields_for_error(str(exc), market_type)
|
|
141
|
+
|
|
107
142
|
if "http" in message or "status" in message:
|
|
108
143
|
return "TradingView API may be rate-limiting or blocked. Retry with fewer requests."
|
|
144
|
+
|
|
109
145
|
return None
|
|
110
146
|
|
|
111
147
|
|
|
@@ -117,9 +153,14 @@ def _build_error_response(tool_name: str, exc: Exception, context: dict[str, Any
|
|
|
117
153
|
"context": _safe_json(context),
|
|
118
154
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
119
155
|
}
|
|
120
|
-
|
|
156
|
+
|
|
157
|
+
hint = _debug_hint(exc, context)
|
|
121
158
|
if hint:
|
|
122
|
-
|
|
159
|
+
if isinstance(hint, dict):
|
|
160
|
+
payload["field_suggestions"] = hint
|
|
161
|
+
else:
|
|
162
|
+
payload["hint"] = hint
|
|
163
|
+
|
|
123
164
|
if DEBUG_MODE:
|
|
124
165
|
payload["trace"] = traceback.format_exc()
|
|
125
166
|
return payload
|
|
@@ -179,320 +220,107 @@ mcp.tool = _debug_mcp_tool
|
|
|
179
220
|
|
|
180
221
|
|
|
181
222
|
# =============================================================================
|
|
182
|
-
# MCP Resources
|
|
223
|
+
# MCP Resources - Compact summaries only, use tools for full data
|
|
183
224
|
# =============================================================================
|
|
184
225
|
|
|
185
226
|
|
|
186
227
|
@mcp.resource("markets://list")
|
|
187
228
|
def list_markets() -> str:
|
|
188
|
-
"""
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
-
"belgium",
|
|
196
|
-
"cyprus",
|
|
197
|
-
"czech",
|
|
198
|
-
"denmark",
|
|
199
|
-
"estonia",
|
|
200
|
-
"finland",
|
|
201
|
-
"france",
|
|
202
|
-
"germany",
|
|
203
|
-
"greece",
|
|
204
|
-
"hungary",
|
|
205
|
-
"iceland",
|
|
206
|
-
"italy",
|
|
207
|
-
"latvia",
|
|
208
|
-
"lithuania",
|
|
209
|
-
"luxembourg",
|
|
210
|
-
"netherlands",
|
|
211
|
-
"norway",
|
|
212
|
-
"poland",
|
|
213
|
-
"portugal",
|
|
214
|
-
"romania",
|
|
215
|
-
"russia",
|
|
216
|
-
"serbia",
|
|
217
|
-
"slovakia",
|
|
218
|
-
"spain",
|
|
219
|
-
"sweden",
|
|
220
|
-
"switzerland",
|
|
221
|
-
"turkey",
|
|
222
|
-
"uk",
|
|
223
|
-
],
|
|
224
|
-
"asia_pacific": [
|
|
225
|
-
"australia",
|
|
226
|
-
"bangladesh",
|
|
227
|
-
"china",
|
|
228
|
-
"hongkong",
|
|
229
|
-
"india",
|
|
230
|
-
"indonesia",
|
|
231
|
-
"japan",
|
|
232
|
-
"korea",
|
|
233
|
-
"malaysia",
|
|
234
|
-
"newzealand",
|
|
235
|
-
"pakistan",
|
|
236
|
-
"philippines",
|
|
237
|
-
"singapore",
|
|
238
|
-
"srilanka",
|
|
239
|
-
"taiwan",
|
|
240
|
-
"thailand",
|
|
241
|
-
"vietnam",
|
|
242
|
-
],
|
|
243
|
-
"middle_east_africa": [
|
|
244
|
-
"bahrain",
|
|
245
|
-
"egypt",
|
|
246
|
-
"israel",
|
|
247
|
-
"kenya",
|
|
248
|
-
"ksa",
|
|
249
|
-
"kuwait",
|
|
250
|
-
"morocco",
|
|
251
|
-
"nigeria",
|
|
252
|
-
"qatar",
|
|
253
|
-
"rsa",
|
|
254
|
-
"tunisia",
|
|
255
|
-
"uae",
|
|
256
|
-
],
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
result = "# Available Markets\n\n"
|
|
260
|
-
for category, markets in categories.items():
|
|
261
|
-
result += f"## {category.replace('_', ' ').title()}\n"
|
|
262
|
-
result += ", ".join(sorted(markets)) + "\n\n"
|
|
263
|
-
return result
|
|
229
|
+
"""Available markets summary."""
|
|
230
|
+
return json.dumps({
|
|
231
|
+
"stock_markets": ["america", "uk", "germany", "japan", "china", "hongkong", "taiwan", "korea", "india", "australia", "canada", "brazil", "france", "singapore"],
|
|
232
|
+
"other_markets": ["crypto", "coin", "forex", "futures", "bonds", "cfd", "options"],
|
|
233
|
+
"total_stock_markets": 68,
|
|
234
|
+
"hint": "Use validate_market(name) to check if a market is valid",
|
|
235
|
+
}, indent=2)
|
|
264
236
|
|
|
265
237
|
|
|
266
238
|
@mcp.resource("columns://list")
|
|
267
239
|
def list_columns() -> str:
|
|
268
|
-
"""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
"Price & Change": [
|
|
274
|
-
"close",
|
|
275
|
-
"open",
|
|
276
|
-
"high",
|
|
277
|
-
"low",
|
|
278
|
-
"volume",
|
|
279
|
-
"change",
|
|
280
|
-
"change_abs",
|
|
281
|
-
"gap",
|
|
282
|
-
],
|
|
283
|
-
"Moving Averages": [
|
|
284
|
-
"SMA5",
|
|
285
|
-
"SMA10",
|
|
286
|
-
"SMA20",
|
|
287
|
-
"SMA50",
|
|
288
|
-
"SMA100",
|
|
289
|
-
"SMA200",
|
|
290
|
-
"EMA5",
|
|
291
|
-
"EMA10",
|
|
292
|
-
"EMA20",
|
|
293
|
-
"EMA50",
|
|
294
|
-
"EMA100",
|
|
295
|
-
"EMA200",
|
|
296
|
-
],
|
|
297
|
-
"Oscillators": [
|
|
298
|
-
"RSI",
|
|
299
|
-
"RSI7",
|
|
300
|
-
"MACD.macd",
|
|
301
|
-
"MACD.signal",
|
|
302
|
-
"Stoch.K",
|
|
303
|
-
"Stoch.D",
|
|
304
|
-
"CCI20",
|
|
305
|
-
"Mom",
|
|
306
|
-
"AO",
|
|
307
|
-
"UO",
|
|
308
|
-
],
|
|
309
|
-
"Volatility": ["BB.upper", "BB.lower", "ATR", "Volatility.D", "Volatility.W", "Volatility.M"],
|
|
310
|
-
"Trend": ["ADX", "ADX+DI", "ADX-DI", "Aroon.Up", "Aroon.Down", "P.SAR"],
|
|
311
|
-
"Volume": [
|
|
312
|
-
"volume",
|
|
313
|
-
"VWAP",
|
|
314
|
-
"VWMA",
|
|
315
|
-
"relative_volume_10d_calc",
|
|
316
|
-
"average_volume_10d_calc",
|
|
317
|
-
"average_volume_30d_calc",
|
|
318
|
-
],
|
|
319
|
-
"Pre/Post Market": [
|
|
320
|
-
"premarket_change",
|
|
321
|
-
"premarket_volume",
|
|
322
|
-
"premarket_gap",
|
|
323
|
-
"postmarket_change",
|
|
324
|
-
"postmarket_volume",
|
|
325
|
-
],
|
|
326
|
-
"Performance": ["Perf.W", "Perf.1M", "Perf.3M", "Perf.6M", "Perf.Y", "Perf.YTD", "Perf.All"],
|
|
327
|
-
"Fundamentals": [
|
|
328
|
-
"market_cap_basic",
|
|
329
|
-
"price_earnings_ttm",
|
|
330
|
-
"earnings_per_share_basic_ttm",
|
|
331
|
-
"dividend_yield_recent",
|
|
332
|
-
"price_book_fq",
|
|
333
|
-
],
|
|
334
|
-
"Ratings": ["Recommend.All", "Recommend.MA", "Recommend.Other"],
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
for category, cols in categories.items():
|
|
338
|
-
result += f"## {category}\n"
|
|
339
|
-
result += ", ".join(cols) + "\n\n"
|
|
340
|
-
|
|
341
|
-
result += "\n## Full Column Mapping\n"
|
|
342
|
-
result += "Use human-readable names or API names:\n\n"
|
|
343
|
-
for human_name, api_name in sorted(COLUMNS.items())[:50]: # First 50
|
|
344
|
-
result += f"- `{human_name}` → `{api_name}`\n"
|
|
345
|
-
result += f"\n... and {len(COLUMNS) - 50} more columns available."
|
|
346
|
-
|
|
347
|
-
return result
|
|
240
|
+
"""Field categories summary."""
|
|
241
|
+
return json.dumps({
|
|
242
|
+
"categories": FIELD_CATEGORIES,
|
|
243
|
+
"hint": "Use lookup_field(name) or search_fields(query) for details",
|
|
244
|
+
}, indent=2)
|
|
348
245
|
|
|
349
246
|
|
|
350
247
|
@mcp.resource("exchanges://crypto")
|
|
351
248
|
def list_crypto_exchanges() -> str:
|
|
352
|
-
"""
|
|
353
|
-
|
|
354
|
-
return
|
|
249
|
+
"""Crypto exchanges."""
|
|
250
|
+
exchanges = [ex for ex, screener in EXCHANGE_SCREENER.items() if screener == "crypto"]
|
|
251
|
+
return json.dumps({"exchanges": sorted(exchanges)[:20], "total": len(exchanges)}, indent=2)
|
|
355
252
|
|
|
356
253
|
|
|
357
254
|
@mcp.resource("scanners://presets")
|
|
358
255
|
def list_scanner_presets() -> str:
|
|
359
|
-
"""
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
result += "\n## Crypto Scanners\n"
|
|
367
|
-
for name in CryptoScanner.names():
|
|
368
|
-
result += f"- `{name}`\n"
|
|
369
|
-
|
|
370
|
-
return result
|
|
256
|
+
"""Scanner presets summary."""
|
|
257
|
+
return json.dumps({
|
|
258
|
+
"stock_scanners": Scanner.names(),
|
|
259
|
+
"crypto_scanners": CryptoScanner.names(),
|
|
260
|
+
"hint": "Use run_scanner_preset(name) to execute",
|
|
261
|
+
}, indent=2)
|
|
371
262
|
|
|
372
263
|
|
|
373
264
|
@mcp.resource("docs://params")
|
|
374
265
|
def docs_params() -> str:
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
{"
|
|
381
|
-
{"
|
|
266
|
+
"""Screener parameter format."""
|
|
267
|
+
return json.dumps({
|
|
268
|
+
"filter_format": {"column": "field_name", "operation": "op", "value": "value"},
|
|
269
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
270
|
+
"examples": [
|
|
271
|
+
{"column": "change", "operation": "gt", "value": 5},
|
|
272
|
+
{"column": "volume", "operation": "gt", "value": 1000000},
|
|
273
|
+
{"column": "RSI", "operation": "between", "value": [20, 30]},
|
|
382
274
|
],
|
|
383
|
-
"
|
|
384
|
-
|
|
385
|
-
"operands": [
|
|
386
|
-
{
|
|
387
|
-
"expression": {"left": "type", "operation": "equal", "right": "stock"}
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
"expression": {"left": "is_primary", "operation": "equal", "right": True}
|
|
391
|
-
},
|
|
392
|
-
],
|
|
393
|
-
},
|
|
394
|
-
"symbols": {"query": {"types": []}, "tickers": []},
|
|
395
|
-
"sort": {"sortBy": "market_cap_basic", "sortOrder": "desc"},
|
|
396
|
-
"range": [0, 50],
|
|
397
|
-
"options": {"lang": "en"},
|
|
398
|
-
"ignore_unknown_fields": False,
|
|
399
|
-
"price_conversion": {"to_symbol": False},
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
format_notes = (
|
|
403
|
-
"# Screener Params Format\n\n"
|
|
404
|
-
"## Core fields\n"
|
|
405
|
-
"- markets: list of market identifiers (e.g., america, crypto, forex)\n"
|
|
406
|
-
"- columns: list of fields to return\n"
|
|
407
|
-
"- filter: flat list of expressions\n"
|
|
408
|
-
"- filter2: nested boolean logic tree (and/or)\n"
|
|
409
|
-
"- symbols: optional tickers/types filter\n"
|
|
410
|
-
"- sort: {sortBy, sortOrder}\n"
|
|
411
|
-
"- range: [offset, limit]\n"
|
|
412
|
-
"- options: {lang}\n"
|
|
413
|
-
"- ignore_unknown_fields: boolean\n"
|
|
414
|
-
"- price_conversion: {to_symbol}\n\n"
|
|
415
|
-
"## Expression format\n"
|
|
416
|
-
"{left: <field>, operation: <op>, right: <value>}\n\n"
|
|
417
|
-
"Common operations: equal, ne, gt, gte, lt, lte, in_range, has, has_none_of\n\n"
|
|
418
|
-
"## Example schema\n"
|
|
419
|
-
)
|
|
420
|
-
return format_notes + json.dumps(schema, ensure_ascii=False, indent=2)
|
|
275
|
+
"hint": "Use get_filter_format(type) for specific type formats",
|
|
276
|
+
}, indent=2)
|
|
421
277
|
|
|
422
278
|
|
|
423
279
|
@mcp.resource("docs://screeners")
|
|
424
280
|
def docs_screeners() -> str:
|
|
425
|
-
"""
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
281
|
+
"""Screener presets (names only)."""
|
|
282
|
+
names = get_screener_preset_names()
|
|
283
|
+
return json.dumps({"presets": names, "hint": "Use get_screener_preset(name) for full details"}, indent=2)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@mcp.resource("docs://stock-screeners")
|
|
287
|
+
def docs_stock_screeners() -> str:
|
|
288
|
+
"""Stock screener markets (summary)."""
|
|
289
|
+
data = get_stock_screeners()
|
|
290
|
+
markets = [{"market": d.get("market"), "title": d.get("title"), "group": d.get("group")} for d in data[:30]]
|
|
291
|
+
return json.dumps({"markets": markets, "total": len(data)}, indent=2)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@mcp.resource("docs://stock-screeners-failed")
|
|
295
|
+
def docs_stock_screeners_failed() -> str:
|
|
296
|
+
"""Failed stock screener markets (summary)."""
|
|
297
|
+
data = get_stock_screeners_failed()
|
|
298
|
+
return json.dumps({"count": len(data), "markets": [d.get("market") for d in data][:20]}, indent=2)
|
|
432
299
|
|
|
433
300
|
|
|
434
301
|
@mcp.resource("docs://fields")
|
|
435
302
|
def docs_fields() -> str:
|
|
436
|
-
"""Field
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"fields": limited,
|
|
442
|
-
"total": len(mapping),
|
|
443
|
-
"note": "Showing first 100 fields. Use search_available_fields() tool to search for specific fields.",
|
|
444
|
-
}
|
|
445
|
-
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
303
|
+
"""Field categories only."""
|
|
304
|
+
return json.dumps({
|
|
305
|
+
"categories": FIELD_CATEGORIES,
|
|
306
|
+
"hint": "Use lookup_field(name) or search_fields(query) for details",
|
|
307
|
+
}, indent=2)
|
|
446
308
|
|
|
447
309
|
|
|
448
310
|
@mcp.resource("docs://markets")
|
|
449
311
|
def docs_markets() -> str:
|
|
450
|
-
"""Markets
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"screener_markets": screener_markets,
|
|
457
|
-
"hint": "Use ai_get_reference() tool for full categorized market list.",
|
|
458
|
-
},
|
|
459
|
-
ensure_ascii=False,
|
|
460
|
-
indent=2,
|
|
461
|
-
)
|
|
312
|
+
"""Markets summary."""
|
|
313
|
+
return json.dumps({
|
|
314
|
+
"stock_markets_count": 68,
|
|
315
|
+
"other_markets": ["crypto", "coin", "forex", "futures", "bonds", "cfd", "options"],
|
|
316
|
+
"hint": "Use validate_market(name) to check if a market is valid",
|
|
317
|
+
}, indent=2)
|
|
462
318
|
|
|
463
319
|
|
|
464
320
|
@mcp.tool()
|
|
465
321
|
def get_field_info(field: str) -> dict[str, Any]:
|
|
466
|
-
"""Get display name
|
|
467
|
-
|
|
468
|
-
Args:
|
|
469
|
-
field: Field/column name to look up
|
|
470
|
-
|
|
471
|
-
Returns:
|
|
472
|
-
Field info including display name, type, and variants if available
|
|
473
|
-
"""
|
|
474
|
-
# Try get_field_summary first (uses common_fields)
|
|
475
|
-
summary = get_field_summary(field)
|
|
476
|
-
if summary and summary.get("display_name"):
|
|
477
|
-
return summary
|
|
478
|
-
|
|
479
|
-
# Fallback to display names mapping
|
|
480
|
-
mapping = get_column_display_names()
|
|
481
|
-
display = mapping.get(field)
|
|
482
|
-
if display:
|
|
483
|
-
return {"field": field, "display_name": display}
|
|
484
|
-
|
|
485
|
-
# Try case-insensitive or partial matches
|
|
486
|
-
normalized = field.lower()
|
|
487
|
-
suggestions = [
|
|
488
|
-
name for name in list(mapping.keys())[:500]
|
|
489
|
-
if normalized in name.lower() or normalized in str(mapping[name]).lower()
|
|
490
|
-
][:10]
|
|
491
|
-
return {
|
|
492
|
-
"field": field,
|
|
493
|
-
"display_name": None,
|
|
494
|
-
"suggestions": suggestions,
|
|
495
|
-
}
|
|
322
|
+
"""Get display name and type for a field."""
|
|
323
|
+
return lookup_field(field)
|
|
496
324
|
|
|
497
325
|
|
|
498
326
|
@mcp.tool()
|
|
@@ -504,303 +332,192 @@ def get_screener_preset(name: str) -> dict[str, Any]:
|
|
|
504
332
|
return preset
|
|
505
333
|
return {
|
|
506
334
|
"error": f"Preset '{name}' not found.",
|
|
507
|
-
"available":
|
|
335
|
+
"available": get_screener_preset_names(),
|
|
508
336
|
}
|
|
509
337
|
|
|
510
338
|
|
|
511
339
|
@mcp.tool()
|
|
512
|
-
def
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
""
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
340
|
+
def list_stock_screener_presets(market: str | None = None, limit: int = 20) -> dict[str, Any]:
|
|
341
|
+
"""List stock screener presets."""
|
|
342
|
+
data = get_stock_screener_presets(market)
|
|
343
|
+
if isinstance(data, dict):
|
|
344
|
+
keys = list(data.keys())[:limit]
|
|
345
|
+
return {"presets": keys, "total": len(data), "market": market}
|
|
346
|
+
return {"data": data[:limit] if isinstance(data, list) else data, "market": market}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@mcp.tool()
|
|
350
|
+
def list_stock_screeners(limit: int = 30) -> dict[str, Any]:
|
|
351
|
+
"""List stock screener markets."""
|
|
352
|
+
data = get_stock_screeners()
|
|
353
|
+
markets = [{"market": d.get("market"), "title": d.get("title")} for d in data[:limit]]
|
|
354
|
+
return {"markets": markets, "total": len(data)}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@mcp.tool()
|
|
358
|
+
def list_failed_stock_screeners() -> dict[str, Any]:
|
|
359
|
+
"""List failed stock screener markets."""
|
|
360
|
+
data = get_stock_screeners_failed()
|
|
361
|
+
return {"markets": [d.get("market") for d in data], "total": len(data)}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@mcp.tool()
|
|
365
|
+
def get_market_metainfo(market: str, limit: int = 30) -> dict[str, Any]:
|
|
366
|
+
"""Return metainfo summary for a market."""
|
|
367
|
+
validation = validate_market(market)
|
|
368
|
+
if not validation.get("valid"):
|
|
369
|
+
return validation
|
|
530
370
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
# Paginate if it's a list or large dict
|
|
535
|
-
if isinstance(metainfo, list):
|
|
536
|
-
return paginate_data(metainfo, offset, limit, confirm_large)
|
|
537
|
-
elif isinstance(metainfo, dict):
|
|
538
|
-
# Return summary if too large
|
|
539
|
-
total_fields = len(metainfo.get("fields", []))
|
|
540
|
-
if total_fields > MAX_ITEMS_DEFAULT and not confirm_large:
|
|
541
|
-
return {
|
|
542
|
-
"market": market,
|
|
543
|
-
"total_fields": total_fields,
|
|
544
|
-
"warning": f"⚠️ Large data ({total_fields} fields). Use limit/offset to paginate or set confirm_large=True.",
|
|
545
|
-
"sample_fields": list(metainfo.get("fields", {}).keys())[:20] if isinstance(metainfo.get("fields"), dict) else None,
|
|
546
|
-
}
|
|
547
|
-
return {"market": market, "metainfo": metainfo}
|
|
548
|
-
return {"market": market, "metainfo": metainfo}
|
|
549
|
-
except FileNotFoundError:
|
|
550
|
-
return {
|
|
551
|
-
"error": f"Metainfo for '{market}' not found.",
|
|
552
|
-
"available_markets": ["stocks", "crypto", "forex", "bond", "bonds", "cfd", "futures", "options", "coin", "economics2"],
|
|
553
|
-
"hint": "Use one of the available markets listed above.",
|
|
554
|
-
}
|
|
371
|
+
market = validation.get("market", market)
|
|
372
|
+
return get_market_fields_summary(market)
|
|
555
373
|
|
|
556
374
|
|
|
557
375
|
# =============================================================================
|
|
558
|
-
# AI-Friendly Tools -
|
|
376
|
+
# AI-Friendly Tools - Fast Lookup (no large data dumps)
|
|
559
377
|
# =============================================================================
|
|
560
378
|
|
|
561
379
|
|
|
562
380
|
@mcp.resource("docs://ai-reference")
|
|
563
381
|
def docs_ai_reference() -> str:
|
|
564
|
-
"""AI quick reference
|
|
565
|
-
|
|
566
|
-
|
|
382
|
+
"""AI quick reference (compact)."""
|
|
383
|
+
return json.dumps({
|
|
384
|
+
"field_categories": FIELD_CATEGORIES,
|
|
385
|
+
"filter_operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
386
|
+
"common_markets": ["america", "crypto", "forex", "uk", "germany", "japan"],
|
|
387
|
+
"tools": ["lookup_field", "search_fields", "validate_market", "get_filter_format"],
|
|
388
|
+
}, indent=2)
|
|
567
389
|
|
|
568
390
|
|
|
569
391
|
@mcp.resource("docs://code-examples")
|
|
570
392
|
def docs_code_examples() -> str:
|
|
571
|
-
"""
|
|
572
|
-
|
|
573
|
-
return json.dumps(examples,
|
|
393
|
+
"""Code example names only."""
|
|
394
|
+
names = get_code_example_names()
|
|
395
|
+
return json.dumps({"examples": names, "hint": "Use get_code_example(name) for code"}, indent=2)
|
|
574
396
|
|
|
575
397
|
|
|
576
398
|
@mcp.tool()
|
|
577
399
|
def ai_get_reference() -> dict[str, Any]:
|
|
578
|
-
"""Get
|
|
579
|
-
|
|
580
|
-
Returns a compact guide with:
|
|
581
|
-
- Available markets by category
|
|
582
|
-
- Common columns by type
|
|
583
|
-
- Filter operations
|
|
584
|
-
- Common filter patterns
|
|
585
|
-
- Timeframes
|
|
586
|
-
|
|
587
|
-
This is the recommended starting point for AI agents to understand how to use this MCP.
|
|
588
|
-
"""
|
|
589
|
-
return get_ai_quick_reference()
|
|
400
|
+
"""Get quick reference for markets and fields."""
|
|
401
|
+
return get_quick_reference()
|
|
590
402
|
|
|
591
403
|
|
|
592
404
|
@mcp.tool()
|
|
593
|
-
def search_available_fields(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
limit: int = 20,
|
|
597
|
-
) -> dict[str, Any]:
|
|
598
|
-
"""Search for fields/columns by name or description.
|
|
599
|
-
|
|
600
|
-
Args:
|
|
601
|
-
query: Search term (e.g., "volume", "RSI", "market cap")
|
|
602
|
-
market: Market type to search in (stocks, crypto, forex, etc.)
|
|
603
|
-
limit: Maximum results (default 20, max 50)
|
|
604
|
-
|
|
605
|
-
Returns:
|
|
606
|
-
Matching fields with their display names and types
|
|
607
|
-
"""
|
|
608
|
-
limit = min(limit, 50)
|
|
609
|
-
results = search_fields(query, market, limit)
|
|
610
|
-
|
|
405
|
+
def search_available_fields(query: str, market: str = "stocks", limit: int = 15) -> dict[str, Any]:
|
|
406
|
+
"""Search fields by name or display name."""
|
|
407
|
+
results = search_fields(query, market, min(limit, 30))
|
|
611
408
|
return {
|
|
612
409
|
"query": query,
|
|
613
410
|
"market": market,
|
|
614
411
|
"count": len(results),
|
|
615
412
|
"fields": results,
|
|
616
|
-
"hint": "Use the 'name' field as the column name in queries." if results else "No matches
|
|
413
|
+
"hint": "Use the 'name' field as the column name in queries." if results else "No matches. Try different keywords.",
|
|
617
414
|
}
|
|
618
415
|
|
|
619
416
|
|
|
620
417
|
@mcp.tool()
|
|
621
418
|
def get_code_example(screener_name: str) -> dict[str, Any]:
|
|
622
|
-
"""Get
|
|
623
|
-
|
|
624
|
-
Args:
|
|
625
|
-
screener_name: Screener name (e.g., "Stocks (legacy)", "Crypto", "Forex", "ETFs", "Bonds")
|
|
626
|
-
|
|
627
|
-
Returns:
|
|
628
|
-
Python code example for building that screener query
|
|
629
|
-
"""
|
|
419
|
+
"""Get code example for a screener type."""
|
|
630
420
|
examples = get_screener_code_examples()
|
|
631
421
|
|
|
632
|
-
# Try exact match first
|
|
633
422
|
if screener_name in examples:
|
|
634
|
-
return {
|
|
635
|
-
"name": screener_name,
|
|
636
|
-
"code": examples[screener_name],
|
|
637
|
-
}
|
|
423
|
+
return {"name": screener_name, "code": examples[screener_name]}
|
|
638
424
|
|
|
639
|
-
# Try case-insensitive match
|
|
640
425
|
for name, code in examples.items():
|
|
641
426
|
if name.lower() == screener_name.lower():
|
|
642
427
|
return {"name": name, "code": code}
|
|
643
428
|
|
|
644
|
-
|
|
645
|
-
return {
|
|
646
|
-
"error": f"Example '{screener_name}' not found.",
|
|
647
|
-
"available": list(examples.keys()),
|
|
648
|
-
"hint": "Use one of the available screener names listed above.",
|
|
649
|
-
}
|
|
429
|
+
return {"error": f"'{screener_name}' not found.", "available": list(examples.keys())}
|
|
650
430
|
|
|
651
431
|
|
|
652
432
|
@mcp.tool()
|
|
653
|
-
def list_fields_for_market(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
confirm_large: bool = False,
|
|
659
|
-
) -> dict[str, Any]:
|
|
660
|
-
"""List available fields for a specific market with pagination.
|
|
661
|
-
|
|
662
|
-
Args:
|
|
663
|
-
market: Market type (stocks, crypto, forex, coin, bond, cfd, futures, options)
|
|
664
|
-
category: Optional filter by field type (price, volume, fundamental, technical)
|
|
665
|
-
limit: Maximum results (default 50, max 200)
|
|
666
|
-
offset: Starting offset for pagination
|
|
667
|
-
confirm_large: Set True to allow more than 100 results
|
|
668
|
-
|
|
669
|
-
Returns:
|
|
670
|
-
Paginated list of field definitions
|
|
671
|
-
"""
|
|
672
|
-
limit = min(limit, 200)
|
|
673
|
-
|
|
674
|
-
fields = get_fields_by_market(market)
|
|
433
|
+
def list_fields_for_market(market: str = "stocks", category: str | None = None, limit: int = 30) -> dict[str, Any]:
|
|
434
|
+
"""List fields for a market."""
|
|
435
|
+
validation = validate_market(market)
|
|
436
|
+
if not validation.get("valid"):
|
|
437
|
+
return validation
|
|
675
438
|
|
|
439
|
+
fields = get_fields_by_market(validation.get("market", market))
|
|
676
440
|
if not fields:
|
|
677
|
-
return {
|
|
678
|
-
"error": f"No fields found for market '{market}'.",
|
|
679
|
-
"available_markets": ["stocks", "crypto", "forex", "coin", "bond", "bonds", "cfd", "futures", "options", "economics2", "ireland"],
|
|
680
|
-
}
|
|
441
|
+
return {"error": f"No fields for '{market}'.", "use": "search_fields(query) instead"}
|
|
681
442
|
|
|
682
|
-
# Filter by category if specified
|
|
683
443
|
if category and isinstance(fields, list):
|
|
684
|
-
|
|
685
|
-
fields = [f for f in fields if category_lower in f.get("type", "").lower()]
|
|
444
|
+
fields = [f for f in fields if category.lower() in f.get("type", "").lower()]
|
|
686
445
|
|
|
687
|
-
|
|
446
|
+
if isinstance(fields, list):
|
|
447
|
+
return {"fields": [f.get("name") for f in fields[:limit]], "total": len(fields)}
|
|
448
|
+
return {"note": "Use search_fields() for this market"}
|
|
688
449
|
|
|
689
450
|
|
|
690
451
|
@mcp.tool()
|
|
691
|
-
def get_common_fields_summary(
|
|
692
|
-
|
|
693
|
-
limit: int = 30,
|
|
694
|
-
) -> dict[str, Any]:
|
|
695
|
-
"""Get summary of commonly used fields across all markets.
|
|
696
|
-
|
|
697
|
-
Args:
|
|
698
|
-
category: Optional filter (price, volume, fundamental, technical, rating)
|
|
699
|
-
limit: Maximum fields to return (default 30)
|
|
700
|
-
|
|
701
|
-
Returns:
|
|
702
|
-
Dict with field names, display names, and types
|
|
703
|
-
"""
|
|
704
|
-
ref = get_ai_quick_reference()
|
|
705
|
-
common_cols = ref.get("common_columns", {})
|
|
706
|
-
|
|
452
|
+
def get_common_fields_summary(category: str | None = None) -> dict[str, Any]:
|
|
453
|
+
"""Get common fields by category."""
|
|
707
454
|
if category:
|
|
708
|
-
|
|
709
|
-
if
|
|
710
|
-
return {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
return {
|
|
715
|
-
"error": f"Category '{category}' not found.",
|
|
716
|
-
"available_categories": list(common_cols.keys()),
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
# Return all categories with limited fields
|
|
720
|
-
result = {}
|
|
721
|
-
for cat, fields in common_cols.items():
|
|
722
|
-
result[cat] = fields[:limit] if len(fields) > limit else fields
|
|
723
|
-
|
|
724
|
-
return {
|
|
725
|
-
"categories": result,
|
|
726
|
-
"total_categories": len(common_cols),
|
|
727
|
-
"hint": "Use category parameter to filter by type.",
|
|
728
|
-
}
|
|
455
|
+
fields = FIELD_CATEGORIES.get(category.lower())
|
|
456
|
+
if fields:
|
|
457
|
+
return {"category": category, "fields": fields}
|
|
458
|
+
return {"error": f"Category '{category}' not found.", "available": list(FIELD_CATEGORIES.keys())}
|
|
459
|
+
return {"categories": FIELD_CATEGORIES}
|
|
729
460
|
|
|
730
461
|
|
|
731
462
|
# =============================================================================
|
|
732
|
-
#
|
|
463
|
+
# Fast Lookup Tools - Direct answers
|
|
464
|
+
# =============================================================================
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@mcp.tool()
|
|
468
|
+
def lookup_single_field(field_name: str) -> dict[str, Any]:
|
|
469
|
+
"""Lookup a single field by exact name."""
|
|
470
|
+
return lookup_field(field_name)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@mcp.tool()
|
|
474
|
+
def get_filter_example(field_type: str = "number") -> dict[str, Any]:
|
|
475
|
+
"""Get correct filter format for a field type."""
|
|
476
|
+
return get_filter_format(field_type)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@mcp.tool()
|
|
480
|
+
def check_market(market: str) -> dict[str, Any]:
|
|
481
|
+
"""Validate market name and get correct format."""
|
|
482
|
+
return validate_market(market)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# =============================================================================
|
|
486
|
+
# Help Tool - Compact guide
|
|
733
487
|
# =============================================================================
|
|
734
488
|
|
|
735
489
|
|
|
736
490
|
@mcp.tool()
|
|
737
491
|
def get_help(topic: str | None = None) -> dict[str, Any]:
|
|
738
|
-
"""
|
|
739
|
-
|
|
740
|
-
Args:
|
|
741
|
-
topic: Optional topic: 'markets', 'columns', 'filters', 'examples', 'tools'
|
|
742
|
-
If omitted, returns the full guide
|
|
743
|
-
|
|
744
|
-
Returns:
|
|
745
|
-
Usage guide and examples
|
|
746
|
-
"""
|
|
492
|
+
"""Usage guide."""
|
|
747
493
|
guide = {
|
|
748
|
-
"overview": {
|
|
749
|
-
"description": "TradingView MCP is a market screener for stocks and crypto",
|
|
750
|
-
"key_points": [
|
|
751
|
-
"All results include the 'description' field (full name like 'Apple Inc.')",
|
|
752
|
-
"Use the limit parameter to control output size (default 25, max 500)",
|
|
753
|
-
"Symbols are returned in ticker format: 'EXCHANGE:SYMBOL' (e.g., 'NASDAQ:AAPL')",
|
|
754
|
-
],
|
|
755
|
-
},
|
|
756
494
|
"quick_start": {
|
|
757
|
-
"
|
|
758
|
-
"
|
|
759
|
-
"
|
|
495
|
+
"1": "get_top_gainers('america', 10) - top gainers",
|
|
496
|
+
"2": "search_symbols('apple', 'america') - find symbols",
|
|
497
|
+
"3": "get_symbol_info('NASDAQ:AAPL') - symbol details",
|
|
760
498
|
},
|
|
761
499
|
"markets": {
|
|
762
|
-
"stocks": ["america
|
|
763
|
-
"crypto": ["crypto
|
|
500
|
+
"stocks": ["america", "uk", "germany", "japan", "china"],
|
|
501
|
+
"crypto": ["crypto", "coin"],
|
|
764
502
|
"others": ["forex", "futures", "bonds"],
|
|
765
503
|
},
|
|
766
|
-
"
|
|
767
|
-
"
|
|
768
|
-
"
|
|
769
|
-
"
|
|
770
|
-
"search_symbols": "🔍 Name search - search_symbols(query='tesla', market='america')",
|
|
771
|
-
"get_symbol_info": "Symbol info - get_symbol_info(symbol='NASDAQ:AAPL')",
|
|
772
|
-
"get_technical_analysis": "Technical analysis - get_technical_analysis(symbol='NASDAQ:AAPL')",
|
|
504
|
+
"tools": {
|
|
505
|
+
"screening": ["screen_market", "get_top_gainers", "get_top_losers", "get_most_active"],
|
|
506
|
+
"lookup": ["search_symbols", "get_symbol_info", "get_technical_analysis"],
|
|
507
|
+
"reference": ["lookup_field", "search_fields", "check_market", "get_filter_example"],
|
|
773
508
|
},
|
|
774
|
-
"
|
|
775
|
-
|
|
776
|
-
"
|
|
777
|
-
"
|
|
778
|
-
"change": "Change (%)",
|
|
779
|
-
"volume": "Volume",
|
|
780
|
-
"market_cap_basic": "Market cap",
|
|
781
|
-
},
|
|
782
|
-
"filters_example": {
|
|
783
|
-
"description": "Use filters to apply conditions",
|
|
784
|
-
"example": [
|
|
785
|
-
{"column": "change", "operation": "gt", "value": 5},
|
|
786
|
-
{"column": "volume", "operation": "gt", "value": 1000000},
|
|
787
|
-
],
|
|
788
|
-
"operations": ["gt (>)", "gte (>=)", "lt (<)", "lte (<=)", "eq (=)", "neq (!=)", "between", "isin"],
|
|
509
|
+
"columns": FIELD_CATEGORIES,
|
|
510
|
+
"filters": {
|
|
511
|
+
"format": {"column": "field", "operation": "op", "value": "val"},
|
|
512
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
789
513
|
},
|
|
790
514
|
}
|
|
791
515
|
|
|
792
516
|
if topic:
|
|
793
|
-
|
|
794
|
-
if
|
|
795
|
-
return {
|
|
796
|
-
|
|
797
|
-
return {"topic": "tools", "content": guide["common_tools"]}
|
|
798
|
-
if topic_lower == "examples":
|
|
799
|
-
return {"topic": "examples", "content": guide["quick_start"]}
|
|
800
|
-
return {
|
|
801
|
-
"error": f"Topic '{topic}' not found",
|
|
802
|
-
"available_topics": list(guide.keys()) + ["tools", "examples"],
|
|
803
|
-
}
|
|
517
|
+
t = topic.lower()
|
|
518
|
+
if t in guide:
|
|
519
|
+
return {topic: guide[t]}
|
|
520
|
+
return {"error": f"Topic '{topic}' not found", "available": list(guide.keys())}
|
|
804
521
|
|
|
805
522
|
return guide
|
|
806
523
|
|
|
@@ -811,22 +528,7 @@ def search_symbols(
|
|
|
811
528
|
market: str = "america",
|
|
812
529
|
limit: int = 25,
|
|
813
530
|
) -> dict[str, Any]:
|
|
814
|
-
"""
|
|
815
|
-
|
|
816
|
-
Supports company names or tickers, e.g., "Apple", "Tesla", "Microsoft".
|
|
817
|
-
|
|
818
|
-
Args:
|
|
819
|
-
query: Search keyword (company name or ticker)
|
|
820
|
-
market: Market (america, crypto, uk, etc.)
|
|
821
|
-
limit: Max results (default 25, max 100)
|
|
822
|
-
|
|
823
|
-
Returns:
|
|
824
|
-
Matching symbols with full names
|
|
825
|
-
|
|
826
|
-
Examples:
|
|
827
|
-
search_symbols("apple", "america") -> Apple Inc., Applebee's, etc.
|
|
828
|
-
search_symbols("BTC", "crypto") -> BTC pairs
|
|
829
|
-
"""
|
|
531
|
+
"""Search symbols by name or ticker."""
|
|
830
532
|
market = sanitize_market(market)
|
|
831
533
|
limit = max(1, min(limit, 100))
|
|
832
534
|
|
|
@@ -921,20 +623,7 @@ def get_symbol_info(
|
|
|
921
623
|
symbol: str,
|
|
922
624
|
include_technical: bool = False,
|
|
923
625
|
) -> dict[str, Any]:
|
|
924
|
-
"""Get detailed information for a symbol.
|
|
925
|
-
|
|
926
|
-
Args:
|
|
927
|
-
symbol: Ticker in 'EXCHANGE:SYMBOL' format (e.g., 'NASDAQ:AAPL')
|
|
928
|
-
or just a ticker (e.g., 'AAPL', auto-search)
|
|
929
|
-
include_technical: Whether to include technical indicators
|
|
930
|
-
|
|
931
|
-
Returns:
|
|
932
|
-
Detailed symbol info including name, price, market cap, etc.
|
|
933
|
-
|
|
934
|
-
Examples:
|
|
935
|
-
get_symbol_info("NASDAQ:AAPL") -> Apple Inc. details
|
|
936
|
-
get_symbol_info("AAPL") -> auto-search
|
|
937
|
-
"""
|
|
626
|
+
"""Get detailed information for a symbol."""
|
|
938
627
|
cols = [
|
|
939
628
|
"name", "description", "close", "open", "high", "low",
|
|
940
629
|
"change", "change_abs", "volume", "market_cap_basic",
|
|
@@ -1001,23 +690,7 @@ def screen_market(
|
|
|
1001
690
|
limit: int = 25,
|
|
1002
691
|
filters: Optional[list[dict[str, Any]]] = None,
|
|
1003
692
|
) -> dict[str, Any]:
|
|
1004
|
-
"""Run a custom market screening query.
|
|
1005
|
-
|
|
1006
|
-
⚠️ Note: Default returns 25 rows. Increase `limit` for more.
|
|
1007
|
-
|
|
1008
|
-
Args:
|
|
1009
|
-
market: Market (america=US stocks, crypto, uk, etc.)
|
|
1010
|
-
columns: Columns to return (default includes name, description, close, change, volume, market_cap)
|
|
1011
|
-
sort_by: Sort column
|
|
1012
|
-
ascending: Sort order (True=asc, False=desc)
|
|
1013
|
-
limit: Max results (1-500, default 25)
|
|
1014
|
-
filters: Filter list, e.g. [{"column": "change", "operation": "gt", "value": 5}]
|
|
1015
|
-
Supported ops: gt, gte, lt, lte, eq, neq, between, isin
|
|
1016
|
-
|
|
1017
|
-
Returns:
|
|
1018
|
-
Dict with total_count, returned, data.
|
|
1019
|
-
Each row includes description (full name like "Apple Inc.").
|
|
1020
|
-
"""
|
|
693
|
+
"""Run a custom market screening query."""
|
|
1021
694
|
market = sanitize_market(market)
|
|
1022
695
|
limit = max(1, min(limit, 500))
|
|
1023
696
|
|
|
@@ -1072,21 +745,7 @@ def screen_market(
|
|
|
1072
745
|
|
|
1073
746
|
@mcp.tool()
|
|
1074
747
|
def get_top_gainers(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
1075
|
-
"""Get top gainers for a market.
|
|
1076
|
-
|
|
1077
|
-
⚠️ Note: Default returns 25 rows. Increase `limit` for more.
|
|
1078
|
-
|
|
1079
|
-
Args:
|
|
1080
|
-
market: Market (america=US stocks, crypto, uk, etc.)
|
|
1081
|
-
limit: Result count (1-100, default 25)
|
|
1082
|
-
|
|
1083
|
-
Returns:
|
|
1084
|
-
Top gainers with description (full name).
|
|
1085
|
-
|
|
1086
|
-
Example:
|
|
1087
|
-
get_top_gainers("america", 10) -> Top 10 US gainers
|
|
1088
|
-
get_top_gainers("crypto", 25) -> Top 25 crypto gainers
|
|
1089
|
-
"""
|
|
748
|
+
"""Get top gainers for a market."""
|
|
1090
749
|
market = sanitize_market(market)
|
|
1091
750
|
limit = max(1, min(limit, 100))
|
|
1092
751
|
|
|
@@ -1118,20 +777,7 @@ def get_top_gainers(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
|
1118
777
|
|
|
1119
778
|
@mcp.tool()
|
|
1120
779
|
def get_top_losers(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
1121
|
-
"""Get top losers for a market.
|
|
1122
|
-
|
|
1123
|
-
⚠️ Note: Default returns 25 rows. Increase `limit` for more.
|
|
1124
|
-
|
|
1125
|
-
Args:
|
|
1126
|
-
market: Market (america=US stocks, crypto, uk, etc.)
|
|
1127
|
-
limit: Result count (1-100, default 25)
|
|
1128
|
-
|
|
1129
|
-
Returns:
|
|
1130
|
-
Top losers with description (full name).
|
|
1131
|
-
|
|
1132
|
-
Example:
|
|
1133
|
-
get_top_losers("america", 10) -> Top 10 US losers
|
|
1134
|
-
"""
|
|
780
|
+
"""Get top losers for a market."""
|
|
1135
781
|
market = sanitize_market(market)
|
|
1136
782
|
limit = max(1, min(limit, 100))
|
|
1137
783
|
|
|
@@ -1163,17 +809,7 @@ def get_top_losers(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
|
1163
809
|
|
|
1164
810
|
@mcp.tool()
|
|
1165
811
|
def get_most_active(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
1166
|
-
"""Get most active by volume.
|
|
1167
|
-
|
|
1168
|
-
⚠️ Note: Default returns 25 rows.
|
|
1169
|
-
|
|
1170
|
-
Args:
|
|
1171
|
-
market: Market
|
|
1172
|
-
limit: Result count (1-100)
|
|
1173
|
-
|
|
1174
|
-
Returns:
|
|
1175
|
-
Most active list with description (full name)
|
|
1176
|
-
"""
|
|
812
|
+
"""Get most active by volume."""
|
|
1177
813
|
market = sanitize_market(market)
|
|
1178
814
|
limit = max(1, min(limit, 100))
|
|
1179
815
|
|
|
@@ -1199,15 +835,7 @@ def get_most_active(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
|
1199
835
|
def get_premarket_movers(
|
|
1200
836
|
scan_type: str = "gainers", limit: int = 25
|
|
1201
837
|
) -> dict[str, Any]:
|
|
1202
|
-
"""Get pre-market movers (US only).
|
|
1203
|
-
|
|
1204
|
-
Args:
|
|
1205
|
-
scan_type: 'gainers', 'losers', 'most_active', 'gappers'
|
|
1206
|
-
limit: Result count (1-100)
|
|
1207
|
-
|
|
1208
|
-
Returns:
|
|
1209
|
-
List of pre-market movers
|
|
1210
|
-
"""
|
|
838
|
+
"""Get pre-market movers (US only)."""
|
|
1211
839
|
limit = max(1, min(limit, 100))
|
|
1212
840
|
|
|
1213
841
|
scanner_map = {
|
|
@@ -1231,16 +859,7 @@ def get_premarket_movers(
|
|
|
1231
859
|
def get_postmarket_movers(
|
|
1232
860
|
scan_type: str = "gainers", limit: int = 25
|
|
1233
861
|
) -> list[dict[str, Any]]:
|
|
1234
|
-
"""
|
|
1235
|
-
Get post-market movers (US market only).
|
|
1236
|
-
|
|
1237
|
-
Args:
|
|
1238
|
-
scan_type: Type of scan - 'gainers', 'losers', 'most_active'
|
|
1239
|
-
limit: Number of results (1-100)
|
|
1240
|
-
|
|
1241
|
-
Returns:
|
|
1242
|
-
List of post-market movers
|
|
1243
|
-
"""
|
|
862
|
+
"""Get post-market movers (US only)."""
|
|
1244
863
|
limit = max(1, min(limit, 100))
|
|
1245
864
|
|
|
1246
865
|
scanner_map = {
|
|
@@ -1271,18 +890,7 @@ def scan_rsi_extremes(
|
|
|
1271
890
|
threshold: float = 30,
|
|
1272
891
|
limit: int = 25,
|
|
1273
892
|
) -> list[dict[str, Any]]:
|
|
1274
|
-
"""
|
|
1275
|
-
Scan for RSI overbought/oversold conditions.
|
|
1276
|
-
|
|
1277
|
-
Args:
|
|
1278
|
-
market: Market to scan
|
|
1279
|
-
condition: 'oversold' or 'overbought'
|
|
1280
|
-
threshold: RSI threshold (default 30 for oversold, 70 for overbought)
|
|
1281
|
-
limit: Number of results (1-100)
|
|
1282
|
-
|
|
1283
|
-
Returns:
|
|
1284
|
-
List of symbols matching RSI criteria
|
|
1285
|
-
"""
|
|
893
|
+
"""Scan for RSI overbought/oversold conditions."""
|
|
1286
894
|
market = sanitize_market(market)
|
|
1287
895
|
limit = max(1, min(limit, 100))
|
|
1288
896
|
|
|
@@ -1326,17 +934,7 @@ def scan_macd_crossover(
|
|
|
1326
934
|
crossover_type: str = "bullish",
|
|
1327
935
|
limit: int = 25,
|
|
1328
936
|
) -> list[dict[str, Any]]:
|
|
1329
|
-
"""
|
|
1330
|
-
Scan for MACD crossover signals.
|
|
1331
|
-
|
|
1332
|
-
Args:
|
|
1333
|
-
market: Market to scan
|
|
1334
|
-
crossover_type: 'bullish' (MACD crosses above signal) or 'bearish'
|
|
1335
|
-
limit: Number of results (1-100)
|
|
1336
|
-
|
|
1337
|
-
Returns:
|
|
1338
|
-
List of symbols with MACD crossover
|
|
1339
|
-
"""
|
|
937
|
+
"""Scan for MACD crossover signals."""
|
|
1340
938
|
market = sanitize_market(market)
|
|
1341
939
|
limit = max(1, min(limit, 100))
|
|
1342
940
|
|
|
@@ -1374,17 +972,7 @@ def scan_bollinger_bands(
|
|
|
1374
972
|
condition: str = "squeeze",
|
|
1375
973
|
limit: int = 25,
|
|
1376
974
|
) -> list[dict[str, Any]]:
|
|
1377
|
-
"""
|
|
1378
|
-
Scan for Bollinger Band conditions.
|
|
1379
|
-
|
|
1380
|
-
Args:
|
|
1381
|
-
market: Market to scan
|
|
1382
|
-
condition: 'squeeze' (low volatility), 'above_upper', 'below_lower'
|
|
1383
|
-
limit: Number of results (1-100)
|
|
1384
|
-
|
|
1385
|
-
Returns:
|
|
1386
|
-
List of symbols matching BB criteria
|
|
1387
|
-
"""
|
|
975
|
+
"""Scan for Bollinger Band conditions."""
|
|
1388
976
|
market = sanitize_market(market)
|
|
1389
977
|
limit = max(1, min(limit, 100))
|
|
1390
978
|
|
|
@@ -1430,18 +1018,7 @@ def scan_volume_breakout(
|
|
|
1430
1018
|
min_price_change: float = 3.0,
|
|
1431
1019
|
limit: int = 25,
|
|
1432
1020
|
) -> list[dict[str, Any]]:
|
|
1433
|
-
"""
|
|
1434
|
-
Scan for volume breakout candidates.
|
|
1435
|
-
|
|
1436
|
-
Args:
|
|
1437
|
-
market: Market to scan
|
|
1438
|
-
volume_multiplier: Minimum relative volume vs 10-day average (default 2.0)
|
|
1439
|
-
min_price_change: Minimum absolute price change % (default 3.0)
|
|
1440
|
-
limit: Number of results (1-100)
|
|
1441
|
-
|
|
1442
|
-
Returns:
|
|
1443
|
-
List of symbols with volume breakouts
|
|
1444
|
-
"""
|
|
1021
|
+
"""Scan for volume breakout candidates."""
|
|
1445
1022
|
market = sanitize_market(market)
|
|
1446
1023
|
limit = max(1, min(limit, 100))
|
|
1447
1024
|
volume_multiplier = max(1.2, min(10.0, volume_multiplier))
|
|
@@ -1493,19 +1070,7 @@ def scan_moving_average_crossover(
|
|
|
1493
1070
|
crossover_type: str = "golden_cross",
|
|
1494
1071
|
limit: int = 25,
|
|
1495
1072
|
) -> list[dict[str, Any]]:
|
|
1496
|
-
"""
|
|
1497
|
-
Scan for moving average crossovers.
|
|
1498
|
-
|
|
1499
|
-
Args:
|
|
1500
|
-
market: Market to scan
|
|
1501
|
-
crossover_type: 'golden_cross' (SMA50 crosses above SMA200),
|
|
1502
|
-
'death_cross' (SMA50 crosses below SMA200),
|
|
1503
|
-
'above_all_mas', 'below_all_mas'
|
|
1504
|
-
limit: Number of results (1-100)
|
|
1505
|
-
|
|
1506
|
-
Returns:
|
|
1507
|
-
List of symbols with MA crossovers
|
|
1508
|
-
"""
|
|
1073
|
+
"""Scan for moving average crossovers."""
|
|
1509
1074
|
market = sanitize_market(market)
|
|
1510
1075
|
limit = max(1, min(limit, 100))
|
|
1511
1076
|
|
|
@@ -1552,18 +1117,7 @@ def scan_52_week_levels(
|
|
|
1552
1117
|
threshold_pct: float = 5.0,
|
|
1553
1118
|
limit: int = 25,
|
|
1554
1119
|
) -> list[dict[str, Any]]:
|
|
1555
|
-
"""
|
|
1556
|
-
Scan for stocks near 52-week highs or lows.
|
|
1557
|
-
|
|
1558
|
-
Args:
|
|
1559
|
-
market: Market to scan
|
|
1560
|
-
level_type: 'near_high', 'near_low', 'new_high', 'new_low'
|
|
1561
|
-
threshold_pct: How close to the level (default 5%)
|
|
1562
|
-
limit: Number of results (1-100)
|
|
1563
|
-
|
|
1564
|
-
Returns:
|
|
1565
|
-
List of symbols near 52-week extremes
|
|
1566
|
-
"""
|
|
1120
|
+
"""Scan for stocks near 52-week highs or lows."""
|
|
1567
1121
|
market = sanitize_market(market)
|
|
1568
1122
|
limit = max(1, min(limit, 100))
|
|
1569
1123
|
threshold_pct = max(1.0, min(20.0, threshold_pct))
|
|
@@ -1622,16 +1176,7 @@ def get_symbol_analysis(
|
|
|
1622
1176
|
symbol: str,
|
|
1623
1177
|
include_fundamentals: bool = True,
|
|
1624
1178
|
) -> dict[str, Any]:
|
|
1625
|
-
"""
|
|
1626
|
-
Get comprehensive technical analysis for a specific symbol.
|
|
1627
|
-
|
|
1628
|
-
Args:
|
|
1629
|
-
symbol: Symbol in format 'EXCHANGE:SYMBOL' (e.g., 'NASDAQ:AAPL', 'BINANCE:BTCUSDT')
|
|
1630
|
-
include_fundamentals: Include fundamental data if available
|
|
1631
|
-
|
|
1632
|
-
Returns:
|
|
1633
|
-
Comprehensive analysis with indicators, signals, and recommendations
|
|
1634
|
-
"""
|
|
1179
|
+
"""Get comprehensive technical analysis for a symbol."""
|
|
1635
1180
|
# Select columns based on what we want
|
|
1636
1181
|
cols = [
|
|
1637
1182
|
"name",
|
|
@@ -1770,16 +1315,7 @@ def compare_symbols(
|
|
|
1770
1315
|
symbols: list[str],
|
|
1771
1316
|
columns: Optional[list[str]] = None,
|
|
1772
1317
|
) -> list[dict[str, Any]]:
|
|
1773
|
-
"""
|
|
1774
|
-
Compare multiple symbols side by side.
|
|
1775
|
-
|
|
1776
|
-
Args:
|
|
1777
|
-
symbols: List of symbols (e.g., ['NASDAQ:AAPL', 'NASDAQ:MSFT', 'NASDAQ:GOOGL'])
|
|
1778
|
-
columns: Columns to compare (default: standard comparison set)
|
|
1779
|
-
|
|
1780
|
-
Returns:
|
|
1781
|
-
Comparison data for all symbols
|
|
1782
|
-
"""
|
|
1318
|
+
"""Compare multiple symbols side by side."""
|
|
1783
1319
|
if not symbols:
|
|
1784
1320
|
return [{"error": "No symbols provided"}]
|
|
1785
1321
|
|
|
@@ -1821,15 +1357,7 @@ def compare_symbols(
|
|
|
1821
1357
|
|
|
1822
1358
|
@mcp.tool()
|
|
1823
1359
|
def get_all_market_symbols(market: str = "america") -> dict[str, Any]:
|
|
1824
|
-
"""
|
|
1825
|
-
Get all available symbols for a market.
|
|
1826
|
-
|
|
1827
|
-
Args:
|
|
1828
|
-
market: Market identifier (america, crypto, uk, etc.)
|
|
1829
|
-
|
|
1830
|
-
Returns:
|
|
1831
|
-
Dictionary with symbol count and list of symbols
|
|
1832
|
-
"""
|
|
1360
|
+
"""Get available symbols for a market."""
|
|
1833
1361
|
market = sanitize_market(market)
|
|
1834
1362
|
|
|
1835
1363
|
try:
|
|
@@ -1850,17 +1378,7 @@ def run_scanner_preset(
|
|
|
1850
1378
|
market: Optional[str] = None,
|
|
1851
1379
|
limit: int = 50,
|
|
1852
1380
|
) -> list[dict[str, Any]]:
|
|
1853
|
-
"""
|
|
1854
|
-
Run a pre-built scanner preset.
|
|
1855
|
-
|
|
1856
|
-
Args:
|
|
1857
|
-
preset_name: Name of the preset (see scanners://presets resource)
|
|
1858
|
-
market: Override market (optional)
|
|
1859
|
-
limit: Number of results (1-100)
|
|
1860
|
-
|
|
1861
|
-
Returns:
|
|
1862
|
-
Scanner results
|
|
1863
|
-
"""
|
|
1381
|
+
"""Run a pre-built scanner preset."""
|
|
1864
1382
|
limit = max(1, min(limit, 100))
|
|
1865
1383
|
|
|
1866
1384
|
# Try stock scanners first
|
|
@@ -1897,31 +1415,7 @@ def advanced_query(
|
|
|
1897
1415
|
ascending: bool = False,
|
|
1898
1416
|
limit: int = 50,
|
|
1899
1417
|
) -> list[dict[str, Any]]:
|
|
1900
|
-
"""
|
|
1901
|
-
Execute an advanced query with complex AND/OR logic.
|
|
1902
|
-
|
|
1903
|
-
Args:
|
|
1904
|
-
market: Market to scan
|
|
1905
|
-
select_columns: Columns to return
|
|
1906
|
-
conditions: List of conditions, each with:
|
|
1907
|
-
- column: Column name
|
|
1908
|
-
- op: Operation (gt, gte, lt, lte, eq, neq, between, isin, crosses_above, crosses_below)
|
|
1909
|
-
- value: Value or [min, max] for between
|
|
1910
|
-
logic: 'and' or 'or' to combine conditions
|
|
1911
|
-
sort_by: Column to sort by
|
|
1912
|
-
ascending: Sort order
|
|
1913
|
-
limit: Maximum results (1-500)
|
|
1914
|
-
|
|
1915
|
-
Returns:
|
|
1916
|
-
Query results
|
|
1917
|
-
|
|
1918
|
-
Example:
|
|
1919
|
-
conditions=[
|
|
1920
|
-
{"column": "RSI", "op": "lt", "value": 30},
|
|
1921
|
-
{"column": "volume", "op": "gt", "value": 1000000},
|
|
1922
|
-
{"column": "change", "op": "between", "value": [-5, 5]}
|
|
1923
|
-
]
|
|
1924
|
-
"""
|
|
1418
|
+
"""Execute an advanced query with AND/OR logic."""
|
|
1925
1419
|
market = sanitize_market(market)
|
|
1926
1420
|
limit = max(1, min(limit, 500))
|
|
1927
1421
|
cols = select_columns or TECHNICAL_COLUMNS
|
|
@@ -1981,16 +1475,7 @@ def advanced_query(
|
|
|
1981
1475
|
|
|
1982
1476
|
@mcp.tool()
|
|
1983
1477
|
def get_crypto_gainers(exchange: Optional[str] = None, limit: int = 25) -> list[dict[str, Any]]:
|
|
1984
|
-
"""
|
|
1985
|
-
Get top gaining cryptocurrencies.
|
|
1986
|
-
|
|
1987
|
-
Args:
|
|
1988
|
-
exchange: Specific exchange (binance, coinbase, etc.) or None for all
|
|
1989
|
-
limit: Number of results (1-100)
|
|
1990
|
-
|
|
1991
|
-
Returns:
|
|
1992
|
-
List of top crypto gainers
|
|
1993
|
-
"""
|
|
1478
|
+
"""Get top gaining cryptocurrencies."""
|
|
1994
1479
|
limit = max(1, min(limit, 100))
|
|
1995
1480
|
|
|
1996
1481
|
cols = ["name", "close", "volume", "change", "change_abs", "market_cap_basic", "24h_vol|5"]
|
|
@@ -2016,16 +1501,7 @@ def get_crypto_gainers(exchange: Optional[str] = None, limit: int = 25) -> list[
|
|
|
2016
1501
|
|
|
2017
1502
|
@mcp.tool()
|
|
2018
1503
|
def get_crypto_losers(exchange: Optional[str] = None, limit: int = 25) -> list[dict[str, Any]]:
|
|
2019
|
-
"""
|
|
2020
|
-
Get top losing cryptocurrencies.
|
|
2021
|
-
|
|
2022
|
-
Args:
|
|
2023
|
-
exchange: Specific exchange (binance, coinbase, etc.) or None for all
|
|
2024
|
-
limit: Number of results (1-100)
|
|
2025
|
-
|
|
2026
|
-
Returns:
|
|
2027
|
-
List of top crypto losers
|
|
2028
|
-
"""
|
|
1504
|
+
"""Get top losing cryptocurrencies."""
|
|
2029
1505
|
limit = max(1, min(limit, 100))
|
|
2030
1506
|
|
|
2031
1507
|
cols = ["name", "close", "volume", "change", "change_abs", "market_cap_basic", "24h_vol|5"]
|
|
@@ -2055,17 +1531,7 @@ def scan_crypto_technicals(
|
|
|
2055
1531
|
exchange: Optional[str] = None,
|
|
2056
1532
|
limit: int = 25,
|
|
2057
1533
|
) -> list[dict[str, Any]]:
|
|
2058
|
-
"""
|
|
2059
|
-
Scan cryptocurrencies based on technical indicators.
|
|
2060
|
-
|
|
2061
|
-
Args:
|
|
2062
|
-
scan_type: Type of scan - 'oversold', 'overbought', 'macd_bullish', 'macd_bearish', 'high_volume'
|
|
2063
|
-
exchange: Specific exchange or None for all
|
|
2064
|
-
limit: Number of results (1-100)
|
|
2065
|
-
|
|
2066
|
-
Returns:
|
|
2067
|
-
Crypto technical scan results
|
|
2068
|
-
"""
|
|
1534
|
+
"""Scan cryptocurrencies based on technical indicators."""
|
|
2069
1535
|
limit = max(1, min(limit, 100))
|
|
2070
1536
|
|
|
2071
1537
|
scanner_map = {
|