tradingview-mcp 26.0.0__py3-none-any.whl → 26.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tradingview_mcp/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 +312 -98
- tradingview_mcp/server.py +266 -800
- tradingview_mcp-26.2.0.dist-info/METADATA +141 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/RECORD +11 -10
- tradingview_mcp-26.0.0.dist-info/METADATA +0 -333
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/WHEEL +0 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.0.dist-info}/entry_points.txt +0 -0
- {tradingview_mcp-26.0.0.dist-info → tradingview_mcp-26.2.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
|
# ============================================================================
|
|
@@ -132,39 +171,27 @@ def paginate_data(
|
|
|
132
171
|
limit: int = MAX_ITEMS_DEFAULT,
|
|
133
172
|
confirm_large: bool = False,
|
|
134
173
|
) -> 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
|
-
"""
|
|
174
|
+
"""Paginate data with offset and limit."""
|
|
146
175
|
if isinstance(data, list):
|
|
147
176
|
total = len(data)
|
|
148
|
-
items = data[offset:offset + limit]
|
|
177
|
+
items = data if confirm_large else data[offset:offset + limit]
|
|
149
178
|
else:
|
|
150
179
|
keys = list(data.keys())
|
|
151
180
|
total = len(keys)
|
|
152
|
-
|
|
153
|
-
|
|
181
|
+
if confirm_large:
|
|
182
|
+
items = data
|
|
183
|
+
else:
|
|
184
|
+
selected_keys = keys[offset:offset + limit]
|
|
185
|
+
items = {k: data[k] for k in selected_keys}
|
|
154
186
|
|
|
155
187
|
result: dict[str, Any] = {
|
|
156
188
|
"data": items,
|
|
157
189
|
"total": total,
|
|
158
190
|
"offset": offset,
|
|
159
|
-
"limit": limit,
|
|
160
|
-
"has_more": (offset + limit) < total,
|
|
191
|
+
"limit": total if confirm_large else limit,
|
|
192
|
+
"has_more": False if confirm_large else (offset + limit) < total,
|
|
193
|
+
"next_offset": None if confirm_large else ((offset + limit) if (offset + limit) < total else None),
|
|
161
194
|
}
|
|
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
195
|
return result
|
|
169
196
|
|
|
170
197
|
|
|
@@ -173,16 +200,7 @@ def search_fields(
|
|
|
173
200
|
market: str | None = None,
|
|
174
201
|
limit: int = 20,
|
|
175
202
|
) -> 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
|
-
"""
|
|
203
|
+
"""Search fields by name or display name."""
|
|
186
204
|
query_lower = query.lower()
|
|
187
205
|
|
|
188
206
|
if market:
|
|
@@ -212,14 +230,7 @@ def search_fields(
|
|
|
212
230
|
|
|
213
231
|
|
|
214
232
|
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
|
-
"""
|
|
233
|
+
"""Get summary info for a field."""
|
|
223
234
|
# Check common fields first
|
|
224
235
|
common = get_common_fields()
|
|
225
236
|
if field_name in common:
|
|
@@ -252,15 +263,7 @@ def truncate_for_tokens(
|
|
|
252
263
|
data: Any,
|
|
253
264
|
max_tokens: int = 10000,
|
|
254
265
|
) -> 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
|
-
"""
|
|
266
|
+
"""Truncate data to fit within a token limit."""
|
|
264
267
|
estimated = estimate_tokens(data)
|
|
265
268
|
if estimated <= max_tokens:
|
|
266
269
|
return data, False
|
|
@@ -295,3 +298,214 @@ def truncate_for_tokens(
|
|
|
295
298
|
if len(json_str) > max_chars:
|
|
296
299
|
return json_str[:max_chars] + "... (truncated)", True
|
|
297
300
|
return data, False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ============================================================================
|
|
304
|
+
# Fast Lookup Functions - Returns compact data, not full dumps
|
|
305
|
+
# ============================================================================
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def get_quick_reference() -> dict[str, Any]:
|
|
309
|
+
"""Get compact quick reference - markets, field categories, filter ops."""
|
|
310
|
+
return {
|
|
311
|
+
"stock_markets": STOCK_MARKETS,
|
|
312
|
+
"other_markets": OTHER_MARKETS,
|
|
313
|
+
"field_categories": FIELD_CATEGORIES,
|
|
314
|
+
"filter_operations": FILTER_OPS,
|
|
315
|
+
"usage": {
|
|
316
|
+
"lookup_field": "lookup_field('RSI') - get field info",
|
|
317
|
+
"search_fields": "search_fields('volume') - search by keyword",
|
|
318
|
+
"get_filter_format": "get_filter_format('price') - get filter example",
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def lookup_field(field_name: str) -> dict[str, Any]:
|
|
324
|
+
"""Lookup a single field by exact name."""
|
|
325
|
+
# Check common fields
|
|
326
|
+
common = get_common_fields()
|
|
327
|
+
if field_name in common:
|
|
328
|
+
info = common[field_name]
|
|
329
|
+
return {
|
|
330
|
+
"found": True,
|
|
331
|
+
"name": field_name,
|
|
332
|
+
"display_name": info.get("display_name"),
|
|
333
|
+
"type": info.get("type"),
|
|
334
|
+
"variants": info.get("variants"),
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Check display names
|
|
338
|
+
display_names = get_column_display_names()
|
|
339
|
+
if field_name in display_names:
|
|
340
|
+
return {
|
|
341
|
+
"found": True,
|
|
342
|
+
"name": field_name,
|
|
343
|
+
"display_name": display_names[field_name],
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Not found - suggest similar
|
|
347
|
+
query_lower = field_name.lower()
|
|
348
|
+
suggestions = [
|
|
349
|
+
name for name in list(common.keys())[:200]
|
|
350
|
+
if query_lower in name.lower()
|
|
351
|
+
][:5]
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
"found": False,
|
|
355
|
+
"name": field_name,
|
|
356
|
+
"suggestions": suggestions,
|
|
357
|
+
"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.",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_filter_format(field_type: str = "number") -> dict[str, Any]:
|
|
362
|
+
"""Get correct filter format for a field type."""
|
|
363
|
+
formats = {
|
|
364
|
+
"number": {
|
|
365
|
+
"example": {"column": "RSI", "operation": "lt", "value": 30},
|
|
366
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between"],
|
|
367
|
+
"between_example": {"column": "RSI", "operation": "between", "value": [20, 80]},
|
|
368
|
+
},
|
|
369
|
+
"price": {
|
|
370
|
+
"example": {"column": "close", "operation": "gt", "value": 100},
|
|
371
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "between"],
|
|
372
|
+
},
|
|
373
|
+
"percent": {
|
|
374
|
+
"example": {"column": "change", "operation": "gt", "value": 5},
|
|
375
|
+
"operations": ["gt", "gte", "lt", "lte", "between"],
|
|
376
|
+
"note": "Values are percentages, e.g., 5 = 5%",
|
|
377
|
+
},
|
|
378
|
+
"text": {
|
|
379
|
+
"example": {"column": "sector", "operation": "eq", "value": "Technology"},
|
|
380
|
+
"operations": ["eq", "neq", "isin"],
|
|
381
|
+
"isin_example": {"column": "exchange", "operation": "isin", "value": ["NASDAQ", "NYSE"]},
|
|
382
|
+
},
|
|
383
|
+
"bool": {
|
|
384
|
+
"example": {"column": "is_primary", "operation": "eq", "value": True},
|
|
385
|
+
"operations": ["eq"],
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if field_type in formats:
|
|
390
|
+
return {"type": field_type, **formats[field_type]}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
"error": f"Unknown type '{field_type}'",
|
|
394
|
+
"available_types": list(formats.keys()),
|
|
395
|
+
"default": formats["number"],
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def get_market_fields_summary(market: str) -> dict[str, Any]:
|
|
400
|
+
"""Get summary of available fields for a market (not full list)."""
|
|
401
|
+
if market not in ALL_MARKETS:
|
|
402
|
+
return {
|
|
403
|
+
"error": f"Invalid market '{market}'",
|
|
404
|
+
"valid_markets": ALL_MARKETS,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# Try metainfo first
|
|
408
|
+
try:
|
|
409
|
+
metainfo = get_metainfo(market if market not in STOCK_MARKETS else "stocks")
|
|
410
|
+
if isinstance(metainfo, list):
|
|
411
|
+
field_count = len(metainfo)
|
|
412
|
+
# Get field types distribution
|
|
413
|
+
types = {}
|
|
414
|
+
for f in metainfo[:500]:
|
|
415
|
+
t = f.get("t", "unknown")
|
|
416
|
+
types[t] = types.get(t, 0) + 1
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
"market": market,
|
|
420
|
+
"total_fields": field_count,
|
|
421
|
+
"field_types": types,
|
|
422
|
+
"sample_fields": [f.get("n") for f in metainfo[:20]],
|
|
423
|
+
"hint": "Use search_fields(query, market) to find specific fields",
|
|
424
|
+
}
|
|
425
|
+
except FileNotFoundError:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
# Fallback to fields_by_market
|
|
429
|
+
fields = get_fields_by_market(market)
|
|
430
|
+
if isinstance(fields, list):
|
|
431
|
+
return {
|
|
432
|
+
"market": market,
|
|
433
|
+
"total_fields": len(fields),
|
|
434
|
+
"sample_fields": [f.get("name") for f in fields[:20]],
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {"market": market, "note": "Use get_common_fields() for this market"}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_screener_preset_names() -> list[str]:
|
|
441
|
+
"""Get list of available screener preset names only."""
|
|
442
|
+
presets = get_screener_presets()
|
|
443
|
+
return [p.get("name", "") for p in presets if p.get("name")]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def get_code_example_names() -> list[str]:
|
|
447
|
+
"""Get list of available code example names."""
|
|
448
|
+
examples = get_screener_code_examples()
|
|
449
|
+
return list(examples.keys())
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def validate_market(market: str) -> dict[str, Any]:
|
|
453
|
+
"""Validate market name and return correct format if invalid."""
|
|
454
|
+
if market in ALL_MARKETS:
|
|
455
|
+
return {"valid": True, "market": market}
|
|
456
|
+
|
|
457
|
+
# Try case-insensitive match
|
|
458
|
+
market_lower = market.lower()
|
|
459
|
+
for m in ALL_MARKETS:
|
|
460
|
+
if m.lower() == market_lower:
|
|
461
|
+
return {"valid": True, "market": m, "normalized": True}
|
|
462
|
+
|
|
463
|
+
# Suggest similar
|
|
464
|
+
suggestions = [m for m in ALL_MARKETS if market_lower in m.lower()][:5]
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
"valid": False,
|
|
468
|
+
"input": market,
|
|
469
|
+
"suggestions": suggestions if suggestions else ALL_MARKETS[:10],
|
|
470
|
+
"hint": f"Invalid market '{market}'. Use one of the valid markets.",
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def build_error_with_format(
|
|
475
|
+
error_type: str,
|
|
476
|
+
context: dict[str, Any] | None = None,
|
|
477
|
+
) -> dict[str, Any]:
|
|
478
|
+
"""Build helpful error response with correct format examples."""
|
|
479
|
+
error_formats = {
|
|
480
|
+
"invalid_market": {
|
|
481
|
+
"error": "Invalid market",
|
|
482
|
+
"valid_markets": ALL_MARKETS,
|
|
483
|
+
"example": "market='america'",
|
|
484
|
+
},
|
|
485
|
+
"invalid_field": {
|
|
486
|
+
"error": "Invalid field",
|
|
487
|
+
"common_fields": list(FIELD_CATEGORIES.get("price", [])) + list(FIELD_CATEGORIES.get("volume", [])),
|
|
488
|
+
"example": "columns=['close', 'volume', 'change']",
|
|
489
|
+
"hint": "Use search_fields(query) to find fields",
|
|
490
|
+
},
|
|
491
|
+
"invalid_filter": {
|
|
492
|
+
"error": "Invalid filter format",
|
|
493
|
+
"correct_format": {"column": "field_name", "operation": "op", "value": "value"},
|
|
494
|
+
"operations": ["gt", "gte", "lt", "lte", "eq", "neq", "between", "isin"],
|
|
495
|
+
"examples": [
|
|
496
|
+
{"column": "change", "operation": "gt", "value": 5},
|
|
497
|
+
{"column": "volume", "operation": "gt", "value": 1000000},
|
|
498
|
+
{"column": "RSI", "operation": "between", "value": [20, 30]},
|
|
499
|
+
],
|
|
500
|
+
},
|
|
501
|
+
"invalid_sort": {
|
|
502
|
+
"error": "Invalid sort field",
|
|
503
|
+
"common_sort_fields": ["volume", "change", "market_cap_basic", "close", "RSI"],
|
|
504
|
+
"example": "sort_by='volume', ascending=False",
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
result = error_formats.get(error_type, {"error": error_type})
|
|
509
|
+
if context:
|
|
510
|
+
result["context"] = context
|
|
511
|
+
return result
|