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,241 @@
1
+ """Indian market data fetcher using jugaad-data and Yahoo Finance."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional
5
+
6
+ import pandas as pd
7
+
8
+ from ..analysis.fundamental import yfinance_fundamentals
9
+ from ..core.constants import TimeFrame
10
+ from .base import DataFetcher
11
+ from .extra_sources import twelvedata_history, twelvedata_key, twelvedata_quote
12
+ from .resilience import FallbackChain
13
+
14
+ STALE_QUOTE_GAP = 0.10
15
+
16
+
17
+ def quote_from_bars(symbol: str, bars: pd.DataFrame) -> dict:
18
+ """A quote from the latest daily bars (Yahoo history: Open/High/Low/Close/Volume)."""
19
+ bar = bars.iloc[-1]
20
+ price = float(bar["Close"])
21
+ prev = float(bars["Close"].iloc[-2]) if len(bars) > 1 else None
22
+ return {
23
+ "symbol": symbol,
24
+ "price": price,
25
+ "previous_close": prev,
26
+ "open": float(bar["Open"]),
27
+ "day_high": float(bar["High"]),
28
+ "day_low": float(bar["Low"]),
29
+ "volume": int(bar["Volume"]),
30
+ "change": price - prev if prev else 0,
31
+ "change_percent": (price - prev) / prev * 100 if prev else 0,
32
+ }
33
+
34
+
35
+ # A BSE history shorter than this is treated as missing (see get_history).
36
+ BSE_MIN_BARS = 30
37
+
38
+
39
+ def is_bse(symbol: str) -> bool:
40
+ """RELIANCE.BO = the BSE listing; RELIANCE or RELIANCE.NS = NSE."""
41
+ return symbol.upper().endswith(".BO")
42
+
43
+
44
+ class IndiaDataFetcher(DataFetcher):
45
+ """Fetches Indian stock market data (NSE/BSE)."""
46
+
47
+ def __init__(self):
48
+ self._cache = {}
49
+ # Try to import jugaad_data, fall back to yfinance if not available
50
+ try:
51
+ from jugaad_data.nse import NSELive, stock_df
52
+ self._nse_live = NSELive()
53
+ self._stock_df = stock_df
54
+ self._use_jugaad = True
55
+ except ImportError:
56
+ self._use_jugaad = False
57
+
58
+ def _get_yf_symbol(self, symbol: str) -> str:
59
+ """Convert to Yahoo Finance format for Indian stocks."""
60
+ # Don't modify index symbols (they start with ^)
61
+ if symbol.startswith("^"):
62
+ return symbol
63
+ if not symbol.endswith(".NS") and not symbol.endswith(".BO"):
64
+ return f"{symbol}.NS" # Default to NSE
65
+ return symbol
66
+
67
+ def _fetch_quote_jugaad(self, symbol: str) -> dict:
68
+ quote = self._nse_live.stock_quote(symbol.replace(".NS", "").replace(".BO", ""))
69
+ price_info = quote.get("priceInfo", {})
70
+ return {
71
+ "symbol": symbol,
72
+ "price": price_info.get("lastPrice", 0),
73
+ "previous_close": price_info.get("previousClose", 0),
74
+ "open": price_info.get("open", 0),
75
+ "day_high": price_info.get("intraDayHighLow", {}).get("max", 0),
76
+ "day_low": price_info.get("intraDayHighLow", {}).get("min", 0),
77
+ "volume": quote.get("securityWiseDP", {}).get("quantityTraded", 0),
78
+ "change": price_info.get("change", 0),
79
+ "change_percent": price_info.get("pChange", 0),
80
+ }
81
+
82
+ def _fetch_quote_yfinance(self, symbol: str) -> dict:
83
+ import yfinance as yf
84
+ ticker = yf.Ticker(self._get_yf_symbol(symbol))
85
+ info = ticker.fast_info
86
+ try:
87
+ last, prev = info.last_price, info.previous_close
88
+ except KeyError:
89
+ # Yahoo has no such symbol (a typo, a fund name): an empty answer,
90
+ # not a source failure, so it can't trip the breaker for every stock.
91
+ return {}
92
+
93
+ # Yahoo's live quote for some listings (REITs, InvITs) is frozen at
94
+ # an old date while its daily bars stay current. A move of more than
95
+ # 10% is checked against the bars; if they disagree, the bars win.
96
+ if last and prev and abs(last / prev - 1) > STALE_QUOTE_GAP:
97
+ bars = ticker.history(period="5d", interval="1d")
98
+ if not bars.empty and abs(last / float(bars["Close"].iloc[-1]) - 1) > STALE_QUOTE_GAP:
99
+ return quote_from_bars(symbol, bars)
100
+
101
+ return {
102
+ "symbol": symbol,
103
+ "price": last,
104
+ "previous_close": prev,
105
+ "open": info.open,
106
+ "day_high": info.day_high,
107
+ "day_low": info.day_low,
108
+ "volume": info.last_volume,
109
+ "change": last - prev if prev else 0,
110
+ "change_percent": ((last - prev) / prev) * 100 if prev else 0,
111
+ }
112
+
113
+ def get_quote(self, symbol: str) -> dict:
114
+ """Get current quote for an Indian stock."""
115
+ chain = FallbackChain("india.get_quote")
116
+ # jugaad-data reads NSE only: a BSE symbol (.BO) goes straight to Yahoo.
117
+ if self._use_jugaad and not is_bse(symbol):
118
+ chain.add("jugaad", self._fetch_quote_jugaad)
119
+ chain.add("yfinance", self._fetch_quote_yfinance)
120
+ if not symbol.startswith("^") and twelvedata_key():
121
+ chain.add("twelvedata", lambda s: twelvedata_quote(s, "india"))
122
+ return chain.call(symbol, is_empty=lambda d: not d or not d.get("price"))
123
+
124
+ _PERIOD_DAYS = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90, "6mo": 180, "1y": 365, "2y": 730, "5y": 1825}
125
+
126
+ def _fetch_history_jugaad(
127
+ self,
128
+ symbol: str,
129
+ period: str = "1y",
130
+ start: Optional[datetime] = None,
131
+ end: Optional[datetime] = None,
132
+ **_ignored,
133
+ ) -> pd.DataFrame:
134
+ clean_symbol = symbol.replace(".NS", "").replace(".BO", "")
135
+ range_start = start or (datetime.now() - timedelta(days=self._PERIOD_DAYS.get(period, 365)))
136
+ range_end = end or datetime.now()
137
+
138
+ df = self._stock_df(symbol=clean_symbol, from_date=range_start.date(), to_date=range_end.date())
139
+ if df.empty:
140
+ return df
141
+
142
+ df = df.rename(
143
+ columns={
144
+ "DATE": "date",
145
+ "OPEN": "open",
146
+ "HIGH": "high",
147
+ "LOW": "low",
148
+ "CLOSE": "close",
149
+ "LTP": "ltp",
150
+ "VOLUME": "volume",
151
+ }
152
+ )
153
+ df.set_index("date", inplace=True)
154
+ return df
155
+
156
+ def get_history(
157
+ self,
158
+ symbol: str,
159
+ period: str = "1y",
160
+ interval: TimeFrame = TimeFrame.DAY_1,
161
+ start: Optional[datetime] = None,
162
+ end: Optional[datetime] = None,
163
+ ) -> pd.DataFrame:
164
+ """Get historical price data for an Indian stock."""
165
+ chain = FallbackChain("india.get_history")
166
+ if self._use_jugaad and interval == TimeFrame.DAY_1 and not is_bse(symbol):
167
+ chain.add("jugaad", self._fetch_history_jugaad)
168
+ chain.add("yfinance", self._fetch_history_yfinance)
169
+ if not symbol.startswith("^") and twelvedata_key():
170
+ chain.add("twelvedata", lambda s, **kw: twelvedata_history(s, "india", **kw))
171
+ df = chain.call(
172
+ symbol, period=period, interval=interval, start=start, end=end, is_empty=lambda df: df.empty
173
+ )
174
+ if is_bse(symbol) and len(df) < BSE_MIN_BARS:
175
+ # Yahoo often has only the last day or two for a BSE listing
176
+ # (RELIANCE.BO). The same company's NSE history trades within
177
+ # paise of it, so charts, indicators and levels use that instead.
178
+ try:
179
+ nse = self.get_history(symbol[:-3], period=period, interval=interval, start=start, end=end)
180
+ except Exception:
181
+ nse = None
182
+ if nse is not None and len(nse) > len(df):
183
+ nse.attrs["history_from"] = "NSE"
184
+ return nse
185
+ return df
186
+
187
+ def _fetch_history_yfinance(self, symbol: str, **kwargs) -> pd.DataFrame:
188
+ from .stocks import StockDataFetcher
189
+
190
+ return StockDataFetcher()._fetch_history_yfinance(self._get_yf_symbol(symbol), **kwargs)
191
+
192
+ def search(self, query: str) -> list[dict]:
193
+ """Search for Indian stocks matching query."""
194
+ # Use yfinance for search
195
+ import yfinance as yf
196
+
197
+ results = []
198
+ for suffix in [".NS", ".BO"]:
199
+ try:
200
+ ticker = yf.Ticker(f"{query}{suffix}")
201
+ info = ticker.info
202
+ if info and info.get("symbol"):
203
+ results.append(
204
+ {
205
+ "symbol": info.get("symbol"),
206
+ "name": info.get("longName", info.get("shortName", "")),
207
+ "exchange": "NSE" if suffix == ".NS" else "BSE",
208
+ "type": info.get("quoteType", "EQUITY"),
209
+ }
210
+ )
211
+ except Exception:
212
+ pass
213
+
214
+ return results
215
+
216
+ def get_info(self, symbol: str) -> dict:
217
+ """Get detailed information about an Indian stock."""
218
+ import yfinance as yf
219
+
220
+ ticker = yf.Ticker(self._get_yf_symbol(symbol))
221
+ info = ticker.info
222
+
223
+ return {
224
+ "symbol": info.get("symbol", symbol),
225
+ "name": info.get("longName", ""),
226
+ "sector": info.get("sector", ""),
227
+ "industry": info.get("industry", ""),
228
+ "description": info.get("longBusinessSummary", ""),
229
+ "currency": "INR",
230
+ "market_cap": info.get("marketCap", 0),
231
+ "pe_ratio": info.get("trailingPE", 0),
232
+ "pb_ratio": info.get("priceToBook", 0),
233
+ "dividend_yield": info.get("dividendYield", 0),
234
+ "52_week_high": info.get("fiftyTwoWeekHigh", 0),
235
+ "52_week_low": info.get("fiftyTwoWeekLow", 0),
236
+ "book_value": info.get("bookValue", 0),
237
+ "face_value": info.get("faceValue", 0),
238
+ # Normalized ratios for FundamentalAnalyzer (overrides the raw
239
+ # yfinance-unit fields above, e.g. dividend_yield)
240
+ **yfinance_fundamentals(info),
241
+ }
@@ -0,0 +1,343 @@
1
+ """Indian financial statements from the companies' own XBRL filings on NSE.
2
+
3
+ Every listed company files its results with the exchange in XBRL (SEBI's
4
+ Ind-AS taxonomy), so these are the official numbers, the same ones data
5
+ sites compile from. Two NSE feeds cover the history:
6
+
7
+ - corporates-financial-results (Quarterly / Annual): 2005 to Dec 2024;
8
+ - integrated-filing-results (Financials): SEBI's integrated filing, 2025 on.
9
+
10
+ Quarterly filings carry the profit and loss for the quarter; the March
11
+ filing also carries the full year, the year-end balance sheet and the
12
+ year's cash flows (the September one the half-year balance sheet).
13
+
14
+ statements() returns three DataFrames shaped like yfinance's statements
15
+ (rows named as yfinance names them, one column per period end, newest
16
+ first), so services/financials.py analyses them unchanged. Filings never
17
+ change once published, so each parsed file is cached on disk for good;
18
+ the filing lists are cached for a few hours. Anything unreadable raises,
19
+ and the caller falls back to Yahoo.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import logging
27
+ import re
28
+ import threading
29
+ import time
30
+ from datetime import date, datetime
31
+ from pathlib import Path
32
+ from typing import Optional
33
+ from xml.etree import ElementTree as ET
34
+
35
+ import pandas as pd
36
+ import requests
37
+
38
+ from marketmyna.core.home import app_home
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ API = "https://www.nseindia.com/api"
43
+ _HEADERS = {
44
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36",
45
+ "Referer": "https://www.nseindia.com/",
46
+ "Accept": "application/json, text/plain, */*",
47
+ }
48
+ _TIMEOUT = 25
49
+ LIST_TTL = 6 * 3600
50
+ QUARTERS = 8
51
+ PARSE_VERSION = 2
52
+ YEARS = 5
53
+
54
+ _lock = threading.Lock()
55
+ _lists: dict[str, tuple[float, list[dict]]] = {}
56
+
57
+
58
+ def _cache_dir() -> Path:
59
+ path = app_home() / "india_filings"
60
+ path.mkdir(parents=True, exist_ok=True)
61
+ return path
62
+
63
+
64
+ def _get(url: str) -> requests.Response:
65
+ resp = requests.get(url, headers=_HEADERS, timeout=_TIMEOUT)
66
+ resp.raise_for_status()
67
+ return resp
68
+
69
+
70
+ # ------------------------------------------------------------------ filing lists
71
+
72
+
73
+ def _day(text: str) -> Optional[date]:
74
+ for fmt in ("%d-%b-%Y", "%d-%B-%Y"):
75
+ try:
76
+ return datetime.strptime(text.strip().title(), fmt).date()
77
+ except (AttributeError, ValueError):
78
+ continue
79
+ return None
80
+
81
+
82
+ def filings(symbol: str) -> list[dict]:
83
+ """[{end, consolidated, url}] for every results filing, newest first."""
84
+ symbol = symbol.upper()
85
+ with _lock:
86
+ hit = _lists.get(symbol)
87
+ if hit and time.time() - hit[0] < LIST_TTL:
88
+ return hit[1]
89
+ out: dict[tuple, dict] = {}
90
+
91
+ def add(end: Optional[date], consolidated: str, url: Optional[str]):
92
+ if end and url and url.endswith(".xml"):
93
+ key = (end, consolidated.lower().startswith("consolidated"))
94
+ out.setdefault(key, {"end": end, "consolidated": key[1], "url": url})
95
+
96
+ errors = []
97
+ try:
98
+ data = _get(f"{API}/integrated-filing-results?symbol={symbol}&type=Integrated%20Filing-%20Financials").json()
99
+ for r in data.get("data") or []:
100
+ add(_day(r.get("qe_Date") or ""), r.get("consolidated") or "", r.get("xbrl"))
101
+ except Exception as exc:
102
+ errors.append(exc)
103
+ for period in ("Annual", "Quarterly"):
104
+ try:
105
+ for r in _get(f"{API}/corporates-financial-results?index=equities&symbol={symbol}&period={period}").json():
106
+ add(_day(r.get("toDate") or ""), r.get("consolidated") or "", r.get("xbrl"))
107
+ except Exception as exc:
108
+ errors.append(exc)
109
+ if not out:
110
+ raise RuntimeError(f"No NSE results filings for {symbol}: {errors[:1]}")
111
+ rows = sorted(out.values(), key=lambda r: r["end"], reverse=True)
112
+ with _lock:
113
+ _lists[symbol] = (time.time(), rows)
114
+ return rows
115
+
116
+
117
+ # ------------------------------------------------------------------ XBRL
118
+
119
+
120
+ def parse(xml: bytes) -> list[tuple[str, Optional[str], str, float]]:
121
+ """[(tag, start or None for an instant, end, value)] for the facts with
122
+ no dimension (the company totals, not segments or breakdowns)."""
123
+ root = ET.fromstring(xml)
124
+ periods: dict[str, tuple[Optional[str], str]] = {}
125
+ for ctx in root:
126
+ if not ctx.tag.endswith("context"):
127
+ continue
128
+ if any(el.tag.endswith(("explicitMember", "typedMember")) for el in ctx.iter()):
129
+ continue
130
+ start = ctx.find(".//{*}startDate")
131
+ end = ctx.find(".//{*}endDate")
132
+ instant = ctx.find(".//{*}instant")
133
+ if instant is not None and instant.text:
134
+ periods[ctx.get("id")] = (None, instant.text.strip())
135
+ elif start is not None and end is not None and start.text and end.text:
136
+ begin, stop = start.text.strip(), end.text.strip()
137
+ if ctx.get("id") == "FourD":
138
+ # "FourD" is the year to date. Filings before 2025 give it the
139
+ # quarter's dates by mistake; it always starts on 1 April.
140
+ last = date.fromisoformat(stop)
141
+ begin = date(last.year if last.month >= 4 else last.year - 1, 4, 1).isoformat()
142
+ periods[ctx.get("id")] = (begin, stop)
143
+ facts = []
144
+ for el in root:
145
+ ref = el.get("contextRef")
146
+ text = (el.text or "").strip()
147
+ if ref not in periods or not re.fullmatch(r"-?\d+(\.\d+)?", text):
148
+ continue
149
+ start, end = periods[ref]
150
+ facts.append((el.tag.split("}")[-1], start, end, float(text)))
151
+ return facts
152
+
153
+
154
+ def _facts(url: str) -> list[tuple]:
155
+ # The version prefix retires cached parses when parse() changes.
156
+ path = _cache_dir() / (hashlib.sha256(f"{PARSE_VERSION}:{url}".encode()).hexdigest()[:24] + ".json")
157
+ try:
158
+ return [tuple(f) for f in json.loads(path.read_text(encoding="utf-8"))]
159
+ except (OSError, ValueError):
160
+ pass
161
+ facts = parse(_get(url).content)
162
+ tmp = path.with_suffix(".tmp")
163
+ tmp.write_text(json.dumps(facts), encoding="utf-8")
164
+ tmp.replace(path)
165
+ return facts
166
+
167
+
168
+ # ------------------------------------------------------------------ statements
169
+
170
+ # yfinance row name -> how to read it from one period's facts {tag: value}.
171
+ def _sum(v: dict, *tags: str) -> Optional[float]:
172
+ vals = [v[t] for t in tags if t in v]
173
+ return sum(vals) if vals else None
174
+
175
+
176
+ def _first(v: dict, *tags: str) -> Optional[float]:
177
+ return next((v[t] for t in tags if t in v), None)
178
+
179
+
180
+ def _minus(a: Optional[float], b: Optional[float]) -> Optional[float]:
181
+ return a - b if a is not None and b is not None else None
182
+
183
+
184
+ def _income(v: dict) -> dict:
185
+ revenue = v.get("RevenueFromOperations")
186
+ cost = _sum(v, "CostOfMaterialsConsumed", "PurchasesOfStockInTrade",
187
+ "ChangesInInventoriesOfFinishedGoodsWorkInProgressAndStockInTrade")
188
+ pbt_before_exceptional = _first(v, "ProfitBeforeExceptionalItemsAndTax", "ProfitBeforeTax")
189
+ finance = v.get("FinanceCosts")
190
+ operating = None
191
+ if pbt_before_exceptional is not None:
192
+ operating = pbt_before_exceptional + (finance or 0) - (v.get("OtherIncome") or 0)
193
+ depreciation = v.get("DepreciationDepletionAndAmortisationExpense")
194
+ return {
195
+ "Total Revenue": revenue,
196
+ "Cost Of Revenue": cost,
197
+ "Gross Profit": _minus(revenue, cost),
198
+ "Operating Income": operating,
199
+ "EBITDA": operating + depreciation if operating is not None and depreciation is not None else None,
200
+ "Interest Expense": finance,
201
+ # Overheads: Ind-AS has no SG&A line; staff plus other expenses stand in
202
+ "Selling General And Administration": _sum(v, "EmployeeBenefitExpense", "OtherExpenses"),
203
+ "Pretax Income": v.get("ProfitBeforeTax"),
204
+ "Tax Provision": v.get("TaxExpense"),
205
+ "Net Income": _first(v, "ProfitOrLossAttributableToOwnersOfParent", "ProfitLossForPeriod"),
206
+ "Diluted EPS": _first(v, "DilutedEarningsLossPerShareFromContinuingAndDiscontinuedOperations",
207
+ "DilutedEarningsLossPerShareFromContinuingOperations"),
208
+ "Basic EPS": _first(v, "BasicEarningsLossPerShareFromContinuingAndDiscontinuedOperations",
209
+ "BasicEarningsLossPerShareFromContinuingOperations"),
210
+ }
211
+
212
+
213
+ def _balance(v: dict) -> dict:
214
+ debt = _sum(v, "BorrowingsNoncurrent", "BorrowingsCurrent")
215
+ cash = v.get("CashAndCashEquivalents")
216
+ paid_up, face = v.get("PaidUpValueOfEquityShareCapital") or v.get("EquityShareCapital"), v.get("FaceValueOfEquityShareCapital")
217
+ return {
218
+ "Total Assets": v.get("Assets"),
219
+ "Current Assets": v.get("CurrentAssets"),
220
+ "Cash And Cash Equivalents": cash,
221
+ "Accounts Receivable": v.get("TradeReceivablesCurrent"),
222
+ "Inventory": v.get("Inventories"),
223
+ "Total Liabilities Net Minority Interest": v.get("Liabilities"),
224
+ "Current Liabilities": v.get("CurrentLiabilities"),
225
+ "Total Debt": debt,
226
+ "Net Debt": _minus(debt, cash),
227
+ "Stockholders Equity": _first(v, "EquityAttributableToOwnersOfParent", "Equity"),
228
+ "Goodwill And Other Intangible Assets": _sum(v, "Goodwill", "OtherIntangibleAssets"),
229
+ "Net PPE": v.get("PropertyPlantAndEquipment"),
230
+ # Ind-AS "other equity" is reserves and surplus, mostly retained earnings
231
+ "Retained Earnings": _first(v, "RetainedEarnings", "OtherEquity"),
232
+ "Ordinary Shares Number": paid_up / face if paid_up and face else None,
233
+ }
234
+
235
+
236
+ def _cashflow(v: dict) -> dict:
237
+ operating = v.get("CashFlowsFromUsedInOperatingActivities")
238
+ purchase = v.get("PurchaseOfPropertyPlantAndEquipmentClassifiedAsInvestingActivities")
239
+ capex = -abs(purchase) if purchase is not None else None
240
+ dividends = v.get("DividendsPaidClassifiedAsFinancingActivities")
241
+ return {
242
+ "Operating Cash Flow": operating,
243
+ "Capital Expenditure": capex,
244
+ "Free Cash Flow": operating + capex if operating is not None and capex is not None else None,
245
+ "Investing Cash Flow": v.get("CashFlowsFromUsedInInvestingActivities"),
246
+ "Depreciation And Amortization": _first(v, "AdjustmentsForDepreciationAndAmortisationExpense",
247
+ "DepreciationDepletionAndAmortisationExpense"),
248
+ "Financing Cash Flow": v.get("CashFlowsFromUsedInFinancingActivities"),
249
+ "Cash Dividends Paid": -abs(dividends) if dividends is not None else None,
250
+ }
251
+
252
+
253
+ def _span_days(start: str, end: str) -> int:
254
+ return (date.fromisoformat(end) - date.fromisoformat(start)).days
255
+
256
+
257
+ def _period_values(facts: list[tuple], end: date, span: tuple[int, int]) -> tuple[dict, dict]:
258
+ """(duration facts covering `span` days and ending at `end`, instant facts at `end`)."""
259
+ iso = end.isoformat()
260
+ durations, instants = {}, {}
261
+ for tag, start, stop, value in facts:
262
+ if stop != iso:
263
+ continue
264
+ if start is None:
265
+ instants.setdefault(tag, value)
266
+ elif span[0] <= _span_days(start, stop) <= span[1]:
267
+ durations.setdefault(tag, value)
268
+ return durations, instants
269
+
270
+
271
+ QUARTER_DAYS = (80, 100)
272
+ YEAR_DAYS = (350, 380)
273
+
274
+
275
+ def _frame(columns: dict[date, dict]) -> pd.DataFrame:
276
+ """yfinance-shaped: rows = line items, columns = period ends (newest first)."""
277
+ if not columns:
278
+ return pd.DataFrame()
279
+ df = pd.DataFrame({pd.Timestamp(d): col for d, col in columns.items()})
280
+ df = df[sorted(df.columns, reverse=True)]
281
+ return df.dropna(how="all")
282
+
283
+
284
+ def statements(symbol: str, quarterly: bool = False) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
285
+ """(income, balance, cashflow) from NSE filings, consolidated when the
286
+ company files consolidated results, else standalone."""
287
+ rows = filings(symbol)
288
+ consolidated = any(r["consolidated"] for r in rows[:4])
289
+ rows = [r for r in rows if r["consolidated"] == consolidated]
290
+ if quarterly:
291
+ rows = rows[:QUARTERS]
292
+ span = QUARTER_DAYS
293
+ else:
294
+ rows = [r for r in rows if r["end"].month == 3 and r["end"].day == 31][:YEARS]
295
+ span = YEAR_DAYS
296
+ if not rows:
297
+ raise RuntimeError(f"No {'quarterly' if quarterly else 'annual'} filings for {symbol}")
298
+
299
+ income, balance, cashflow = {}, {}, {}
300
+ shares: dict[date, float] = {}
301
+ for r in rows:
302
+ try:
303
+ facts = _facts(r["url"])
304
+ except Exception as exc:
305
+ logger.info("NSE filing %s unreadable: %s", r["url"], exc)
306
+ continue
307
+ durations, instants = _period_values(facts, r["end"], span)
308
+ if durations.get("RevenueFromOperations") is not None:
309
+ income[r["end"]] = _income(durations)
310
+ count = _share_count({**instants, **durations})
311
+ if count:
312
+ shares[r["end"]] = count
313
+ if instants.get("Assets") is not None:
314
+ balance[r["end"]] = _balance(instants)
315
+ if durations.get("CashFlowsFromUsedInOperatingActivities") is not None:
316
+ cashflow[r["end"]] = _cashflow(durations)
317
+ if len(income) < 2:
318
+ # Banks and insurers file a different layout (interest earned, ...): leave them to Yahoo.
319
+ raise RuntimeError(f"NSE filings for {symbol} have no readable income statement")
320
+ _restate_eps(income, shares)
321
+ return _frame(income), _frame(balance), _frame(cashflow)
322
+
323
+
324
+ def _share_count(v: dict) -> Optional[float]:
325
+ paid_up = v.get("PaidUpValueOfEquityShareCapital") or v.get("EquityShareCapital")
326
+ face = v.get("FaceValueOfEquityShareCapital")
327
+ return paid_up / face if paid_up and face else None
328
+
329
+
330
+ def _restate_eps(income: dict[date, dict], shares: dict[date, float]) -> None:
331
+ """Filings report EPS on the share count of their day; a later bonus or
332
+ split would read as an earnings collapse. Restate each period's EPS onto
333
+ the latest share count (as Yahoo does)."""
334
+ if not shares:
335
+ return
336
+ latest = shares[max(shares)]
337
+ for end, row in income.items():
338
+ count = shares.get(end)
339
+ if not count or abs(count / latest - 1) < 0.01:
340
+ continue
341
+ for key in ("Diluted EPS", "Basic EPS"):
342
+ if row.get(key) is not None:
343
+ row[key] = round(row[key] * count / latest, 2)