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.
Files changed (344) hide show
  1. marketmyna/__init__.py +14 -0
  2. marketmyna/agent/__init__.py +8 -0
  3. marketmyna/agent/agents.py +543 -0
  4. marketmyna/agent/context.py +69 -0
  5. marketmyna/agent/dashboard.py +55 -0
  6. marketmyna/agent/orchestrator.py +153 -0
  7. marketmyna/agent/sentiment.py +150 -0
  8. marketmyna/agent/skills.py +119 -0
  9. marketmyna/ai/__init__.py +1 -0
  10. marketmyna/ai/portfolio_optimizer.py +330 -0
  11. marketmyna/analysis/__init__.py +6 -0
  12. marketmyna/analysis/fundamental.py +329 -0
  13. marketmyna/analysis/sip_optimizer.py +81 -0
  14. marketmyna/analysis/technical.py +321 -0
  15. marketmyna/api/__init__.py +1 -0
  16. marketmyna/api/app.py +209 -0
  17. marketmyna/api/deps.py +63 -0
  18. marketmyna/api/middlewares/__init__.py +0 -0
  19. marketmyna/api/middlewares/auth.py +116 -0
  20. marketmyna/api/middlewares/csrf.py +66 -0
  21. marketmyna/api/middlewares/error_handler.py +38 -0
  22. marketmyna/api/middlewares/slashes.py +27 -0
  23. marketmyna/api/v1/__init__.py +0 -0
  24. marketmyna/api/v1/endpoints/__init__.py +0 -0
  25. marketmyna/api/v1/endpoints/account.py +166 -0
  26. marketmyna/api/v1/endpoints/agent.py +114 -0
  27. marketmyna/api/v1/endpoints/bot.py +38 -0
  28. marketmyna/api/v1/endpoints/calendar.py +269 -0
  29. marketmyna/api/v1/endpoints/data_sources.py +174 -0
  30. marketmyna/api/v1/endpoints/health.py +10 -0
  31. marketmyna/api/v1/endpoints/listings.py +564 -0
  32. marketmyna/api/v1/endpoints/macro.py +163 -0
  33. marketmyna/api/v1/endpoints/portfolio.py +530 -0
  34. marketmyna/api/v1/endpoints/push.py +119 -0
  35. marketmyna/api/v1/endpoints/reports.py +49 -0
  36. marketmyna/api/v1/endpoints/scheduler.py +219 -0
  37. marketmyna/api/v1/endpoints/stocks.py +400 -0
  38. marketmyna/api/v1/endpoints/system_config.py +157 -0
  39. marketmyna/api/v1/endpoints/thesis.py +112 -0
  40. marketmyna/api/v1/endpoints/trade.py +709 -0
  41. marketmyna/api/v1/endpoints/updates.py +394 -0
  42. marketmyna/api/v1/endpoints/watchlists.py +179 -0
  43. marketmyna/api/v1/router.py +43 -0
  44. marketmyna/bot/__init__.py +14 -0
  45. marketmyna/bot/commands/__init__.py +0 -0
  46. marketmyna/bot/commands/analyze.py +40 -0
  47. marketmyna/bot/commands/ask.py +51 -0
  48. marketmyna/bot/commands/base.py +35 -0
  49. marketmyna/bot/commands/batch.py +53 -0
  50. marketmyna/bot/commands/common.py +84 -0
  51. marketmyna/bot/commands/help.py +43 -0
  52. marketmyna/bot/commands/markets.py +153 -0
  53. marketmyna/bot/commands/portfolio.py +145 -0
  54. marketmyna/bot/commands/research.py +323 -0
  55. marketmyna/bot/commands/status.py +48 -0
  56. marketmyna/bot/commands/trade.py +176 -0
  57. marketmyna/bot/dispatcher.py +95 -0
  58. marketmyna/bot/formatting.py +25 -0
  59. marketmyna/bot/models.py +27 -0
  60. marketmyna/bot/registry.py +50 -0
  61. marketmyna/bot/telegram_listener.py +227 -0
  62. marketmyna/cli/__init__.py +1 -0
  63. marketmyna/cli/scheduled_run.py +292 -0
  64. marketmyna/core/__init__.py +6 -0
  65. marketmyna/core/compliance.py +53 -0
  66. marketmyna/core/config.py +377 -0
  67. marketmyna/core/constants.py +69 -0
  68. marketmyna/core/home.py +22 -0
  69. marketmyna/core/numbers.py +28 -0
  70. marketmyna/core/secret_store.py +104 -0
  71. marketmyna/core/tenant.py +130 -0
  72. marketmyna/data/__init__.py +8 -0
  73. marketmyna/data/base.py +36 -0
  74. marketmyna/data/categories.py +272 -0
  75. marketmyna/data/china.py +360 -0
  76. marketmyna/data/crypto.py +200 -0
  77. marketmyna/data/ecb_fx.py +131 -0
  78. marketmyna/data/extra_sources.py +569 -0
  79. marketmyna/data/india.py +241 -0
  80. marketmyna/data/india_filings.py +343 -0
  81. marketmyna/data/listings.py +1314 -0
  82. marketmyna/data/mutual_funds.py +34 -0
  83. marketmyna/data/news.py +98 -0
  84. marketmyna/data/news_search.py +346 -0
  85. marketmyna/data/nse_deals.py +138 -0
  86. marketmyna/data/resilience.py +173 -0
  87. marketmyna/data/sec_facts.py +252 -0
  88. marketmyna/data/shariah.py +283 -0
  89. marketmyna/data/social/__init__.py +9 -0
  90. marketmyna/data/social/base.py +100 -0
  91. marketmyna/data/social/browser.py +92 -0
  92. marketmyna/data/social/hackernews.py +53 -0
  93. marketmyna/data/social/reddit.py +397 -0
  94. marketmyna/data/social/stocktwits.py +63 -0
  95. marketmyna/data/social/valuepickr.py +53 -0
  96. marketmyna/data/stocks.py +197 -0
  97. marketmyna/data/yahoo_price.py +48 -0
  98. marketmyna/launcher.py +71 -0
  99. marketmyna/llm/__init__.py +33 -0
  100. marketmyna/llm/backend_factory.py +51 -0
  101. marketmyna/llm/backend_registry.py +19 -0
  102. marketmyna/llm/channels.py +447 -0
  103. marketmyna/llm/json_reply.py +63 -0
  104. marketmyna/llm/litellm_backend.py +197 -0
  105. marketmyna/llm/model_catalog.py +166 -0
  106. marketmyna/llm/model_health.py +193 -0
  107. marketmyna/llm/protocol.py +111 -0
  108. marketmyna/llm/providers.py +128 -0
  109. marketmyna/llm/store.py +128 -0
  110. marketmyna/notifiers/__init__.py +35 -0
  111. marketmyna/notifiers/apprise_sender.py +43 -0
  112. marketmyna/notifiers/astrbot.py +47 -0
  113. marketmyna/notifiers/bark.py +24 -0
  114. marketmyna/notifiers/base.py +80 -0
  115. marketmyna/notifiers/dingtalk.py +54 -0
  116. marketmyna/notifiers/discord.py +39 -0
  117. marketmyna/notifiers/dispatcher.py +153 -0
  118. marketmyna/notifiers/email.py +99 -0
  119. marketmyna/notifiers/feishu.py +57 -0
  120. marketmyna/notifiers/googlechat.py +24 -0
  121. marketmyna/notifiers/gotify.py +36 -0
  122. marketmyna/notifiers/matrix.py +28 -0
  123. marketmyna/notifiers/ntfy.py +36 -0
  124. marketmyna/notifiers/pushbullet.py +24 -0
  125. marketmyna/notifiers/pushover.py +41 -0
  126. marketmyna/notifiers/pushplus.py +41 -0
  127. marketmyna/notifiers/serverchan3.py +40 -0
  128. marketmyna/notifiers/settings_schema.py +119 -0
  129. marketmyna/notifiers/signal_messenger.py +25 -0
  130. marketmyna/notifiers/slack.py +36 -0
  131. marketmyna/notifiers/teams.py +37 -0
  132. marketmyna/notifiers/telegram.py +81 -0
  133. marketmyna/notifiers/twilio.py +33 -0
  134. marketmyna/notifiers/webhook.py +44 -0
  135. marketmyna/notifiers/webpush.py +222 -0
  136. marketmyna/notifiers/wechat.py +40 -0
  137. marketmyna/notifiers/whatsapp.py +27 -0
  138. marketmyna/portfolio/__init__.py +6 -0
  139. marketmyna/portfolio/analytics.py +336 -0
  140. marketmyna/portfolio/manager.py +383 -0
  141. marketmyna/portfolio/models.py +155 -0
  142. marketmyna/portfolio/optimizers.py +88 -0
  143. marketmyna/portfolio/review.py +411 -0
  144. marketmyna/services/__init__.py +1 -0
  145. marketmyna/services/accounts.py +278 -0
  146. marketmyna/services/ai_access.py +94 -0
  147. marketmyna/services/analysis_run.py +491 -0
  148. marketmyna/services/big_trades.py +335 -0
  149. marketmyna/services/brokers/__init__.py +0 -0
  150. marketmyna/services/brokers/angel.py +271 -0
  151. marketmyna/services/brokers/finnhub.py +209 -0
  152. marketmyna/services/byok_detect.py +196 -0
  153. marketmyna/services/calendar.py +748 -0
  154. marketmyna/services/central_banks.py +202 -0
  155. marketmyna/services/commodities.py +134 -0
  156. marketmyna/services/compare.py +307 -0
  157. marketmyna/services/cot.py +148 -0
  158. marketmyna/services/country_macro.py +135 -0
  159. marketmyna/services/crude_sensitivity.py +239 -0
  160. marketmyna/services/crypto_mood.py +135 -0
  161. marketmyna/services/crypto_screens.py +226 -0
  162. marketmyna/services/data_export.py +91 -0
  163. marketmyna/services/dcf.py +220 -0
  164. marketmyna/services/dcf_model.py +194 -0
  165. marketmyna/services/delivery.py +142 -0
  166. marketmyna/services/discovery.py +146 -0
  167. marketmyna/services/dividends.py +95 -0
  168. marketmyna/services/earnings.py +305 -0
  169. marketmyna/services/econ_calendar.py +495 -0
  170. marketmyna/services/eia_stocks.py +115 -0
  171. marketmyna/services/estimates.py +109 -0
  172. marketmyna/services/etf.py +236 -0
  173. marketmyna/services/etf_rotation.py +193 -0
  174. marketmyna/services/etf_screens.py +196 -0
  175. marketmyna/services/financials.py +639 -0
  176. marketmyna/services/fund_ai.py +181 -0
  177. marketmyna/services/fund_analytics.py +371 -0
  178. marketmyna/services/fund_cost.py +147 -0
  179. marketmyna/services/fund_holdings.py +664 -0
  180. marketmyna/services/fund_peers.py +247 -0
  181. marketmyna/services/fund_rotation.py +206 -0
  182. marketmyna/services/fund_style.py +179 -0
  183. marketmyna/services/funds.py +70 -0
  184. marketmyna/services/hot_stocks.py +358 -0
  185. marketmyna/services/india_flows.py +263 -0
  186. marketmyna/services/india_rates.py +258 -0
  187. marketmyna/services/investor_lenses.py +271 -0
  188. marketmyna/services/leadership.py +165 -0
  189. marketmyna/services/live_market.py +497 -0
  190. marketmyna/services/live_quotes.py +295 -0
  191. marketmyna/services/macro.py +182 -0
  192. marketmyna/services/market_calendar.py +399 -0
  193. marketmyna/services/market_mood.py +168 -0
  194. marketmyna/services/market_valuation.py +231 -0
  195. marketmyna/services/mf_holdings.py +199 -0
  196. marketmyna/services/mf_insights.py +132 -0
  197. marketmyna/services/mf_lumpsum.py +587 -0
  198. marketmyna/services/mf_portfolio_ai.py +195 -0
  199. marketmyna/services/nse_events.py +98 -0
  200. marketmyna/services/overview.py +366 -0
  201. marketmyna/services/ownership.py +143 -0
  202. marketmyna/services/quality_scores.py +199 -0
  203. marketmyna/services/rates.py +335 -0
  204. marketmyna/services/reconcile.py +343 -0
  205. marketmyna/services/report.py +584 -0
  206. marketmyna/services/report_i18n.py +678 -0
  207. marketmyna/services/rotation.py +224 -0
  208. marketmyna/services/runtime_scheduler.py +548 -0
  209. marketmyna/services/schedule_config.py +251 -0
  210. marketmyna/services/schedule_time.py +39 -0
  211. marketmyna/services/screens.py +398 -0
  212. marketmyna/services/sec_mdna.py +173 -0
  213. marketmyna/services/shariah.py +246 -0
  214. marketmyna/services/sip.py +148 -0
  215. marketmyna/services/sip_days.py +362 -0
  216. marketmyna/services/smart_file.py +595 -0
  217. marketmyna/services/smart_pdf.py +425 -0
  218. marketmyna/services/smart_report.py +370 -0
  219. marketmyna/services/social_sentiment.py +511 -0
  220. marketmyna/services/thesis.py +328 -0
  221. marketmyna/services/translate.py +146 -0
  222. marketmyna/services/valuation.py +173 -0
  223. marketmyna/services/worker_pool.py +149 -0
  224. marketmyna/services/world_markets.py +208 -0
  225. marketmyna/storage.py +722 -0
  226. marketmyna/strategies/README.md +51 -0
  227. marketmyna/strategies/bottom_volume.yaml +49 -0
  228. marketmyna/strategies/box_oscillation.yaml +69 -0
  229. marketmyna/strategies/bull_trend.yaml +49 -0
  230. marketmyna/strategies/chan_theory.yaml +58 -0
  231. marketmyna/strategies/dragon_head.yaml +44 -0
  232. marketmyna/strategies/emotion_cycle.yaml +79 -0
  233. marketmyna/strategies/event_driven.yaml +54 -0
  234. marketmyna/strategies/expectation_repricing.yaml +54 -0
  235. marketmyna/strategies/growth_quality.yaml +54 -0
  236. marketmyna/strategies/hot_theme.yaml +54 -0
  237. marketmyna/strategies/ma_golden_cross.yaml +46 -0
  238. marketmyna/strategies/one_yang_three_yin.yaml +37 -0
  239. marketmyna/strategies/shrink_pullback.yaml +46 -0
  240. marketmyna/strategies/volume_breakout.yaml +47 -0
  241. marketmyna/strategies/wave_theory.yaml +64 -0
  242. marketmyna/trading/__init__.py +2 -0
  243. marketmyna/trading/alerts.py +356 -0
  244. marketmyna/trading/backtest.py +317 -0
  245. marketmyna/trading/charges.py +82 -0
  246. marketmyna/trading/chart.py +197 -0
  247. marketmyna/trading/indicators.py +303 -0
  248. marketmyna/trading/journal.py +74 -0
  249. marketmyna/trading/knn.py +164 -0
  250. marketmyna/trading/lots.py +109 -0
  251. marketmyna/trading/market_hours.py +66 -0
  252. marketmyna/trading/options.py +554 -0
  253. marketmyna/trading/pairs.py +94 -0
  254. marketmyna/trading/risk.py +170 -0
  255. marketmyna/trading/scanner.py +500 -0
  256. marketmyna/trading/volatility.py +163 -0
  257. marketmyna/utils/__init__.py +1 -0
  258. marketmyna/utils/deadline.py +36 -0
  259. marketmyna/utils/log_redact.py +52 -0
  260. marketmyna/utils/yahoo_gate.py +183 -0
  261. marketmyna/utils/yf_lock.py +12 -0
  262. marketmyna/web/apple-touch-icon.png +0 -0
  263. marketmyna/web/assets/Account-CA9NFDKO.js +1 -0
  264. marketmyna/web/assets/Analyze-uM0srB_U.js +7 -0
  265. marketmyna/web/assets/AreaChart-2i67V6Jf.js +1 -0
  266. marketmyna/web/assets/BarChart-_yngarVl.js +3 -0
  267. marketmyna/web/assets/Bot-E4gpf1qZ.js +1 -0
  268. marketmyna/web/assets/CandleChart-CioqTqMV.js +1 -0
  269. marketmyna/web/assets/Compare-D6_2CPl4.js +1 -0
  270. marketmyna/web/assets/ExportMenu-DaZ1xVmW.js +1 -0
  271. marketmyna/web/assets/LineChart-BaOkD1Ta.js +57 -0
  272. marketmyna/web/assets/Markets-BMw_M1sJ.js +1 -0
  273. marketmyna/web/assets/Portfolio-DQEdHVc2.js +1 -0
  274. marketmyna/web/assets/ReportView-BzhdcqSh.js +1 -0
  275. marketmyna/web/assets/Scheduler-I9PDKxTd.js +1 -0
  276. marketmyna/web/assets/Settings-DpXfHolW.js +3 -0
  277. marketmyna/web/assets/SipDays-C9q3bNSx.js +1 -0
  278. marketmyna/web/assets/SymbolSearch-CBOVPEk1.js +1 -0
  279. marketmyna/web/assets/Trade-_CM7HC4H.js +2 -0
  280. marketmyna/web/assets/TradeChart-ZjsycacC.js +1 -0
  281. marketmyna/web/assets/WatchButton-ComYW56_.js +1 -0
  282. marketmyna/web/assets/Watchlist-DAjLb0jd.js +1 -0
  283. marketmyna/web/assets/app-BP1o7WZS.js +1 -0
  284. marketmyna/web/assets/app-BkIUKdld.js +1 -0
  285. marketmyna/web/assets/app-CKeSYhEt.js +1 -0
  286. marketmyna/web/assets/app-CN1OERYX.js +1 -0
  287. marketmyna/web/assets/app-DIQllha5.js +1 -0
  288. marketmyna/web/assets/app-DeiAW0tX.js +1 -0
  289. marketmyna/web/assets/br-Cu5YU29T.svg +45 -0
  290. marketmyna/web/assets/browser-CR-ks2Fp.js +19 -0
  291. marketmyna/web/assets/browser-D5K_K_gj.js +1 -0
  292. marketmyna/web/assets/common-0wvpAVpd.js +1 -0
  293. marketmyna/web/assets/common-3ghy53Us.js +1 -0
  294. marketmyna/web/assets/common-Bib2unjX.js +1 -0
  295. marketmyna/web/assets/common-BvSJP0w_.js +1 -0
  296. marketmyna/web/assets/common-CSfyC0Sp.js +1 -0
  297. marketmyna/web/assets/common-C_t-iZPf.js +1 -0
  298. marketmyna/web/assets/compare-DMcsx70H.js +1 -0
  299. marketmyna/web/assets/es-d5m8M5h8.svg +544 -0
  300. marketmyna/web/assets/exporter-DiMg2O5d.js +3 -0
  301. marketmyna/web/assets/files-CpgiZRbN.js +1 -0
  302. marketmyna/web/assets/format-d8InM0Zw.js +9 -0
  303. marketmyna/web/assets/html2canvas-Bw0OShmD.js +5 -0
  304. marketmyna/web/assets/index-CKAnq-Or.js +12 -0
  305. marketmyna/web/assets/index-D4U8E6LH.css +2 -0
  306. marketmyna/web/assets/index.es-iBEw7jOv.js +5 -0
  307. marketmyna/web/assets/ir-cCIgaNf6.svg +219 -0
  308. marketmyna/web/assets/jspdf.es.min-C4285IdL.js +79 -0
  309. marketmyna/web/assets/jspdf.plugin.autotable-2eOJeXl-.js +2 -0
  310. marketmyna/web/assets/lightweight-charts.production-BF2noug5.js +1 -0
  311. marketmyna/web/assets/listings-BefgXpb1.js +1 -0
  312. marketmyna/web/assets/liveQuotes-CtSmCUFA.js +1 -0
  313. marketmyna/web/assets/markets-BI4AhI9W.js +1 -0
  314. marketmyna/web/assets/markets-BUHUdYpT.js +1 -0
  315. marketmyna/web/assets/markets-BqvN1Bvf.js +1 -0
  316. marketmyna/web/assets/markets-DPO40TTe.js +1 -0
  317. marketmyna/web/assets/markets-bsiPo-Tk.js +1 -0
  318. marketmyna/web/assets/markets-ff1e792u.js +1 -0
  319. marketmyna/web/assets/montecarlo-BsNlH2qe.js +1 -0
  320. marketmyna/web/assets/options-BYAZnwzW.js +1 -0
  321. marketmyna/web/assets/preload-helper-BLdHbfdN.js +1 -0
  322. marketmyna/web/assets/purify.es-Dqk5qvad.js +3 -0
  323. marketmyna/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  324. marketmyna/web/assets/sa-Dh79zbT9.svg +25 -0
  325. marketmyna/web/assets/toast-DDradTZw.js +1 -0
  326. marketmyna/web/assets/trade-fndw1Ql3.js +1 -0
  327. marketmyna/web/assets/ui-gsFb8B2m.js +1 -0
  328. marketmyna/web/assets/watchlists-B6YqwUIk.js +1 -0
  329. marketmyna/web/brand/logo-mark.webp +0 -0
  330. marketmyna/web/favicon.png +0 -0
  331. marketmyna/web/icon-192.png +0 -0
  332. marketmyna/web/icon-512.png +0 -0
  333. marketmyna/web/index.html +34 -0
  334. marketmyna/web/manifest.webmanifest +28 -0
  335. marketmyna/web/robots.txt +2 -0
  336. marketmyna/web/sw.js +40 -0
  337. marketmyna/web/theme-init.js +37 -0
  338. marketmyna-0.2.0.dist-info/METADATA +345 -0
  339. marketmyna-0.2.0.dist-info/RECORD +344 -0
  340. marketmyna-0.2.0.dist-info/WHEEL +5 -0
  341. marketmyna-0.2.0.dist-info/entry_points.txt +2 -0
  342. marketmyna-0.2.0.dist-info/licenses/LICENSE +202 -0
  343. marketmyna-0.2.0.dist-info/licenses/NOTICE +22 -0
  344. 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