tradingview-mcp 26.0.0__py3-none-any.whl → 26.3.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
@@ -6,85 +6,83 @@ Provides comprehensive MCP tools and resources for TradingView market screening.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- import argparse
10
9
  import inspect
11
10
  import json
12
11
  import os
13
12
  import traceback
14
13
  from datetime import datetime
15
14
  from functools import wraps
16
- from typing import Any, Optional
15
+ from typing import Any
17
16
 
18
17
  from mcp.server.fastmcp import FastMCP
19
18
 
20
- from tradingview_mcp.column import Column
21
- from tradingview_mcp.constants import (
22
- COLUMNS,
23
- CRYPTO_COLUMNS,
24
- DEFAULT_COLUMNS,
25
- EXCHANGE_SCREENER,
26
- MARKETS,
27
- POSTMARKET_COLUMNS,
28
- PREMARKET_COLUMNS,
29
- TECHNICAL_COLUMNS,
19
+ from tradingview_mcp.docs_data import suggest_fields_for_error
20
+ from tradingview_mcp.tools.search import search_symbols, get_symbol_info
21
+ from tradingview_mcp.tools.screener import (
22
+ screen_market,
23
+ get_top_gainers,
24
+ get_top_losers,
25
+ get_most_active
30
26
  )
31
- from tradingview_mcp.query import And, Or, Query, get_all_symbols
32
- from tradingview_mcp.scanner import CryptoScanner, Scanner
33
- from tradingview_mcp.utils import (
34
- analyze_indicators,
35
- compute_bollinger_band_width,
36
- compute_bollinger_rating,
37
- compute_percent_change,
38
- format_symbol,
39
- format_technical_rating,
40
- sanitize_exchange,
41
- sanitize_market,
42
- sanitize_timeframe,
43
- timeframe_to_resolution,
27
+ from tradingview_mcp.tools.technical import (
28
+ get_technical_analysis,
29
+ scan_rsi_extremes,
30
+ scan_bollinger_bands,
31
+ scan_macd_crossover
44
32
  )
45
- from tradingview_mcp.docs_data import (
46
- get_ai_quick_reference,
47
- get_column_display_names,
48
- get_common_fields,
49
- get_fields_by_market,
50
- get_markets_data,
51
- get_metainfo as load_metainfo,
52
- get_screener_code_examples,
53
- get_screener_markets,
54
- get_screener_presets,
55
- paginate_data,
56
- search_fields,
57
- get_field_summary,
58
- estimate_tokens,
59
- truncate_for_tokens,
60
- MAX_ITEMS_DEFAULT,
33
+ from tradingview_mcp.tools.reference import (
34
+ ai_get_reference,
35
+ search_available_fields,
36
+ get_code_example,
37
+ list_fields_for_market,
38
+ get_common_fields_summary,
39
+ lookup_single_field,
40
+ get_filter_example,
41
+ check_market,
42
+ get_help,
43
+ get_field_info,
44
+ get_screener_preset,
45
+ get_market_metainfo,
46
+ )
47
+ from tradingview_mcp.resources import (
48
+ list_markets,
49
+ list_columns,
50
+ list_crypto_exchanges,
51
+ list_scanner_presets,
52
+ docs_params,
53
+ docs_screeners,
54
+ docs_stock_screeners,
55
+ docs_stock_screeners_failed,
56
+ docs_fields,
57
+ docs_markets,
58
+ docs_ai_reference,
59
+ docs_code_examples,
61
60
  )
62
61
 
63
62
  # Initialize MCP Server
64
63
  mcp = FastMCP(
65
64
  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
65
+ instructions="""TradingView Market Screener MCP
66
+
67
+ Quick start:
68
+ 1. Call `get_help()` for usage
69
+ 2. Call `ai_get_reference()` for markets and fields
70
+
71
+ Common tools:
72
+ - `screen_market(market, limit)`
73
+ - `get_top_gainers(market, limit)`
74
+ - `get_top_losers(market, limit)`
75
+ - `search_symbols(query, market)`
76
+ - `get_symbol_info(symbol)`
77
+
78
+ Notes:
79
+ - `limit` default 25, max 500
80
+ - Results include `description`
81
+
82
+ Resources:
83
+ - docs://ai-reference
84
+ - markets://list
85
+ - columns://list
88
86
  """,
89
87
  )
90
88
 
@@ -98,14 +96,34 @@ def _safe_json(value: Any) -> Any:
98
96
  return str(value)
99
97
 
100
98
 
101
- def _debug_hint(exc: Exception) -> str | None:
99
+ def _debug_hint(exc: Exception, context: dict[str, Any] | None = None) -> dict[str, Any] | str | None:
100
+ """Generate helpful hints based on error type."""
102
101
  message = str(exc).lower()
102
+
103
+ # Extract market from context if available
104
+ market = None
105
+ if context:
106
+ market = context.get("kwargs", {}).get("market") or context.get("market")
107
+
103
108
  if "market" in message and "invalid" in message:
104
109
  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."
110
+
111
+ if "column" in message or "unknown" in message or "field" in message:
112
+ # Return structured field suggestions
113
+ market_type = market or "stocks"
114
+ if market_type in ["crypto", "coin"]:
115
+ market_type = market_type
116
+ elif market_type in ["forex"]:
117
+ market_type = "forex"
118
+ else:
119
+ # Default to stocks for field suggestions
120
+ market_type = "stocks"
121
+
122
+ return suggest_fields_for_error(str(exc), market_type)
123
+
107
124
  if "http" in message or "status" in message:
108
125
  return "TradingView API may be rate-limiting or blocked. Retry with fewer requests."
126
+
109
127
  return None
110
128
 
111
129
 
@@ -117,20 +135,19 @@ def _build_error_response(tool_name: str, exc: Exception, context: dict[str, Any
117
135
  "context": _safe_json(context),
118
136
  "timestamp": datetime.utcnow().isoformat() + "Z",
119
137
  }
120
- hint = _debug_hint(exc)
138
+
139
+ hint = _debug_hint(exc, context)
121
140
  if hint:
122
- payload["hint"] = hint
141
+ if isinstance(hint, dict):
142
+ payload["field_suggestions"] = hint
143
+ else:
144
+ payload["hint"] = hint
145
+
123
146
  if DEBUG_MODE:
124
147
  payload["trace"] = traceback.format_exc()
125
148
  return payload
126
149
 
127
150
 
128
- def _error_response(exc: Exception, context: dict[str, Any] | None = None) -> dict[str, Any]:
129
- frame = inspect.currentframe()
130
- caller = frame.f_back.f_code.co_name if frame and frame.f_back else "unknown"
131
- return _build_error_response(caller, exc, context or {})
132
-
133
-
134
151
  def debug_tool(fn):
135
152
  """Decorator to return structured debug responses on failure."""
136
153
 
@@ -156,7 +173,7 @@ def debug_tool(fn):
156
173
  {"args": args, "kwargs": kwargs, "note": "error returned by tool"},
157
174
  )
158
175
  return result
159
- except Exception as exc: # pragma: no cover - defensive guard
176
+ except Exception as exc:
160
177
  context = {"args": args, "kwargs": kwargs}
161
178
  return _build_error_response(fn.__name__, exc, context)
162
179
 
@@ -179,1971 +196,54 @@ mcp.tool = _debug_mcp_tool
179
196
 
180
197
 
181
198
  # =============================================================================
182
- # MCP Resources
199
+ # Register Tools
183
200
  # =============================================================================
184
201
 
202
+ mcp.tool()(search_symbols)
203
+ mcp.tool()(get_symbol_info)
204
+ mcp.tool()(screen_market)
205
+ mcp.tool()(get_top_gainers)
206
+ mcp.tool()(get_top_losers)
207
+ mcp.tool()(get_most_active)
185
208
 
186
- @mcp.resource("markets://list")
187
- 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
264
-
265
-
266
- @mcp.resource("columns://list")
267
- 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
348
-
349
-
350
- @mcp.resource("exchanges://crypto")
351
- 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))
355
-
356
-
357
- @mcp.resource("scanners://presets")
358
- 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
371
-
372
-
373
- @mcp.resource("docs://params")
374
- 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},
382
- ],
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)
421
-
422
-
423
- @mcp.resource("docs://screeners")
424
- 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)
432
-
433
-
434
- @mcp.resource("docs://fields")
435
- 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)
446
-
447
-
448
- @mcp.resource("docs://markets")
449
- 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
- )
462
-
463
-
464
- @mcp.tool()
465
- 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
- }
496
-
497
-
498
- @mcp.tool()
499
- def get_screener_preset(name: str) -> dict[str, Any]:
500
- """Return a full screener preset (query + code) by name."""
501
- presets = get_screener_presets()
502
- for preset in presets:
503
- if preset.get("name", "").lower() == name.lower():
504
- return preset
505
- return {
506
- "error": f"Preset '{name}' not found.",
507
- "available": [p.get("name") for p in presets],
508
- }
509
-
510
-
511
- @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
530
-
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
- }
209
+ mcp.tool()(get_technical_analysis)
210
+ mcp.tool()(scan_rsi_extremes)
211
+ mcp.tool()(scan_bollinger_bands)
212
+ mcp.tool()(scan_macd_crossover)
555
213
 
