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,370 @@
1
+ """Smart messaging for big watchlists (100+ stocks).
2
+
3
+ Instead of a paragraph per stock, a run produces:
4
+ - a full table of every monitored stock (saved, opened through a link and
5
+ emailed as HTML), and
6
+ - a short message with only the day's top N "hot" stocks.
7
+
8
+ Every stock gets cheap, computed metrics (no AI):
9
+ normal range 20-day Bollinger band (where price usually trades)
10
+ buy point support: the higher of the lower band and the 20-day low
11
+ sell point resistance: the lower of the upper band and the 60-day high
12
+ (upper band if price is already above it)
13
+ stop loss buy point minus one average true range
14
+ 1-week range price +/- one weekly standard deviation (~68% of weeks)
15
+ valuation P/E vs its industry average (fundamental analyzer)
16
+ action a score from trend, RSI, levels and valuation
17
+
18
+ The points, stop and action are working values only: without_calls()
19
+ turns the action into a bullish / neutral / bearish reading and drops the
20
+ rest before anything is saved, sent or shown (marketmyna/core/compliance.py).
21
+
22
+ Only the `ai_top` most "attention-worthy" stocks also get the full AI
23
+ pipeline, so a 100-stock list doesn't burn 400 LLM calls a day.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import html as html_lib
29
+ import logging
30
+ import math
31
+ import secrets
32
+ from concurrent.futures import ThreadPoolExecutor
33
+ from datetime import datetime
34
+ from typing import Optional
35
+ from zoneinfo import ZoneInfo
36
+
37
+ from ..core.numbers import finite as _num
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ WORKERS = 6
42
+
43
+
44
+ def compute_snapshot(history, info: Optional[dict] = None) -> dict:
45
+ """Levels, forecast range and rule-based action from price history
46
+ (and optional yfinance info for valuation). Pure: no network."""
47
+ close = history["close"].astype(float)
48
+ high = history["high"].astype(float) if "high" in history else close
49
+ low = history["low"].astype(float) if "low" in history else close
50
+ if len(close) < 30:
51
+ raise ValueError("not enough history")
52
+
53
+ price = float(close.iloc[-1])
54
+ prev = float(close.iloc[-2])
55
+ change_pct = (price / prev - 1) * 100 if prev else None
56
+
57
+ mid = float(close.tail(20).mean())
58
+ sd = float(close.tail(20).std())
59
+ band_low, band_high = mid - 2 * sd, mid + 2 * sd
60
+ low20 = float(low.tail(20).min())
61
+ high60 = float(high.tail(60).max())
62
+
63
+ prev_close = close.shift(1)
64
+ true_range = (high - low).combine((high - prev_close).abs(), max).combine((low - prev_close).abs(), max)
65
+ atr = float(true_range.tail(14).mean())
66
+
67
+ buy_point = max(band_low, low20)
68
+ sell_point = min(band_high, high60) if min(band_high, high60) > price else band_high
69
+ stop_loss = buy_point - atr
70
+
71
+ daily = close.pct_change().dropna()
72
+ week_sd = float(daily.tail(60).std()) * math.sqrt(5)
73
+ # Risk over the history given (a year): annualised volatility and the deepest fall from a high.
74
+ volatility = float(daily.std()) * math.sqrt(252) if len(daily) > 1 else None
75
+ max_drawdown = float((close / close.cummax() - 1).min())
76
+ week_low, week_high = price * (1 - week_sd), price * (1 + week_sd)
77
+
78
+ delta = close.diff()
79
+ gain = delta.clip(lower=0).tail(14).mean()
80
+ loss = (-delta.clip(upper=0)).tail(14).mean()
81
+ rsi = 100.0 if loss == 0 else float(100 - 100 / (1 + gain / loss))
82
+
83
+ sma50 = float(close.tail(50).mean())
84
+ sma200 = float(close.tail(200).mean()) if len(close) >= 200 else None
85
+ if sma200 and price > sma50 > sma200:
86
+ trend = "uptrend"
87
+ elif sma200 and price < sma50 < sma200:
88
+ trend = "downtrend"
89
+ else:
90
+ trend = "sideways"
91
+
92
+ valuation, pe = None, None
93
+ if info:
94
+ try:
95
+ from ..analysis.fundamental import FundamentalAnalyzer, yfinance_fundamentals
96
+
97
+ summary = FundamentalAnalyzer(yfinance_fundamentals(info)).get_summary()
98
+ status = (summary.get("valuation") or {}).get("status")
99
+ valuation = status if status and status != "N/A" else None
100
+ pe = _num((summary.get("valuation") or {}).get("value")) or None
101
+ except Exception:
102
+ pass
103
+
104
+ score = {"uptrend": 1, "downtrend": -1}.get(trend, 0)
105
+ score += 1 if rsi < 35 else -1 if rsi > 70 else 0
106
+ near_buy = price <= buy_point * 1.02
107
+ near_sell = price >= sell_point * 0.98
108
+ score += 1 if near_buy else 0
109
+ score -= 1 if near_sell else 0
110
+ score += {"undervalued": 1, "overvalued": -1}.get(valuation or "", 0)
111
+ action = "buy" if score >= 2 else "sell" if score <= -2 else "hold"
112
+
113
+ volume = history["volume"].astype(float) if "volume" in history else None
114
+ vol_ratio = None
115
+ if volume is not None and len(volume) > 21 and float(volume.tail(21).head(20).mean()) > 0:
116
+ vol_ratio = float(volume.iloc[-1] / volume.tail(21).head(20).mean())
117
+
118
+ # Why this stock deserves attention today (ranking for the top N).
119
+ reasons, attention = [], 0.0
120
+ if change_pct is not None and abs(change_pct) >= 2:
121
+ reasons.append(f"moved {change_pct:+.1f}% today")
122
+ attention += abs(change_pct or 0)
123
+ if action != "hold":
124
+ attention += 2
125
+ if near_buy:
126
+ attention += 1.5
127
+ reasons.append("at its buy point")
128
+ elif near_sell:
129
+ attention += 1.5
130
+ reasons.append("at its sell point")
131
+ if rsi < 30 or rsi > 70:
132
+ attention += 1
133
+ reasons.append("oversold" if rsi < 30 else "overbought")
134
+ if vol_ratio and vol_ratio >= 1.5:
135
+ attention += 1
136
+ reasons.append(f"volume {vol_ratio:.1f}x normal")
137
+
138
+ return {
139
+ "price": price, "change_percent": change_pct,
140
+ "normal_low": band_low, "normal_high": band_high,
141
+ "buy_point": buy_point, "sell_point": sell_point, "stop_loss": stop_loss,
142
+ "week_low": week_low, "week_high": week_high,
143
+ "rsi": rsi, "trend": trend, "valuation": valuation, "pe": pe,
144
+ "atr": atr, "volume_ratio": vol_ratio,
145
+ "volatility": volatility, "max_drawdown": max_drawdown,
146
+ "high_52w": float(high.max()), "low_52w": float(low.min()),
147
+ "action": action, "attention": round(attention, 2), "reasons": reasons,
148
+ }
149
+
150
+
151
+ def snapshot(symbol: str, market: str) -> dict:
152
+ """Network wrapper: history (fallback chains) + yfinance info."""
153
+ from ..agent.agents import _resolve_fetcher
154
+ from .analysis_run import yahoo_symbol
155
+
156
+ row = {"symbol": symbol, "market": market}
157
+ try:
158
+ history = _resolve_fetcher(market).get_history(symbol, period="1y")
159
+ info = {}
160
+ if market != "crypto":
161
+ try:
162
+ import yfinance as yf
163
+
164
+ from ..utils.deadline import call_with_deadline
165
+
166
+ info = call_with_deadline(lambda: yf.Ticker(yahoo_symbol(symbol, market)).info, 15) or {}
167
+ except Exception:
168
+ info = {}
169
+ row.update(compute_snapshot(history, info))
170
+ row["name"] = info.get("shortName") or info.get("longName")
171
+ except Exception as exc:
172
+ row["error"] = str(exc) or type(exc).__name__
173
+ return row
174
+
175
+
176
+ def build_rows(targets, config, ai_top: int, analyze=None, snap=snapshot, mode: str = "standard") -> list[dict]:
177
+ """Snapshot every stock (parallel), rank by attention, then run the AI
178
+ pipeline (at the schedule's analysis depth) on the top `ai_top` only."""
179
+ from .analysis_run import analyze_symbol
180
+
181
+ analyze = analyze or analyze_symbol
182
+ with ThreadPoolExecutor(max_workers=WORKERS) as pool:
183
+ rows = list(pool.map(lambda t: snap(t.symbol, t.market), targets))
184
+ rows.sort(key=lambda r: r.get("attention", -1), reverse=True)
185
+
186
+ for row in [r for r in rows if "error" not in r][: max(0, ai_top)]:
187
+ result = analyze(row["symbol"], row["market"], mode, config)
188
+ dash = result.get("dashboard") or {}
189
+ # The AI's outlook replaces the computed reading; none, and the computed one stands.
190
+ if dash.get("outlook"):
191
+ row["action"] = {"bullish": "buy", "bearish": "sell"}.get(dash["outlook"], "hold")
192
+ row["ai"] = True
193
+ row["ai_summary"] = dash.get("analysis_summary")
194
+ return rows
195
+
196
+
197
+ # A report shows what the indicators read, never a call (marketmyna/core/compliance.py).
198
+ READINGS = {"strong_buy": "positive", "buy": "positive", "hold": "mixed", "sell": "negative", "strong_sell": "negative"}
199
+ READING_LABELS = {"positive": "Bullish", "mixed": "Neutral", "negative": "Bearish"}
200
+ READING_ICONS = {"positive": "▲", "mixed": "◆", "negative": "▼"}
201
+ _NEUTRAL_REASONS = {"at its buy point": "near its support level", "at its sell point": "near its resistance level",
202
+ "oversold": "RSI below 30", "overbought": "RSI above 70"}
203
+
204
+
205
+ def without_calls(row: dict) -> dict:
206
+ """The row with every call, level and stop removed and the computed
207
+ action turned into a reading of the indicators (positive / mixed /
208
+ negative). Verdict words (undervalued, at its buy point) go too."""
209
+ from ..core.compliance import CALL_FIELDS
210
+
211
+ if "error" in row:
212
+ return row
213
+ out = {k: v for k, v in row.items() if k not in CALL_FIELDS}
214
+ out["reading"] = READINGS.get(str(row.get("action") or "hold").lower(), "mixed")
215
+ out["reasons"] = [_NEUTRAL_REASONS.get(r, r) for r in row.get("reasons") or []]
216
+ out["valuation"] = None # "undervalued" / "overvalued" is a verdict; the P/E stays
217
+ return out
218
+
219
+
220
+ def reading_label(row: dict) -> str:
221
+ reading = row.get("reading") or "mixed"
222
+ return f"{READING_ICONS[reading]} {READING_LABELS[reading]}"
223
+
224
+
225
+ def save_report(config, rows: list[dict], title: str, meta: dict) -> str:
226
+ from ..storage import DatabaseManager, ReportRunRecord, get_db_url
227
+
228
+ token = secrets.token_urlsafe(16)
229
+ db = DatabaseManager.instance(get_db_url(config=config))
230
+ with db.get_session() as session:
231
+ session.add(ReportRunRecord(token=token, title=title, stock_count=len(rows), rows=rows, meta=meta))
232
+ return token
233
+
234
+
235
+ # ------------------------------------------------------------- rendering
236
+
237
+ def _f(value, digits: int = 2) -> str:
238
+ v = _num(value)
239
+ if v is None:
240
+ return "—"
241
+ return f"{v:,.{digits}f}" if abs(v) >= 1 else f"{v:.4f}"
242
+
243
+
244
+ def smart_message(rows: list[dict], prefs, lang: str, link: Optional[str], now: datetime) -> tuple[str, str]:
245
+ """(title, body): only the top N, one line each, then the link."""
246
+ from .report import CURRENCY_SYMBOLS
247
+ from .report_i18n import has_builtin, t
248
+
249
+ lang_labels = lang if has_builtin(lang) else "en"
250
+ good = [r for r in rows if "error" not in r]
251
+ top = good[: prefs.smart_top_n]
252
+ lines = [
253
+ f"📊 {t(lang_labels, 'smart_top', n=len(top))} · {now:%d %b %Y}",
254
+ "",
255
+ ]
256
+ for r in top:
257
+ cur = CURRENCY_SYMBOLS.get(r["market"], "")
258
+ reading = r.get("reading") or "mixed"
259
+ change = f" ({r['change_percent']:+.1f}%)" if _num(r.get("change_percent")) is not None else ""
260
+ lines.append(f"{READING_ICONS[reading]} {r['symbol']} {cur}{_f(r['price'])}{change} · "
261
+ f"{t(lang_labels, 'indicators')}: {t(lang_labels, 'reading_' + reading)}")
262
+ lines.append(f" {t(lang_labels, 'normal')} {cur}{_f(r['normal_low'])}–{_f(r['normal_high'])} · "
263
+ f"{t(lang_labels, 'week')} {cur}{_f(r['week_low'])}–{_f(r['week_high'])}")
264
+ why = r.get("ai_summary") or ", ".join(r.get("reasons") or [])
265
+ if why:
266
+ lines.append(f" {why[:140]}{'…' if len(why) > 140 else ''}")
267
+ lines.append("")
268
+ lines.append(f"📄 {t(lang_labels, 'full_report', n=len(rows))}" + (f": {link}" if link else ""))
269
+ lines.append(t(lang_labels, "reading_note"))
270
+ lines.append(t(lang_labels, "disclaimer"))
271
+ return t(lang_labels, "title"), "\n".join(lines)
272
+
273
+
274
+ def html_report(rows: list[dict], title: str, now: datetime, link: Optional[str]) -> str:
275
+ """Self-contained HTML table of every stock (for email and the artifact):
276
+ the indicators' reading and the ranges, no call, level or stop, with a
277
+ note on how to read it."""
278
+ from .report import CURRENCY_SYMBOLS
279
+
280
+ esc = html_lib.escape
281
+ colors = {"positive": "#0b7a52", "mixed": "#8f5b00", "negative": "#c02f2f"}
282
+ head = ["Stock", "Price", "Day", "Indicators", "Normal range", "1-week range", "Trend", "RSI", "P/E", "What stands out"]
283
+ body = []
284
+ for r in rows:
285
+ if "error" in r:
286
+ body.append(f"<tr><td>{esc(r['symbol'])}</td><td colspan='9' style='color:#888'>no data: {esc(r['error'][:80])}</td></tr>")
287
+ continue
288
+ cur = CURRENCY_SYMBOLS.get(r["market"], "")
289
+ chg = _num(r.get("change_percent"))
290
+ cells = [
291
+ f"<b>{esc(r['symbol'])}</b><br><span style='color:#666;font-size:11px'>{esc(r.get('name') or '')}</span>",
292
+ f"{cur}{_f(r['price'])}",
293
+ f"<span style='color:{'#0b7a52' if (chg or 0) >= 0 else '#c02f2f'}'>{'' if chg is None else f'{chg:+.1f}%'}</span>",
294
+ f"<b style='color:{colors[r.get('reading') or 'mixed']}'>{esc(reading_label(r))}</b>",
295
+ f"{_f(r['normal_low'])}–{_f(r['normal_high'])}",
296
+ f"{_f(r['week_low'])}–{_f(r['week_high'])}",
297
+ esc(r["trend"]), f"{r['rsi']:.0f}", _f(r.get("pe"), 1),
298
+ esc((r.get("ai_summary") or ", ".join(r.get("reasons") or []))[:200]),
299
+ ]
300
+ body.append("<tr>" + "".join(f"<td>{c}</td>" for c in cells) + "</tr>")
301
+ link_html = f"<p><a href='{esc(link)}'>Open the interactive report</a></p>" if link else ""
302
+ return f"""<!doctype html><html><body style="font-family:Segoe UI,Arial,sans-serif;color:#0f1522">
303
+ <h2 style="margin:0">{esc(title)}</h2>
304
+ <p style="color:#555;margin:4px 0 12px">{now:%d %b %Y, %H:%M %Z} · {len(rows)} stocks</p>
305
+ <p style="background:#eef3fb;border-left:4px solid #1f3a8a;padding:8px 12px;font-size:12px">{esc(READING_NOTE)}</p>
306
+ {link_html}
307
+ <table cellpadding="6" style="border-collapse:collapse;font-size:12px">
308
+ <thead><tr style="background:#eceff4">{''.join(f'<th align="left">{h}</th>' for h in head)}</tr></thead>
309
+ <tbody>{''.join(body)}</tbody></table>
310
+ <p style="color:#777;font-size:11px">Indicators = how trend, momentum (RSI), where the price sits in its range and
311
+ valuation lean today: bullish, neutral or bearish. Normal range = 20-day Bollinger band. 1-week range ≈
312
+ where ~2 of 3 weeks end, from recent volatility. Computed from market data.</p></body></html>"""
313
+
314
+
315
+ # Shown on every general user's report: what the reading is and is not.
316
+ READING_NOTE = ("This report describes what the market data shows. It is not a recommendation to buy, sell or "
317
+ "hold any stock. \"Bullish\" or \"bearish\" means most of the indicators we "
318
+ "track lean that way today; they can change quickly and are often wrong. Do your own research or "
319
+ "speak to a SEBI-registered adviser before you invest.")
320
+
321
+
322
+ def run_smart(targets, prefs, config, dispatcher=None, dry_run: bool = False, analyze=None, snap=snapshot):
323
+ """Full smart run: rows -> saved report -> short messages (+ full HTML
324
+ email). Returns (reports by language, delivery, rows, token)."""
325
+ from ..notifiers.dispatcher import NotificationDispatcher
326
+ from .report import translate_report
327
+ from .report_i18n import normalize_language
328
+
329
+ rows = build_rows(targets, config, prefs.smart_ai_top, analyze=analyze, snap=snap,
330
+ mode=getattr(prefs, "mode", None) or "standard")
331
+ rows = [without_calls(r) for r in rows]
332
+ now = datetime.now(ZoneInfo(prefs.timezone))
333
+ title = f"MarketMyna · {len(rows)} stocks · {now:%d %b %Y}"
334
+ token = None
335
+ try:
336
+ token = save_report(config, rows, title, {"markets": sorted({t.market for t in targets}),
337
+ "compliance": "analytics"})
338
+ except Exception as exc:
339
+ logger.warning("Could not save full report: %s", exc)
340
+ link = f"{prefs.report_base_url.rstrip('/')}/r/{token}" if prefs.report_base_url and token else None
341
+
342
+ dispatcher = dispatcher or NotificationDispatcher(config)
343
+ channels = dispatcher.configured_channels("report")
344
+ by_lang = prefs.languages_needed(channels)
345
+ languages = list(dict.fromkeys([normalize_language(prefs.language), *by_lang]))
346
+
347
+ reports = {}
348
+ for lang in languages:
349
+ msg_title, body = smart_message(rows, prefs, lang, link, now)
350
+ if lang != "en":
351
+ body = translate_report(body, lang, config) or body
352
+ reports[lang] = (msg_title, body)
353
+
354
+ full_html = html_report(rows, title, now, link) if prefs.smart_email_full else None
355
+ if dry_run or not channels:
356
+ return reports, {}, rows, token, full_html
357
+
358
+ # The report file (PDF or Excel, from the ticked sections) to the channels the user picked.
359
+ from .smart_file import report_file
360
+
361
+ wanted = [c for c in (getattr(prefs, "smart_file_channels", None) or []) if c in ("email", "telegram")]
362
+ report = report_file(rows, prefs, config, now, title) if wanted else None
363
+ files = {c: [report] for c in wanted} if report else {}
364
+
365
+ messages = {}
366
+ for lang, chs in by_lang.items():
367
+ for ch in chs:
368
+ msg_title, body = reports[lang]
369
+ messages[ch] = (msg_title, body, full_html) if ch == "email" and full_html else (msg_title, body)
370
+ return reports, dispatcher.send_each(messages, purpose="report", files=files), rows, token, full_html