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,36 @@
1
+ """Hard deadlines for third-party calls that can hang (Yahoo's `.info`,
2
+ option chains, batch downloads have no reliable timeout of their own).
3
+
4
+ The call runs on a daemon thread: if it overruns, the caller gets
5
+ TimeoutError right away and the stuck thread can't delay a reload or
6
+ process exit (a ThreadPoolExecutor's threads would)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextvars
11
+ import threading
12
+ from typing import Any, Callable, TypeVar
13
+
14
+ T = TypeVar("T")
15
+
16
+
17
+ def call_with_deadline(fn: Callable[..., T], seconds: float, *args: Any, **kwargs: Any) -> T:
18
+ box: dict[str, Any] = {}
19
+ done = threading.Event()
20
+
21
+ def run() -> None:
22
+ try:
23
+ box["value"] = fn(*args, **kwargs)
24
+ except BaseException as exc: # re-raised in the caller's thread
25
+ box["error"] = exc
26
+ finally:
27
+ done.set()
28
+
29
+ # Copy the caller's context so the per-user data scope (marketmyna/core/tenant.py) follows the call.
30
+ ctx = contextvars.copy_context()
31
+ threading.Thread(target=ctx.run, args=(run,), name=f"deadline:{getattr(fn, '__name__', 'call')}", daemon=True).start()
32
+ if not done.wait(seconds):
33
+ raise TimeoutError(f"{getattr(fn, '__name__', 'call')} took longer than {seconds:.0f}s")
34
+ if "error" in box:
35
+ raise box["error"]
36
+ return box["value"]
@@ -0,0 +1,52 @@
1
+ """Mask secrets in every log line, tracebacks included.
2
+
3
+ A failed Telegram call raises an error whose text carries the request URL,
4
+ and that URL holds the bot token (api.telegram.org/bot<token>/...); a plain
5
+ `logger.exception` then wrote the token to the server logs. The same goes for
6
+ AI keys echoed back by a provider. `install()` wraps the log record factory,
7
+ so every record from every logger is cleaned before any handler sees it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import re
14
+ import traceback
15
+
16
+ SECRETS = re.compile(
17
+ r"(?<=/bot)\d{5,}:[\w-]{20,}" # Telegram bot token in an API URL
18
+ r"|\b\d{8,12}:AA[\w-]{30,}" # Telegram bot token on its own
19
+ r"|AIza[\w-]{10,}|gsk_\w{10,}|sk-[\w-]{10,}" # AI provider keys
20
+ r"|(?<=key=)[^&\s\"']+|(?<=Bearer )[\w.-]+"
21
+ )
22
+
23
+ _installed = False
24
+
25
+
26
+ def redact(text: str) -> str:
27
+ return SECRETS.sub("***", text)
28
+
29
+
30
+ def install() -> None:
31
+ """Idempotent; safe to call from every entry point."""
32
+ global _installed
33
+ if _installed:
34
+ return
35
+ make = logging.getLogRecordFactory()
36
+
37
+ def factory(*args, **kwargs):
38
+ record = make(*args, **kwargs)
39
+ try:
40
+ message = record.getMessage()
41
+ except Exception: # noqa: BLE001 - a bad format string is the handler's problem, not ours
42
+ return record
43
+ clean = redact(message)
44
+ if clean != message:
45
+ record.msg, record.args = clean, None
46
+ if record.exc_info and record.exc_info[0] is not None:
47
+ # Formatters reuse exc_text when it is set, so the raw traceback is never printed.
48
+ record.exc_text = redact("".join(traceback.format_exception(*record.exc_info)).rstrip("\n"))
49
+ return record
50
+
51
+ logging.setLogRecordFactory(factory)
52
+ _installed = True
@@ -0,0 +1,183 @@
1
+ """One shared pace for every Yahoo Finance request on this server.
2
+
3
+ Yahoo throttles by IP, not by process. The web app, every scheduled-report
4
+ worker and the background jobs each run their own yfinance, so before this
5
+ gate a report wave of 4 workers fired requests in bursts and one 429 in one
6
+ worker said nothing to the others, which kept hammering and extended the ban.
7
+
8
+ - Pacing (scheduled-report workers only; web requests are never delayed):
9
+ a token bucket shared by all worker processes (a small state file under
10
+ a file lock). Up to `BURST` requests go straight through, so a page that
11
+ fetches dozens of tickers at once is not slowed; beyond that, requests
12
+ are released at `RATE` per second. (A strict gap between every request
13
+ queued page loads behind each other: 80 tickers took 16 s.)
14
+ - Shared cooldown: a 429 anywhere pauses Yahoo for every process for
15
+ `COOLDOWN` seconds. Scheduled workers (`patient()`) wait it out, up to
16
+ `MAX_WAIT`; web requests fail fast with yfinance's own YFRateLimitError, so
17
+ pages fall back to caches instead of hanging.
18
+
19
+ `install()` wraps yfinance's single request method (YfData._make_request), so
20
+ every Ticker / download / search call goes through the gate without touching
21
+ the call sites. Without fcntl (Windows) the gate works per process only.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import functools
27
+ import logging
28
+ import os
29
+ import threading
30
+ import time
31
+ from pathlib import Path
32
+
33
+ from marketmyna.core.home import app_home
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ RATE = float(os.environ.get("YAHOO_RATE", 10)) # sustained requests per second
38
+ BURST = float(os.environ.get("YAHOO_BURST", 60))
39
+ COOLDOWN = float(os.environ.get("YAHOO_COOLDOWN", 30))
40
+ MAX_WAIT = float(os.environ.get("YAHOO_MAX_WAIT", 90))
41
+
42
+ try:
43
+ import fcntl
44
+ except ImportError: # pragma: no cover - Windows
45
+ fcntl = None
46
+ try:
47
+ import msvcrt # Windows file locks
48
+ except ImportError:
49
+ msvcrt = None
50
+
51
+ _local_lock = threading.Lock()
52
+ _local_state = [0.0, 0.0] # bucket clock (see reserve), cooldown until (no-fcntl fallback)
53
+ _patient = False
54
+ _installed = False
55
+
56
+
57
+ def state_path() -> Path:
58
+ return Path(os.environ.get("YAHOO_GATE_FILE") or app_home() / "yahoo_gate")
59
+
60
+
61
+ def patient(on: bool = True) -> None:
62
+ """This process waits out a shared cooldown instead of failing fast
63
+ (scheduled-report workers: a late report beats a failed one)."""
64
+ global _patient
65
+ _patient = on
66
+
67
+
68
+ def _lock(f) -> None:
69
+ if fcntl is not None:
70
+ fcntl.flock(f, fcntl.LOCK_EX)
71
+ else: # Windows: lock the first byte (LK_LOCK retries for about 10 s, then raises OSError)
72
+ f.seek(0)
73
+ msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
74
+
75
+
76
+ def _unlock(f) -> None:
77
+ if fcntl is not None:
78
+ fcntl.flock(f, fcntl.LOCK_UN)
79
+ else:
80
+ f.seek(0)
81
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
82
+
83
+
84
+ def _update(change):
85
+ """Run change(clock, cooldown_until) -> (clock, cooldown_until, result)
86
+ under the cross-process lock and return result."""
87
+ if fcntl is not None or msvcrt is not None:
88
+ try:
89
+ path = state_path()
90
+ path.parent.mkdir(parents=True, exist_ok=True)
91
+ with open(path, "a+", encoding="utf-8") as f:
92
+ _lock(f)
93
+ try:
94
+ f.seek(0)
95
+ parts = f.read().split()
96
+ try:
97
+ nxt, cool = float(parts[0]), float(parts[1])
98
+ except (IndexError, ValueError):
99
+ nxt, cool = 0.0, 0.0
100
+ nxt, cool, result = change(nxt, cool)
101
+ f.seek(0)
102
+ f.truncate()
103
+ f.write(f"{nxt:.3f} {cool:.3f}")
104
+ f.flush()
105
+ return result
106
+ finally:
107
+ _unlock(f)
108
+ except OSError as exc:
109
+ logger.debug("Yahoo gate file unavailable, pacing this process only: %s", exc)
110
+ with _local_lock:
111
+ nxt, cool, result = change(*_local_state)
112
+ _local_state[:] = [nxt, cool]
113
+ return result
114
+
115
+
116
+ def reserve() -> float:
117
+ """Book the next request slot; returns seconds to sleep before sending.
118
+ Raises YFRateLimitError while Yahoo is cooling down (unless patient)."""
119
+ now = time.time()
120
+
121
+ # The bucket is kept as one number, `clock`: the moment it would be full
122
+ # again. Each request pushes it 1/RATE later; a request may go once the
123
+ # clock is no more than BURST/RATE ahead of now.
124
+ def change(clock, cool):
125
+ if cool > now and (not _patient or cool - now > MAX_WAIT):
126
+ return clock, cool, -(cool - now)
127
+ if not _patient:
128
+ # Web requests are never paced (a page waits on them); only the
129
+ # scheduled-report workers, which burst for minutes, use the bucket.
130
+ return clock, cool, 0.0
131
+ start = max(now, cool)
132
+ clock = max(clock, start) + 1.0 / RATE
133
+ return clock, cool, max(0.0, clock - start - BURST / RATE) + (start - now)
134
+
135
+ wait = _update(change)
136
+ if wait < 0:
137
+ from yfinance.exceptions import YFRateLimitError
138
+
139
+ logger.info("Yahoo cooling down for another %.0fs after a 429; not sending", -wait)
140
+ raise YFRateLimitError()
141
+ return wait
142
+
143
+
144
+ def record_limit() -> None:
145
+ """Yahoo said 429: every process on this server pauses for COOLDOWN."""
146
+ until = time.time() + COOLDOWN
147
+
148
+ def change(clock, cool):
149
+ return clock, max(cool, until), None
150
+
151
+ _update(change)
152
+ logger.warning("Yahoo rate limit hit; pausing Yahoo requests for %.0fs on this server", COOLDOWN)
153
+
154
+
155
+ def install() -> None:
156
+ """Route every yfinance HTTP request through the gate (idempotent)."""
157
+ global _installed
158
+ if _installed:
159
+ return
160
+ try:
161
+ from yfinance.data import YfData
162
+ from yfinance.exceptions import YFRateLimitError
163
+ except Exception as exc: # noqa: BLE001 - yfinance missing or reshaped: run ungated
164
+ logger.warning("Yahoo gate not installed: %s", exc)
165
+ return
166
+ original = YfData._make_request
167
+
168
+ @functools.wraps(original)
169
+ def paced(self, *args, **kwargs):
170
+ wait = reserve()
171
+ if wait > 0:
172
+ time.sleep(wait)
173
+ try:
174
+ response = original(self, *args, **kwargs)
175
+ except YFRateLimitError:
176
+ record_limit()
177
+ raise
178
+ if getattr(response, "status_code", None) == 429:
179
+ record_limit()
180
+ return response
181
+
182
+ YfData._make_request = paced
183
+ _installed = True
@@ -0,0 +1,12 @@
1
+ """One process-wide lock for yfinance.download().
2
+
3
+ yf.download keeps its results in module-level state (yfinance.shared), so two
4
+ batch downloads running at once on different threads can hand each other's
5
+ rows back: a 1-year commodities fetch and a 2-year crude fetch overlapping
6
+ returned 1-year Brent to the 2-year caller. Every batch download goes through
7
+ this lock; single-ticker Ticker().history() calls do not need it.
8
+ """
9
+
10
+ import threading
11
+
12
+ YF_DOWNLOAD_LOCK = threading.Lock()
Binary file
@@ -0,0 +1 @@
1
+ import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{b as t,n,p as r,r as i}from"./format-d8InM0Zw.js";import{t as a}from"./toast-DDradTZw.js";import{D as o,_ as s,a as c,c as l,f as u,g as d,i as f,o as p,p as m,r as h}from"./ui-gsFb8B2m.js";import{C as g,D as _,_ as v,b as y,g as b,j as x,y as S}from"./index-CKAnq-Or.js";var C=e(t(),1),w=r(),T=[{value:`profile`,label:`Profile`},{value:`data`,label:`Your data`}];function E(){let e=b(e=>e.me),t=b(e=>e.setMe),[n,r]=x(),i=T,a=i.some(e=>e.value===n.get(`tab`))?n.get(`tab`):`profile`;return(0,C.useEffect)(()=>{g().then(t).catch(()=>{})},[t]),e?(0,w.jsxs)(`div`,{className:`space-y-6`,children:[(0,w.jsx)(m,{title:`Account settings`,description:`Your name, time zone and data.`}),(0,w.jsx)(d,{options:i,value:a,onChange:e=>{let t=new URLSearchParams(n);t.set(`tab`,e),r(t,{replace:!0})}}),a===`profile`&&(0,w.jsx)(D,{}),a===`data`&&(0,w.jsx)(j,{})]}):null}function D(){let e=b(e=>e.me),t=b(e=>e.setMe),[r,o]=(0,C.useState)(e.name),d=e.timezone??`Asia/Kolkata`,m=e.timezone_auto!==!1,[g,y]=(0,C.useState)(d),[x,T]=(0,C.useState)(m),[E,D]=(0,C.useState)(!1),A=S(),j=x&&A?A:g,M=(0,C.useMemo)(()=>{let e=typeof Intl.supportedValuesOf==`function`?Intl.supportedValuesOf(`timeZone`).map(v):[];return[...new Set([g,A,...e])].filter(Boolean).sort()},[g,A]);async function N(e){e.preventDefault(),D(!0);try{t(await _({name:r,timezone:j,timezone_auto:x})),a.success(`Profile saved`)}catch(e){a.error(n(e))}finally{D(!1)}}return(0,w.jsxs)(f,{children:[(0,w.jsx)(p,{title:`Profile`,icon:`user`}),(0,w.jsxs)(c,{className:`space-y-5`,children:[(0,w.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,w.jsx)(`span`,{className:`flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-accent-soft text-lg font-semibold text-accent-fg ring-1 ring-accent/30`,children:i(e.name,e.email)}),(0,w.jsx)(`div`,{className:`min-w-0`,children:(0,w.jsx)(`div`,{className:`truncate text-base font-semibold`,children:e.name||`No name yet`})})]}),(0,w.jsxs)(`form`,{onSubmit:N,className:`grid gap-3 sm:grid-cols-[1fr_1fr_auto] sm:items-start`,children:[(0,w.jsx)(l,{label:`Name`,hint:`Shown in the menu.`,children:(0,w.jsx)(u,{value:r,maxLength:80,autoComplete:`name`,onChange:e=>o(e.target.value)})}),(0,w.jsx)(l,{label:(0,w.jsxs)(`span`,{className:`flex items-center justify-between gap-2`,children:[`Time zone`,(0,w.jsx)(k,{checked:x,disabled:!A,onChange:e=>{T(e),!e&&A&&y(A)}})]}),hint:x?`From this device's clock: now ${O(j)}. Changes when you travel.`:`Scheduled report times are in this zone: now ${O(j)}.`,children:x?(0,w.jsx)(u,{value:j.replace(/_/g,` `),readOnly:!0,"aria-readonly":!0,className:`cursor-default text-muted`}):(0,w.jsx)(s,{value:g,onChange:e=>y(e.target.value),options:M.map(e=>({value:e,label:e.replace(/_/g,` `)}))})}),(0,w.jsx)(h,{type:`submit`,variant:`secondary`,className:`sm:mt-[calc(var(--text-xs--line-height)+0.375rem)]`,loading:E,disabled:r.trim()===e.name.trim()&&j===d&&x===m,children:`Save`})]})]})]})}function O(e){try{return new Intl.DateTimeFormat(void 0,{timeZone:e,hour:`numeric`,minute:`2-digit`,timeZoneName:`short`}).format(new Date)}catch{return`—`}}function k({checked:e,disabled:t,onChange:n}){return(0,w.jsxs)(`button`,{type:`button`,role:`switch`,"aria-checked":e,disabled:t,title:t?`This browser doesn't share its time zone`:`Follow this device's time zone`,onClick:t=>{t.preventDefault(),n(!e)},className:`inline-flex items-center gap-1.5 text-xs font-normal text-muted hover:text-fg disabled:opacity-50`,children:[`Set automatically`,(0,w.jsx)(`span`,{className:o(`relative h-4 w-7 shrink-0 rounded-full transition-colors`,e?`bg-accent`:`bg-line-strong`),children:(0,w.jsx)(`span`,{className:o(`absolute top-0.5 start-0 h-3 w-3 rounded-full bg-white transition-transform`,e?`translate-x-3.5`:`translate-x-0.5`)})})]})}function A(){let[e,t]=(0,C.useState)(!1);async function r(){t(!0);try{await y(),a.success(`Your data file is downloading.`)}catch(e){a.error(n(e))}finally{t(!1)}}return(0,w.jsx)(h,{variant:`secondary`,icon:`download`,loading:e,onClick:r,children:`Download my data`})}function j(){return(0,w.jsxs)(f,{children:[(0,w.jsx)(p,{title:`Your data`,icon:`download`,description:`Download a copy of everything MarketMyna stores about you, whenever you like.`}),(0,w.jsxs)(c,{className:`space-y-3`,children:[(0,w.jsx)(`p`,{className:`text-sm text-muted`,children:`One JSON file with your profile, plan and payments, portfolios and transactions, watchlists, alerts, scheduled report settings and usage history. Passwords, login tokens and your saved AI keys are never included.`}),(0,w.jsx)(A,{})]})]})}export{E as default};