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,197 @@
1
+ """Stock data fetcher using Yahoo Finance."""
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ import pandas as pd
7
+ import yfinance as yf
8
+
9
+ from ..analysis.fundamental import yfinance_fundamentals
10
+ from ..core.constants import TimeFrame
11
+ from .base import DataFetcher
12
+ from .extra_sources import (
13
+ alphavantage_history,
14
+ alphavantage_key,
15
+ alphavantage_quote,
16
+ finnhub_key,
17
+ finnhub_quote,
18
+ nasdaq_history,
19
+ nasdaq_quote,
20
+ polygon_history,
21
+ polygon_key,
22
+ polygon_quote,
23
+ tiingo_history,
24
+ tiingo_key,
25
+ tiingo_quote,
26
+ twelvedata_history,
27
+ twelvedata_key,
28
+ twelvedata_quote,
29
+ )
30
+ from .resilience import FallbackChain
31
+
32
+
33
+ def _plain_us(symbol: str) -> bool:
34
+ return symbol.replace(".", "").replace("-", "").isalpha()
35
+
36
+
37
+ class StockDataFetcher(DataFetcher):
38
+ """Fetches stock data from Yahoo Finance (US & Global markets).
39
+
40
+ Yahoo first; plain US tickers then fall back to the keyed sources
41
+ configured (Finnhub, Twelve Data, Tiingo, Alpha Vantage, Polygon) and
42
+ Nasdaq, which needs no key. Index and foreign symbols (^GSPC,
43
+ RELIANCE.NS) have only Yahoo.
44
+ """
45
+
46
+ def __init__(self):
47
+ self._cache = {}
48
+
49
+ def _fetch_quote_yfinance(self, symbol: str) -> dict:
50
+ ticker = yf.Ticker(symbol)
51
+ info = ticker.fast_info
52
+ try:
53
+ price = info.last_price
54
+ except KeyError:
55
+ return {} # unknown symbol: empty, not a source failure (see india.py)
56
+
57
+ return {
58
+ "symbol": symbol,
59
+ "price": price,
60
+ "previous_close": info.previous_close,
61
+ "open": info.open,
62
+ "day_high": info.day_high,
63
+ "day_low": info.day_low,
64
+ "volume": info.last_volume,
65
+ "market_cap": info.market_cap,
66
+ "change": info.last_price - info.previous_close,
67
+ "change_percent": ((info.last_price - info.previous_close) / info.previous_close) * 100
68
+ if info.previous_close
69
+ else 0,
70
+ }
71
+
72
+ def get_quote(self, symbol: str) -> dict:
73
+ """Get current quote for a stock."""
74
+ chain = FallbackChain("stocks.get_quote").add("yfinance", self._fetch_quote_yfinance)
75
+ # Keyed backups for US tickers (index/foreign symbols like ^GSPC or
76
+ # RELIANCE.NS aren't covered by these APIs' free tiers).
77
+ if _plain_us(symbol):
78
+ if finnhub_key():
79
+ chain.add("finnhub", finnhub_quote)
80
+ if twelvedata_key():
81
+ chain.add("twelvedata", twelvedata_quote)
82
+ if tiingo_key():
83
+ chain.add("tiingo", tiingo_quote)
84
+ chain.add("nasdaq", nasdaq_quote)
85
+ if alphavantage_key():
86
+ chain.add("alphavantage", alphavantage_quote)
87
+ if polygon_key():
88
+ chain.add("polygon", polygon_quote)
89
+ return chain.call(symbol, is_empty=lambda d: not d or d.get("price") is None)
90
+
91
+ def _fetch_history_yfinance(
92
+ self,
93
+ symbol: str,
94
+ period: str = "1y",
95
+ interval: TimeFrame = TimeFrame.DAY_1,
96
+ start: Optional[datetime] = None,
97
+ end: Optional[datetime] = None,
98
+ ) -> pd.DataFrame:
99
+ ticker = yf.Ticker(symbol)
100
+
101
+ if start and end:
102
+ df = ticker.history(start=start, end=end, interval=interval.value)
103
+ else:
104
+ df = ticker.history(period=period, interval=interval.value)
105
+
106
+ if df.empty:
107
+ return pd.DataFrame()
108
+
109
+ # Standardize column names
110
+ df.columns = [col.lower().replace(" ", "_") for col in df.columns]
111
+ return df
112
+
113
+ def get_history(
114
+ self,
115
+ symbol: str,
116
+ period: str = "1y",
117
+ interval: TimeFrame = TimeFrame.DAY_1,
118
+ start: Optional[datetime] = None,
119
+ end: Optional[datetime] = None,
120
+ ) -> pd.DataFrame:
121
+ """Get historical price data for a stock."""
122
+ chain = FallbackChain("stocks.get_history").add("yfinance", self._fetch_history_yfinance)
123
+ # Daily bars only: an intraday request skips these (they raise).
124
+ if _plain_us(symbol):
125
+ if twelvedata_key():
126
+ chain.add("twelvedata", twelvedata_history)
127
+ if tiingo_key():
128
+ chain.add("tiingo", tiingo_history)
129
+ if polygon_key():
130
+ chain.add("polygon", polygon_history)
131
+ chain.add("nasdaq", nasdaq_history)
132
+ if alphavantage_key():
133
+ chain.add("alphavantage", alphavantage_history)
134
+ return chain.call(symbol, period=period, interval=interval, start=start, end=end, is_empty=lambda df: df.empty)
135
+
136
+ def search(self, query: str) -> list[dict]:
137
+ """Search for stocks matching query."""
138
+ try:
139
+ ticker = yf.Ticker(query)
140
+ info = ticker.info
141
+ if info and "symbol" in info:
142
+ return [
143
+ {
144
+ "symbol": info.get("symbol", query),
145
+ "name": info.get("longName", info.get("shortName", "")),
146
+ "exchange": info.get("exchange", ""),
147
+ "type": info.get("quoteType", ""),
148
+ }
149
+ ]
150
+ except Exception:
151
+ pass
152
+ return []
153
+
154
+ def get_info(self, symbol: str) -> dict:
155
+ """Get detailed information about a stock."""
156
+ ticker = yf.Ticker(symbol)
157
+ info = ticker.info
158
+
159
+ return {
160
+ "symbol": info.get("symbol", symbol),
161
+ "name": info.get("longName", ""),
162
+ "sector": info.get("sector", ""),
163
+ "industry": info.get("industry", ""),
164
+ "description": info.get("longBusinessSummary", ""),
165
+ "website": info.get("website", ""),
166
+ "employees": info.get("fullTimeEmployees", 0),
167
+ "country": info.get("country", ""),
168
+ "currency": info.get("currency", "USD"),
169
+ # Valuation metrics
170
+ "market_cap": info.get("marketCap", 0),
171
+ "pe_ratio": info.get("trailingPE", 0),
172
+ "forward_pe": info.get("forwardPE", 0),
173
+ "peg_ratio": info.get("pegRatio", 0),
174
+ "price_to_book": info.get("priceToBook", 0),
175
+ "price_to_sales": info.get("priceToSalesTrailing12Months", 0),
176
+ # Financial metrics
177
+ "revenue": info.get("totalRevenue", 0),
178
+ "gross_profit": info.get("grossProfits", 0),
179
+ "ebitda": info.get("ebitda", 0),
180
+ "net_income": info.get("netIncomeToCommon", 0),
181
+ "eps": info.get("trailingEps", 0),
182
+ # Dividend info
183
+ "dividend_yield": info.get("dividendYield", 0),
184
+ "dividend_rate": info.get("dividendRate", 0),
185
+ "payout_ratio": info.get("payoutRatio", 0),
186
+ # Trading info
187
+ "52_week_high": info.get("fiftyTwoWeekHigh", 0),
188
+ "52_week_low": info.get("fiftyTwoWeekLow", 0),
189
+ "50_day_avg": info.get("fiftyDayAverage", 0),
190
+ "200_day_avg": info.get("twoHundredDayAverage", 0),
191
+ "avg_volume": info.get("averageVolume", 0),
192
+ "beta": info.get("beta", 0),
193
+ # Normalized ratios for FundamentalAnalyzer (overrides the raw
194
+ # yfinance-unit fields above, e.g. dividend_yield)
195
+ **yfinance_fundamentals(info),
196
+ }
197
+
@@ -0,0 +1,48 @@
1
+ """A price from a Yahoo `info` dict that is safe to use.
2
+
3
+ Yahoo's quote fields go stale for some listings while their daily bars stay
4
+ current: Indian REITs and InvITs report a `regularMarketPrice` frozen in
5
+ 2024 with no `currentPrice` at all. A price older than MAX_AGE is treated as
6
+ missing (or replaced by the latest daily close when a ticker is given), so
7
+ a yield or upside is never worked out from it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ import time
14
+ from typing import Any, Optional
15
+
16
+ MAX_AGE = 10 * 24 * 3600 # a week of holidays plus a weekend
17
+
18
+
19
+ def _num(value: Any) -> Optional[float]:
20
+ try:
21
+ v = float(value)
22
+ except (TypeError, ValueError):
23
+ return None
24
+ return v if math.isfinite(v) and v > 0 else None
25
+
26
+
27
+ def info_price(info: dict, now: Optional[float] = None) -> Optional[float]:
28
+ """currentPrice, else a regularMarketPrice that is still fresh."""
29
+ price = _num(info.get("currentPrice"))
30
+ if price:
31
+ return price
32
+ price = _num(info.get("regularMarketPrice"))
33
+ stamp = _num(info.get("regularMarketTime"))
34
+ if price and stamp and (now or time.time()) - stamp > MAX_AGE:
35
+ return None
36
+ return price
37
+
38
+
39
+ def fresh_price(info: dict, ticker=None) -> Optional[float]:
40
+ """info_price(), falling back to the ticker's latest daily close."""
41
+ price = info_price(info)
42
+ if price or ticker is None:
43
+ return price
44
+ try:
45
+ bars = ticker.history(period="5d", interval="1d")
46
+ return _num(bars["Close"].iloc[-1]) if not bars.empty else None
47
+ except Exception:
48
+ return None
marketmyna/launcher.py ADDED
@@ -0,0 +1,71 @@
1
+ """Start MarketMyna: the `marketmyna` command (pip / pipx install) and start.py.
2
+
3
+ marketmyna # http://127.0.0.1:8001, opens your browser
4
+ marketmyna --port 9000 --no-browser
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import os
11
+ import sys
12
+ import threading
13
+ import time
14
+ import urllib.request
15
+ import webbrowser
16
+
17
+
18
+ def parser() -> argparse.ArgumentParser:
19
+ p = argparse.ArgumentParser(prog="marketmyna", description="Start MarketMyna.")
20
+ p.add_argument("--host", default="127.0.0.1",
21
+ help="address to listen on (default 127.0.0.1: only this computer can reach it)")
22
+ p.add_argument("--port", type=int, default=8001)
23
+ p.add_argument("--no-browser", action="store_true", help="don't open the browser")
24
+ p.add_argument("--version", action="version", version=f"%(prog)s {_version()}")
25
+ return p
26
+
27
+
28
+ def _version() -> str:
29
+ from marketmyna import __version__
30
+ return __version__
31
+
32
+
33
+ def open_when_ready(url: str) -> None:
34
+ for _ in range(120):
35
+ try:
36
+ with urllib.request.urlopen(f"{url}/health", timeout=2):
37
+ webbrowser.open(url)
38
+ return
39
+ except OSError:
40
+ time.sleep(1)
41
+
42
+
43
+ def run(args: argparse.Namespace) -> None:
44
+ try:
45
+ import uvicorn
46
+ except ImportError:
47
+ sys.exit("Install the requirements first: pip install -r requirements-web.txt")
48
+ url = f"http://{'127.0.0.1' if args.host in ('0.0.0.0', '::') else args.host}:{args.port}"
49
+ print(f"MarketMyna: {url} (Ctrl+C to stop)")
50
+ if not args.no_browser:
51
+ threading.Thread(target=open_when_ready, args=(url,), daemon=True).start()
52
+ # One process: the scheduler, price alerts and the Telegram bot run inside it.
53
+ # Ctrl+C stops within a few seconds even while a slow data download is running.
54
+ try:
55
+ uvicorn.run("marketmyna.api.app:app", host=args.host, port=args.port, timeout_graceful_shutdown=5)
56
+ except KeyboardInterrupt:
57
+ pass
58
+ # The app has shut down; don't wait minutes at exit for download threads
59
+ # (a first China / Hong Kong listing fetch) that nothing will read.
60
+ if any(t.is_alive() and not t.daemon for t in threading.enumerate() if t is not threading.main_thread()):
61
+ sys.stdout.flush()
62
+ sys.stderr.flush()
63
+ os._exit(0)
64
+
65
+
66
+ def main(argv: list[str] | None = None) -> None:
67
+ run(parser().parse_args(argv))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,33 @@
1
+ """LLM generation backend layer.
2
+
3
+ A GenerationBackend Protocol (marketmyna/llm/protocol.py) is the swappable
4
+ interface; backend_factory.create_generation_backend(config) builds a
5
+ concrete implementation chosen by backend_registry from Config, the
6
+ same "small pluggable backend" shape daily_stock_analysis uses for its
7
+ own litellm/local-CLI backends.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .backend_factory import create_generation_backend
13
+ from .backend_registry import LITELLM_BACKEND_ID, resolve_generation_backend_id
14
+ from .litellm_backend import LiteLLMGenerationBackend
15
+ from .protocol import (
16
+ GenerationBackend,
17
+ GenerationCapabilities,
18
+ GenerationError,
19
+ GenerationErrorCode,
20
+ GenerationResult,
21
+ )
22
+
23
+ __all__ = [
24
+ "GenerationBackend",
25
+ "GenerationCapabilities",
26
+ "GenerationError",
27
+ "GenerationErrorCode",
28
+ "GenerationResult",
29
+ "LiteLLMGenerationBackend",
30
+ "LITELLM_BACKEND_ID",
31
+ "resolve_generation_backend_id",
32
+ "create_generation_backend",
33
+ ]
@@ -0,0 +1,51 @@
1
+ """Builds a concrete GenerationBackend from Config."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .backend_registry import LITELLM_BACKEND_ID, resolve_generation_backend_id
6
+ from .litellm_backend import LiteLLMGenerationBackend
7
+ from .protocol import GenerationBackend, GenerationError, GenerationErrorCode
8
+
9
+
10
+ def create_generation_backend(config, tier: str = "deep") -> GenerationBackend:
11
+ """tier="deep": the main model chain, for decisions and idea picks.
12
+ tier="fast": the cheaper level-1 model first (commentary, news tone,
13
+ translation), falling back to the main chain. The deep backend also
14
+ exposes `.fast`, so agents can pick the right level per job.
15
+
16
+ Saved AI keys come first, then the config.json / env setup
17
+ (marketmyna/services/ai_access.py)."""
18
+ from ..services.ai_access import wrap_backend
19
+
20
+ return wrap_backend(config, _create_platform_backend, tier)
21
+
22
+
23
+ def _create_platform_backend(config, tier: str = "deep") -> GenerationBackend:
24
+ backend_id = resolve_generation_backend_id(config)
25
+
26
+ if backend_id == LITELLM_BACKEND_ID:
27
+ from .channels import FailoverGenerationBackend, build_routes
28
+ from .store import LLMStore
29
+
30
+ routes = build_routes(config, tier=tier)
31
+ if not routes:
32
+ # Raises a clear "No llm_model configured" on first use.
33
+ return LiteLLMGenerationBackend(config)
34
+ try:
35
+ store = LLMStore(config)
36
+ except Exception: # cache/usage are optional extras
37
+ store = None
38
+ backend = FailoverGenerationBackend(config, routes, store=store, tier=tier)
39
+ if tier == "deep" and getattr(config, "llm_fast_model", None):
40
+ backend.fast = FailoverGenerationBackend(config, build_routes(config, tier="fast"), store=store,
41
+ tier="fast")
42
+ return backend
43
+
44
+ raise GenerationError(
45
+ GenerationErrorCode.BACKEND_NOT_CONFIGURED,
46
+ f"Unknown or unsupported llm_backend '{backend_id}'",
47
+ stage="setup",
48
+ retryable=False,
49
+ fallbackable=False,
50
+ backend=backend_id,
51
+ )
@@ -0,0 +1,19 @@
1
+ """Resolves which LLM backend id a Config selects.
2
+
3
+ Only "litellm" exists in this phase. daily_stock_analysis also has
4
+ local-CLI backends (codex_cli/claude_code_cli/opencode_cli - subprocess-
5
+ based, with allowlisted env, output caps, redaction) - deliberately not
6
+ built here yet: that's real extra scope (subprocess sandboxing is
7
+ security-sensitive to get right) rather than a small addition, so it's
8
+ cut from Phase 3a rather than half-implemented. backend_factory already
9
+ raises a clear, typed error for any backend id other than "litellm" so
10
+ adding one later doesn't require reshaping this function's callers.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ LITELLM_BACKEND_ID = "litellm"
16
+
17
+
18
+ def resolve_generation_backend_id(config) -> str:
19
+ return getattr(config, "llm_backend", None) or LITELLM_BACKEND_ID