tradingview-mcp 26.0.0__py3-none-any.whl → 26.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tradingview_mcp/constants.py +100 -331
- tradingview_mcp/data/extracted/ai_quick_reference.json +130 -131
- tradingview_mcp/data/extracted/common_fields.json +5185 -1450
- tradingview_mcp/data/extracted/fields_by_market.json +23704 -3429
- tradingview_mcp/data/extracted/stock_screener_presets.json +1871 -0
- tradingview_mcp/docs_data.py +353 -98
- tradingview_mcp/resources.py +63 -0
- tradingview_mcp/server.py +130 -2030
- 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 +326 -0
- tradingview_mcp/tools/technical.py +113 -0
- tradingview_mcp-26.3.0.dist-info/METADATA +148 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.3.0.dist-info}/RECORD +18 -11
- tradingview_mcp-26.0.0.dist-info/METADATA +0 -333
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.3.0.dist-info}/WHEEL +0 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.3.0.dist-info}/entry_points.txt +0 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.3.0.dist-info}/licenses/LICENSE +0 -0
tradingview_mcp/docs_data.py
CHANGED
|
@@ -1,47 +1,52 @@
|
|
|
1
|
-
"""Load packaged docs
|
|
2
|
-
|
|
3
|
-
This module provides access to:
|
|
4
|
-
- Market metadata (markets.json)
|
|
5
|
-
- Column/field display names (column_display_names.json)
|
|
6
|
-
- Screener presets (screeners/*.json)
|
|
7
|
-
- Market metainfo with field definitions (metainfo/*.json)
|
|
8
|
-
- AI-friendly quick reference (extracted/ai_quick_reference.json)
|
|
9
|
-
- Screener code examples (extracted/screener_code_examples.json)
|
|
10
|
-
- Field definitions by market (extracted/fields_by_market.json)
|
|
11
|
-
|
|
12
|
-
Data is loaded from packaged files first, with fallback to the reference path.
|
|
13
|
-
Set TRADINGVIEW_SCREENER_DOCS_DATA env var to override the reference path.
|
|
14
|
-
"""
|
|
1
|
+
"""Load packaged docs datasets for MCP resources - AI-friendly fast lookup."""
|
|
15
2
|
|
|
16
3
|
from __future__ import annotations
|
|
17
4
|
|
|
18
5
|
import json
|
|
19
|
-
import os
|
|
20
6
|
from functools import lru_cache
|
|
21
7
|
from importlib.resources import files
|
|
22
|
-
from pathlib import Path
|
|
23
8
|
from typing import Any
|
|
24
9
|
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
11
|
+
# Token/size limits
|
|
12
|
+
MAX_ITEMS_DEFAULT = 50
|
|
13
|
+
MAX_ITEMS_LARGE = 200
|
|
14
|
+
|
|
15
|
+
# Valid markets for quick reference
|
|
16
|
+
STOCK_MARKETS = [
|
|
17
|
+
"america", "argentina", "australia", "austria", "bahrain", "bangladesh",
|
|
18
|
+
"belgium", "brazil", "canada", "chile", "china", "colombia", "cyprus",
|
|
19
|
+
"czech", "denmark", "egypt", "estonia", "finland", "france", "germany",
|
|
20
|
+
"greece", "hongkong", "hungary", "iceland", "india", "indonesia", "ireland",
|
|
21
|
+
"israel", "italy", "japan", "kenya", "korea", "ksa", "kuwait", "latvia",
|
|
22
|
+
"lithuania", "luxembourg", "malaysia", "mexico", "morocco", "netherlands",
|
|
23
|
+
"newzealand", "nigeria", "norway", "pakistan", "peru", "philippines",
|
|
24
|
+
"poland", "portugal", "qatar", "romania", "rsa", "russia", "serbia",
|
|
25
|
+
"singapore", "slovakia", "spain", "srilanka", "sweden", "switzerland",
|
|
26
|
+
"taiwan", "thailand", "tunisia", "turkey", "uae", "uk", "venezuela", "vietnam",
|
|
27
|
+
]
|
|
28
|
+
OTHER_MARKETS = ["bond", "bonds", "cfd", "coin", "crypto", "economics2", "forex", "futures", "options"]
|
|
29
|
+
ALL_MARKETS = STOCK_MARKETS + OTHER_MARKETS
|
|
30
|
+
|
|
31
|
+
# Common field categories for quick lookup
|
|
32
|
+
FIELD_CATEGORIES = {
|
|
33
|
+
"price": ["close", "open", "high", "low", "change", "change_abs", "gap"],
|
|
34
|
+
"volume": ["volume", "Value.Traded", "relative_volume_10d_calc", "average_volume_10d_calc"],
|
|
35
|
+
"market_cap": ["market_cap_basic", "market_cap_calc"],
|
|
36
|
+
"fundamental": ["price_earnings_ttm", "earnings_per_share_basic_ttm", "dividends_yield_current"],
|
|
37
|
+
"technical": ["RSI", "MACD.macd", "BB.upper", "BB.lower", "SMA50", "SMA200", "EMA50", "Recommend.All"],
|
|
38
|
+
"performance": ["Perf.W", "Perf.1M", "Perf.3M", "Perf.6M", "Perf.Y", "Perf.YTD"],
|
|
39
|
+
"metadata": ["name", "description", "sector", "exchange", "type", "currency"],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Filter operations reference
|
|
43
|
+
FILTER_OPS = {
|
|
44
|
+
"comparison": ["greater", "less", "equal", "not_equal", "greater_or_equal", "less_or_equal"],
|
|
45
|
+
"range": ["in_range", "not_in_range"],
|
|
46
|
+
"list": ["in_list", "not_in_list"],
|
|
47
|
+
"text": ["match", "not_match", "has", "has_none_of"],
|
|
48
|
+
"null": ["empty", "not_empty"],
|
|
49
|
+
}
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
def _data_path(*parts: str):
|
|
@@ -49,22 +54,12 @@ def _data_path(*parts: str):
|
|
|
49
54
|
return files("tradingview_mcp.data").joinpath(*parts)
|
|
50
55
|
|
|
51
56
|
|
|
52
|
-
def _reference_path(*parts: str) -> Path:
|
|
53
|
-
"""Get path to reference data file."""
|
|
54
|
-
return DEFAULT_REFERENCE_ROOT.joinpath(*parts)
|
|
55
|
-
|
|
56
|
-
|
|
57
57
|
@lru_cache(maxsize=128)
|
|
58
58
|
def load_json(*parts: str) -> Any:
|
|
59
|
-
"""Load JSON file from
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return json.load(handle)
|
|
64
|
-
except (FileNotFoundError, TypeError):
|
|
65
|
-
fallback = _reference_path(*parts)
|
|
66
|
-
with fallback.open("r", encoding="utf-8") as handle:
|
|
67
|
-
return json.load(handle)
|
|
59
|
+
"""Load JSON file from packaged data."""
|
|
60
|
+
path = _data_path(*parts)
|
|
61
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
62
|
+
return json.load(handle)
|
|
68
63
|
|
|
69
64
|
|
|
70
65
|
def get_markets_data() -> dict[str, list[str]]:
|
|
@@ -98,6 +93,50 @@ def get_screeners_data(name: str) -> Any:
|
|
|
98
93
|
return load_json("screeners", name)
|
|
99
94
|
|
|
100
95
|
|
|
96
|
+
def get_stock_screeners() -> list[dict[str, Any]]:
|
|
97
|
+
"""Get stock screeners metadata."""
|
|
98
|
+
return load_json("screeners", "stocks.json")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_stock_screeners_failed() -> list[dict[str, Any]]:
|
|
102
|
+
"""Get failed stock screeners metadata."""
|
|
103
|
+
return load_json("screeners", "stocks_failed.json")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_stock_screener_presets(market: str | None = None) -> dict[str, Any]:
|
|
107
|
+
"""Get stock screener presets by market."""
|
|
108
|
+
data = load_json("extracted", "stock_screener_presets.json")
|
|
109
|
+
if market:
|
|
110
|
+
return data.get(market, {})
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_valid_fields_for_market(market: str) -> list[str]:
|
|
115
|
+
"""Get list of valid field names for a market."""
|
|
116
|
+
fields_data = get_fields_by_market(market)
|
|
117
|
+
if isinstance(fields_data, list):
|
|
118
|
+
return [f.get("name", "") for f in fields_data if f.get("name")]
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def suggest_fields_for_error(error_message: str, market: str = "stocks") -> dict[str, Any]:
|
|
123
|
+
"""Suggest valid fields when an error occurs."""
|
|
124
|
+
valid_fields = get_valid_fields_for_market(market)
|
|
125
|
+
common = get_common_fields()
|
|
126
|
+
|
|
127
|
+
# Extract commonly used fields
|
|
128
|
+
common_names = list(common.keys())[:50]
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"error": error_message,
|
|
132
|
+
"market": market,
|
|
133
|
+
"suggestion": "Use fields from the valid_fields list",
|
|
134
|
+
"common_fields": common_names,
|
|
135
|
+
"total_valid_fields": len(valid_fields),
|
|
136
|
+
"hint": "Call list_fields_for_market(market) for full field list"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
101
140
|
# ============================================================================
|
|
102
141
|
# AI-Friendly Data Access with Output Limits
|
|
103
142
|
# ============================================================================
|
|
@@ -126,45 +165,74 @@ def get_fields_by_market(market: str | None = None) -> dict[str, Any]:
|
|
|
126
165
|
return data
|
|
127
166
|
|
|
128
167
|
|
|
168
|
+
def get_default_columns_for_market(market: str) -> list[str]:
|
|
169
|
+
"""Get smart default columns for a market to avoid 400 errors."""
|
|
170
|
+
# Base columns safe for all
|
|
171
|
+
cols = ["name", "description", "close", "change", "change_abs", "exchange", "type"]
|
|
172
|
+
|
|
173
|
+
# Check if market has specific fields in data
|
|
174
|
+
fields = get_fields_by_market(market)
|
|
175
|
+
if not fields:
|
|
176
|
+
# Fallback defaults if market data missing
|
|
177
|
+
if market == "crypto":
|
|
178
|
+
return cols + ["volume", "market_cap_basic", "24h_vol|5"]
|
|
179
|
+
if market == "forex":
|
|
180
|
+
return cols + ["bid", "ask"]
|
|
181
|
+
return cols + ["volume", "market_cap_basic", "price_earnings_ttm", "sector", "industry"]
|
|
182
|
+
|
|
183
|
+
# Extract available field names
|
|
184
|
+
valid_names = {f.get("name") for f in fields if f.get("name")}
|
|
185
|
+
|
|
186
|
+
# Add common extra columns if they exist in this market
|
|
187
|
+
extras = [
|
|
188
|
+
"open", "high", "low", "volume", "market_cap_basic",
|
|
189
|
+
"24h_vol|5", "bid", "ask", "price_earnings_ttm",
|
|
190
|
+
"earnings_per_share_basic_ttm", "dividend_yield_recent",
|
|
191
|
+
"sector", "industry"
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
for extra in extras:
|
|
195
|
+
if extra in valid_names:
|
|
196
|
+
cols.append(extra)
|
|
197
|
+
|
|
198
|
+
# Deduplicate while preserving order
|
|
199
|
+
seen = set()
|
|
200
|
+
unique_cols = []
|
|
201
|
+
for c in cols:
|
|
202
|
+
if c not in seen:
|
|
203
|
+
seen.add(c)
|
|
204
|
+
unique_cols.append(c)
|
|
205
|
+
|
|
206
|
+
return unique_cols
|
|
207
|
+
|
|
208
|
+
|
|
129
209
|
def paginate_data(
|
|
130
210
|
data: list | dict,
|
|
131
211
|
offset: int = 0,
|
|
132
212
|
limit: int = MAX_ITEMS_DEFAULT,
|
|
133
213
|
confirm_large: bool = False,
|
|
134
214
|
) -> dict[str, Any]:
|
|
135
|
-
"""Paginate
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
data: List or dict to paginate
|
|
139
|
-
offset: Starting index (for lists) or skip count (for dicts)
|
|
140
|
-
limit: Maximum items to return
|
|
141
|
-
confirm_large: If True, allow large outputs without warning
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
Dict with 'data', 'total', 'offset', 'limit', and optional 'warning'
|
|
145
|
-
"""
|
|
215
|
+
"""Paginate data with offset and limit."""
|
|
146
216
|
if isinstance(data, list):
|
|
147
217
|
total = len(data)
|
|
148
|
-
items = data[offset:offset + limit]
|
|
218
|
+
items = data if confirm_large else data[offset:offset + limit]
|
|
149
219
|
else:
|
|
150
220
|
keys = list(data.keys())
|
|
151
221
|
total = len(keys)
|
|
152
|
-
|
|
153
|
-
|
|
222
|
+
if confirm_large:
|
|
223
|
+
items = data
|
|
224
|
+
else:
|
|
225
|
+
selected_keys = keys[offset:offset + limit]
|
|
226
|
+
items = {k: data[k] for k in selected_keys}
|
|
154
227
|
|
|
155
228
|
result: dict[str, Any] = {
|
|
156
229
|
"data": items,
|
|
157
230
|
"total": total,
|
|
158
231
|
"offset": offset,
|
|
159
|
-
"limit": limit,
|
|
160
|
-
"has_more": (offset + limit) < total,
|
|
232
|
+
"limit": total if confirm_large else limit,
|
|
233
|
+
"has_more": False if confirm_large else (offset + limit) < total,
|
|
234
|
+
"next_offset": None if confirm_large else ((offset + limit) if (offset + limit) < total else None),
|
|
161
235
|
}
|
|
162
|
-
|
|
163
|
-
# Add warning for large data if not confirmed
|
|
164
|
-
if total > MAX_ITEMS_DEFAULT and not confirm_large and limit >= total:
|
|
165
|
-
result["warning"] = LARGE_DATA_WARNING.format(size=total)
|
|
166
|
-
result["hint"] = f"Use limit={MAX_ITEMS_DEFAULT} or set confirm_large=True"
|
|
167
|
-
|
|
168
236
|
return result
|
|
169
237
|
|
|
170
238
|
|
|
@@ -173,16 +241,7 @@ def search_fields(
|
|
|
173
241
|
market: str | None = None,
|
|
174
242
|
limit: int = 20,
|
|
175
243
|
) -> list[dict[str, Any]]:
|
|
176
|
-
"""Search fields by name or display name.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
query: Search term (case-insensitive)
|
|
180
|
-
market: Optional market to search in (stocks, crypto, forex, etc.)
|
|
181
|
-
limit: Maximum results to return
|
|
182
|
-
|
|
183
|
-
Returns:
|
|
184
|
-
List of matching field definitions
|
|
185
|
-
"""
|
|
244
|
+
"""Search fields by name or display name."""
|
|
186
245
|
query_lower = query.lower()
|
|
187
246
|
|
|
188
247
|
if market:
|
|
@@ -212,14 +271,7 @@ def search_fields(
|
|
|
212
271
|
|
|
213
272
|
|
|
214
273
|
def get_field_summary(field_name: str) -> dict[str, Any] | None:
|
|
215
|
-
"""Get summary info for a
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
field_name: Field name to look up
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
Field info dict or None if not found
|
|
222
|
-
"""
|
|
274
|
+
"""Get summary info for a field."""
|
|
223
275
|
# Check common fields first
|
|
224
276
|
common = get_common_fields()
|
|
225
277
|
if field_name in common:
|
|
@@ -252,15 +304,7 @@ def truncate_for_tokens(
|
|
|
252
304
|
data: Any,
|
|
253
305
|
max_tokens: int = 10000,
|
|
254
306
|
) -> tuple[Any, bool]:
|
|
255
|
-
"""Truncate data to fit within token limit.
|
|
256
|
-
|
|
257
|
-
Args:
|
|
258
|
-
data: Data to potentially truncate
|
|
259
|
-
max_tokens: Maximum tokens allowed
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
Tuple of (truncated_data, was_truncated)
|
|
263
|
-
"""
|
|
307
|
+
"""Truncate data to fit within a token limit."""
|
|
264
308
|
estimated = estimate_tokens(data)
|
|
265
309
|
if estimated <= max_tokens:
|
|
266
310
|
return data, False
|
|
@@ -295,3 +339,214 @@ def truncate_for_tokens(
|
|
|
295
339
|
if len(json_str) > max_chars:
|
|
296
340
|
return json_str[:max_chars] + "... (truncated)", True
|
|
297
341
|
return data, False
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ============================================================================
|
|
345
|
+
# Fast Lookup Functions - Returns compact data, not full dumps
|
|
346
|
+
# ============================================================================
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def get_quick_reference() -> dict[str, Any]:
|
|
350
|
+
"""Get compact quick reference - markets, field categories, filter ops."""
|
|
351
|
+
return {
|
|
352
|
+
"stock_markets": STOCK_MARKETS,
|
|
353
|
+
"other_markets": OTHER_MARKETS,
|
|
354
|
+
"field_categories": FIELD_CATEGORIES,
|
|
355
|
+
"filter_operations": FILTER_OPS,
|
|
356
|
+
"usage": {
|
|
357
|
+
"lookup_field": "lookup_field('RSI') - get field info",
|
|
358
|
+
"search_fields": "search_fields('volume') - search by keyword",
|
|
359
|
+
"get_filter_format": "get_filter_format('price') - get filter example",
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def lookup_field(field_name: str) -> dict[str, Any]:
|
|
365
|
+
"""Lookup a single field by exact name."""
|
|
366
|
+
# Check common fields
|
|
367
|
+
common = get_common_fields()
|
|
368
|
+
if field_name in common:
|
|
369
|
+
info = common[field_name]
|
|
370
|
+
return {
|
|
371
|
+
"found": True,
|
|
372
|
+
"name": field_name,
|
|
373
|
+
"display_name": info.get("display_name"),
|
|
374
|
+
"type": info.get("type"),
|
|
375
|
+
"variants": info.get("variants"),
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Check display names
|
|
379
|
+
display_names = get_column_display_names()
|
|
380
|
+
if field_name in display_names:
|
|
381
|
+
return {
|
|
382
|
+
"found": True,
|
|
383
|
+
"name": field_name,
|
|
384
|
+
"display_name": display_names[field_name],
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# Not found - suggest similar
|
|
388
|
+
query_lower = field_name.lower()
|
|
389
|
+
suggestions = [
|
|
390
|
+
name for name in list(common.keys())[:200]
|
|
391
|
+
if query_lower in name.lower()
|
|
392
|
+
][:5]
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
"found": False,
|
|
396
|
+
"name": field_name,
|
|
397
|
+
"suggestions": suggestions,
|
|
398
|
+
"hint": f"Field '{field_name}' not found. Try one of: {suggestions}" if suggestions else f"Field '{field_name}' not found. Use search_fields() to find available fields.",
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def get_filter_format(field_type: str = "number") -> dict[str, Any]:
|
|
403
|
+
"""Get correct filter format for a field type."""
|
|
404
|
+
formats = {
|
|
405
|
+
"number": {
|
|
406
|
+
"example": {"column": "RSI", "operation": "lt", "value": 30},
|
|
407
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between"],
|
|
408
|
+
"between_example": {"column": "RSI", "operation": "between", "value": [20, 80]},
|
|
409
|
+
},
|
|
410
|
+
"price": {
|
|
411
|
+
"example": {"column": "close", "operation": "gt", "value": 100},
|
|
412
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "between"],
|
|
413
|
+
},
|
|
414
|
+
"percent": {
|
|
415
|
+
"example": {"column": "change", "operation": "gt", "value": 5},
|
|
416
|
+
"operations": ["gt", "gte", "lt", "lte", "between"],
|
|
417
|
+
"note": "Values are percentages, e.g., 5 = 5%",
|
|
418
|
+
},
|
|
419
|
+
"text": {
|
|
420
|
+
"example": {"column": "sector", "operation": "eq", "value": "Technology"},
|
|
421
|
+
"operations": ["eq", "neq", "isin"],
|
|
422
|
+
"isin_example": {"column": "exchange", "operation": "isin", "value": ["NASDAQ", "NYSE"]},
|
|
423
|
+
},
|
|
424
|
+
"bool": {
|
|
425
|
+
"example": {"column": "is_primary", "operation": "eq", "value": True},
|
|
426
|
+
"operations": ["eq"],
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if field_type in formats:
|
|
431
|
+
return {"type": field_type, **formats[field_type]}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
"error": f"Unknown type '{field_type}'",
|
|
435
|
+
"available_types": list(formats.keys()),
|
|
436
|
+
"default": formats["number"],
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_market_fields_summary(market: str) -> dict[str, Any]:
|
|
441
|
+
"""Get summary of available fields for a market (not full list)."""
|
|
442
|
+
if market not in ALL_MARKETS:
|
|
443
|
+
return {
|
|
444
|
+
"error": f"Invalid market '{market}'",
|
|
445
|
+
"valid_markets": ALL_MARKETS,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Try metainfo first
|
|
449
|
+
try:
|
|
450
|
+
metainfo = get_metainfo(market if market not in STOCK_MARKETS else "stocks")
|
|
451
|
+
if isinstance(metainfo, list):
|
|
452
|
+
field_count = len(metainfo)
|
|
453
|
+
# Get field types distribution
|
|
454
|
+
types = {}
|
|
455
|
+
for f in metainfo[:500]:
|
|
456
|
+
t = f.get("t", "unknown")
|
|
457
|
+
types[t] = types.get(t, 0) + 1
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
"market": market,
|
|
461
|
+
"total_fields": field_count,
|
|
462
|
+
"field_types": types,
|
|
463
|
+
"sample_fields": [f.get("n") for f in metainfo[:20]],
|
|
464
|
+
"hint": "Use search_fields(query, market) to find specific fields",
|
|
465
|
+
}
|
|
466
|
+
except FileNotFoundError:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
# Fallback to fields_by_market
|
|
470
|
+
fields = get_fields_by_market(market)
|
|
471
|
+
if isinstance(fields, list):
|
|
472
|
+
return {
|
|
473
|
+
"market": market,
|
|
474
|
+
"total_fields": len(fields),
|
|
475
|
+
"sample_fields": [f.get("name") for f in fields[:20]],
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {"market": market, "note": "Use get_common_fields() for this market"}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def get_screener_preset_names() -> list[str]:
|
|
482
|
+
"""Get list of available screener preset names only."""
|
|
483
|
+
presets = get_screener_presets()
|
|
484
|
+
return [p.get("name", "") for p in presets if p.get("name")]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def get_code_example_names() -> list[str]:
|
|
488
|
+
"""Get list of available code example names."""
|
|
489
|
+
examples = get_screener_code_examples()
|
|
490
|
+
return list(examples.keys())
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def validate_market(market: str) -> dict[str, Any]:
|
|
494
|
+
"""Validate market name and return correct format if invalid."""
|
|
495
|
+
if market in ALL_MARKETS:
|
|
496
|
+
return {"valid": True, "market": market}
|
|
497
|
+
|
|
498
|
+
# Try case-insensitive match
|
|
499
|
+
market_lower = market.lower()
|
|
500
|
+
for m in ALL_MARKETS:
|
|
501
|
+
if m.lower() == market_lower:
|
|
502
|
+
return {"valid": True, "market": m, "normalized": True}
|
|
503
|
+
|
|
504
|
+
# Suggest similar
|
|
505
|
+
suggestions = [m for m in ALL_MARKETS if market_lower in m.lower()][:5]
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
"valid": False,
|
|
509
|
+
"input": market,
|
|
510
|
+
"suggestions": suggestions if suggestions else ALL_MARKETS[:10],
|
|
511
|
+
"hint": f"Invalid market '{market}'. Use one of the valid markets.",
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def build_error_with_format(
|
|
516
|
+
error_type: str,
|
|
517
|
+
context: dict[str, Any] | None = None,
|
|
518
|
+
) -> dict[str, Any]:
|
|
519
|
+
"""Build helpful error response with correct format examples."""
|
|
520
|
+
error_formats = {
|
|
521
|
+
"invalid_market": {
|
|
522
|
+
"error": "Invalid market",
|
|
523
|
+
"valid_markets": ALL_MARKETS,
|
|
524
|
+
"example": "market='america'",
|
|
525
|
+
},
|
|
526
|
+
"invalid_field": {
|
|
527
|
+
"error": "Invalid field",
|
|
528
|
+
"common_fields": list(FIELD_CATEGORIES.get("price", [])) + list(FIELD_CATEGORIES.get("volume", [])),
|
|
529
|
+
"example": "columns=['close', 'volume', 'change']",
|
|
530
|
+
"hint": "Use search_fields(query) to find fields",
|
|
531
|
+
},
|
|
532
|
+
"invalid_filter": {
|
|
533
|
+
"error": "Invalid filter format",
|
|
534
|
+
"correct_format": {"column": "field_name", "operation": "op", "value": "value"},
|
|
535
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
536
|
+
"examples": [
|
|
537
|
+
{"column": "change", "operation": "gt", "value": 5},
|
|
538
|
+
{"column": "volume", "operation": "gt", "value": 1000000},
|
|
539
|
+
{"column": "RSI", "operation": "between", "value": [20, 30]},
|
|
540
|
+
],
|
|
541
|
+
},
|
|
542
|
+
"invalid_sort": {
|
|
543
|
+
"error": "Invalid sort field",
|
|
544
|
+
"common_sort_fields": ["volume", "change", "market_cap_basic", "close", "RSI"],
|
|
545
|
+
"example": "sort_by='volume', ascending=False",
|
|
546
|
+
},
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
result = error_formats.get(error_type, {"error": error_type})
|
|
550
|
+
if context:
|
|
551
|
+
result["context"] = context
|
|
552
|
+
return result
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from tradingview_mcp.docs_data import (
|
|
5
|
+
get_markets_data,
|
|
6
|
+
get_column_display_names,
|
|
7
|
+
get_screener_presets,
|
|
8
|
+
get_screener_markets,
|
|
9
|
+
get_stock_screeners,
|
|
10
|
+
get_stock_screeners_failed,
|
|
11
|
+
get_fields_by_market,
|
|
12
|
+
get_ai_quick_reference,
|
|
13
|
+
get_screener_code_examples,
|
|
14
|
+
)
|
|
15
|
+
from tradingview_mcp.constants import EXCHANGE_SCREENER
|
|
16
|
+
|
|
17
|
+
def list_markets() -> dict[str, Any]:
|
|
18
|
+
"""List all available markets."""
|
|
19
|
+
return get_markets_data()
|
|
20
|
+
|
|
21
|
+
def list_columns() -> dict[str, str]:
|
|
22
|
+
"""List all available columns."""
|
|
23
|
+
return get_column_display_names()
|
|
24
|
+
|
|
25
|
+
def list_crypto_exchanges() -> list[str]:
|
|
26
|
+
"""List all crypto exchanges."""
|
|
27
|
+
return [k for k, v in EXCHANGE_SCREENER.items() if v == "crypto"]
|
|
28
|
+
|
|
29
|
+
def list_scanner_presets() -> list[dict[str, Any]]:
|
|
30
|
+
"""List scanner presets."""
|
|
31
|
+
return get_screener_presets()
|
|
32
|
+
|
|
33
|
+
def docs_params() -> dict[str, Any]:
|
|
34
|
+
"""Documentation for parameters."""
|
|
35
|
+
return {"limit": "Max 500", "range": "[0, 100]"}
|
|
36
|
+
|
|
37
|
+
def docs_screeners() -> list[str]:
|
|
38
|
+
"""Documentation for screeners."""
|
|
39
|
+
return get_screener_markets()
|
|
40
|
+
|
|
41
|
+
def docs_stock_screeners() -> list[dict[str, Any]]:
|
|
42
|
+
"""Documentation for stock screeners."""
|
|
43
|
+
return get_stock_screeners()
|
|
44
|
+
|
|
45
|
+
def docs_stock_screeners_failed() -> list[dict[str, Any]]:
|
|
46
|
+
"""Documentation for failed stock screeners."""
|
|
47
|
+
return get_stock_screeners_failed()
|
|
48
|
+
|
|
49
|
+
def docs_fields() -> dict[str, Any]:
|
|
50
|
+
"""Documentation for fields."""
|
|
51
|
+
return get_fields_by_market("stocks") # Default to stocks
|
|
52
|
+
|
|
53
|
+
def docs_markets() -> dict[str, Any]:
|
|
54
|
+
"""Documentation for markets."""
|
|
55
|
+
return get_markets_data()
|
|
56
|
+
|
|
57
|
+
def docs_ai_reference() -> dict[str, Any]:
|
|
58
|
+
"""Documentation for AI reference."""
|
|
59
|
+
return get_ai_quick_reference()
|
|
60
|
+
|
|
61
|
+
def docs_code_examples() -> dict[str, Any]:
|
|
62
|
+
"""Documentation for code examples."""
|
|
63
|
+
return get_screener_code_examples()
|