214
+ mcp.tool()(ai_get_reference)
215
+ mcp.tool()(search_available_fields)
216
+ mcp.tool()(get_code_example)
217
+ mcp.tool()(list_fields_for_market)
218
+ mcp.tool()(get_common_fields_summary)
219
+ mcp.tool()(lookup_single_field)
220
+ mcp.tool()(get_field_info)
221
+ mcp.tool()(get_filter_example)
222
+ mcp.tool()(check_market)
223
+ mcp.tool()(get_help)
224
+ mcp.tool()(get_screener_preset)
225
+ mcp.tool()(get_market_metainfo)
556
226
 
557
227
  # =============================================================================
558
- # AI-Friendly Tools - Quick Reference and Search
228
+ # Register Resources
559
229
  # =============================================================================
560
230
 
231
+ mcp.resource("markets://list")(list_markets)
232
+ mcp.resource("columns://list")(list_columns)
233
+ mcp.resource("exchanges://crypto")(list_crypto_exchanges)
234
+ mcp.resource("scanners://presets")(list_scanner_presets)
235
+ mcp.resource("docs://params")(docs_params)
236
+ mcp.resource("docs://screeners")(docs_screeners)
237
+ mcp.resource("docs://stock-screeners")(docs_stock_screeners)
238
+ mcp.resource("docs://stock-screeners-failed")(docs_stock_screeners_failed)
239
+ mcp.resource("docs://fields")(docs_fields)
240
+ mcp.resource("docs://markets")(docs_markets)
241
+ mcp.resource("docs://ai-reference")(docs_ai_reference)
242
+ mcp.resource("docs://code-examples")(docs_code_examples)
561
243
 
