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,196 @@
1
+ """Ready-made ETF screens. ETFs carry no earnings of their own, so like the
2
+ crypto screens (marketmyna/services/crypto_screens.py) these work on price and
3
+ volume: returns over 1 / 3 / 12 months, strength against the home index,
4
+ volatility, distance from the 52-week high, liquidity.
5
+
6
+ Universe per market (india, us, hong_kong, china_a): the hand-picked ETFs of
7
+ the ETF rotation view (marketmyna/services/etf_rotation.py) plus the market's most
8
+ traded ETFs by turnover in our listing, up to UNIVERSE. Leveraged and inverse
9
+ funds are left out: their returns are not comparable. A year of daily closes
10
+ from the bar cache. Same result shape as marketmyna/services/screens.py.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import math
17
+ import re
18
+ import threading
19
+ import time
20
+ from typing import Callable, Optional
21
+
22
+ import pandas as pd
23
+
24
+ from .crypto_screens import _passes, _ret
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ MARKETS = ("india", "us", "hong_kong", "china_a")
29
+ UNIVERSE = 120
30
+ CACHE_SECONDS = 3600
31
+ LEVERAGED = re.compile(r"\b(\d(\.\d)?x|ultra|ultrapro|inverse|bear|short|leveraged|daily (2|3)x)\b", re.I)
32
+
33
+ # Large US funds the rotation view leaves out (it groups by theme, not by size).
34
+ US_CORE = [
35
+ ("SPY", "SPDR S&P 500"), ("VOO", "Vanguard S&P 500"), ("IVV", "iShares Core S&P 500"), ("VTI", "Vanguard Total Stock Market"),
36
+ ("DIA", "SPDR Dow Jones"), ("VUG", "Vanguard Growth"), ("VTV", "Vanguard Value"), ("SCHD", "Schwab US Dividend Equity"),
37
+ ("VEA", "Vanguard Developed Markets"), ("VWO", "Vanguard Emerging Markets"), ("IEMG", "iShares Core MSCI Emerging"),
38
+ ("VNQ", "Vanguard Real Estate"), ("BND", "Vanguard Total Bond"), ("AGG", "iShares Core US Aggregate Bond"),
39
+ ("IEF", "iShares 7-10 Year Treasury"), ("SHY", "iShares 1-3 Year Treasury"), ("LQD", "iShares Investment Grade Corporate"),
40
+ ("SLV", "iShares Silver"), ("IAU", "iShares Gold"), ("XLK", "Technology Select Sector"),
41
+ ]
42
+
43
+ LABELS = {
44
+ "volume": "Avg daily turnover", "ret_1m": "1M %", "ret_3m": "3M %", "ret_1y": "1Y %", "vs_index_3m": "vs index 3M pts",
45
+ "volatility": "Volatility %/yr", "from_high": "From 52w high %", "above_ma200": "Above 200d avg %",
46
+ }
47
+
48
+ # key: title, description, [(field, op, value)], (sort field, descending), columns
49
+ PRESETS = {
50
+ "most_traded": ("Most traded", "The most liquid ETFs by average daily turnover (20 days)",
51
+ [], ("volume", True), ["volume", "ret_1m", "ret_1y", "volatility"]),
52
+ "best_1y": ("Best 1-year return", "Highest price return over the past 12 months",
53
+ [("ret_1y", ">", 0)], ("ret_1y", True), ["ret_1y", "ret_3m", "volatility", "from_high"]),
54
+ "momentum": ("Strong momentum", "Up over 5% in 3 months and above the 200-day average",
55
+ [("ret_3m", ">", 5), ("above_ma200", ">", 0)], ("ret_3m", True), ["ret_3m", "ret_1m", "above_ma200", "volume"]),
56
+ "beating_index": ("Beating the index", "3-month return ahead of the home market index",
57
+ [("vs_index_3m", ">", 0)], ("vs_index_3m", True), ["vs_index_3m", "ret_3m", "ret_1y", "volatility"]),
58
+ "low_volatility": ("Low volatility", "Calmest price path over the past year (under 15%/yr)",
59
+ [("volatility", "<", 15)], ("volatility", False), ["volatility", "ret_1y", "from_high", "volume"]),
60
+ "near_high": ("Near 52-week high", "Within 3% of the 52-week closing high",
61
+ [("from_high", ">", -3)], ("from_high", True), ["from_high", "ret_3m", "ret_1y", "volume"]),
62
+ "drawdown": ("Deep drawdown", "More than 20% below the 52-week high",
63
+ [("from_high", "<", -20)], ("from_high", False), ["from_high", "ret_1y", "volatility", "volume"]),
64
+ }
65
+
66
+ # ticker -> daily bars with "close" and (where the feed has it) "volume".
67
+ Fetch = Callable[[list[str]], dict[str, pd.DataFrame]]
68
+
69
+ _cache: dict[str, tuple[float, tuple[list[dict], dict]]] = {}
70
+ _lock = threading.Lock()
71
+
72
+
73
+ def _bars(tickers: list[str]) -> dict[str, pd.DataFrame]:
74
+ from ..trading.scanner import _cache as bars
75
+
76
+ return bars.get(tickers, "1d", "1y")
77
+
78
+
79
+ def turnover(df: pd.DataFrame) -> Optional[float]:
80
+ """Average daily traded value over the last 20 sessions, in the fund's currency."""
81
+ if "volume" not in df:
82
+ return None
83
+ value = (df["close"].astype(float) * df["volume"].astype(float)).dropna().tail(20)
84
+ return float(value.mean()) if len(value) and value.mean() > 0 else None
85
+
86
+
87
+ def _leveraged(r: dict, market: str = "") -> bool:
88
+ sym = r.get("symbol") or ""
89
+ # HKEX lists its leveraged and inverse products under stock codes 07200-07599.
90
+ if market == "hong_kong" and sym.isdigit() and 7200 <= int(sym) <= 7599:
91
+ return True
92
+ return r.get("category") == "Leveraged & Inverse" or bool(LEVERAGED.search(r.get("name") or ""))
93
+
94
+
95
+ def universe(market: str, rows: list[dict]) -> list[dict]:
96
+ """[{symbol, name, ticker, ...listing row}] for the market, liquid first."""
97
+ from .analysis_run import yahoo_symbol
98
+ from .etf_rotation import UNIVERSES, app_symbol
99
+
100
+ etfs = {r["symbol"]: r for r in rows if r.get("type") == "etf" and not _leveraged(r, market)}
101
+ picked: dict[str, dict] = {}
102
+ curated = [pair for group in UNIVERSES.get(market, {}).get("views", {}).values() for pair in group]
103
+ if market == "us":
104
+ curated = US_CORE + curated
105
+ for ticker, name in curated:
106
+ sym = app_symbol(ticker, market)
107
+ picked.setdefault(sym, {**etfs.get(sym, {}), "symbol": sym, "name": (etfs.get(sym) or {}).get("name") or name, "ticker": ticker})
108
+ traded = sorted((r for r in etfs.values() if r.get("volume") and r.get("price")),
109
+ key=lambda r: -(r["volume"] * r["price"]))
110
+ for r in traded:
111
+ if len(picked) >= UNIVERSE:
112
+ break
113
+ picked.setdefault(r["symbol"], {**r, "ticker": yahoo_symbol(r["symbol"], market)})
114
+ return list(picked.values())[:UNIVERSE]
115
+
116
+
117
+ def metrics(s: pd.Series, index_3m: Optional[float]) -> dict:
118
+ s = s.dropna()
119
+ out = {"ret_1m": _ret(s, 21), "ret_3m": _ret(s, 63)}
120
+ # A year of bars is 240-252 sessions depending on the market's holidays: measure from the first one.
121
+ covers_year = len(s) > 1 and (s.index[-1] - s.index[0]).days >= 330
122
+ out["ret_1y"] = float(s.iloc[-1] / s.iloc[0] - 1) * 100 if covers_year else None
123
+ out["from_high"] = float(s.iloc[-1] / s.max() - 1) * 100 if len(s) else None
124
+ out["above_ma200"] = float(s.iloc[-1] / s.tail(200).mean() - 1) * 100 if len(s) >= 200 else None
125
+ daily = s.pct_change().dropna()
126
+ out["volatility"] = float(daily.std() * math.sqrt(252) * 100) if len(daily) > 60 else None
127
+ out["vs_index_3m"] = out["ret_3m"] - index_3m if out["ret_3m"] is not None and index_3m is not None else None
128
+ return out
129
+
130
+
131
+ def _rule_text(rule) -> str:
132
+ field, op, value = rule
133
+ return f"{LABELS.get(field, field)} {op} {value:g}"
134
+
135
+
136
+ def list_presets() -> list[dict]:
137
+ return [{"key": k, "title": p[0], "description": p[1], "large_only": False, "rules": [_rule_text(r) for r in p[2]]}
138
+ for k, p in PRESETS.items()]
139
+
140
+
141
+ def _measured(market: str, rows: Optional[list[dict]], fetch: Fetch) -> tuple[list[dict], dict]:
142
+ from .analysis_run import MARKET_INDICES
143
+
144
+ if rows is None:
145
+ from ..data.listings import get_store
146
+
147
+ rows = get_store().rows(market)
148
+ funds = universe(market, rows)
149
+ index = MARKET_INDICES[market][0][0]
150
+ bars = fetch([f["ticker"] for f in funds] + [index])
151
+ closes = {t: df["close"].astype(float).dropna() for t, df in bars.items()}
152
+ index_3m = _ret(closes[index], 63) if index in closes else None
153
+ return funds, {f["symbol"]: {**metrics(closes[f["ticker"]], index_3m), "volume": turnover(bars[f["ticker"]])}
154
+ for f in funds if f["ticker"] in closes and len(closes[f["ticker"]]) > 60}
155
+
156
+
157
+ def run(key: str, market: str, fetch: Fetch = _bars, rows: Optional[list[dict]] = None, use_cache: bool = True) -> dict:
158
+ from .analysis_run import MARKET_INDICES
159
+
160
+ if market not in MARKETS:
161
+ raise ValueError("ETF screens cover India, US, Hong Kong and China A")
162
+ if key not in PRESETS:
163
+ raise ValueError(f"Unknown screen '{key}'")
164
+ title, desc, rules, (sort_field, desc_sort), columns = PRESETS[key]
165
+ hit = None
166
+ if use_cache:
167
+ with _lock:
168
+ hit = _cache.get(market)
169
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
170
+ funds, ms = hit[1]
171
+ else:
172
+ funds, ms = _measured(market, rows, fetch)
173
+ if use_cache and ms:
174
+ with _lock:
175
+ _cache[market] = (time.time(), (funds, ms))
176
+
177
+ hits = []
178
+ for f in funds:
179
+ m = ms.get(f["symbol"])
180
+ if m is None:
181
+ continue
182
+ if _passes(m, rules):
183
+ hits.append({"symbol": f["symbol"], "name": f.get("name"), "market": market, "market_cap": None,
184
+ "volume": m["volume"], "price": f.get("price"), "currency": f.get("currency"),
185
+ "change_percent": f.get("change_percent"), "sector": f.get("category"),
186
+ **{col: m.get(col) for col in columns}})
187
+ hits.sort(key=lambda h: (h.get(sort_field) is None, -(h.get(sort_field) or 0) if desc_sort else (h.get(sort_field) or 0)))
188
+ return {
189
+ "key": key, "title": title, "description": desc, "market": market,
190
+ "rules": [_rule_text(r) for r in rules] or ["Ranked by turnover"],
191
+ "columns": [{"key": c, "label": LABELS.get(c, c)} for c in columns],
192
+ "universe": f"{len(funds)} large and most-traded ETFs (leveraged and inverse excluded), "
193
+ f"index: {MARKET_INDICES[market][0][1]}",
194
+ "scanned": len(ms),
195
+ "results": hits[:50],
196
+ }