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,263 @@
|
|
|
1
|
+
"""India institutional flows: FII / DII cash-market buying and selling, and
|
|
2
|
+
how each participant group is positioned in index derivatives.
|
|
3
|
+
|
|
4
|
+
Cash flows: NSE's daily provisional FII/FPI and DII figures (buy, sell, net,
|
|
5
|
+
in ₹ crore; NSE publishes only the latest session, so every session fetched
|
|
6
|
+
is saved under ~/.marketmyna/india/fii_dii.json and the history builds up
|
|
7
|
+
from there - a background thread saves each evening's figures even with no
|
|
8
|
+
visitors; a failed fetch serves the saved copy, marked).
|
|
9
|
+
|
|
10
|
+
Positioning: NSE's participant-wise open interest file (one CSV per session
|
|
11
|
+
in nsearchives, so the last 30 sessions are fetched directly and each file
|
|
12
|
+
is kept on disk for good). Shows FII / DII / client / pro long and short
|
|
13
|
+
index futures and index options in contracts, and the FII index-futures
|
|
14
|
+
long share, the number Indian traders watch most.
|
|
15
|
+
|
|
16
|
+
Figures as published; a reading of positioning, not a forecast.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import csv
|
|
22
|
+
import io
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
28
|
+
from datetime import date, datetime, timedelta
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Callable, Optional
|
|
31
|
+
|
|
32
|
+
import requests
|
|
33
|
+
|
|
34
|
+
from .india_rates import _dir # same ~/.marketmyna/india folder
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
NSE = "https://www.nseindia.com"
|
|
39
|
+
ARCHIVE = "https://nsearchives.nseindia.com/content/nsccl/fao_participant_oi_{d:%d%m%Y}.csv"
|
|
40
|
+
HEADERS = {
|
|
41
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36",
|
|
42
|
+
"Accept": "application/json, text/plain, */*",
|
|
43
|
+
"Referer": "https://www.nseindia.com/reports/fii-dii",
|
|
44
|
+
}
|
|
45
|
+
SESSIONS = 30
|
|
46
|
+
CACHE_SECONDS = 30 * 60
|
|
47
|
+
GROUPS = ("FII", "DII", "Client", "Pro")
|
|
48
|
+
|
|
49
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
50
|
+
_lock = threading.Lock()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _num(x) -> Optional[float]:
|
|
54
|
+
try:
|
|
55
|
+
return float(str(x).replace(",", "").strip())
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------ cash flows
|
|
61
|
+
|
|
62
|
+
def _fetch_cash() -> list[dict]:
|
|
63
|
+
s = requests.Session()
|
|
64
|
+
s.headers.update(HEADERS)
|
|
65
|
+
s.get(f"{NSE}/reports/fii-dii", timeout=15) # cookies the API wants
|
|
66
|
+
r = s.get(f"{NSE}/api/fiidiiTradeReact", timeout=15)
|
|
67
|
+
r.raise_for_status()
|
|
68
|
+
return r.json()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_cash(rows: list[dict]) -> Optional[dict]:
|
|
72
|
+
"""NSE rows -> {"date": "YYYY-MM-DD", "fii": {...}, "dii": {...}} in ₹ crore."""
|
|
73
|
+
out: dict = {}
|
|
74
|
+
for r in rows or []:
|
|
75
|
+
cat = (r.get("category") or "").upper()
|
|
76
|
+
key = "fii" if cat.startswith("FII") else "dii" if cat.startswith("DII") else None
|
|
77
|
+
if not key:
|
|
78
|
+
continue
|
|
79
|
+
out[key] = {"buy": _num(r.get("buyValue")), "sell": _num(r.get("sellValue")), "net": _num(r.get("netValue"))}
|
|
80
|
+
try:
|
|
81
|
+
out["date"] = datetime.strptime(r.get("date", ""), "%d-%b-%Y").date().isoformat()
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
return out if "date" in out and ("fii" in out or "dii" in out) else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _history_file() -> Path:
|
|
88
|
+
return _dir() / "fii_dii.json"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_history() -> dict[str, dict]:
|
|
92
|
+
try:
|
|
93
|
+
return json.loads(_history_file().read_text(encoding="utf-8"))
|
|
94
|
+
except (OSError, ValueError):
|
|
95
|
+
return {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def save_session(day: dict) -> dict[str, dict]:
|
|
99
|
+
hist = load_history()
|
|
100
|
+
hist[day["date"]] = {"fii": day.get("fii"), "dii": day.get("dii")}
|
|
101
|
+
path = _history_file()
|
|
102
|
+
tmp = path.with_suffix(".tmp")
|
|
103
|
+
tmp.write_text(json.dumps(dict(sorted(hist.items())), indent=0), encoding="utf-8")
|
|
104
|
+
tmp.replace(path)
|
|
105
|
+
return hist
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cash(fetch: Callable[[], list[dict]] = _fetch_cash) -> dict:
|
|
109
|
+
stale = None
|
|
110
|
+
try:
|
|
111
|
+
day = parse_cash(fetch())
|
|
112
|
+
if not day:
|
|
113
|
+
raise ValueError("NSE returned no FII/DII rows")
|
|
114
|
+
hist = save_session(day)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
logger.warning("FII/DII fetch failed: %s", exc)
|
|
117
|
+
hist = load_history()
|
|
118
|
+
stale = f"Live fetch failed ({type(exc).__name__}); showing the saved figures"
|
|
119
|
+
dates = sorted(hist)
|
|
120
|
+
series = [{"date": d, "fii": (hist[d].get("fii") or {}).get("net"), "dii": (hist[d].get("dii") or {}).get("net")} for d in dates]
|
|
121
|
+
|
|
122
|
+
def total(key: str, rows: list[dict]) -> Optional[float]:
|
|
123
|
+
vals = [r[key] for r in rows if r[key] is not None]
|
|
124
|
+
return round(sum(vals), 2) if vals else None
|
|
125
|
+
|
|
126
|
+
latest = dates[-1] if dates else None
|
|
127
|
+
month = latest[:7] if latest else None
|
|
128
|
+
mtd = [r for r in series if month and r["date"].startswith(month)]
|
|
129
|
+
last5 = series[-5:]
|
|
130
|
+
return {
|
|
131
|
+
"latest": {"date": latest, **hist[latest]} if latest else None,
|
|
132
|
+
"series": series[-120:],
|
|
133
|
+
"sessions_saved": len(dates),
|
|
134
|
+
"five_day": {"fii": total("fii", last5), "dii": total("dii", last5), "sessions": len(last5)},
|
|
135
|
+
"month_to_date": {"fii": total("fii", mtd), "dii": total("dii", mtd), "sessions": len(mtd)},
|
|
136
|
+
"stale": stale,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------ positioning
|
|
141
|
+
|
|
142
|
+
def _archive_file(d: date) -> Path:
|
|
143
|
+
path = _dir() / "participant_oi"
|
|
144
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
return path / f"{d:%Y%m%d}.csv"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _fetch_archive(d: date) -> Optional[str]:
|
|
149
|
+
"""One session's CSV, from disk if fetched before; None on holidays / not yet out."""
|
|
150
|
+
path = _archive_file(d)
|
|
151
|
+
if path.exists():
|
|
152
|
+
return path.read_text(encoding="utf-8") or None # empty file = no session that day (holiday)
|
|
153
|
+
r = requests.get(ARCHIVE.format(d=d), headers={"User-Agent": HEADERS["User-Agent"]}, timeout=20)
|
|
154
|
+
if r.status_code == 404 or "Participant" not in r.text[:200]:
|
|
155
|
+
if r.status_code == 404 and d < date.today() - timedelta(days=3):
|
|
156
|
+
path.write_text("", encoding="utf-8")
|
|
157
|
+
return None
|
|
158
|
+
r.raise_for_status()
|
|
159
|
+
path.write_text(r.text, encoding="utf-8")
|
|
160
|
+
return r.text
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_participants(text: str) -> dict[str, dict]:
|
|
164
|
+
rows = list(csv.reader(io.StringIO(text)))
|
|
165
|
+
head_i = next(i for i, r in enumerate(rows) if r and r[0].strip().lower() == "client type")
|
|
166
|
+
head = [h.strip() for h in rows[head_i]]
|
|
167
|
+
out = {}
|
|
168
|
+
for r in rows[head_i + 1:]:
|
|
169
|
+
if not r or not r[0].strip():
|
|
170
|
+
continue
|
|
171
|
+
vals = {h: _num(v) for h, v in zip(head[1:], r[1:])}
|
|
172
|
+
out[r[0].strip()] = vals
|
|
173
|
+
return out
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _summarise(groups: dict[str, dict]) -> dict:
|
|
177
|
+
out = {}
|
|
178
|
+
for g in GROUPS:
|
|
179
|
+
v = groups.get(g)
|
|
180
|
+
if not v:
|
|
181
|
+
continue
|
|
182
|
+
fl, fs = v.get("Future Index Long") or 0, v.get("Future Index Short") or 0
|
|
183
|
+
cl, cs = v.get("Option Index Call Long") or 0, v.get("Option Index Call Short") or 0
|
|
184
|
+
pl, ps = v.get("Option Index Put Long") or 0, v.get("Option Index Put Short") or 0
|
|
185
|
+
out[g] = {
|
|
186
|
+
"index_futures_long": fl, "index_futures_short": fs, "index_futures_net": fl - fs,
|
|
187
|
+
"index_futures_long_pct": round(fl / (fl + fs) * 100, 1) if fl + fs else None,
|
|
188
|
+
# bullish option exposure: calls bought + puts sold, minus puts bought + calls sold
|
|
189
|
+
"index_options_net": (cl - cs) - (pl - ps),
|
|
190
|
+
"stock_futures_net": (v.get("Future Stock Long") or 0) - (v.get("Future Stock Short") or 0),
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def positioning(fetch: Callable[[date], Optional[str]] = _fetch_archive, today: Optional[date] = None,
|
|
196
|
+
sessions: int = SESSIONS) -> Optional[dict]:
|
|
197
|
+
today = today or date.today()
|
|
198
|
+
days = [today - timedelta(days=i) for i in range(int(sessions * 1.6) + 5)]
|
|
199
|
+
days = [d for d in days if d.weekday() < 5]
|
|
200
|
+
|
|
201
|
+
def one(d):
|
|
202
|
+
try:
|
|
203
|
+
text = fetch(d)
|
|
204
|
+
return (d, _summarise(parse_participants(text))) if text else None
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
logger.info("Participant OI %s unavailable: %s", d, exc)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
with ThreadPoolExecutor(max_workers=6) as pool:
|
|
210
|
+
got = [x for x in pool.map(one, days) if x and x[1].get("FII")]
|
|
211
|
+
got.sort(key=lambda x: x[0])
|
|
212
|
+
got = got[-sessions:]
|
|
213
|
+
if not got:
|
|
214
|
+
return None
|
|
215
|
+
latest_day, latest = got[-1]
|
|
216
|
+
prev = got[-2][1] if len(got) > 1 else None
|
|
217
|
+
fii_pct = [x[1]["FII"]["index_futures_long_pct"] for x in got]
|
|
218
|
+
lows_highs = [v for v in fii_pct if v is not None]
|
|
219
|
+
return {
|
|
220
|
+
"date": latest_day.isoformat(),
|
|
221
|
+
"groups": latest,
|
|
222
|
+
"previous": prev,
|
|
223
|
+
"fii_long_pct_series": [{"date": d.isoformat(), "pct": g["FII"]["index_futures_long_pct"],
|
|
224
|
+
"net": g["FII"]["index_futures_net"]} for d, g in got],
|
|
225
|
+
"fii_long_pct_range": [min(lows_highs), max(lows_highs)] if lows_highs else None,
|
|
226
|
+
"sessions": len(got),
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def snapshot_loop(check_seconds: int = 3600) -> None:
|
|
231
|
+
"""Background thread: save each session's FII/DII figures in the evening
|
|
232
|
+
(NSE posts them around 18:00-19:30 IST) and fetch the day's participant
|
|
233
|
+
file, so the history builds without visitors."""
|
|
234
|
+
from zoneinfo import ZoneInfo
|
|
235
|
+
|
|
236
|
+
time.sleep(240) # let start-up traffic settle
|
|
237
|
+
while True:
|
|
238
|
+
try:
|
|
239
|
+
now = datetime.now(ZoneInfo("Asia/Kolkata"))
|
|
240
|
+
if now.weekday() < 5 and now.hour >= 19 and now.date().isoformat() not in load_history():
|
|
241
|
+
cash()
|
|
242
|
+
positioning()
|
|
243
|
+
except Exception as exc: # noqa: BLE001 - never kill the thread
|
|
244
|
+
logger.info("FII/DII snapshot failed: %s", exc)
|
|
245
|
+
time.sleep(check_seconds)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def build(use_cache: bool = True) -> dict:
|
|
249
|
+
if use_cache:
|
|
250
|
+
with _lock:
|
|
251
|
+
hit = _cache.get("all")
|
|
252
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
253
|
+
return hit[1]
|
|
254
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
255
|
+
c = pool.submit(cash)
|
|
256
|
+
p = pool.submit(positioning)
|
|
257
|
+
out = {"cash": c.result(), "positioning": p.result(),
|
|
258
|
+
"source": "NSE provisional FII/DII cash-market figures (₹ crore) and participant-wise open interest "
|
|
259
|
+
"in equity derivatives (contracts)"}
|
|
260
|
+
if use_cache and (out["positioning"] or (out["cash"] and out["cash"]["latest"])):
|
|
261
|
+
with _lock:
|
|
262
|
+
_cache["all"] = (time.time(), out)
|
|
263
|
+
return out
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""India rates and bullion: RBI policy / money-market rates, T-bill and G-sec
|
|
2
|
+
yields (an India yield curve), and IBJA benchmark gold / silver prices.
|
|
3
|
+
|
|
4
|
+
Sources, both free and public, scraped from their HTML:
|
|
5
|
+
- rbi.org.in home page "Current Rates" box: policy rates, CRR / SLR, FBIL
|
|
6
|
+
reference FX, call money, T-bill cut-offs and benchmark G-sec yields.
|
|
7
|
+
- ibjarates.com: India Bullion and Jewellers Association daily AM / PM rates
|
|
8
|
+
(Rs per 10 g gold, per kg silver, before GST and making charges), the
|
|
9
|
+
benchmark most Indian jewellers and gold loans quote. MCX itself blocks
|
|
10
|
+
scripted access (HTTP 403), so it is not used.
|
|
11
|
+
|
|
12
|
+
Either site may block cloud IPs or change layout. Every good fetch is saved
|
|
13
|
+
under ~/.marketmyna/india/, and a failed fetch serves that last copy marked
|
|
14
|
+
stale, so the card degrades instead of disappearing.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import html as htmllib
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from datetime import date, datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Callable, Optional
|
|
28
|
+
|
|
29
|
+
from marketmyna.core.home import app_home
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
RBI_URL = "https://www.rbi.org.in/"
|
|
34
|
+
IBJA_URL = "https://ibjarates.com/"
|
|
35
|
+
CACHE_SECONDS = 3600
|
|
36
|
+
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36"
|
|
37
|
+
|
|
38
|
+
Fetch = Callable[[str], str]
|
|
39
|
+
|
|
40
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
41
|
+
_lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _http(url: str) -> str:
|
|
45
|
+
import requests
|
|
46
|
+
|
|
47
|
+
r = requests.get(url, headers={"User-Agent": UA, "Accept-Language": "en-IN,en;q=0.9"}, timeout=20)
|
|
48
|
+
r.raise_for_status()
|
|
49
|
+
return r.text
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _dir() -> Path:
|
|
53
|
+
path = app_home() / "india"
|
|
54
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
return path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _pct(text: str) -> Optional[float]:
|
|
59
|
+
m = re.search(r"-?\d+(?:\.\d+)?", text.replace(",", ""))
|
|
60
|
+
return float(m.group(0)) if m else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _text(fragment: str) -> str:
|
|
64
|
+
return re.sub(r"\s+", " ", htmllib.unescape(re.sub(r"<[^>]+>", " ", fragment))).strip()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------ RBI
|
|
68
|
+
|
|
69
|
+
POLICY = {
|
|
70
|
+
"Policy Repo Rate": ("repo", "Repo rate", "What banks pay to borrow from RBI; the main policy rate"),
|
|
71
|
+
"Standing Deposit Facility Rate": ("sdf", "SDF rate", "Standing deposit facility: floor of the overnight corridor"),
|
|
72
|
+
"Marginal Standing Facility Rate": ("msf", "MSF rate", "Marginal standing facility: ceiling of the overnight corridor"),
|
|
73
|
+
"Bank Rate": ("bank_rate", "Bank rate", "Long-term lending rate to banks"),
|
|
74
|
+
"CRR": ("crr", "CRR", "Cash reserve ratio: deposits banks must park with RBI, unpaid"),
|
|
75
|
+
"SLR": ("slr", "SLR", "Statutory liquidity ratio: deposits banks must hold in government bonds"),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_rbi(page: str) -> dict:
|
|
80
|
+
"""Pull the rate table rows from the RBI home page."""
|
|
81
|
+
rows = [(_text(k), _text(v).lstrip(": ").strip())
|
|
82
|
+
for k, v in re.findall(r"<th[^>]*>(.*?)</th>\s*<td[^>]*>(.*?)</td>", page, re.S)]
|
|
83
|
+
out: dict = {"policy": [], "tbills": [], "gsecs": [], "fx": [], "lending": [], "call_rate": None}
|
|
84
|
+
for key, val in rows:
|
|
85
|
+
if key in POLICY:
|
|
86
|
+
k, label, note = POLICY[key]
|
|
87
|
+
out["policy"].append({"key": k, "label": label, "value": _pct(val), "note": note})
|
|
88
|
+
elif m := re.match(r"(\d+) day T-bills", key):
|
|
89
|
+
out["tbills"].append({"days": int(m.group(1)), "years": int(m.group(1)) / 365, "yield": _pct(val)})
|
|
90
|
+
elif m := re.match(r"(\d+(?:\.\d+)?)% GS (\d{4})", key):
|
|
91
|
+
out["gsecs"].append({"name": key, "coupon": float(m.group(1)), "maturity_year": int(m.group(2)),
|
|
92
|
+
"yield": _pct(val)})
|
|
93
|
+
elif m := re.match(r"INR / (\d+) (\w+)", key):
|
|
94
|
+
out["fx"].append({"pair": f"{m.group(2)}/INR", "per": int(m.group(1)), "value": _pct(val)})
|
|
95
|
+
elif key in ("Base Rate", "MCLR (Overnight)", "Savings Deposit Rate", "Term Deposit Rate > 1 Year"):
|
|
96
|
+
out["lending"].append({"label": key, "value": val})
|
|
97
|
+
if m := re.search(r"Call Rates\s*</[^>]+>.*?:\s*([\d.]+)%\s*-\s*([\d.]+)%", page, re.S):
|
|
98
|
+
out["call_rate"] = {"low": float(m.group(1)), "high": float(m.group(2))}
|
|
99
|
+
if m := re.search(r"As at[^)]*?of\s+([A-Za-z]+ \d{1,2}, \d{4})", _text(page)):
|
|
100
|
+
out["fx_as_of"] = m.group(1)
|
|
101
|
+
if m := re.search(r"Government Securities Market.*?as on\s*([A-Za-z]+ \d{1,2}, \d{4})", _text(page)):
|
|
102
|
+
out["gsec_as_of"] = m.group(1)
|
|
103
|
+
if not out["policy"]:
|
|
104
|
+
raise ValueError("RBI page layout not recognised (no policy rates found)")
|
|
105
|
+
out["curve"] = _curve(out["tbills"], out["gsecs"])
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _curve(tbills: list[dict], gsecs: list[dict]) -> list[dict]:
|
|
110
|
+
"""T-bills + benchmark G-secs as one curve, x = years to maturity (G-secs approximate,
|
|
111
|
+
mid-year maturity assumed since the page gives only the year)."""
|
|
112
|
+
today = date.today()
|
|
113
|
+
pts = [{"label": f"{t['days']}D", "years": round(t["years"], 3), "yield": t["yield"]} for t in tbills]
|
|
114
|
+
for g in gsecs:
|
|
115
|
+
yrs = max(g["maturity_year"] + 0.5 - (today.year + today.timetuple().tm_yday / 365), 0.1)
|
|
116
|
+
pts.append({"label": str(g["maturity_year"]), "years": round(yrs, 2), "yield": g["yield"]})
|
|
117
|
+
return sorted((p for p in pts if p["yield"] is not None), key=lambda p: p["years"])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------ IBJA
|
|
121
|
+
|
|
122
|
+
IBJA_FIELDS = {
|
|
123
|
+
"Gold999": ("gold_999", "Gold 999 (24K)", "₹/10g"),
|
|
124
|
+
"Gold995": ("gold_995", "Gold 995", "₹/10g"),
|
|
125
|
+
"Gold916": ("gold_916", "Gold 916 (22K)", "₹/10g"),
|
|
126
|
+
"Gold750": ("gold_750", "Gold 750 (18K)", "₹/10g"),
|
|
127
|
+
"Silver999": ("silver_999", "Silver 999", "₹/kg"),
|
|
128
|
+
"Platinum999": ("platinum_999", "Platinum 999", "₹/10g"),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def parse_ibja(page: str) -> dict:
|
|
133
|
+
def label(id_):
|
|
134
|
+
m = re.search(rf'id="{id_}"[^>]*>([^<]*)<', page)
|
|
135
|
+
return _pct(m.group(1)) if m and m.group(1).strip() else None
|
|
136
|
+
|
|
137
|
+
items = []
|
|
138
|
+
for field, (key, name, unit) in IBJA_FIELDS.items():
|
|
139
|
+
am, pm = label(f"lbl{field}_AM"), label(f"lbl{field}_PM")
|
|
140
|
+
if am is None and pm is None:
|
|
141
|
+
continue
|
|
142
|
+
items.append({"key": key, "label": name, "unit": unit, "am": am, "pm": pm,
|
|
143
|
+
"latest": pm if pm is not None else am, "session": "PM" if pm is not None else "AM"})
|
|
144
|
+
# The headline table holds today's rates; dated tables below it are past days.
|
|
145
|
+
as_of = _ist_today()
|
|
146
|
+
|
|
147
|
+
def series(hidden: str, value_key: str) -> list[dict]:
|
|
148
|
+
m = re.search(rf'id="{hidden}"[^>]*value="([^"]*)"', page) or re.search(rf'value="([^"]*)"[^>]*id="{hidden}"', page)
|
|
149
|
+
if not m:
|
|
150
|
+
return []
|
|
151
|
+
try:
|
|
152
|
+
data = json.loads(htmllib.unescape(m.group(1)))
|
|
153
|
+
return [{"date": datetime.strptime(d, "%d/%m/%Y").date().isoformat(), "value": float(v)}
|
|
154
|
+
for d, v in zip(data.get("labels", []), data.get(value_key, [])) if v]
|
|
155
|
+
except (ValueError, TypeError):
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
hist = {"gold_999": series("HdnGold", "purity999"), "silver_999": series("HdnSilver", "silverRate")}
|
|
159
|
+
if not items: # before the morning fix is published: fall back to the last PM close
|
|
160
|
+
for key, (name, unit) in {"gold_999": ("Gold 999 (24K)", "₹/10g"), "silver_999": ("Silver 999", "₹/kg")}.items():
|
|
161
|
+
if hist[key]:
|
|
162
|
+
last = hist[key][-1]
|
|
163
|
+
items.append({"key": key, "label": name, "unit": unit, "am": None, "pm": last["value"],
|
|
164
|
+
"latest": last["value"], "session": "PM"})
|
|
165
|
+
as_of = last["date"]
|
|
166
|
+
if not items:
|
|
167
|
+
raise ValueError("IBJA page layout not recognised (no rates found)")
|
|
168
|
+
for i in items: # change vs the previous day's PM close
|
|
169
|
+
prev = [h for h in hist.get(i["key"], []) if h["date"] < as_of]
|
|
170
|
+
i["change_pct"] = (i["latest"] / prev[-1]["value"] - 1) * 100 if prev and i["latest"] else None
|
|
171
|
+
return {"items": items, "as_of": as_of, "history": hist}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _ist_today() -> str:
|
|
175
|
+
from datetime import timedelta, timezone
|
|
176
|
+
|
|
177
|
+
return datetime.now(timezone(timedelta(hours=5, minutes=30))).date().isoformat()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------ fetch with fallback
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _load(name: str, fetch: Fetch, url: str, parser) -> dict:
|
|
184
|
+
path = _dir() / f"{name}.json"
|
|
185
|
+
try:
|
|
186
|
+
data = parser(fetch(url))
|
|
187
|
+
data["fetched_at"] = datetime.now().isoformat(timespec="seconds")
|
|
188
|
+
data["stale"] = False
|
|
189
|
+
try:
|
|
190
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
191
|
+
_append_history(name, data)
|
|
192
|
+
except OSError as exc:
|
|
193
|
+
logger.info("Could not save %s snapshot: %s", name, exc)
|
|
194
|
+
return data
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
logger.warning("%s fetch failed: %s", name.upper(), exc)
|
|
197
|
+
if path.exists():
|
|
198
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
199
|
+
data["stale"] = True
|
|
200
|
+
data["error"] = f"Live fetch failed; showing the copy from {data.get('fetched_at', 'earlier')}."
|
|
201
|
+
return data
|
|
202
|
+
return {"error": f"{name.upper()} unavailable right now", "stale": True}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _append_history(name: str, data: dict) -> None:
|
|
206
|
+
"""One snapshot per day of the headline numbers, so trends build up over time."""
|
|
207
|
+
path = _dir() / f"{name}_history.json"
|
|
208
|
+
hist = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
|
209
|
+
day = date.today().isoformat()
|
|
210
|
+
if name == "rbi":
|
|
211
|
+
snap = {p["key"]: p["value"] for p in data.get("policy", [])}
|
|
212
|
+
snap.update({f"tbill_{t['days']}": t["yield"] for t in data.get("tbills", [])})
|
|
213
|
+
snap.update({f"gs_{g['maturity_year']}": g["yield"] for g in data.get("gsecs", [])})
|
|
214
|
+
else:
|
|
215
|
+
snap = {i["key"]: i["latest"] for i in data.get("items", [])}
|
|
216
|
+
hist[day] = snap
|
|
217
|
+
path.write_text(json.dumps(dict(sorted(hist.items())[-1500:])), encoding="utf-8")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def history(name: str) -> dict:
|
|
221
|
+
path = _dir() / f"{name}_history.json"
|
|
222
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def build(fetch: Fetch = _http, use_cache: bool = True) -> dict:
|
|
226
|
+
if use_cache:
|
|
227
|
+
with _lock:
|
|
228
|
+
hit = _cache.get("all")
|
|
229
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
230
|
+
return hit[1]
|
|
231
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
232
|
+
|
|
233
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
234
|
+
rbi = pool.submit(_load, "rbi", fetch, RBI_URL, parse_rbi)
|
|
235
|
+
ibja = pool.submit(_load, "ibja", fetch, IBJA_URL, parse_ibja)
|
|
236
|
+
out = {"rbi": rbi.result(), "ibja": ibja.result()}
|
|
237
|
+
out["sources"] = {"rbi": RBI_URL, "ibja": IBJA_URL}
|
|
238
|
+
if use_cache and not (out["rbi"].get("stale") and out["ibja"].get("stale")):
|
|
239
|
+
with _lock:
|
|
240
|
+
_cache["all"] = (time.time(), out)
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def context_lines(data: Optional[dict] = None) -> list[str]:
|
|
245
|
+
"""Short lines for LLM prompts."""
|
|
246
|
+
data = data or build()
|
|
247
|
+
lines = []
|
|
248
|
+
rbi = data.get("rbi", {})
|
|
249
|
+
for p in rbi.get("policy", []):
|
|
250
|
+
if p["key"] in ("repo", "crr") and p["value"] is not None:
|
|
251
|
+
lines.append(f"RBI {p['label']}: {p['value']:.2f}%")
|
|
252
|
+
for t in rbi.get("tbills", []):
|
|
253
|
+
if t["days"] == 364 and t["yield"] is not None:
|
|
254
|
+
lines.append(f"India 364-day T-bill: {t['yield']:.2f}%")
|
|
255
|
+
for i in data.get("ibja", {}).get("items", []):
|
|
256
|
+
if i["key"] in ("gold_999", "silver_999") and i["latest"]:
|
|
257
|
+
lines.append(f"IBJA {i['label']}: Rs {i['latest']:,.0f} {i['unit'].replace('₹', '')}")
|
|
258
|
+
return lines
|