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,149 @@
1
+ """A pool of long-lived worker processes for scheduled-report analyses.
2
+
3
+ Starting a fresh Python process per stock costs ~5 s of imports (pandas,
4
+ yfinance, litellm...) before any work; a report wave of hundreds of stocks
5
+ would spend a fifth of its time just starting up. These workers import once
6
+ and then take one stock after another.
7
+
8
+ Safety, as with one process per stock:
9
+ - every task has a deadline; a worker past it (or one that died) is killed
10
+ and replaced, and only that task fails;
11
+ - each worker talks to the parent over its own pipe, so killing one never
12
+ corrupts the others' results;
13
+ - a worker is replaced after `max_tasks` tasks, so leaks can't build up.
14
+
15
+ One batch runs at a time (the scheduler's timer waves); the interactive
16
+ "Run now" keeps its own short-lived processes, so it never waits on a wave.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import threading
23
+ import time
24
+ from multiprocessing.connection import wait
25
+ from typing import Callable, Optional
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # (symbol, market, mode, llm_kwargs, sections, user_id, shared)
30
+ Task = tuple
31
+
32
+
33
+ def _worker_main(conn, run: Callable) -> None:
34
+ """Child process: run tasks until told to stop (None) or the pipe closes."""
35
+ while True:
36
+ try:
37
+ msg = conn.recv()
38
+ except (EOFError, OSError):
39
+ return
40
+ if msg is None:
41
+ return
42
+ task_id, task = msg
43
+ try:
44
+ result = run(*task)
45
+ except Exception as exc: # noqa: BLE001 - always answer the parent
46
+ result = {"ok": False, "symbol": task[0], "market": task[1], "error": str(exc) or type(exc).__name__}
47
+ try:
48
+ conn.send((task_id, result))
49
+ except (EOFError, OSError):
50
+ return
51
+
52
+
53
+ class _Worker:
54
+ def __init__(self, ctx, run: Callable):
55
+ parent, child = ctx.Pipe()
56
+ self.conn = parent
57
+ self.process = ctx.Process(target=_worker_main, args=(child, run), daemon=True)
58
+ self.process.start()
59
+ child.close()
60
+ self.tasks = 0
61
+ self.busy: Optional[tuple[int, float]] = None # (task index, deadline)
62
+
63
+ def stop(self, kill: bool = False) -> None:
64
+ try:
65
+ if kill:
66
+ self.process.kill()
67
+ else:
68
+ self.conn.send(None)
69
+ except Exception:
70
+ pass
71
+ self.process.join(timeout=5)
72
+ if self.process.is_alive():
73
+ self.process.kill()
74
+ self.process.join(timeout=5)
75
+ try:
76
+ self.conn.close()
77
+ except Exception:
78
+ pass
79
+
80
+
81
+ class WorkerPool:
82
+ def __init__(self, ctx, run: Callable, size: int, task_timeout: float, max_tasks: int = 200):
83
+ self._ctx, self._run = ctx, run
84
+ self.size = max(1, size)
85
+ self.task_timeout = task_timeout
86
+ self.max_tasks = max_tasks
87
+ self._workers: list[_Worker] = []
88
+ self._lock = threading.Lock()
89
+
90
+ def _ensure(self) -> None:
91
+ self._workers = [w for w in self._workers if w.process.is_alive()]
92
+ while len(self._workers) < self.size:
93
+ self._workers.append(_Worker(self._ctx, self._run))
94
+ while len(self._workers) > self.size: # size lowered by the admin
95
+ self._workers.pop().stop()
96
+
97
+ def resize(self, size: int) -> None:
98
+ self.size = max(1, size)
99
+
100
+ def run_many(self, tasks: list[Task]) -> list[dict]:
101
+ """Runs every task on the pool; results in task order, never raises."""
102
+ with self._lock:
103
+ self._ensure()
104
+ results: list[Optional[dict]] = [None] * len(tasks)
105
+ pending = list(range(len(tasks)))
106
+ pending.reverse()
107
+ while pending or any(w.busy for w in self._workers):
108
+ for w in self._workers:
109
+ if w.busy is None and pending:
110
+ i = pending.pop()
111
+ try:
112
+ w.conn.send((i, tasks[i]))
113
+ w.busy = (i, time.monotonic() + self.task_timeout)
114
+ except Exception as exc:
115
+ results[i] = self._failed(tasks[i], f"worker unavailable: {exc}")
116
+ busy = {w.conn: w for w in self._workers if w.busy}
117
+ for conn in wait(list(busy), timeout=0.5) if busy else []:
118
+ w = busy[conn]
119
+ try:
120
+ i, result = conn.recv()
121
+ except (EOFError, OSError):
122
+ continue # died: handled below
123
+ results[i] = result
124
+ w.busy = None
125
+ w.tasks += 1
126
+ now = time.monotonic()
127
+ for idx, w in enumerate(self._workers):
128
+ if w.busy and (now > w.busy[1] or not w.process.is_alive()):
129
+ i = w.busy[0]
130
+ reason = (f"watchdog timeout after {self.task_timeout:.0f}s" if w.process.is_alive()
131
+ else "worker process exited without a result")
132
+ logger.warning("Scheduled analysis of %s: %s; replacing the worker", tasks[i][0], reason)
133
+ results[i] = self._failed(tasks[i], reason)
134
+ w.stop(kill=True)
135
+ self._workers[idx] = _Worker(self._ctx, self._run)
136
+ elif not w.busy and w.tasks >= self.max_tasks:
137
+ w.stop()
138
+ self._workers[idx] = _Worker(self._ctx, self._run)
139
+ return [r if r is not None else self._failed(t, "no result") for r, t in zip(results, tasks)]
140
+
141
+ @staticmethod
142
+ def _failed(task: Task, error: str) -> dict:
143
+ return {"ok": False, "symbol": task[0], "market": task[1], "error": error}
144
+
145
+ def close(self) -> None:
146
+ with self._lock:
147
+ for w in self._workers:
148
+ w.stop()
149
+ self._workers = []
@@ -0,0 +1,208 @@
1
+ """World markets: major stock indices by region, currencies, US Treasury
2
+ yields and fear gauges, each with 1-day / 1-month / 1-year change and a
3
+ 3-month sparkline.
4
+
5
+ Prices are Yahoo daily closes (no key); currency pairs Yahoo misses come from
6
+ the ECB reference rates (marketmyna/data/ecb_fx.py). The VIX family's latest value is
7
+ taken from CBOE's own delayed quotes when they answer (fresher than Yahoo
8
+ before the US open). Crypto mood is alternative.me's fear & greed index.
9
+ The VIX term structure is reported as a reading, not advice: a 1-month VIX
10
+ above the 3-month one (backwardation) has historically come with market stress.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import threading
17
+ import time
18
+ from concurrent.futures import ThreadPoolExecutor
19
+ from typing import Callable, Optional
20
+
21
+ import pandas as pd
22
+ import requests
23
+
24
+ from .commodities import _change, _yahoo
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ CACHE_SECONDS = 15 * 60
29
+
30
+ # ticker, label, group, note
31
+ ITEMS = [
32
+ ("^GSPC", "S&P 500", "americas", "US large caps"),
33
+ ("^IXIC", "Nasdaq Composite", "americas", "US, tech-heavy"),
34
+ ("^DJI", "Dow Jones", "americas", "30 US blue chips"),
35
+ ("^GSPTSE", "S&P/TSX", "americas", "Canada"),
36
+ ("^BVSP", "Bovespa", "americas", "Brazil"),
37
+ ("^STOXX50E", "Euro Stoxx 50", "europe", "Eurozone blue chips"),
38
+ ("^FTSE", "FTSE 100", "europe", "UK"),
39
+ ("^GDAXI", "DAX", "europe", "Germany"),
40
+ ("^FCHI", "CAC 40", "europe", "France"),
41
+ ("^NSEI", "Nifty 50", "asia", "India"),
42
+ ("^N225", "Nikkei 225", "asia", "Japan"),
43
+ ("000001.SS", "Shanghai Composite", "asia", "China A-shares"),
44
+ ("^HSI", "Hang Seng", "asia", "Hong Kong"),
45
+ ("^KS11", "KOSPI", "asia", "South Korea"),
46
+ ("^TWII", "Taiwan Weighted", "asia", "Taiwan"),
47
+ ("^AXJO", "ASX 200", "asia", "Australia"),
48
+ ("DX-Y.NYB", "US dollar index", "fx", "Dollar against six major currencies"),
49
+ ("EURUSD=X", "EUR / USD", "fx", "Dollars per euro"),
50
+ ("GBPUSD=X", "GBP / USD", "fx", "Dollars per pound"),
51
+ ("USDJPY=X", "USD / JPY", "fx", "Yen per dollar"),
52
+ ("USDCNY=X", "USD / CNY", "fx", "Yuan per dollar"),
53
+ ("USDINR=X", "USD / INR", "fx", "Rupees per dollar"),
54
+ ("AUDUSD=X", "AUD / USD", "fx", "Dollars per Australian dollar"),
55
+ ("^IRX", "US 3-month", "yields", "Treasury bill yield, %"),
56
+ ("^FVX", "US 5-year", "yields", "Treasury yield, %"),
57
+ ("^TNX", "US 10-year", "yields", "Treasury yield, %"),
58
+ ("^TYX", "US 30-year", "yields", "Treasury yield, %"),
59
+ ("^VIX9D", "VIX 9-day", "volatility", "Expected S&P 500 volatility, next 9 days"),
60
+ ("^VIX", "VIX", "volatility", "Expected S&P 500 volatility, next 30 days"),
61
+ ("^VIX3M", "VIX 3-month", "volatility", "Expected S&P 500 volatility, next 3 months"),
62
+ ("^VIX6M", "VIX 6-month", "volatility", "Expected S&P 500 volatility, next 6 months"),
63
+ ("^INDIAVIX", "India VIX", "volatility", "Expected Nifty volatility, next 30 days"),
64
+ ]
65
+ # Yields move in points, not percent: their changes are reported as differences.
66
+ POINT_CHANGE = {"yields", "volatility"}
67
+
68
+ Fetch = Callable[[list[str]], dict[str, pd.Series]]
69
+
70
+ _cache: dict[str, tuple[float, dict]] = {}
71
+ _raw_cache: dict[str, tuple[float, tuple]] = {}
72
+ _lock = threading.Lock()
73
+
74
+
75
+ def _cboe_latest(symbols: list[str]) -> dict[str, float]:
76
+ """Latest CBOE delayed values for VIX indices (^VIX -> _VIX.json)."""
77
+ def one(sym: str) -> tuple[str, Optional[float]]:
78
+ try:
79
+ r = requests.get(f"https://cdn.cboe.com/api/global/delayed_quotes/quotes/_{sym.lstrip('^')}.json",
80
+ headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
81
+ r.raise_for_status()
82
+ price = ((r.json() or {}).get("data") or {}).get("current_price")
83
+ return sym, float(price) if price else None
84
+ except Exception: # noqa: BLE001 - CBOE is only a fresher override
85
+ return sym, None
86
+
87
+ with ThreadPoolExecutor(max_workers=4) as pool:
88
+ return {s: v for s, v in pool.map(one, symbols) if v}
89
+
90
+
91
+ def _fear_greed() -> Optional[dict]:
92
+ from ..data.extra_sources import crypto_fear_greed
93
+
94
+ try:
95
+ rows = crypto_fear_greed(31)
96
+ except Exception as exc: # noqa: BLE001
97
+ logger.info("Crypto fear & greed unavailable: %s", exc)
98
+ return None
99
+ if not rows:
100
+ return None
101
+ month = rows[30]["value"] if len(rows) > 30 else None
102
+ return {"value": rows[0]["value"], "label": rows[0]["label"], "date": rows[0]["date"],
103
+ "week_ago": rows[7]["value"] if len(rows) > 7 else None, "month_ago": month,
104
+ "spark": [r["value"] for r in reversed(rows)], "source": "Crypto fear & greed index"}
105
+
106
+
107
+ def _diff(s: pd.Series, days: int) -> Optional[float]:
108
+ when = s.index[-1] - pd.Timedelta(days=days)
109
+ past = s[s.index <= when]
110
+ return float(s.iloc[-1] - past.iloc[-1]) if len(past) else None
111
+
112
+
113
+ def _row(ticker: str, label: str, group: str, note: str, s: pd.Series) -> dict:
114
+ spark = s[s.index >= s.index[-1] - pd.Timedelta(days=92)]
115
+ points = group in POINT_CHANGE
116
+ prev = s.iloc[-2] if len(s) > 1 else None
117
+ return {
118
+ "key": ticker, "label": label, "group": group, "note": note,
119
+ "price": float(s.iloc[-1]),
120
+ "as_of": str(pd.Timestamp(s.index[-1]).date()),
121
+ "change_unit": "points" if points else "%",
122
+ "change_1d": (float(s.iloc[-1] - prev) if points else float(s.iloc[-1] / prev - 1) * 100)
123
+ if prev else None,
124
+ "change_1m": _diff(s, 30) if points else _change(s, 30),
125
+ "change_1y": _diff(s, 365) if points else _change(s, 365),
126
+ "spark": [round(float(v), 4) for v in spark.to_numpy()],
127
+ "source": s.attrs.get("source"),
128
+ }
129
+
130
+
131
+ def term_structure(rows: list[dict]) -> Optional[dict]:
132
+ by = {r["key"]: r["price"] for r in rows}
133
+ vix, vix3m = by.get("^VIX"), by.get("^VIX3M")
134
+ if not vix or not vix3m:
135
+ return None
136
+ ratio = vix / vix3m
137
+ shape = "backwardation" if ratio > 1 else "contango"
138
+ return {"vix": vix, "vix3m": vix3m, "ratio": round(ratio, 3), "shape": shape,
139
+ "note": ("Near-term fear above longer-term: usually seen during sell-offs"
140
+ if shape == "backwardation" else "Calm near term: the usual shape")}
141
+
142
+
143
+ def tickers() -> list[str]:
144
+ return [t for t, *_ in ITEMS]
145
+
146
+
147
+ def build(fetch: Fetch = _yahoo, cboe: Callable[[list[str]], dict[str, float]] = _cboe_latest,
148
+ fear_greed: Callable[[], Optional[dict]] = _fear_greed, use_cache: bool = True,
149
+ live: Optional[dict] = None) -> dict:
150
+ """`live`: ticker -> {price, day} from live_market.latest, moving each
151
+ item's latest value; the history behind it is cached CACHE_SECONDS."""
152
+ from .live_market import extend_all
153
+
154
+ if use_cache and not live:
155
+ with _lock:
156
+ hit = _cache.get("all")
157
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
158
+ return hit[1]
159
+
160
+ raw = None
161
+ if use_cache:
162
+ with _lock:
163
+ hit = _raw_cache.get("all")
164
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
165
+ raw = hit[1]
166
+ if raw is None:
167
+ with ThreadPoolExecutor(max_workers=2) as pool:
168
+ mood = pool.submit(fear_greed)
169
+ fresh = pool.submit(cboe, [t for t, *_ in ITEMS if t.startswith("^VIX")])
170
+ try:
171
+ data = fetch([t for t, *_ in ITEMS])
172
+ except Exception as exc: # network / Yahoo hiccup
173
+ logger.warning("World markets fetch failed: %s", exc)
174
+ data = {}
175
+ raw = (data, mood.result(), fresh.result())
176
+ if use_cache and raw[0]:
177
+ with _lock:
178
+ _raw_cache["all"] = (time.time(), raw)
179
+ data, crypto, latest = raw
180
+ data = dict(data)
181
+
182
+ today = pd.Timestamp.today().normalize()
183
+ for sym, value in latest.items(): # CBOE's value is today's; replace or append the last bar
184
+ s = data.get(sym)
185
+ if s is None or not len(s):
186
+ continue
187
+ s = s.copy()
188
+ if s.index[-1].normalize() >= today:
189
+ s.iloc[-1] = value
190
+ else:
191
+ s.loc[today] = value
192
+ data[sym] = s
193
+ if live:
194
+ data = extend_all(data, live)
195
+
196
+ rows = [_row(t, label, group, note, data[t]) for t, label, group, note in ITEMS
197
+ if t in data and len(data[t]) > 1]
198
+ failed = [label for t, label, *_ in ITEMS if not any(r["key"] == t for r in rows)]
199
+ source = "Daily closes from the market data feed; VIX from the exchange's delayed quotes; crypto fear & greed index"
200
+ backup = [r["label"] for r in rows if r["source"]]
201
+ if backup:
202
+ source += f"; central bank reference rates for {', '.join(backup)} (feed missing or stale)"
203
+ out = {"items": rows, "failed": failed, "vix_term": term_structure(rows), "crypto_mood": crypto,
204
+ "updated": time.strftime("%Y-%m-%dT%H:%M:%S"), "source": source}
205
+ if use_cache and rows and not live:
206
+ with _lock:
207
+ _cache["all"] = (time.time(), out)
208
+ return out