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/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 - Stocks/Crypto screening tools
67
-
68
- 🚀 Quick start:
69
- 1. Call `get_help()` for the full usage guide
70
- 2. Call `ai_get_reference()` for markets, fields, and filter references
71
-
72
- 📊 Common tools:
73
- - `screen_market(market, limit)` - Market screening (stocks, crypto, etc.)
74
- - `get_top_gainers(market, limit)` - Top gainers
75
- - `get_top_losers(market, limit)` - Top losers
76
- - `search_symbols(query, market)` - 🔍 Search by name (e.g., "Apple", "Tesla")
77
- - `get_symbol_info(symbol)` - Get detailed symbol info
78
-
79
- ⚠️ Important notes:
80
- - `limit` controls result count (default 25, max 500)
81
- - Results include the `description` field with full names (e.g., "Apple Inc.")
82
- - `market` examples: america (US stocks), crypto, uk, etc.
83
-
84
- 📖 Resources (read-only):
85
- - docs://ai-reference - AI quick reference
86
- - markets://list - All markets
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
- if "column" in message and "unknown" in message:
106
- return "Check available columns via columns://list or docs://fields."
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
- hint = _debug_hint(exc)
156
+
157
+ hint = _debug_hint(exc, context)
121
158
  if hint:
122
- payload["hint"] = hint
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
- """List all available markets for screening."""
189
- categories = {
190
- "crypto": ["crypto", "coin"],
191
- "instruments": ["forex", "futures", "bonds", "cfd", "options", "economics2"],
192
- "americas": ["america", "argentina", "brazil", "canada", "chile", "colombia", "mexico", "peru", "venezuela"],
193
- "europe": [
194
- "austria",
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
- """List all available columns/indicators for screening."""
269
- result = "# Available Columns\n\n"
270
-
271
- # Group columns by category
272
- categories = {
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
- """List supported cryptocurrency exchanges."""
353
- crypto_exchanges = [ex for ex, screener in EXCHANGE_SCREENER.items() if screener == "crypto"]
354
- return f"# Supported Crypto Exchanges\n\n" + ", ".join(sorted(crypto_exchanges))
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
- """List available pre-built scanner presets."""
360
- result = "# Scanner Presets\n\n"
361
-
362
- result += "## Stock Scanners\n"
363
- for name in Scanner.names():
364
- result += f"- `{name}`\n"
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
- """TradingView screener parameter format and schema."""
376
- schema = {
377
- "markets": ["america"],
378
- "columns": ["close", "volume", "market_cap_basic"],
379
- "filter": [
380
- {"left": "change", "operation": "gt", "right": 2},
381
- {"left": "volume", "operation": "greater", "right": 1000000},
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
- "filter2": {
384
- "operator": "and",
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
- """Predefined screener presets from docs data (compact overview)."""
426
- presets = get_screener_presets()
427
- overview = [
428
- {"name": p.get("name"), "url": p.get("url"), "has_query": bool(p.get("query"))}
429
- for p in presets
430
- ]
431
- return json.dumps(overview, ensure_ascii=False, indent=2)
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 display names (first 100). Use search_available_fields tool for more."""
437
- mapping = get_column_display_names()
438
- # Limit output to save tokens
439
- limited = dict(list(mapping.items())[:100])
440
- result = {
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 metadata (compact). Use ai_get_reference tool for categorized list."""
451
- markets = get_markets_data()
452
- screener_markets = get_screener_markets()
453
- return json.dumps(
454
- {
455
- "markets_count": len(markets) if isinstance(markets, (list, dict)) else 0,
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, type, and related info for a field.
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": [p.get("name") for p in presets],
335
+ "available": get_screener_preset_names(),
508
336
  }
509
337
 
510
338
 
511
339
  @mcp.tool()
512
- def get_market_metainfo(
513
- market: str,
514
- limit: int = 50,
515
- offset: int = 0,
516
- confirm_large: bool = False,
517
- ) -> dict[str, Any]:
518
- """Return metainfo (field definitions and allowed values) for a market.
519
-
520
- Args:
521
- market: Market type (stocks, crypto, forex, bond, etc.)
522
- limit: Maximum fields to return (default 50, max 500)
523
- offset: Starting offset for pagination
524
- confirm_large: Set True to allow returning more than 100 items
525
-
526
- Returns:
527
- Paginated metainfo with field definitions
528
- """
529
- limit = min(limit, 500) # Hard cap
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
- try:
532
- metainfo = load_metainfo(market)
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 - Quick Reference and Search
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 guide with common patterns, markets, and filters."""
565
- ref = get_ai_quick_reference()
566
- return json.dumps(ref, ensure_ascii=False, indent=2)
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
- """Python code examples for common screener queries."""
572
- examples = get_screener_code_examples()
573
- return json.dumps(examples, ensure_ascii=False, indent=2)
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 AI-friendly quick reference for this MCP.
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
- query: str,
595
- market: str = "stocks",
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 found. Try a different search term.",
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 Python code example for a specific screener type.
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
- # Return available options
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
- market: str = "stocks",
655
- category: str | None = None,
656
- limit: int = 50,
657
- offset: int = 0,
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
- category_lower = category.lower()
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
- return paginate_data(fields, offset, limit, confirm_large)
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
- category: str | None = None,
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
- category_lower = category.lower()
709
- if category_lower in common_cols:
710
- return {
711
- "category": category,
712
- "fields": common_cols[category_lower],
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
- # Help and Discovery Tools - Help AI understand usage
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
- """📖 Usage guide - required reading for AI!
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
- "step_1": "Call get_top_gainers('america', 10) for top 10 US stock gainers",
758
- "step_2": "Call search_symbols('apple', 'america') to find symbols by name",
759
- "step_3": "Call get_symbol_info('NASDAQ:AAPL') for Apple details",
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 (US)", "uk (UK)", "germany (DE)", "japan (JP)", "china (CN)", "hongkong (HK)", "taiwan (TW)"],
763
- "crypto": ["crypto (pairs)", "coin (coins)",],
500
+ "stocks": ["america", "uk", "germany", "japan", "china"],
501
+ "crypto": ["crypto", "coin"],
764
502
  "others": ["forex", "futures", "bonds"],
765
503
  },
766
- "common_tools": {
767
- "screen_market": "Custom screening - screen_market(market='america', limit=50, sort_by='volume')",
768
- "get_top_gainers": "Top gainers - get_top_gainers(market='crypto', limit=25)",
769
- "get_top_losers": "Top losers - get_top_losers(market='america', limit=25)",
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
- "important_columns": {
775
- "name": "Ticker (e.g., 'NASDAQ:AAPL')",
776
- "description": "Full name (e.g., 'Apple Inc.')",
777
- "close": "Close/last price",
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
- topic_lower = topic.lower()
794
- if topic_lower in guide:
795
- return {"topic": topic, "content": guide[topic_lower]}
796
- if topic_lower == "tools":
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
- """🔍 Search symbols by name (fuzzy search)
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 = {