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,564 @@
1
+ """Market-wide listing endpoints: browse/search every stock, ETF and mutual
2
+ fund per market (screener-style), backed by marketmyna/data/listings.py."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Annotated, Optional
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, Query
9
+ from pydantic import BaseModel, Field
10
+
11
+ from marketmyna.api.deps import get_config_dep
12
+ from marketmyna.data.listings import MARKET_LABELS, get_store, query
13
+ from marketmyna.data.mutual_funds import MutualFundDataFetcher
14
+
15
+ router = APIRouter()
16
+
17
+ _MARKETS = set(MARKET_LABELS) | {"all", "global"}
18
+ _TYPES = {"all", "stock", "etf", "mf", "crypto"}
19
+
20
+ _mf = MutualFundDataFetcher()
21
+
22
+
23
+ def _market(market: str) -> str:
24
+ market = market.lower()
25
+ if market not in _MARKETS:
26
+ raise HTTPException(status_code=400, detail=f"Unknown market '{market}'")
27
+ return market
28
+
29
+
30
+ def _csv(value: str) -> list[str]:
31
+ return [v.strip() for v in value.split(",") if v.strip()]
32
+
33
+
34
+ @router.get("/listings")
35
+ def list_instruments(
36
+ market: str = Query(default="india"),
37
+ type: str = Query(default="all", description="all | stock | etf | mf | crypto"),
38
+ q: str = Query(default="", max_length=100),
39
+ sector: str = Query(default=""),
40
+ exchange: str = Query(default=""),
41
+ plan: str = Query(default="", description="Mutual funds: Direct | Regular"),
42
+ option: str = Query(default="", description="Mutual funds: Growth | IDCW"),
43
+ min_cap: Optional[float] = Query(default=None),
44
+ max_cap: Optional[float] = Query(default=None),
45
+ category: str = Query(default="", description="Comma-separated broad categories, e.g. Technology,Agriculture"),
46
+ cap: str = Query(default="", description="Comma-separated size bands: large,mid,small,micro"),
47
+ shariah: str = Query(default="", description="Comma-separated Shariah screen statuses: halal,doubtful,haram,unknown"),
48
+ price_min: Optional[float] = Query(default=None, ge=0),
49
+ price_max: Optional[float] = Query(default=None, ge=0),
50
+ change_min: Optional[float] = Query(default=None, description="Day change %, lower bound"),
51
+ change_max: Optional[float] = Query(default=None, description="Day change %, upper bound"),
52
+ near_high: Optional[float] = Query(default=None, ge=0, le=100, description="Within N% of 52-week high"),
53
+ near_low: Optional[float] = Query(default=None, ge=0, le=100, description="Within N% of 52-week low"),
54
+ min_volume: Optional[float] = Query(default=None, ge=0),
55
+ max_volume: Optional[float] = Query(default=None, ge=0),
56
+ markets: str = Query(default="", description="All-markets view: comma-separated markets to keep"),
57
+ sip_edge: str = Query(default="", description="Funds / ETFs: consistent | no_edge | short (historically best SIP day)"),
58
+ sort: str = Query(default="market_cap"),
59
+ order: str = Query(default="desc", pattern="^(asc|desc)$"),
60
+ offset: int = Query(default=0, ge=0),
61
+ limit: int = Query(default=50, ge=1, le=500),
62
+ ) -> dict:
63
+ market = _market(market)
64
+ if type not in _TYPES:
65
+ raise HTTPException(status_code=400, detail=f"Unknown type '{type}'")
66
+ store = get_store()
67
+ from marketmyna.services import sip_days
68
+
69
+ rows = store.rows(market)
70
+ sip_days.annotate(rows, market)
71
+ if sip_edge:
72
+ rows = [r for r in rows if r.get("sip_edge") in set(_csv(sip_edge))]
73
+ result = query(
74
+ rows,
75
+ q=q,
76
+ type_=type,
77
+ sector=sector,
78
+ exchange=exchange,
79
+ plan=plan,
80
+ option=option,
81
+ min_cap=min_cap,
82
+ max_cap=max_cap,
83
+ categories=_csv(category),
84
+ cap_sizes=_csv(cap),
85
+ shariah=_csv(shariah),
86
+ price_min=price_min,
87
+ price_max=price_max,
88
+ change_min=change_min,
89
+ change_max=change_max,
90
+ near_high=near_high,
91
+ near_low=near_low,
92
+ min_volume=min_volume,
93
+ max_volume=max_volume,
94
+ markets=_csv(markets),
95
+ sort=sort,
96
+ order=order,
97
+ offset=offset,
98
+ limit=limit,
99
+ )
100
+ result["sources"] = [s for s in store.status() if s["source"] in store.keys_for(market)]
101
+ return result
102
+
103
+
104
+ # Typeahead answers with the markets loaded by then; slower ones join later searches.
105
+ SEARCH_WAIT = 8.0
106
+
107
+
108
+ @router.get("/listings/search")
109
+ def search_instruments(
110
+ q: str = Query(..., min_length=1, max_length=100),
111
+ market: str = Query(default="all"),
112
+ type: str = Query(default="all"),
113
+ limit: int = Query(default=10, ge=1, le=50),
114
+ ) -> list[dict]:
115
+ """Typeahead: best matches by relevance, then size."""
116
+ market = _market(market)
117
+ rows = get_store().rows(market, wait=SEARCH_WAIT)
118
+ return query(rows, q=q, type_=type if type in _TYPES else "all", limit=limit)["rows"]
119
+
120
+
121
+ @router.get("/listings/discovery")
122
+ def market_discovery(
123
+ market: str = Query(default="india"),
124
+ size: int = Query(default=500, ge=20, le=3000, description="Rank the N largest names"),
125
+ ) -> dict:
126
+ """Index snapshot, breadth, gainers / losers / most active, 52-week extremes, sectors."""
127
+ from marketmyna.services import live_market
128
+ from marketmyna.services.discovery import DISCOVERY_MARKETS, build
129
+
130
+ market = market.lower()
131
+ if market not in DISCOVERY_MARKETS:
132
+ raise HTTPException(status_code=400, detail=f"Discovery not available for '{market}'")
133
+ return build(market, size, live=live_market.market(market))
134
+
135
+
136
+ @router.get("/listings/hot")
137
+ def hot_stocks(
138
+ market: str = Query(default="india"),
139
+ area: str = Query(default="trader", pattern="^(trader|investor)$"),
140
+ ) -> dict:
141
+ """Hot stocks for the login area: what is moving today (trader) or the
142
+ businesses being re-priced with fundamentals behind them (investor)."""
143
+ from marketmyna.services import hot_stocks as hot
144
+ from marketmyna.services import live_market
145
+ from marketmyna.services.discovery import DISCOVERY_MARKETS
146
+ from marketmyna.utils.deadline import call_with_deadline
147
+
148
+ market = market.lower()
149
+ if market not in DISCOVERY_MARKETS:
150
+ raise HTTPException(status_code=400, detail=f"Hot stocks not available for '{market}'")
151
+ live = live_market.market(market)
152
+ if area == "trader":
153
+ return hot.trader(market, live=live)
154
+ return hot.with_live_prices(call_with_deadline(hot.investor, 90, market), live)
155
+
156
+
157
+ @router.get("/listings/rotation")
158
+ def sector_rotation(market: str = Query(default="india")) -> dict:
159
+ """Relative rotation of sector baskets vs the market index (RRG), with
160
+ today's live prices moving the current week's point."""
161
+ from marketmyna.services import live_market
162
+ from marketmyna.services.analysis_run import MARKET_INDICES
163
+ from marketmyna.services.rotation import ROTATION_MARKETS, build
164
+
165
+ market = market.lower()
166
+ if market not in ROTATION_MARKETS:
167
+ raise HTTPException(status_code=400, detail=f"Sector rotation not available for '{market}'")
168
+
169
+ def run() -> dict:
170
+ live = live_market.market(market)
171
+ prices = live_market.for_tickers(market, live["quotes"])
172
+ if market != "crypto":
173
+ prices.update(live_market.latest([MARKET_INDICES[market][0][0]]))
174
+ return {**build(market, live=prices), "live": live["status"]}
175
+
176
+ try:
177
+ return live_market.memo(("rotation", market), run)
178
+ except ValueError as exc:
179
+ raise HTTPException(status_code=503, detail=str(exc))
180
+
181
+
182
+ @router.get("/listings/deals")
183
+ def nse_deals(days: int = Query(default=1, ge=1, le=90)) -> dict:
184
+ """NSE bulk and block deals over the last N days (history builds up daily)."""
185
+ from marketmyna.data.nse_deals import recent
186
+
187
+ return recent(days)
188
+
189
+
190
+ @router.get("/listings/live")
191
+ def live_quotes(market: str, symbols: str = Query(..., max_length=2000)) -> dict:
192
+ """Latest prices for the rows on screen; the page asks every second.
193
+ Served from a shared cache that refreshes upstream every few seconds
194
+ (marketmyna/services/live_quotes.py), so polling stays cheap."""
195
+ from marketmyna.services import live_quotes as live
196
+
197
+ if market not in MARKET_LABELS:
198
+ raise HTTPException(status_code=400, detail=f"Unknown market '{market}'")
199
+ return live.get(market, symbols.split(","))
200
+
201
+
202
+ _KNOWN: dict[str, tuple[float, frozenset]] = {}
203
+ KNOWN_TTL = 600.0
204
+
205
+
206
+ def _listed(market: str, symbol: str) -> bool:
207
+ """Whether `symbol` is a listing of `market` (set rebuilt every 10 min).
208
+ Unlisted text never joins the live watch list, where every extra symbol
209
+ slows the shared per-second refresh. With no listings loaded, allow."""
210
+ import time
211
+
212
+ hit = _KNOWN.get(market)
213
+ if hit is None or time.monotonic() - hit[0] > KNOWN_TTL:
214
+ hit = (time.monotonic(), frozenset(str(r.get("symbol", "")).upper() for r in get_store().rows(market)))
215
+ _KNOWN[market] = hit
216
+ return not hit[1] or symbol in hit[1]
217
+
218
+
219
+ @router.get("/listings/activity")
220
+ def trading_activity(market: str, symbol: str = Query(..., max_length=40)) -> dict:
221
+ """Live trading activity for one stock page: price and volume from the
222
+ shared live cache, plus for India the broker's pending buy / sell
223
+ quantity and best five bids / offers, and last session's delivery %."""
224
+ from marketmyna.services import live_quotes as live
225
+
226
+ if market not in MARKET_LABELS:
227
+ raise HTTPException(status_code=400, detail=f"Unknown market '{market}'")
228
+ sym = symbol.strip().upper()
229
+ if market == "india":
230
+ sym = sym.removesuffix(".NS").removesuffix(".BO")
231
+ elif market == "hong_kong":
232
+ sym = sym.removesuffix(".HK").zfill(5) # listings use 00700
233
+ elif market == "china_a":
234
+ sym = sym.removesuffix(".SS").removesuffix(".SZ")
235
+ quotes = live.get(market, [sym] if _listed(market, sym) else [], depth=True)
236
+ out = {k: quotes[k] for k in ("market", "open", "age_seconds", "refresh_seconds", "source", "error")}
237
+ out["symbol"], out["quote"] = sym, quotes["quotes"].get(sym)
238
+ if market == "india":
239
+ from marketmyna.services import delivery
240
+
241
+ out["delivery"] = delivery.get(sym)
242
+ return out
243
+
244
+
245
+ @router.get("/listings/etf-rotation")
246
+ def etf_rotation(market: str = Query(default="us"), view: Optional[str] = Query(default=None, max_length=60)) -> dict:
247
+ """ETF rotation vs the home market (us | hong_kong | china_a): the
248
+ non-India counterpart of the mutual-fund rotation."""
249
+ from marketmyna.services import etf_rotation as er
250
+ from marketmyna.services import live_market
251
+ from marketmyna.utils.deadline import call_with_deadline
252
+
253
+ market = market.lower()
254
+
255
+ def run() -> dict:
256
+ prices = live_market.latest(er.tickers(market, view)) if market in er.UNIVERSES else {}
257
+ out = call_with_deadline(er.build, 90, market, view, live=prices)
258
+ return {**out, "live": live_market.status_for(prices)}
259
+
260
+ try:
261
+ return live_market.memo(("etf-rotation", market, view), run)
262
+ except ValueError as exc:
263
+ raise HTTPException(status_code=400, detail=str(exc))
264
+ except TimeoutError:
265
+ raise HTTPException(status_code=503, detail="Still loading ETF prices - try again in a minute.")
266
+
267
+
268
+ class TranslateRequest(BaseModel):
269
+ market: str = "china_a"
270
+ names: list[dict] = Field(default_factory=list, max_length=200) # [{"symbol", "name"}]
271
+ texts: list[str] = Field(default_factory=list, max_length=300)
272
+
273
+
274
+ @router.post("/translate")
275
+ def translate(req: TranslateRequest) -> dict:
276
+ """English for Chinese stock names (official English name from Yahoo,
277
+ cached) and free text such as broker branches (glossary + pinyin)."""
278
+ from marketmyna.services import translate as tr
279
+ from marketmyna.utils.deadline import call_with_deadline
280
+
281
+ items = [(str(n.get("symbol", ""))[:20], str(n.get("name", ""))[:200]) for n in req.names
282
+ if n.get("symbol") and tr.has_cjk(str(n.get("name", "")))]
283
+ try:
284
+ names = call_with_deadline(tr.english_names, 60, items, req.market.lower()) if items else {}
285
+ except TimeoutError:
286
+ names = {}
287
+ texts = {t: tr.english_text(t) for t in dict.fromkeys(req.texts) if tr.has_cjk(t)}
288
+ return {"names": names, "texts": texts}
289
+
290
+
291
+ @router.get("/listings/big-trades")
292
+ def big_trades(market: str = Query(default="us"), days: int = Query(default=1, ge=1, le=30)) -> dict:
293
+ """Big trades outside India: US insider Form 4 trades, China block trades,
294
+ HK southbound Stock Connect, crypto big-trader positioning."""
295
+ from marketmyna.services import big_trades as bt
296
+ from marketmyna.services.sec_mdna import SecNotConfigured
297
+ from marketmyna.utils.deadline import call_with_deadline
298
+
299
+ try:
300
+ return call_with_deadline(bt.build, 120, market, days)
301
+ except SecNotConfigured as exc:
302
+ raise HTTPException(status_code=409, detail=str(exc))
303
+ except ValueError as exc:
304
+ raise HTTPException(status_code=400, detail=str(exc))
305
+ except TimeoutError:
306
+ raise HTTPException(status_code=503, detail="Still collecting trades - try again in a minute.")
307
+
308
+
309
+ @router.get("/listings/fund-rotation")
310
+ def fund_rotation(by: str = Query(default="category", pattern="^(category|amc)$"),
311
+ group: Optional[str] = Query(default=None, max_length=120)) -> dict:
312
+ """Leading / weakening / lagging / improving for equity fund categories or
313
+ fund houses vs the Nifty 500; with `group`, each fund inside it."""
314
+ from marketmyna.services import fund_rotation as fr
315
+ from marketmyna.utils.deadline import call_with_deadline
316
+
317
+ try:
318
+ return call_with_deadline(fr.build, 120, by, group)
319
+ except ValueError as exc:
320
+ raise HTTPException(status_code=404 if group else 503, detail=str(exc))
321
+ except TimeoutError:
322
+ raise HTTPException(status_code=503, detail="Still gathering fund NAVs - try again in a minute.")
323
+
324
+
325
+ @router.post("/listings/refresh")
326
+ def refresh_listings(market: str = Query(default="all")) -> list[dict]:
327
+ market = _market(market)
328
+ store = get_store()
329
+ store.rows(market, force=True)
330
+ return [s for s in store.status() if s["source"] in store.keys_for(market)]
331
+
332
+
333
+ @router.get("/listings/mf/{scheme_code}/history")
334
+ def mutual_fund_history(scheme_code: int, days: int = Query(default=365, ge=7, le=3650)) -> list[dict]:
335
+ df = _mf.get_historical_nav(scheme_code, days=days)
336
+ if df.empty:
337
+ raise HTTPException(status_code=502, detail=f"No NAV history for scheme {scheme_code}")
338
+ return [{"date": d.strftime("%Y-%m-%d"), "nav": float(v)} for d, v in df["nav"].items()]
339
+
340
+
341
+ @router.get("/listings/mf/{scheme_code}/holdings")
342
+ def mutual_fund_holdings(scheme_code: int) -> dict:
343
+ """Stocks (and other assets) a fund holds, from its fund house's official
344
+ monthly portfolio file; index funds fall back to the NSE index list."""
345
+ from marketmyna.services import fund_holdings as fh
346
+
347
+ return fh.for_fund(str(scheme_code))
348
+
349
+
350
+ @router.get("/listings/mf/{scheme_code}/peers")
351
+ def mutual_fund_peers(scheme_code: int) -> dict:
352
+ """The fund against its competitors: same SEBI category and plan, one
353
+ scheme per fund house, ranked on returns and risk from NAV history."""
354
+ from marketmyna.services import fund_peers
355
+ from marketmyna.utils.deadline import call_with_deadline
356
+
357
+ try:
358
+ return call_with_deadline(fund_peers.compare, 90, str(scheme_code))
359
+ except TimeoutError as exc:
360
+ raise HTTPException(status_code=504, detail=str(exc))
361
+
362
+
363
+ @router.get("/listings/mf/{scheme_code}/style")
364
+ def mutual_fund_style(scheme_code: int) -> dict:
365
+ """Style (value / blend / growth, size) from the holdings, and SEBI category rule checks."""
366
+ from marketmyna.services import fund_style
367
+ from marketmyna.utils.deadline import call_with_deadline
368
+
369
+ try:
370
+ return call_with_deadline(fund_style.build, 90, str(scheme_code))
371
+ except TimeoutError as exc:
372
+ raise HTTPException(status_code=504, detail=str(exc))
373
+
374
+
375
+ @router.get("/listings/mf/{scheme_code}/cost")
376
+ def mutual_fund_cost(scheme_code: int) -> dict:
377
+ """Direct vs Regular plan cost (from both plans' NAVs) and how gains are taxed."""
378
+ from marketmyna.services import fund_cost
379
+ from marketmyna.utils.deadline import call_with_deadline
380
+
381
+ try:
382
+ return call_with_deadline(fund_cost.build, 60, str(scheme_code))
383
+ except TimeoutError as exc:
384
+ raise HTTPException(status_code=504, detail=str(exc))
385
+
386
+
387
+ @router.get("/listings/mf/{scheme_code}/overlap")
388
+ def mutual_fund_overlap(scheme_code: int, other: int = Query(...)) -> dict:
389
+ """Stocks two funds hold in common and how much of the money that is."""
390
+ from marketmyna.services import fund_holdings as fh
391
+
392
+ return fh.overlap(str(scheme_code), str(other))
393
+
394
+
395
+ @router.get("/listings/mf/{scheme_code}/analytics")
396
+ def mutual_fund_analytics(scheme_code: int) -> dict:
397
+ """Rolling returns, risk against the category benchmark (total return),
398
+ and drawdowns with recovery times, from the fund's daily NAV history."""
399
+ from marketmyna.services import fund_analytics
400
+ from marketmyna.utils.deadline import call_with_deadline
401
+
402
+ try:
403
+ return call_with_deadline(fund_analytics.build, 90, str(scheme_code))
404
+ except TimeoutError as exc:
405
+ raise HTTPException(status_code=504, detail=str(exc))
406
+
407
+
408
+ @router.post("/listings/mf/{scheme_code}/ai-review")
409
+ def mutual_fund_ai_review(scheme_code: int, refresh: bool = Query(default=False), config=Depends(get_config_dep)) -> dict:
410
+ """AI read of any fund from the figures on its page (an explanation for
411
+ public users, a view for the owner; see marketmyna/services/fund_ai.py)."""
412
+ from marketmyna.llm.channels import llm_configured
413
+ from marketmyna.llm.protocol import GenerationError
414
+ from marketmyna.services import fund_ai
415
+ from marketmyna.utils.deadline import call_with_deadline
416
+
417
+ if not llm_configured(config):
418
+ raise HTTPException(status_code=409, detail="No AI model is set up. Add an AI key under Settings → AI keys.")
419
+ try:
420
+ out = call_with_deadline(lambda: fund_ai.review(str(scheme_code), config, use_cache=not refresh), 180)
421
+ except LookupError as exc:
422
+ raise HTTPException(status_code=404, detail=str(exc))
423
+ except GenerationError as exc:
424
+ raise HTTPException(status_code=402, detail=str(exc))
425
+ except TimeoutError as exc:
426
+ raise HTTPException(status_code=504, detail=str(exc))
427
+ if not out:
428
+ raise HTTPException(status_code=502, detail="The AI model did not return a usable review. Try again.")
429
+ return out
430
+
431
+
432
+ @router.get("/listings/stock/{symbol}/funds")
433
+ def funds_holding_stock(symbol: str) -> dict:
434
+ """Mutual fund schemes holding an NSE stock (loaded fund houses only)."""
435
+ from marketmyna.services import fund_holdings as fh
436
+
437
+ return fh.funds_holding(symbol)
438
+
439
+
440
+ @router.get("/listings/mf/{scheme_code}/sip")
441
+ def mutual_fund_sip(
442
+ scheme_code: int,
443
+ monthly: float = Query(default=5000, gt=0, le=10_000_000),
444
+ years: int = Query(default=5, ge=1, le=30),
445
+ day: int = Query(default=5, ge=1, le=28),
446
+ step_up: float = Query(default=10, ge=0, le=100),
447
+ expected: float = Query(default=12, ge=-50, le=50),
448
+ horizon: int = Query(default=10, ge=1, le=40),
449
+ ) -> dict:
450
+ """SIP backtest on real NAV (vs lumpsum, by SIP day) plus a step-up projection."""
451
+ from marketmyna.services import sip
452
+
453
+ df = _mf.get_historical_nav(scheme_code, days=100_000)
454
+ if df.empty:
455
+ raise HTTPException(status_code=502, detail=f"No NAV history for scheme {scheme_code}")
456
+ return {"backtest": sip.backtest(df["nav"], monthly, years, day),
457
+ "step_up": sip.step_up(monthly, step_up, horizon, expected)}
458
+
459
+
460
+ @router.get("/listings/{market}/{kind}/{symbol}/sip-days")
461
+ def best_sip_day(market: str, kind: str, symbol: str, years: int = Query(default=10, ge=3, le=10)) -> dict:
462
+ """Historically best SIP day for one mutual fund or ETF: the stored
463
+ 3 / 5 / 10-year summary (computed fresh if missing) plus the XIRR of
464
+ every day 1-28 over `years`."""
465
+ from marketmyna.services import sip_days
466
+
467
+ market = _market(market)
468
+ if kind not in ("mf", "etf") or (kind == "mf" and market != "india") or market not in ("india", *sip_days.ETF_MARKETS):
469
+ raise HTTPException(status_code=400, detail="Best SIP day covers Indian mutual funds and ETFs in India, US and Hong Kong")
470
+ series = sip_days.mf_nav(symbol) if kind == "mf" else sip_days.etf_prices(symbol, market)
471
+ if series is None or series.empty:
472
+ raise HTTPException(status_code=502, detail=f"No price history for {symbol}")
473
+ summary = sip_days.analyze(series)
474
+ if summary is None:
475
+ return {"summary": None, "curve": [], "years": years, "note": "Needs at least 3 years of prices."}
476
+ span = (series.index.max() - series.index.min()).days / 365.25
477
+ years = min(years, max(3, int(span)))
478
+ return {"summary": summary, "curve": sip_days.curve(series, years), "years": years}
479
+
480
+
481
+ @router.get("/screens")
482
+ def screen_presets(market: str = Query(default="india"), asset: str = Query(default="stock")) -> list[dict]:
483
+ """Ready-made screens: fundamental for stock markets, price-based for crypto and ETFs."""
484
+ if asset == "etf":
485
+ from marketmyna.services.etf_screens import list_presets as etf_presets
486
+
487
+ return etf_presets()
488
+ if market.lower() == "crypto":
489
+ from marketmyna.services.crypto_screens import list_presets as crypto_presets
490
+
491
+ return crypto_presets()
492
+ from marketmyna.services.screens import list_presets
493
+
494
+ return list_presets()
495
+
496
+
497
+ @router.get("/screens/metrics")
498
+ def screen_metrics() -> list[dict]:
499
+ """Metrics a custom screen can filter on, in groups."""
500
+ from marketmyna.services.screens import catalogue
501
+
502
+ return catalogue()
503
+
504
+
505
+ class ScreenRuleIn(BaseModel):
506
+ field: str = Field(max_length=40)
507
+ op: str = Field(max_length=20)
508
+ value: Optional[float] = None
509
+ value2: Optional[float] = None
510
+
511
+
512
+ class CustomScreenIn(BaseModel):
513
+ market: str = "india"
514
+ rules: list[ScreenRuleIn] = Field(default_factory=list, max_length=12)
515
+ sectors: list[Annotated[str, Field(max_length=80)]] = Field(default_factory=list, max_length=40)
516
+ sort: Optional[str] = Field(default=None, max_length=40)
517
+ descending: bool = True
518
+ columns: list[Annotated[str, Field(max_length=40)]] = Field(default_factory=list, max_length=20)
519
+ large_only: bool = False
520
+
521
+
522
+ @router.post("/screens/custom")
523
+ def run_custom_screen(body: CustomScreenIn) -> dict:
524
+ """Run the user's own rules over the largest stocks of a market."""
525
+ from marketmyna.services import screens
526
+ from marketmyna.utils.deadline import call_with_deadline
527
+
528
+ market = _market(body.market)
529
+ if market not in ("india", "us", "hong_kong", "china_a"):
530
+ raise HTTPException(status_code=400, detail="Custom screens cover India, US, Hong Kong and China A")
531
+ try:
532
+ return call_with_deadline(screens.run_custom, 90, market, [r.model_dump() for r in body.rules], body.sectors,
533
+ body.sort, body.descending, body.columns, body.large_only)
534
+ except screens.RuleError as exc:
535
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
536
+
537
+
538
+ @router.get("/screens/{key}")
539
+ def run_screen(key: str, market: str = Query(default="india"), asset: str = Query(default="stock")) -> dict:
540
+ """Run one preset over the largest stocks (or most-traded coins, or ETFs) of a market."""
541
+ from marketmyna.utils.deadline import call_with_deadline
542
+
543
+ market = _market(market)
544
+ if asset == "etf":
545
+ from marketmyna.services import etf_screens
546
+
547
+ if market not in etf_screens.MARKETS:
548
+ raise HTTPException(status_code=400, detail="ETF screens cover India, US, Hong Kong and China A")
549
+ if key not in etf_screens.PRESETS:
550
+ raise HTTPException(status_code=404, detail=f"Unknown screen '{key}'")
551
+ return call_with_deadline(etf_screens.run, 90, key, market)
552
+ if market == "crypto":
553
+ from marketmyna.services import crypto_screens
554
+
555
+ if key not in crypto_screens.PRESETS:
556
+ raise HTTPException(status_code=404, detail=f"Unknown screen '{key}'")
557
+ return call_with_deadline(crypto_screens.run, 90, key)
558
+ from marketmyna.services.screens import PRESETS, run
559
+
560
+ if key not in PRESETS:
561
+ raise HTTPException(status_code=404, detail=f"Unknown screen '{key}'")
562
+ if market not in ("india", "us", "hong_kong", "china_a"):
563
+ raise HTTPException(status_code=400, detail="Screens cover India, US, Hong Kong, China A and crypto")
564
+ return call_with_deadline(run, 90, key, market)