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,51 @@
1
+ """/ask <question> - a single LLM call for a free-form question, not the
2
+ full agent pipeline (no tool use, no multi-turn memory in this phase).
3
+
4
+ "/research" is registered as an alias here rather than a distinct
5
+ command: a real deep-research capability (multi-step, tool-using, like
6
+ daily_stock_analysis's own research agent) is real extra scope this
7
+ phase doesn't build. Aliasing it to /ask is an honest "this exists but
8
+ is shallower than the name implies", not a silent stand-in for
9
+ something it isn't.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ...llm.backend_factory import create_generation_backend
15
+ from ...llm.protocol import GenerationConfig
16
+ from ..models import CommandResult, IncomingMessage
17
+ from .base import Command
18
+
19
+ SYSTEM_PROMPT = (
20
+ "You are a stock research assistant embedded in a chat bot. Answer the "
21
+ "user's question concisely (a few sentences). You have no live tool "
22
+ "access in this mode - if the question needs a live quote or a full "
23
+ "technical/risk analysis, say so and suggest /analyze SYMBOL instead "
24
+ "of guessing at numbers you don't have."
25
+ )
26
+
27
+
28
+ class AskCommand(Command):
29
+ name = "ask"
30
+ aliases = ["research", "r"]
31
+ usage = "/ask QUESTION"
32
+ description = "Free-form question to the AI model (no live data)."
33
+ group = "AI analysis"
34
+ example = "/ask What does a falling RSI mean?"
35
+ help_text = "/ask <question> - ask a free-form question (also: /research, shallower than a full deep-research pass)"
36
+
37
+ def __init__(self, config):
38
+ self._config = config
39
+
40
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
41
+ question = args.strip()
42
+ if not question:
43
+ return CommandResult(text=f"Usage: {self.help_text}", success=False)
44
+
45
+ try:
46
+ backend = create_generation_backend(self._config, tier="fast")
47
+ result = backend.generate(question, GenerationConfig(), system_prompt=SYSTEM_PROMPT)
48
+ except Exception as exc:
49
+ return CommandResult(text=f"Couldn't get an answer: {exc}", success=False)
50
+
51
+ return CommandResult(text=result.text.strip())
@@ -0,0 +1,35 @@
1
+ """Command protocol every bot command implements."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import List
7
+
8
+ from ..models import CommandResult, IncomingMessage
9
+
10
+
11
+ class Command(ABC):
12
+ name: str = ""
13
+ aliases: List[str] = []
14
+ # Shown by /help, the web console's reference and its "/" menu.
15
+ usage: str = "" # "/quote SYMBOL [market]"
16
+ description: str = "" # one line, what it does
17
+ group: str = "Other" # heading in the reference (Research, Markets, ...)
18
+ example: str = "" # a runnable example, "/quote RELIANCE india"
19
+ help_text: str = "" # legacy one-liner; derived from usage + description when empty
20
+
21
+ def help_line(self) -> str:
22
+ if self.help_text:
23
+ return self.help_text
24
+ return f"{self.usage or '/' + self.name} - {self.description}".rstrip(" -")
25
+
26
+ def describe(self) -> dict:
27
+ return {"name": self.name, "aliases": list(self.aliases), "usage": self.usage or f"/{self.name}",
28
+ "description": self.description or self.help_line(), "group": self.group,
29
+ "example": self.example or self.usage or f"/{self.name}"}
30
+
31
+ @abstractmethod
32
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
33
+ """`args` is the raw text after the command word, unparsed - each
34
+ command splits/validates it however it needs to."""
35
+ ...
@@ -0,0 +1,53 @@
1
+ """/batch SYM1,SYM2,SYM3 [market] [mode] - runs the agent pipeline over
2
+ several symbols sequentially (same no-concurrency approach as the
3
+ runtime scheduler) and replies with one combined summary."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from ...agent.dashboard import build_dashboard
8
+ from ...agent.orchestrator import AgentOrchestrator
9
+ from ..formatting import format_dashboard
10
+ from ..models import CommandResult, IncomingMessage
11
+ from .base import Command
12
+
13
+ MAX_SYMBOLS = 10
14
+
15
+
16
+ class BatchCommand(Command):
17
+ name = "batch"
18
+ aliases = ["b"]
19
+ usage = "/batch SYM1,SYM2 [market] [mode]"
20
+ description = "AI analysis of several stocks in one go (max 10)."
21
+ group = "AI analysis"
22
+ example = "/batch TCS,INFY india quick"
23
+ help_text = "/batch SYM1,SYM2,SYM3 [market] [mode] - run the agent pipeline over several symbols (max 10)"
24
+
25
+ def __init__(self, config):
26
+ self._config = config
27
+
28
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
29
+ parts = args.split()
30
+ if not parts:
31
+ return CommandResult(text=f"Usage: {self.help_text}", success=False)
32
+
33
+ symbols = [s.strip() for s in parts[0].split(",") if s.strip()]
34
+ if not symbols:
35
+ return CommandResult(text=f"Usage: {self.help_text}", success=False)
36
+ if len(symbols) > MAX_SYMBOLS:
37
+ return CommandResult(text=f"Too many symbols (max {MAX_SYMBOLS}).", success=False)
38
+
39
+ market = parts[1] if len(parts) > 1 else "us"
40
+ mode = parts[2] if len(parts) > 2 else "standard"
41
+
42
+ orchestrator = AgentOrchestrator(config=self._config)
43
+ blocks = []
44
+ any_ok = False
45
+ for symbol in symbols:
46
+ try:
47
+ ctx = orchestrator.run(symbol, market, mode=mode)
48
+ blocks.append(format_dashboard(build_dashboard(ctx)))
49
+ any_ok = True
50
+ except Exception as exc:
51
+ blocks.append(f"{symbol}: failed ({exc})")
52
+
53
+ return CommandResult(text="\n\n".join(blocks), success=any_ok)
@@ -0,0 +1,84 @@
1
+ """Shared parsing and number formatting for the research / market / portfolio commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Optional
7
+
8
+ MARKETS = ("us", "india", "china_a", "hong_kong", "crypto", "global")
9
+ MARKET_WORDS = {"in": "india", "nse": "india", "bse": "india", "usa": "us", "hk": "hong_kong", "cn": "china_a",
10
+ "china": "china_a"}
11
+
12
+
13
+ def market_word(token: str) -> Optional[str]:
14
+ t = token.lower()
15
+ return t if t in MARKETS else MARKET_WORDS.get(t)
16
+
17
+
18
+ def default_market(config) -> str:
19
+ """The Trade tab's market, as the trading commands use."""
20
+ try:
21
+ from ...trading.risk import load_settings
22
+
23
+ return load_settings(config).get("market") or "india"
24
+ except Exception:
25
+ return "india"
26
+
27
+
28
+ def guess_market(symbol: str, config) -> str:
29
+ """No market given: look the symbol up in the listings, preferring the
30
+ user's default market; fall back to the default."""
31
+ first = default_market(config)
32
+ order = [first] + [m for m in ("india", "us") if m != first]
33
+ if symbol.endswith("-USD"):
34
+ return "crypto"
35
+ try:
36
+ from ...data.listings import get_store
37
+
38
+ found = get_store().lookup([(m, kind, symbol) for m in order for kind in ("stock", "etf")])
39
+ except Exception:
40
+ return first
41
+ for m in order:
42
+ if any((m, kind, symbol) in found for kind in ("stock", "etf")):
43
+ return m
44
+ return first
45
+
46
+
47
+ def parse_symbol(args: str, config) -> tuple[Optional[str], str, list[str]]:
48
+ """"AAPL", "AAPL us", "us:AAPL", "RELIANCE india annual" ->
49
+ (symbol, market, remaining words). Symbol None when args are empty."""
50
+ parts = args.split()
51
+ if not parts:
52
+ return None, default_market(config), []
53
+ head, rest = parts[0], parts[1:]
54
+ market = None
55
+ if ":" in head:
56
+ m, _, sym = head.partition(":")
57
+ if market_word(m):
58
+ market, head = market_word(m), sym
59
+ if market is None and rest and market_word(rest[0]):
60
+ market, rest = market_word(rest[0]), rest[1:]
61
+ symbol = head.upper()
62
+ return symbol, market or guess_market(symbol, config), rest
63
+
64
+
65
+ def num(v, digits: int = 2) -> str:
66
+ if v is None or (isinstance(v, float) and not math.isfinite(v)):
67
+ return "n/a"
68
+ return f"{v:,.{digits}f}"
69
+
70
+
71
+ def pct(v, digits: int = 1, sign: bool = True) -> str:
72
+ if v is None or (isinstance(v, float) and not math.isfinite(v)):
73
+ return "n/a"
74
+ return f"{v:+.{digits}f}%" if sign else f"{v:.{digits}f}%"
75
+
76
+
77
+ def big(v) -> str:
78
+ """4.87e12 -> 4.87T"""
79
+ if v is None:
80
+ return "n/a"
81
+ for unit, size in (("T", 1e12), ("B", 1e9), ("M", 1e6), ("K", 1e3)):
82
+ if abs(v) >= size:
83
+ return f"{v / size:,.2f}{unit}"
84
+ return f"{v:,.0f}"
@@ -0,0 +1,43 @@
1
+ """/help - lists every registered command.
2
+
3
+ Needs a reference to the dispatcher's registry, which creates an
4
+ ordering requirement rather than a real circular-construction problem:
5
+ register every other command first, then construct and register
6
+ HelpCommand(dispatcher) last - by then the dispatcher already holds
7
+ every other command, and HelpCommand just reads from it at call time
8
+ (not at construction time), so it also correctly lists itself.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from ..models import CommandResult, IncomingMessage
14
+ from .base import Command
15
+
16
+
17
+ class HelpCommand(Command):
18
+ name = "help"
19
+ aliases = ["h", "?"]
20
+ usage = "/help"
21
+ description = "List every command."
22
+ group = "System"
23
+ example = "/help"
24
+ help_text = "/help - list available commands"
25
+
26
+ def __init__(self, dispatcher):
27
+ self._dispatcher = dispatcher
28
+
29
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
30
+ want = args.strip().lstrip("/").lower()
31
+ if want:
32
+ command = self._dispatcher.get_command(want)
33
+ if command is None:
34
+ return CommandResult(text=f"No command '{want}'. /help lists them.", success=False)
35
+ d = command.describe()
36
+ aliases = f"\nAlso: {', '.join('/' + a for a in d['aliases'])}" if d["aliases"] else ""
37
+ return CommandResult(text=f"{d['usage']}\n{d['description']}\nExample: {d['example']}{aliases}")
38
+ lines = []
39
+ for group, commands in self._dispatcher.groups().items():
40
+ lines.append(f"\n{group}" if lines else group)
41
+ lines += [f" {c.usage or '/' + c.name} - {c.description or c.help_line()}" for c in commands]
42
+ lines.append("\n/help COMMAND for details and an example.")
43
+ return CommandResult(text="\n".join(lines))
@@ -0,0 +1,153 @@
1
+ """Market-wide views from chat: the Markets page's pulse and sector
2
+ rotation, the ready-made screens and the events calendar."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from ...utils.deadline import call_with_deadline
7
+ from ..models import CommandResult, IncomingMessage
8
+ from .base import Command
9
+ from .common import default_market, market_word, num, pct
10
+
11
+ GROUP = "Markets"
12
+
13
+
14
+ class _MarketCommand(Command):
15
+ group = GROUP
16
+
17
+ def __init__(self, config):
18
+ self._config = config
19
+
20
+ def _market(self, parts: list[str]) -> str:
21
+ return next((market_word(p) for p in parts if market_word(p)), None) or default_market(self._config)
22
+
23
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
24
+ try:
25
+ return CommandResult(text=self.run(args.split()))
26
+ except (ValueError, TimeoutError) as exc:
27
+ return CommandResult(text=str(exc), success=False)
28
+
29
+ def run(self, parts: list[str]) -> str:
30
+ raise NotImplementedError
31
+
32
+
33
+ class MarketCommand(_MarketCommand):
34
+ name = "market"
35
+ aliases = ["pulse", "m"]
36
+ usage = "/market [market]"
37
+ description = "Market pulse: index levels, breadth, top gainers and losers, sector moves today."
38
+ example = "/market india"
39
+
40
+ def run(self, parts):
41
+ from ...services.discovery import DISCOVERY_MARKETS, build
42
+
43
+ market = self._market(parts)
44
+ if market not in DISCOVERY_MARKETS:
45
+ raise ValueError(f"Market pulse covers: {', '.join(DISCOVERY_MARKETS)}")
46
+ d = build(market, 500)
47
+ lines = [f"Market pulse · {market} (largest {d['universe']})"]
48
+ for i in d.get("indices") or []:
49
+ lines.append(f"{i['name']} {num(i.get('price'))} ({pct(i.get('change_percent'), 2)})")
50
+ b = d.get("breadth") or {}
51
+ if b.get("total"):
52
+ lines.append(f"Breadth: {b['advancers']} up / {b['decliners']} down of {b['total']}")
53
+ for key, title in (("gainers", "Top gainers"), ("losers", "Top losers")):
54
+ rows = (d.get(key) or [])[:5]
55
+ if rows:
56
+ lines.append(f"{title}: " + ", ".join(f"{r['symbol']} {pct(r.get('change_percent'))}" for r in rows))
57
+ secs = d.get("sectors") or []
58
+ if secs:
59
+ lines.append("Sectors: " + ", ".join(f"{s['sector']} {pct(s.get('change_percent'), 2)}" for s in secs[:4])
60
+ + " … " + ", ".join(f"{s['sector']} {pct(s.get('change_percent'), 2)}" for s in secs[-3:]))
61
+ return "\n".join(lines)
62
+
63
+
64
+ class SectorsCommand(_MarketCommand):
65
+ name = "sectors"
66
+ aliases = ["rotation", "rrg"]
67
+ usage = "/sectors [market]"
68
+ description = "Sector rotation: which sectors are leading, weakening, lagging or improving vs the index."
69
+ example = "/sectors india"
70
+
71
+ def run(self, parts):
72
+ from ...services.rotation import ROTATION_MARKETS, build
73
+
74
+ market = self._market(parts)
75
+ if market not in ROTATION_MARKETS:
76
+ raise ValueError(f"Sector rotation covers: {', '.join(ROTATION_MARKETS)}")
77
+ d = call_with_deadline(build, 90, market)
78
+ lines = [f"Sector rotation vs {d['benchmark']['name']}, weekly to {d.get('as_of')}"]
79
+ for q, title in (("leading", "Leading"), ("improving", "Improving"), ("weakening", "Weakening"), ("lagging", "Lagging")):
80
+ rows = [s for s in d.get("sectors") or [] if s.get("quadrant") == q]
81
+ if rows:
82
+ lines.append(f"{title}: " + ", ".join(f"{s['sector']} ({pct(s.get('return_4w'))} 4w)" for s in rows))
83
+ lines.append("Cycle: improving → leading → weakening → lagging.")
84
+ return "\n".join(lines)
85
+
86
+
87
+ class ScreenCommand(_MarketCommand):
88
+ name = "screen"
89
+ aliases = ["screens"]
90
+ usage = "/screen [KEY] [india|us]"
91
+ description = "List the ready-made fundamental screens, or run one over the largest stocks."
92
+ example = "/screen undervalued_large india"
93
+
94
+ def run(self, parts):
95
+ from ...services.screens import PRESETS, list_presets, run
96
+
97
+ keys = [p for p in parts if not market_word(p)]
98
+ if not keys:
99
+ lines = ["Screens (run one with /screen KEY [india|us]):"]
100
+ lines += [f"• {p['key']}: {p['title']} - {p['description']}" for p in list_presets()]
101
+ return "\n".join(lines)
102
+ key = keys[0].lower()
103
+ if key not in PRESETS:
104
+ match = [k for k in PRESETS if key in k]
105
+ if len(match) != 1:
106
+ raise ValueError(f"Unknown screen '{key}'. /screen lists them.")
107
+ key = match[0]
108
+ market = self._market(parts)
109
+ if market not in ("india", "us"):
110
+ raise ValueError("Screens cover the India and US markets.")
111
+ d = call_with_deadline(run, 90, key, market)
112
+ rows = d.get("results") or []
113
+ lines = [f"{d['title']} ({market}): {len(rows)} of {d.get('scanned')} stocks pass",
114
+ "Rules: " + "; ".join(d.get("rules") or [])]
115
+ for r in rows[:15]:
116
+ lines.append(f"• {r['symbol']} {num(r.get('price'))} · P/E {num(r.get('pe'), 1)} · "
117
+ f"ROE {pct(r.get('roe'), 1, False)}")
118
+ return "\n".join(lines)
119
+
120
+
121
+ class CalendarCommand(_MarketCommand):
122
+ name = "calendar"
123
+ aliases = ["events", "cal"]
124
+ usage = "/calendar [DAYS]"
125
+ description = "Upcoming results and dividends for stocks you follow, plus central bank and economic events and market holidays."
126
+ example = "/calendar 14"
127
+
128
+ def run(self, parts):
129
+ from datetime import datetime
130
+ from zoneinfo import ZoneInfo
131
+
132
+ from ...services import econ_calendar, market_calendar
133
+ from ...services.calendar import collect, followed_symbols, load_settings
134
+
135
+ days = next((int(p) for p in parts if p.isdigit()), 14)
136
+ days = max(1, min(days, 120))
137
+ settings = load_settings(self._config)
138
+ tz = settings.get("timezone") or "Asia/Kolkata"
139
+ today = datetime.now(ZoneInfo(tz)).date()
140
+ events = collect(followed_symbols(self._config), today, days)
141
+ econ = econ_calendar.collect(today, days)["events"]
142
+ markets = [e for e in market_calendar.collect(today, days)["events"]
143
+ if e["key"] in ("market_holiday", "short_day", "special_session")]
144
+ lines = [f"Next {days} days"]
145
+ for e in econ[:10]:
146
+ lines.append(f"• {e['date']} {econ_calendar.local_time(e, tz) or ''} {e['title']} ({e.get('region')})".replace(" ", " "))
147
+ for e in markets[:10]:
148
+ lines.append(f"• {e['date']} {e['title']}")
149
+ for e in events[:15]:
150
+ lines.append(f"• {e['date']} {e['symbol']} ({e['market']}): {e['label']}")
151
+ if len(lines) == 1:
152
+ lines.append("Nothing scheduled. Add stocks to a watchlist or portfolio to track their results.")
153
+ return "\n".join(lines)
@@ -0,0 +1,145 @@
1
+ """Your own data from chat: portfolio value and holdings, the portfolio
2
+ review (marketmyna/portfolio/review.py) and watchlists. In accounts mode the
3
+ caller's tenant scope (web request or the linked Telegram chat) limits
4
+ every query to that user's rows."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from ..models import CommandResult, IncomingMessage
9
+ from .base import Command
10
+ from .common import big, num, pct
11
+
12
+ GROUP = "Portfolio"
13
+
14
+
15
+ def _manager(config):
16
+ from ...portfolio.manager import PortfolioManager
17
+
18
+ return PortfolioManager(config.data_dir, config=config)
19
+
20
+
21
+ def _pick(manager, args: str) -> tuple[str | None, str]:
22
+ """Portfolio by (case-insensitive, prefix) name; the first one when no name is given."""
23
+ names = manager.get_all_portfolios()
24
+ if not names:
25
+ return None, "No portfolios yet. Create one on the Portfolio page."
26
+ want = args.strip().lower()
27
+ if not want:
28
+ return names[0], ""
29
+ match = next((n for n in names if n.lower() == want), None) or next((n for n in names if n.lower().startswith(want)), None)
30
+ if match is None:
31
+ return None, f"No portfolio '{args.strip()}'. You have: {', '.join(names)}"
32
+ return match, ""
33
+
34
+
35
+ class PortfolioCommand(Command):
36
+ name = "portfolio"
37
+ aliases = ["pf", "holdings"]
38
+ usage = "/portfolio [NAME]"
39
+ description = "Portfolio value, profit / loss and every holding with its weight and return."
40
+ example = "/portfolio"
41
+ group = GROUP
42
+
43
+ def __init__(self, config):
44
+ self._config = config
45
+
46
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
47
+ manager = _manager(self._config)
48
+ name, error = _pick(manager, args)
49
+ if not name:
50
+ return CommandResult(text=error, success=not args.strip())
51
+ s = manager.get_portfolio_summary(name)
52
+ cur = s.get("currency") or ""
53
+ holdings = sorted(s["holdings"].values(), key=lambda h: -(h.get("market_value") or 0))
54
+ total = s.get("total_market_value") or 0
55
+ lines = [f"{name} · {cur} {num(s.get('total_value'))} "
56
+ f"(invested {num(s.get('total_cost_basis'))}, P/L {num(s.get('total_unrealized_gain'))} "
57
+ f"{pct(s.get('total_unrealized_gain_percent'))})"]
58
+ if s.get("cash_balance"):
59
+ lines.append(f"Cash {num(s['cash_balance'])}")
60
+ if not holdings:
61
+ lines.append("No holdings yet.")
62
+ for h in holdings[:25]:
63
+ w = (h.get("market_value") or 0) / total * 100 if total else 0
64
+ lines.append(f"• {h['symbol']} ({h.get('market')}) {num(h.get('quantity'), 4).rstrip('0').rstrip('.')} × "
65
+ f"{num(h.get('current_price'))} = {big(h.get('market_value'))} · {w:.1f}% · "
66
+ f"{pct(h.get('unrealized_gain_percent'))}")
67
+ if len(holdings) > 25:
68
+ lines.append(f"… and {len(holdings) - 25} more")
69
+ others = [n for n in manager.get_all_portfolios() if n != name]
70
+ if others:
71
+ lines.append(f"Other portfolios: {', '.join(others)}")
72
+ return CommandResult(text="\n".join(lines))
73
+
74
+
75
+ class ReviewCommand(Command):
76
+ name = "review"
77
+ aliases = ["checkup"]
78
+ usage = "/review [NAME]"
79
+ description = "Portfolio review: what could be changed, why it matters and how, checked against your limits."
80
+ example = "/review"
81
+ group = GROUP
82
+
83
+ def __init__(self, config):
84
+ self._config = config
85
+
86
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
87
+ from ...portfolio.review import for_portfolio
88
+
89
+ manager = _manager(self._config)
90
+ name, error = _pick(manager, args)
91
+ if not name:
92
+ return CommandResult(text=error, success=not args.strip())
93
+ r = for_portfolio(manager, name, self._config)
94
+ if r.get("score") is None:
95
+ return CommandResult(text=f"{name}: {r.get('summary')}")
96
+ lines = [f"{name} review: {r['score']}/100 ({r['grade']}) · {r['summary']}"]
97
+ marks = {"high": "🔴", "medium": "🟠", "low": "⚪"}
98
+ for i, f in enumerate(r["findings"], 1):
99
+ lines.append(f"\n{marks[f['severity']]} {i}. {f['title']}" + (f" ({f['rule']})" if f.get("rule") else ""))
100
+ lines.append(f" {f['fact']}")
101
+ lines.append(f" Why: {f['why']}")
102
+ for lever in f["levers"]:
103
+ lines.append(f" → {lever}")
104
+ if r.get("good"):
105
+ lines.append("\nWorking well:")
106
+ lines += [f"✓ {g}" for g in r["good"]]
107
+ return CommandResult(text="\n".join(lines))
108
+
109
+
110
+ class WatchlistCommand(Command):
111
+ name = "watchlist"
112
+ aliases = ["wl", "watch"]
113
+ usage = "/watchlist [NAME]"
114
+ description = "Your watchlists, or one list's stocks with price and day change."
115
+ example = "/watchlist"
116
+ group = GROUP
117
+
118
+ def __init__(self, config):
119
+ self._config = config
120
+
121
+ def handle(self, message: IncomingMessage, args: str) -> CommandResult:
122
+ from ...data.listings import get_store
123
+ from ...storage import DatabaseManager, WatchlistRecord, get_db_url
124
+
125
+ with DatabaseManager.instance(get_db_url(config=self._config)).get_session() as session:
126
+ lists = session.query(WatchlistRecord).order_by(WatchlistRecord.id).all()
127
+ if not lists:
128
+ return CommandResult(text="No watchlists yet. Star a stock on Markets or Analyze to start one.")
129
+ want = args.strip().lower()
130
+ record = (next((w for w in lists if w.name.lower() == want), None)
131
+ or next((w for w in lists if w.name.lower().startswith(want)), None)) if want else (
132
+ lists[0] if len(lists) == 1 else None)
133
+ if record is None:
134
+ if want:
135
+ return CommandResult(text=f"No watchlist '{args.strip()}'.", success=False)
136
+ return CommandResult(text="Your watchlists:\n" + "\n".join(
137
+ f"• {w.name} ({len(w.items)} stocks)" for w in lists) + "\nShow one with /watchlist NAME")
138
+ items = [(i.symbol, i.market, i.type) for i in record.items]
139
+ title = record.name
140
+ live = get_store().lookup([(m, t, s) for s, m, t in items]) if items else {}
141
+ lines = [f"{title} ({len(items)})"]
142
+ for s, m, t in items[:40]:
143
+ row = live.get((m, t, s)) or {}
144
+ lines.append(f"• {s} ({m}) {num(row.get('price'))} {pct(row.get('change_percent'))}" if row else f"• {s} ({m})")
145
+ return CommandResult(text="\n".join(lines))