tradingview-mcp 26.2.0__py3-none-any.whl → 26.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tradingview_mcp/constants.py +100 -331
- tradingview_mcp/docs_data.py +41 -0
- tradingview_mcp/resources.py +63 -0
- tradingview_mcp/server.py +79 -1445
- tradingview_mcp/tools/__init__.py +0 -0
- tradingview_mcp/tools/reference.py +70 -0
- tradingview_mcp/tools/screener.py +87 -0
- tradingview_mcp/tools/search.py +350 -0
- tradingview_mcp/tools/technical.py +136 -0
- tradingview_mcp/utils.py +62 -3
- {tradingview_mcp-26.2.0.dist-info → tradingview_mcp-26.3.1.dist-info}/METADATA +10 -3
- {tradingview_mcp-26.2.0.dist-info → tradingview_mcp-26.3.1.dist-info}/RECORD +15 -9
- {tradingview_mcp-26.2.0.dist-info → tradingview_mcp-26.3.1.dist-info}/WHEEL +0 -0
- {tradingview_mcp-26.2.0.dist-info → tradingview_mcp-26.3.1.dist-info}/entry_points.txt +0 -0
- {tradingview_mcp-26.2.0.dist-info → tradingview_mcp-26.3.1.dist-info}/licenses/LICENSE +0 -0
tradingview_mcp/server.py
CHANGED
|
@@ -6,75 +6,57 @@ 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
|
|
15
|
+
from typing import Any
|
|
17
16
|
|
|
18
17
|
from mcp.server.fastmcp import FastMCP
|
|
19
18
|
|
|
20
|
-
from tradingview_mcp.
|
|
21
|
-
from tradingview_mcp.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
get_code_example_names,
|
|
73
|
-
validate_market,
|
|
74
|
-
build_error_with_format,
|
|
75
|
-
ALL_MARKETS,
|
|
76
|
-
FIELD_CATEGORIES,
|
|
77
|
-
FILTER_OPS,
|
|
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,
|
|
78
60
|
)
|
|
79
61
|
|
|
80
62
|
# Initialize MCP Server
|
|
@@ -166,12 +148,6 @@ def _build_error_response(tool_name: str, exc: Exception, context: dict[str, Any
|
|
|
166
148
|
return payload
|
|
167
149
|
|
|
168
150
|
|
|
169
|
-
def _error_response(exc: Exception, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
170
|
-
frame = inspect.currentframe()
|
|
171
|
-
caller = frame.f_back.f_code.co_name if frame and frame.f_back else "unknown"
|
|
172
|
-
return _build_error_response(caller, exc, context or {})
|
|
173
|
-
|
|
174
|
-
|
|
175
151
|
def debug_tool(fn):
|
|
176
152
|
"""Decorator to return structured debug responses on failure."""
|
|
177
153
|
|
|
@@ -197,7 +173,7 @@ def debug_tool(fn):
|
|
|
197
173
|
{"args": args, "kwargs": kwargs, "note": "error returned by tool"},
|
|
198
174
|
)
|
|
199
175
|
return result
|
|
200
|
-
except Exception as exc:
|
|
176
|
+
except Exception as exc:
|
|
201
177
|
context = {"args": args, "kwargs": kwargs}
|
|
202
178
|
return _build_error_response(fn.__name__, exc, context)
|
|
203
179
|
|
|
@@ -220,1396 +196,54 @@ mcp.tool = _debug_mcp_tool
|
|
|
220
196
|
|
|
221
197
|
|
|
222
198
|
# =============================================================================
|
|
223
|
-
#
|
|
224
|
-
# =============================================================================
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
@mcp.resource("markets://list")
|
|
228
|
-
def list_markets() -> str:
|
|
229
|
-
"""Available markets summary."""
|
|
230
|
-
return json.dumps({
|
|
231
|
-
"stock_markets": ["america", "uk", "germany", "japan", "china", "hongkong", "taiwan", "korea", "india", "australia", "canada", "brazil", "france", "singapore"],
|
|
232
|
-
"other_markets": ["crypto", "coin", "forex", "futures", "bonds", "cfd", "options"],
|
|
233
|
-
"total_stock_markets": 68,
|
|
234
|
-
"hint": "Use validate_market(name) to check if a market is valid",
|
|
235
|
-
}, indent=2)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@mcp.resource("columns://list")
|
|
239
|
-
def list_columns() -> str:
|
|
240
|
-
"""Field categories summary."""
|
|
241
|
-
return json.dumps({
|
|
242
|
-
"categories": FIELD_CATEGORIES,
|
|
243
|
-
"hint": "Use lookup_field(name) or search_fields(query) for details",
|
|
244
|
-
}, indent=2)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@mcp.resource("exchanges://crypto")
|
|
248
|
-
def list_crypto_exchanges() -> str:
|
|
249
|
-
"""Crypto exchanges."""
|
|
250
|
-
exchanges = [ex for ex, screener in EXCHANGE_SCREENER.items() if screener == "crypto"]
|
|
251
|
-
return json.dumps({"exchanges": sorted(exchanges)[:20], "total": len(exchanges)}, indent=2)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@mcp.resource("scanners://presets")
|
|
255
|
-
def list_scanner_presets() -> str:
|
|
256
|
-
"""Scanner presets summary."""
|
|
257
|
-
return json.dumps({
|
|
258
|
-
"stock_scanners": Scanner.names(),
|
|
259
|
-
"crypto_scanners": CryptoScanner.names(),
|
|
260
|
-
"hint": "Use run_scanner_preset(name) to execute",
|
|
261
|
-
}, indent=2)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
@mcp.resource("docs://params")
|
|
265
|
-
def docs_params() -> str:
|
|
266
|
-
"""Screener parameter format."""
|
|
267
|
-
return json.dumps({
|
|
268
|
-
"filter_format": {"column": "field_name", "operation": "op", "value": "value"},
|
|
269
|
-
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
270
|
-
"examples": [
|
|
271
|
-
{"column": "change", "operation": "gt", "value": 5},
|
|
272
|
-
{"column": "volume", "operation": "gt", "value": 1000000},
|
|
273
|
-
{"column": "RSI", "operation": "between", "value": [20, 30]},
|
|
274
|
-
],
|
|
275
|
-
"hint": "Use get_filter_format(type) for specific type formats",
|
|
276
|
-
}, indent=2)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
@mcp.resource("docs://screeners")
|
|
280
|
-
def docs_screeners() -> str:
|
|
281
|
-
"""Screener presets (names only)."""
|
|
282
|
-
names = get_screener_preset_names()
|
|
283
|
-
return json.dumps({"presets": names, "hint": "Use get_screener_preset(name) for full details"}, indent=2)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
@mcp.resource("docs://stock-screeners")
|
|
287
|
-
def docs_stock_screeners() -> str:
|
|
288
|
-
"""Stock screener markets (summary)."""
|
|
289
|
-
data = get_stock_screeners()
|
|
290
|
-
markets = [{"market": d.get("market"), "title": d.get("title"), "group": d.get("group")} for d in data[:30]]
|
|
291
|
-
return json.dumps({"markets": markets, "total": len(data)}, indent=2)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
@mcp.resource("docs://stock-screeners-failed")
|
|
295
|
-
def docs_stock_screeners_failed() -> str:
|
|
296
|
-
"""Failed stock screener markets (summary)."""
|
|
297
|
-
data = get_stock_screeners_failed()
|
|
298
|
-
return json.dumps({"count": len(data), "markets": [d.get("market") for d in data][:20]}, indent=2)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
@mcp.resource("docs://fields")
|
|
302
|
-
def docs_fields() -> str:
|
|
303
|
-
"""Field categories only."""
|
|
304
|
-
return json.dumps({
|
|
305
|
-
"categories": FIELD_CATEGORIES,
|
|
306
|
-
"hint": "Use lookup_field(name) or search_fields(query) for details",
|
|
307
|
-
}, indent=2)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
@mcp.resource("docs://markets")
|
|
311
|
-
def docs_markets() -> str:
|
|
312
|
-
"""Markets summary."""
|
|
313
|
-
return json.dumps({
|
|
314
|
-
"stock_markets_count": 68,
|
|
315
|
-
"other_markets": ["crypto", "coin", "forex", "futures", "bonds", "cfd", "options"],
|
|
316
|
-
"hint": "Use validate_market(name) to check if a market is valid",
|
|
317
|
-
}, indent=2)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
@mcp.tool()
|
|
321
|
-
def get_field_info(field: str) -> dict[str, Any]:
|
|
322
|
-
"""Get display name and type for a field."""
|
|
323
|
-
return lookup_field(field)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
@mcp.tool()
|
|
327
|
-
def get_screener_preset(name: str) -> dict[str, Any]:
|
|
328
|
-
"""Return a full screener preset (query + code) by name."""
|
|
329
|
-
presets = get_screener_presets()
|
|
330
|
-
for preset in presets:
|
|
331
|
-
if preset.get("name", "").lower() == name.lower():
|
|
332
|
-
return preset
|
|
333
|
-
return {
|
|
334
|
-
"error": f"Preset '{name}' not found.",
|
|
335
|
-
"available": get_screener_preset_names(),
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
@mcp.tool()
|
|
340
|
-
def list_stock_screener_presets(market: str | None = None, limit: int = 20) -> dict[str, Any]:
|
|
341
|
-
"""List stock screener presets."""
|
|
342
|
-
data = get_stock_screener_presets(market)
|
|
343
|
-
if isinstance(data, dict):
|
|
344
|
-
keys = list(data.keys())[:limit]
|
|
345
|
-
return {"presets": keys, "total": len(data), "market": market}
|
|
346
|
-
return {"data": data[:limit] if isinstance(data, list) else data, "market": market}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
@mcp.tool()
|
|
350
|
-
def list_stock_screeners(limit: int = 30) -> dict[str, Any]:
|
|
351
|
-
"""List stock screener markets."""
|
|
352
|
-
data = get_stock_screeners()
|
|
353
|
-
markets = [{"market": d.get("market"), "title": d.get("title")} for d in data[:limit]]
|
|
354
|
-
return {"markets": markets, "total": len(data)}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
@mcp.tool()
|
|
358
|
-
def list_failed_stock_screeners() -> dict[str, Any]:
|
|
359
|
-
"""List failed stock screener markets."""
|
|
360
|
-
data = get_stock_screeners_failed()
|
|
361
|
-
return {"markets": [d.get("market") for d in data], "total": len(data)}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
@mcp.tool()
|
|
365
|
-
def get_market_metainfo(market: str, limit: int = 30) -> dict[str, Any]:
|
|
366
|
-
"""Return metainfo summary for a market."""
|
|
367
|
-
validation = validate_market(market)
|
|
368
|
-
if not validation.get("valid"):
|
|
369
|
-
return validation
|
|
370
|
-
|
|
371
|
-
market = validation.get("market", market)
|
|
372
|
-
return get_market_fields_summary(market)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# =============================================================================
|
|
376
|
-
# AI-Friendly Tools - Fast Lookup (no large data dumps)
|
|
377
|
-
# =============================================================================
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
@mcp.resource("docs://ai-reference")
|
|
381
|
-
def docs_ai_reference() -> str:
|
|
382
|
-
"""AI quick reference (compact)."""
|
|
383
|
-
return json.dumps({
|
|
384
|
-
"field_categories": FIELD_CATEGORIES,
|
|
385
|
-
"filter_operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
386
|
-
"common_markets": ["america", "crypto", "forex", "uk", "germany", "japan"],
|
|
387
|
-
"tools": ["lookup_field", "search_fields", "validate_market", "get_filter_format"],
|
|
388
|
-
}, indent=2)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
@mcp.resource("docs://code-examples")
|
|
392
|
-
def docs_code_examples() -> str:
|
|
393
|
-
"""Code example names only."""
|
|
394
|
-
names = get_code_example_names()
|
|
395
|
-
return json.dumps({"examples": names, "hint": "Use get_code_example(name) for code"}, indent=2)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
@mcp.tool()
|
|
399
|
-
def ai_get_reference() -> dict[str, Any]:
|
|
400
|
-
"""Get quick reference for markets and fields."""
|
|
401
|
-
return get_quick_reference()
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
@mcp.tool()
|
|
405
|
-
def search_available_fields(query: str, market: str = "stocks", limit: int = 15) -> dict[str, Any]:
|
|
406
|
-
"""Search fields by name or display name."""
|
|
407
|
-
results = search_fields(query, market, min(limit, 30))
|
|
408
|
-
return {
|
|
409
|
-
"query": query,
|
|
410
|
-
"market": market,
|
|
411
|
-
"count": len(results),
|
|
412
|
-
"fields": results,
|
|
413
|
-
"hint": "Use the 'name' field as the column name in queries." if results else "No matches. Try different keywords.",
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
@mcp.tool()
|
|
418
|
-
def get_code_example(screener_name: str) -> dict[str, Any]:
|
|
419
|
-
"""Get code example for a screener type."""
|
|
420
|
-
examples = get_screener_code_examples()
|
|
421
|
-
|
|
422
|
-
if screener_name in examples:
|
|
423
|
-
return {"name": screener_name, "code": examples[screener_name]}
|
|
424
|
-
|
|
425
|
-
for name, code in examples.items():
|
|
426
|
-
if name.lower() == screener_name.lower():
|
|
427
|
-
return {"name": name, "code": code}
|
|
428
|
-
|
|
429
|
-
return {"error": f"'{screener_name}' not found.", "available": list(examples.keys())}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
@mcp.tool()
|
|
433
|
-
def list_fields_for_market(market: str = "stocks", category: str | None = None, limit: int = 30) -> dict[str, Any]:
|
|
434
|
-
"""List fields for a market."""
|
|
435
|
-
validation = validate_market(market)
|
|
436
|
-
if not validation.get("valid"):
|
|
437
|
-
return validation
|
|
438
|
-
|
|
439
|
-
fields = get_fields_by_market(validation.get("market", market))
|
|
440
|
-
if not fields:
|
|
441
|
-
return {"error": f"No fields for '{market}'.", "use": "search_fields(query) instead"}
|
|
442
|
-
|
|
443
|
-
if category and isinstance(fields, list):
|
|
444
|
-
fields = [f for f in fields if category.lower() in f.get("type", "").lower()]
|
|
445
|
-
|
|
446
|
-
if isinstance(fields, list):
|
|
447
|
-
return {"fields": [f.get("name") for f in fields[:limit]], "total": len(fields)}
|
|
448
|
-
return {"note": "Use search_fields() for this market"}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
@mcp.tool()
|
|
452
|
-
def get_common_fields_summary(category: str | None = None) -> dict[str, Any]:
|
|
453
|
-
"""Get common fields by category."""
|
|
454
|
-
if category:
|
|
455
|
-
fields = FIELD_CATEGORIES.get(category.lower())
|
|
456
|
-
if fields:
|
|
457
|
-
return {"category": category, "fields": fields}
|
|
458
|
-
return {"error": f"Category '{category}' not found.", "available": list(FIELD_CATEGORIES.keys())}
|
|
459
|
-
return {"categories": FIELD_CATEGORIES}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
# =============================================================================
|
|
463
|
-
# Fast Lookup Tools - Direct answers
|
|
464
|
-
# =============================================================================
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
@mcp.tool()
|
|
468
|
-
def lookup_single_field(field_name: str) -> dict[str, Any]:
|
|
469
|
-
"""Lookup a single field by exact name."""
|
|
470
|
-
return lookup_field(field_name)
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
@mcp.tool()
|
|
474
|
-
def get_filter_example(field_type: str = "number") -> dict[str, Any]:
|
|
475
|
-
"""Get correct filter format for a field type."""
|
|
476
|
-
return get_filter_format(field_type)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
@mcp.tool()
|
|
480
|
-
def check_market(market: str) -> dict[str, Any]:
|
|
481
|
-
"""Validate market name and get correct format."""
|
|
482
|
-
return validate_market(market)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
# =============================================================================
|
|
486
|
-
# Help Tool - Compact guide
|
|
487
|
-
# =============================================================================
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
@mcp.tool()
|
|
491
|
-
def get_help(topic: str | None = None) -> dict[str, Any]:
|
|
492
|
-
"""Usage guide."""
|
|
493
|
-
guide = {
|
|
494
|
-
"quick_start": {
|
|
495
|
-
"1": "get_top_gainers('america', 10) - top gainers",
|
|
496
|
-
"2": "search_symbols('apple', 'america') - find symbols",
|
|
497
|
-
"3": "get_symbol_info('NASDAQ:AAPL') - symbol details",
|
|
498
|
-
},
|
|
499
|
-
"markets": {
|
|
500
|
-
"stocks": ["america", "uk", "germany", "japan", "china"],
|
|
501
|
-
"crypto": ["crypto", "coin"],
|
|
502
|
-
"others": ["forex", "futures", "bonds"],
|
|
503
|
-
},
|
|
504
|
-
"tools": {
|
|
505
|
-
"screening": ["screen_market", "get_top_gainers", "get_top_losers", "get_most_active"],
|
|
506
|
-
"lookup": ["search_symbols", "get_symbol_info", "get_technical_analysis"],
|
|
507
|
-
"reference": ["lookup_field", "search_fields", "check_market", "get_filter_example"],
|
|
508
|
-
},
|
|
509
|
-
"columns": FIELD_CATEGORIES,
|
|
510
|
-
"filters": {
|
|
511
|
-
"format": {"column": "field", "operation": "op", "value": "val"},
|
|
512
|
-
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
513
|
-
},
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if topic:
|
|
517
|
-
t = topic.lower()
|
|
518
|
-
if t in guide:
|
|
519
|
-
return {topic: guide[t]}
|
|
520
|
-
return {"error": f"Topic '{topic}' not found", "available": list(guide.keys())}
|
|
521
|
-
|
|
522
|
-
return guide
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
@mcp.tool()
|
|
526
|
-
def search_symbols(
|
|
527
|
-
query: str,
|
|
528
|
-
market: str = "america",
|
|
529
|
-
limit: int = 25,
|
|
530
|
-
) -> dict[str, Any]:
|
|
531
|
-
"""Search symbols by name or ticker."""
|
|
532
|
-
market = sanitize_market(market)
|
|
533
|
-
limit = max(1, min(limit, 100))
|
|
534
|
-
|
|
535
|
-
# Search fields: name (ticker) and description (full name)
|
|
536
|
-
cols = ["name", "description", "close", "change", "volume", "market_cap_basic", "type", "exchange"]
|
|
537
|
-
|
|
538
|
-
try:
|
|
539
|
-
# Use TradingView text search
|
|
540
|
-
# Search description (company name)
|
|
541
|
-
query_obj = (
|
|
542
|
-
Query()
|
|
543
|
-
.set_markets(market)
|
|
544
|
-
.select(*cols)
|
|
545
|
-
.where(Column("description").like(query))
|
|
546
|
-
.order_by("market_cap_basic", ascending=False)
|
|
547
|
-
.limit(limit)
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
total, df = query_obj.get_scanner_data()
|
|
551
|
-
results = df.to_dict("records")
|
|
552
|
-
|
|
553
|
-
# If nothing found, try searching the name (ticker) field
|
|
554
|
-
if not results:
|
|
555
|
-
query_obj2 = (
|
|
556
|
-
Query()
|
|
557
|
-
.set_markets(market)
|
|
558
|
-
.select(*cols)
|
|
559
|
-
.where(Column("name").like(query))
|
|
560
|
-
.order_by("market_cap_basic", ascending=False)
|
|
561
|
-
.limit(limit)
|
|
562
|
-
)
|
|
563
|
-
total, df = query_obj2.get_scanner_data()
|
|
564
|
-
results = df.to_dict("records")
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
"query": query,
|
|
568
|
-
"market": market,
|
|
569
|
-
"total_found": total,
|
|
570
|
-
"returned": len(results),
|
|
571
|
-
"results": results,
|
|
572
|
-
"hint": "Use 'name' field as symbol for other tools (e.g., get_symbol_info)" if results else "No matches found. Try different keywords.",
|
|
573
|
-
}
|
|
574
|
-
except Exception as e:
|
|
575
|
-
# If LIKE isn't supported, use fallback
|
|
576
|
-
return _search_symbols_fallback(query, market, limit, cols, str(e))
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
def _search_symbols_fallback(
|
|
580
|
-
query: str, market: str, limit: int, cols: list[str], original_error: str
|
|
581
|
-
) -> dict[str, Any]:
|
|
582
|
-
"""Fallback search when LIKE is not supported."""
|
|
583
|
-
try:
|
|
584
|
-
# Fetch more data and filter locally
|
|
585
|
-
query_obj = (
|
|
586
|
-
Query()
|
|
587
|
-
.set_markets(market)
|
|
588
|
-
.select(*cols)
|
|
589
|
-
.order_by("market_cap_basic", ascending=False)
|
|
590
|
-
.limit(500) # Fetch top 500
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
total, df = query_obj.get_scanner_data()
|
|
594
|
-
|
|
595
|
-
# Local filter
|
|
596
|
-
query_lower = query.lower()
|
|
597
|
-
filtered = df[
|
|
598
|
-
df["name"].str.lower().str.contains(query_lower, na=False) |
|
|
599
|
-
df["description"].str.lower().str.contains(query_lower, na=False)
|
|
600
|
-
].head(limit)
|
|
601
|
-
|
|
602
|
-
results = filtered.to_dict("records")
|
|
603
|
-
|
|
604
|
-
return {
|
|
605
|
-
"query": query,
|
|
606
|
-
"market": market,
|
|
607
|
-
"total_found": len(results),
|
|
608
|
-
"returned": len(results),
|
|
609
|
-
"results": results,
|
|
610
|
-
"note": "Results from local filtering of top 500 by market cap",
|
|
611
|
-
"hint": "Use 'name' field as symbol for other tools" if results else "No matches found",
|
|
612
|
-
}
|
|
613
|
-
except Exception as e:
|
|
614
|
-
return {
|
|
615
|
-
"error": f"Search failed: {str(e)}",
|
|
616
|
-
"original_error": original_error,
|
|
617
|
-
"hint": "Try using screen_market with specific filters instead",
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
@mcp.tool()
|
|
622
|
-
def get_symbol_info(
|
|
623
|
-
symbol: str,
|
|
624
|
-
include_technical: bool = False,
|
|
625
|
-
) -> dict[str, Any]:
|
|
626
|
-
"""Get detailed information for a symbol."""
|
|
627
|
-
cols = [
|
|
628
|
-
"name", "description", "close", "open", "high", "low",
|
|
629
|
-
"change", "change_abs", "volume", "market_cap_basic",
|
|
630
|
-
"price_earnings_ttm", "earnings_per_share_basic_ttm",
|
|
631
|
-
"dividend_yield_recent", "sector", "industry", "exchange", "type",
|
|
632
|
-
]
|
|
633
|
-
|
|
634
|
-
if include_technical:
|
|
635
|
-
cols.extend([
|
|
636
|
-
"RSI", "RSI7", "MACD.macd", "MACD.signal",
|
|
637
|
-
"SMA20", "SMA50", "SMA200", "EMA20", "EMA50", "EMA200",
|
|
638
|
-
"BB.upper", "BB.lower", "ATR", "ADX",
|
|
639
|
-
"Recommend.All", "Recommend.MA", "Recommend.Other",
|
|
640
|
-
])
|
|
641
|
-
|
|
642
|
-
try:
|
|
643
|
-
# Determine market
|
|
644
|
-
if ":" in symbol:
|
|
645
|
-
exchange, ticker = symbol.split(":", 1)
|
|
646
|
-
market = EXCHANGE_SCREENER.get(exchange.lower(), "america")
|
|
647
|
-
else:
|
|
648
|
-
ticker = symbol
|
|
649
|
-
market = "america"
|
|
650
|
-
|
|
651
|
-
query = (
|
|
652
|
-
Query()
|
|
653
|
-
.set_markets(market)
|
|
654
|
-
.select(*cols)
|
|
655
|
-
.where(Column("name").isin([symbol, ticker, symbol.upper(), ticker.upper()]))
|
|
656
|
-
.limit(5)
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
total, df = query.get_scanner_data()
|
|
660
|
-
|
|
661
|
-
if df.empty:
|
|
662
|
-
# Fallback to search
|
|
663
|
-
return search_symbols(ticker, market, 5)
|
|
664
|
-
|
|
665
|
-
results = df.to_dict("records")
|
|
666
|
-
|
|
667
|
-
if len(results) == 1:
|
|
668
|
-
return {"symbol": symbol, "found": True, "data": results[0]}
|
|
669
|
-
else:
|
|
670
|
-
return {"symbol": symbol, "found": True, "matches": results}
|
|
671
|
-
|
|
672
|
-
except Exception as e:
|
|
673
|
-
return {
|
|
674
|
-
"error": f"Failed to get symbol info: {str(e)}",
|
|
675
|
-
"hint": "Try using search_symbols to find the correct symbol format",
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
# =============================================================================
|
|
680
|
-
# MCP Tools - Basic Screening
|
|
681
|
-
# =============================================================================
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
@mcp.tool()
|
|
685
|
-
def screen_market(
|
|
686
|
-
market: str = "america",
|
|
687
|
-
columns: Optional[list[str]] = None,
|
|
688
|
-
sort_by: str = "volume",
|
|
689
|
-
ascending: bool = False,
|
|
690
|
-
limit: int = 25,
|
|
691
|
-
filters: Optional[list[dict[str, Any]]] = None,
|
|
692
|
-
) -> dict[str, Any]:
|
|
693
|
-
"""Run a custom market screening query."""
|
|
694
|
-
market = sanitize_market(market)
|
|
695
|
-
limit = max(1, min(limit, 500))
|
|
696
|
-
|
|
697
|
-
# Ensure description is always included
|
|
698
|
-
cols = columns or DEFAULT_COLUMNS
|
|
699
|
-
if "description" not in cols:
|
|
700
|
-
cols = ["description"] + list(cols)
|
|
701
|
-
|
|
702
|
-
query = Query().set_markets(market).select(*cols).order_by(sort_by, ascending=ascending).limit(limit)
|
|
703
|
-
|
|
704
|
-
# Apply filters if provided
|
|
705
|
-
if filters:
|
|
706
|
-
filter_expressions = []
|
|
707
|
-
for f in filters:
|
|
708
|
-
col = Column(f.get("column", "close"))
|
|
709
|
-
op = f.get("operation", "gt")
|
|
710
|
-
val = f.get("value")
|
|
711
|
-
|
|
712
|
-
if op == "gt":
|
|
713
|
-
filter_expressions.append(col > val)
|
|
714
|
-
elif op == "gte":
|
|
715
|
-
filter_expressions.append(col >= val)
|
|
716
|
-
elif op == "lt":
|
|
717
|
-
filter_expressions.append(col < val)
|
|
718
|
-
elif op == "lte":
|
|
719
|
-
filter_expressions.append(col <= val)
|
|
720
|
-
elif op == "eq":
|
|
721
|
-
filter_expressions.append(col == val)
|
|
722
|
-
elif op == "neq":
|
|
723
|
-
filter_expressions.append(col != val)
|
|
724
|
-
elif op == "between" and isinstance(val, (list, tuple)) and len(val) == 2:
|
|
725
|
-
filter_expressions.append(col.between(val[0], val[1]))
|
|
726
|
-
elif op == "isin" and isinstance(val, (list, tuple)):
|
|
727
|
-
filter_expressions.append(col.isin(val))
|
|
728
|
-
|
|
729
|
-
if filter_expressions:
|
|
730
|
-
query = query.where(*filter_expressions)
|
|
731
|
-
|
|
732
|
-
try:
|
|
733
|
-
total_count, df = query.get_scanner_data()
|
|
734
|
-
results = df.to_dict("records")
|
|
735
|
-
|
|
736
|
-
return {
|
|
737
|
-
"total_count": total_count,
|
|
738
|
-
"returned": len(results),
|
|
739
|
-
"market": market,
|
|
740
|
-
"data": results,
|
|
741
|
-
}
|
|
742
|
-
except Exception as e:
|
|
743
|
-
return {"error": str(e), "market": market}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
@mcp.tool()
|
|
747
|
-
def get_top_gainers(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
748
|
-
"""Get top gainers for a market."""
|
|
749
|
-
market = sanitize_market(market)
|
|
750
|
-
limit = max(1, min(limit, 100))
|
|
751
|
-
|
|
752
|
-
# Ensure description is included
|
|
753
|
-
cols = ["name", "description", "close", "change", "change_abs", "volume", "market_cap_basic"]
|
|
754
|
-
|
|
755
|
-
query = (
|
|
756
|
-
Query()
|
|
757
|
-
.set_markets(market)
|
|
758
|
-
.select(*cols)
|
|
759
|
-
.where(Column("change") > 0)
|
|
760
|
-
.order_by("change", ascending=False)
|
|
761
|
-
.limit(limit)
|
|
762
|
-
)
|
|
763
|
-
|
|
764
|
-
try:
|
|
765
|
-
total, df = query.get_scanner_data()
|
|
766
|
-
results = df.to_dict("records")
|
|
767
|
-
return {
|
|
768
|
-
"market": market,
|
|
769
|
-
"type": "top_gainers",
|
|
770
|
-
"total_found": total,
|
|
771
|
-
"returned": len(results),
|
|
772
|
-
"data": results,
|
|
773
|
-
}
|
|
774
|
-
except Exception as e:
|
|
775
|
-
return {"error": str(e), "market": market}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
@mcp.tool()
|
|
779
|
-
def get_top_losers(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
780
|
-
"""Get top losers for a market."""
|
|
781
|
-
market = sanitize_market(market)
|
|
782
|
-
limit = max(1, min(limit, 100))
|
|
783
|
-
|
|
784
|
-
# Ensure description is included
|
|
785
|
-
cols = ["name", "description", "close", "change", "change_abs", "volume", "market_cap_basic"]
|
|
786
|
-
|
|
787
|
-
query = (
|
|
788
|
-
Query()
|
|
789
|
-
.set_markets(market)
|
|
790
|
-
.select(*cols)
|
|
791
|
-
.where(Column("change") < 0)
|
|
792
|
-
.order_by("change", ascending=True)
|
|
793
|
-
.limit(limit)
|
|
794
|
-
)
|
|
795
|
-
|
|
796
|
-
try:
|
|
797
|
-
total, df = query.get_scanner_data()
|
|
798
|
-
results = df.to_dict("records")
|
|
799
|
-
return {
|
|
800
|
-
"market": market,
|
|
801
|
-
"type": "top_losers",
|
|
802
|
-
"total_found": total,
|
|
803
|
-
"returned": len(results),
|
|
804
|
-
"data": results,
|
|
805
|
-
}
|
|
806
|
-
except Exception as e:
|
|
807
|
-
return {"error": str(e), "market": market}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
@mcp.tool()
|
|
811
|
-
def get_most_active(market: str = "america", limit: int = 25) -> dict[str, Any]:
|
|
812
|
-
"""Get most active by volume."""
|
|
813
|
-
market = sanitize_market(market)
|
|
814
|
-
limit = max(1, min(limit, 100))
|
|
815
|
-
|
|
816
|
-
cols = ["name", "description", "close", "change", "volume", "relative_volume_10d_calc", "market_cap_basic"]
|
|
817
|
-
|
|
818
|
-
query = Query().set_markets(market).select(*cols).order_by("volume", ascending=False).limit(limit)
|
|
819
|
-
|
|
820
|
-
try:
|
|
821
|
-
total, df = query.get_scanner_data()
|
|
822
|
-
results = df.to_dict("records")
|
|
823
|
-
return {
|
|
824
|
-
"market": market,
|
|
825
|
-
"type": "most_active",
|
|
826
|
-
"total_found": total,
|
|
827
|
-
"returned": len(results),
|
|
828
|
-
"data": results,
|
|
829
|
-
}
|
|
830
|
-
except Exception as e:
|
|
831
|
-
return {"error": str(e), "market": market}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
@mcp.tool()
|
|
835
|
-
def get_premarket_movers(
|
|
836
|
-
scan_type: str = "gainers", limit: int = 25
|
|
837
|
-
) -> dict[str, Any]:
|
|
838
|
-
"""Get pre-market movers (US only)."""
|
|
839
|
-
limit = max(1, min(limit, 100))
|
|
840
|
-
|
|
841
|
-
scanner_map = {
|
|
842
|
-
"gainers": Scanner.premarket_gainers,
|
|
843
|
-
"losers": Scanner.premarket_losers,
|
|
844
|
-
"most_active": Scanner.premarket_most_active,
|
|
845
|
-
"gappers": Scanner.premarket_gappers,
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
scanner = scanner_map.get(scan_type, Scanner.premarket_gainers).copy()
|
|
849
|
-
scanner = scanner.limit(limit)
|
|
850
|
-
|
|
851
|
-
try:
|
|
852
|
-
total, df = scanner.get_scanner_data()
|
|
853
|
-
return df.to_dict("records")
|
|
854
|
-
except Exception as e:
|
|
855
|
-
return [{"error": str(e)}]
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
@mcp.tool()
|
|
859
|
-
def get_postmarket_movers(
|
|
860
|
-
scan_type: str = "gainers", limit: int = 25
|
|
861
|
-
) -> list[dict[str, Any]]:
|
|
862
|
-
"""Get post-market movers (US only)."""
|
|
863
|
-
limit = max(1, min(limit, 100))
|
|
864
|
-
|
|
865
|
-
scanner_map = {
|
|
866
|
-
"gainers": Scanner.postmarket_gainers,
|
|
867
|
-
"losers": Scanner.postmarket_losers,
|
|
868
|
-
"most_active": Scanner.postmarket_most_active,
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
scanner = scanner_map.get(scan_type, Scanner.postmarket_gainers).copy()
|
|
872
|
-
scanner = scanner.limit(limit)
|
|
873
|
-
|
|
874
|
-
try:
|
|
875
|
-
total, df = scanner.get_scanner_data()
|
|
876
|
-
return df.to_dict("records")
|
|
877
|
-
except Exception as e:
|
|
878
|
-
return [{"error": str(e)}]
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
# =============================================================================
|
|
882
|
-
# MCP Tools - Technical Analysis Scanners
|
|
883
|
-
# =============================================================================
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
@mcp.tool()
|
|
887
|
-
def scan_rsi_extremes(
|
|
888
|
-
market: str = "america",
|
|
889
|
-
condition: str = "oversold",
|
|
890
|
-
threshold: float = 30,
|
|
891
|
-
limit: int = 25,
|
|
892
|
-
) -> list[dict[str, Any]]:
|
|
893
|
-
"""Scan for RSI overbought/oversold conditions."""
|
|
894
|
-
market = sanitize_market(market)
|
|
895
|
-
limit = max(1, min(limit, 100))
|
|
896
|
-
|
|
897
|
-
cols = ["name", "close", "volume", "change", "RSI", "RSI7", "market_cap_basic"]
|
|
898
|
-
|
|
899
|
-
query = Query().set_markets(market).select(*cols).limit(limit)
|
|
900
|
-
|
|
901
|
-
if condition == "oversold":
|
|
902
|
-
threshold = min(threshold, 50)
|
|
903
|
-
query = query.where(Column("RSI") < threshold).order_by("RSI", ascending=True)
|
|
904
|
-
else: # overbought
|
|
905
|
-
threshold = max(threshold, 50)
|
|
906
|
-
query = query.where(Column("RSI") > threshold).order_by("RSI", ascending=False)
|
|
907
|
-
|
|
908
|
-
try:
|
|
909
|
-
total, df = query.get_scanner_data()
|
|
910
|
-
results = df.to_dict("records")
|
|
911
|
-
|
|
912
|
-
# Add signal interpretation
|
|
913
|
-
for r in results:
|
|
914
|
-
rsi = r.get("RSI", 50)
|
|
915
|
-
if rsi < 20:
|
|
916
|
-
r["signal"] = "EXTREMELY_OVERSOLD"
|
|
917
|
-
elif rsi < 30:
|
|
918
|
-
r["signal"] = "OVERSOLD"
|
|
919
|
-
elif rsi > 80:
|
|
920
|
-
r["signal"] = "EXTREMELY_OVERBOUGHT"
|
|
921
|
-
elif rsi > 70:
|
|
922
|
-
r["signal"] = "OVERBOUGHT"
|
|
923
|
-
else:
|
|
924
|
-
r["signal"] = "NEUTRAL"
|
|
925
|
-
|
|
926
|
-
return results
|
|
927
|
-
except Exception as e:
|
|
928
|
-
return [{"error": str(e)}]
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
@mcp.tool()
|
|
932
|
-
def scan_macd_crossover(
|
|
933
|
-
market: str = "america",
|
|
934
|
-
crossover_type: str = "bullish",
|
|
935
|
-
limit: int = 25,
|
|
936
|
-
) -> list[dict[str, Any]]:
|
|
937
|
-
"""Scan for MACD crossover signals."""
|
|
938
|
-
market = sanitize_market(market)
|
|
939
|
-
limit = max(1, min(limit, 100))
|
|
940
|
-
|
|
941
|
-
cols = ["name", "close", "volume", "change", "MACD.macd", "MACD.signal", "market_cap_basic"]
|
|
942
|
-
|
|
943
|
-
query = Query().set_markets(market).select(*cols).limit(limit)
|
|
944
|
-
|
|
945
|
-
if crossover_type == "bullish":
|
|
946
|
-
query = query.where(Column("MACD.macd").crosses_above(Column("MACD.signal"))).order_by(
|
|
947
|
-
"change", ascending=False
|
|
948
|
-
)
|
|
949
|
-
else:
|
|
950
|
-
query = query.where(Column("MACD.macd").crosses_below(Column("MACD.signal"))).order_by(
|
|
951
|
-
"change", ascending=True
|
|
952
|
-
)
|
|
953
|
-
|
|
954
|
-
try:
|
|
955
|
-
total, df = query.get_scanner_data()
|
|
956
|
-
results = df.to_dict("records")
|
|
957
|
-
|
|
958
|
-
for r in results:
|
|
959
|
-
macd = r.get("MACD.macd", 0)
|
|
960
|
-
signal = r.get("MACD.signal", 0)
|
|
961
|
-
r["macd_histogram"] = round(macd - signal, 4) if macd and signal else None
|
|
962
|
-
r["crossover"] = crossover_type
|
|
963
|
-
|
|
964
|
-
return results
|
|
965
|
-
except Exception as e:
|
|
966
|
-
return [{"error": str(e)}]
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
@mcp.tool()
|
|
970
|
-
def scan_bollinger_bands(
|
|
971
|
-
market: str = "america",
|
|
972
|
-
condition: str = "squeeze",
|
|
973
|
-
limit: int = 25,
|
|
974
|
-
) -> list[dict[str, Any]]:
|
|
975
|
-
"""Scan for Bollinger Band conditions."""
|
|
976
|
-
market = sanitize_market(market)
|
|
977
|
-
limit = max(1, min(limit, 100))
|
|
978
|
-
|
|
979
|
-
cols = ["name", "close", "volume", "change", "BB.upper", "BB.lower", "SMA20", "ATR", "Volatility.D"]
|
|
980
|
-
|
|
981
|
-
query = Query().set_markets(market).select(*cols).limit(limit)
|
|
982
|
-
|
|
983
|
-
if condition == "squeeze":
|
|
984
|
-
query = query.order_by("Volatility.D", ascending=True)
|
|
985
|
-
elif condition == "above_upper":
|
|
986
|
-
query = query.where(Column("close") > Column("BB.upper")).order_by("change", ascending=False)
|
|
987
|
-
elif condition == "below_lower":
|
|
988
|
-
query = query.where(Column("close") < Column("BB.lower")).order_by("change", ascending=True)
|
|
989
|
-
|
|
990
|
-
try:
|
|
991
|
-
total, df = query.get_scanner_data()
|
|
992
|
-
results = df.to_dict("records")
|
|
993
|
-
|
|
994
|
-
# Calculate BBW and add signals
|
|
995
|
-
for r in results:
|
|
996
|
-
bb_upper = r.get("BB.upper", 0)
|
|
997
|
-
bb_lower = r.get("BB.lower", 0)
|
|
998
|
-
sma20 = r.get("SMA20", 0)
|
|
999
|
-
close = r.get("close", 0)
|
|
1000
|
-
|
|
1001
|
-
if sma20 and bb_upper and bb_lower:
|
|
1002
|
-
bbw = compute_bollinger_band_width(sma20, bb_upper, bb_lower)
|
|
1003
|
-
r["bbw"] = round(bbw, 4) if bbw else None
|
|
1004
|
-
|
|
1005
|
-
rating, signal = compute_bollinger_rating(close, bb_upper, sma20, bb_lower)
|
|
1006
|
-
r["bb_rating"] = rating
|
|
1007
|
-
r["bb_signal"] = signal
|
|
1008
|
-
|
|
1009
|
-
return results
|
|
1010
|
-
except Exception as e:
|
|
1011
|
-
return [{"error": str(e)}]
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
@mcp.tool()
|
|
1015
|
-
def scan_volume_breakout(
|
|
1016
|
-
market: str = "america",
|
|
1017
|
-
volume_multiplier: float = 2.0,
|
|
1018
|
-
min_price_change: float = 3.0,
|
|
1019
|
-
limit: int = 25,
|
|
1020
|
-
) -> list[dict[str, Any]]:
|
|
1021
|
-
"""Scan for volume breakout candidates."""
|
|
1022
|
-
market = sanitize_market(market)
|
|
1023
|
-
limit = max(1, min(limit, 100))
|
|
1024
|
-
volume_multiplier = max(1.2, min(10.0, volume_multiplier))
|
|
1025
|
-
min_price_change = max(0.5, min(20.0, min_price_change))
|
|
1026
|
-
|
|
1027
|
-
cols = [
|
|
1028
|
-
"name",
|
|
1029
|
-
"close",
|
|
1030
|
-
"volume",
|
|
1031
|
-
"change",
|
|
1032
|
-
"change_abs",
|
|
1033
|
-
"relative_volume_10d_calc",
|
|
1034
|
-
"average_volume_10d_calc",
|
|
1035
|
-
]
|
|
1036
|
-
|
|
1037
|
-
query = (
|
|
1038
|
-
Query()
|
|
1039
|
-
.set_markets(market)
|
|
1040
|
-
.select(*cols)
|
|
1041
|
-
.where(
|
|
1042
|
-
Column("relative_volume_10d_calc") >= volume_multiplier,
|
|
1043
|
-
)
|
|
1044
|
-
.order_by("relative_volume_10d_calc", ascending=False)
|
|
1045
|
-
.limit(limit * 2) # Get more to filter
|
|
1046
|
-
)
|
|
1047
|
-
|
|
1048
|
-
try:
|
|
1049
|
-
total, df = query.get_scanner_data()
|
|
1050
|
-
results = df.to_dict("records")
|
|
1051
|
-
|
|
1052
|
-
# Filter by price change
|
|
1053
|
-
filtered = []
|
|
1054
|
-
for r in results:
|
|
1055
|
-
change = abs(r.get("change", 0) or 0)
|
|
1056
|
-
if change >= min_price_change:
|
|
1057
|
-
rel_vol = r.get("relative_volume_10d_calc", 1)
|
|
1058
|
-
r["breakout_type"] = "bullish" if r.get("change", 0) > 0 else "bearish"
|
|
1059
|
-
r["volume_strength"] = "VERY_STRONG" if rel_vol >= 3 else "STRONG" if rel_vol >= 2 else "MODERATE"
|
|
1060
|
-
filtered.append(r)
|
|
1061
|
-
|
|
1062
|
-
return filtered[:limit]
|
|
1063
|
-
except Exception as e:
|
|
1064
|
-
return [{"error": str(e)}]
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
@mcp.tool()
|
|
1068
|
-
def scan_moving_average_crossover(
|
|
1069
|
-
market: str = "america",
|
|
1070
|
-
crossover_type: str = "golden_cross",
|
|
1071
|
-
limit: int = 25,
|
|
1072
|
-
) -> list[dict[str, Any]]:
|
|
1073
|
-
"""Scan for moving average crossovers."""
|
|
1074
|
-
market = sanitize_market(market)
|
|
1075
|
-
limit = max(1, min(limit, 100))
|
|
1076
|
-
|
|
1077
|
-
scanner_map = {
|
|
1078
|
-
"golden_cross": Scanner.golden_cross,
|
|
1079
|
-
"death_cross": Scanner.death_cross,
|
|
1080
|
-
"above_all_mas": Scanner.above_all_mas,
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
scanner = scanner_map.get(crossover_type)
|
|
1084
|
-
if scanner:
|
|
1085
|
-
scanner = scanner.copy().set_markets(market).limit(limit)
|
|
1086
|
-
else:
|
|
1087
|
-
# below_all_mas
|
|
1088
|
-
scanner = (
|
|
1089
|
-
Query()
|
|
1090
|
-
.set_markets(market)
|
|
1091
|
-
.select("name", "close", "volume", "change", "SMA20", "SMA50", "SMA200")
|
|
1092
|
-
.where(
|
|
1093
|
-
Column("close") < Column("SMA20"),
|
|
1094
|
-
Column("close") < Column("SMA50"),
|
|
1095
|
-
Column("close") < Column("SMA200"),
|
|
1096
|
-
)
|
|
1097
|
-
.order_by("change", ascending=True)
|
|
1098
|
-
.limit(limit)
|
|
1099
|
-
)
|
|
1100
|
-
|
|
1101
|
-
try:
|
|
1102
|
-
total, df = scanner.get_scanner_data()
|
|
1103
|
-
results = df.to_dict("records")
|
|
1104
|
-
|
|
1105
|
-
for r in results:
|
|
1106
|
-
r["crossover_type"] = crossover_type
|
|
1107
|
-
|
|
1108
|
-
return results
|
|
1109
|
-
except Exception as e:
|
|
1110
|
-
return [{"error": str(e)}]
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
@mcp.tool()
|
|
1114
|
-
def scan_52_week_levels(
|
|
1115
|
-
market: str = "america",
|
|
1116
|
-
level_type: str = "near_high",
|
|
1117
|
-
threshold_pct: float = 5.0,
|
|
1118
|
-
limit: int = 25,
|
|
1119
|
-
) -> list[dict[str, Any]]:
|
|
1120
|
-
"""Scan for stocks near 52-week highs or lows."""
|
|
1121
|
-
market = sanitize_market(market)
|
|
1122
|
-
limit = max(1, min(limit, 100))
|
|
1123
|
-
threshold_pct = max(1.0, min(20.0, threshold_pct))
|
|
1124
|
-
|
|
1125
|
-
cols = ["name", "close", "volume", "change", "price_52_week_high", "price_52_week_low", "Perf.Y"]
|
|
1126
|
-
|
|
1127
|
-
query = Query().set_markets(market).select(*cols).limit(limit)
|
|
1128
|
-
|
|
1129
|
-
if level_type == "near_high":
|
|
1130
|
-
# Price within threshold_pct of 52-week high
|
|
1131
|
-
multiplier = 1 - (threshold_pct / 100)
|
|
1132
|
-
query = query.where(Column("close").above_pct("price_52_week_high", multiplier)).order_by(
|
|
1133
|
-
"change", ascending=False
|
|
1134
|
-
)
|
|
1135
|
-
elif level_type == "near_low":
|
|
1136
|
-
# Price within threshold_pct of 52-week low
|
|
1137
|
-
multiplier = 1 + (threshold_pct / 100)
|
|
1138
|
-
query = query.where(Column("close").below_pct("price_52_week_low", multiplier)).order_by(
|
|
1139
|
-
"change", ascending=True
|
|
1140
|
-
)
|
|
1141
|
-
elif level_type == "new_high":
|
|
1142
|
-
query = query.where(Column("close") >= Column("price_52_week_high")).order_by(
|
|
1143
|
-
"change", ascending=False
|
|
1144
|
-
)
|
|
1145
|
-
else: # new_low
|
|
1146
|
-
query = query.where(Column("close") <= Column("price_52_week_low")).order_by(
|
|
1147
|
-
"change", ascending=True
|
|
1148
|
-
)
|
|
1149
|
-
|
|
1150
|
-
try:
|
|
1151
|
-
total, df = query.get_scanner_data()
|
|
1152
|
-
results = df.to_dict("records")
|
|
1153
|
-
|
|
1154
|
-
for r in results:
|
|
1155
|
-
close = r.get("close", 0)
|
|
1156
|
-
high52 = r.get("price_52_week_high", 0)
|
|
1157
|
-
low52 = r.get("price_52_week_low", 0)
|
|
1158
|
-
|
|
1159
|
-
if high52:
|
|
1160
|
-
r["pct_from_52w_high"] = round(((close - high52) / high52) * 100, 2)
|
|
1161
|
-
if low52:
|
|
1162
|
-
r["pct_from_52w_low"] = round(((close - low52) / low52) * 100, 2)
|
|
1163
|
-
|
|
1164
|
-
return results
|
|
1165
|
-
except Exception as e:
|
|
1166
|
-
return [{"error": str(e)}]
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
# =============================================================================
|
|
1170
|
-
# MCP Tools - Symbol Analysis
|
|
1171
|
-
# =============================================================================
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
@mcp.tool()
|
|
1175
|
-
def get_symbol_analysis(
|
|
1176
|
-
symbol: str,
|
|
1177
|
-
include_fundamentals: bool = True,
|
|
1178
|
-
) -> dict[str, Any]:
|
|
1179
|
-
"""Get comprehensive technical analysis for a symbol."""
|
|
1180
|
-
# Select columns based on what we want
|
|
1181
|
-
cols = [
|
|
1182
|
-
"name",
|
|
1183
|
-
"description",
|
|
1184
|
-
"type",
|
|
1185
|
-
"exchange",
|
|
1186
|
-
"close",
|
|
1187
|
-
"open",
|
|
1188
|
-
"high",
|
|
1189
|
-
"low",
|
|
1190
|
-
"volume",
|
|
1191
|
-
"change",
|
|
1192
|
-
"change_abs",
|
|
1193
|
-
# Technical indicators
|
|
1194
|
-
"RSI",
|
|
1195
|
-
"RSI7",
|
|
1196
|
-
"MACD.macd",
|
|
1197
|
-
"MACD.signal",
|
|
1198
|
-
"BB.upper",
|
|
1199
|
-
"BB.lower",
|
|
1200
|
-
"SMA20",
|
|
1201
|
-
"SMA50",
|
|
1202
|
-
"SMA200",
|
|
1203
|
-
"EMA20",
|
|
1204
|
-
"EMA50",
|
|
1205
|
-
"EMA200",
|
|
1206
|
-
"ADX",
|
|
1207
|
-
"ATR",
|
|
1208
|
-
"Stoch.K",
|
|
1209
|
-
"Stoch.D",
|
|
1210
|
-
"VWAP",
|
|
1211
|
-
"Volatility.D",
|
|
1212
|
-
# Performance
|
|
1213
|
-
"Perf.W",
|
|
1214
|
-
"Perf.1M",
|
|
1215
|
-
"Perf.3M",
|
|
1216
|
-
"Perf.Y",
|
|
1217
|
-
# Recommendations
|
|
1218
|
-
"Recommend.All",
|
|
1219
|
-
"Recommend.MA",
|
|
1220
|
-
"Recommend.Other",
|
|
1221
|
-
]
|
|
1222
|
-
|
|
1223
|
-
if include_fundamentals:
|
|
1224
|
-
cols.extend(
|
|
1225
|
-
[
|
|
1226
|
-
"market_cap_basic",
|
|
1227
|
-
"price_earnings_ttm",
|
|
1228
|
-
"earnings_per_share_basic_ttm",
|
|
1229
|
-
"dividend_yield_recent",
|
|
1230
|
-
"price_52_week_high",
|
|
1231
|
-
"price_52_week_low",
|
|
1232
|
-
]
|
|
1233
|
-
)
|
|
1234
|
-
|
|
1235
|
-
try:
|
|
1236
|
-
query = Query().set_tickers(symbol).select(*cols)
|
|
1237
|
-
total, df = query.get_scanner_data()
|
|
1238
|
-
|
|
1239
|
-
if df.empty:
|
|
1240
|
-
return {"error": f"No data found for {symbol}"}
|
|
1241
|
-
|
|
1242
|
-
row = df.iloc[0].to_dict()
|
|
1243
|
-
|
|
1244
|
-
# Perform technical analysis
|
|
1245
|
-
analysis = analyze_indicators(row)
|
|
1246
|
-
|
|
1247
|
-
# Build result
|
|
1248
|
-
result = {
|
|
1249
|
-
"symbol": symbol,
|
|
1250
|
-
"name": row.get("name"),
|
|
1251
|
-
"exchange": row.get("exchange"),
|
|
1252
|
-
"type": row.get("type"),
|
|
1253
|
-
"price_data": {
|
|
1254
|
-
"close": row.get("close"),
|
|
1255
|
-
"open": row.get("open"),
|
|
1256
|
-
"high": row.get("high"),
|
|
1257
|
-
"low": row.get("low"),
|
|
1258
|
-
"volume": row.get("volume"),
|
|
1259
|
-
"change": row.get("change"),
|
|
1260
|
-
"change_abs": row.get("change_abs"),
|
|
1261
|
-
},
|
|
1262
|
-
"technical_indicators": {
|
|
1263
|
-
"rsi": row.get("RSI"),
|
|
1264
|
-
"rsi7": row.get("RSI7"),
|
|
1265
|
-
"macd": row.get("MACD.macd"),
|
|
1266
|
-
"macd_signal": row.get("MACD.signal"),
|
|
1267
|
-
"bb_upper": row.get("BB.upper"),
|
|
1268
|
-
"bb_lower": row.get("BB.lower"),
|
|
1269
|
-
"sma20": row.get("SMA20"),
|
|
1270
|
-
"sma50": row.get("SMA50"),
|
|
1271
|
-
"sma200": row.get("SMA200"),
|
|
1272
|
-
"ema20": row.get("EMA20"),
|
|
1273
|
-
"ema50": row.get("EMA50"),
|
|
1274
|
-
"ema200": row.get("EMA200"),
|
|
1275
|
-
"adx": row.get("ADX"),
|
|
1276
|
-
"atr": row.get("ATR"),
|
|
1277
|
-
"stoch_k": row.get("Stoch.K"),
|
|
1278
|
-
"stoch_d": row.get("Stoch.D"),
|
|
1279
|
-
"vwap": row.get("VWAP"),
|
|
1280
|
-
"volatility": row.get("Volatility.D"),
|
|
1281
|
-
},
|
|
1282
|
-
"performance": {
|
|
1283
|
-
"weekly": row.get("Perf.W"),
|
|
1284
|
-
"monthly": row.get("Perf.1M"),
|
|
1285
|
-
"quarterly": row.get("Perf.3M"),
|
|
1286
|
-
"yearly": row.get("Perf.Y"),
|
|
1287
|
-
},
|
|
1288
|
-
"ratings": {
|
|
1289
|
-
"overall": row.get("Recommend.All"),
|
|
1290
|
-
"overall_label": format_technical_rating(row.get("Recommend.All", 0)),
|
|
1291
|
-
"moving_averages": row.get("Recommend.MA"),
|
|
1292
|
-
"oscillators": row.get("Recommend.Other"),
|
|
1293
|
-
},
|
|
1294
|
-
"analysis": analysis,
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
if include_fundamentals:
|
|
1298
|
-
result["fundamentals"] = {
|
|
1299
|
-
"market_cap": row.get("market_cap_basic"),
|
|
1300
|
-
"pe_ratio": row.get("price_earnings_ttm"),
|
|
1301
|
-
"eps": row.get("earnings_per_share_basic_ttm"),
|
|
1302
|
-
"dividend_yield": row.get("dividend_yield_recent"),
|
|
1303
|
-
"52_week_high": row.get("price_52_week_high"),
|
|
1304
|
-
"52_week_low": row.get("price_52_week_low"),
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
return result
|
|
1308
|
-
|
|
1309
|
-
except Exception as e:
|
|
1310
|
-
return {"error": str(e), "symbol": symbol}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
@mcp.tool()
|
|
1314
|
-
def compare_symbols(
|
|
1315
|
-
symbols: list[str],
|
|
1316
|
-
columns: Optional[list[str]] = None,
|
|
1317
|
-
) -> list[dict[str, Any]]:
|
|
1318
|
-
"""Compare multiple symbols side by side."""
|
|
1319
|
-
if not symbols:
|
|
1320
|
-
return [{"error": "No symbols provided"}]
|
|
1321
|
-
|
|
1322
|
-
cols = columns or [
|
|
1323
|
-
"name",
|
|
1324
|
-
"close",
|
|
1325
|
-
"change",
|
|
1326
|
-
"volume",
|
|
1327
|
-
"RSI",
|
|
1328
|
-
"MACD.macd",
|
|
1329
|
-
"market_cap_basic",
|
|
1330
|
-
"Perf.1M",
|
|
1331
|
-
"Perf.Y",
|
|
1332
|
-
"Recommend.All",
|
|
1333
|
-
]
|
|
1334
|
-
|
|
1335
|
-
try:
|
|
1336
|
-
query = Query().set_tickers(*symbols).select(*cols)
|
|
1337
|
-
total, df = query.get_scanner_data()
|
|
1338
|
-
|
|
1339
|
-
results = df.to_dict("records")
|
|
1340
|
-
|
|
1341
|
-
# Add technical rating labels
|
|
1342
|
-
for r in results:
|
|
1343
|
-
rec = r.get("Recommend.All")
|
|
1344
|
-
if rec is not None:
|
|
1345
|
-
r["rating_label"] = format_technical_rating(rec)
|
|
1346
|
-
|
|
1347
|
-
return results
|
|
1348
|
-
|
|
1349
|
-
except Exception as e:
|
|
1350
|
-
return [{"error": str(e)}]
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
# =============================================================================
|
|
1354
|
-
# MCP Tools - Utility Functions
|
|
199
|
+
# Register Tools
|
|
1355
200
|
# =============================================================================
|
|
1356
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)
|
|
1357
208
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
try:
|
|
1364
|
-
symbols = get_all_symbols(market)
|
|
1365
|
-
return {
|
|
1366
|
-
"market": market,
|
|
1367
|
-
"total_symbols": len(symbols),
|
|
1368
|
-
"symbols": symbols[:500], # Return first 500 to avoid huge responses
|
|
1369
|
-
"note": f"Showing first 500 of {len(symbols)} symbols" if len(symbols) > 500 else None,
|
|
1370
|
-
}
|
|
1371
|
-
except Exception as e:
|
|
1372
|
-
return {"error": str(e), "market": market}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
@mcp.tool()
|
|
1376
|
-
def run_scanner_preset(
|
|
1377
|
-
preset_name: str,
|
|
1378
|
-
market: Optional[str] = None,
|
|
1379
|
-
limit: int = 50,
|
|
1380
|
-
) -> list[dict[str, Any]]:
|
|
1381
|
-
"""Run a pre-built scanner preset."""
|
|
1382
|
-
limit = max(1, min(limit, 100))
|
|
1383
|
-
|
|
1384
|
-
# Try stock scanners first
|
|
1385
|
-
scanner = getattr(Scanner, preset_name, None)
|
|
1386
|
-
|
|
1387
|
-
# Try crypto scanners
|
|
1388
|
-
if scanner is None:
|
|
1389
|
-
scanner = getattr(CryptoScanner, preset_name, None)
|
|
1390
|
-
|
|
1391
|
-
if scanner is None:
|
|
1392
|
-
available = Scanner.names() + CryptoScanner.names()
|
|
1393
|
-
return [{"error": f"Unknown preset: {preset_name}", "available_presets": available}]
|
|
1394
|
-
|
|
1395
|
-
scanner = scanner.copy().limit(limit)
|
|
1396
|
-
|
|
1397
|
-
if market:
|
|
1398
|
-
market = sanitize_market(market)
|
|
1399
|
-
scanner = scanner.set_markets(market)
|
|
1400
|
-
|
|
1401
|
-
try:
|
|
1402
|
-
total, df = scanner.get_scanner_data()
|
|
1403
|
-
return df.to_dict("records")
|
|
1404
|
-
except Exception as e:
|
|
1405
|
-
return [{"error": str(e)}]
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
@mcp.tool()
|
|
1409
|
-
def advanced_query(
|
|
1410
|
-
market: str = "america",
|
|
1411
|
-
select_columns: list[str] = None,
|
|
1412
|
-
conditions: list[dict[str, Any]] = None,
|
|
1413
|
-
logic: str = "and",
|
|
1414
|
-
sort_by: str = "volume",
|
|
1415
|
-
ascending: bool = False,
|
|
1416
|
-
limit: int = 50,
|
|
1417
|
-
) -> list[dict[str, Any]]:
|
|
1418
|
-
"""Execute an advanced query with AND/OR logic."""
|
|
1419
|
-
market = sanitize_market(market)
|
|
1420
|
-
limit = max(1, min(limit, 500))
|
|
1421
|
-
cols = select_columns or TECHNICAL_COLUMNS
|
|
1422
|
-
|
|
1423
|
-
query = Query().set_markets(market).select(*cols).order_by(sort_by, ascending=ascending).limit(limit)
|
|
1424
|
-
|
|
1425
|
-
if conditions:
|
|
1426
|
-
expressions = []
|
|
1427
|
-
for cond in conditions:
|
|
1428
|
-
col = Column(cond.get("column", "close"))
|
|
1429
|
-
op = cond.get("op", "gt")
|
|
1430
|
-
val = cond.get("value")
|
|
1431
|
-
|
|
1432
|
-
if op == "gt":
|
|
1433
|
-
expressions.append(col > val)
|
|
1434
|
-
elif op == "gte":
|
|
1435
|
-
expressions.append(col >= val)
|
|
1436
|
-
elif op == "lt":
|
|
1437
|
-
expressions.append(col < val)
|
|
1438
|
-
elif op == "lte":
|
|
1439
|
-
expressions.append(col <= val)
|
|
1440
|
-
elif op == "eq":
|
|
1441
|
-
expressions.append(col == val)
|
|
1442
|
-
elif op == "neq":
|
|
1443
|
-
expressions.append(col != val)
|
|
1444
|
-
elif op == "between" and isinstance(val, (list, tuple)) and len(val) == 2:
|
|
1445
|
-
expressions.append(col.between(val[0], val[1]))
|
|
1446
|
-
elif op == "isin" and isinstance(val, (list, tuple)):
|
|
1447
|
-
expressions.append(col.isin(val))
|
|
1448
|
-
elif op == "crosses_above":
|
|
1449
|
-
expressions.append(col.crosses_above(Column(val) if isinstance(val, str) else val))
|
|
1450
|
-
elif op == "crosses_below":
|
|
1451
|
-
expressions.append(col.crosses_below(Column(val) if isinstance(val, str) else val))
|
|
1452
|
-
elif op == "above_pct" and isinstance(val, (list, tuple)) and len(val) == 2:
|
|
1453
|
-
expressions.append(col.above_pct(val[0], val[1]))
|
|
1454
|
-
elif op == "below_pct" and isinstance(val, (list, tuple)) and len(val) == 2:
|
|
1455
|
-
expressions.append(col.below_pct(val[0], val[1]))
|
|
1456
|
-
|
|
1457
|
-
if expressions:
|
|
1458
|
-
if logic == "or":
|
|
1459
|
-
query = query.where2(Or(*expressions))
|
|
1460
|
-
else:
|
|
1461
|
-
query = query.where(*expressions)
|
|
1462
|
-
|
|
1463
|
-
try:
|
|
1464
|
-
total, df = query.get_scanner_data()
|
|
1465
|
-
results = df.to_dict("records")
|
|
1466
|
-
return [{"total_count": total, "returned": len(results), "data": results}]
|
|
1467
|
-
except Exception as e:
|
|
1468
|
-
return [{"error": str(e)}]
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
# =============================================================================
|
|
1472
|
-
# MCP Tools - Crypto Specific
|
|
1473
|
-
# =============================================================================
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
@mcp.tool()
|
|
1477
|
-
def get_crypto_gainers(exchange: Optional[str] = None, limit: int = 25) -> list[dict[str, Any]]:
|
|
1478
|
-
"""Get top gaining cryptocurrencies."""
|
|
1479
|
-
limit = max(1, min(limit, 100))
|
|
1480
|
-
|
|
1481
|
-
cols = ["name", "close", "volume", "change", "change_abs", "market_cap_basic", "24h_vol|5"]
|
|
1482
|
-
|
|
1483
|
-
query = (
|
|
1484
|
-
Query()
|
|
1485
|
-
.set_markets("crypto")
|
|
1486
|
-
.select(*cols)
|
|
1487
|
-
.where(Column("change") > 0)
|
|
1488
|
-
.order_by("change", ascending=False)
|
|
1489
|
-
.limit(limit)
|
|
1490
|
-
)
|
|
1491
|
-
|
|
1492
|
-
if exchange:
|
|
1493
|
-
query = query.where(Column("exchange") == exchange.upper())
|
|
1494
|
-
|
|
1495
|
-
try:
|
|
1496
|
-
total, df = query.get_scanner_data()
|
|
1497
|
-
return df.to_dict("records")
|
|
1498
|
-
except Exception as e:
|
|
1499
|
-
return [{"error": str(e)}]
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
@mcp.tool()
|
|
1503
|
-
def get_crypto_losers(exchange: Optional[str] = None, limit: int = 25) -> list[dict[str, Any]]:
|
|
1504
|
-
"""Get top losing cryptocurrencies."""
|
|
1505
|
-
limit = max(1, min(limit, 100))
|
|
1506
|
-
|
|
1507
|
-
cols = ["name", "close", "volume", "change", "change_abs", "market_cap_basic", "24h_vol|5"]
|
|
1508
|
-
|
|
1509
|
-
query = (
|
|
1510
|
-
Query()
|
|
1511
|
-
.set_markets("crypto")
|
|
1512
|
-
.select(*cols)
|
|
1513
|
-
.where(Column("change") < 0)
|
|
1514
|
-
.order_by("change", ascending=True)
|
|
1515
|
-
.limit(limit)
|
|
1516
|
-
)
|
|
1517
|
-
|
|
1518
|
-
if exchange:
|
|
1519
|
-
query = query.where(Column("exchange") == exchange.upper())
|
|
1520
|
-
|
|
1521
|
-
try:
|
|
1522
|
-
total, df = query.get_scanner_data()
|
|
1523
|
-
return df.to_dict("records")
|
|
1524
|
-
except Exception as e:
|
|
1525
|
-
return [{"error": str(e)}]
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
@mcp.tool()
|
|
1529
|
-
def scan_crypto_technicals(
|
|
1530
|
-
scan_type: str = "oversold",
|
|
1531
|
-
exchange: Optional[str] = None,
|
|
1532
|
-
limit: int = 25,
|
|
1533
|
-
) -> list[dict[str, Any]]:
|
|
1534
|
-
"""Scan cryptocurrencies based on technical indicators."""
|
|
1535
|
-
limit = max(1, min(limit, 100))
|
|
1536
|
-
|
|
1537
|
-
scanner_map = {
|
|
1538
|
-
"oversold": CryptoScanner.oversold,
|
|
1539
|
-
"overbought": CryptoScanner.overbought,
|
|
1540
|
-
"high_volume": CryptoScanner.high_volume,
|
|
1541
|
-
"most_volatile": CryptoScanner.most_volatile,
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
if scan_type in scanner_map:
|
|
1545
|
-
scanner = scanner_map[scan_type].copy().limit(limit)
|
|
1546
|
-
elif scan_type == "macd_bullish":
|
|
1547
|
-
scanner = (
|
|
1548
|
-
Query()
|
|
1549
|
-
.set_markets("crypto")
|
|
1550
|
-
.select("name", "close", "volume", "change", "MACD.macd", "MACD.signal", "RSI")
|
|
1551
|
-
.where(Column("MACD.macd").crosses_above(Column("MACD.signal")))
|
|
1552
|
-
.order_by("change", ascending=False)
|
|
1553
|
-
.limit(limit)
|
|
1554
|
-
)
|
|
1555
|
-
elif scan_type == "macd_bearish":
|
|
1556
|
-
scanner = (
|
|
1557
|
-
Query()
|
|
1558
|
-
.set_markets("crypto")
|
|
1559
|
-
.select("name", "close", "volume", "change", "MACD.macd", "MACD.signal", "RSI")
|
|
1560
|
-
.where(Column("MACD.macd").crosses_below(Column("MACD.signal")))
|
|
1561
|
-
.order_by("change", ascending=True)
|
|
1562
|
-
.limit(limit)
|
|
1563
|
-
)
|
|
1564
|
-
else:
|
|
1565
|
-
return [{"error": f"Unknown scan_type: {scan_type}"}]
|
|
1566
|
-
|
|
1567
|
-
if exchange:
|
|
1568
|
-
scanner = scanner.where(Column("exchange") == exchange.upper())
|
|
1569
|
-
|
|
1570
|
-
try:
|
|
1571
|
-
total, df = scanner.get_scanner_data()
|
|
1572
|
-
return df.to_dict("records")
|
|
1573
|
-
except Exception as e:
|
|
1574
|
-
return [{"error": str(e)}]
|
|
209
|
+
mcp.tool()(get_technical_analysis)
|
|
210
|
+
mcp.tool()(scan_rsi_extremes)
|
|
211
|
+
mcp.tool()(scan_bollinger_bands)
|
|
212
|
+
mcp.tool()(scan_macd_crossover)
|
|
1575
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)
|
|
1576
226
|
|
|
1577
227
|
# =============================================================================
|
|
1578
|
-
#
|
|
228
|
+
# Register Resources
|
|
1579
229
|
# =============================================================================
|
|
1580
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)
|
|
1581
243
|
|
|
1582
|
-
def main() -> None:
|
|
1583
|
-
"""Main entry point for the MCP server."""
|
|
1584
|
-
parser = argparse.ArgumentParser(description="TradingView Screener MCP Server")
|
|
1585
|
-
parser.add_argument(
|
|
1586
|
-
"transport",
|
|
1587
|
-
choices=["stdio", "streamable-http"],
|
|
1588
|
-
default="stdio",
|
|
1589
|
-
nargs="?",
|
|
1590
|
-
help="Transport protocol (default: stdio)",
|
|
1591
|
-
)
|
|
1592
|
-
parser.add_argument("--host", default=os.environ.get("HOST", "127.0.0.1"), help="HTTP host")
|
|
1593
|
-
parser.add_argument(
|
|
1594
|
-
"--port", type=int, default=int(os.environ.get("PORT", "8000")), help="HTTP port"
|
|
1595
|
-
)
|
|
1596
|
-
args = parser.parse_args()
|
|
1597
|
-
|
|
1598
|
-
if os.environ.get("DEBUG_MCP"):
|
|
1599
|
-
import sys
|
|
1600
|
-
|
|
1601
|
-
print(f"[DEBUG] TradingView MCP starting: transport={args.transport}", file=sys.stderr, flush=True)
|
|
1602
|
-
|
|
1603
|
-
if args.transport == "stdio":
|
|
1604
|
-
mcp.run()
|
|
1605
|
-
else:
|
|
1606
|
-
try:
|
|
1607
|
-
mcp.settings.host = args.host
|
|
1608
|
-
mcp.settings.port = args.port
|
|
1609
|
-
except AttributeError:
|
|
1610
|
-
pass
|
|
1611
|
-
mcp.run(transport="streamable-http")
|
|
1612
244
|
|
|
245
|
+
def main():
|
|
246
|
+
mcp.run()
|
|
1613
247
|
|
|
1614
248
|
if __name__ == "__main__":
|
|
1615
249
|
main()
|