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,271 @@
|
|
|
1
|
+
"""Investor checklists: one company run through the published rules of five
|
|
2
|
+
well-known investing styles, each rule pass / fail with its number.
|
|
3
|
+
|
|
4
|
+
Rules are taken from the investors' own books and letters, with the
|
|
5
|
+
thresholds they gave; where a rule needs more history than the filings
|
|
6
|
+
shown (Graham's ten years of earnings), it is scaled to the years available
|
|
7
|
+
and says so. These are checklists of public criteria applied to published
|
|
8
|
+
figures. They do not say what any of these investors would do, and they are
|
|
9
|
+
not advice.
|
|
10
|
+
|
|
11
|
+
- Graham, defensive investor (The Intelligent Investor, ch. 14)
|
|
12
|
+
- Buffett-style quality (Berkshire letters: durable high returns on equity,
|
|
13
|
+
little debt, owner earnings)
|
|
14
|
+
- Lynch (One Up on Wall Street: PEG, dividend-adjusted PEG, debt, inventories)
|
|
15
|
+
- Greenblatt magic formula (The Little Book That Beats the Market: earnings
|
|
16
|
+
yield and return on capital; the book ranks a whole market, fixed
|
|
17
|
+
thresholds stand in for the ranking here)
|
|
18
|
+
- Marks-style risk check (Howard Marks' memos: avoid fragile balance sheets,
|
|
19
|
+
prefer buying after prices have fallen)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from ..core.numbers import ratio as _div
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
CACHE_SECONDS = 6 * 3600
|
|
34
|
+
# Graham's "adequate size" (sales of $100m in 1972), in today's money
|
|
35
|
+
MIN_SALES = {"USD": 700e6, "INR": 5_000e7} # ₹5,000 crore
|
|
36
|
+
|
|
37
|
+
_cache: dict[tuple[str, str], tuple[float, dict]] = {}
|
|
38
|
+
_lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _row(fin: dict, statement: str, key: str) -> list[Optional[float]]:
|
|
42
|
+
return next((r["values"] for r in fin.get(statement, {}).get("rows", []) if r["key"] == key), [])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _first(values: list) -> Optional[float]:
|
|
46
|
+
return values[0] if values and values[0] is not None else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _num(x) -> Optional[float]:
|
|
50
|
+
try:
|
|
51
|
+
v = float(x)
|
|
52
|
+
except (TypeError, ValueError):
|
|
53
|
+
return None
|
|
54
|
+
return v if v == v and abs(v) != float("inf") else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _cagr(newest, oldest, years) -> Optional[float]:
|
|
58
|
+
if newest is None or oldest is None or newest <= 0 or oldest <= 0 or years <= 0:
|
|
59
|
+
return None
|
|
60
|
+
return (newest / oldest) ** (1 / years) - 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pct(x: Optional[float], digits: int = 1) -> str:
|
|
64
|
+
return f"{x * 100:.{digits}f}%" if x is not None else "n/a"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _x(x: Optional[float], digits: int = 2) -> str:
|
|
68
|
+
return f"{x:.{digits}f}" if x is not None else "n/a"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def rule(key: str, label: str, ok: Optional[bool], value: str, why: str) -> dict:
|
|
72
|
+
return {"key": key, "label": label, "pass": ok, "value": value, "why": why}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _lens(key: str, name: str, source: str, rules: list[dict], note: Optional[str] = None) -> dict:
|
|
76
|
+
tested = [r for r in rules if r["pass"] is not None]
|
|
77
|
+
passed = sum(1 for r in tested if r["pass"])
|
|
78
|
+
return {"key": key, "name": name, "source": source, "rules": rules, "passed": passed,
|
|
79
|
+
"tested": len(tested), "total": len(rules), "note": note}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def evaluate(fin: dict, info: dict) -> dict:
|
|
83
|
+
"""All five lenses from financials.py's annual payload and Yahoo's info."""
|
|
84
|
+
financial = bool(fin.get("financial_sector"))
|
|
85
|
+
ccy = fin.get("currency") or info.get("financialCurrency")
|
|
86
|
+
years = len(fin.get("periods") or [])
|
|
87
|
+
|
|
88
|
+
revenue = _row(fin, "income", "revenue")
|
|
89
|
+
ni = _row(fin, "income", "net_income")
|
|
90
|
+
eps = _row(fin, "income", "eps")
|
|
91
|
+
gp = _row(fin, "income", "gross_profit")
|
|
92
|
+
ebit_row = _row(fin, "income", "operating_income")
|
|
93
|
+
interest = _row(fin, "income", "interest_expense")
|
|
94
|
+
ebitda = _first(_row(fin, "income", "ebitda"))
|
|
95
|
+
equity = _row(fin, "balance", "equity")
|
|
96
|
+
ca, cl = _first(_row(fin, "balance", "current_assets")), _first(_row(fin, "balance", "current_liabilities"))
|
|
97
|
+
debt = _first(_row(fin, "balance", "total_debt"))
|
|
98
|
+
cash = _first(_row(fin, "balance", "cash"))
|
|
99
|
+
inventory = _row(fin, "balance", "inventory")
|
|
100
|
+
ppe = _first(_row(fin, "balance", "ppe"))
|
|
101
|
+
ocf, capex = _row(fin, "cashflow", "operating_cf"), _row(fin, "cashflow", "capex")
|
|
102
|
+
dividends = _row(fin, "cashflow", "dividends")
|
|
103
|
+
|
|
104
|
+
pe = _num(info.get("trailingPE"))
|
|
105
|
+
pb = _num(info.get("priceToBook"))
|
|
106
|
+
beta = _num(info.get("beta"))
|
|
107
|
+
price = _num(info.get("currentPrice") or info.get("regularMarketPrice"))
|
|
108
|
+
high = _num(info.get("fiftyTwoWeekHigh"))
|
|
109
|
+
dy = _num(info.get("dividendYield"))
|
|
110
|
+
dy = dy / 100 if dy is not None and dy > 1 else dy # Yahoo now reports percent
|
|
111
|
+
shares = _num(info.get("sharesOutstanding"))
|
|
112
|
+
|
|
113
|
+
ebit = _first(ebit_row)
|
|
114
|
+
cur_ratio = _div(ca, cl)
|
|
115
|
+
de = _div(debt, _first(equity))
|
|
116
|
+
known_eps = [(i, v) for i, v in enumerate(eps) if v is not None]
|
|
117
|
+
eps_growth = (_cagr(known_eps[0][1], known_eps[-1][1], known_eps[-1][0] - known_eps[0][0])
|
|
118
|
+
if len(known_eps) >= 2 else None)
|
|
119
|
+
growth_basis = "EPS"
|
|
120
|
+
if eps_growth is None: # some filings carry no EPS history: net income instead
|
|
121
|
+
known_ni = [(i, v) for i, v in enumerate(ni) if v is not None]
|
|
122
|
+
if len(known_ni) >= 2:
|
|
123
|
+
eps_growth = _cagr(known_ni[0][1], known_ni[-1][1], known_ni[-1][0] - known_ni[0][0])
|
|
124
|
+
growth_basis = "net income"
|
|
125
|
+
ni_known = [v for v in ni if v is not None]
|
|
126
|
+
fcf = [o + c for o, c in zip(ocf, capex) if o is not None and c is not None]
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------- Graham
|
|
129
|
+
min_sales = MIN_SALES.get(ccy or "")
|
|
130
|
+
wc = ca - cl if ca is not None and cl is not None else None
|
|
131
|
+
if financial or debt is None or wc is None:
|
|
132
|
+
debt_wc_ok, debt_wc_text = None, "n/a"
|
|
133
|
+
elif wc <= 0:
|
|
134
|
+
debt_wc_ok, debt_wc_text = debt <= 0, "working capital negative"
|
|
135
|
+
else:
|
|
136
|
+
debt_wc_ok, debt_wc_text = debt <= wc, f"debt {debt / wc:.2f}x working capital"
|
|
137
|
+
graham = [
|
|
138
|
+
rule("size", "Adequate size", None if min_sales is None or not revenue or revenue[0] is None else revenue[0] >= min_sales,
|
|
139
|
+
f"sales {revenue[0] / 1e9:.1f}bn {ccy}" if revenue and revenue[0] else "n/a",
|
|
140
|
+
"Large enough to weather bad years (sales of $100m in 1972, about $700m / ₹5,000 cr today)"),
|
|
141
|
+
rule("current", "Current ratio at least 2", None if financial or cur_ratio is None else cur_ratio >= 2,
|
|
142
|
+
_x(cur_ratio), "Current assets at least twice current liabilities"),
|
|
143
|
+
rule("debt_wc", "Debt no more than working capital", debt_wc_ok, debt_wc_text,
|
|
144
|
+
"Long-term debt should not exceed net current assets"),
|
|
145
|
+
rule("stable", f"Profit in each of the {len(ni_known)} years shown", None if len(ni_known) < 3 else all(v > 0 for v in ni_known),
|
|
146
|
+
f"{sum(1 for v in ni_known if v > 0)} of {len(ni_known)} profitable", "Graham asked for 10 years without a loss"),
|
|
147
|
+
rule("dividend", "Dividend paid each year shown", None if not dividends else all((v or 0) != 0 for v in dividends),
|
|
148
|
+
f"{sum(1 for v in dividends if v)} of {len(dividends)} years" if dividends else "n/a",
|
|
149
|
+
"Graham asked for an unbroken dividend record of 20 years"),
|
|
150
|
+
rule("growth", "Earnings growth of 3%+ a year", None if eps_growth is None else eps_growth >= 0.029,
|
|
151
|
+
f"{growth_basis} {_pct(eps_growth)} a year", "A third over ten years, about 2.9% a year"),
|
|
152
|
+
rule("pe", "P/E at most 15", None if pe is None else pe <= 15, _x(pe, 1), "Price no more than 15 times earnings"),
|
|
153
|
+
rule("pe_pb", "P/E x P/B at most 22.5", None if pe is None or pb is None else pe * pb <= 22.5,
|
|
154
|
+
_x(pe * pb if pe is not None and pb is not None else None, 1), "Allows a higher P/E when the price is close to book value"),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------- Buffett
|
|
158
|
+
roes = [_div(n, e) for n, e in zip(ni, equity) if n is not None and e not in (None, 0) and e > 0]
|
|
159
|
+
roe_avg = sum(roes) / len(roes) if roes else None
|
|
160
|
+
margin = _div(_first(ni), _first(revenue))
|
|
161
|
+
gross = _div(_first(gp), _first(revenue))
|
|
162
|
+
if gross is not None and gross >= 0.999:
|
|
163
|
+
gross = None # no cost of goods reported
|
|
164
|
+
eps_steps = [a > b for (_, a), (_, b) in zip(known_eps, known_eps[1:])]
|
|
165
|
+
capex_ratio = None
|
|
166
|
+
cap_vals = [abs(c) for c in capex if c is not None]
|
|
167
|
+
if cap_vals and ni_known and sum(ni_known) > 0:
|
|
168
|
+
capex_ratio = sum(cap_vals[:len(ni_known)]) / sum(ni_known)
|
|
169
|
+
buffett = [
|
|
170
|
+
rule("roe", "Return on equity 15%+ on average", None if roe_avg is None else roe_avg >= 0.15,
|
|
171
|
+
f"{_pct(roe_avg)} average over {len(roes)} years", "Durable high returns on the owners' money"),
|
|
172
|
+
rule("roe_floor", "No year below 10% return on equity", None if len(roes) < 3 else min(roes) >= 0.10,
|
|
173
|
+
f"lowest {_pct(min(roes)) if roes else 'n/a'}", "Consistency matters more than one good year"),
|
|
174
|
+
rule("debt", "Debt under half of equity", None if financial or de is None else de <= 0.5, _x(de), "Little borrowing"),
|
|
175
|
+
rule("margin", "Net margin 10%+", None if margin is None else margin >= 0.10, _pct(margin), "Pricing power shows up in margins"),
|
|
176
|
+
rule("gross", "Gross margin 40%+", None if financial or gross is None else gross >= 0.40, _pct(gross),
|
|
177
|
+
"A moat usually means a high gross margin"),
|
|
178
|
+
rule("eps_steady", "Earnings rose in every year shown", None if len(eps_steps) < 2 else all(eps_steps),
|
|
179
|
+
f"{sum(eps_steps)} of {len(eps_steps)} years up" if eps_steps else "n/a", "Predictable, growing earnings"),
|
|
180
|
+
rule("fcf", "Free cash flow positive each year", None if financial or len(fcf) < 3 else all(v > 0 for v in fcf),
|
|
181
|
+
f"{sum(1 for v in fcf if v > 0)} of {len(fcf)} years" if fcf else "n/a", "Owner earnings: cash left after keeping the business going"),
|
|
182
|
+
rule("capex", "Capital spending under half of profits", None if financial or capex_ratio is None else capex_ratio <= 0.5,
|
|
183
|
+
_pct(capex_ratio, 0), "The business does not eat its own earnings"),
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------- Lynch
|
|
187
|
+
growth_pct = eps_growth * 100 if eps_growth is not None else None
|
|
188
|
+
peg = pe / growth_pct if pe is not None and growth_pct and growth_pct > 0 else None
|
|
189
|
+
yield_pct = dy * 100 if dy is not None else 0.0
|
|
190
|
+
adj = (growth_pct + yield_pct) / pe if pe and growth_pct is not None else None
|
|
191
|
+
inv_growth = _div(inventory[0], inventory[1]) if len(inventory) > 1 and inventory[0] is not None and inventory[1] else None
|
|
192
|
+
sales_growth = _div(revenue[0], revenue[1]) if len(revenue) > 1 and revenue[0] is not None and revenue[1] else None
|
|
193
|
+
net_cash_ps = (cash - debt) / shares if cash is not None and debt is not None and shares else None
|
|
194
|
+
lynch = [
|
|
195
|
+
rule("peg", "PEG at most 1", None if peg is None else peg <= 1, _x(peg),
|
|
196
|
+
f"P/E {_x(pe, 1)} against {growth_basis} growth of {_x(growth_pct, 1)}% a year"),
|
|
197
|
+
rule("adj_peg", "(Growth + dividend yield) / P/E at least 1", None if adj is None else adj >= 1, _x(adj),
|
|
198
|
+
"Lynch's dividend-adjusted test: 1 is okay, 2 is very good"),
|
|
199
|
+
rule("debt", "Debt at most a third of equity", None if financial or de is None else de <= 0.33, _x(de),
|
|
200
|
+
"A normal balance sheet is about 75% equity, 25% debt"),
|
|
201
|
+
rule("inventory", "Inventories not growing faster than sales", None if inv_growth is None or sales_growth is None else inv_growth <= sales_growth * 1.02,
|
|
202
|
+
f"inventory {_pct(inv_growth - 1 if inv_growth else None)}, sales {_pct(sales_growth - 1 if sales_growth else None)}",
|
|
203
|
+
"Piling-up stock is a warning sign"),
|
|
204
|
+
rule("net_cash", "Net cash on the balance sheet", None if financial or net_cash_ps is None else net_cash_ps > 0,
|
|
205
|
+
f"{net_cash_ps:.2f} a share" if net_cash_ps is not None else "n/a", "Cash minus debt per share: a cushion"),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------- Greenblatt
|
|
209
|
+
ev = _num(info.get("enterpriseValue"))
|
|
210
|
+
if ev is None and info.get("marketCap") and debt is not None and cash is not None:
|
|
211
|
+
ev = float(info["marketCap"]) + debt - cash
|
|
212
|
+
ey = _div(ebit, ev) if ev and ev > 0 else None
|
|
213
|
+
capital = (ca - cl + ppe) if ca is not None and cl is not None and ppe is not None else None
|
|
214
|
+
roc = _div(ebit, capital) if capital and capital > 0 else None
|
|
215
|
+
same_ccy = not ccy or ccy == info.get("currency") # the filings' currency, not Yahoo's
|
|
216
|
+
greenblatt = [
|
|
217
|
+
rule("ey", "Earnings yield (EBIT / EV) 8%+", None if financial or ey is None or not same_ccy else ey >= 0.08, _pct(ey),
|
|
218
|
+
"Cheap: operating profit against the whole firm's price"),
|
|
219
|
+
rule("roc", "Return on capital 25%+", None if financial or roc is None else roc >= 0.25, _pct(roc),
|
|
220
|
+
"Good: operating profit against working capital plus fixed assets"),
|
|
221
|
+
]
|
|
222
|
+
g_note = "The book excludes banks, insurers and utilities." if financial else \
|
|
223
|
+
"The magic formula ranks every stock on both measures; fixed cut-offs stand in for the ranking here."
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------- Marks
|
|
226
|
+
int_cover = _div(ebit, abs(_first(interest))) if _first(interest) else None
|
|
227
|
+
nd_ebitda = _div((debt or 0) - (cash or 0), ebitda) if ebitda and ebitda > 0 and debt is not None else None
|
|
228
|
+
off_high = 1 - price / high if price and high else None
|
|
229
|
+
marks = [
|
|
230
|
+
rule("cover", "Interest covered 5 times", None if financial else (True if int_cover is None and debt == 0 else None if int_cover is None else int_cover >= 5),
|
|
231
|
+
_x(int_cover, 1) + "x" if int_cover is not None else ("no debt" if debt == 0 else "n/a"), "Can pay its lenders in a bad year"),
|
|
232
|
+
rule("leverage", "Net debt under 2 years of EBITDA", None if financial or nd_ebitda is None else nd_ebitda <= 2, _x(nd_ebitda, 1),
|
|
233
|
+
"Room to survive a downturn"),
|
|
234
|
+
rule("liquidity", "Current ratio 1.5+", None if financial or cur_ratio is None else cur_ratio >= 1.5, _x(cur_ratio), "Short-term bills covered"),
|
|
235
|
+
rule("beta", "Moves less than the market (beta under 1)", None if beta is None else beta < 1, _x(beta), "Lower volatility"),
|
|
236
|
+
rule("fallen", "At least 15% below the 52-week high", None if off_high is None else off_high >= 0.15, _pct(off_high),
|
|
237
|
+
"Buying after prices fall, when others are fearful"),
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
lenses = [
|
|
241
|
+
_lens("graham", "Graham: defensive investor", "The Intelligent Investor, chapter 14", graham,
|
|
242
|
+
f"{years} years of filings shown; Graham's tests ask for 10-20." if years and years < 10 else None),
|
|
243
|
+
_lens("buffett", "Buffett-style quality", "Berkshire Hathaway shareholder letters", buffett),
|
|
244
|
+
_lens("lynch", "Lynch", "One Up on Wall Street", lynch),
|
|
245
|
+
_lens("greenblatt", "Greenblatt: magic formula", "The Little Book That Beats the Market", greenblatt, g_note),
|
|
246
|
+
_lens("marks", "Marks-style risk check", "Howard Marks' memos to Oaktree clients", marks),
|
|
247
|
+
]
|
|
248
|
+
return {"lenses": lenses, "financial_sector": financial, "currency": ccy}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build(symbol: str, market: str, use_cache: bool = True) -> dict:
|
|
252
|
+
from .analysis_run import yahoo_symbol
|
|
253
|
+
from .compare import _yahoo_info
|
|
254
|
+
from .financials import cached_financials
|
|
255
|
+
|
|
256
|
+
if market == "crypto":
|
|
257
|
+
raise ValueError("Investor checklists apply to companies, not crypto")
|
|
258
|
+
key = (symbol, market)
|
|
259
|
+
if use_cache:
|
|
260
|
+
with _lock:
|
|
261
|
+
hit = _cache.get(key)
|
|
262
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
263
|
+
return hit[1]
|
|
264
|
+
fin = cached_financials(symbol, market, "annual")
|
|
265
|
+
info = _yahoo_info(yahoo_symbol(symbol, market))
|
|
266
|
+
out = {"symbol": symbol, "market": market, "name": fin.get("name") or symbol, **evaluate(fin, info),
|
|
267
|
+
"source": f"Annual figures: {fin.get('source') or 'reported financial statements'}; price and ratios: market data feed"}
|
|
268
|
+
if use_cache:
|
|
269
|
+
with _lock:
|
|
270
|
+
_cache[key] = (time.time(), out)
|
|
271
|
+
return out
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""CEO and leadership snapshot for one company.
|
|
2
|
+
|
|
3
|
+
The facts come from Yahoo Finance: the officer list (name, title, age,
|
|
4
|
+
reported pay) and ISS governance risk scores (1 = low risk ... 10 = high,
|
|
5
|
+
US listings mostly). An optional AI brief (ai_ceo_brief) adds a short
|
|
6
|
+
introduction, track record, pitfalls and a verdict on whether the company
|
|
7
|
+
looks in safe hands - grounded on the current officer list and recent
|
|
8
|
+
headlines so the model does not describe a predecessor.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from ..llm.json_reply import parse_object
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Most specific first: an "Executive Chairman" must not win over the "CEO".
|
|
23
|
+
_CEO_PATTERNS = (
|
|
24
|
+
r"\bceo\b",
|
|
25
|
+
r"chief executive",
|
|
26
|
+
r"\bmanaging director\b|\bmd\b",
|
|
27
|
+
r"\bpresident\b",
|
|
28
|
+
r"\bchairman\b|\bchair\b",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_RISK_KEYS = (
|
|
32
|
+
("overallRisk", "Overall"),
|
|
33
|
+
("boardRisk", "Board"),
|
|
34
|
+
("auditRisk", "Audit"),
|
|
35
|
+
("compensationRisk", "Compensation"),
|
|
36
|
+
("shareHolderRightsRisk", "Shareholder rights"),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _clean_name(name: Optional[str]) -> Optional[str]:
|
|
41
|
+
if not name:
|
|
42
|
+
return None
|
|
43
|
+
return re.sub(r"\s+", " ", name).strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _officer(raw: dict) -> dict:
|
|
47
|
+
pay = raw.get("totalPay")
|
|
48
|
+
return {
|
|
49
|
+
"name": _clean_name(raw.get("name")),
|
|
50
|
+
"title": raw.get("title"),
|
|
51
|
+
"age": raw.get("age"),
|
|
52
|
+
"year_born": raw.get("yearBorn"),
|
|
53
|
+
"total_pay": pay if isinstance(pay, (int, float)) and pay > 0 else None,
|
|
54
|
+
"fiscal_year": raw.get("fiscalYear"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def pick_ceo(officers: list[dict]) -> Optional[dict]:
|
|
59
|
+
"""The officer whose title best matches the chief executive role."""
|
|
60
|
+
for pattern in _CEO_PATTERNS:
|
|
61
|
+
for o in officers:
|
|
62
|
+
if o.get("title") and re.search(pattern, o["title"], re.IGNORECASE):
|
|
63
|
+
return o
|
|
64
|
+
return officers[0] if officers else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _epoch_date(value) -> Optional[str]:
|
|
68
|
+
if not isinstance(value, (int, float)) or value <= 0:
|
|
69
|
+
return None
|
|
70
|
+
return datetime.fromtimestamp(value, tz=timezone.utc).date().isoformat()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def leadership_from_info(info: dict) -> dict:
|
|
74
|
+
officers = [_officer(o) for o in info.get("companyOfficers") or [] if o.get("name")]
|
|
75
|
+
if not officers:
|
|
76
|
+
raise ValueError("no officer data published for this company")
|
|
77
|
+
ceo = pick_ceo(officers)
|
|
78
|
+
governance = [
|
|
79
|
+
{"key": key, "label": label, "score": info[key]}
|
|
80
|
+
for key, label in _RISK_KEYS
|
|
81
|
+
if isinstance(info.get(key), (int, float))
|
|
82
|
+
]
|
|
83
|
+
return {
|
|
84
|
+
"name": info.get("longName") or info.get("shortName"),
|
|
85
|
+
"currency": info.get("financialCurrency") or info.get("currency"),
|
|
86
|
+
"ceo": ceo,
|
|
87
|
+
"officers": [o for o in officers if o is not ceo][:6],
|
|
88
|
+
"governance": governance,
|
|
89
|
+
"governance_as_of": _epoch_date(info.get("governanceEpochDate")),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_leadership(symbol: str, market: str) -> dict:
|
|
94
|
+
if market == "crypto":
|
|
95
|
+
raise ValueError("not applicable to crypto")
|
|
96
|
+
import yfinance as yf
|
|
97
|
+
|
|
98
|
+
from .analysis_run import yahoo_symbol
|
|
99
|
+
|
|
100
|
+
info = yf.Ticker(yahoo_symbol(symbol, market)).info or {}
|
|
101
|
+
return {"symbol": symbol, "market": market, **leadership_from_info(info)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_brief(text: str) -> Optional[dict]:
|
|
105
|
+
data = parse_object(text)
|
|
106
|
+
if data is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def strings(value) -> list[str]:
|
|
110
|
+
return [str(v).strip() for v in value if str(v).strip()] if isinstance(value, list) else []
|
|
111
|
+
|
|
112
|
+
verdict = str(data.get("verdict") or "").lower()
|
|
113
|
+
brief = {
|
|
114
|
+
"introduction": str(data.get("introduction") or "").strip(),
|
|
115
|
+
"achievements": strings(data.get("achievements")),
|
|
116
|
+
"pitfalls": strings(data.get("pitfalls")),
|
|
117
|
+
"verdict": verdict if verdict in ("strong", "steady", "uncertain", "concerning") else "uncertain",
|
|
118
|
+
"verdict_reason": str(data.get("verdict_reason") or "").strip(),
|
|
119
|
+
}
|
|
120
|
+
return brief if brief["introduction"] or brief["achievements"] else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def ai_ceo_brief(data: dict, config, headlines: Optional[list[str]] = None) -> Optional[dict]:
|
|
124
|
+
"""LLM brief on the current CEO. None when no LLM is configured or the
|
|
125
|
+
reply cannot be parsed."""
|
|
126
|
+
from ..llm.channels import llm_configured
|
|
127
|
+
|
|
128
|
+
if config is None or not llm_configured(config) or not data.get("ceo"):
|
|
129
|
+
return None
|
|
130
|
+
from ..llm.backend_factory import create_generation_backend
|
|
131
|
+
from ..llm.protocol import GenerationConfig
|
|
132
|
+
|
|
133
|
+
ceo = data["ceo"]
|
|
134
|
+
lines = [
|
|
135
|
+
f"Company: {data['name']} ({data['symbol']}, market {data['market']}).",
|
|
136
|
+
f"Current chief executive per the latest filing: {ceo['name']} - {ceo['title']}"
|
|
137
|
+
+ (f", age {ceo['age']}" if ceo.get("age") else "") + ".",
|
|
138
|
+
"Other officers: " + "; ".join(f"{o['name']} ({o['title']})" for o in data["officers"]) + ".",
|
|
139
|
+
]
|
|
140
|
+
if data["governance"]:
|
|
141
|
+
lines.append("ISS governance risk (1 low - 10 high): "
|
|
142
|
+
+ ", ".join(f"{g['label']} {g['score']}" for g in data["governance"]) + ".")
|
|
143
|
+
if headlines:
|
|
144
|
+
lines.append("Recent headlines:\n" + "\n".join(f"- {h}" for h in headlines))
|
|
145
|
+
|
|
146
|
+
system = (
|
|
147
|
+
"You profile company chief executives for retail investors. Write about the CURRENT chief "
|
|
148
|
+
"executive named in the input - if they took the role recently, say so and cover their record "
|
|
149
|
+
"before the role. Reply with ONLY a JSON object, no prose around it:\n"
|
|
150
|
+
'{"introduction": "2-3 sentences: who they are, background, when they took the role", '
|
|
151
|
+
'"achievements": ["3-5 short, concrete wins with years where known"], '
|
|
152
|
+
'"pitfalls": ["2-4 short, concrete missteps, controversies or open risks under their leadership"], '
|
|
153
|
+
'"verdict": "strong | steady | uncertain | concerning", '
|
|
154
|
+
'"verdict_reason": "1-2 sentences on how safe the company looks under this leader"}\n'
|
|
155
|
+
"Be factual and balanced; if you are unsure of a fact, leave it out rather than guess. "
|
|
156
|
+
"No buy/sell advice, no disclaimers."
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
result = create_generation_backend(config).generate(
|
|
160
|
+
"\n".join(lines), GenerationConfig(max_tokens=800, timeout=110), system_prompt=system
|
|
161
|
+
)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
logger.warning("CEO brief unavailable: %s", exc)
|
|
164
|
+
return None
|
|
165
|
+
return _parse_brief(result.text or "")
|