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,251 @@
1
+ """Persisted runtime-scheduler configuration: a singleton row (id=1) in
2
+ ScheduleConfigRecord, loaded/saved through the same DatabaseManager
3
+ pattern as PortfolioManager."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from typing import Optional
10
+
11
+ from ..storage import DatabaseManager, ScheduleConfigRecord, get_db_url
12
+
13
+
14
+ @dataclass
15
+ class ScheduleConfig:
16
+ enabled: bool = False
17
+ # Exactly one of these two should be set: interval_minutes for "every
18
+ # N minutes", time_of_day ("HH:MM") for "once daily". No cron grammar.
19
+ interval_minutes: Optional[int] = None
20
+ time_of_day: Optional[str] = None
21
+ symbols: list[str] = field(default_factory=list)
22
+ market: str = "us"
23
+ mode: str = "standard"
24
+ last_run_at: Optional[datetime] = None
25
+ last_run_status: Optional[str] = None
26
+ last_run_summary: Optional[str] = None
27
+ # Report content & delivery (marketmyna/services/report.py)
28
+ report_language: str = "en"
29
+ report_sections: Optional[list[str]] = None # None = default sections
30
+ stock_sections: dict[str, list[str]] = field(default_factory=dict)
31
+ channel_languages: dict[str, str] = field(default_factory=dict)
32
+ mf_codes: list[str] = field(default_factory=list)
33
+ # Smart messaging (marketmyna/services/smart_report.py)
34
+ smart_enabled: bool = False
35
+ smart_top_n: int = 5
36
+ smart_ai_top: int = 10
37
+ smart_email_full: bool = True
38
+ smart_file: str = "pdf" # "pdf" | "xlsx" | "none"
39
+ smart_file_channels: Optional[list[str]] = None # None = email and telegram
40
+ report_base_url: Optional[str] = None
41
+ # What to run: any of SOURCES. None = legacy config, i.e. ["custom"].
42
+ sources: Optional[list[str]] = None
43
+ watchlist_ids: list[int] = field(default_factory=list) # empty = all
44
+ portfolio_names: list[str] = field(default_factory=list) # empty = all
45
+ stock_count: Optional[int] = None # stocks a run covers: its cost in a time slot
46
+ area: Optional[str] = None # "trader" / "investor": only that area's sections; None = all
47
+
48
+ def run_sources(self) -> list[str]:
49
+ return [s for s in (self.sources if self.sources is not None else ["custom"]) if s in SOURCES]
50
+
51
+ def has_targets_configured(self) -> bool:
52
+ """Cheap due-check: something to run, without touching the DB."""
53
+ srcs = self.run_sources()
54
+ return bool(("custom" in srcs and self.symbols) or "watchlist" in srcs or "portfolio" in srcs)
55
+
56
+ def report_prefs(self, timezone: str = "Asia/Kolkata"):
57
+ from .report import DEFAULT_SECTIONS, SECTION_AREAS, ReportPrefs, clean_sections
58
+
59
+ def own_area(sections: list[str]) -> list[str]:
60
+ # The page keeps the other area's choices saved; the report leaves them out.
61
+ if self.area not in ("trader", "investor"):
62
+ return sections
63
+ return [s for s in sections if SECTION_AREAS.get(s, self.area) == self.area]
64
+
65
+ return ReportPrefs(
66
+ language=self.report_language or "en",
67
+ sections=own_area(clean_sections(self.report_sections) if self.report_sections is not None
68
+ else list(DEFAULT_SECTIONS)),
69
+ stock_sections={k.upper(): own_area(clean_sections(v)) for k, v in (self.stock_sections or {}).items()},
70
+ channel_languages=dict(self.channel_languages or {}),
71
+ mode=self.mode,
72
+ timezone=timezone,
73
+ funds=list(self.mf_codes or []),
74
+ portfolios=list(self.portfolio_names or []),
75
+ smart=bool(self.smart_enabled),
76
+ smart_top_n=self.smart_top_n or 5,
77
+ smart_ai_top=self.smart_ai_top if self.smart_ai_top is not None else 10,
78
+ smart_email_full=self.smart_email_full is not False,
79
+ smart_file=self.smart_file if self.smart_file in SMART_FILES else "pdf",
80
+ smart_file_channels=list(self.smart_file_channels if self.smart_file_channels is not None
81
+ else SMART_FILE_CHANNELS),
82
+ report_base_url=self.report_base_url or None,
83
+ )
84
+
85
+
86
+ SOURCES = ("watchlist", "portfolio", "custom")
87
+ # Smart messaging report file formats and the channels that can carry a file.
88
+ SMART_FILES = ("pdf", "xlsx", "none")
89
+ SMART_FILE_CHANNELS = ("email", "telegram")
90
+ KNOWN_MARKETS = {"us", "india", "china_a", "hong_kong", "crypto", "global"}
91
+
92
+
93
+ def source_catalog(config=None) -> dict:
94
+ """Every watchlist and portfolio with the symbols a run would take from
95
+ it (funds and closed positions excluded - they have no chart analysis)."""
96
+ from ..storage import HoldingRecord, PortfolioRecord, WatchlistRecord
97
+
98
+ db = DatabaseManager.instance(get_db_url(config=config))
99
+ with db.get_session() as session:
100
+ watchlists = [
101
+ {
102
+ "id": w.id,
103
+ "name": w.name,
104
+ "symbols": [{"symbol": i.symbol.upper(), "market": i.market} for i in items if i.type != "mf"],
105
+ "funds": [str(i.symbol) for i in items if i.type == "mf"],
106
+ }
107
+ for w in session.query(WatchlistRecord).order_by(WatchlistRecord.id)
108
+ for items in [list(w.items)]
109
+ ]
110
+ portfolios = []
111
+ for p in session.query(PortfolioRecord).order_by(PortfolioRecord.id):
112
+ holdings = session.query(HoldingRecord).filter(HoldingRecord.portfolio_id == p.id).all()
113
+ symbols, funds = [], []
114
+ for h in holdings:
115
+ if (h.quantity or 0) <= 0:
116
+ continue
117
+ market = (h.market or "us").lower()
118
+ if market == "mf": # mutual fund (AMFI code): reported in the funds section, not analysed
119
+ funds.append(str(h.symbol))
120
+ continue
121
+ symbols.append({"symbol": h.symbol.upper(), "market": market if market in KNOWN_MARKETS else "us"})
122
+ portfolios.append({"id": p.id, "name": p.name, "symbols": symbols, "funds": funds})
123
+ return {"watchlists": watchlists, "portfolios": portfolios}
124
+
125
+
126
+ def resolve_targets(cfg: ScheduleConfig, config=None, catalog: Optional[dict] = None):
127
+ """The (symbol, market) pairs a run covers, from the chosen sources,
128
+ de-duplicated in source order (portfolio first: what you own matters most)."""
129
+ from .analysis_run import Target
130
+
131
+ srcs = cfg.run_sources()
132
+ if srcs != ["custom"] and catalog is None:
133
+ catalog = source_catalog(config)
134
+ pairs: list[tuple[str, str]] = []
135
+ if "portfolio" in srcs:
136
+ for p in catalog["portfolios"]:
137
+ if not cfg.portfolio_names or p["name"] in cfg.portfolio_names:
138
+ pairs += [(s["symbol"], s["market"]) for s in p["symbols"]]
139
+ if "watchlist" in srcs:
140
+ for w in catalog["watchlists"]:
141
+ if not cfg.watchlist_ids or w["id"] in cfg.watchlist_ids:
142
+ pairs += [(s["symbol"], s["market"]) for s in w["symbols"]]
143
+ if "custom" in srcs:
144
+ pairs += [(s.upper(), cfg.market) for s in cfg.symbols]
145
+ return [Target(sym, mkt) for sym, mkt in dict.fromkeys(pairs)]
146
+
147
+
148
+ def resolve_funds(cfg: ScheduleConfig, config=None, catalog: Optional[dict] = None) -> list[str]:
149
+ """AMFI codes for the report's funds section: the ones picked in Settings,
150
+ then funds held in / watched on the run's chosen portfolios and watchlists."""
151
+ codes = [str(c) for c in (cfg.mf_codes or [])]
152
+ srcs = cfg.run_sources()
153
+ if srcs != ["custom"]:
154
+ catalog = catalog or source_catalog(config)
155
+ if "portfolio" in srcs:
156
+ for p in catalog["portfolios"]:
157
+ if not cfg.portfolio_names or p["name"] in cfg.portfolio_names:
158
+ codes += p.get("funds", [])
159
+ if "watchlist" in srcs:
160
+ for w in catalog["watchlists"]:
161
+ if not cfg.watchlist_ids or w["id"] in cfg.watchlist_ids:
162
+ codes += w.get("funds", [])
163
+ return list(dict.fromkeys(codes))
164
+
165
+
166
+ class ScheduleConfigStore:
167
+ def __init__(self, config=None):
168
+ self._db = DatabaseManager.instance(get_db_url(config=config))
169
+
170
+ @staticmethod
171
+ def _record(session):
172
+ # One row per user in accounts mode (the query is user-scoped), a
173
+ # single row otherwise.
174
+ return session.query(ScheduleConfigRecord).order_by(ScheduleConfigRecord.id).first()
175
+
176
+ def load(self) -> ScheduleConfig:
177
+ with self._db.get_session() as session:
178
+ record = self._record(session)
179
+ if record is None:
180
+ return ScheduleConfig()
181
+ return ScheduleConfig(
182
+ enabled=bool(record.enabled),
183
+ interval_minutes=record.interval_minutes,
184
+ time_of_day=record.time_of_day,
185
+ symbols=list(record.symbols or []),
186
+ market=record.market or "us",
187
+ mode=record.mode or "standard",
188
+ last_run_at=record.last_run_at,
189
+ last_run_status=record.last_run_status,
190
+ last_run_summary=record.last_run_summary,
191
+ report_language=record.report_language or "en",
192
+ report_sections=list(record.report_sections) if record.report_sections is not None else None,
193
+ stock_sections=dict(record.stock_sections or {}),
194
+ channel_languages=dict(record.channel_languages or {}),
195
+ mf_codes=list(record.mf_codes or []),
196
+ smart_enabled=bool(record.smart_enabled),
197
+ smart_top_n=record.smart_top_n or 5,
198
+ smart_ai_top=record.smart_ai_top if record.smart_ai_top is not None else 10,
199
+ smart_email_full=record.smart_email_full is not False,
200
+ smart_file=record.smart_file or "pdf",
201
+ smart_file_channels=list(record.smart_file_channels) if record.smart_file_channels is not None else None,
202
+ report_base_url=record.report_base_url,
203
+ sources=list(record.sources) if record.sources is not None else None,
204
+ watchlist_ids=[int(i) for i in (record.watchlist_ids or [])],
205
+ portfolio_names=list(record.portfolio_names or []),
206
+ stock_count=record.stock_count,
207
+ area=record.area,
208
+ )
209
+
210
+ def save(self, cfg: ScheduleConfig) -> None:
211
+ with self._db.get_session() as session:
212
+ record = self._record(session)
213
+ if record is None:
214
+ record = ScheduleConfigRecord()
215
+ session.add(record)
216
+ record.enabled = cfg.enabled
217
+ record.interval_minutes = cfg.interval_minutes
218
+ record.time_of_day = cfg.time_of_day
219
+ record.symbols = list(cfg.symbols)
220
+ record.market = cfg.market
221
+ record.mode = cfg.mode
222
+ record.last_run_at = cfg.last_run_at
223
+ record.last_run_status = cfg.last_run_status
224
+ record.last_run_summary = cfg.last_run_summary
225
+ record.report_language = cfg.report_language
226
+ record.report_sections = list(cfg.report_sections) if cfg.report_sections is not None else None
227
+ record.stock_sections = dict(cfg.stock_sections or {})
228
+ record.channel_languages = dict(cfg.channel_languages or {})
229
+ record.mf_codes = list(cfg.mf_codes or [])
230
+ record.smart_enabled = cfg.smart_enabled
231
+ record.smart_top_n = cfg.smart_top_n
232
+ record.smart_ai_top = cfg.smart_ai_top
233
+ record.smart_email_full = cfg.smart_email_full
234
+ record.smart_file = cfg.smart_file
235
+ record.smart_file_channels = list(cfg.smart_file_channels) if cfg.smart_file_channels is not None else None
236
+ record.report_base_url = cfg.report_base_url
237
+ record.sources = list(cfg.sources) if cfg.sources is not None else None
238
+ record.watchlist_ids = list(cfg.watchlist_ids or [])
239
+ record.portfolio_names = list(cfg.portfolio_names or [])
240
+ record.stock_count = cfg.stock_count
241
+ record.area = cfg.area
242
+
243
+ def update(self, **overrides) -> ScheduleConfig:
244
+ """Partial update: load, apply only the given fields, save, return
245
+ the merged result - same semantics as system_config.py's PUT
246
+ endpoints (exclude_unset upstream, full-replace fields here)."""
247
+ cfg = self.load()
248
+ for key, value in overrides.items():
249
+ setattr(cfg, key, value)
250
+ self.save(cfg)
251
+ return cfg
@@ -0,0 +1,39 @@
1
+ """Times and time zones for scheduled reports, and how many stocks a run
2
+ analyses in parallel."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ from typing import Optional
8
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
9
+
10
+ DEFAULT_TIMEZONE = "Asia/Kolkata"
11
+
12
+
13
+ def workers(config) -> int:
14
+ """Parallel analyses in a run: SCHEDULER_WORKERS, else up to 4 by CPU count."""
15
+ n = getattr(config, "scheduler_workers", None)
16
+ return max(1, int(n)) if n else max(1, min(4, os.cpu_count() or 1))
17
+
18
+
19
+ def zone(name: Optional[str]) -> ZoneInfo:
20
+ try:
21
+ return ZoneInfo(name or DEFAULT_TIMEZONE)
22
+ except (ZoneInfoNotFoundError, ValueError):
23
+ return ZoneInfo(DEFAULT_TIMEZONE)
24
+
25
+
26
+ def valid_zone(name: str) -> bool:
27
+ try:
28
+ ZoneInfo(name)
29
+ return "/" in name or name == "UTC"
30
+ except (ZoneInfoNotFoundError, ValueError):
31
+ return False
32
+
33
+
34
+ def parse_time(value: str) -> tuple[int, int]:
35
+ """"HH:MM" in 24-hour time; ValueError otherwise."""
36
+ hour, minute = (int(p) for p in str(value).strip().split(":", 1))
37
+ if not (0 <= hour <= 23 and 0 <= minute <= 59):
38
+ raise ValueError(value)
39
+ return hour, minute