562
- @mcp.resource("docs://ai-reference")
563
- 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)
567
-
568
-
569
- @mcp.resource("docs://code-examples")
570
- 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)
574
-
575
-
576
- @mcp.tool()
577
- 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()
590
-
591
-
592
- @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
-
611
- return {
612
- "query": query,
613
- "market": market,
614
- "count": len(results),
615
- "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.",
617
- }
618
-
619
-
620
- @mcp.tool()
621
- 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
- """
630
- examples = get_screener_code_examples()
631
-
632
- # Try exact match first
633
- if screener_name in examples:
634
- return {
635
- "name": screener_name,
636
- "code": examples[screener_name],
637
- }
638
-
639
- # Try case-insensitive match
640
- for name, code in examples.items():
641
- if name.lower() == screener_name.lower():
642
- return {"name": name, "code": code}
643
-
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
- }
650
-
651
-
652
- @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)
675
-
676
- 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
- }
681
-
682
- # Filter by category if specified
683
- 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()]
686
-
687
- return paginate_data(fields, offset, limit, confirm_large)
688
-
689
-
690
- @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
-
707
- 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
- }
729
-
730
-
731
- # =============================================================================
732
- # Help and Discovery Tools - Help AI understand usage
733
- # =============================================================================
734
-
735
-
736
- @mcp.tool()
737
- 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
- """
747
- 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
- "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",
760
- },
761
- "markets": {
762
- "stocks": ["america (US)", "uk (UK)", "germany (DE)", "japan (JP)", "china (CN)", "hongkong (HK)", "taiwan (TW)"],
763
- "crypto": ["crypto (pairs)", "coin (coins)",],
764
- "others": ["forex", "futures", "bonds"],
765
- },
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')",
773
- },
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"],
789
- },
790
- }
791
-
792
- 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
- }
804
-
805
- return guide
806
-
807
-
808
- @mcp.tool()
809
- def search_symbols(
810
- query: str,
811
- market: str = "america",
812
- limit: int = 25,
813
- ) -> 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
- """
830
- market = sanitize_market(market)
831
- limit = max(1, min(limit, 100))
832
-
833
- # Search fields: name (ticker) and description (full name)
834
- cols = ["name", "description", "close", "change", "volume", "market_cap_basic", "type", "exchange"]
835
-
836
- try:
837
- # Use TradingView text search
838
- # Search description (company name)
839
- query_obj = (
840
- Query()
841
- .set_markets(market)
842
- .select(*cols)
843
- .where(Column("description").like(query))
844
- .order_by("market_cap_basic", ascending=False)
845
- .limit(limit)
846
- )
847
-
848
- total, df = query_obj.get_scanner_data()
849
- results = df.to_dict("records")
850
-
851
- # If nothing found, try searching the name (ticker) field
852
- if not results:
853
- query_obj2 = (
854
- Query()
855
- .set_markets(market)
856
- .select(*cols)
857
- .where(Column("name").like(query))
858
- .order_by("market_cap_basic", ascending=False)
859
- .limit(limit)
860
- )
861
- total, df = query_obj2.get_scanner_data()
862
- results = df.to_dict("records")
863
-
864
- return {
865
- "query": query,
866
- "market": market,
867
- "total_found": total,
868
- "returned": len(results),
869
- "results": results,
870
- "hint": "Use 'name' field as symbol for other tools (e.g., get_symbol_info)" if results else "No matches found. Try different keywords.",
871
- }
872
- except Exception as e:
873
- # If LIKE isn't supported, use fallback
874
- return _search_symbols_fallback(query, market, limit, cols, str(e))
875
-
876
-
877
- def _search_symbols_fallback(
878
- query: str, market: str, limit: int, cols: list[str], original_error: str
879
- ) -> dict[str, Any]:
880
- """Fallback search when LIKE is not supported."""
881
- try:
882
- # Fetch more data and filter locally
883
- query_obj = (
884
- Query()
885
- .set_markets(market)
886
- .select(*cols)
887
- .order_by("market_cap_basic", ascending=False)
888
- .limit(500) # Fetch top 500
889
- )
890
-
891
- total, df = query_obj.get_scanner_data()
892
-
893
- # Local filter
894
- query_lower = query.lower()
895
- filtered = df[
896
- df["name"].str.lower().str.contains(query_lower, na=False) |
897
- df["description"].str.lower().str.contains(query_lower, na=False)
898
- ].head(limit)
899
-
900
- results = filtered.to_dict("records")
901
-
902
- return {
903
- "query": query,
904
- "market": market,
905
- "total_found": len(results),
906
- "returned": len(results),
907
- "results": results,
908
- "note": "Results from local filtering of top 500 by market cap",
909
- "hint": "Use 'name' field as symbol for other tools" if results else "No matches found",
910
- }
911
- except Exception as e:
912
- return {
913
- "error": f"Search failed: {str(e)}",
914
- "original_error": original_error,
915
- "hint": "Try using screen_market with specific filters instead",
916
- }
917
-
918
-
919
- @mcp.tool()
920
- def get_symbol_info(
921
- symbol: str,
922
- include_technical: bool = False,
923
- ) -> 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
- """
938
- cols = [
939
- "name", "description", "close", "open", "high", "low",
940
- "change", "change_abs", "volume", "market_cap_basic",
941
- "price_earnings_ttm", "earnings_per_share_basic_ttm",
942
- "dividend_yield_recent", "sector", "industry", "exchange", "type",
943
- ]
944
-
945
- if include_technical:
946
- cols.extend([
947
- "RSI", "RSI7", "MACD.macd", "MACD.signal",
948
- "SMA20", "SMA50", "SMA200", "EMA20", "EMA50", "EMA200",
949
- "BB.upper", "BB.lower", "ATR", "ADX",
950
- "Recommend.All", "Recommend.MA", "Recommend.Other",
951
- ])
952
-
953
- try:
954
- # Determine market
955
- if ":" in symbol:
956
- exchange, ticker = symbol.split(":", 1)
957
- market = EXCHANGE_SCREENER.get(exchange.lower(), "america")
958
- else:
959
- ticker = symbol
960
- market = "america"
961
-
962
- query = (
963
- Query()
964
- .set_markets(market)
965
- .select(*cols)
966
- .where(Column("name").isin([symbol, ticker, symbol.upper(), ticker.upper()]))
967
- .limit(5)
968
- )
969
-
970
- total, df = query.get_scanner_data()
971
-
972
- if df.empty:
973
- # Fallback to search
974
- return search_symbols(ticker, market, 5)
975
-
976
- results = df.to_dict("records")
977
-
978
- if len(results) == 1:
979
- return {"symbol": symbol, "found": True, "data": results[0]}
980
- else:
981
- return {"symbol": symbol, "found": True, "matches": results}
982
-
983
- except Exception as e:
984
- return {
985
- "error": f"Failed to get symbol info: {str(e)}",
986
- "hint": "Try using search_symbols to find the correct symbol format",
987
- }
988
-
989
-
990
- # =============================================================================
991
- # MCP Tools - Basic Screening
992
- # =============================================================================
993
-
994
-
995
- @mcp.tool()
996
- def screen_market(
997
- market: str = "america",
998
- columns: Optional[list[str]] = None,
999
- sort_by: str = "volume",
1000
- ascending: bool = False,
1001
- limit: int = 25,
1002
- filters: Optional[list[dict[str, Any]]] = None,
1003
- ) -> 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
- """
1021
- market = sanitize_market(market)
1022
- limit = max(1, min(limit, 500))
1023
-
1024
- # Ensure description is always included
1025
- cols = columns or DEFAULT_COLUMNS
1026
- if "description" not in cols:
1027
- cols = ["description"] + list(cols)
1028
-
1029
- query = Query().set_markets(market).select(*cols).order_by(sort_by, ascending=ascending).limit(limit)
1030
-
1031
- # Apply filters if provided
1032
- if filters:
1033
- filter_expressions = []
1034
- for f in filters:
1035
- col = Column(f.get("column", "close"))
1036
- op = f.get("operation", "gt")
1037
- val = f.get("value")
1038
-
1039
- if op == "gt":
1040
- filter_expressions.append(col > val)
1041
- elif op == "gte":
1042
- filter_expressions.append(col >= val)
1043
- elif op == "lt":
1044
- filter_expressions.append(col < val)
1045
- elif op == "lte":
1046
- filter_expressions.append(col <= val)
1047
- elif op == "eq":
1048
- filter_expressions.append(col == val)
1049
- elif op == "neq":
1050
- filter_expressions.append(col != val)
1051
- elif op == "between" and isinstance(val, (list, tuple)) and len(val) == 2:
1052
- filter_expressions.append(col.between(val[0], val[1]))
1053
- elif op == "isin" and isinstance(val, (list, tuple)):
1054
- filter_expressions.append(col.isin(val))
1055
-
1056
- if filter_expressions:
1057
- query = query.where(*filter_expressions)
1058
-
1059
- try:
1060
- total_count, df = query.get_scanner_data()
1061
- results = df.to_dict("records")
1062
-
1063
- return {
1064
- "total_count": total_count,
1065
- "returned": len(results),
1066
- "market": market,
1067
- "data": results,
1068
- }
1069
- except Exception as e:
1070
- return {"error": str(e), "market": market}
1071
-
1072
-
1073
- @mcp.tool()
1074
- 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
- """
1090
- market = sanitize_market(market)
1091
- limit = max(1, min(limit, 100))
1092
-
1093
- # Ensure description is included
1094
- cols = ["name", "description", "close", "change", "change_abs", "volume", "market_cap_basic"]
1095
-
1096
- query = (
1097
- Query()
1098
- .set_markets(market)
1099
- .select(*cols)
1100
- .where(Column("change") > 0)
1101
- .order_by("change", ascending=False)
1102
- .limit(limit)
1103
- )
1104
-
1105
- try:
1106
- total, df = query.get_scanner_data()
1107
- results = df.to_dict("records")
1108
- return {
1109
- "market": market,
1110
- "type": "top_gainers",
1111
- "total_found": total,
1112
- "returned": len(results),
1113
- "data": results,
1114
- }
1115
- except Exception as e:
1116
- return {"error": str(e), "market": market}
1117
-
1118
-
1119
- @mcp.tool()
1120
- 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
- """
1135
- market = sanitize_market(market)
1136
- limit = max(1, min(limit, 100))
1137
-
1138
- # Ensure description is included
1139
- cols = ["name", "description", "close", "change", "change_abs", "volume", "market_cap_basic"]
1140
-
1141
- query = (
1142
- Query()
1143
- .set_markets(market)
1144
- .select(*cols)
1145
- .where(Column("change") < 0)
1146
- .order_by("change", ascending=True)
1147
- .limit(limit)
1148
- )
1149
-
1150
- try:
1151
- total, df = query.get_scanner_data()
1152
- results = df.to_dict("records")
1153
- return {
1154
- "market": market,
1155
- "type": "top_losers",
1156
- "total_found": total,
1157
- "returned": len(results),
1158
- "data": results,
1159
- }
1160
- except Exception as e:
1161
- return {"error": str(e), "market": market}
1162
-
1163
-
1164
- @mcp.tool()
1165
- 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
- """
1177
- market = sanitize_market(market)
1178
- limit = max(1, min(limit, 100))
1179
-
1180
- cols = ["name", "description", "close", "change", "volume", "relative_volume_10d_calc", "market_cap_basic"]
1181
-
1182
- query = Query().set_markets(market).select(*cols).order_by("volume", ascending=False).limit(limit)
1183
-
1184
- try:
1185
- total, df = query.get_scanner_data()
1186
- results = df.to_dict("records")
1187
- return {
1188
- "market": market,
1189
- "type": "most_active",
1190
- "total_found": total,
1191
- "returned": len(results),
1192
- "data": results,
1193
- }
1194
- except Exception as e:
1195
- return {"error": str(e), "market": market}
1196
-
1197
-
1198
- @mcp.tool()
1199
- def get_premarket_movers(
1200
- scan_type: str = "gainers", limit: int = 25
1201
- ) -> 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
- """
1211
- limit = max(1, min(limit, 100))
1212
-
1213
- scanner_map = {
1214
- "gainers": Scanner.premarket_gainers,
1215
- "losers": Scanner.premarket_losers,
1216
- "most_active": Scanner.premarket_most_active,
1217
- "gappers": Scanner.premarket_gappers,
1218
- }
1219
-
1220
- scanner = scanner_map.get(scan_type, Scanner.premarket_gainers).copy()
1221
- scanner = scanner.limit(limit)
1222
-
1223
- try:
1224
- total, df = scanner.get_scanner_data()
1225
- return df.to_dict("records")
1226
- except Exception as e:
1227
- return [{"error": str(e)}]
1228
-
1229
-
1230
- @mcp.tool()
1231
- def get_postmarket_movers(
1232
- scan_type: str = "gainers", limit: int = 25
1233
- ) -> 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
- """
1244
- limit = max(1, min(limit, 100))
1245
-
1246
- scanner_map = {
1247
- "gainers": Scanner.postmarket_gainers,
1248
- "losers": Scanner.postmarket_losers,
1249
- "most_active": Scanner.postmarket_most_active,
1250
- }
1251
-
1252
- scanner = scanner_map.get(scan_type, Scanner.postmarket_gainers).copy()
1253
- scanner = scanner.limit(limit)
1254
-
1255
- try:
1256
- total, df = scanner.get_scanner_data()
1257
- return df.to_dict("records")
1258
- except Exception as e:
1259
- return [{"error": str(e)}]
1260
-
1261
-
1262
- # =============================================================================
1263
- # MCP Tools - Technical Analysis Scanners
1264
- # =============================================================================
1265
-
1266
-
1267
- @mcp.tool()
1268
- def scan_rsi_extremes(
1269
- market: str = "america",
1270
- condition: str = "oversold",
1271
- threshold: float = 30,
1272
- limit: int = 25,
1273
- ) -> 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
- """
1286
- market = sanitize_market(market)
1287
- limit = max(1, min(limit, 100))
1288
-
1289
- cols = ["name", "close", "volume", "change", "RSI", "RSI7", "market_cap_basic"]
1290
-
1291
- query = Query().set_markets(market).select(*cols).limit(limit)
1292
-
1293
- if condition == "oversold":
1294
- threshold = min(threshold, 50)
1295
- query = query.where(Column("RSI") < threshold).order_by("RSI", ascending=True)
1296
- else: # overbought
1297
- threshold = max(threshold, 50)
1298
- query = query.where(Column("RSI") > threshold).order_by("RSI", ascending=False)
1299
-
1300
- try:
1301
- total, df = query.get_scanner_data()
1302
- results = df.to_dict("records")
1303
-
1304
- # Add signal interpretation
1305
- for r in results:
1306
- rsi = r.get("RSI", 50)
1307
- if rsi < 20:
1308
- r["signal"] = "EXTREMELY_OVERSOLD"
1309
- elif rsi < 30:
1310
- r["signal"] = "OVERSOLD"
1311
- elif rsi > 80:
1312
- r["signal"] = "EXTREMELY_OVERBOUGHT"
1313
- elif rsi > 70:
1314
- r["signal"] = "OVERBOUGHT"
1315
- else:
1316
- r["signal"] = "NEUTRAL"
1317
-
1318
- return results
1319
- except Exception as e:
1320
- return [{"error": str(e)}]
1321
-
1322
-
1323
- @mcp.tool()
1324
- def scan_macd_crossover(
1325
- market: str = "america",
1326
- crossover_type: str = "bullish",
1327
- limit: int = 25,
1328
- ) -> 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
- """
1340
- market = sanitize_market(market)
1341
- limit = max(1, min(limit, 100))
1342
-
1343
- cols = ["name", "close", "volume", "change", "MACD.macd", "MACD.signal", "market_cap_basic"]
1344
-
1345
- query = Query().set_markets(market).select(*cols).limit(limit)
1346
-
1347
- if crossover_type == "bullish":
1348
- query = query.where(Column("MACD.macd").crosses_above(Column("MACD.signal"))).order_by(
1349
- "change", ascending=False
1350
- )
1351
- else:
1352
- query = query.where(Column("MACD.macd").crosses_below(Column("MACD.signal"))).order_by(
1353
- "change", ascending=True
1354
- )
1355
-
1356
- try:
1357
- total, df = query.get_scanner_data()
1358
- results = df.to_dict("records")
1359
-
1360
- for r in results:
1361
- macd = r.get("MACD.macd", 0)
1362
- signal = r.get("MACD.signal", 0)
1363
- r["macd_histogram"] = round(macd - signal, 4) if macd and signal else None
1364
- r["crossover"] = crossover_type
1365
-
1366
- return results
1367
- except Exception as e:
1368
- return [{"error": str(e)}]
1369
-
1370
-
1371
- @mcp.tool()
1372
- def scan_bollinger_bands(
1373
- market: str = "america",
1374
- condition: str = "squeeze",
1375
- limit: int = 25,
1376
- ) -> 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
- """
1388
- market = sanitize_market(market)
1389
- limit = max(1, min(limit, 100))
1390
-
1391
- cols = ["name", "close", "volume", "change", "BB.upper", "BB.lower", "SMA20", "ATR", "Volatility.D"]
1392
-
1393
- query = Query().set_markets(market).select(*cols).limit(limit)
1394
-
1395
- if condition == "squeeze":
1396
- query = query.order_by("Volatility.D", ascending=True)
1397
- elif condition == "above_upper":
1398
- query = query.where(Column("close") > Column("BB.upper")).order_by("change", ascending=False)
1399
- elif condition == "below_lower":
1400
- query = query.where(Column("close") < Column("BB.lower")).order_by("change", ascending=True)
1401
-
1402
- try:
1403
- total, df = query.get_scanner_data()
1404
- results = df.to_dict("records")
1405
-
1406
- # Calculate BBW and add signals
1407
- for r in results:
1408
- bb_upper = r.get("BB.upper", 0)
1409
- bb_lower = r.get("BB.lower", 0)
1410
- sma20 = r.get("SMA20", 0)
1411
- close = r.get("close", 0)
1412
-
1413
- if sma20 and bb_upper and bb_lower:
1414
- bbw = compute_bollinger_band_width(sma20, bb_upper, bb_lower)
1415
- r["bbw"] = round(bbw, 4) if bbw else None
1416
-
1417
- rating, signal = compute_bollinger_rating(close, bb_upper, sma20, bb_lower)
1418
- r["bb_rating"] = rating
1419
- r["bb_signal"] = signal
1420
-
1421
- return results
1422
- except Exception as e:
1423
- return [{"error": str(e)}]
1424
-
1425
-
1426
- @mcp.tool()
1427
- def scan_volume_breakout(
1428
- market: str = "america",
1429
- volume_multiplier: float = 2.0,
1430
- min_price_change: float = 3.0,
1431
- limit: int = 25,
1432
- ) -> 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
- """
1445
- market = sanitize_market(market)
1446
- limit = max(1, min(limit, 100))
1447
- volume_multiplier = max(1.2, min(10.0, volume_multiplier))
1448
- min_price_change = max(0.5, min(20.0, min_price_change))
1449
-
1450
- cols = [
1451
- "name",
1452
- "close",
1453
- "volume",
1454
- "change",
1455
- "change_abs",
1456
- "relative_volume_10d_calc",
1457
- "average_volume_10d_calc",
1458
- ]
1459
-
1460
- query = (
1461
- Query()
1462
- .set_markets(market)
1463
- .select(*cols)
1464
- .where(
1465
- Column("relative_volume_10d_calc") >= volume_multiplier,
1466
- )
1467
- .order_by("relative_volume_10d_calc", ascending=False)
1468
- .limit(limit * 2) # Get more to filter
1469
- )
1470
-
1471
- try:
1472
- total, df = query.get_scanner_data()
1473
- results = df.to_dict("records")
1474
-
1475
- # Filter by price change
1476
- filtered = []
1477
- for r in results:
1478
- change = abs(r.get("change", 0) or 0)
1479
- if change >= min_price_change:
1480
- rel_vol = r.get("relative_volume_10d_calc", 1)
1481
- r["breakout_type"] = "bullish" if r.get("change", 0) > 0 else "bearish"
1482
- r["volume_strength"] = "VERY_STRONG" if rel_vol >= 3 else "STRONG" if rel_vol >= 2 else "MODERATE"
1483
- filtered.append(r)
1484
-
1485
- return filtered[:limit]
1486
- except Exception as e:
1487
- return [{"error": str(e)}]
1488
-
1489
-
1490
- @mcp.tool()
1491
- def scan_moving_average_crossover(
1492
- market: str = "america",
1493
- crossover_type: str = "golden_cross",
1494
- limit: int = 25,
1495
- ) -> 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
- """
1509
- market = sanitize_market(market)
1510
- limit = max(1, min(limit, 100))
1511
-
1512
- scanner_map = {
1513
- "golden_cross": Scanner.golden_cross,
1514
- "death_cross": Scanner.death_cross,
1515
- "above_all_mas": Scanner.above_all_mas,
1516
- }
1517
-
1518
- scanner = scanner_map.get(crossover_type)
1519
- if scanner:
1520
- scanner = scanner.copy().set_markets(market).limit(limit)
1521
- else:
1522
- # below_all_mas
1523
- scanner = (
1524
- Query()
1525
- .set_markets(market)
1526
- .select("name", "close", "volume", "change", "SMA20", "SMA50", "SMA200")
1527
- .where(
1528
- Column("close") < Column("SMA20"),
1529
- Column("close") < Column("SMA50"),
1530
- Column("close") < Column("SMA200"),
1531
- )
1532
- .order_by("change", ascending=True)
1533
- .limit(limit)
1534
- )
1535
-
1536
- try:
1537
- total, df = scanner.get_scanner_data()
1538
- results = df.to_dict("records")
1539
-
1540
- for r in results:
1541
- r["crossover_type"] = crossover_type
1542
-
1543
- return results
1544
- except Exception as e:
1545
- return [{"error": str(e)}]
1546
-
1547
-
1548
- @mcp.tool()
1549
- def scan_52_week_levels(
1550
- market: str = "america",
1551
- level_type: str = "near_high",
1552
- threshold_pct: float = 5.0,
1553
- limit: int = 25,
1554
- ) -> 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
- """
1567
- market = sanitize_market(market)
1568
- limit = max(1, min(limit, 100))
1569
- threshold_pct = max(1.0, min(20.0, threshold_pct))
1570
-
1571
- cols = ["name", "close", "volume", "change", "price_52_week_high", "price_52_week_low", "Perf.Y"]
1572
-
1573
- query = Query().set_markets(market).select(*cols).limit(limit)
1574
-
1575
- if level_type == "near_high":
1576
- # Price within threshold_pct of 52-week high
1577
- multiplier = 1 - (threshold_pct / 100)
1578
- query = query.where(Column("close").above_pct("price_52_week_high", multiplier)).order_by(
1579
- "change", ascending=False
1580
- )
1581
- elif level_type == "near_low":
1582
- # Price within threshold_pct of 52-week low
1583
- multiplier = 1 + (threshold_pct / 100)
1584
- query = query.where(Column("close").below_pct("price_52_week_low", multiplier)).order_by(
1585
- "change", ascending=True
1586
- )
1587
- elif level_type == "new_high":
1588
- query = query.where(Column("close") >= Column("price_52_week_high")).order_by(
1589
- "change", ascending=False
1590
- )
1591
- else: # new_low
1592
- query = query.where(Column("close") <= Column("price_52_week_low")).order_by(
1593
- "change", ascending=True
1594
- )
1595
-
1596
- try:
1597
- total, df = query.get_scanner_data()
1598
- results = df.to_dict("records")
1599
-
1600
- for r in results:
1601
- close = r.get("close", 0)
1602
- high52 = r.get("price_52_week_high", 0)
1603
- low52 = r.get("price_52_week_low", 0)
1604
-
1605
- if high52:
1606
- r["pct_from_52w_high"] = round(((close - high52) / high52) * 100, 2)
1607
- if low52:
1608
- r["pct_from_52w_low"] = round(((close - low52) / low52) * 100, 2)
1609
-
1610
- return results
1611
- except Exception as e:
1612
- return [{"error": str(e)}]
1613
-
1614
-
1615
- # =============================================================================
1616
- # MCP Tools - Symbol Analysis
1617
- # =============================================================================
1618
-
1619
-
1620
- @mcp.tool()
1621
- def get_symbol_analysis(
1622
- symbol: str,
1623
- include_fundamentals: bool = True,
1624
- ) -> 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
- """
1635
- # Select columns based on what we want
1636
- cols = [
1637
- "name",
1638
- "description",
1639
- "type",
1640
- "exchange",
1641
- "close",
1642
- "open",
1643
- "high",
1644
- "low",
1645
- "volume",
1646
- "change",
1647
- "change_abs",
1648
- # Technical indicators
1649
- "RSI",
1650
- "RSI7",
1651
- "MACD.macd",
1652
- "MACD.signal",
1653
- "BB.upper",
1654
- "BB.lower",
1655
- "SMA20",
1656
- "SMA50",
1657
- "SMA200",
1658
- "EMA20",
1659
- "EMA50",
1660
- "EMA200",
1661
- "ADX",
1662
- "ATR",
1663
- "Stoch.K",
1664
- "Stoch.D",
1665
- "VWAP",
1666
- "Volatility.D",
1667
- # Performance
1668
- "Perf.W",
1669
- "Perf.1M",
1670
- "Perf.3M",
1671
- "Perf.Y",
1672
- # Recommendations
1673
- "Recommend.All",
1674
- "Recommend.MA",
1675
- "Recommend.Other",
1676
- ]
1677
-
1678
- if include_fundamentals:
1679
- cols.extend(
1680
- [
1681
- "market_cap_basic",
1682
- "price_earnings_ttm",
1683
- "earnings_per_share_basic_ttm",
1684
- "dividend_yield_recent",
1685
- "price_52_week_high",
1686
- "price_52_week_low",
1687
- ]
1688
- )
1689
-
1690
- try:
1691
- query = Query().set_tickers(symbol).select(*cols)
1692
- total, df = query.get_scanner_data()
1693
-
1694
- if df.empty:
1695
- return {"error": f"No data found for {symbol}"}
1696
-
1697
- row = df.iloc[0].to_dict()
1698
-
1699
- # Perform technical analysis
1700
- analysis = analyze_indicators(row)
1701
-
1702
- # Build result
1703
- result = {
1704
- "symbol": symbol,
1705
- "name": row.get("name"),
1706
- "exchange": row.get("exchange"),
1707
- "type": row.get("type"),
1708
- "price_data": {
1709
- "close": row.get("close"),
1710
- "open": row.get("open"),
1711
- "high": row.get("high"),
1712
- "low": row.get("low"),
1713
- "volume": row.get("volume"),
1714
- "change": row.get("change"),
1715
- "change_abs": row.get("change_abs"),
1716
- },
1717
- "technical_indicators": {
1718
- "rsi": row.get("RSI"),
1719
- "rsi7": row.get("RSI7"),
1720
- "macd": row.get("MACD.macd"),
1721
- "macd_signal": row.get("MACD.signal"),
1722
- "bb_upper": row.get("BB.upper"),
1723
- "bb_lower": row.get("BB.lower"),
1724
- "sma20": row.get("SMA20"),
1725
- "sma50": row.get("SMA50"),
1726
- "sma200": row.get("SMA200"),
1727
- "ema20": row.get("EMA20"),
1728
- "ema50": row.get("EMA50"),
1729
- "ema200": row.get("EMA200"),
1730
- "adx": row.get("ADX"),
1731
- "atr": row.get("ATR"),
1732
- "stoch_k": row.get("Stoch.K"),
1733
- "stoch_d": row.get("Stoch.D"),
1734
- "vwap": row.get("VWAP"),
1735
- "volatility": row.get("Volatility.D"),
1736
- },
1737
- "performance": {
1738
- "weekly": row.get("Perf.W"),
1739
- "monthly": row.get("Perf.1M"),
1740
- "quarterly": row.get("Perf.3M"),
1741
- "yearly": row.get("Perf.Y"),
1742
- },
1743
- "ratings": {
1744
- "overall": row.get("Recommend.All"),
1745
- "overall_label": format_technical_rating(row.get("Recommend.All", 0)),
1746
- "moving_averages": row.get("Recommend.MA"),
1747
- "oscillators": row.get("Recommend.Other"),
1748
- },
1749
- "analysis": analysis,
1750
- }
1751
-
1752
- if include_fundamentals:
1753
- result["fundamentals"] = {
1754
- "market_cap": row.get("market_cap_basic"),
1755
- "pe_ratio": row.get("price_earnings_ttm"),
1756
- "eps": row.get("earnings_per_share_basic_ttm"),
1757
- "dividend_yield": row.get("dividend_yield_recent"),
1758
- "52_week_high": row.get("price_52_week_high"),
1759
- "52_week_low": row.get("price_52_week_low"),
1760
- }
1761
-
1762
- return result
1763
-
1764
- except Exception as e:
1765
- return {"error": str(e), "symbol": symbol}
1766
-
1767
-
1768
- @mcp.tool()
1769
- def compare_symbols(
1770
- symbols: list[str],
1771
- columns: Optional[list[str]] = None,
1772
- ) -> 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
- """
1783
- if not symbols:
1784
- return [{"error": "No symbols provided"}]
1785
-
1786
- cols = columns or [
1787
- "name",
1788
- "close",
1789
- "change",
1790
- "volume",
1791
- "RSI",
1792
- "MACD.macd",
1793
- "market_cap_basic",
1794
- "Perf.1M",
1795
- "Perf.Y",
1796
- "Recommend.All",
1797
- ]
1798
-
1799
- try:
1800
- query = Query().set_tickers(*symbols).select(*cols)
1801
- total, df = query.get_scanner_data()
1802
-
1803
- results = df.to_dict("records")
1804
-
1805
- # Add technical rating labels
1806
- for r in results:
1807
- rec = r.get("Recommend.All")
1808
- if rec is not None:
1809
- r["rating_label"] = format_technical_rating(rec)
1810
-
1811
- return results
1812
-
1813
- except Exception as e:
1814
- return [{"error": str(e)}]
1815
-
1816
-
1817
- # =============================================================================
1818
- # MCP Tools - Utility Functions
1819
- # =============================================================================
1820
-
1821
-
1822
- @mcp.tool()
1823
- 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
- """
1833
- market = sanitize_market(market)
1834
-
1835
- try:
1836
- symbols = get_all_symbols(market)
1837
- return {
1838
- "market": market,
1839
- "total_symbols": len(symbols),
1840
- "symbols": symbols[:500], # Return first 500 to avoid huge responses
1841
- "note": f"Showing first 500 of {len(symbols)} symbols" if len(symbols) > 500 else None,
1842
- }
1843
- except Exception as e:
1844
- return {"error": str(e), "market": market}
1845
-
1846
-
1847
- @mcp.tool()
1848
- def run_scanner_preset(
1849
- preset_name: str,
1850
- market: Optional[str] = None,
1851
- limit: int = 50,
1852
- ) -> 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
- """
1864
- limit = max(1, min(limit, 100))
1865
-
1866
- # Try stock scanners first
1867
- scanner = getattr(Scanner, preset_name, None)
1868
-
1869
- # Try crypto scanners
1870
- if scanner is None:
1871
- scanner = getattr(CryptoScanner, preset_name, None)
1872
-
1873
- if scanner is None:
1874
- available = Scanner.names() + CryptoScanner.names()
1875
- return [{"error": f"Unknown preset: {preset_name}", "available_presets": available}]
1876
-
1877
- scanner = scanner.copy().limit(limit)
1878
-
1879
- if market:
1880
- market = sanitize_market(market)
1881
- scanner = scanner.set_markets(market)
1882
-
1883
- try:
1884
- total, df = scanner.get_scanner_data()
1885
- return df.to_dict("records")
1886
- except Exception as e:
1887
- return [{"error": str(e)}]
1888
-
1889
-
1890
- @mcp.tool()
1891
- def advanced_query(
1892
- market: str = "america",
1893
- select_columns: list[str] = None,
1894
- conditions: list[dict[str, Any]] = None,
1895
- logic: str = "and",
1896
- sort_by: str = "volume",
1897
- ascending: bool = False,
1898
- limit: int = 50,
1899
- ) -> 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
- """
1925
- market = sanitize_market(market)
1926
- limit = max(1, min(limit, 500))
1927
- cols = select_columns or TECHNICAL_COLUMNS
1928
-
1929
- query = Query().set_markets(market).select(*cols).order_by(sort_by, ascending=ascending).limit(limit)
1930
-
1931
- if conditions:
1932
- expressions = []
1933
- for cond in conditions:
1934
- col = Column(cond.get("column", "close"))
1935
- op = cond.get("op", "gt")
1936
- val = cond.get("value")
1937
-
1938
- if op == "gt":
1939
- expressions.append(col > val)
1940
- elif op == "gte":
1941
- expressions.append(col >= val)
1942
- elif op == "lt":
1943
- expressions.append(col < val)
1944
- elif op == "lte":
1945
- expressions.append(col <= val)
1946
- elif op == "eq":
1947
- expressions.append(col == val)
1948
- elif op == "neq":
1949
- expressions.append(col != val)
1950
- elif op == "between" and isinstance(val, (list, tuple)) and len(val) == 2:
1951
- expressions.append(col.between(val[0], val[1]))
1952
- elif op == "isin" and isinstance(val, (list, tuple)):
1953
- expressions.append(col.isin(val))
1954
- elif op == "crosses_above":
1955
- expressions.append(col.crosses_above(Column(val) if isinstance(val, str) else val))
1956
- elif op == "crosses_below":
1957
- expressions.append(col.crosses_below(Column(val) if isinstance(val, str) else val))
1958
- elif op == "above_pct" and isinstance(val, (list, tuple)) and len(val) == 2:
1959
- expressions.append(col.above_pct(val[0], val[1]))
1960
- elif op == "below_pct" and isinstance(val, (list, tuple)) and len(val) == 2:
1961
- expressions.append(col.below_pct(val[0], val[1]))
1962
-
1963
- if expressions:
1964
- if logic == "or":
1965
- query = query.where2(Or(*expressions))
1966
- else:
1967
- query = query.where(*expressions)
1968
-
1969
- try:
1970
- total, df = query.get_scanner_data()
1971
- results = df.to_dict("records")
1972
- return [{"total_count": total, "returned": len(results), "data": results}]
1973
- except Exception as e:
1974
- return [{"error": str(e)}]
1975
-
1976
-
1977
- # =============================================================================
1978
- # MCP Tools - Crypto Specific
1979
- # =============================================================================
1980
-
1981
-
1982
- @mcp.tool()
1983
- 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
- """
1994
- limit = max(1, min(limit, 100))
1995
-
1996
- cols = ["name", "close", "volume", "change", "change_abs", "market_cap_basic", "24h_vol|5"]
1997
-
1998
- query = (
1999
- Query()
2000
- .set_markets("crypto")
2001
- .select(*cols)
2002
- .where(Column("change") > 0)
2003
- .order_by("change", ascending=False)
2004
- .limit(limit)
2005
- )
2006
-
2007
- if exchange:
2008
- query = query.where(Column("exchange") == exchange.upper())
2009
-
2010
- try:
2011
- total, df = query.get_scanner_data()
2012
- return df.to_dict("records")
2013
- except Exception as e:
2014
- return [{"error": str(e)}]
2015
-
2016
-
2017
- @mcp.tool()
2018
- 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
- """
2029
- limit = max(1, min(limit, 100))
2030
-
2031
- cols = ["name", "close", "volume", "change", "change_abs", "market_cap_basic", "24h_vol|5"]
2032
-
2033
- query = (
2034
- Query()
2035
- .set_markets("crypto")
2036
- .select(*cols)
2037
- .where(Column("change") < 0)
2038
- .order_by("change", ascending=True)
2039
- .limit(limit)
2040
- )
2041
-
2042
- if exchange:
2043
- query = query.where(Column("exchange") == exchange.upper())
2044
-
2045
- try:
2046
- total, df = query.get_scanner_data()
2047
- return df.to_dict("records")
2048
- except Exception as e:
2049
- return [{"error": str(e)}]
2050
-
2051
-
2052
- @mcp.tool()
2053
- def scan_crypto_technicals(
2054
- scan_type: str = "oversold",
2055
- exchange: Optional[str] = None,
2056
- limit: int = 25,
2057
- ) -> 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
- """
2069
- limit = max(1, min(limit, 100))
2070
-
2071
- scanner_map = {
2072
- "oversold": CryptoScanner.oversold,
2073
- "overbought": CryptoScanner.overbought,
2074
- "high_volume": CryptoScanner.high_volume,
2075
- "most_volatile": CryptoScanner.most_volatile,
2076
- }
2077
-
2078
- if scan_type in scanner_map:
2079
- scanner = scanner_map[scan_type].copy().limit(limit)
2080
- elif scan_type == "macd_bullish":
2081
- scanner = (
2082
- Query()
2083
- .set_markets("crypto")
2084
- .select("name", "close", "volume", "change", "MACD.macd", "MACD.signal", "RSI")
2085
- .where(Column("MACD.macd").crosses_above(Column("MACD.signal")))
2086
- .order_by("change", ascending=False)
2087
- .limit(limit)
2088
- )
2089
- elif scan_type == "macd_bearish":
2090
- scanner = (
2091
- Query()
2092
- .set_markets("crypto")
2093
- .select("name", "close", "volume", "change", "MACD.macd", "MACD.signal", "RSI")
2094
- .where(Column("MACD.macd").crosses_below(Column("MACD.signal")))
2095
- .order_by("change", ascending=True)
2096
- .limit(limit)
2097
- )
2098
- else:
2099
- return [{"error": f"Unknown scan_type: {scan_type}"}]
2100
-
2101
- if exchange:
2102
- scanner = scanner.where(Column("exchange") == exchange.upper())
2103
-
2104
- try:
2105
- total, df = scanner.get_scanner_data()
2106
- return df.to_dict("records")
2107
- except Exception as e:
2108
- return [{"error": str(e)}]
2109
-
2110
-
2111
- # =============================================================================
2112
- # Server Entry Point
2113
- # =============================================================================
2114
-
2115
-
2116
- def main() -> None:
2117
- """Main entry point for the MCP server."""
2118
- parser = argparse.ArgumentParser(description="TradingView Screener MCP Server")
2119
- parser.add_argument(
2120
- "transport",
2121
- choices=["stdio", "streamable-http"],
2122
- default="stdio",
2123
- nargs="?",
2124
- help="Transport protocol (default: stdio)",
2125
- )
2126
- parser.add_argument("--host", default=os.environ.get("HOST", "127.0.0.1"), help="HTTP host")
2127
- parser.add_argument(
2128
- "--port", type=int, default=int(os.environ.get("PORT", "8000")), help="HTTP port"
2129
- )
2130
- args = parser.parse_args()
2131
-
2132
- if os.environ.get("DEBUG_MCP"):
2133
- import sys
2134
-
2135
- print(f"[DEBUG] TradingView MCP starting: transport={args.transport}", file=sys.stderr, flush=True)
2136
-
2137
- if args.transport == "stdio":
2138
- mcp.run()
2139
- else:
2140
- try:
2141
- mcp.settings.host = args.host
2142
- mcp.settings.port = args.port
2143
- except AttributeError:
2144
- pass
2145
- mcp.run(transport="streamable-http")
2146
244
 
245
+ def main():
246
+ mcp.run()
2147
247
 
2148
248
  if __name__ == "__main__":
2149
249
  main()