import urllib.request import json import time _CACHE = {"data": [], "last_fetch": 0} CACHE_TTL = 120 # 2 minutes BONDS_DEF = [ # US Curve {"symbol": "TVC:US02Y", "code": "US02Y", "title": "آمریکا ۲ ساله", "desc": "اوراق ۲ ساله خزانه‌داری آمریکا", "country": "United States", "flag": "🇺🇸", "tenor": "2Y"}, {"symbol": "TVC:US10Y", "code": "US10Y", "title": "آمریکا ۱۰ ساله", "desc": "اوراق ۱۰ ساله خزانه‌داری آمریکا (بنچ‌مارک)", "country": "United States", "flag": "🇺🇸", "tenor": "10Y"}, {"symbol": "TVC:US30Y", "code": "US30Y", "title": "آمریکا ۳۰ ساله", "desc": "اوراق ۳۰ ساله خزانه‌داری آمریکا", "country": "United States", "flag": "🇺🇸", "tenor": "30Y"}, # Global 10Y Benchmarks {"symbol": "TVC:DE10Y", "code": "DE10Y", "title": "آلمان ۱۰ ساله (Bund)", "desc": "اوراق ۱۰ ساله دولتی آلمان", "country": "Germany", "flag": "🇩🇪", "tenor": "10Y"}, {"symbol": "TVC:GB10Y", "code": "GB10Y", "title": "بریتانیا ۱۰ ساله (Gilt)", "desc": "اوراق ۱۰ ساله دولتی انگلیس", "country": "United Kingdom", "flag": "🇬🇧", "tenor": "10Y"}, {"symbol": "TVC:JP10Y", "code": "JP10Y", "title": "ژاپن ۱۰ ساله (JGB)", "desc": "اوراق ۱۰ ساله دولتی ژاپن", "country": "Japan", "flag": "🇯🇵", "tenor": "10Y"}, {"symbol": "TVC:CN10Y", "code": "CN10Y", "title": "چین ۱۰ ساله", "desc": "اوراق ۱۰ ساله دولتی چین", "country": "China", "flag": "🇨🇳", "tenor": "10Y"}, {"symbol": "TVC:FR10Y", "code": "FR10Y", "title": "فرانسه ۱۰ ساله (OAT)", "desc": "اوراق ۱۰ ساله دولتی فرانسه", "country": "France", "flag": "🇫🇷", "tenor": "10Y"}, {"symbol": "TVC:IT10Y", "code": "IT10Y", "title": "ایتالیا ۱۰ ساله (BTP)", "desc": "اوراق ۱۰ ساله دولتی ایتالیا", "country": "Italy", "flag": "🇮🇹", "tenor": "10Y"}, {"symbol": "TVC:CA10Y", "code": "CA10Y", "title": "کانادا ۱۰ ساله", "desc": "اوراق ۱۰ ساله دولتی کانادا", "country": "Canada", "flag": "🇨🇦", "tenor": "10Y"}, {"symbol": "TVC:AU10Y", "code": "AU10Y", "title": "استرالیا ۱۰ ساله", "desc": "اوراق ۱۰ ساله دولتی استرالیا", "country": "Australia", "flag": "🇦🇺", "tenor": "10Y"}, {"symbol": "TVC:IN10Y", "code": "IN10Y", "title": "هند ۱۰ ساله", "desc": "اوراق ۱۰ ساله دولتی هند", "country": "India", "flag": "🇮🇳", "tenor": "10Y"}, {"symbol": "TVC:BR10Y", "code": "BR10Y", "title": "برزیل ۱۰ ساله", "desc": "اوراق ۱۰ ساله دولتی برزیل", "country": "Brazil", "flag": "🇧🇷", "tenor": "10Y"}, {"symbol": "TVC:TR10Y", "code": "TR10Y", "title": "ترکیه ۱۰ ساله", "desc": "اوراق ۱۰ ساله دولتی ترکیه", "country": "Turkey", "flag": "🇹🇷", "tenor": "10Y"}, {"symbol": "TVC:EU10Y", "code": "EU10Y", "title": "منطقه یورو ۱۰ ساله", "desc": "اوراق ۱۰ ساله بنچ‌مارک اتحادیه اروپا", "country": "Euro Area", "flag": "🇪🇺", "tenor": "10Y"}, ] def get_bonds(): now = time.time() if _CACHE["data"] and (now - _CACHE["last_fetch"] < CACHE_TTL): return _CACHE["data"] url = "https://scanner.tradingview.com/bonds/scan" tickers = [b["symbol"] for b in BONDS_DEF] payload = { "symbols": {"tickers": tickers}, "columns": [ "name", "description", "close", "change", "change_abs", "open", "high", "low", "Perf.W", "Perf.1M", "Perf.Y", "Perf.YTD" ] } try: req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers={ "User-Agent": "Mozilla/5.0", "Content-Type": "application/json" }) with urllib.request.urlopen(req, timeout=8) as resp: data = json.loads(resp.read().decode('utf-8')) scan_map = {r.get('s'): r.get('d', []) for r in data.get('data', [])} out = [] for b in BONDS_DEF: d = scan_map.get(b["symbol"], []) close = round(d[2], 4) if len(d) > 2 and d[2] is not None else None change = round(d[3], 2) if len(d) > 3 and d[3] is not None else 0.0 chg_abs = round(d[4], 4) if len(d) > 4 and d[4] is not None else 0.0 open_val = round(d[5], 4) if len(d) > 5 and d[5] is not None else close high_val = round(d[6], 4) if len(d) > 6 and d[6] is not None else close low_val = round(d[7], 4) if len(d) > 7 and d[7] is not None else close perf_w = round(d[8], 2) if len(d) > 8 and d[8] is not None else None perf_m = round(d[9], 2) if len(d) > 9 and d[9] is not None else None perf_y = round(d[10], 2) if len(d) > 10 and d[10] is not None else None if close is not None and open_val is not None: step = (close - open_val) / 11.0 sparkline = [round(open_val + step * i, 4) for i in range(11)] + [close] else: sparkline = [close] if close is not None else [] out.append({ "symbol": b["symbol"], "code": b["code"], "title": b["title"], "desc": b["desc"], "country": b["country"], "flag": b["flag"], "tenor": b["tenor"], "yield": close, "change": change, "change_abs": chg_abs, "open": open_val, "high": high_val, "low": low_val, "perf_w": perf_w, "perf_m": perf_m, "perf_y": perf_y, "sparkline": sparkline }) _CACHE["data"] = out _CACHE["last_fetch"] = now return out except Exception as e: print(f"[bonds] TradingView scanner error: {e}") return _CACHE["data"] if _CACHE["data"] else [] if __name__ == "__main__": bonds = get_bonds() print(f"Loaded {len(bonds)} sovereign bonds from TradingView:") for b in bonds[:5]: print(f" {b['flag']} {b['code']:6} | {b['title']:20} | Yield: {b['yield']}% | Chg: {b['change']}%")