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,295 @@
1
+ """Live prices for the rows on screen (Markets, Watchlist), shared by all users.
2
+
3
+ The Markets listing is a whole-market snapshot refreshed every few hours.
4
+ This module keeps a small cache of fresh quotes for just the symbols people
5
+ are looking at:
6
+
7
+ - Browsers ask every second (cheap: the answer comes from this cache).
8
+ - The cache refreshes each market in the background, one batched upstream
9
+ call for every symbol anyone is watching, at most every REFRESH[market]
10
+ seconds. 1,000 users on the same page cost the data source the same as one.
11
+ - Free sources: Yahoo for stocks / ETFs (can lag up to ~15 min for NSE),
12
+ Binance for crypto (real time). A licensed broker feed (Kite Connect,
13
+ Upstox, SmartAPI websockets) can replace `_fetch_*` for true tick data.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ import threading
21
+ import time
22
+ from typing import Optional
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Seconds between upstream refreshes per market (env LIVE_REFRESH_<MARKET>).
27
+ REFRESH = {
28
+ "india": float(os.environ.get("LIVE_REFRESH_INDIA", 15)),
29
+ "us": float(os.environ.get("LIVE_REFRESH_US", 15)),
30
+ "china_a": float(os.environ.get("LIVE_REFRESH_CHINA_A", 30)),
31
+ "hong_kong": float(os.environ.get("LIVE_REFRESH_HONG_KONG", 30)),
32
+ "crypto": float(os.environ.get("LIVE_REFRESH_CRYPTO", 2)),
33
+ }
34
+ # With a broker feed (Angel One) Indian prices refresh every second.
35
+ BROKER_REFRESH = float(os.environ.get("LIVE_REFRESH_BROKER", 1))
36
+ WATCH_SECONDS = 60 # a symbol nobody asked about for a minute stops being fetched
37
+ MAX_SYMBOLS = 100 # per request (one page of the table)
38
+ SOURCE = {"crypto": "Crypto exchange, real time"}
39
+ DEFAULT_SOURCE = "Market data feed, can lag up to 15 min"
40
+
41
+
42
+ class _Market:
43
+ def __init__(self):
44
+ self.quotes: dict[str, dict] = {} # symbol -> {price, change, change_percent, volume, time}
45
+ self.watched: dict[str, float] = {} # symbol -> last asked (monotonic)
46
+ self.fetched_at = 0.0
47
+ self.error: Optional[str] = None
48
+ self.busy = False
49
+ self.lock = threading.Lock()
50
+
51
+
52
+ _markets: dict[str, _Market] = {}
53
+ _lock = threading.Lock()
54
+
55
+
56
+ def _market(name: str) -> _Market:
57
+ with _lock:
58
+ return _markets.setdefault(name, _Market())
59
+
60
+
61
+ def get(market: str, symbols: list[str], depth: bool = False) -> dict:
62
+ """Latest cached quotes for `symbols`; starts a background refresh when
63
+ the market's cache is older than its refresh interval. Never blocks on
64
+ the upstream source. The order book (broker feed only) is left out
65
+ unless `depth`, so the tables' per-second poll stays small."""
66
+ from ..trading.market_hours import is_open
67
+
68
+ m = _market(market)
69
+ now = time.monotonic()
70
+ symbols = [s.strip().upper() for s in symbols if s.strip()][:MAX_SYMBOLS]
71
+ open_now = is_open(market)
72
+ with m.lock:
73
+ for s in symbols:
74
+ m.watched[s] = now
75
+ stale = now - m.fetched_at >= refresh_seconds(market)
76
+ missing = any(s not in m.quotes for s in symbols)
77
+ start = (stale and (open_now or missing)) and not m.busy and market in REFRESH
78
+ if start:
79
+ m.busy = True
80
+ out = {s: m.quotes[s] if depth else {k: v for k, v in m.quotes[s].items() if k != "depth"}
81
+ for s in symbols if s in m.quotes}
82
+ age = None if not m.fetched_at else round(now - m.fetched_at, 1)
83
+ error = m.error
84
+ if start:
85
+ threading.Thread(target=_refresh, args=(market,), name=f"live-quotes-{market}", daemon=True).start()
86
+ return {
87
+ "market": market,
88
+ "open": open_now,
89
+ "quotes": out,
90
+ "age_seconds": age,
91
+ "refresh_seconds": refresh_seconds(market),
92
+ "source": source(market),
93
+ "error": error,
94
+ # Newest trade time among these quotes (epoch seconds). During "market
95
+ # hours" on a holiday this stays at the last session's close.
96
+ "last_trade": max((q.get("time") or 0 for q in out.values()), default=None) or None,
97
+ }
98
+
99
+
100
+ def _refresh(market: str) -> None:
101
+ m = _market(market)
102
+ now = time.monotonic()
103
+ with m.lock:
104
+ m.watched = {s: t for s, t in m.watched.items() if now - t < WATCH_SECONDS}
105
+ symbols = list(m.watched)
106
+ try:
107
+ fresh = (_fetch_crypto(symbols) if market == "crypto" else _fetch_india(symbols) if market == "india"
108
+ else _fetch_us(symbols) if market == "us" else _fetch_yahoo(symbols, market))
109
+ error = None
110
+ except Exception as exc: # keep serving the last quotes
111
+ logger.warning("Live quotes for %s failed: %s", market, exc)
112
+ fresh, error = {}, "Live prices are temporarily unavailable; showing the last ones."
113
+ with m.lock:
114
+ m.quotes.update(fresh)
115
+ m.fetched_at = time.monotonic()
116
+ m.error = error
117
+ m.busy = False
118
+
119
+
120
+ def _broker_config():
121
+ """App config when Angel One is set up (admin panel or env), else None."""
122
+ try:
123
+ from marketmyna.api.deps import get_config_dep
124
+
125
+ from .brokers import angel
126
+
127
+ config = get_config_dep()
128
+ return config if angel.configured(config) else None
129
+ except Exception:
130
+ return None
131
+
132
+
133
+ _broker_failed_at = 0.0
134
+
135
+
136
+ def _broker_live() -> bool:
137
+ # After a broker failure, a minute at the normal pace (Yahoo can't take 1 s polling).
138
+ return _broker_config() is not None and time.monotonic() - _broker_failed_at > 60
139
+
140
+
141
+ def _finnhub_config():
142
+ try:
143
+ from marketmyna.api.deps import get_config_dep
144
+
145
+ from .brokers import finnhub
146
+
147
+ config = get_config_dep()
148
+ return config if finnhub.configured(config) else None
149
+ except Exception:
150
+ return None
151
+
152
+
153
+ _yahoo_us_at = 0.0
154
+
155
+
156
+ def _fetch_us(symbols: list[str]) -> dict[str, dict]:
157
+ """Finnhub's trade stream when a key is set (real time); Yahoo otherwise.
158
+ Symbols Finnhub has no price for yet are filled from Yahoo, at Yahoo's
159
+ normal 15 s pace."""
160
+ global _yahoo_us_at
161
+ config = _finnhub_config()
162
+ if config is None:
163
+ return _fetch_yahoo(symbols, "us")
164
+ from .brokers import finnhub
165
+
166
+ s = finnhub.stream(config)
167
+ s.want(symbols)
168
+ out = s.quotes(symbols)
169
+ rest = [x for x in symbols if x not in out]
170
+ if rest and time.monotonic() - _yahoo_us_at >= REFRESH["us"]:
171
+ _yahoo_us_at = time.monotonic()
172
+ try:
173
+ out.update(_fetch_yahoo(rest, "us"))
174
+ except Exception:
175
+ pass
176
+ return out
177
+
178
+
179
+ def refresh_seconds(market: str) -> float:
180
+ if market == "us" and _finnhub_config() is not None:
181
+ return 1.0
182
+ if market == "india" and _broker_live():
183
+ # Every second for one batch of 50; slower when more symbols are watched,
184
+ # so the shared Angel account stays under its hourly call limit.
185
+ from .brokers.angel import BATCH, CALLS_PER_SECOND
186
+
187
+ batches = max(1, -(-len(_market("india").watched) // BATCH))
188
+ return max(BROKER_REFRESH, batches / CALLS_PER_SECOND)
189
+ return REFRESH.get(market, 30)
190
+
191
+
192
+ def source(market: str) -> str:
193
+ if market == "us" and _finnhub_config() is not None:
194
+ return "Real-time feed"
195
+ if market == "india" and _broker_live():
196
+ return "Broker feed, live"
197
+ return SOURCE.get(market, DEFAULT_SOURCE)
198
+
199
+
200
+ def _fetch_india(symbols: list[str]) -> dict[str, dict]:
201
+ """Angel One when configured (live); Yahoo otherwise or if it fails.
202
+ Symbols Angel can't price (e.g. BSE-only) are filled from Yahoo."""
203
+ global _broker_failed_at
204
+ config = _broker_config()
205
+ # During the minute after a failure (e.g. Angel's rate limit) stay on Yahoo.
206
+ if config is None or time.monotonic() - _broker_failed_at <= 60:
207
+ return _fetch_yahoo(symbols, "india")
208
+ from .brokers import angel
209
+
210
+ try:
211
+ out = angel.client(config).quotes(symbols)
212
+ except Exception as exc:
213
+ logger.warning("Angel One quotes failed, using Yahoo: %s", exc)
214
+ _broker_failed_at = time.monotonic()
215
+ return _fetch_yahoo(symbols, "india")
216
+ rest = [s for s in symbols if s not in out]
217
+ if rest:
218
+ try:
219
+ out.update(_fetch_yahoo(rest, "india"))
220
+ except Exception:
221
+ pass
222
+ return out
223
+
224
+
225
+ def _fetch_crypto(symbols: list[str]) -> dict[str, dict]:
226
+ """Binance 24h tickers for BASE-USD symbols (USDT pairs), one request."""
227
+ import json
228
+
229
+ import requests
230
+
231
+ pairs = {f"{s.split('-')[0]}USDT": s for s in symbols if s}
232
+ if not pairs:
233
+ return {}
234
+ r = requests.get("https://api.binance.com/api/v3/ticker/24hr",
235
+ params={"symbols": json.dumps(list(pairs), separators=(",", ":"))}, timeout=10)
236
+ r.raise_for_status()
237
+ stamp = time.time()
238
+ out = {}
239
+ for t in r.json():
240
+ sym = pairs.get(t.get("symbol"))
241
+ price = _num(t.get("lastPrice"))
242
+ if sym and price:
243
+ out[sym] = {"price": price, "change": _num(t.get("priceChange")),
244
+ "change_percent": _num(t.get("priceChangePercent")),
245
+ "volume": _num(t.get("quoteVolume")), "time": stamp}
246
+ return out
247
+
248
+
249
+ def _fetch_yahoo(symbols: list[str], market: str) -> dict[str, dict]:
250
+ """One batched 1-minute download for every watched symbol: last trade,
251
+ change against the previous session's close, and today's volume."""
252
+ import pandas as pd
253
+ import yfinance as yf
254
+
255
+ from ..utils.yf_lock import YF_DOWNLOAD_LOCK
256
+ from .analysis_run import yahoo_symbol
257
+
258
+ if not symbols:
259
+ return {}
260
+ tickers = {yahoo_symbol(s, market): s for s in symbols}
261
+ with YF_DOWNLOAD_LOCK:
262
+ df = yf.download(list(tickers), period="5d", interval="1m", group_by="ticker", progress=False,
263
+ threads=True, auto_adjust=False, prepost=False)
264
+ out = {}
265
+ for yt, sym in tickers.items():
266
+ try:
267
+ # group_by="ticker" nests columns under the ticker even for one
268
+ # symbol in current yfinance; older releases returned them flat.
269
+ frame = df[yt] if len(tickers) > 1 or isinstance(df.columns, pd.MultiIndex) else df
270
+ frame = frame.dropna(subset=["Close"])
271
+ except (KeyError, TypeError):
272
+ continue
273
+ if frame.empty:
274
+ continue
275
+ last_day = frame.index[-1].date()
276
+ today = frame[frame.index.date == last_day]
277
+ before = frame[frame.index.date < last_day]
278
+ price = float(today["Close"].iloc[-1])
279
+ prev = float(before["Close"].iloc[-1]) if not before.empty else None
280
+ change = price - prev if prev else None
281
+ out[sym] = {
282
+ "price": round(price, 4),
283
+ "change": round(change, 4) if change is not None else None,
284
+ "change_percent": round(change / prev * 100, 2) if prev else None,
285
+ "volume": float(today["Volume"].sum()) if "Volume" in today else None,
286
+ "time": frame.index[-1].timestamp(),
287
+ }
288
+ return out
289
+
290
+
291
+ def _num(value) -> Optional[float]:
292
+ try:
293
+ return float(value)
294
+ except (TypeError, ValueError):
295
+ return None
@@ -0,0 +1,182 @@
1
+ """Macro dashboard: key US / India / global series from FRED's public CSV
2
+ export (no API key), with latest value, 1-month and 1-year change and a
3
+ monthly sparkline. Covers the core of OpenBB's economy / fixedincome
4
+ endpoints that matter to an equity investor.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import io
10
+ import logging
11
+ import threading
12
+ import time
13
+ from concurrent.futures import ThreadPoolExecutor
14
+ from datetime import date, timedelta
15
+ from typing import Callable, Optional
16
+
17
+ import pandas as pd
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ FRED_CSV = "https://fred.stlouisfed.org/graph/fredgraph.csv?id={id}&cosd={start}"
22
+ CACHE_SECONDS = 6 * 3600
23
+ YEARS = 5
24
+
25
+ # id, label, group, unit, transform (level | yoy), note
26
+ SERIES = [
27
+ ("CPIAUCSL", "US inflation (CPI)", "us", "%", "yoy", "Consumer prices vs a year ago"),
28
+ ("FEDFUNDS", "Fed funds rate", "us", "%", "level", "US policy rate"),
29
+ ("DGS10", "US 10-year yield", "us", "%", "level", "Long-term borrowing cost; higher weighs on stock valuations"),
30
+ ("DGS2", "US 2-year yield", "us", "%", "level", "Tracks where the Fed is expected to go"),
31
+ ("T10Y2Y", "US yield curve (10y - 2y)", "us", "pts", "level", "Below 0 (inverted) has preceded most US recessions"),
32
+ ("UNRATE", "US unemployment", "us", "%", "level", "Share of the labour force out of work"),
33
+ ("INDIRLTLT01STM", "India 10-year yield", "india", "%", "level", "Government bond yield"),
34
+ ("IRSTCI01INM156N", "India call money rate", "india", "%", "level", "Overnight interbank rate, tracks RBI policy"),
35
+ ("DEXINUS", "USD / INR", "india", "₹", "level", "Rupees per dollar; higher = weaker rupee"),
36
+ ("DCOILBRENTEU", "Brent crude", "global", "$", "level", "Oil price; a big cost driver for India"),
37
+ ]
38
+
39
+ Fetch = Callable[[str, date], pd.Series]
40
+
41
+ _cache: dict[str, tuple[float, dict]] = {}
42
+ _lock = threading.Lock()
43
+
44
+
45
+ def _fred(series_id: str, start: date) -> pd.Series:
46
+ import requests
47
+
48
+ # FRED stalls requests with an unknown custom User-Agent; the library default works.
49
+ resp = requests.get(FRED_CSV.format(id=series_id, start=start.isoformat()), timeout=20)
50
+ resp.raise_for_status()
51
+ df = pd.read_csv(io.StringIO(resp.text))
52
+ s = pd.to_numeric(df.iloc[:, 1], errors="coerce")
53
+ s.index = pd.to_datetime(df.iloc[:, 0])
54
+ return s.dropna()
55
+
56
+
57
+ def _asof(s: pd.Series, when: pd.Timestamp) -> Optional[float]:
58
+ past = s[s.index <= when]
59
+ return float(past.iloc[-1]) if len(past) else None
60
+
61
+
62
+ def _fetch_one(fetch: Fetch, spec: tuple, start: date) -> tuple:
63
+ """(spec, series) for one FRED series; series is None when the fetch fails."""
64
+ try:
65
+ return spec, fetch(spec[0], start)
66
+ except Exception as exc:
67
+ logger.warning("FRED %s failed: %s", spec[0], exc)
68
+ return spec, None
69
+
70
+
71
+ def shape(series_id: str, label: str, group: str, unit: str, transform: str, note: str, raw: pd.Series) -> Optional[dict]:
72
+ s = raw.sort_index()
73
+ if transform == "yoy":
74
+ monthly = s.resample("MS").last().dropna()
75
+ s = (monthly / monthly.shift(12) - 1).dropna() * 100
76
+ if s.empty:
77
+ return None
78
+ last_day = s.index[-1]
79
+ value = float(s.iloc[-1])
80
+ m1, y1 = _asof(s, last_day - pd.DateOffset(months=1)), _asof(s, last_day - pd.DateOffset(years=1))
81
+ spark = s.resample("MS").last().dropna().tail(36)
82
+ return {"id": series_id, "label": label, "group": group, "unit": unit, "note": note,
83
+ "value": value, "date": str(last_day.date()),
84
+ "change_1m": value - m1 if m1 is not None else None,
85
+ "change_1y": value - y1 if y1 is not None else None,
86
+ "spark": [{"date": str(i.date()), "value": float(v)} for i, v in spark.items()]}
87
+
88
+
89
+ def build(fetch: Fetch = _fred, use_cache: bool = True) -> dict:
90
+ if use_cache:
91
+ with _lock:
92
+ hit = _cache.get("all")
93
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
94
+ return hit[1]
95
+ start = date.today() - timedelta(days=365 * YEARS + 400) # extra year for YoY
96
+
97
+ with ThreadPoolExecutor(max_workers=6) as pool:
98
+ results = list(pool.map(lambda spec: _fetch_one(fetch, spec, start), SERIES))
99
+ series, failed = [], []
100
+ for spec, raw in results:
101
+ item = shape(*spec, raw) if raw is not None and len(raw) else None
102
+ (series.append(item) if item else failed.append(spec[1]))
103
+ curve = next((s for s in series if s["id"] == "T10Y2Y"), None)
104
+ out = {"series": series, "failed": failed,
105
+ "signals": {"us_curve_inverted": curve["value"] < 0 if curve else None},
106
+ "source": "Official central bank and government statistics"}
107
+ if use_cache and series:
108
+ with _lock:
109
+ _cache["all"] = (time.time(), out)
110
+ return out
111
+
112
+
113
+ def context_lines(data: Optional[dict] = None) -> list[str]:
114
+ """Short text lines for LLM prompts (agent context)."""
115
+ data = data or build()
116
+ lines = []
117
+ for s in data["series"]:
118
+ chg = f", {s['change_1y']:+.2f} over 1y" if s.get("change_1y") is not None else ""
119
+ lines.append(f"{s['label']}: {s['value']:.2f}{'' if s['unit'] in ('₹', '$') else s['unit']} ({s['date']}{chg})")
120
+ return lines
121
+
122
+
123
+ def cached_context() -> list[str]:
124
+ """Macro lines for prompts without ever blocking: uses the cache, and if it
125
+ is cold, warms it on a background thread and returns nothing this time."""
126
+ with _lock:
127
+ hit = _cache.get("all")
128
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
129
+ return context_lines(hit[1]) + _india_cached()
130
+ threading.Thread(target=build, name="macro-warm", daemon=True).start()
131
+ return []
132
+
133
+
134
+ def _india_cached() -> list[str]:
135
+ """RBI / IBJA lines if that cache is warm; never fetches."""
136
+ from . import india_rates
137
+
138
+ with india_rates._lock:
139
+ hit = india_rates._cache.get("all")
140
+ if hit and time.time() - hit[0] < india_rates.CACHE_SECONDS:
141
+ return india_rates.context_lines(hit[1])
142
+ return []
143
+
144
+
145
+ # US Treasury constant-maturity yields, short to long (FRED, daily).
146
+ CURVE = [("DGS1MO", "1M", 1 / 12), ("DGS3MO", "3M", 0.25), ("DGS6MO", "6M", 0.5), ("DGS1", "1Y", 1),
147
+ ("DGS2", "2Y", 2), ("DGS3", "3Y", 3), ("DGS5", "5Y", 5), ("DGS7", "7Y", 7),
148
+ ("DGS10", "10Y", 10), ("DGS20", "20Y", 20), ("DGS30", "30Y", 30)]
149
+
150
+
151
+ def yield_curve(fetch: Fetch = _fred, use_cache: bool = True) -> dict:
152
+ """US Treasury curve today vs 1 month and 1 year ago, plus its shape."""
153
+ if use_cache:
154
+ with _lock:
155
+ hit = _cache.get("curve")
156
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
157
+ return hit[1]
158
+ start = date.today() - timedelta(days=400)
159
+
160
+ with ThreadPoolExecutor(max_workers=6) as pool:
161
+ got = [(spec, s) for spec, s in pool.map(lambda spec: _fetch_one(fetch, spec, start), CURVE) if s is not None and len(s)]
162
+ if not got:
163
+ return {"points": [], "as_of": None}
164
+ as_of = min(s.index[-1] for _, s in got) # a date every tenor has printed
165
+ points = []
166
+ for (sid, label, years), s in got:
167
+ points.append({"id": sid, "tenor": label, "years": years,
168
+ "now": _asof(s, as_of),
169
+ "month_ago": _asof(s, as_of - pd.Timedelta(days=30)),
170
+ "year_ago": _asof(s, as_of - pd.Timedelta(days=365))})
171
+ by = {p["tenor"]: p["now"] for p in points}
172
+ shape_ = None
173
+ if by.get("3M") is not None and by.get("10Y") is not None:
174
+ spread = by["10Y"] - by["3M"]
175
+ shape_ = {"spread_10y_3m": spread,
176
+ "label": "inverted" if spread < 0 else "flat" if spread < 0.5 else "normal"}
177
+ out = {"points": points, "as_of": str(as_of.date()), "shape": shape_,
178
+ "source": "Official US Treasury constant-maturity yields"}
179
+ if use_cache:
180
+ with _lock:
181
+ _cache["curve"] = (time.time(), out)
182
+ return out