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.
- marketmyna/__init__.py +14 -0
- marketmyna/agent/__init__.py +8 -0
- marketmyna/agent/agents.py +543 -0
- marketmyna/agent/context.py +69 -0
- marketmyna/agent/dashboard.py +55 -0
- marketmyna/agent/orchestrator.py +153 -0
- marketmyna/agent/sentiment.py +150 -0
- marketmyna/agent/skills.py +119 -0
- marketmyna/ai/__init__.py +1 -0
- marketmyna/ai/portfolio_optimizer.py +330 -0
- marketmyna/analysis/__init__.py +6 -0
- marketmyna/analysis/fundamental.py +329 -0
- marketmyna/analysis/sip_optimizer.py +81 -0
- marketmyna/analysis/technical.py +321 -0
- marketmyna/api/__init__.py +1 -0
- marketmyna/api/app.py +209 -0
- marketmyna/api/deps.py +63 -0
- marketmyna/api/middlewares/__init__.py +0 -0
- marketmyna/api/middlewares/auth.py +116 -0
- marketmyna/api/middlewares/csrf.py +66 -0
- marketmyna/api/middlewares/error_handler.py +38 -0
- marketmyna/api/middlewares/slashes.py +27 -0
- marketmyna/api/v1/__init__.py +0 -0
- marketmyna/api/v1/endpoints/__init__.py +0 -0
- marketmyna/api/v1/endpoints/account.py +166 -0
- marketmyna/api/v1/endpoints/agent.py +114 -0
- marketmyna/api/v1/endpoints/bot.py +38 -0
- marketmyna/api/v1/endpoints/calendar.py +269 -0
- marketmyna/api/v1/endpoints/data_sources.py +174 -0
- marketmyna/api/v1/endpoints/health.py +10 -0
- marketmyna/api/v1/endpoints/listings.py +564 -0
- marketmyna/api/v1/endpoints/macro.py +163 -0
- marketmyna/api/v1/endpoints/portfolio.py +530 -0
- marketmyna/api/v1/endpoints/push.py +119 -0
- marketmyna/api/v1/endpoints/reports.py +49 -0
- marketmyna/api/v1/endpoints/scheduler.py +219 -0
- marketmyna/api/v1/endpoints/stocks.py +400 -0
- marketmyna/api/v1/endpoints/system_config.py +157 -0
- marketmyna/api/v1/endpoints/thesis.py +112 -0
- marketmyna/api/v1/endpoints/trade.py +709 -0
- marketmyna/api/v1/endpoints/updates.py +394 -0
- marketmyna/api/v1/endpoints/watchlists.py +179 -0
- marketmyna/api/v1/router.py +43 -0
- marketmyna/bot/__init__.py +14 -0
- marketmyna/bot/commands/__init__.py +0 -0
- marketmyna/bot/commands/analyze.py +40 -0
- marketmyna/bot/commands/ask.py +51 -0
- marketmyna/bot/commands/base.py +35 -0
- marketmyna/bot/commands/batch.py +53 -0
- marketmyna/bot/commands/common.py +84 -0
- marketmyna/bot/commands/help.py +43 -0
- marketmyna/bot/commands/markets.py +153 -0
- marketmyna/bot/commands/portfolio.py +145 -0
- marketmyna/bot/commands/research.py +323 -0
- marketmyna/bot/commands/status.py +48 -0
- marketmyna/bot/commands/trade.py +176 -0
- marketmyna/bot/dispatcher.py +95 -0
- marketmyna/bot/formatting.py +25 -0
- marketmyna/bot/models.py +27 -0
- marketmyna/bot/registry.py +50 -0
- marketmyna/bot/telegram_listener.py +227 -0
- marketmyna/cli/__init__.py +1 -0
- marketmyna/cli/scheduled_run.py +292 -0
- marketmyna/core/__init__.py +6 -0
- marketmyna/core/compliance.py +53 -0
- marketmyna/core/config.py +377 -0
- marketmyna/core/constants.py +69 -0
- marketmyna/core/home.py +22 -0
- marketmyna/core/numbers.py +28 -0
- marketmyna/core/secret_store.py +104 -0
- marketmyna/core/tenant.py +130 -0
- marketmyna/data/__init__.py +8 -0
- marketmyna/data/base.py +36 -0
- marketmyna/data/categories.py +272 -0
- marketmyna/data/china.py +360 -0
- marketmyna/data/crypto.py +200 -0
- marketmyna/data/ecb_fx.py +131 -0
- marketmyna/data/extra_sources.py +569 -0
- marketmyna/data/india.py +241 -0
- marketmyna/data/india_filings.py +343 -0
- marketmyna/data/listings.py +1314 -0
- marketmyna/data/mutual_funds.py +34 -0
- marketmyna/data/news.py +98 -0
- marketmyna/data/news_search.py +346 -0
- marketmyna/data/nse_deals.py +138 -0
- marketmyna/data/resilience.py +173 -0
- marketmyna/data/sec_facts.py +252 -0
- marketmyna/data/shariah.py +283 -0
- marketmyna/data/social/__init__.py +9 -0
- marketmyna/data/social/base.py +100 -0
- marketmyna/data/social/browser.py +92 -0
- marketmyna/data/social/hackernews.py +53 -0
- marketmyna/data/social/reddit.py +397 -0
- marketmyna/data/social/stocktwits.py +63 -0
- marketmyna/data/social/valuepickr.py +53 -0
- marketmyna/data/stocks.py +197 -0
- marketmyna/data/yahoo_price.py +48 -0
- marketmyna/launcher.py +71 -0
- marketmyna/llm/__init__.py +33 -0
- marketmyna/llm/backend_factory.py +51 -0
- marketmyna/llm/backend_registry.py +19 -0
- marketmyna/llm/channels.py +447 -0
- marketmyna/llm/json_reply.py +63 -0
- marketmyna/llm/litellm_backend.py +197 -0
- marketmyna/llm/model_catalog.py +166 -0
- marketmyna/llm/model_health.py +193 -0
- marketmyna/llm/protocol.py +111 -0
- marketmyna/llm/providers.py +128 -0
- marketmyna/llm/store.py +128 -0
- marketmyna/notifiers/__init__.py +35 -0
- marketmyna/notifiers/apprise_sender.py +43 -0
- marketmyna/notifiers/astrbot.py +47 -0
- marketmyna/notifiers/bark.py +24 -0
- marketmyna/notifiers/base.py +80 -0
- marketmyna/notifiers/dingtalk.py +54 -0
- marketmyna/notifiers/discord.py +39 -0
- marketmyna/notifiers/dispatcher.py +153 -0
- marketmyna/notifiers/email.py +99 -0
- marketmyna/notifiers/feishu.py +57 -0
- marketmyna/notifiers/googlechat.py +24 -0
- marketmyna/notifiers/gotify.py +36 -0
- marketmyna/notifiers/matrix.py +28 -0
- marketmyna/notifiers/ntfy.py +36 -0
- marketmyna/notifiers/pushbullet.py +24 -0
- marketmyna/notifiers/pushover.py +41 -0
- marketmyna/notifiers/pushplus.py +41 -0
- marketmyna/notifiers/serverchan3.py +40 -0
- marketmyna/notifiers/settings_schema.py +119 -0
- marketmyna/notifiers/signal_messenger.py +25 -0
- marketmyna/notifiers/slack.py +36 -0
- marketmyna/notifiers/teams.py +37 -0
- marketmyna/notifiers/telegram.py +81 -0
- marketmyna/notifiers/twilio.py +33 -0
- marketmyna/notifiers/webhook.py +44 -0
- marketmyna/notifiers/webpush.py +222 -0
- marketmyna/notifiers/wechat.py +40 -0
- marketmyna/notifiers/whatsapp.py +27 -0
- marketmyna/portfolio/__init__.py +6 -0
- marketmyna/portfolio/analytics.py +336 -0
- marketmyna/portfolio/manager.py +383 -0
- marketmyna/portfolio/models.py +155 -0
- marketmyna/portfolio/optimizers.py +88 -0
- marketmyna/portfolio/review.py +411 -0
- marketmyna/services/__init__.py +1 -0
- marketmyna/services/accounts.py +278 -0
- marketmyna/services/ai_access.py +94 -0
- marketmyna/services/analysis_run.py +491 -0
- marketmyna/services/big_trades.py +335 -0
- marketmyna/services/brokers/__init__.py +0 -0
- marketmyna/services/brokers/angel.py +271 -0
- marketmyna/services/brokers/finnhub.py +209 -0
- marketmyna/services/byok_detect.py +196 -0
- marketmyna/services/calendar.py +748 -0
- marketmyna/services/central_banks.py +202 -0
- marketmyna/services/commodities.py +134 -0
- marketmyna/services/compare.py +307 -0
- marketmyna/services/cot.py +148 -0
- marketmyna/services/country_macro.py +135 -0
- marketmyna/services/crude_sensitivity.py +239 -0
- marketmyna/services/crypto_mood.py +135 -0
- marketmyna/services/crypto_screens.py +226 -0
- marketmyna/services/data_export.py +91 -0
- marketmyna/services/dcf.py +220 -0
- marketmyna/services/dcf_model.py +194 -0
- marketmyna/services/delivery.py +142 -0
- marketmyna/services/discovery.py +146 -0
- marketmyna/services/dividends.py +95 -0
- marketmyna/services/earnings.py +305 -0
- marketmyna/services/econ_calendar.py +495 -0
- marketmyna/services/eia_stocks.py +115 -0
- marketmyna/services/estimates.py +109 -0
- marketmyna/services/etf.py +236 -0
- marketmyna/services/etf_rotation.py +193 -0
- marketmyna/services/etf_screens.py +196 -0
- marketmyna/services/financials.py +639 -0
- marketmyna/services/fund_ai.py +181 -0
- marketmyna/services/fund_analytics.py +371 -0
- marketmyna/services/fund_cost.py +147 -0
- marketmyna/services/fund_holdings.py +664 -0
- marketmyna/services/fund_peers.py +247 -0
- marketmyna/services/fund_rotation.py +206 -0
- marketmyna/services/fund_style.py +179 -0
- marketmyna/services/funds.py +70 -0
- marketmyna/services/hot_stocks.py +358 -0
- marketmyna/services/india_flows.py +263 -0
- marketmyna/services/india_rates.py +258 -0
- marketmyna/services/investor_lenses.py +271 -0
- marketmyna/services/leadership.py +165 -0
- marketmyna/services/live_market.py +497 -0
- marketmyna/services/live_quotes.py +295 -0
- marketmyna/services/macro.py +182 -0
- marketmyna/services/market_calendar.py +399 -0
- marketmyna/services/market_mood.py +168 -0
- marketmyna/services/market_valuation.py +231 -0
- marketmyna/services/mf_holdings.py +199 -0
- marketmyna/services/mf_insights.py +132 -0
- marketmyna/services/mf_lumpsum.py +587 -0
- marketmyna/services/mf_portfolio_ai.py +195 -0
- marketmyna/services/nse_events.py +98 -0
- marketmyna/services/overview.py +366 -0
- marketmyna/services/ownership.py +143 -0
- marketmyna/services/quality_scores.py +199 -0
- marketmyna/services/rates.py +335 -0
- marketmyna/services/reconcile.py +343 -0
- marketmyna/services/report.py +584 -0
- marketmyna/services/report_i18n.py +678 -0
- marketmyna/services/rotation.py +224 -0
- marketmyna/services/runtime_scheduler.py +548 -0
- marketmyna/services/schedule_config.py +251 -0
- marketmyna/services/schedule_time.py +39 -0
- marketmyna/services/screens.py +398 -0
- marketmyna/services/sec_mdna.py +173 -0
- marketmyna/services/shariah.py +246 -0
- marketmyna/services/sip.py +148 -0
- marketmyna/services/sip_days.py +362 -0
- marketmyna/services/smart_file.py +595 -0
- marketmyna/services/smart_pdf.py +425 -0
- marketmyna/services/smart_report.py +370 -0
- marketmyna/services/social_sentiment.py +511 -0
- marketmyna/services/thesis.py +328 -0
- marketmyna/services/translate.py +146 -0
- marketmyna/services/valuation.py +173 -0
- marketmyna/services/worker_pool.py +149 -0
- marketmyna/services/world_markets.py +208 -0
- marketmyna/storage.py +722 -0
- marketmyna/strategies/README.md +51 -0
- marketmyna/strategies/bottom_volume.yaml +49 -0
- marketmyna/strategies/box_oscillation.yaml +69 -0
- marketmyna/strategies/bull_trend.yaml +49 -0
- marketmyna/strategies/chan_theory.yaml +58 -0
- marketmyna/strategies/dragon_head.yaml +44 -0
- marketmyna/strategies/emotion_cycle.yaml +79 -0
- marketmyna/strategies/event_driven.yaml +54 -0
- marketmyna/strategies/expectation_repricing.yaml +54 -0
- marketmyna/strategies/growth_quality.yaml +54 -0
- marketmyna/strategies/hot_theme.yaml +54 -0
- marketmyna/strategies/ma_golden_cross.yaml +46 -0
- marketmyna/strategies/one_yang_three_yin.yaml +37 -0
- marketmyna/strategies/shrink_pullback.yaml +46 -0
- marketmyna/strategies/volume_breakout.yaml +47 -0
- marketmyna/strategies/wave_theory.yaml +64 -0
- marketmyna/trading/__init__.py +2 -0
- marketmyna/trading/alerts.py +356 -0
- marketmyna/trading/backtest.py +317 -0
- marketmyna/trading/charges.py +82 -0
- marketmyna/trading/chart.py +197 -0
- marketmyna/trading/indicators.py +303 -0
- marketmyna/trading/journal.py +74 -0
- marketmyna/trading/knn.py +164 -0
- marketmyna/trading/lots.py +109 -0
- marketmyna/trading/market_hours.py +66 -0
- marketmyna/trading/options.py +554 -0
- marketmyna/trading/pairs.py +94 -0
- marketmyna/trading/risk.py +170 -0
- marketmyna/trading/scanner.py +500 -0
- marketmyna/trading/volatility.py +163 -0
- marketmyna/utils/__init__.py +1 -0
- marketmyna/utils/deadline.py +36 -0
- marketmyna/utils/log_redact.py +52 -0
- marketmyna/utils/yahoo_gate.py +183 -0
- marketmyna/utils/yf_lock.py +12 -0
- marketmyna/web/apple-touch-icon.png +0 -0
- marketmyna/web/assets/Account-CA9NFDKO.js +1 -0
- marketmyna/web/assets/Analyze-uM0srB_U.js +7 -0
- marketmyna/web/assets/AreaChart-2i67V6Jf.js +1 -0
- marketmyna/web/assets/BarChart-_yngarVl.js +3 -0
- marketmyna/web/assets/Bot-E4gpf1qZ.js +1 -0
- marketmyna/web/assets/CandleChart-CioqTqMV.js +1 -0
- marketmyna/web/assets/Compare-D6_2CPl4.js +1 -0
- marketmyna/web/assets/ExportMenu-DaZ1xVmW.js +1 -0
- marketmyna/web/assets/LineChart-BaOkD1Ta.js +57 -0
- marketmyna/web/assets/Markets-BMw_M1sJ.js +1 -0
- marketmyna/web/assets/Portfolio-DQEdHVc2.js +1 -0
- marketmyna/web/assets/ReportView-BzhdcqSh.js +1 -0
- marketmyna/web/assets/Scheduler-I9PDKxTd.js +1 -0
- marketmyna/web/assets/Settings-DpXfHolW.js +3 -0
- marketmyna/web/assets/SipDays-C9q3bNSx.js +1 -0
- marketmyna/web/assets/SymbolSearch-CBOVPEk1.js +1 -0
- marketmyna/web/assets/Trade-_CM7HC4H.js +2 -0
- marketmyna/web/assets/TradeChart-ZjsycacC.js +1 -0
- marketmyna/web/assets/WatchButton-ComYW56_.js +1 -0
- marketmyna/web/assets/Watchlist-DAjLb0jd.js +1 -0
- marketmyna/web/assets/app-BP1o7WZS.js +1 -0
- marketmyna/web/assets/app-BkIUKdld.js +1 -0
- marketmyna/web/assets/app-CKeSYhEt.js +1 -0
- marketmyna/web/assets/app-CN1OERYX.js +1 -0
- marketmyna/web/assets/app-DIQllha5.js +1 -0
- marketmyna/web/assets/app-DeiAW0tX.js +1 -0
- marketmyna/web/assets/br-Cu5YU29T.svg +45 -0
- marketmyna/web/assets/browser-CR-ks2Fp.js +19 -0
- marketmyna/web/assets/browser-D5K_K_gj.js +1 -0
- marketmyna/web/assets/common-0wvpAVpd.js +1 -0
- marketmyna/web/assets/common-3ghy53Us.js +1 -0
- marketmyna/web/assets/common-Bib2unjX.js +1 -0
- marketmyna/web/assets/common-BvSJP0w_.js +1 -0
- marketmyna/web/assets/common-CSfyC0Sp.js +1 -0
- marketmyna/web/assets/common-C_t-iZPf.js +1 -0
- marketmyna/web/assets/compare-DMcsx70H.js +1 -0
- marketmyna/web/assets/es-d5m8M5h8.svg +544 -0
- marketmyna/web/assets/exporter-DiMg2O5d.js +3 -0
- marketmyna/web/assets/files-CpgiZRbN.js +1 -0
- marketmyna/web/assets/format-d8InM0Zw.js +9 -0
- marketmyna/web/assets/html2canvas-Bw0OShmD.js +5 -0
- marketmyna/web/assets/index-CKAnq-Or.js +12 -0
- marketmyna/web/assets/index-D4U8E6LH.css +2 -0
- marketmyna/web/assets/index.es-iBEw7jOv.js +5 -0
- marketmyna/web/assets/ir-cCIgaNf6.svg +219 -0
- marketmyna/web/assets/jspdf.es.min-C4285IdL.js +79 -0
- marketmyna/web/assets/jspdf.plugin.autotable-2eOJeXl-.js +2 -0
- marketmyna/web/assets/lightweight-charts.production-BF2noug5.js +1 -0
- marketmyna/web/assets/listings-BefgXpb1.js +1 -0
- marketmyna/web/assets/liveQuotes-CtSmCUFA.js +1 -0
- marketmyna/web/assets/markets-BI4AhI9W.js +1 -0
- marketmyna/web/assets/markets-BUHUdYpT.js +1 -0
- marketmyna/web/assets/markets-BqvN1Bvf.js +1 -0
- marketmyna/web/assets/markets-DPO40TTe.js +1 -0
- marketmyna/web/assets/markets-bsiPo-Tk.js +1 -0
- marketmyna/web/assets/markets-ff1e792u.js +1 -0
- marketmyna/web/assets/montecarlo-BsNlH2qe.js +1 -0
- marketmyna/web/assets/options-BYAZnwzW.js +1 -0
- marketmyna/web/assets/preload-helper-BLdHbfdN.js +1 -0
- marketmyna/web/assets/purify.es-Dqk5qvad.js +3 -0
- marketmyna/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- marketmyna/web/assets/sa-Dh79zbT9.svg +25 -0
- marketmyna/web/assets/toast-DDradTZw.js +1 -0
- marketmyna/web/assets/trade-fndw1Ql3.js +1 -0
- marketmyna/web/assets/ui-gsFb8B2m.js +1 -0
- marketmyna/web/assets/watchlists-B6YqwUIk.js +1 -0
- marketmyna/web/brand/logo-mark.webp +0 -0
- marketmyna/web/favicon.png +0 -0
- marketmyna/web/icon-192.png +0 -0
- marketmyna/web/icon-512.png +0 -0
- marketmyna/web/index.html +34 -0
- marketmyna/web/manifest.webmanifest +28 -0
- marketmyna/web/robots.txt +2 -0
- marketmyna/web/sw.js +40 -0
- marketmyna/web/theme-init.js +37 -0
- marketmyna-0.2.0.dist-info/METADATA +345 -0
- marketmyna-0.2.0.dist-info/RECORD +344 -0
- marketmyna-0.2.0.dist-info/WHEEL +5 -0
- marketmyna-0.2.0.dist-info/entry_points.txt +2 -0
- marketmyna-0.2.0.dist-info/licenses/LICENSE +202 -0
- marketmyna-0.2.0.dist-info/licenses/NOTICE +22 -0
- 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,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
|
+
}
|