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,224 @@
1
+ """Relative rotation (RRG): where each sector sits against the market index on
2
+ relative strength (RS-Ratio) and its momentum (RS-Momentum), with a weekly tail.
3
+
4
+ Yahoo lacks most NSE sector indices, so every sector is a market-cap-weighted
5
+ basket of its largest stocks (by our listings category), priced from the
6
+ scanner's batched daily bars. The JdK-style normalisation follows OpenBB's
7
+ technical.relative_rotation (Apache 2.0).
8
+
9
+ Quadrants: leading (strong, rising) -> weakening -> lagging -> improving.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import threading
16
+ import time
17
+ from typing import Callable, Optional
18
+
19
+ import numpy as np
20
+ import pandas as pd
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ ROTATION_MARKETS = ("india", "us", "china_a", "hong_kong", "crypto")
25
+
26
+ # Crypto has no sectors in the listings; these are the usual narrative
27
+ # groups, each an equal-weight basket measured against Bitcoin.
28
+ CRYPTO_GROUPS = {
29
+ "Smart-contract L1s": ["ETH", "SOL", "ADA", "AVAX", "DOT", "NEAR", "APT", "SUI", "TON", "TRX"],
30
+ "Layer 2s": ["ARB", "OP", "POL", "STRK", "IMX", "MNT"],
31
+ "DeFi": ["UNI", "AAVE", "LDO", "CRV", "COMP", "PENDLE", "JUP", "RAY"],
32
+ "Payments & legacy": ["XRP", "LTC", "BCH", "XLM", "ETC"],
33
+ "Exchange tokens": ["BNB", "OKB", "CRO"],
34
+ "Oracles & infra": ["LINK", "FIL", "GRT", "QNT", "HBAR"],
35
+ "AI": ["FET", "RENDER", "TAO", "WLD", "AR"],
36
+ "Memecoins": ["DOGE", "SHIB", "PEPE", "BONK", "WIF", "FLOKI"],
37
+ "Gaming & metaverse": ["SAND", "MANA", "AXS", "GALA"],
38
+ }
39
+ MEMBERS = 8
40
+ WINDOW = 10 # weeks used to normalise
41
+ TAIL = 8 # weeks of trail shown
42
+ CACHE_SECONDS = 1800
43
+
44
+ Fetch = Callable[[list[str]], dict[str, pd.Series]]
45
+
46
+ _cache: dict[str, tuple[float, dict]] = {}
47
+ _closes_cache: dict[str, tuple[float, dict[str, pd.Series], set[str]]] = {}
48
+ _lock = threading.Lock()
49
+
50
+
51
+ def _closes(tickers: list[str]) -> dict[str, pd.Series]:
52
+ from ..trading.scanner import _cache as bars
53
+
54
+ return {t: df["close"].astype(float) for t, df in bars.get(tickers, "1d", "1y").items()}
55
+
56
+
57
+ def _weekly(s: pd.Series) -> pd.Series:
58
+ s = s.dropna().copy()
59
+ s.index = pd.DatetimeIndex(s.index).tz_localize(None)
60
+ return s.resample("W-FRI").last().dropna()
61
+
62
+
63
+ def rrg(series: pd.Series, bench: pd.Series, window: int = WINDOW) -> pd.DataFrame:
64
+ """Weekly RS-Ratio / RS-Momentum (both centred on 100) of `series` vs `bench`."""
65
+ both = pd.concat([series, bench], axis=1).dropna()
66
+ rs = both.iloc[:, 0] / both.iloc[:, 1]
67
+ z = lambda s: (s - s.rolling(window).mean()) / s.rolling(window).std() # noqa: E731
68
+ ratio = 100 + z(rs)
69
+ momentum = 100 + z(ratio.pct_change() * 100)
70
+ return pd.DataFrame({"ratio": ratio, "momentum": momentum}).dropna()
71
+
72
+
73
+ def quadrant(ratio: float, momentum: float) -> str:
74
+ if ratio >= 100:
75
+ return "leading" if momentum >= 100 else "weakening"
76
+ return "improving" if momentum >= 100 else "lagging"
77
+
78
+
79
+ def baskets(rows: list[dict], members: int = MEMBERS) -> dict[str, list[dict]]:
80
+ """category -> its largest priced stocks (with market cap)."""
81
+ out: dict[str, list[dict]] = {}
82
+ for r in sorted((r for r in rows if r.get("type") == "stock" and r.get("market_cap")), key=lambda r: -r["market_cap"]):
83
+ cat = r.get("category")
84
+ if not cat or cat == "Other":
85
+ continue
86
+ group = out.setdefault(cat, [])
87
+ if len(group) < members:
88
+ group.append(r)
89
+ return {k: v for k, v in out.items() if len(v) >= 3}
90
+
91
+
92
+ def _crypto_closes(tickers: list[str]) -> dict[str, pd.Series]:
93
+ from .crypto_screens import _fetch
94
+
95
+ return _fetch(tickers, days=400)
96
+
97
+
98
+ def _cached_closes(key: str, tickers: list[str], fetch: Fetch, use_cache: bool) -> dict[str, pd.Series]:
99
+ """Daily closes, kept CACHE_SECONDS: live prices only move the last bar,
100
+ so the year of history is fetched once and re-used."""
101
+ if use_cache:
102
+ with _lock:
103
+ hit = _closes_cache.get(key)
104
+ if hit and time.time() - hit[0] < CACHE_SECONDS and set(tickers) <= hit[2]:
105
+ return hit[1]
106
+ closes = fetch(tickers)
107
+ if use_cache and closes:
108
+ with _lock:
109
+ _closes_cache[key] = (time.time(), closes, set(tickers))
110
+ return closes
111
+
112
+
113
+ def build_crypto(fetch: Fetch = _crypto_closes, use_cache: bool = True, live: Optional[dict] = None) -> dict:
114
+ """Coin groups vs Bitcoin, same output shape as build(). `live`: ticker ->
115
+ {price, day} moving today's close."""
116
+ from .live_market import extend_all
117
+
118
+ if use_cache and not live:
119
+ with _lock:
120
+ hit = _cache.get("crypto")
121
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
122
+ return hit[1]
123
+ tickers = ["BTC-USD"] + [f"{c}-USD" for coins in CRYPTO_GROUPS.values() for c in coins]
124
+ closes = _cached_closes("crypto", list(dict.fromkeys(tickers)), fetch, use_cache)
125
+ if live:
126
+ closes = extend_all(closes, live)
127
+ if "BTC-USD" not in closes:
128
+ raise ValueError("Bitcoin price history unavailable")
129
+ bench = _weekly(closes["BTC-USD"])
130
+ sectors = []
131
+ for group, coins in CRYPTO_GROUPS.items():
132
+ parts, members = [], []
133
+ for c in coins:
134
+ s = closes.get(f"{c}-USD")
135
+ if s is None or len(s.dropna()) < 120:
136
+ continue
137
+ w = _weekly(s)
138
+ parts.append(w / w.iloc[0])
139
+ members.append(f"{c}-USD")
140
+ if len(parts) < 2:
141
+ continue
142
+ basket = pd.concat(parts, axis=1).ffill().dropna().mean(axis=1) # equal weight
143
+ path = rrg(basket, bench)
144
+ if path.empty:
145
+ continue
146
+ tail = path.tail(TAIL)
147
+ last = tail.iloc[-1]
148
+ sectors.append({
149
+ "sector": group, "members": members,
150
+ "ratio": float(last["ratio"]), "momentum": float(last["momentum"]),
151
+ "quadrant": quadrant(float(last["ratio"]), float(last["momentum"])),
152
+ "return_4w": float(basket.iloc[-1] / basket.iloc[-5] - 1) * 100 if len(basket) > 5 else None,
153
+ "tail": [{"week": str(i.date()), "ratio": float(v["ratio"]), "momentum": float(v["momentum"])} for i, v in tail.iterrows()],
154
+ })
155
+ order = {"leading": 0, "improving": 1, "weakening": 2, "lagging": 3}
156
+ sectors.sort(key=lambda s: (order[s["quadrant"]], -s["ratio"]))
157
+ out = {"market": "crypto", "benchmark": {"symbol": "BTC-USD", "name": "Bitcoin"},
158
+ "as_of": str(bench.index[-1].date()), "window_weeks": WINDOW, "sectors": sectors}
159
+ if use_cache and sectors and not live:
160
+ with _lock:
161
+ _cache["crypto"] = (time.time(), out)
162
+ return out
163
+
164
+
165
+ def build(market: str, rows: Optional[list[dict]] = None, fetch: Fetch = _closes, live: Optional[dict] = None) -> dict:
166
+ """`live`: Yahoo ticker -> {price, day} for the basket members and the
167
+ benchmark; today's close moves with it, so the current week's point (and
168
+ its quadrant) follows the session."""
169
+ if market == "crypto":
170
+ return build_crypto(live=live)
171
+ from ..data.listings import get_store
172
+ from .analysis_run import MARKET_INDICES, yahoo_symbol
173
+ from .live_market import extend_all
174
+
175
+ if rows is None and not live:
176
+ with _lock:
177
+ hit = _cache.get(market)
178
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
179
+ return hit[1]
180
+ groups = baskets(rows if rows is not None else get_store().rows(market))
181
+ bench_ticker, bench_name = MARKET_INDICES[market][0]
182
+ tickers = {r["symbol"]: yahoo_symbol(r["symbol"], market) for g in groups.values() for r in g}
183
+ closes = _cached_closes(market, list(dict.fromkeys([*tickers.values(), bench_ticker])), fetch, rows is None)
184
+ if live:
185
+ closes = extend_all(closes, live)
186
+ if bench_ticker not in closes:
187
+ raise ValueError(f"Benchmark {bench_name} unavailable")
188
+ bench = _weekly(closes[bench_ticker])
189
+
190
+ sectors = []
191
+ for cat, members in groups.items():
192
+ parts, weights = [], []
193
+ for r in members:
194
+ s = closes.get(tickers[r["symbol"]])
195
+ if s is None or len(s.dropna()) < 120:
196
+ continue
197
+ w = _weekly(s)
198
+ parts.append(w / w.iloc[0])
199
+ weights.append(float(r["market_cap"]))
200
+ if len(parts) < 3:
201
+ continue
202
+ frame = pd.concat(parts, axis=1).ffill().dropna()
203
+ basket = frame.mul(np.array(weights) / sum(weights), axis=1).sum(axis=1)
204
+ path = rrg(basket, bench)
205
+ if path.empty:
206
+ continue
207
+ tail = path.tail(TAIL)
208
+ last = tail.iloc[-1]
209
+ sectors.append({
210
+ "sector": cat,
211
+ "members": [r["symbol"] for r in members],
212
+ "ratio": float(last["ratio"]), "momentum": float(last["momentum"]),
213
+ "quadrant": quadrant(float(last["ratio"]), float(last["momentum"])),
214
+ "return_4w": float(basket.iloc[-1] / basket.iloc[-5] - 1) * 100 if len(basket) > 5 else None,
215
+ "tail": [{"week": str(i.date()), "ratio": float(v["ratio"]), "momentum": float(v["momentum"])} for i, v in tail.iterrows()],
216
+ })
217
+ order = {"leading": 0, "improving": 1, "weakening": 2, "lagging": 3}
218
+ sectors.sort(key=lambda s: (order[s["quadrant"]], -s["ratio"]))
219
+ out = {"market": market, "benchmark": {"symbol": bench_ticker, "name": bench_name},
220
+ "as_of": str(bench.index[-1].date()), "window_weeks": WINDOW, "sectors": sectors}
221
+ if rows is None and not live:
222
+ with _lock:
223
+ _cache[market] = (time.time(), out)
224
+ return out