tradingview-mcp 1.0.0__py3-none-any.whl → 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tradingview_mcp/constants.py +6 -4
- tradingview_mcp/data/__init__.py +9 -0
- tradingview_mcp/data/column_display_names.json +827 -0
- tradingview_mcp/data/extracted/__init__.py +1 -0
- tradingview_mcp/data/extracted/ai_quick_reference.json +212 -0
- tradingview_mcp/data/extracted/common_fields.json +3627 -0
- tradingview_mcp/data/extracted/fields_by_market.json +23299 -0
- tradingview_mcp/data/extracted/screener_code_examples.json +9 -0
- tradingview_mcp/data/markets.json +83 -0
- tradingview_mcp/data/metainfo/bond.json +17406 -0
- tradingview_mcp/data/metainfo/bonds.json +1303 -0
- tradingview_mcp/data/metainfo/cfd.json +21603 -0
- tradingview_mcp/data/metainfo/coin.json +21111 -0
- tradingview_mcp/data/metainfo/crypto.json +23078 -0
- tradingview_mcp/data/metainfo/economics2.json +1026 -0
- tradingview_mcp/data/metainfo/forex.json +21003 -0
- tradingview_mcp/data/metainfo/futures.json +2972 -0
- tradingview_mcp/data/metainfo/ireland.json +22517 -0
- tradingview_mcp/data/metainfo/options.json +499 -0
- tradingview_mcp/data/metainfo/stocks.json +29808 -0
- tradingview_mcp/data/screeners/main_screeners.json +540 -0
- tradingview_mcp/data/screeners/markets.json +70 -0
- tradingview_mcp/data/screeners/stocks.json +416 -0
- tradingview_mcp/data/screeners/stocks_failed.json +36081 -0
- tradingview_mcp/docs_data.py +297 -0
- tradingview_mcp/scanner.py +2 -1
- tradingview_mcp/server.py +839 -51
- tradingview_mcp-1.0.dist-info/METADATA +334 -0
- tradingview_mcp-1.0.dist-info/RECORD +37 -0
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.0.dist-info}/licenses/LICENSE +0 -2
- tradingview_mcp-1.0.0.dist-info/METADATA +0 -182
- tradingview_mcp-1.0.0.dist-info/RECORD +0 -13
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.0.dist-info}/WHEEL +0 -0
- {tradingview_mcp-1.0.0.dist-info → tradingview_mcp-1.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Load packaged docs and reference datasets for MCP resources.
|
|
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
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from functools import lru_cache
|
|
21
|
+
from importlib.resources import files
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
DEFAULT_REFERENCE_ROOT = Path(
|
|
27
|
+
os.getenv(
|
|
28
|
+
"TRADINGVIEW_SCREENER_DOCS_DATA",
|
|
29
|
+
"/home/henrik/tradingview-mcp/referance/TradingView-Screener-docs/data",
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Token/size limits for AI-friendly responses
|
|
34
|
+
MAX_ITEMS_DEFAULT = 100
|
|
35
|
+
MAX_ITEMS_LARGE = 500
|
|
36
|
+
MAX_CHARS_DEFAULT = 50000 # ~12.5k tokens
|
|
37
|
+
MAX_CHARS_LARGE = 200000 # ~50k tokens
|
|
38
|
+
|
|
39
|
+
# Large data warnings
|
|
40
|
+
LARGE_DATA_WARNING = (
|
|
41
|
+
"⚠️ This data is large ({size} items). "
|
|
42
|
+
"Use `limit` parameter to reduce output. "
|
|
43
|
+
"Set `confirm_large=True` to retrieve full data."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _data_path(*parts: str):
|
|
48
|
+
"""Get path to packaged data file."""
|
|
49
|
+
return files("tradingview_mcp.data").joinpath(*parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _reference_path(*parts: str) -> Path:
|
|
53
|
+
"""Get path to reference data file."""
|
|
54
|
+
return DEFAULT_REFERENCE_ROOT.joinpath(*parts)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@lru_cache(maxsize=128)
|
|
58
|
+
def load_json(*parts: str) -> Any:
|
|
59
|
+
"""Load JSON file from package data or reference fallback."""
|
|
60
|
+
try:
|
|
61
|
+
path = _data_path(*parts)
|
|
62
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
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)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_markets_data() -> dict[str, list[str]]:
|
|
71
|
+
"""Get markets metadata."""
|
|
72
|
+
return load_json("markets.json")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_column_display_names() -> dict[str, str]:
|
|
76
|
+
"""Get field name to display name mapping."""
|
|
77
|
+
data = load_json("column_display_names.json")
|
|
78
|
+
return data.get("fields", data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_screener_presets() -> list[dict[str, Any]]:
|
|
82
|
+
"""Get predefined screener presets."""
|
|
83
|
+
return load_json("screeners", "main_screeners.json")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_screener_markets() -> list[str]:
|
|
87
|
+
"""Get list of screener market identifiers."""
|
|
88
|
+
return load_json("screeners", "markets.json")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_metainfo(market: str) -> dict[str, Any]:
|
|
92
|
+
"""Get field metainfo for a specific market."""
|
|
93
|
+
return load_json("metainfo", f"{market}.json")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_screeners_data(name: str) -> Any:
|
|
97
|
+
"""Get screeners data by filename."""
|
|
98
|
+
return load_json("screeners", name)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ============================================================================
|
|
102
|
+
# AI-Friendly Data Access with Output Limits
|
|
103
|
+
# ============================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_ai_quick_reference() -> dict[str, Any]:
|
|
107
|
+
"""Get AI quick reference guide with common patterns and examples."""
|
|
108
|
+
return load_json("extracted", "ai_quick_reference.json")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_screener_code_examples() -> dict[str, str]:
|
|
112
|
+
"""Get screener code examples by name."""
|
|
113
|
+
return load_json("extracted", "screener_code_examples.json")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_common_fields() -> dict[str, dict[str, Any]]:
|
|
117
|
+
"""Get common field definitions (from stocks, most comprehensive)."""
|
|
118
|
+
return load_json("extracted", "common_fields.json")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_fields_by_market(market: str | None = None) -> dict[str, Any]:
|
|
122
|
+
"""Get field definitions by market type."""
|
|
123
|
+
data = load_json("extracted", "fields_by_market.json")
|
|
124
|
+
if market:
|
|
125
|
+
return data.get(market, {})
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def paginate_data(
|
|
130
|
+
data: list | dict,
|
|
131
|
+
offset: int = 0,
|
|
132
|
+
limit: int = MAX_ITEMS_DEFAULT,
|
|
133
|
+
confirm_large: bool = False,
|
|
134
|
+
) -> dict[str, Any]:
|
|
135
|
+
"""Paginate large data with size warnings.
|
|
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
|
+
"""
|
|
146
|
+
if isinstance(data, list):
|
|
147
|
+
total = len(data)
|
|
148
|
+
items = data[offset:offset + limit]
|
|
149
|
+
else:
|
|
150
|
+
keys = list(data.keys())
|
|
151
|
+
total = len(keys)
|
|
152
|
+
selected_keys = keys[offset:offset + limit]
|
|
153
|
+
items = {k: data[k] for k in selected_keys}
|
|
154
|
+
|
|
155
|
+
result: dict[str, Any] = {
|
|
156
|
+
"data": items,
|
|
157
|
+
"total": total,
|
|
158
|
+
"offset": offset,
|
|
159
|
+
"limit": limit,
|
|
160
|
+
"has_more": (offset + limit) < total,
|
|
161
|
+
}
|
|
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
|
+
return result
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def search_fields(
|
|
172
|
+
query: str,
|
|
173
|
+
market: str | None = None,
|
|
174
|
+
limit: int = 20,
|
|
175
|
+
) -> 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
|
+
"""
|
|
186
|
+
query_lower = query.lower()
|
|
187
|
+
|
|
188
|
+
if market:
|
|
189
|
+
fields_data = get_fields_by_market(market)
|
|
190
|
+
if isinstance(fields_data, list):
|
|
191
|
+
fields = fields_data
|
|
192
|
+
else:
|
|
193
|
+
return []
|
|
194
|
+
else:
|
|
195
|
+
fields = list(get_common_fields().items())
|
|
196
|
+
fields = [
|
|
197
|
+
{"name": k, **v} if isinstance(v, dict) else {"name": k, "display_name": str(v)}
|
|
198
|
+
for k, v in fields[:500] # Limit iteration
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
matches = []
|
|
202
|
+
for field in fields:
|
|
203
|
+
if isinstance(field, dict):
|
|
204
|
+
name = field.get("name", "")
|
|
205
|
+
display = field.get("display_name", "")
|
|
206
|
+
if query_lower in name.lower() or query_lower in display.lower():
|
|
207
|
+
matches.append(field)
|
|
208
|
+
if len(matches) >= limit:
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
return matches
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_field_summary(field_name: str) -> dict[str, Any] | None:
|
|
215
|
+
"""Get summary info for a specific field.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
field_name: Field name to look up
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Field info dict or None if not found
|
|
222
|
+
"""
|
|
223
|
+
# Check common fields first
|
|
224
|
+
common = get_common_fields()
|
|
225
|
+
if field_name in common:
|
|
226
|
+
info = common[field_name]
|
|
227
|
+
return {
|
|
228
|
+
"name": field_name,
|
|
229
|
+
"display_name": info.get("display_name"),
|
|
230
|
+
"type": info.get("type"),
|
|
231
|
+
"variants": info.get("variants"),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Check display names mapping
|
|
235
|
+
display_names = get_column_display_names()
|
|
236
|
+
if field_name in display_names:
|
|
237
|
+
return {
|
|
238
|
+
"name": field_name,
|
|
239
|
+
"display_name": display_names[field_name],
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def estimate_tokens(data: Any) -> int:
|
|
246
|
+
"""Estimate token count for data (rough: 4 chars = 1 token)."""
|
|
247
|
+
json_str = json.dumps(data, ensure_ascii=False, default=str)
|
|
248
|
+
return len(json_str) // 4
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def truncate_for_tokens(
|
|
252
|
+
data: Any,
|
|
253
|
+
max_tokens: int = 10000,
|
|
254
|
+
) -> 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
|
+
"""
|
|
264
|
+
estimated = estimate_tokens(data)
|
|
265
|
+
if estimated <= max_tokens:
|
|
266
|
+
return data, False
|
|
267
|
+
|
|
268
|
+
# Try to truncate intelligently
|
|
269
|
+
if isinstance(data, list):
|
|
270
|
+
# Binary search for right size
|
|
271
|
+
low, high = 0, len(data)
|
|
272
|
+
while low < high:
|
|
273
|
+
mid = (low + high + 1) // 2
|
|
274
|
+
if estimate_tokens(data[:mid]) <= max_tokens:
|
|
275
|
+
low = mid
|
|
276
|
+
else:
|
|
277
|
+
high = mid - 1
|
|
278
|
+
return data[:low], True
|
|
279
|
+
|
|
280
|
+
elif isinstance(data, dict):
|
|
281
|
+
keys = list(data.keys())
|
|
282
|
+
low, high = 0, len(keys)
|
|
283
|
+
while low < high:
|
|
284
|
+
mid = (low + high + 1) // 2
|
|
285
|
+
subset = {k: data[k] for k in keys[:mid]}
|
|
286
|
+
if estimate_tokens(subset) <= max_tokens:
|
|
287
|
+
low = mid
|
|
288
|
+
else:
|
|
289
|
+
high = mid - 1
|
|
290
|
+
return {k: data[k] for k in keys[:low]}, True
|
|
291
|
+
|
|
292
|
+
# Fallback: stringify and truncate
|
|
293
|
+
json_str = json.dumps(data, ensure_ascii=False, default=str)
|
|
294
|
+
max_chars = max_tokens * 4
|
|
295
|
+
if len(json_str) > max_chars:
|
|
296
|
+
return json_str[:max_chars] + "... (truncated)", True
|
|
297
|
+
return data, False
|
tradingview_mcp/scanner.py
CHANGED
|
@@ -10,7 +10,8 @@ from tradingview_mcp.column import Column
|
|
|
10
10
|
from tradingview_mcp.query import Query
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# Always include 'description' for full name (e.g., "Apple Inc." instead of just "AAPL")
|
|
14
|
+
DEFAULT_COLUMNS = ["name", "description", "close", "change", "volume", "market_cap_basic"]
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class Scanner:
|