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,81 @@
1
+ """SIP (Systematic Investment Plan) Optimization Module."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from marketmyna.data.mutual_funds import MutualFundDataFetcher
7
+
8
+
9
+ @dataclass
10
+ class StepUpSIPProjection:
11
+ """Step-up SIP projection."""
12
+ base_amount: float
13
+ annual_increase_pct: float
14
+ duration_years: int
15
+ expected_return_pct: float
16
+ total_invested: float
17
+ projected_corpus: float
18
+ projected_gain: float
19
+ yearly_breakdown: list[dict]
20
+
21
+
22
+
23
+ class SIPOptimizer:
24
+ """SIP optimization and analysis tools."""
25
+
26
+ def __init__(self, data_fetcher: Optional[MutualFundDataFetcher] = None):
27
+ self.data_fetcher = data_fetcher or MutualFundDataFetcher()
28
+
29
+
30
+
31
+
32
+ def calculate_step_up_sip(
33
+ self,
34
+ base_amount: float,
35
+ annual_increase_pct: float,
36
+ duration_years: int,
37
+ expected_return_pct: float,
38
+ ) -> StepUpSIPProjection:
39
+ """
40
+ Calculate step-up SIP projection where SIP amount increases annually.
41
+ """
42
+ monthly_rate = expected_return_pct / 100 / 12
43
+
44
+ total_invested = 0
45
+ corpus = 0
46
+ yearly_breakdown = []
47
+ current_amount = base_amount
48
+
49
+ for year in range(1, duration_years + 1):
50
+ year_invested = 0
51
+ year_start_corpus = corpus
52
+
53
+ for month in range(12):
54
+ # Add this month's investment
55
+ corpus = corpus * (1 + monthly_rate) + current_amount
56
+ year_invested += current_amount
57
+ total_invested += current_amount
58
+
59
+ yearly_breakdown.append({
60
+ 'year': year,
61
+ 'monthly_sip': current_amount,
62
+ 'year_invested': year_invested,
63
+ 'year_end_corpus': corpus,
64
+ 'year_growth': corpus - year_start_corpus - year_invested,
65
+ })
66
+
67
+ # Increase for next year
68
+ current_amount = current_amount * (1 + annual_increase_pct / 100)
69
+
70
+ projected_gain = corpus - total_invested
71
+
72
+ return StepUpSIPProjection(
73
+ base_amount=base_amount,
74
+ annual_increase_pct=annual_increase_pct,
75
+ duration_years=duration_years,
76
+ expected_return_pct=expected_return_pct,
77
+ total_invested=total_invested,
78
+ projected_corpus=corpus,
79
+ projected_gain=projected_gain,
80
+ yearly_breakdown=yearly_breakdown,
81
+ )
@@ -0,0 +1,321 @@
1
+ """Technical analysis indicators and signals."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+
10
+ class Signal(Enum):
11
+ """Trading signal types."""
12
+ STRONG_BUY = "strong_buy"
13
+ BUY = "buy"
14
+ NEUTRAL = "neutral"
15
+ SELL = "sell"
16
+ STRONG_SELL = "strong_sell"
17
+
18
+
19
+ @dataclass
20
+ class IndicatorResult:
21
+ """Result of a technical indicator calculation."""
22
+ name: str
23
+ value: float
24
+ signal: Signal
25
+ description: str = ""
26
+
27
+
28
+ class TechnicalAnalyzer:
29
+ """Technical analysis calculator."""
30
+
31
+ def __init__(self, df: pd.DataFrame):
32
+ """
33
+ Initialize with OHLCV dataframe.
34
+
35
+ Args:
36
+ df: DataFrame with columns: open, high, low, close, volume
37
+ """
38
+ self.df = df.copy()
39
+ self._ensure_columns()
40
+
41
+ def _ensure_columns(self):
42
+ """Ensure required columns exist with proper names."""
43
+ col_map = {
44
+ "Open": "open",
45
+ "High": "high",
46
+ "Low": "low",
47
+ "Close": "close",
48
+ "Volume": "volume",
49
+ }
50
+ for old, new in col_map.items():
51
+ if old in self.df.columns and new not in self.df.columns:
52
+ self.df[new] = self.df[old]
53
+
54
+ # Moving Averages
55
+ def sma(self, period: int = 20, column: str = "close") -> pd.Series:
56
+ """Simple Moving Average."""
57
+ return self.df[column].rolling(window=period).mean()
58
+
59
+ def ema(self, period: int = 20, column: str = "close") -> pd.Series:
60
+ """Exponential Moving Average."""
61
+ return self.df[column].ewm(span=period, adjust=False).mean()
62
+
63
+
64
+ # Momentum Indicators
65
+ def rsi(self, period: int = 14) -> pd.Series:
66
+ """Relative Strength Index, Wilder smoothing - the same formula as
67
+ broker charts and the rest of the app (marketmyna/trading/indicators.py)."""
68
+ from ..trading.indicators import rsi
69
+
70
+ return rsi(self.df["close"].astype(float), period)
71
+
72
+ def macd(
73
+ self, fast: int = 12, slow: int = 26, signal: int = 9
74
+ ) -> tuple[pd.Series, pd.Series, pd.Series]:
75
+ """
76
+ MACD - Moving Average Convergence Divergence.
77
+
78
+ Returns: (macd_line, signal_line, histogram)
79
+ """
80
+ ema_fast = self.ema(fast)
81
+ ema_slow = self.ema(slow)
82
+ macd_line = ema_fast - ema_slow
83
+ signal_line = macd_line.ewm(span=signal, adjust=False).mean()
84
+ histogram = macd_line - signal_line
85
+
86
+ return macd_line, signal_line, histogram
87
+
88
+ def stochastic(
89
+ self, k_period: int = 14, d_period: int = 3
90
+ ) -> tuple[pd.Series, pd.Series]:
91
+ """
92
+ Stochastic Oscillator.
93
+
94
+ Returns: (k_line, d_line)
95
+ """
96
+ low_min = self.df["low"].rolling(window=k_period).min()
97
+ high_max = self.df["high"].rolling(window=k_period).max()
98
+
99
+ k = 100 * (self.df["close"] - low_min) / (high_max - low_min)
100
+ d = k.rolling(window=d_period).mean()
101
+
102
+ return k, d
103
+
104
+ def williams_r(self, period: int = 14) -> pd.Series:
105
+ """Williams %R."""
106
+ high_max = self.df["high"].rolling(window=period).max()
107
+ low_min = self.df["low"].rolling(window=period).min()
108
+
109
+ return -100 * (high_max - self.df["close"]) / (high_max - low_min)
110
+
111
+ def cci(self, period: int = 20) -> pd.Series:
112
+ """Commodity Channel Index."""
113
+ typical_price = (self.df["high"] + self.df["low"] + self.df["close"]) / 3
114
+ sma_tp = typical_price.rolling(window=period).mean()
115
+ mean_dev = typical_price.rolling(window=period).apply(
116
+ lambda x: np.abs(x - x.mean()).mean()
117
+ )
118
+
119
+ return (typical_price - sma_tp) / (0.015 * mean_dev)
120
+
121
+
122
+ # Volatility Indicators
123
+ def bollinger_bands(
124
+ self, period: int = 20, std_dev: float = 2.0
125
+ ) -> tuple[pd.Series, pd.Series, pd.Series]:
126
+ """
127
+ Bollinger Bands.
128
+
129
+ Returns: (upper_band, middle_band, lower_band)
130
+ """
131
+ middle = self.sma(period)
132
+ std = self.df["close"].rolling(window=period).std()
133
+ upper = middle + (std * std_dev)
134
+ lower = middle - (std * std_dev)
135
+
136
+ return upper, middle, lower
137
+
138
+ def atr(self, period: int = 14) -> pd.Series:
139
+ """Average True Range."""
140
+ high = self.df["high"]
141
+ low = self.df["low"]
142
+ close = self.df["close"]
143
+
144
+ tr1 = high - low
145
+ tr2 = abs(high - close.shift())
146
+ tr3 = abs(low - close.shift())
147
+
148
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
149
+ return tr.rolling(window=period).mean()
150
+
151
+
152
+ # Volume Indicators
153
+ def obv(self) -> pd.Series:
154
+ """On-Balance Volume."""
155
+ obv = [0]
156
+ for i in range(1, len(self.df)):
157
+ if self.df["close"].iloc[i] > self.df["close"].iloc[i - 1]:
158
+ obv.append(obv[-1] + self.df["volume"].iloc[i])
159
+ elif self.df["close"].iloc[i] < self.df["close"].iloc[i - 1]:
160
+ obv.append(obv[-1] - self.df["volume"].iloc[i])
161
+ else:
162
+ obv.append(obv[-1])
163
+ return pd.Series(obv, index=self.df.index)
164
+
165
+ def vwap(self) -> pd.Series:
166
+ """Volume Weighted Average Price."""
167
+ typical_price = (self.df["high"] + self.df["low"] + self.df["close"]) / 3
168
+ return (typical_price * self.df["volume"]).cumsum() / self.df["volume"].cumsum()
169
+
170
+ def mfi(self, period: int = 14) -> pd.Series:
171
+ """Money Flow Index."""
172
+ typical_price = (self.df["high"] + self.df["low"] + self.df["close"]) / 3
173
+ money_flow = typical_price * self.df["volume"]
174
+
175
+ positive_flow = money_flow.where(typical_price > typical_price.shift(), 0)
176
+ negative_flow = money_flow.where(typical_price < typical_price.shift(), 0)
177
+
178
+ positive_mf = positive_flow.rolling(window=period).sum()
179
+ negative_mf = negative_flow.rolling(window=period).sum()
180
+
181
+ mfi = 100 - (100 / (1 + positive_mf / negative_mf))
182
+ return mfi
183
+
184
+ # Trend Indicators
185
+ def adx(self, period: int = 14) -> pd.Series:
186
+ """Average Directional Index."""
187
+ high = self.df["high"]
188
+ low = self.df["low"]
189
+
190
+ plus_dm = high.diff()
191
+ minus_dm = low.diff().abs()
192
+
193
+ plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0)
194
+ minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0)
195
+
196
+ atr_val = self.atr(period)
197
+
198
+ plus_di = 100 * (plus_dm.rolling(window=period).mean() / atr_val)
199
+ minus_di = 100 * (minus_dm.rolling(window=period).mean() / atr_val)
200
+
201
+ dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
202
+ adx = dx.rolling(window=period).mean()
203
+
204
+ return adx
205
+
206
+ def supertrend(
207
+ self, period: int = 10, multiplier: float = 3.0
208
+ ) -> tuple[pd.Series, pd.Series]:
209
+ """
210
+ Supertrend indicator.
211
+
212
+ Returns: (supertrend, direction) direction: 1=uptrend, -1=downtrend
213
+ """
214
+ atr_val = self.atr(period)
215
+ hl2 = (self.df["high"] + self.df["low"]) / 2
216
+
217
+ upper_band = hl2 + (multiplier * atr_val)
218
+ lower_band = hl2 - (multiplier * atr_val)
219
+
220
+ supertrend = pd.Series(index=self.df.index, dtype=float)
221
+ direction = pd.Series(index=self.df.index, dtype=float)
222
+
223
+ for i in range(period, len(self.df)):
224
+ if self.df["close"].iloc[i] > upper_band.iloc[i - 1]:
225
+ direction.iloc[i] = 1
226
+ elif self.df["close"].iloc[i] < lower_band.iloc[i - 1]:
227
+ direction.iloc[i] = -1
228
+ else:
229
+ direction.iloc[i] = direction.iloc[i - 1]
230
+
231
+ if direction.iloc[i] == 1:
232
+ supertrend.iloc[i] = lower_band.iloc[i]
233
+ else:
234
+ supertrend.iloc[i] = upper_band.iloc[i]
235
+
236
+ return supertrend, direction
237
+
238
+ # Analysis Summary
239
+ def get_signals(self) -> dict[str, IndicatorResult]:
240
+ """Get trading signals from multiple indicators."""
241
+ signals = {}
242
+ close = self.df["close"].iloc[-1]
243
+
244
+ # RSI Signal
245
+ rsi_val = self.rsi().iloc[-1]
246
+ if rsi_val < 30:
247
+ rsi_signal = Signal.BUY
248
+ elif rsi_val > 70:
249
+ rsi_signal = Signal.SELL
250
+ else:
251
+ rsi_signal = Signal.NEUTRAL
252
+ signals["rsi"] = IndicatorResult("RSI", rsi_val, rsi_signal)
253
+
254
+ # MACD Signal
255
+ macd_line, signal_line, _ = self.macd()
256
+ macd_val = macd_line.iloc[-1]
257
+ if macd_line.iloc[-1] > signal_line.iloc[-1] and macd_line.iloc[-2] <= signal_line.iloc[-2]:
258
+ macd_signal = Signal.BUY
259
+ elif macd_line.iloc[-1] < signal_line.iloc[-1] and macd_line.iloc[-2] >= signal_line.iloc[-2]:
260
+ macd_signal = Signal.SELL
261
+ else:
262
+ macd_signal = Signal.NEUTRAL
263
+ signals["macd"] = IndicatorResult("MACD", macd_val, macd_signal)
264
+
265
+ # Moving Average Signal
266
+ sma20 = self.sma(20).iloc[-1]
267
+ sma50 = self.sma(50).iloc[-1]
268
+ if close > sma20 > sma50:
269
+ ma_signal = Signal.BUY
270
+ elif close < sma20 < sma50:
271
+ ma_signal = Signal.SELL
272
+ else:
273
+ ma_signal = Signal.NEUTRAL
274
+ signals["ma"] = IndicatorResult("Moving Averages", sma20, ma_signal)
275
+
276
+ # Bollinger Bands Signal
277
+ upper, middle, lower = self.bollinger_bands()
278
+ bb_position = (close - lower.iloc[-1]) / (upper.iloc[-1] - lower.iloc[-1])
279
+ if bb_position < 0.2:
280
+ bb_signal = Signal.BUY
281
+ elif bb_position > 0.8:
282
+ bb_signal = Signal.SELL
283
+ else:
284
+ bb_signal = Signal.NEUTRAL
285
+ signals["bollinger"] = IndicatorResult("Bollinger Bands", bb_position * 100, bb_signal)
286
+
287
+ # Stochastic Signal
288
+ k, d = self.stochastic()
289
+ stoch_val = k.iloc[-1]
290
+ if stoch_val < 20:
291
+ stoch_signal = Signal.BUY
292
+ elif stoch_val > 80:
293
+ stoch_signal = Signal.SELL
294
+ else:
295
+ stoch_signal = Signal.NEUTRAL
296
+ signals["stochastic"] = IndicatorResult("Stochastic", stoch_val, stoch_signal)
297
+
298
+ return signals
299
+
300
+ def get_summary(self) -> dict:
301
+ """Get overall analysis summary."""
302
+ signals = self.get_signals()
303
+
304
+ buy_count = sum(1 for s in signals.values() if s.signal in [Signal.BUY, Signal.STRONG_BUY])
305
+ sell_count = sum(1 for s in signals.values() if s.signal in [Signal.SELL, Signal.STRONG_SELL])
306
+ neutral_count = sum(1 for s in signals.values() if s.signal == Signal.NEUTRAL)
307
+
308
+ if buy_count > sell_count + neutral_count:
309
+ overall = Signal.BUY
310
+ elif sell_count > buy_count + neutral_count:
311
+ overall = Signal.SELL
312
+ else:
313
+ overall = Signal.NEUTRAL
314
+
315
+ return {
316
+ "overall_signal": overall,
317
+ "buy_signals": buy_count,
318
+ "sell_signals": sell_count,
319
+ "neutral_signals": neutral_count,
320
+ "indicators": {name: {"value": r.value, "signal": r.signal.value} for name, r in signals.items()},
321
+ }
@@ -0,0 +1 @@
1
+ """FastAPI backend for MarketMyna (self-hosted web app)."""
marketmyna/api/app.py ADDED
@@ -0,0 +1,209 @@
1
+ """FastAPI application factory.
2
+
3
+ Auth: single-user build, every request runs as the local owner
4
+ (marketmyna/api/middlewares/auth.py).
5
+
6
+ Phase 5 adds a lifespan: RuntimeSchedulerService is started on app
7
+ startup and stopped on shutdown, stashed on app.state.scheduler so
8
+ endpoints can reach the one running instance (marketmyna/api/deps.py's
9
+ get_scheduler_service pulls it from there, matching the same
10
+ "shared services live on app.state" pattern this whole API follows).
11
+
12
+ Phase 6 adds a second background service to the same lifespan:
13
+ TelegramListener (marketmyna/bot/telegram_listener.py), started only if
14
+ config.telegram_bot_token is set (a no-op, logged, not an error,
15
+ otherwise), stashed on app.state.telegram_listener and app.state.bot_dispatcher.
16
+
17
+ The lifespan also warms the market listing cache (marketmyna/data/listings.py)
18
+ on a daemon thread so the Markets page and symbol search are instant.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import threading
25
+ import time
26
+ from contextlib import asynccontextmanager
27
+ from pathlib import Path
28
+
29
+ from fastapi import FastAPI, HTTPException
30
+ from fastapi.middleware.cors import CORSMiddleware
31
+ from fastapi.responses import FileResponse
32
+
33
+ from marketmyna import __version__
34
+ from marketmyna.api.deps import get_config_dep
35
+ from marketmyna.api.middlewares.auth import AuthMiddleware, api_docs_enabled, check_auth_config
36
+ from marketmyna.api.middlewares.auth import router as auth_router
37
+ from marketmyna.api.middlewares.csrf import CSRFMiddleware
38
+ from marketmyna.api.middlewares.error_handler import add_error_handlers
39
+ from marketmyna.api.middlewares.slashes import StripTrailingSlashMiddleware
40
+ from marketmyna.api.v1.endpoints.account import router as account_router
41
+ from marketmyna.api.v1.router import router as api_v1_router
42
+ from marketmyna.bot.registry import build_default_dispatcher
43
+ from marketmyna.bot.telegram_listener import TelegramListener
44
+ from marketmyna.core import tenant
45
+ from marketmyna.data.listings import get_store
46
+ from marketmyna.services.runtime_scheduler import RuntimeSchedulerService
47
+ from marketmyna.trading.alerts import AlertMonitor
48
+
49
+ # The server keeps times as naive UTC (datetime.now() in the UTC container) and
50
+ # the frontend's serverTime() reads them as UTC; pin the zone so a dev machine
51
+ # in another zone writes the same times as the server.
52
+ os.environ["TZ"] = "UTC"
53
+ if hasattr(time, "tzset"):
54
+ time.tzset()
55
+ else: # Windows: no time.tzset(); the C runtime re-reads TZ ("UTC0": UTC, no daylight saving) on _tzset()
56
+ os.environ["TZ"] = "UTC0"
57
+ try:
58
+ import ctypes
59
+
60
+ ctypes.CDLL("ucrtbase")._tzset()
61
+ except (OSError, AttributeError):
62
+ pass
63
+
64
+ DEFAULT_CORS_ORIGINS = [
65
+ "http://localhost:5174",
66
+ "http://localhost:3000",
67
+ "http://127.0.0.1:5174",
68
+ "http://127.0.0.1:3000",
69
+ ]
70
+ # Extra allowed origins for a frontend on another domain that calls the API
71
+ # directly (comma-separated). Not needed normally: the API serves the built
72
+ # frontend itself (serve_frontend below), so both share one origin.
73
+ EXTRA_CORS_ORIGINS = [o.strip() for o in os.environ.get("CORS_ORIGINS", "").split(",") if o.strip()]
74
+
75
+ # The built frontend: frontend/dist in a git checkout (cd frontend && npm run build),
76
+ # or marketmyna/web inside the installed package.
77
+ _PACKAGE = Path(__file__).resolve().parent.parent
78
+ FRONTEND_DIST = next((d for d in (_PACKAGE.parent / "frontend" / "dist", _PACKAGE / "web")
79
+ if (d / "index.html").is_file()), _PACKAGE / "web")
80
+
81
+
82
+ @asynccontextmanager
83
+ async def app_lifespan(app: FastAPI):
84
+ config = get_config_dep()
85
+ if getattr(config, "_plaintext_secrets", False):
86
+ # One-time upgrade: re-save so older plain-text keys are encrypted on disk.
87
+ config.save()
88
+
89
+ scheduler = RuntimeSchedulerService(config)
90
+ scheduler.start()
91
+ app.state.scheduler = scheduler
92
+
93
+ dispatcher = build_default_dispatcher(config, scheduler_running=scheduler.is_running)
94
+ app.state.bot_dispatcher = dispatcher
95
+
96
+ telegram_listener = TelegramListener(config, dispatcher)
97
+ telegram_listener.start()
98
+ app.state.telegram_listener = telegram_listener
99
+
100
+ # Trade section: price alerts, checked every ALERT_POLL_SECONDS while markets are open.
101
+ alert_monitor = AlertMonitor(config, poll_seconds=int(os.environ.get("ALERT_POLL_SECONDS", "60")))
102
+ alert_monitor.start()
103
+ app.state.alert_monitor = alert_monitor
104
+
105
+ threading.Thread(target=get_store().rows, args=("all",), name="listings-warmup", daemon=True).start()
106
+
107
+ def _warm_funds():
108
+ import time as _t
109
+
110
+ _t.sleep(60) # after the listings warm-up
111
+ from marketmyna.services.fund_rotation import warm
112
+
113
+ warm()
114
+
115
+ threading.Thread(target=_warm_funds, name="fund-rotation-warmup", daemon=True).start()
116
+
117
+ from marketmyna.services.fund_holdings import refresh_loop
118
+
119
+ threading.Thread(target=refresh_loop, name="fund-portfolios", daemon=True).start()
120
+
121
+ from marketmyna.services.sip_days import refresh_loop as sip_days_loop
122
+
123
+ threading.Thread(target=sip_days_loop, name="sip-days", daemon=True).start()
124
+
125
+ from marketmyna.services.market_valuation import snapshot_loop as valuation_loop
126
+
127
+ threading.Thread(target=valuation_loop, name="market-valuation", daemon=True).start()
128
+
129
+ from marketmyna.services.india_flows import snapshot_loop as flows_loop
130
+
131
+ threading.Thread(target=flows_loop, name="india-flows", daemon=True).start()
132
+
133
+ from marketmyna.services.mf_lumpsum import alert_loop as fund_dip_loop
134
+
135
+ threading.Thread(target=fund_dip_loop, args=(config,), name="fund-dip-alerts", daemon=True).start()
136
+
137
+ from marketmyna.api.v1.endpoints.updates import check_loop as updates_loop
138
+
139
+ # Looks for a new MarketMyna release / newer code on GitHub (Settings -> Updates).
140
+ threading.Thread(target=updates_loop, name="update-check", daemon=True).start()
141
+
142
+ try:
143
+ yield
144
+ finally:
145
+ telegram_listener.stop()
146
+ alert_monitor.stop()
147
+ scheduler.stop()
148
+
149
+
150
+ def create_app() -> FastAPI:
151
+ check_auth_config()
152
+ # Every Yahoo Finance request on this server shares one pace and 429 cooldown.
153
+ from marketmyna.utils import yahoo_gate
154
+
155
+ yahoo_gate.install()
156
+ # Scope every owned-table query to the signed-in user.
157
+ tenant.enable(bool(get_config_dep().accounts_enabled))
158
+ docs = api_docs_enabled()
159
+ app = FastAPI(title="MarketMyna API", version=__version__, lifespan=app_lifespan, redirect_slashes=False,
160
+ docs_url="/docs" if docs else None, redoc_url="/redoc" if docs else None,
161
+ openapi_url="/openapi.json" if docs else None)
162
+
163
+ # Added before CORS so CORS ends up outermost and 401s still carry CORS headers.
164
+ app.add_middleware(AuthMiddleware)
165
+ # Cross-site writes are refused before any session lookup (marketmyna/api/middlewares/csrf.py).
166
+ app.add_middleware(CSRFMiddleware, allowed_origins=DEFAULT_CORS_ORIGINS + EXTRA_CORS_ORIGINS)
167
+ app.add_middleware(
168
+ CORSMiddleware,
169
+ allow_origins=DEFAULT_CORS_ORIGINS + EXTRA_CORS_ORIGINS,
170
+ allow_credentials=True,
171
+ allow_methods=["*"],
172
+ allow_headers=["*"],
173
+ )
174
+ app.add_middleware(StripTrailingSlashMiddleware)
175
+
176
+ add_error_handlers(app)
177
+
178
+ app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
179
+ app.include_router(account_router, prefix="/api/auth/account", tags=["account"])
180
+ app.include_router(api_v1_router, prefix="/api/v1")
181
+
182
+ @app.get("/health")
183
+ @app.get("/api/health")
184
+ def health() -> dict:
185
+ return {"status": "ok"}
186
+
187
+ serve_frontend(app)
188
+ return app
189
+
190
+
191
+ def serve_frontend(app: FastAPI, dist: Path = FRONTEND_DIST) -> None:
192
+ """Serves the built frontend at /, so one uvicorn process is the whole
193
+ app. Paths that are not files get index.html for the React router;
194
+ unknown /api/* paths stay 404s. Does nothing before the first build."""
195
+ dist = dist.resolve()
196
+ if not (dist / "index.html").is_file():
197
+ return
198
+
199
+ @app.get("/{path:path}", include_in_schema=False)
200
+ def frontend(path: str):
201
+ if path == "api" or path.startswith("api/"):
202
+ raise HTTPException(status_code=404)
203
+ file = (dist / path).resolve()
204
+ if path and file.is_file() and file.is_relative_to(dist):
205
+ return FileResponse(file)
206
+ return FileResponse(dist / "index.html")
207
+
208
+
209
+ app = create_app()
marketmyna/api/deps.py ADDED
@@ -0,0 +1,63 @@
1
+ """FastAPI dependency-injection functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ from typing import Iterator
7
+
8
+ from fastapi import Depends, Request
9
+ from sqlalchemy.orm import Session
10
+
11
+ from marketmyna.core.config import Config
12
+ from marketmyna.storage import DatabaseManager, get_db_url
13
+
14
+
15
+ @lru_cache(maxsize=1)
16
+ def get_config_dep() -> Config:
17
+ """Process-wide Config, loaded once per process."""
18
+ return Config.load()
19
+
20
+
21
+ def get_database_manager() -> DatabaseManager:
22
+ config = get_config_dep()
23
+ return DatabaseManager.instance(get_db_url(config=config))
24
+
25
+
26
+ def get_db_session(config: Config = Depends(get_config_dep)) -> Iterator[Session]:
27
+ """A request-scoped session: committed on success, rolled back on error."""
28
+ session = DatabaseManager.instance(get_db_url(config=config)).SessionLocal()
29
+ try:
30
+ yield session
31
+ session.commit()
32
+ except Exception:
33
+ session.rollback()
34
+ raise
35
+ finally:
36
+ session.close()
37
+
38
+
39
+ def get_scheduler_service(request: Request):
40
+ """The RuntimeSchedulerService started in marketmyna/api/app.py's lifespan, off
41
+ app.state - lazily created if somehow missing (e.g. a test app built
42
+ without going through the real lifespan) rather than 500ing."""
43
+ scheduler = getattr(request.app.state, "scheduler", None)
44
+ if scheduler is None:
45
+ from marketmyna.services.runtime_scheduler import RuntimeSchedulerService
46
+
47
+ scheduler = RuntimeSchedulerService(get_config_dep())
48
+ scheduler.start()
49
+ request.app.state.scheduler = scheduler
50
+ return scheduler
51
+
52
+
53
+ def get_bot_dispatcher(request: Request):
54
+ """The CommandDispatcher built in marketmyna/api/app.py's lifespan, off
55
+ app.state - lazily created if somehow missing, same fallback shape
56
+ as get_scheduler_service."""
57
+ dispatcher = getattr(request.app.state, "bot_dispatcher", None)
58
+ if dispatcher is None:
59
+ from marketmyna.bot.registry import build_default_dispatcher
60
+
61
+ dispatcher = build_default_dispatcher(get_config_dep())
62
+ request.app.state.bot_dispatcher = dispatcher
63
+ return dispatcher
File without changes