marketmyna 0.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.
- marketmyna/__init__.py +14 -0
- marketmyna/agent/__init__.py +8 -0
- marketmyna/agent/agents.py +543 -0
- marketmyna/agent/context.py +69 -0
- marketmyna/agent/dashboard.py +55 -0
- marketmyna/agent/orchestrator.py +153 -0
- marketmyna/agent/sentiment.py +150 -0
- marketmyna/agent/skills.py +119 -0
- marketmyna/ai/__init__.py +1 -0
- marketmyna/ai/portfolio_optimizer.py +330 -0
- marketmyna/analysis/__init__.py +6 -0
- marketmyna/analysis/fundamental.py +329 -0
- marketmyna/analysis/sip_optimizer.py +81 -0
- marketmyna/analysis/technical.py +321 -0
- marketmyna/api/__init__.py +1 -0
- marketmyna/api/app.py +209 -0
- marketmyna/api/deps.py +63 -0
- marketmyna/api/middlewares/__init__.py +0 -0
- marketmyna/api/middlewares/auth.py +116 -0
- marketmyna/api/middlewares/csrf.py +66 -0
- marketmyna/api/middlewares/error_handler.py +38 -0
- marketmyna/api/middlewares/slashes.py +27 -0
- marketmyna/api/v1/__init__.py +0 -0
- marketmyna/api/v1/endpoints/__init__.py +0 -0
- marketmyna/api/v1/endpoints/account.py +166 -0
- marketmyna/api/v1/endpoints/agent.py +114 -0
- marketmyna/api/v1/endpoints/bot.py +38 -0
- marketmyna/api/v1/endpoints/calendar.py +269 -0
- marketmyna/api/v1/endpoints/data_sources.py +174 -0
- marketmyna/api/v1/endpoints/health.py +10 -0
- marketmyna/api/v1/endpoints/listings.py +564 -0
- marketmyna/api/v1/endpoints/macro.py +163 -0
- marketmyna/api/v1/endpoints/portfolio.py +530 -0
- marketmyna/api/v1/endpoints/push.py +119 -0
- marketmyna/api/v1/endpoints/reports.py +49 -0
- marketmyna/api/v1/endpoints/scheduler.py +219 -0
- marketmyna/api/v1/endpoints/stocks.py +400 -0
- marketmyna/api/v1/endpoints/system_config.py +157 -0
- marketmyna/api/v1/endpoints/thesis.py +112 -0
- marketmyna/api/v1/endpoints/trade.py +709 -0
- marketmyna/api/v1/endpoints/updates.py +394 -0
- marketmyna/api/v1/endpoints/watchlists.py +179 -0
- marketmyna/api/v1/router.py +43 -0
- marketmyna/bot/__init__.py +14 -0
- marketmyna/bot/commands/__init__.py +0 -0
- marketmyna/bot/commands/analyze.py +40 -0
- marketmyna/bot/commands/ask.py +51 -0
- marketmyna/bot/commands/base.py +35 -0
- marketmyna/bot/commands/batch.py +53 -0
- marketmyna/bot/commands/common.py +84 -0
- marketmyna/bot/commands/help.py +43 -0
- marketmyna/bot/commands/markets.py +153 -0
- marketmyna/bot/commands/portfolio.py +145 -0
- marketmyna/bot/commands/research.py +323 -0
- marketmyna/bot/commands/status.py +48 -0
- marketmyna/bot/commands/trade.py +176 -0
- marketmyna/bot/dispatcher.py +95 -0
- marketmyna/bot/formatting.py +25 -0
- marketmyna/bot/models.py +27 -0
- marketmyna/bot/registry.py +50 -0
- marketmyna/bot/telegram_listener.py +227 -0
- marketmyna/cli/__init__.py +1 -0
- marketmyna/cli/scheduled_run.py +292 -0
- marketmyna/core/__init__.py +6 -0
- marketmyna/core/compliance.py +53 -0
- marketmyna/core/config.py +377 -0
- marketmyna/core/constants.py +69 -0
- marketmyna/core/home.py +22 -0
- marketmyna/core/numbers.py +28 -0
- marketmyna/core/secret_store.py +104 -0
- marketmyna/core/tenant.py +130 -0
- marketmyna/data/__init__.py +8 -0
- marketmyna/data/base.py +36 -0
- marketmyna/data/categories.py +272 -0
- marketmyna/data/china.py +360 -0
- marketmyna/data/crypto.py +200 -0
- marketmyna/data/ecb_fx.py +131 -0
- marketmyna/data/extra_sources.py +569 -0
- marketmyna/data/india.py +241 -0
- marketmyna/data/india_filings.py +343 -0
- marketmyna/data/listings.py +1314 -0
- marketmyna/data/mutual_funds.py +34 -0
- marketmyna/data/news.py +98 -0
- marketmyna/data/news_search.py +346 -0
- marketmyna/data/nse_deals.py +138 -0
- marketmyna/data/resilience.py +173 -0
- marketmyna/data/sec_facts.py +252 -0
- marketmyna/data/shariah.py +283 -0
- marketmyna/data/social/__init__.py +9 -0
- marketmyna/data/social/base.py +100 -0
- marketmyna/data/social/browser.py +92 -0
- marketmyna/data/social/hackernews.py +53 -0
- marketmyna/data/social/reddit.py +397 -0
- marketmyna/data/social/stocktwits.py +63 -0
- marketmyna/data/social/valuepickr.py +53 -0
- marketmyna/data/stocks.py +197 -0
- marketmyna/data/yahoo_price.py +48 -0
- marketmyna/launcher.py +71 -0
- marketmyna/llm/__init__.py +33 -0
- marketmyna/llm/backend_factory.py +51 -0
- marketmyna/llm/backend_registry.py +19 -0
- marketmyna/llm/channels.py +447 -0
- marketmyna/llm/json_reply.py +63 -0
- marketmyna/llm/litellm_backend.py +197 -0
- marketmyna/llm/model_catalog.py +166 -0
- marketmyna/llm/model_health.py +193 -0
- marketmyna/llm/protocol.py +111 -0
- marketmyna/llm/providers.py +128 -0
- marketmyna/llm/store.py +128 -0
- marketmyna/notifiers/__init__.py +35 -0
- marketmyna/notifiers/apprise_sender.py +43 -0
- marketmyna/notifiers/astrbot.py +47 -0
- marketmyna/notifiers/bark.py +24 -0
- marketmyna/notifiers/base.py +80 -0
- marketmyna/notifiers/dingtalk.py +54 -0
- marketmyna/notifiers/discord.py +39 -0
- marketmyna/notifiers/dispatcher.py +153 -0
- marketmyna/notifiers/email.py +99 -0
- marketmyna/notifiers/feishu.py +57 -0
- marketmyna/notifiers/googlechat.py +24 -0
- marketmyna/notifiers/gotify.py +36 -0
- marketmyna/notifiers/matrix.py +28 -0
- marketmyna/notifiers/ntfy.py +36 -0
- marketmyna/notifiers/pushbullet.py +24 -0
- marketmyna/notifiers/pushover.py +41 -0
- marketmyna/notifiers/pushplus.py +41 -0
- marketmyna/notifiers/serverchan3.py +40 -0
- marketmyna/notifiers/settings_schema.py +119 -0
- marketmyna/notifiers/signal_messenger.py +25 -0
- marketmyna/notifiers/slack.py +36 -0
- marketmyna/notifiers/teams.py +37 -0
- marketmyna/notifiers/telegram.py +81 -0
- marketmyna/notifiers/twilio.py +33 -0
- marketmyna/notifiers/webhook.py +44 -0
- marketmyna/notifiers/webpush.py +222 -0
- marketmyna/notifiers/wechat.py +40 -0
- marketmyna/notifiers/whatsapp.py +27 -0
- marketmyna/portfolio/__init__.py +6 -0
- marketmyna/portfolio/analytics.py +336 -0
- marketmyna/portfolio/manager.py +383 -0
- marketmyna/portfolio/models.py +155 -0
- marketmyna/portfolio/optimizers.py +88 -0
- marketmyna/portfolio/review.py +411 -0
- marketmyna/services/__init__.py +1 -0
- marketmyna/services/accounts.py +278 -0
- marketmyna/services/ai_access.py +94 -0
- marketmyna/services/analysis_run.py +491 -0
- marketmyna/services/big_trades.py +335 -0
- marketmyna/services/brokers/__init__.py +0 -0
- marketmyna/services/brokers/angel.py +271 -0
- marketmyna/services/brokers/finnhub.py +209 -0
- marketmyna/services/byok_detect.py +196 -0
- marketmyna/services/calendar.py +748 -0
- marketmyna/services/central_banks.py +202 -0
- marketmyna/services/commodities.py +134 -0
- marketmyna/services/compare.py +307 -0
- marketmyna/services/cot.py +148 -0
- marketmyna/services/country_macro.py +135 -0
- marketmyna/services/crude_sensitivity.py +239 -0
- marketmyna/services/crypto_mood.py +135 -0
- marketmyna/services/crypto_screens.py +226 -0
- marketmyna/services/data_export.py +91 -0
- marketmyna/services/dcf.py +220 -0
- marketmyna/services/dcf_model.py +194 -0
- marketmyna/services/delivery.py +142 -0
- marketmyna/services/discovery.py +146 -0
- marketmyna/services/dividends.py +95 -0
- marketmyna/services/earnings.py +305 -0
- marketmyna/services/econ_calendar.py +495 -0
- marketmyna/services/eia_stocks.py +115 -0
- marketmyna/services/estimates.py +109 -0
- marketmyna/services/etf.py +236 -0
- marketmyna/services/etf_rotation.py +193 -0
- marketmyna/services/etf_screens.py +196 -0
- marketmyna/services/financials.py +639 -0
- marketmyna/services/fund_ai.py +181 -0
- marketmyna/services/fund_analytics.py +371 -0
- marketmyna/services/fund_cost.py +147 -0
- marketmyna/services/fund_holdings.py +664 -0
- marketmyna/services/fund_peers.py +247 -0
- marketmyna/services/fund_rotation.py +206 -0
- marketmyna/services/fund_style.py +179 -0
- marketmyna/services/funds.py +70 -0
- marketmyna/services/hot_stocks.py +358 -0
- marketmyna/services/india_flows.py +263 -0
- marketmyna/services/india_rates.py +258 -0
- marketmyna/services/investor_lenses.py +271 -0
- marketmyna/services/leadership.py +165 -0
- marketmyna/services/live_market.py +497 -0
- marketmyna/services/live_quotes.py +295 -0
- marketmyna/services/macro.py +182 -0
- marketmyna/services/market_calendar.py +399 -0
- marketmyna/services/market_mood.py +168 -0
- marketmyna/services/market_valuation.py +231 -0
- marketmyna/services/mf_holdings.py +199 -0
- marketmyna/services/mf_insights.py +132 -0
- marketmyna/services/mf_lumpsum.py +587 -0
- marketmyna/services/mf_portfolio_ai.py +195 -0
- marketmyna/services/nse_events.py +98 -0
- marketmyna/services/overview.py +366 -0
- marketmyna/services/ownership.py +143 -0
- marketmyna/services/quality_scores.py +199 -0
- marketmyna/services/rates.py +335 -0
- marketmyna/services/reconcile.py +343 -0
- marketmyna/services/report.py +584 -0
- marketmyna/services/report_i18n.py +678 -0
- marketmyna/services/rotation.py +224 -0
- marketmyna/services/runtime_scheduler.py +548 -0
- marketmyna/services/schedule_config.py +251 -0
- marketmyna/services/schedule_time.py +39 -0
- marketmyna/services/screens.py +398 -0
- marketmyna/services/sec_mdna.py +173 -0
- marketmyna/services/shariah.py +246 -0
- marketmyna/services/sip.py +148 -0
- marketmyna/services/sip_days.py +362 -0
- marketmyna/services/smart_file.py +595 -0
- marketmyna/services/smart_pdf.py +425 -0
- marketmyna/services/smart_report.py +370 -0
- marketmyna/services/social_sentiment.py +511 -0
- marketmyna/services/thesis.py +328 -0
- marketmyna/services/translate.py +146 -0
- marketmyna/services/valuation.py +173 -0
- marketmyna/services/worker_pool.py +149 -0
- marketmyna/services/world_markets.py +208 -0
- marketmyna/storage.py +722 -0
- marketmyna/strategies/README.md +51 -0
- marketmyna/strategies/bottom_volume.yaml +49 -0
- marketmyna/strategies/box_oscillation.yaml +69 -0
- marketmyna/strategies/bull_trend.yaml +49 -0
- marketmyna/strategies/chan_theory.yaml +58 -0
- marketmyna/strategies/dragon_head.yaml +44 -0
- marketmyna/strategies/emotion_cycle.yaml +79 -0
- marketmyna/strategies/event_driven.yaml +54 -0
- marketmyna/strategies/expectation_repricing.yaml +54 -0
- marketmyna/strategies/growth_quality.yaml +54 -0
- marketmyna/strategies/hot_theme.yaml +54 -0
- marketmyna/strategies/ma_golden_cross.yaml +46 -0
- marketmyna/strategies/one_yang_three_yin.yaml +37 -0
- marketmyna/strategies/shrink_pullback.yaml +46 -0
- marketmyna/strategies/volume_breakout.yaml +47 -0
- marketmyna/strategies/wave_theory.yaml +64 -0
- marketmyna/trading/__init__.py +2 -0
- marketmyna/trading/alerts.py +356 -0
- marketmyna/trading/backtest.py +317 -0
- marketmyna/trading/charges.py +82 -0
- marketmyna/trading/chart.py +197 -0
- marketmyna/trading/indicators.py +303 -0
- marketmyna/trading/journal.py +74 -0
- marketmyna/trading/knn.py +164 -0
- marketmyna/trading/lots.py +109 -0
- marketmyna/trading/market_hours.py +66 -0
- marketmyna/trading/options.py +554 -0
- marketmyna/trading/pairs.py +94 -0
- marketmyna/trading/risk.py +170 -0
- marketmyna/trading/scanner.py +500 -0
- marketmyna/trading/volatility.py +163 -0
- marketmyna/utils/__init__.py +1 -0
- marketmyna/utils/deadline.py +36 -0
- marketmyna/utils/log_redact.py +52 -0
- marketmyna/utils/yahoo_gate.py +183 -0
- marketmyna/utils/yf_lock.py +12 -0
- marketmyna/web/apple-touch-icon.png +0 -0
- marketmyna/web/assets/Account-CA9NFDKO.js +1 -0
- marketmyna/web/assets/Analyze-uM0srB_U.js +7 -0
- marketmyna/web/assets/AreaChart-2i67V6Jf.js +1 -0
- marketmyna/web/assets/BarChart-_yngarVl.js +3 -0
- marketmyna/web/assets/Bot-E4gpf1qZ.js +1 -0
- marketmyna/web/assets/CandleChart-CioqTqMV.js +1 -0
- marketmyna/web/assets/Compare-D6_2CPl4.js +1 -0
- marketmyna/web/assets/ExportMenu-DaZ1xVmW.js +1 -0
- marketmyna/web/assets/LineChart-BaOkD1Ta.js +57 -0
- marketmyna/web/assets/Markets-BMw_M1sJ.js +1 -0
- marketmyna/web/assets/Portfolio-DQEdHVc2.js +1 -0
- marketmyna/web/assets/ReportView-BzhdcqSh.js +1 -0
- marketmyna/web/assets/Scheduler-I9PDKxTd.js +1 -0
- marketmyna/web/assets/Settings-DpXfHolW.js +3 -0
- marketmyna/web/assets/SipDays-C9q3bNSx.js +1 -0
- marketmyna/web/assets/SymbolSearch-CBOVPEk1.js +1 -0
- marketmyna/web/assets/Trade-_CM7HC4H.js +2 -0
- marketmyna/web/assets/TradeChart-ZjsycacC.js +1 -0
- marketmyna/web/assets/WatchButton-ComYW56_.js +1 -0
- marketmyna/web/assets/Watchlist-DAjLb0jd.js +1 -0
- marketmyna/web/assets/app-BP1o7WZS.js +1 -0
- marketmyna/web/assets/app-BkIUKdld.js +1 -0
- marketmyna/web/assets/app-CKeSYhEt.js +1 -0
- marketmyna/web/assets/app-CN1OERYX.js +1 -0
- marketmyna/web/assets/app-DIQllha5.js +1 -0
- marketmyna/web/assets/app-DeiAW0tX.js +1 -0
- marketmyna/web/assets/br-Cu5YU29T.svg +45 -0
- marketmyna/web/assets/browser-CR-ks2Fp.js +19 -0
- marketmyna/web/assets/browser-D5K_K_gj.js +1 -0
- marketmyna/web/assets/common-0wvpAVpd.js +1 -0
- marketmyna/web/assets/common-3ghy53Us.js +1 -0
- marketmyna/web/assets/common-Bib2unjX.js +1 -0
- marketmyna/web/assets/common-BvSJP0w_.js +1 -0
- marketmyna/web/assets/common-CSfyC0Sp.js +1 -0
- marketmyna/web/assets/common-C_t-iZPf.js +1 -0
- marketmyna/web/assets/compare-DMcsx70H.js +1 -0
- marketmyna/web/assets/es-d5m8M5h8.svg +544 -0
- marketmyna/web/assets/exporter-DiMg2O5d.js +3 -0
- marketmyna/web/assets/files-CpgiZRbN.js +1 -0
- marketmyna/web/assets/format-d8InM0Zw.js +9 -0
- marketmyna/web/assets/html2canvas-Bw0OShmD.js +5 -0
- marketmyna/web/assets/index-CKAnq-Or.js +12 -0
- marketmyna/web/assets/index-D4U8E6LH.css +2 -0
- marketmyna/web/assets/index.es-iBEw7jOv.js +5 -0
- marketmyna/web/assets/ir-cCIgaNf6.svg +219 -0
- marketmyna/web/assets/jspdf.es.min-C4285IdL.js +79 -0
- marketmyna/web/assets/jspdf.plugin.autotable-2eOJeXl-.js +2 -0
- marketmyna/web/assets/lightweight-charts.production-BF2noug5.js +1 -0
- marketmyna/web/assets/listings-BefgXpb1.js +1 -0
- marketmyna/web/assets/liveQuotes-CtSmCUFA.js +1 -0
- marketmyna/web/assets/markets-BI4AhI9W.js +1 -0
- marketmyna/web/assets/markets-BUHUdYpT.js +1 -0
- marketmyna/web/assets/markets-BqvN1Bvf.js +1 -0
- marketmyna/web/assets/markets-DPO40TTe.js +1 -0
- marketmyna/web/assets/markets-bsiPo-Tk.js +1 -0
- marketmyna/web/assets/markets-ff1e792u.js +1 -0
- marketmyna/web/assets/montecarlo-BsNlH2qe.js +1 -0
- marketmyna/web/assets/options-BYAZnwzW.js +1 -0
- marketmyna/web/assets/preload-helper-BLdHbfdN.js +1 -0
- marketmyna/web/assets/purify.es-Dqk5qvad.js +3 -0
- marketmyna/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- marketmyna/web/assets/sa-Dh79zbT9.svg +25 -0
- marketmyna/web/assets/toast-DDradTZw.js +1 -0
- marketmyna/web/assets/trade-fndw1Ql3.js +1 -0
- marketmyna/web/assets/ui-gsFb8B2m.js +1 -0
- marketmyna/web/assets/watchlists-B6YqwUIk.js +1 -0
- marketmyna/web/brand/logo-mark.webp +0 -0
- marketmyna/web/favicon.png +0 -0
- marketmyna/web/icon-192.png +0 -0
- marketmyna/web/icon-512.png +0 -0
- marketmyna/web/index.html +34 -0
- marketmyna/web/manifest.webmanifest +28 -0
- marketmyna/web/robots.txt +2 -0
- marketmyna/web/sw.js +40 -0
- marketmyna/web/theme-init.js +37 -0
- marketmyna-0.2.0.dist-info/METADATA +345 -0
- marketmyna-0.2.0.dist-info/RECORD +344 -0
- marketmyna-0.2.0.dist-info/WHEEL +5 -0
- marketmyna-0.2.0.dist-info/entry_points.txt +2 -0
- marketmyna-0.2.0.dist-info/licenses/LICENSE +202 -0
- marketmyna-0.2.0.dist-info/licenses/NOTICE +22 -0
- marketmyna-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Live prices for the rows on screen (Markets, Watchlist), shared by all users.
|
|
2
|
+
|
|
3
|
+
The Markets listing is a whole-market snapshot refreshed every few hours.
|
|
4
|
+
This module keeps a small cache of fresh quotes for just the symbols people
|
|
5
|
+
are looking at:
|
|
6
|
+
|
|
7
|
+
- Browsers ask every second (cheap: the answer comes from this cache).
|
|
8
|
+
- The cache refreshes each market in the background, one batched upstream
|
|
9
|
+
call for every symbol anyone is watching, at most every REFRESH[market]
|
|
10
|
+
seconds. 1,000 users on the same page cost the data source the same as one.
|
|
11
|
+
- Free sources: Yahoo for stocks / ETFs (can lag up to ~15 min for NSE),
|
|
12
|
+
Binance for crypto (real time). A licensed broker feed (Kite Connect,
|
|
13
|
+
Upstox, SmartAPI websockets) can replace `_fetch_*` for true tick data.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Seconds between upstream refreshes per market (env LIVE_REFRESH_<MARKET>).
|
|
27
|
+
REFRESH = {
|
|
28
|
+
"india": float(os.environ.get("LIVE_REFRESH_INDIA", 15)),
|
|
29
|
+
"us": float(os.environ.get("LIVE_REFRESH_US", 15)),
|
|
30
|
+
"china_a": float(os.environ.get("LIVE_REFRESH_CHINA_A", 30)),
|
|
31
|
+
"hong_kong": float(os.environ.get("LIVE_REFRESH_HONG_KONG", 30)),
|
|
32
|
+
"crypto": float(os.environ.get("LIVE_REFRESH_CRYPTO", 2)),
|
|
33
|
+
}
|
|
34
|
+
# With a broker feed (Angel One) Indian prices refresh every second.
|
|
35
|
+
BROKER_REFRESH = float(os.environ.get("LIVE_REFRESH_BROKER", 1))
|
|
36
|
+
WATCH_SECONDS = 60 # a symbol nobody asked about for a minute stops being fetched
|
|
37
|
+
MAX_SYMBOLS = 100 # per request (one page of the table)
|
|
38
|
+
SOURCE = {"crypto": "Crypto exchange, real time"}
|
|
39
|
+
DEFAULT_SOURCE = "Market data feed, can lag up to 15 min"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _Market:
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.quotes: dict[str, dict] = {} # symbol -> {price, change, change_percent, volume, time}
|
|
45
|
+
self.watched: dict[str, float] = {} # symbol -> last asked (monotonic)
|
|
46
|
+
self.fetched_at = 0.0
|
|
47
|
+
self.error: Optional[str] = None
|
|
48
|
+
self.busy = False
|
|
49
|
+
self.lock = threading.Lock()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_markets: dict[str, _Market] = {}
|
|
53
|
+
_lock = threading.Lock()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _market(name: str) -> _Market:
|
|
57
|
+
with _lock:
|
|
58
|
+
return _markets.setdefault(name, _Market())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get(market: str, symbols: list[str], depth: bool = False) -> dict:
|
|
62
|
+
"""Latest cached quotes for `symbols`; starts a background refresh when
|
|
63
|
+
the market's cache is older than its refresh interval. Never blocks on
|
|
64
|
+
the upstream source. The order book (broker feed only) is left out
|
|
65
|
+
unless `depth`, so the tables' per-second poll stays small."""
|
|
66
|
+
from ..trading.market_hours import is_open
|
|
67
|
+
|
|
68
|
+
m = _market(market)
|
|
69
|
+
now = time.monotonic()
|
|
70
|
+
symbols = [s.strip().upper() for s in symbols if s.strip()][:MAX_SYMBOLS]
|
|
71
|
+
open_now = is_open(market)
|
|
72
|
+
with m.lock:
|
|
73
|
+
for s in symbols:
|
|
74
|
+
m.watched[s] = now
|
|
75
|
+
stale = now - m.fetched_at >= refresh_seconds(market)
|
|
76
|
+
missing = any(s not in m.quotes for s in symbols)
|
|
77
|
+
start = (stale and (open_now or missing)) and not m.busy and market in REFRESH
|
|
78
|
+
if start:
|
|
79
|
+
m.busy = True
|
|
80
|
+
out = {s: m.quotes[s] if depth else {k: v for k, v in m.quotes[s].items() if k != "depth"}
|
|
81
|
+
for s in symbols if s in m.quotes}
|
|
82
|
+
age = None if not m.fetched_at else round(now - m.fetched_at, 1)
|
|
83
|
+
error = m.error
|
|
84
|
+
if start:
|
|
85
|
+
threading.Thread(target=_refresh, args=(market,), name=f"live-quotes-{market}", daemon=True).start()
|
|
86
|
+
return {
|
|
87
|
+
"market": market,
|
|
88
|
+
"open": open_now,
|
|
89
|
+
"quotes": out,
|
|
90
|
+
"age_seconds": age,
|
|
91
|
+
"refresh_seconds": refresh_seconds(market),
|
|
92
|
+
"source": source(market),
|
|
93
|
+
"error": error,
|
|
94
|
+
# Newest trade time among these quotes (epoch seconds). During "market
|
|
95
|
+
# hours" on a holiday this stays at the last session's close.
|
|
96
|
+
"last_trade": max((q.get("time") or 0 for q in out.values()), default=None) or None,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _refresh(market: str) -> None:
|
|
101
|
+
m = _market(market)
|
|
102
|
+
now = time.monotonic()
|
|
103
|
+
with m.lock:
|
|
104
|
+
m.watched = {s: t for s, t in m.watched.items() if now - t < WATCH_SECONDS}
|
|
105
|
+
symbols = list(m.watched)
|
|
106
|
+
try:
|
|
107
|
+
fresh = (_fetch_crypto(symbols) if market == "crypto" else _fetch_india(symbols) if market == "india"
|
|
108
|
+
else _fetch_us(symbols) if market == "us" else _fetch_yahoo(symbols, market))
|
|
109
|
+
error = None
|
|
110
|
+
except Exception as exc: # keep serving the last quotes
|
|
111
|
+
logger.warning("Live quotes for %s failed: %s", market, exc)
|
|
112
|
+
fresh, error = {}, "Live prices are temporarily unavailable; showing the last ones."
|
|
113
|
+
with m.lock:
|
|
114
|
+
m.quotes.update(fresh)
|
|
115
|
+
m.fetched_at = time.monotonic()
|
|
116
|
+
m.error = error
|
|
117
|
+
m.busy = False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _broker_config():
|
|
121
|
+
"""App config when Angel One is set up (admin panel or env), else None."""
|
|
122
|
+
try:
|
|
123
|
+
from marketmyna.api.deps import get_config_dep
|
|
124
|
+
|
|
125
|
+
from .brokers import angel
|
|
126
|
+
|
|
127
|
+
config = get_config_dep()
|
|
128
|
+
return config if angel.configured(config) else None
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
_broker_failed_at = 0.0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _broker_live() -> bool:
|
|
137
|
+
# After a broker failure, a minute at the normal pace (Yahoo can't take 1 s polling).
|
|
138
|
+
return _broker_config() is not None and time.monotonic() - _broker_failed_at > 60
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _finnhub_config():
|
|
142
|
+
try:
|
|
143
|
+
from marketmyna.api.deps import get_config_dep
|
|
144
|
+
|
|
145
|
+
from .brokers import finnhub
|
|
146
|
+
|
|
147
|
+
config = get_config_dep()
|
|
148
|
+
return config if finnhub.configured(config) else None
|
|
149
|
+
except Exception:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_yahoo_us_at = 0.0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _fetch_us(symbols: list[str]) -> dict[str, dict]:
|
|
157
|
+
"""Finnhub's trade stream when a key is set (real time); Yahoo otherwise.
|
|
158
|
+
Symbols Finnhub has no price for yet are filled from Yahoo, at Yahoo's
|
|
159
|
+
normal 15 s pace."""
|
|
160
|
+
global _yahoo_us_at
|
|
161
|
+
config = _finnhub_config()
|
|
162
|
+
if config is None:
|
|
163
|
+
return _fetch_yahoo(symbols, "us")
|
|
164
|
+
from .brokers import finnhub
|
|
165
|
+
|
|
166
|
+
s = finnhub.stream(config)
|
|
167
|
+
s.want(symbols)
|
|
168
|
+
out = s.quotes(symbols)
|
|
169
|
+
rest = [x for x in symbols if x not in out]
|
|
170
|
+
if rest and time.monotonic() - _yahoo_us_at >= REFRESH["us"]:
|
|
171
|
+
_yahoo_us_at = time.monotonic()
|
|
172
|
+
try:
|
|
173
|
+
out.update(_fetch_yahoo(rest, "us"))
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def refresh_seconds(market: str) -> float:
|
|
180
|
+
if market == "us" and _finnhub_config() is not None:
|
|
181
|
+
return 1.0
|
|
182
|
+
if market == "india" and _broker_live():
|
|
183
|
+
# Every second for one batch of 50; slower when more symbols are watched,
|
|
184
|
+
# so the shared Angel account stays under its hourly call limit.
|
|
185
|
+
from .brokers.angel import BATCH, CALLS_PER_SECOND
|
|
186
|
+
|
|
187
|
+
batches = max(1, -(-len(_market("india").watched) // BATCH))
|
|
188
|
+
return max(BROKER_REFRESH, batches / CALLS_PER_SECOND)
|
|
189
|
+
return REFRESH.get(market, 30)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def source(market: str) -> str:
|
|
193
|
+
if market == "us" and _finnhub_config() is not None:
|
|
194
|
+
return "Real-time feed"
|
|
195
|
+
if market == "india" and _broker_live():
|
|
196
|
+
return "Broker feed, live"
|
|
197
|
+
return SOURCE.get(market, DEFAULT_SOURCE)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _fetch_india(symbols: list[str]) -> dict[str, dict]:
|
|
201
|
+
"""Angel One when configured (live); Yahoo otherwise or if it fails.
|
|
202
|
+
Symbols Angel can't price (e.g. BSE-only) are filled from Yahoo."""
|
|
203
|
+
global _broker_failed_at
|
|
204
|
+
config = _broker_config()
|
|
205
|
+
# During the minute after a failure (e.g. Angel's rate limit) stay on Yahoo.
|
|
206
|
+
if config is None or time.monotonic() - _broker_failed_at <= 60:
|
|
207
|
+
return _fetch_yahoo(symbols, "india")
|
|
208
|
+
from .brokers import angel
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
out = angel.client(config).quotes(symbols)
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
logger.warning("Angel One quotes failed, using Yahoo: %s", exc)
|
|
214
|
+
_broker_failed_at = time.monotonic()
|
|
215
|
+
return _fetch_yahoo(symbols, "india")
|
|
216
|
+
rest = [s for s in symbols if s not in out]
|
|
217
|
+
if rest:
|
|
218
|
+
try:
|
|
219
|
+
out.update(_fetch_yahoo(rest, "india"))
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _fetch_crypto(symbols: list[str]) -> dict[str, dict]:
|
|
226
|
+
"""Binance 24h tickers for BASE-USD symbols (USDT pairs), one request."""
|
|
227
|
+
import json
|
|
228
|
+
|
|
229
|
+
import requests
|
|
230
|
+
|
|
231
|
+
pairs = {f"{s.split('-')[0]}USDT": s for s in symbols if s}
|
|
232
|
+
if not pairs:
|
|
233
|
+
return {}
|
|
234
|
+
r = requests.get("https://api.binance.com/api/v3/ticker/24hr",
|
|
235
|
+
params={"symbols": json.dumps(list(pairs), separators=(",", ":"))}, timeout=10)
|
|
236
|
+
r.raise_for_status()
|
|
237
|
+
stamp = time.time()
|
|
238
|
+
out = {}
|
|
239
|
+
for t in r.json():
|
|
240
|
+
sym = pairs.get(t.get("symbol"))
|
|
241
|
+
price = _num(t.get("lastPrice"))
|
|
242
|
+
if sym and price:
|
|
243
|
+
out[sym] = {"price": price, "change": _num(t.get("priceChange")),
|
|
244
|
+
"change_percent": _num(t.get("priceChangePercent")),
|
|
245
|
+
"volume": _num(t.get("quoteVolume")), "time": stamp}
|
|
246
|
+
return out
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _fetch_yahoo(symbols: list[str], market: str) -> dict[str, dict]:
|
|
250
|
+
"""One batched 1-minute download for every watched symbol: last trade,
|
|
251
|
+
change against the previous session's close, and today's volume."""
|
|
252
|
+
import pandas as pd
|
|
253
|
+
import yfinance as yf
|
|
254
|
+
|
|
255
|
+
from ..utils.yf_lock import YF_DOWNLOAD_LOCK
|
|
256
|
+
from .analysis_run import yahoo_symbol
|
|
257
|
+
|
|
258
|
+
if not symbols:
|
|
259
|
+
return {}
|
|
260
|
+
tickers = {yahoo_symbol(s, market): s for s in symbols}
|
|
261
|
+
with YF_DOWNLOAD_LOCK:
|
|
262
|
+
df = yf.download(list(tickers), period="5d", interval="1m", group_by="ticker", progress=False,
|
|
263
|
+
threads=True, auto_adjust=False, prepost=False)
|
|
264
|
+
out = {}
|
|
265
|
+
for yt, sym in tickers.items():
|
|
266
|
+
try:
|
|
267
|
+
# group_by="ticker" nests columns under the ticker even for one
|
|
268
|
+
# symbol in current yfinance; older releases returned them flat.
|
|
269
|
+
frame = df[yt] if len(tickers) > 1 or isinstance(df.columns, pd.MultiIndex) else df
|
|
270
|
+
frame = frame.dropna(subset=["Close"])
|
|
271
|
+
except (KeyError, TypeError):
|
|
272
|
+
continue
|
|
273
|
+
if frame.empty:
|
|
274
|
+
continue
|
|
275
|
+
last_day = frame.index[-1].date()
|
|
276
|
+
today = frame[frame.index.date == last_day]
|
|
277
|
+
before = frame[frame.index.date < last_day]
|
|
278
|
+
price = float(today["Close"].iloc[-1])
|
|
279
|
+
prev = float(before["Close"].iloc[-1]) if not before.empty else None
|
|
280
|
+
change = price - prev if prev else None
|
|
281
|
+
out[sym] = {
|
|
282
|
+
"price": round(price, 4),
|
|
283
|
+
"change": round(change, 4) if change is not None else None,
|
|
284
|
+
"change_percent": round(change / prev * 100, 2) if prev else None,
|
|
285
|
+
"volume": float(today["Volume"].sum()) if "Volume" in today else None,
|
|
286
|
+
"time": frame.index[-1].timestamp(),
|
|
287
|
+
}
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _num(value) -> Optional[float]:
|
|
292
|
+
try:
|
|
293
|
+
return float(value)
|
|
294
|
+
except (TypeError, ValueError):
|
|
295
|
+
return None
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Macro dashboard: key US / India / global series from FRED's public CSV
|
|
2
|
+
export (no API key), with latest value, 1-month and 1-year change and a
|
|
3
|
+
monthly sparkline. Covers the core of OpenBB's economy / fixedincome
|
|
4
|
+
endpoints that matter to an equity investor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import logging
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from datetime import date, timedelta
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
FRED_CSV = "https://fred.stlouisfed.org/graph/fredgraph.csv?id={id}&cosd={start}"
|
|
22
|
+
CACHE_SECONDS = 6 * 3600
|
|
23
|
+
YEARS = 5
|
|
24
|
+
|
|
25
|
+
# id, label, group, unit, transform (level | yoy), note
|
|
26
|
+
SERIES = [
|
|
27
|
+
("CPIAUCSL", "US inflation (CPI)", "us", "%", "yoy", "Consumer prices vs a year ago"),
|
|
28
|
+
("FEDFUNDS", "Fed funds rate", "us", "%", "level", "US policy rate"),
|
|
29
|
+
("DGS10", "US 10-year yield", "us", "%", "level", "Long-term borrowing cost; higher weighs on stock valuations"),
|
|
30
|
+
("DGS2", "US 2-year yield", "us", "%", "level", "Tracks where the Fed is expected to go"),
|
|
31
|
+
("T10Y2Y", "US yield curve (10y - 2y)", "us", "pts", "level", "Below 0 (inverted) has preceded most US recessions"),
|
|
32
|
+
("UNRATE", "US unemployment", "us", "%", "level", "Share of the labour force out of work"),
|
|
33
|
+
("INDIRLTLT01STM", "India 10-year yield", "india", "%", "level", "Government bond yield"),
|
|
34
|
+
("IRSTCI01INM156N", "India call money rate", "india", "%", "level", "Overnight interbank rate, tracks RBI policy"),
|
|
35
|
+
("DEXINUS", "USD / INR", "india", "₹", "level", "Rupees per dollar; higher = weaker rupee"),
|
|
36
|
+
("DCOILBRENTEU", "Brent crude", "global", "$", "level", "Oil price; a big cost driver for India"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
Fetch = Callable[[str, date], pd.Series]
|
|
40
|
+
|
|
41
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
42
|
+
_lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _fred(series_id: str, start: date) -> pd.Series:
|
|
46
|
+
import requests
|
|
47
|
+
|
|
48
|
+
# FRED stalls requests with an unknown custom User-Agent; the library default works.
|
|
49
|
+
resp = requests.get(FRED_CSV.format(id=series_id, start=start.isoformat()), timeout=20)
|
|
50
|
+
resp.raise_for_status()
|
|
51
|
+
df = pd.read_csv(io.StringIO(resp.text))
|
|
52
|
+
s = pd.to_numeric(df.iloc[:, 1], errors="coerce")
|
|
53
|
+
s.index = pd.to_datetime(df.iloc[:, 0])
|
|
54
|
+
return s.dropna()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _asof(s: pd.Series, when: pd.Timestamp) -> Optional[float]:
|
|
58
|
+
past = s[s.index <= when]
|
|
59
|
+
return float(past.iloc[-1]) if len(past) else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _fetch_one(fetch: Fetch, spec: tuple, start: date) -> tuple:
|
|
63
|
+
"""(spec, series) for one FRED series; series is None when the fetch fails."""
|
|
64
|
+
try:
|
|
65
|
+
return spec, fetch(spec[0], start)
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
logger.warning("FRED %s failed: %s", spec[0], exc)
|
|
68
|
+
return spec, None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def shape(series_id: str, label: str, group: str, unit: str, transform: str, note: str, raw: pd.Series) -> Optional[dict]:
|
|
72
|
+
s = raw.sort_index()
|
|
73
|
+
if transform == "yoy":
|
|
74
|
+
monthly = s.resample("MS").last().dropna()
|
|
75
|
+
s = (monthly / monthly.shift(12) - 1).dropna() * 100
|
|
76
|
+
if s.empty:
|
|
77
|
+
return None
|
|
78
|
+
last_day = s.index[-1]
|
|
79
|
+
value = float(s.iloc[-1])
|
|
80
|
+
m1, y1 = _asof(s, last_day - pd.DateOffset(months=1)), _asof(s, last_day - pd.DateOffset(years=1))
|
|
81
|
+
spark = s.resample("MS").last().dropna().tail(36)
|
|
82
|
+
return {"id": series_id, "label": label, "group": group, "unit": unit, "note": note,
|
|
83
|
+
"value": value, "date": str(last_day.date()),
|
|
84
|
+
"change_1m": value - m1 if m1 is not None else None,
|
|
85
|
+
"change_1y": value - y1 if y1 is not None else None,
|
|
86
|
+
"spark": [{"date": str(i.date()), "value": float(v)} for i, v in spark.items()]}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build(fetch: Fetch = _fred, use_cache: bool = True) -> dict:
|
|
90
|
+
if use_cache:
|
|
91
|
+
with _lock:
|
|
92
|
+
hit = _cache.get("all")
|
|
93
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
94
|
+
return hit[1]
|
|
95
|
+
start = date.today() - timedelta(days=365 * YEARS + 400) # extra year for YoY
|
|
96
|
+
|
|
97
|
+
with ThreadPoolExecutor(max_workers=6) as pool:
|
|
98
|
+
results = list(pool.map(lambda spec: _fetch_one(fetch, spec, start), SERIES))
|
|
99
|
+
series, failed = [], []
|
|
100
|
+
for spec, raw in results:
|
|
101
|
+
item = shape(*spec, raw) if raw is not None and len(raw) else None
|
|
102
|
+
(series.append(item) if item else failed.append(spec[1]))
|
|
103
|
+
curve = next((s for s in series if s["id"] == "T10Y2Y"), None)
|
|
104
|
+
out = {"series": series, "failed": failed,
|
|
105
|
+
"signals": {"us_curve_inverted": curve["value"] < 0 if curve else None},
|
|
106
|
+
"source": "Official central bank and government statistics"}
|
|
107
|
+
if use_cache and series:
|
|
108
|
+
with _lock:
|
|
109
|
+
_cache["all"] = (time.time(), out)
|
|
110
|
+
return out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def context_lines(data: Optional[dict] = None) -> list[str]:
|
|
114
|
+
"""Short text lines for LLM prompts (agent context)."""
|
|
115
|
+
data = data or build()
|
|
116
|
+
lines = []
|
|
117
|
+
for s in data["series"]:
|
|
118
|
+
chg = f", {s['change_1y']:+.2f} over 1y" if s.get("change_1y") is not None else ""
|
|
119
|
+
lines.append(f"{s['label']}: {s['value']:.2f}{'' if s['unit'] in ('₹', '$') else s['unit']} ({s['date']}{chg})")
|
|
120
|
+
return lines
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cached_context() -> list[str]:
|
|
124
|
+
"""Macro lines for prompts without ever blocking: uses the cache, and if it
|
|
125
|
+
is cold, warms it on a background thread and returns nothing this time."""
|
|
126
|
+
with _lock:
|
|
127
|
+
hit = _cache.get("all")
|
|
128
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
129
|
+
return context_lines(hit[1]) + _india_cached()
|
|
130
|
+
threading.Thread(target=build, name="macro-warm", daemon=True).start()
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _india_cached() -> list[str]:
|
|
135
|
+
"""RBI / IBJA lines if that cache is warm; never fetches."""
|
|
136
|
+
from . import india_rates
|
|
137
|
+
|
|
138
|
+
with india_rates._lock:
|
|
139
|
+
hit = india_rates._cache.get("all")
|
|
140
|
+
if hit and time.time() - hit[0] < india_rates.CACHE_SECONDS:
|
|
141
|
+
return india_rates.context_lines(hit[1])
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# US Treasury constant-maturity yields, short to long (FRED, daily).
|
|
146
|
+
CURVE = [("DGS1MO", "1M", 1 / 12), ("DGS3MO", "3M", 0.25), ("DGS6MO", "6M", 0.5), ("DGS1", "1Y", 1),
|
|
147
|
+
("DGS2", "2Y", 2), ("DGS3", "3Y", 3), ("DGS5", "5Y", 5), ("DGS7", "7Y", 7),
|
|
148
|
+
("DGS10", "10Y", 10), ("DGS20", "20Y", 20), ("DGS30", "30Y", 30)]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def yield_curve(fetch: Fetch = _fred, use_cache: bool = True) -> dict:
|
|
152
|
+
"""US Treasury curve today vs 1 month and 1 year ago, plus its shape."""
|
|
153
|
+
if use_cache:
|
|
154
|
+
with _lock:
|
|
155
|
+
hit = _cache.get("curve")
|
|
156
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
157
|
+
return hit[1]
|
|
158
|
+
start = date.today() - timedelta(days=400)
|
|
159
|
+
|
|
160
|
+
with ThreadPoolExecutor(max_workers=6) as pool:
|
|
161
|
+
got = [(spec, s) for spec, s in pool.map(lambda spec: _fetch_one(fetch, spec, start), CURVE) if s is not None and len(s)]
|
|
162
|
+
if not got:
|
|
163
|
+
return {"points": [], "as_of": None}
|
|
164
|
+
as_of = min(s.index[-1] for _, s in got) # a date every tenor has printed
|
|
165
|
+
points = []
|
|
166
|
+
for (sid, label, years), s in got:
|
|
167
|
+
points.append({"id": sid, "tenor": label, "years": years,
|
|
168
|
+
"now": _asof(s, as_of),
|
|
169
|
+
"month_ago": _asof(s, as_of - pd.Timedelta(days=30)),
|
|
170
|
+
"year_ago": _asof(s, as_of - pd.Timedelta(days=365))})
|
|
171
|
+
by = {p["tenor"]: p["now"] for p in points}
|
|
172
|
+
shape_ = None
|
|
173
|
+
if by.get("3M") is not None and by.get("10Y") is not None:
|
|
174
|
+
spread = by["10Y"] - by["3M"]
|
|
175
|
+
shape_ = {"spread_10y_3m": spread,
|
|
176
|
+
"label": "inverted" if spread < 0 else "flat" if spread < 0.5 else "normal"}
|
|
177
|
+
out = {"points": points, "as_of": str(as_of.date()), "shape": shape_,
|
|
178
|
+
"source": "Official US Treasury constant-maturity yields"}
|
|
179
|
+
if use_cache:
|
|
180
|
+
with _lock:
|
|
181
|
+
_cache["curve"] = (time.time(), out)
|
|
182
|
+
return out
|