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,330 @@
1
+ """AI-powered portfolio optimization."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Optional, Tuple
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+
11
+ @dataclass
12
+ class OptimizationResult:
13
+ """Result of portfolio optimization."""
14
+ optimal_weights: dict[str, float]
15
+ expected_return: float
16
+ expected_volatility: float
17
+ sharpe_ratio: float
18
+ suggestions: list[str]
19
+ rebalance_trades: list[dict]
20
+ risk_metrics: dict
21
+ timestamp: datetime
22
+
23
+ def to_dict(self) -> dict:
24
+ return {
25
+ "optimal_weights": self.optimal_weights,
26
+ "expected_return": self.expected_return,
27
+ "expected_volatility": self.expected_volatility,
28
+ "sharpe_ratio": self.sharpe_ratio,
29
+ "suggestions": self.suggestions,
30
+ "rebalance_trades": self.rebalance_trades,
31
+ "risk_metrics": self.risk_metrics,
32
+ "timestamp": self.timestamp.isoformat(),
33
+ }
34
+
35
+
36
+ class PortfolioOptimizer:
37
+ """
38
+ AI-powered portfolio optimization using Modern Portfolio Theory
39
+ and Monte Carlo simulation.
40
+ """
41
+
42
+ def __init__(self, risk_free_rate: float = 0.05):
43
+ self.risk_free_rate = risk_free_rate
44
+
45
+ def calculate_returns(self, prices_df: pd.DataFrame) -> pd.DataFrame:
46
+ """Calculate daily returns from price data."""
47
+ return prices_df.pct_change().dropna()
48
+
49
+ def calculate_portfolio_metrics(
50
+ self,
51
+ weights: np.ndarray,
52
+ returns: pd.DataFrame,
53
+ ) -> Tuple[float, float, float]:
54
+ """
55
+ Calculate portfolio expected return, volatility, and Sharpe ratio.
56
+
57
+ Returns:
58
+ (expected_return, volatility, sharpe_ratio)
59
+ """
60
+ mean_returns = returns.mean() * 252 # Annualized
61
+ cov_matrix = returns.cov() * 252 # Annualized
62
+
63
+ portfolio_return = np.dot(weights, mean_returns)
64
+ portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
65
+ sharpe_ratio = (portfolio_return - self.risk_free_rate) / portfolio_volatility
66
+
67
+ return portfolio_return, portfolio_volatility, sharpe_ratio
68
+
69
+ def monte_carlo_optimization(
70
+ self,
71
+ returns: pd.DataFrame,
72
+ num_portfolios: int = 10000,
73
+ ) -> Tuple[np.ndarray, float, float, float]:
74
+ """
75
+ Find optimal portfolio using Monte Carlo simulation.
76
+
77
+ Returns:
78
+ (optimal_weights, max_sharpe_return, max_sharpe_vol, max_sharpe)
79
+ """
80
+ num_assets = len(returns.columns)
81
+ results = np.zeros((3, num_portfolios))
82
+ weights_record = np.zeros((num_portfolios, num_assets))
83
+
84
+ for i in range(num_portfolios):
85
+ # Random weights
86
+ weights = np.random.random(num_assets)
87
+ weights /= np.sum(weights)
88
+ weights_record[i, :] = weights
89
+
90
+ portfolio_return, portfolio_vol, sharpe = self.calculate_portfolio_metrics(
91
+ weights, returns
92
+ )
93
+
94
+ results[0, i] = portfolio_return
95
+ results[1, i] = portfolio_vol
96
+ results[2, i] = sharpe
97
+
98
+ # Find max Sharpe ratio portfolio
99
+ max_sharpe_idx = np.argmax(results[2])
100
+ optimal_weights = weights_record[max_sharpe_idx, :]
101
+
102
+ return (
103
+ optimal_weights,
104
+ results[0, max_sharpe_idx],
105
+ results[1, max_sharpe_idx],
106
+ results[2, max_sharpe_idx],
107
+ )
108
+
109
+ def mean_variance_optimization(
110
+ self,
111
+ returns: pd.DataFrame,
112
+ target_return: Optional[float] = None,
113
+ max_weight: float = 1.0,
114
+ ) -> np.ndarray:
115
+ """
116
+ Mean-variance optimization using scipy.
117
+
118
+ Returns:
119
+ Optimal weights array
120
+ """
121
+ try:
122
+ from scipy.optimize import minimize
123
+
124
+ num_assets = len(returns.columns)
125
+ mean_returns = returns.mean() * 252
126
+ cov_matrix = returns.cov() * 252
127
+
128
+ def neg_sharpe(weights):
129
+ port_return = np.dot(weights, mean_returns)
130
+ port_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
131
+ return -(port_return - self.risk_free_rate) / port_vol
132
+
133
+ constraints = [
134
+ {'type': 'eq', 'fun': lambda x: np.sum(x) - 1} # Weights sum to 1
135
+ ]
136
+
137
+ if target_return:
138
+ constraints.append(
139
+ {'type': 'eq', 'fun': lambda x: np.dot(x, mean_returns) - target_return}
140
+ )
141
+
142
+ # A cap below 1/n cannot sum to 1, so never go under equal weight
143
+ cap = max(min(max_weight, 1.0), 1 / num_assets)
144
+ bounds = tuple((0, cap) for _ in range(num_assets))
145
+ initial_weights = np.array([1 / num_assets] * num_assets)
146
+
147
+ result = minimize(
148
+ neg_sharpe,
149
+ initial_weights,
150
+ method='SLSQP',
151
+ bounds=bounds,
152
+ constraints=constraints,
153
+ )
154
+
155
+ return result.x
156
+
157
+ except ImportError:
158
+ # Fallback to equal weights
159
+ return np.array([1 / len(returns.columns)] * len(returns.columns))
160
+
161
+ def calculate_var(
162
+ self,
163
+ returns: pd.DataFrame,
164
+ weights: np.ndarray,
165
+ confidence: float = 0.95,
166
+ days: int = 1,
167
+ ) -> float:
168
+ """Calculate Value at Risk (VaR)."""
169
+ portfolio_returns = returns.dot(weights)
170
+ var = np.percentile(portfolio_returns, (1 - confidence) * 100) * np.sqrt(days)
171
+ return abs(var)
172
+
173
+ def calculate_cvar(
174
+ self,
175
+ returns: pd.DataFrame,
176
+ weights: np.ndarray,
177
+ confidence: float = 0.95,
178
+ ) -> float:
179
+ """Calculate Conditional VaR (Expected Shortfall)."""
180
+ portfolio_returns = returns.dot(weights)
181
+ var = np.percentile(portfolio_returns, (1 - confidence) * 100)
182
+ cvar = portfolio_returns[portfolio_returns <= var].mean()
183
+ return abs(cvar)
184
+
185
+ def calculate_max_drawdown(self, returns: pd.DataFrame, weights: np.ndarray) -> float:
186
+ """Calculate maximum drawdown."""
187
+ portfolio_returns = returns.dot(weights)
188
+ cumulative = (1 + portfolio_returns).cumprod()
189
+ running_max = cumulative.expanding().max()
190
+ drawdown = (cumulative - running_max) / running_max
191
+ return abs(drawdown.min())
192
+
193
+
194
+ def optimize(
195
+ self,
196
+ prices_df: pd.DataFrame,
197
+ current_weights: Optional[dict[str, float]] = None,
198
+ portfolio_value: float = 10000,
199
+ method: str = "monte_carlo",
200
+ max_weight: float = 1.0,
201
+ ) -> OptimizationResult:
202
+ """
203
+ Optimize portfolio allocation.
204
+
205
+ Args:
206
+ prices_df: DataFrame with columns as symbols and prices as values
207
+ current_weights: Current portfolio weights (optional)
208
+ portfolio_value: Total portfolio value
209
+ method: "monte_carlo" or "mean_variance"
210
+ max_weight: Per-asset weight cap for mean_variance (1.0 = uncapped)
211
+
212
+ Returns:
213
+ OptimizationResult with optimal allocation
214
+ """
215
+ returns = self.calculate_returns(prices_df)
216
+ symbols = list(returns.columns)
217
+
218
+ # Optimize
219
+ if method == "monte_carlo":
220
+ weights, exp_return, exp_vol, sharpe = self.monte_carlo_optimization(returns)
221
+ else:
222
+ weights = self.mean_variance_optimization(returns, max_weight=max_weight)
223
+ exp_return, exp_vol, sharpe = self.calculate_portfolio_metrics(weights, returns)
224
+
225
+ # Create weights dict
226
+ optimal_weights = {symbol: float(w) for symbol, w in zip(symbols, weights)}
227
+
228
+ # Calculate risk metrics
229
+ var_95 = self.calculate_var(returns, weights, 0.95)
230
+ cvar_95 = self.calculate_cvar(returns, weights, 0.95)
231
+ max_dd = self.calculate_max_drawdown(returns, weights)
232
+
233
+ risk_metrics = {
234
+ "var_95": var_95 * portfolio_value,
235
+ "cvar_95": cvar_95 * portfolio_value,
236
+ "max_drawdown": max_dd,
237
+ "var_95_percent": var_95 * 100,
238
+ }
239
+
240
+ # Generate rebalance trades
241
+ rebalance_trades = []
242
+ suggestions = []
243
+
244
+ if current_weights:
245
+ for symbol in symbols:
246
+ current = current_weights.get(symbol, 0)
247
+ target = optimal_weights.get(symbol, 0)
248
+ diff = target - current
249
+
250
+ if abs(diff) > 0.02: # More than 2% difference
251
+ trade_value = diff * portfolio_value
252
+ rebalance_trades.append({
253
+ "symbol": symbol,
254
+ "action": "buy" if diff > 0 else "sell",
255
+ "current_weight": current,
256
+ "target_weight": target,
257
+ "trade_value": abs(trade_value),
258
+ })
259
+
260
+ # Generate suggestions
261
+ # High concentration warning
262
+ max_weight = max(optimal_weights.values())
263
+ if max_weight > 0.3:
264
+ max_symbol = max(optimal_weights, key=optimal_weights.get)
265
+ suggestions.append(f"High concentration in {max_symbol} ({max_weight:.1%}). Consider diversifying.")
266
+
267
+ # Risk level suggestions
268
+ if exp_vol > 0.25:
269
+ suggestions.append("Portfolio volatility is high. Consider adding bonds or stable assets.")
270
+ elif exp_vol < 0.10:
271
+ suggestions.append("Portfolio is conservative. May underperform in bull markets.")
272
+
273
+ # Sharpe ratio assessment
274
+ if sharpe > 1.5:
275
+ suggestions.append(f"Excellent risk-adjusted returns (Sharpe: {sharpe:.2f})")
276
+ elif sharpe < 0.5:
277
+ suggestions.append(f"Consider adjusting allocation for better risk-adjusted returns (Sharpe: {sharpe:.2f})")
278
+
279
+ # Diversification suggestion
280
+ non_zero_positions = sum(1 for w in weights if w > 0.01)
281
+ if non_zero_positions < 5:
282
+ suggestions.append("Consider adding more positions for diversification.")
283
+
284
+ return OptimizationResult(
285
+ optimal_weights=optimal_weights,
286
+ expected_return=exp_return,
287
+ expected_volatility=exp_vol,
288
+ sharpe_ratio=sharpe,
289
+ suggestions=suggestions,
290
+ rebalance_trades=rebalance_trades,
291
+ risk_metrics=risk_metrics,
292
+ timestamp=datetime.now(),
293
+ )
294
+
295
+
296
+ def risk_parity_optimization(self, returns: pd.DataFrame) -> np.ndarray:
297
+ """
298
+ Risk parity optimization - equal risk contribution from each asset.
299
+ """
300
+ try:
301
+ from scipy.optimize import minimize
302
+
303
+ num_assets = len(returns.columns)
304
+ cov_matrix = returns.cov() * 252
305
+
306
+ def risk_budget_objective(weights):
307
+ portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
308
+ marginal_contrib = np.dot(cov_matrix, weights)
309
+ risk_contrib = weights * marginal_contrib / portfolio_vol
310
+
311
+ # We want equal risk contribution
312
+ target_risk = portfolio_vol / num_assets
313
+ return np.sum((risk_contrib - target_risk) ** 2)
314
+
315
+ constraints = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
316
+ bounds = tuple((0.01, 1) for _ in range(num_assets))
317
+ initial_weights = np.array([1 / num_assets] * num_assets)
318
+
319
+ result = minimize(
320
+ risk_budget_objective,
321
+ initial_weights,
322
+ method='SLSQP',
323
+ bounds=bounds,
324
+ constraints=constraints,
325
+ )
326
+
327
+ return result.x
328
+
329
+ except ImportError:
330
+ return np.array([1 / len(returns.columns)] * len(returns.columns))
@@ -0,0 +1,6 @@
1
+ """Analysis module - Technical and Fundamental analysis."""
2
+
3
+ from .fundamental import FundamentalAnalyzer
4
+ from .technical import TechnicalAnalyzer
5
+
6
+ __all__ = ["TechnicalAnalyzer", "FundamentalAnalyzer"]
@@ -0,0 +1,329 @@
1
+ """Fundamental analysis metrics and valuation."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ class Valuation(Enum):
8
+ """Valuation status."""
9
+ UNDERVALUED = "undervalued"
10
+ FAIR = "fair"
11
+ OVERVALUED = "overvalued"
12
+
13
+
14
+ @dataclass
15
+ class FundamentalMetrics:
16
+ """Collection of fundamental metrics."""
17
+ pe_ratio: float = 0
18
+ forward_pe: float = 0
19
+ peg_ratio: float = 0
20
+ price_to_book: float = 0
21
+ price_to_sales: float = 0
22
+ ev_to_ebitda: float = 0
23
+ debt_to_equity: float = 0
24
+ current_ratio: float = 0
25
+ quick_ratio: float = 0
26
+ roe: float = 0
27
+ roa: float = 0
28
+ profit_margin: float = 0
29
+ operating_margin: float = 0
30
+ revenue_growth: float = 0
31
+ earnings_growth: float = 0
32
+ dividend_yield: float = 0
33
+ payout_ratio: float = 0
34
+
35
+
36
+ def yfinance_fundamentals(info: dict) -> dict:
37
+ """Map a raw yfinance `Ticker.info` dict onto the keys (and units)
38
+ FundamentalAnalyzer reads: ratios as fractions, e.g. 0.15 = 15%.
39
+
40
+ yfinance's own units are inconsistent: `debtToEquity` is a percent
41
+ (78.4 = 0.784x) and `dividendYield` is now a percent too (0.32 =
42
+ 0.32%), while ROE/margins/growth are fractions.
43
+ """
44
+ def num(key):
45
+ value = info.get(key)
46
+ try:
47
+ return float(value) if value is not None else None
48
+ except (TypeError, ValueError):
49
+ return None
50
+
51
+ from ..data.yahoo_price import info_price # here: marketmyna.data imports this module
52
+
53
+ price = info_price(info)
54
+ dividend_yield = num("trailingAnnualDividendYield")
55
+ # A 0 trailing yield is often a data gap (seen on NSE listings), not "no dividend".
56
+ if not dividend_yield and num("dividendRate") and price:
57
+ dividend_yield = num("dividendRate") / price
58
+ if not dividend_yield and num("dividendYield") is not None:
59
+ dividend_yield = num("dividendYield") / 100
60
+ debt_to_equity = num("debtToEquity")
61
+
62
+ out = {
63
+ "pe_ratio": num("trailingPE"),
64
+ "forward_pe": num("forwardPE"),
65
+ "peg_ratio": num("trailingPegRatio") or num("pegRatio"),
66
+ "price_to_book": num("priceToBook"),
67
+ "price_to_sales": num("priceToSalesTrailing12Months"),
68
+ "debt_to_equity": debt_to_equity / 100 if debt_to_equity is not None else None,
69
+ "current_ratio": num("currentRatio"),
70
+ "return_on_equity": num("returnOnEquity"),
71
+ "return_on_assets": num("returnOnAssets"),
72
+ "profit_margin": num("profitMargins"),
73
+ "revenue_growth": num("revenueGrowth"),
74
+ "earnings_growth": num("earningsGrowth"),
75
+ "dividend_yield": dividend_yield,
76
+ "payout_ratio": num("payoutRatio"),
77
+ }
78
+ return {k: v for k, v in out.items() if v is not None}
79
+
80
+
81
+ class FundamentalAnalyzer:
82
+ """Fundamental analysis calculator."""
83
+
84
+ # Industry average P/E ratios for comparison
85
+ INDUSTRY_PE = {
86
+ "Technology": 25,
87
+ "Healthcare": 20,
88
+ "Financial Services": 15,
89
+ "Consumer Cyclical": 18,
90
+ "Consumer Defensive": 20,
91
+ "Industrials": 18,
92
+ "Energy": 12,
93
+ "Utilities": 16,
94
+ "Real Estate": 35,
95
+ "Basic Materials": 14,
96
+ "Communication Services": 20,
97
+ }
98
+
99
+ def __init__(self, stock_info: dict):
100
+ """
101
+ Initialize with stock information.
102
+
103
+ Args:
104
+ stock_info: Dictionary containing stock fundamentals from data fetcher
105
+ """
106
+ self.info = stock_info
107
+ self.metrics = self._extract_metrics()
108
+
109
+ def _extract_metrics(self) -> FundamentalMetrics:
110
+ """Extract fundamental metrics from stock info."""
111
+ return FundamentalMetrics(
112
+ pe_ratio=self.info.get("pe_ratio", 0) or 0,
113
+ forward_pe=self.info.get("forward_pe", 0) or 0,
114
+ peg_ratio=self.info.get("peg_ratio", 0) or 0,
115
+ price_to_book=self.info.get("price_to_book", 0) or 0,
116
+ price_to_sales=self.info.get("price_to_sales", 0) or 0,
117
+ debt_to_equity=self.info.get("debt_to_equity", 0) or 0,
118
+ current_ratio=self.info.get("current_ratio", 0) or 0,
119
+ roe=self.info.get("return_on_equity", 0) or 0,
120
+ roa=self.info.get("return_on_assets", 0) or 0,
121
+ profit_margin=self.info.get("profit_margin", 0) or 0,
122
+ dividend_yield=self.info.get("dividend_yield", 0) or 0,
123
+ payout_ratio=self.info.get("payout_ratio", 0) or 0,
124
+ revenue_growth=self.info.get("revenue_growth", 0) or 0,
125
+ earnings_growth=self.info.get("earnings_growth", 0) or 0,
126
+ )
127
+
128
+ def pe_analysis(self) -> dict:
129
+ """Analyze P/E ratio."""
130
+ pe = self.metrics.pe_ratio
131
+ sector = self.info.get("sector", "")
132
+ industry_pe = self.INDUSTRY_PE.get(sector, 18)
133
+
134
+ if pe <= 0:
135
+ return {
136
+ "value": pe,
137
+ "status": "N/A",
138
+ "message": "No P/E ratio available (negative earnings)",
139
+ }
140
+
141
+ if pe < industry_pe * 0.7:
142
+ status = Valuation.UNDERVALUED
143
+ message = f"P/E of {pe:.1f} is below industry average of {industry_pe}"
144
+ elif pe > industry_pe * 1.3:
145
+ status = Valuation.OVERVALUED
146
+ message = f"P/E of {pe:.1f} is above industry average of {industry_pe}"
147
+ else:
148
+ status = Valuation.FAIR
149
+ message = f"P/E of {pe:.1f} is near industry average of {industry_pe}"
150
+
151
+ return {"value": pe, "status": status.value, "industry_avg": industry_pe, "message": message}
152
+
153
+ def peg_analysis(self) -> dict:
154
+ """Analyze PEG ratio (P/E to Growth)."""
155
+ peg = self.metrics.peg_ratio
156
+
157
+ if peg <= 0:
158
+ return {
159
+ "value": peg,
160
+ "status": "N/A",
161
+ "message": "No PEG ratio available",
162
+ }
163
+
164
+ if peg < 1:
165
+ status = Valuation.UNDERVALUED
166
+ message = "PEG < 1 suggests undervaluation relative to growth"
167
+ elif peg > 2:
168
+ status = Valuation.OVERVALUED
169
+ message = "PEG > 2 suggests overvaluation relative to growth"
170
+ else:
171
+ status = Valuation.FAIR
172
+ message = "PEG between 1-2 suggests fair valuation"
173
+
174
+ return {"value": peg, "status": status.value, "message": message}
175
+
176
+ def dividend_analysis(self) -> dict:
177
+ """Analyze dividend metrics."""
178
+ div_yield = self.metrics.dividend_yield * 100 if self.metrics.dividend_yield else 0
179
+ payout = self.metrics.payout_ratio * 100 if self.metrics.payout_ratio else 0
180
+
181
+ analysis = {
182
+ "yield": div_yield,
183
+ "payout_ratio": payout,
184
+ "sustainable": True,
185
+ "messages": [],
186
+ }
187
+
188
+ if div_yield == 0:
189
+ analysis["messages"].append("No dividend paid")
190
+ return analysis
191
+
192
+ if div_yield > 6:
193
+ analysis["messages"].append("High yield - verify sustainability")
194
+ elif div_yield > 3:
195
+ analysis["messages"].append("Good dividend yield")
196
+ else:
197
+ analysis["messages"].append("Modest dividend yield")
198
+
199
+ if payout > 80:
200
+ analysis["sustainable"] = False
201
+ analysis["messages"].append("High payout ratio - may not be sustainable")
202
+ elif payout > 60:
203
+ analysis["messages"].append("Moderate payout ratio")
204
+ else:
205
+ analysis["messages"].append("Low payout ratio - room for growth")
206
+
207
+ return analysis
208
+
209
+ def profitability_analysis(self) -> dict:
210
+ """Analyze profitability metrics."""
211
+ return {
212
+ "roe": {
213
+ "value": self.metrics.roe * 100 if self.metrics.roe else 0,
214
+ "rating": "good" if self.metrics.roe and self.metrics.roe > 0.15 else "average"
215
+ if self.metrics.roe and self.metrics.roe > 0.1 else "poor",
216
+ },
217
+ "roa": {
218
+ "value": self.metrics.roa * 100 if self.metrics.roa else 0,
219
+ "rating": "good" if self.metrics.roa and self.metrics.roa > 0.1 else "average"
220
+ if self.metrics.roa and self.metrics.roa > 0.05 else "poor",
221
+ },
222
+ "profit_margin": {
223
+ "value": self.metrics.profit_margin * 100 if self.metrics.profit_margin else 0,
224
+ "rating": "good" if self.metrics.profit_margin and self.metrics.profit_margin > 0.2 else "average"
225
+ if self.metrics.profit_margin and self.metrics.profit_margin > 0.1 else "poor",
226
+ },
227
+ }
228
+
229
+ def financial_health(self) -> dict:
230
+ """Analyze financial health metrics."""
231
+ debt_equity = self.metrics.debt_to_equity
232
+ current = self.metrics.current_ratio
233
+
234
+ health = {
235
+ "debt_to_equity": debt_equity,
236
+ "current_ratio": current,
237
+ "rating": "good",
238
+ "messages": [],
239
+ }
240
+
241
+ if debt_equity > 2:
242
+ health["rating"] = "poor"
243
+ health["messages"].append("High debt levels")
244
+ elif debt_equity > 1:
245
+ health["rating"] = "average"
246
+ health["messages"].append("Moderate debt levels")
247
+ else:
248
+ health["messages"].append("Low debt levels")
249
+
250
+ if not current:
251
+ # Not reported (typical for banks/insurers) - don't penalise.
252
+ pass
253
+ elif current < 1:
254
+ health["rating"] = "poor"
255
+ health["messages"].append("Current ratio below 1 - liquidity concern")
256
+ elif current > 2:
257
+ health["messages"].append("Strong liquidity position")
258
+ else:
259
+ health["messages"].append("Adequate liquidity")
260
+
261
+ return health
262
+
263
+ def growth_analysis(self) -> dict:
264
+ """Analyze growth metrics."""
265
+ rev_growth = self.metrics.revenue_growth * 100 if self.metrics.revenue_growth else 0
266
+ earn_growth = self.metrics.earnings_growth * 100 if self.metrics.earnings_growth else 0
267
+
268
+ return {
269
+ "revenue_growth": {
270
+ "value": rev_growth,
271
+ "rating": "high" if rev_growth > 20 else "moderate" if rev_growth > 10 else "low",
272
+ },
273
+ "earnings_growth": {
274
+ "value": earn_growth,
275
+ "rating": "high" if earn_growth > 20 else "moderate" if earn_growth > 10 else "low",
276
+ },
277
+ }
278
+
279
+
280
+ def get_summary(self) -> dict:
281
+ """Get comprehensive fundamental analysis summary."""
282
+ pe = self.pe_analysis()
283
+ peg = self.peg_analysis()
284
+ dividend = self.dividend_analysis()
285
+ profitability = self.profitability_analysis()
286
+ health = self.financial_health()
287
+ growth = self.growth_analysis()
288
+
289
+ # Calculate overall score (0-100)
290
+ scores = []
291
+
292
+ # PE Score
293
+ if pe["status"] == Valuation.UNDERVALUED.value:
294
+ scores.append(80)
295
+ elif pe["status"] == Valuation.FAIR.value:
296
+ scores.append(60)
297
+ elif pe["status"] == Valuation.OVERVALUED.value:
298
+ scores.append(30)
299
+
300
+ # Profitability Score
301
+ prof_scores = {"good": 80, "average": 50, "poor": 20}
302
+ scores.append(prof_scores.get(profitability["roe"]["rating"], 50))
303
+
304
+ # Health Score
305
+ health_scores = {"good": 80, "average": 50, "poor": 20}
306
+ scores.append(health_scores.get(health["rating"], 50))
307
+
308
+ # Growth Score
309
+ growth_scores = {"high": 80, "moderate": 50, "low": 30}
310
+ scores.append(growth_scores.get(growth["revenue_growth"]["rating"], 50))
311
+
312
+ overall_score = sum(scores) / len(scores) if scores else 50
313
+
314
+ return {
315
+ "overall_score": round(overall_score),
316
+ "valuation": pe,
317
+ "peg": peg,
318
+ "dividend": dividend,
319
+ "profitability": profitability,
320
+ "financial_health": health,
321
+ "growth": growth,
322
+ "metrics": {
323
+ "pe_ratio": self.metrics.pe_ratio,
324
+ "price_to_book": self.metrics.price_to_book,
325
+ "debt_to_equity": self.metrics.debt_to_equity,
326
+ "roe": self.metrics.roe,
327
+ "dividend_yield": self.metrics.dividend_yield,
328
+ },
329
+ }