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,305 @@
|
|
|
1
|
+
"""Earnings preview and review for one stock (the equity-research "earnings
|
|
2
|
+
preview" / "earnings reviewer" workflow, on free data).
|
|
3
|
+
|
|
4
|
+
Before the report:
|
|
5
|
+
- next date and time, consensus EPS / revenue and how estimates moved
|
|
6
|
+
(services/estimates.py)
|
|
7
|
+
- beat / miss record and how the stock really moved on past report days:
|
|
8
|
+
last close before the announcement to the first close after it, so a
|
|
9
|
+
pre-open and an after-close report are both measured on the right day
|
|
10
|
+
- the move the options market prices in (at-the-money straddle on the first
|
|
11
|
+
expiry after the date), where an option chain is available
|
|
12
|
+
- what to watch: the metrics that move this kind of business
|
|
13
|
+
|
|
14
|
+
After the report: actual vs estimated EPS, the surprise and the price
|
|
15
|
+
reaction, for the "results are out" view and message.
|
|
16
|
+
|
|
17
|
+
Every figure carries its source (Yahoo Finance; options from the exchange or
|
|
18
|
+
Yahoo). Nothing here is a recommendation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from datetime import date, datetime, timedelta, timezone
|
|
27
|
+
from typing import Callable, Optional
|
|
28
|
+
from zoneinfo import ZoneInfo
|
|
29
|
+
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
from ..core.numbers import finite
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
CACHE_SECONDS = 6 * 3600
|
|
37
|
+
RECENT_DAYS = 4 # a report this many days old still counts as "just out"
|
|
38
|
+
# An estimate or actual this many times off the company's usual quarterly EPS
|
|
39
|
+
# means Yahoo mixed up periods (annual vs quarterly, a split), not a real miss:
|
|
40
|
+
# shown, but flagged and left out of the averages.
|
|
41
|
+
OFF_SCALE = 2.5
|
|
42
|
+
CLOSE = {"india": (15, 30, "Asia/Kolkata"), "us": (16, 0, "America/New_York"),
|
|
43
|
+
"hong_kong": (16, 0, "Asia/Hong_Kong"), "china_a": (15, 0, "Asia/Shanghai")}
|
|
44
|
+
|
|
45
|
+
# What moves each kind of business on results day (beyond revenue, EPS,
|
|
46
|
+
# margins and guidance, which matter everywhere).
|
|
47
|
+
WATCH = {
|
|
48
|
+
"Technology": ["Revenue growth in constant currency and deal wins / bookings",
|
|
49
|
+
"Operating margin and wage or cloud-cost pressure",
|
|
50
|
+
"Full-year revenue guidance (raised, held or cut)",
|
|
51
|
+
"Headcount, attrition and utilisation (IT services)"],
|
|
52
|
+
"Financial Services": ["Net interest margin (NIM)", "Asset quality: gross / net NPA and slippages",
|
|
53
|
+
"Loan and deposit growth, CASA ratio", "Credit cost and provisions",
|
|
54
|
+
"Fee income and cost-to-income ratio"],
|
|
55
|
+
"Consumer Defensive": ["Volume growth (not just price-led growth)", "Rural vs urban demand",
|
|
56
|
+
"Gross margin vs input costs (palm oil, crude, milk...)", "Ad spend as a share of sales"],
|
|
57
|
+
"Consumer Cyclical": ["Volumes and average selling price", "Same-store sales growth (retail)",
|
|
58
|
+
"EBITDA margin vs raw-material costs", "Inventory and dealer stock levels"],
|
|
59
|
+
"Healthcare": ["US generics pricing and new launches (pharma)", "Specialty / branded mix",
|
|
60
|
+
"R&D spend as a share of sales", "Regulatory status: FDA observations or warning letters"],
|
|
61
|
+
"Energy": ["Refining margins (GRM) and crude realisations", "Production volumes",
|
|
62
|
+
"Marketing margins on fuel", "Capex plans and debt"],
|
|
63
|
+
"Industrials": ["Order inflow and order book (book-to-bill)", "Execution: revenue from the order book",
|
|
64
|
+
"Margins on fixed-price contracts", "Working capital and receivables"],
|
|
65
|
+
"Basic Materials": ["Realisation per tonne vs cost per tonne", "Sales volume and capacity use",
|
|
66
|
+
"Energy and raw-material costs", "Capex and net debt"],
|
|
67
|
+
"Communication Services": ["Average revenue per user (ARPU)", "Subscriber additions and churn",
|
|
68
|
+
"Capex intensity", "Advertising revenue (media)"],
|
|
69
|
+
"Utilities": ["Plant load factor / generation", "Regulated equity and tariff orders",
|
|
70
|
+
"Receivables from state utilities", "Renewable capacity additions"],
|
|
71
|
+
"Real Estate": ["Pre-sales / bookings and collections", "New launches", "Net debt", "Rental income (offices, malls)"],
|
|
72
|
+
}
|
|
73
|
+
COMMON = ["Revenue vs consensus", "EPS vs consensus", "Operating / EBITDA margin vs last year",
|
|
74
|
+
"Guidance or management commentary for the next quarter"]
|
|
75
|
+
|
|
76
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
77
|
+
_lock = threading.Lock()
|
|
78
|
+
|
|
79
|
+
Fetch = Callable[[str], dict]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _yahoo(yf_symbol: str) -> dict:
|
|
83
|
+
"""Raw inputs: earnings dates table, 2 years of daily closes, sector."""
|
|
84
|
+
import yfinance as yf
|
|
85
|
+
|
|
86
|
+
t = yf.Ticker(yf_symbol)
|
|
87
|
+
out: dict = {}
|
|
88
|
+
try:
|
|
89
|
+
out["dates"] = t.get_earnings_dates(limit=16)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
logger.info("Earnings dates for %s unavailable: %s", yf_symbol, exc)
|
|
92
|
+
out["dates"] = None
|
|
93
|
+
try:
|
|
94
|
+
hist = t.history(period="3y", interval="1d", auto_adjust=True)
|
|
95
|
+
out["closes"] = hist["Close"] if not hist.empty else None
|
|
96
|
+
except Exception:
|
|
97
|
+
out["closes"] = None
|
|
98
|
+
try:
|
|
99
|
+
info = t.info or {}
|
|
100
|
+
out["sector"], out["industry"] = info.get("sector"), info.get("industry")
|
|
101
|
+
out["currency"] = info.get("currency")
|
|
102
|
+
except Exception:
|
|
103
|
+
out["sector"] = out["industry"] = out["currency"] = None
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def events(dates: Optional[pd.DataFrame]) -> list[dict]:
|
|
108
|
+
"""Yahoo's earnings table -> [{at (UTC datetime), estimate, actual, surprise}], newest first."""
|
|
109
|
+
if dates is None or dates.empty:
|
|
110
|
+
return []
|
|
111
|
+
out = []
|
|
112
|
+
for at, row in dates.iterrows():
|
|
113
|
+
ts = pd.Timestamp(at)
|
|
114
|
+
ts = ts.tz_localize("America/New_York") if ts.tzinfo is None else ts
|
|
115
|
+
out.append({"at": ts.tz_convert("UTC").to_pydatetime(),
|
|
116
|
+
"estimate": finite(row.get("EPS Estimate")), "actual": finite(row.get("Reported EPS")),
|
|
117
|
+
"surprise": finite(row.get("Surprise(%)"))})
|
|
118
|
+
out.sort(key=lambda e: e["at"], reverse=True)
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _close_times(closes: pd.Series, market: str) -> pd.Series:
|
|
123
|
+
"""Daily closes re-indexed by the moment each session closed (UTC)."""
|
|
124
|
+
hour, minute, tz = CLOSE.get(market, CLOSE["us"])
|
|
125
|
+
days = pd.DatetimeIndex(closes.index)
|
|
126
|
+
days = days.tz_localize(None) if days.tz is not None else days
|
|
127
|
+
stamps = [datetime(d.year, d.month, d.day, hour, minute, tzinfo=ZoneInfo(tz)).astimezone(timezone.utc) for d in days]
|
|
128
|
+
return pd.Series(closes.values, index=pd.DatetimeIndex(stamps)).dropna()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def reaction(at: datetime, closes_utc: pd.Series) -> Optional[dict]:
|
|
132
|
+
"""Move from the last close before the announcement to the first close after it."""
|
|
133
|
+
before = closes_utc[closes_utc.index <= at]
|
|
134
|
+
after = closes_utc[closes_utc.index > at]
|
|
135
|
+
if before.empty or after.empty or (after.index[0] - at) > timedelta(days=6):
|
|
136
|
+
return None
|
|
137
|
+
return {"move": float(after.iloc[0] / before.iloc[-1] - 1) * 100,
|
|
138
|
+
"from": str(before.index[-1].date()), "to": str(after.index[0].date())}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def mark_unusual(evs: list[dict]) -> None:
|
|
142
|
+
"""Flag events whose estimate or actual is off the scale of the others."""
|
|
143
|
+
sizes = sorted(abs(e["estimate"]) for e in evs if e.get("estimate"))
|
|
144
|
+
usual = sizes[len(sizes) // 2] if sizes else None
|
|
145
|
+
for e in evs:
|
|
146
|
+
e["unusual"] = False
|
|
147
|
+
if not usual or usual < 0.05: # tiny or loss-making EPS: ratios mean nothing
|
|
148
|
+
continue
|
|
149
|
+
for v in (e.get("estimate"), e.get("actual")):
|
|
150
|
+
if v and (abs(v) > usual * OFF_SCALE or abs(v) < usual / OFF_SCALE):
|
|
151
|
+
e["unusual"] = True
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def unusual(e: dict) -> bool:
|
|
155
|
+
return bool(e.get("unusual"))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def history_stats(past: list[dict]) -> Optional[dict]:
|
|
159
|
+
moves = [e["reaction"]["move"] for e in past if e.get("reaction")]
|
|
160
|
+
if not moves:
|
|
161
|
+
return None
|
|
162
|
+
beats = [e for e in past if e.get("surprise") is not None and not unusual(e)]
|
|
163
|
+
on_beat = [e["reaction"]["move"] for e in beats if e["surprise"] > 0 and e.get("reaction")]
|
|
164
|
+
on_miss = [e["reaction"]["move"] for e in beats if e["surprise"] < 0 and e.get("reaction")]
|
|
165
|
+
avg = lambda v: sum(v) / len(v) if v else None # noqa: E731
|
|
166
|
+
return {"reports": len(moves), "avg_abs_move": avg([abs(m) for m in moves]),
|
|
167
|
+
"biggest_up": max(moves), "biggest_down": min(moves), "up_days": sum(1 for m in moves if m > 0),
|
|
168
|
+
"beats": sum(1 for e in beats if e["surprise"] > 0), "misses": sum(1 for e in beats if e["surprise"] < 0),
|
|
169
|
+
"avg_move_on_beat": avg(on_beat), "avg_move_on_miss": avg(on_miss)}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def implied_move(symbol: str, market: str, report_day: datetime) -> Optional[dict]:
|
|
173
|
+
"""At-the-money straddle on the first expiry on / after the report, as a % of spot."""
|
|
174
|
+
from ..trading import options
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
exps = options.expiries(symbol, market)
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
logger.info("No option expiries for %s: %s", symbol, exc)
|
|
180
|
+
return None
|
|
181
|
+
day = report_day.date()
|
|
182
|
+
after = sorted((options._parse_expiry(e), e) for e in exps if options._parse_expiry(e) >= day)
|
|
183
|
+
if not after or (after[0][0] - day).days > 35:
|
|
184
|
+
return None
|
|
185
|
+
try:
|
|
186
|
+
ch = options.chain(symbol, market, after[0][1])
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
logger.info("Option chain for %s unavailable: %s", symbol, exc)
|
|
189
|
+
return None
|
|
190
|
+
spot, rows = (ch.get("summary") or {}).get("spot") or 0, ch.get("rows") or []
|
|
191
|
+
if not spot or not rows:
|
|
192
|
+
return None
|
|
193
|
+
atm = min(rows, key=lambda r: abs(r["strike"] - spot))
|
|
194
|
+
|
|
195
|
+
def mid(leg: Optional[dict]) -> Optional[float]:
|
|
196
|
+
if not leg:
|
|
197
|
+
return None
|
|
198
|
+
bid, ask = finite(leg.get("bid")), finite(leg.get("ask"))
|
|
199
|
+
if bid and ask and ask >= bid:
|
|
200
|
+
return (bid + ask) / 2
|
|
201
|
+
return finite(leg.get("ltp"))
|
|
202
|
+
|
|
203
|
+
call, put = mid(atm.get("call")), mid(atm.get("put"))
|
|
204
|
+
if not call or not put:
|
|
205
|
+
return None
|
|
206
|
+
return {"move": (call + put) / spot * 100, "expiry": after[0][1], "strike": atm["strike"]}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def watch_for(sector: Optional[str]) -> list[str]:
|
|
210
|
+
return COMMON + WATCH.get(sector or "", [])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _estimates(yf_symbol: str) -> Optional[dict]:
|
|
214
|
+
from . import estimates
|
|
215
|
+
|
|
216
|
+
return estimates.build(yf_symbol)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build(symbol: str, market: str, fetch: Fetch = _yahoo, with_options: bool = True,
|
|
220
|
+
now: Optional[datetime] = None, use_cache: bool = True,
|
|
221
|
+
estimates_fetch: Callable[[str], Optional[dict]] = _estimates) -> dict:
|
|
222
|
+
from .analysis_run import yahoo_symbol
|
|
223
|
+
|
|
224
|
+
key = f"{symbol}:{market}"
|
|
225
|
+
if use_cache:
|
|
226
|
+
with _lock:
|
|
227
|
+
hit = _cache.get(key)
|
|
228
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
229
|
+
return hit[1]
|
|
230
|
+
now = now or datetime.now(timezone.utc)
|
|
231
|
+
raw = fetch(yahoo_symbol(symbol, market))
|
|
232
|
+
evs = events(raw.get("dates"))
|
|
233
|
+
mark_unusual(evs)
|
|
234
|
+
closes = raw.get("closes")
|
|
235
|
+
closes_utc = _close_times(closes, market) if closes is not None and len(closes) else None
|
|
236
|
+
|
|
237
|
+
upcoming = [e for e in evs if e["actual"] is None and e["at"] > now - timedelta(days=1)]
|
|
238
|
+
past = [e for e in evs if e["actual"] is not None]
|
|
239
|
+
for e in past:
|
|
240
|
+
e["reaction"] = reaction(e["at"], closes_utc) if closes_utc is not None else None
|
|
241
|
+
nxt = upcoming[-1] if upcoming else None # the soonest still-unreported date
|
|
242
|
+
next_out = _event_out(nxt, market) if nxt else None
|
|
243
|
+
|
|
244
|
+
latest = None
|
|
245
|
+
if past and (now - past[0]["at"]).days <= RECENT_DAYS:
|
|
246
|
+
latest = _event_out(past[0], market)
|
|
247
|
+
estimates = None
|
|
248
|
+
try:
|
|
249
|
+
estimates = estimates_fetch(yahoo_symbol(symbol, market))
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
logger.info("Estimates for %s unavailable: %s", symbol, exc)
|
|
252
|
+
|
|
253
|
+
out = {
|
|
254
|
+
"symbol": symbol, "market": market, "currency": raw.get("currency"),
|
|
255
|
+
"sector": raw.get("sector"), "industry": raw.get("industry"),
|
|
256
|
+
"next": next_out,
|
|
257
|
+
"days_to_next": (date.fromisoformat(next_out["date"]) - now.date()).days if next_out else None,
|
|
258
|
+
"implied": implied_move(symbol, market, nxt["at"]) if nxt and with_options and market in ("us", "india") else None,
|
|
259
|
+
"latest": latest,
|
|
260
|
+
"history": [_event_out(e, market) for e in past[:12]],
|
|
261
|
+
"stats": history_stats(past[:12]),
|
|
262
|
+
"consensus": _consensus(estimates),
|
|
263
|
+
"watch": watch_for(raw.get("sector")),
|
|
264
|
+
"available": bool(evs),
|
|
265
|
+
"source": "Market data feed (earnings dates, EPS estimates and actuals, prices)",
|
|
266
|
+
}
|
|
267
|
+
if use_cache:
|
|
268
|
+
with _lock:
|
|
269
|
+
_cache[key] = (time.time(), out)
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _event_out(e: dict, market: str) -> dict:
|
|
274
|
+
"""`date` is the day on the exchange's own calendar (a 4 pm New York report is that day,
|
|
275
|
+
even though it is already the next morning in India)."""
|
|
276
|
+
tz = ZoneInfo(CLOSE.get(market, CLOSE["us"])[2])
|
|
277
|
+
return {"date": e["at"].astimezone(tz).strftime("%Y-%m-%d"),
|
|
278
|
+
"at": e["at"].astimezone(timezone.utc).isoformat(),
|
|
279
|
+
"estimate": e.get("estimate"), "actual": e.get("actual"), "surprise": e.get("surprise"),
|
|
280
|
+
"reaction": e.get("reaction"), "unusual": unusual(e)}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _consensus(est: Optional[dict]) -> Optional[dict]:
|
|
284
|
+
"""The current-quarter consensus and how it moved, from services/estimates."""
|
|
285
|
+
if not est or not est.get("periods"):
|
|
286
|
+
return None
|
|
287
|
+
q = next((p for p in est["periods"] if p["key"] == "0q"), est["periods"][0])
|
|
288
|
+
return {"period": q["label"], "eps": q["eps"], "revenue": q["revenue"], "trend": q["trend"],
|
|
289
|
+
"revisions": q["revisions"], "momentum": est.get("momentum")}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def results_line(symbol: str, data: dict) -> Optional[str]:
|
|
293
|
+
"""One message line once results are out: actual vs estimate and the move."""
|
|
294
|
+
e = data.get("latest")
|
|
295
|
+
if not e or e.get("actual") is None:
|
|
296
|
+
return None
|
|
297
|
+
parts = [f"{symbol} results: EPS {e['actual']:.2f}"]
|
|
298
|
+
if e.get("estimate") is not None:
|
|
299
|
+
parts.append(f"vs {e['estimate']:.2f} expected")
|
|
300
|
+
if e.get("surprise") is not None:
|
|
301
|
+
parts.append(f"({'beat' if e['surprise'] > 0 else 'missed' if e['surprise'] < 0 else 'in line'} by {abs(e['surprise']):.1f}%)")
|
|
302
|
+
line = " ".join(parts)
|
|
303
|
+
if e.get("reaction"):
|
|
304
|
+
line += f"; stock {e['reaction']['move']:+.1f}% on the day"
|
|
305
|
+
return line
|