tradingview-mcp 1.0.0__py3-none-any.whl → 1.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/constants.py +6 -4
- tradingview_mcp/data/__init__.py +9 -0
- tradingview_mcp/data/column_display_names.json +827 -0
- tradingview_mcp/data/extracted/__init__.py +1 -0
- tradingview_mcp/data/extracted/ai_quick_reference.json +212 -0
- tradingview_mcp/data/extracted/common_fields.json +3627 -0
- tradingview_mcp/data/extracted/fields_by_market.json +23299 -0
- tradingview_mcp/data/extracted/screener_code_examples.json +9 -0
- tradingview_mcp/data/markets.json +83 -0
- tradingview_mcp/data/metainfo/bond.json +17406 -0
- tradingview_mcp/data/metainfo/bonds.json +1303 -0
- tradingview_mcp/data/metainfo/cfd.json +21603 -0
- tradingview_mcp/data/metainfo/coin.json +21111 -0
- tradingview_mcp/data/metainfo/crypto.json +23078 -0
- tradingview_mcp/data/metainfo/economics2.json +1026 -0
- tradingview_mcp/data/metainfo/forex.json +21003 -0
- tradingview_mcp/data/metainfo/futures.json +2972 -0
- tradingview_mcp/data/metainfo/ireland.json +22517 -0
- tradingview_mcp/data/metainfo/options.json +499 -0
- tradingview_mcp/data/metainfo/stocks.json +29808 -0
- tradingview_mcp/data/screeners/main_screeners.json +540 -0
- tradingview_mcp/data/screeners/markets.json +70 -0
- tradingview_mcp/data/screeners/stocks.json +416 -0
- tradingview_mcp/data/screeners/stocks_failed.json +36081 -0
- tradingview_mcp/docs_data.py +297 -0
- tradingview_mcp/scanner.py +2 -1
- tradingview_mcp/server.py +839 -51
- tradingview_mcp-1.0.dist-info/METADATA +334 -0
- tradingview_mcp-1.0.dist-info/RECORD +37 -0
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.0.dist-info}/licenses/LICENSE +0 -2
- tradingview_mcp-1.0.0.dist-info/METADATA +0 -182
- tradingview_mcp-1.0.0.dist-info/RECORD +0 -13
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.0.dist-info}/WHEEL +0 -0
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.0.dist-info}/entry_points.txt +0 -0
tradingview_mcp/server.py
CHANGED
|
@@ -7,8 +7,12 @@ Provides comprehensive MCP tools and resources for TradingView market screening.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import inspect
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
13
|
+
import traceback
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from functools import wraps
|
|
12
16
|
from typing import Any, Optional
|
|
13
17
|
|
|
14
18
|
from mcp.server.fastmcp import FastMCP
|
|
@@ -38,17 +42,141 @@ from tradingview_mcp.utils import (
|
|
|
38
42
|
sanitize_timeframe,
|
|
39
43
|
timeframe_to_resolution,
|
|
40
44
|
)
|
|
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,
|
|
61
|
+
)
|
|
41
62
|
|
|
42
63
|
# Initialize MCP Server
|
|
43
64
|
mcp = FastMCP(
|
|
44
65
|
name="TradingView Screener MCP",
|
|
45
|
-
instructions=
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
88
|
+
""",
|
|
50
89
|
)
|
|
51
90
|
|
|
91
|
+
DEBUG_MODE = os.getenv("TRADINGVIEW_MCP_DEBUG", "0").lower() in {"1", "true", "yes"}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _safe_json(value: Any) -> Any:
|
|
95
|
+
try:
|
|
96
|
+
return json.loads(json.dumps(value, default=str))
|
|
97
|
+
except Exception:
|
|
98
|
+
return str(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _debug_hint(exc: Exception) -> str | None:
|
|
102
|
+
message = str(exc).lower()
|
|
103
|
+
if "market" in message and "invalid" in message:
|
|
104
|
+
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."
|
|
107
|
+
if "http" in message or "status" in message:
|
|
108
|
+
return "TradingView API may be rate-limiting or blocked. Retry with fewer requests."
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _build_error_response(tool_name: str, exc: Exception, context: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
payload: dict[str, Any] = {
|
|
114
|
+
"ok": False,
|
|
115
|
+
"tool": tool_name,
|
|
116
|
+
"error": {"type": exc.__class__.__name__, "message": str(exc)},
|
|
117
|
+
"context": _safe_json(context),
|
|
118
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
119
|
+
}
|
|
120
|
+
hint = _debug_hint(exc)
|
|
121
|
+
if hint:
|
|
122
|
+
payload["hint"] = hint
|
|
123
|
+
if DEBUG_MODE:
|
|
124
|
+
payload["trace"] = traceback.format_exc()
|
|
125
|
+
return payload
|
|
126
|
+
|
|
127
|
+
|
|
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
|
+
def debug_tool(fn):
|
|
135
|
+
"""Decorator to return structured debug responses on failure."""
|
|
136
|
+
|
|
137
|
+
@wraps(fn)
|
|
138
|
+
def wrapper(*args, **kwargs):
|
|
139
|
+
try:
|
|
140
|
+
result = fn(*args, **kwargs)
|
|
141
|
+
if isinstance(result, dict) and "error" in result:
|
|
142
|
+
return _build_error_response(
|
|
143
|
+
fn.__name__,
|
|
144
|
+
Exception(str(result.get("error"))),
|
|
145
|
+
{"args": args, "kwargs": kwargs, "note": "error returned by tool"},
|
|
146
|
+
)
|
|
147
|
+
if (
|
|
148
|
+
isinstance(result, list)
|
|
149
|
+
and len(result) == 1
|
|
150
|
+
and isinstance(result[0], dict)
|
|
151
|
+
and "error" in result[0]
|
|
152
|
+
):
|
|
153
|
+
return _build_error_response(
|
|
154
|
+
fn.__name__,
|
|
155
|
+
Exception(str(result[0].get("error"))),
|
|
156
|
+
{"args": args, "kwargs": kwargs, "note": "error returned by tool"},
|
|
157
|
+
)
|
|
158
|
+
return result
|
|
159
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
160
|
+
context = {"args": args, "kwargs": kwargs}
|
|
161
|
+
return _build_error_response(fn.__name__, exc, context)
|
|
162
|
+
|
|
163
|
+
return wrapper
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_original_mcp_tool = mcp.tool
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _debug_mcp_tool(*args, **kwargs):
|
|
170
|
+
"""Wrap FastMCP tool registration with debug handling."""
|
|
171
|
+
|
|
172
|
+
def decorator(fn):
|
|
173
|
+
return _original_mcp_tool(*args, **kwargs)(debug_tool(fn))
|
|
174
|
+
|
|
175
|
+
return decorator
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
mcp.tool = _debug_mcp_tool
|
|
179
|
+
|
|
52
180
|
|
|
53
181
|
# =============================================================================
|
|
54
182
|
# MCP Resources
|
|
@@ -242,6 +370,623 @@ def list_scanner_presets() -> str:
|
|
|
242
370
|
return result
|
|
243
371
|
|
|
244
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
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# =============================================================================
|
|
558
|
+
# AI-Friendly Tools - Quick Reference and Search
|
|
559
|
+
# =============================================================================
|
|
560
|
+
|
|
561
|
+
|
|
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
|
+
|
|
245
990
|
# =============================================================================
|
|
246
991
|
# MCP Tools - Basic Screening
|
|
247
992
|
# =============================================================================
|
|
@@ -253,27 +998,33 @@ def screen_market(
|
|
|
253
998
|
columns: Optional[list[str]] = None,
|
|
254
999
|
sort_by: str = "volume",
|
|
255
1000
|
ascending: bool = False,
|
|
256
|
-
limit: int =
|
|
1001
|
+
limit: int = 25,
|
|
257
1002
|
filters: Optional[list[dict[str, Any]]] = None,
|
|
258
|
-
) ->
|
|
259
|
-
"""
|
|
260
|
-
|
|
1003
|
+
) -> dict[str, Any]:
|
|
1004
|
+
"""Run a custom market screening query.
|
|
1005
|
+
|
|
1006
|
+
⚠️ Note: Default returns 25 rows. Increase `limit` for more.
|
|
261
1007
|
|
|
262
1008
|
Args:
|
|
263
|
-
market: Market
|
|
264
|
-
columns: Columns to return (default
|
|
265
|
-
sort_by:
|
|
266
|
-
ascending: Sort order (True=
|
|
267
|
-
limit:
|
|
268
|
-
filters:
|
|
269
|
-
|
|
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
|
|
270
1016
|
|
|
271
1017
|
Returns:
|
|
272
|
-
|
|
1018
|
+
Dict with total_count, returned, data.
|
|
1019
|
+
Each row includes description (full name like "Apple Inc.").
|
|
273
1020
|
"""
|
|
274
1021
|
market = sanitize_market(market)
|
|
275
1022
|
limit = max(1, min(limit, 500))
|
|
1023
|
+
|
|
1024
|
+
# Ensure description is always included
|
|
276
1025
|
cols = columns or DEFAULT_COLUMNS
|
|
1026
|
+
if "description" not in cols:
|
|
1027
|
+
cols = ["description"] + list(cols)
|
|
277
1028
|
|
|
278
1029
|
query = Query().set_markets(market).select(*cols).order_by(sort_by, ascending=ascending).limit(limit)
|
|
279
1030
|
|
|
@@ -309,27 +1060,38 @@ def screen_market(
|
|
|
309
1060
|
total_count, df = query.get_scanner_data()
|
|
310
1061
|
results = df.to_dict("records")
|
|
311
1062
|
|
|
312
|
-
return
|
|
1063
|
+
return {
|
|
1064
|
+
"total_count": total_count,
|
|
1065
|
+
"returned": len(results),
|
|
1066
|
+
"market": market,
|
|
1067
|
+
"data": results,
|
|
1068
|
+
}
|
|
313
1069
|
except Exception as e:
|
|
314
|
-
return
|
|
1070
|
+
return {"error": str(e), "market": market}
|
|
315
1071
|
|
|
316
1072
|
|
|
317
1073
|
@mcp.tool()
|
|
318
|
-
def get_top_gainers(market: str = "america", limit: int = 25) ->
|
|
319
|
-
"""
|
|
320
|
-
|
|
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.
|
|
321
1078
|
|
|
322
1079
|
Args:
|
|
323
|
-
market: Market
|
|
324
|
-
limit:
|
|
1080
|
+
market: Market (america=US stocks, crypto, uk, etc.)
|
|
1081
|
+
limit: Result count (1-100, default 25)
|
|
325
1082
|
|
|
326
1083
|
Returns:
|
|
327
|
-
|
|
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
|
|
328
1089
|
"""
|
|
329
1090
|
market = sanitize_market(market)
|
|
330
1091
|
limit = max(1, min(limit, 100))
|
|
331
1092
|
|
|
332
|
-
|
|
1093
|
+
# Ensure description is included
|
|
1094
|
+
cols = ["name", "description", "close", "change", "change_abs", "volume", "market_cap_basic"]
|
|
333
1095
|
|
|
334
1096
|
query = (
|
|
335
1097
|
Query()
|
|
@@ -342,27 +1104,39 @@ def get_top_gainers(market: str = "america", limit: int = 25) -> list[dict[str,
|
|
|
342
1104
|
|
|
343
1105
|
try:
|
|
344
1106
|
total, df = query.get_scanner_data()
|
|
345
|
-
|
|
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
|
+
}
|
|
346
1115
|
except Exception as e:
|
|
347
|
-
return
|
|
1116
|
+
return {"error": str(e), "market": market}
|
|
348
1117
|
|
|
349
1118
|
|
|
350
1119
|
@mcp.tool()
|
|
351
|
-
def get_top_losers(market: str = "america", limit: int = 25) ->
|
|
352
|
-
"""
|
|
353
|
-
|
|
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.
|
|
354
1124
|
|
|
355
1125
|
Args:
|
|
356
|
-
market: Market
|
|
357
|
-
limit:
|
|
1126
|
+
market: Market (america=US stocks, crypto, uk, etc.)
|
|
1127
|
+
limit: Result count (1-100, default 25)
|
|
358
1128
|
|
|
359
1129
|
Returns:
|
|
360
|
-
|
|
1130
|
+
Top losers with description (full name).
|
|
1131
|
+
|
|
1132
|
+
Example:
|
|
1133
|
+
get_top_losers("america", 10) -> Top 10 US losers
|
|
361
1134
|
"""
|
|
362
1135
|
market = sanitize_market(market)
|
|
363
1136
|
limit = max(1, min(limit, 100))
|
|
364
1137
|
|
|
365
|
-
|
|
1138
|
+
# Ensure description is included
|
|
1139
|
+
cols = ["name", "description", "close", "change", "change_abs", "volume", "market_cap_basic"]
|
|
366
1140
|
|
|
367
1141
|
query = (
|
|
368
1142
|
Query()
|
|
@@ -375,47 +1149,61 @@ def get_top_losers(market: str = "america", limit: int = 25) -> list[dict[str, A
|
|
|
375
1149
|
|
|
376
1150
|
try:
|
|
377
1151
|
total, df = query.get_scanner_data()
|
|
378
|
-
|
|
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
|
+
}
|
|
379
1160
|
except Exception as e:
|
|
380
|
-
return
|
|
1161
|
+
return {"error": str(e), "market": market}
|
|
381
1162
|
|
|
382
1163
|
|
|
383
1164
|
@mcp.tool()
|
|
384
|
-
def get_most_active(market: str = "america", limit: int = 25) ->
|
|
385
|
-
"""
|
|
386
|
-
|
|
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.
|
|
387
1169
|
|
|
388
1170
|
Args:
|
|
389
|
-
market: Market
|
|
390
|
-
limit:
|
|
1171
|
+
market: Market
|
|
1172
|
+
limit: Result count (1-100)
|
|
391
1173
|
|
|
392
1174
|
Returns:
|
|
393
|
-
|
|
1175
|
+
Most active list with description (full name)
|
|
394
1176
|
"""
|
|
395
1177
|
market = sanitize_market(market)
|
|
396
1178
|
limit = max(1, min(limit, 100))
|
|
397
1179
|
|
|
398
|
-
cols = ["name", "
|
|
1180
|
+
cols = ["name", "description", "close", "change", "volume", "relative_volume_10d_calc", "market_cap_basic"]
|
|
399
1181
|
|
|
400
1182
|
query = Query().set_markets(market).select(*cols).order_by("volume", ascending=False).limit(limit)
|
|
401
1183
|
|
|
402
1184
|
try:
|
|
403
1185
|
total, df = query.get_scanner_data()
|
|
404
|
-
|
|
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
|
+
}
|
|
405
1194
|
except Exception as e:
|
|
406
|
-
return
|
|
1195
|
+
return {"error": str(e), "market": market}
|
|
407
1196
|
|
|
408
1197
|
|
|
409
1198
|
@mcp.tool()
|
|
410
1199
|
def get_premarket_movers(
|
|
411
1200
|
scan_type: str = "gainers", limit: int = 25
|
|
412
|
-
) ->
|
|
413
|
-
"""
|
|
414
|
-
Get pre-market movers (US market only).
|
|
1201
|
+
) -> dict[str, Any]:
|
|
1202
|
+
"""Get pre-market movers (US only).
|
|
415
1203
|
|
|
416
1204
|
Args:
|
|
417
|
-
scan_type:
|
|
418
|
-
limit:
|
|
1205
|
+
scan_type: 'gainers', 'losers', 'most_active', 'gappers'
|
|
1206
|
+
limit: Result count (1-100)
|
|
419
1207
|
|
|
420
1208
|
Returns:
|
|
421
1209
|
List of pre-market movers
|