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,370 @@
|
|
|
1
|
+
"""Smart messaging for big watchlists (100+ stocks).
|
|
2
|
+
|
|
3
|
+
Instead of a paragraph per stock, a run produces:
|
|
4
|
+
- a full table of every monitored stock (saved, opened through a link and
|
|
5
|
+
emailed as HTML), and
|
|
6
|
+
- a short message with only the day's top N "hot" stocks.
|
|
7
|
+
|
|
8
|
+
Every stock gets cheap, computed metrics (no AI):
|
|
9
|
+
normal range 20-day Bollinger band (where price usually trades)
|
|
10
|
+
buy point support: the higher of the lower band and the 20-day low
|
|
11
|
+
sell point resistance: the lower of the upper band and the 60-day high
|
|
12
|
+
(upper band if price is already above it)
|
|
13
|
+
stop loss buy point minus one average true range
|
|
14
|
+
1-week range price +/- one weekly standard deviation (~68% of weeks)
|
|
15
|
+
valuation P/E vs its industry average (fundamental analyzer)
|
|
16
|
+
action a score from trend, RSI, levels and valuation
|
|
17
|
+
|
|
18
|
+
The points, stop and action are working values only: without_calls()
|
|
19
|
+
turns the action into a bullish / neutral / bearish reading and drops the
|
|
20
|
+
rest before anything is saved, sent or shown (marketmyna/core/compliance.py).
|
|
21
|
+
|
|
22
|
+
Only the `ai_top` most "attention-worthy" stocks also get the full AI
|
|
23
|
+
pipeline, so a 100-stock list doesn't burn 400 LLM calls a day.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import html as html_lib
|
|
29
|
+
import logging
|
|
30
|
+
import math
|
|
31
|
+
import secrets
|
|
32
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
from typing import Optional
|
|
35
|
+
from zoneinfo import ZoneInfo
|
|
36
|
+
|
|
37
|
+
from ..core.numbers import finite as _num
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
WORKERS = 6
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compute_snapshot(history, info: Optional[dict] = None) -> dict:
|
|
45
|
+
"""Levels, forecast range and rule-based action from price history
|
|
46
|
+
(and optional yfinance info for valuation). Pure: no network."""
|
|
47
|
+
close = history["close"].astype(float)
|
|
48
|
+
high = history["high"].astype(float) if "high" in history else close
|
|
49
|
+
low = history["low"].astype(float) if "low" in history else close
|
|
50
|
+
if len(close) < 30:
|
|
51
|
+
raise ValueError("not enough history")
|
|
52
|
+
|
|
53
|
+
price = float(close.iloc[-1])
|
|
54
|
+
prev = float(close.iloc[-2])
|
|
55
|
+
change_pct = (price / prev - 1) * 100 if prev else None
|
|
56
|
+
|
|
57
|
+
mid = float(close.tail(20).mean())
|
|
58
|
+
sd = float(close.tail(20).std())
|
|
59
|
+
band_low, band_high = mid - 2 * sd, mid + 2 * sd
|
|
60
|
+
low20 = float(low.tail(20).min())
|
|
61
|
+
high60 = float(high.tail(60).max())
|
|
62
|
+
|
|
63
|
+
prev_close = close.shift(1)
|
|
64
|
+
true_range = (high - low).combine((high - prev_close).abs(), max).combine((low - prev_close).abs(), max)
|
|
65
|
+
atr = float(true_range.tail(14).mean())
|
|
66
|
+
|
|
67
|
+
buy_point = max(band_low, low20)
|
|
68
|
+
sell_point = min(band_high, high60) if min(band_high, high60) > price else band_high
|
|
69
|
+
stop_loss = buy_point - atr
|
|
70
|
+
|
|
71
|
+
daily = close.pct_change().dropna()
|
|
72
|
+
week_sd = float(daily.tail(60).std()) * math.sqrt(5)
|
|
73
|
+
# Risk over the history given (a year): annualised volatility and the deepest fall from a high.
|
|
74
|
+
volatility = float(daily.std()) * math.sqrt(252) if len(daily) > 1 else None
|
|
75
|
+
max_drawdown = float((close / close.cummax() - 1).min())
|
|
76
|
+
week_low, week_high = price * (1 - week_sd), price * (1 + week_sd)
|
|
77
|
+
|
|
78
|
+
delta = close.diff()
|
|
79
|
+
gain = delta.clip(lower=0).tail(14).mean()
|
|
80
|
+
loss = (-delta.clip(upper=0)).tail(14).mean()
|
|
81
|
+
rsi = 100.0 if loss == 0 else float(100 - 100 / (1 + gain / loss))
|
|
82
|
+
|
|
83
|
+
sma50 = float(close.tail(50).mean())
|
|
84
|
+
sma200 = float(close.tail(200).mean()) if len(close) >= 200 else None
|
|
85
|
+
if sma200 and price > sma50 > sma200:
|
|
86
|
+
trend = "uptrend"
|
|
87
|
+
elif sma200 and price < sma50 < sma200:
|
|
88
|
+
trend = "downtrend"
|
|
89
|
+
else:
|
|
90
|
+
trend = "sideways"
|
|
91
|
+
|
|
92
|
+
valuation, pe = None, None
|
|
93
|
+
if info:
|
|
94
|
+
try:
|
|
95
|
+
from ..analysis.fundamental import FundamentalAnalyzer, yfinance_fundamentals
|
|
96
|
+
|
|
97
|
+
summary = FundamentalAnalyzer(yfinance_fundamentals(info)).get_summary()
|
|
98
|
+
status = (summary.get("valuation") or {}).get("status")
|
|
99
|
+
valuation = status if status and status != "N/A" else None
|
|
100
|
+
pe = _num((summary.get("valuation") or {}).get("value")) or None
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
score = {"uptrend": 1, "downtrend": -1}.get(trend, 0)
|
|
105
|
+
score += 1 if rsi < 35 else -1 if rsi > 70 else 0
|
|
106
|
+
near_buy = price <= buy_point * 1.02
|
|
107
|
+
near_sell = price >= sell_point * 0.98
|
|
108
|
+
score += 1 if near_buy else 0
|
|
109
|
+
score -= 1 if near_sell else 0
|
|
110
|
+
score += {"undervalued": 1, "overvalued": -1}.get(valuation or "", 0)
|
|
111
|
+
action = "buy" if score >= 2 else "sell" if score <= -2 else "hold"
|
|
112
|
+
|
|
113
|
+
volume = history["volume"].astype(float) if "volume" in history else None
|
|
114
|
+
vol_ratio = None
|
|
115
|
+
if volume is not None and len(volume) > 21 and float(volume.tail(21).head(20).mean()) > 0:
|
|
116
|
+
vol_ratio = float(volume.iloc[-1] / volume.tail(21).head(20).mean())
|
|
117
|
+
|
|
118
|
+
# Why this stock deserves attention today (ranking for the top N).
|
|
119
|
+
reasons, attention = [], 0.0
|
|
120
|
+
if change_pct is not None and abs(change_pct) >= 2:
|
|
121
|
+
reasons.append(f"moved {change_pct:+.1f}% today")
|
|
122
|
+
attention += abs(change_pct or 0)
|
|
123
|
+
if action != "hold":
|
|
124
|
+
attention += 2
|
|
125
|
+
if near_buy:
|
|
126
|
+
attention += 1.5
|
|
127
|
+
reasons.append("at its buy point")
|
|
128
|
+
elif near_sell:
|
|
129
|
+
attention += 1.5
|
|
130
|
+
reasons.append("at its sell point")
|
|
131
|
+
if rsi < 30 or rsi > 70:
|
|
132
|
+
attention += 1
|
|
133
|
+
reasons.append("oversold" if rsi < 30 else "overbought")
|
|
134
|
+
if vol_ratio and vol_ratio >= 1.5:
|
|
135
|
+
attention += 1
|
|
136
|
+
reasons.append(f"volume {vol_ratio:.1f}x normal")
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"price": price, "change_percent": change_pct,
|
|
140
|
+
"normal_low": band_low, "normal_high": band_high,
|
|
141
|
+
"buy_point": buy_point, "sell_point": sell_point, "stop_loss": stop_loss,
|
|
142
|
+
"week_low": week_low, "week_high": week_high,
|
|
143
|
+
"rsi": rsi, "trend": trend, "valuation": valuation, "pe": pe,
|
|
144
|
+
"atr": atr, "volume_ratio": vol_ratio,
|
|
145
|
+
"volatility": volatility, "max_drawdown": max_drawdown,
|
|
146
|
+
"high_52w": float(high.max()), "low_52w": float(low.min()),
|
|
147
|
+
"action": action, "attention": round(attention, 2), "reasons": reasons,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def snapshot(symbol: str, market: str) -> dict:
|
|
152
|
+
"""Network wrapper: history (fallback chains) + yfinance info."""
|
|
153
|
+
from ..agent.agents import _resolve_fetcher
|
|
154
|
+
from .analysis_run import yahoo_symbol
|
|
155
|
+
|
|
156
|
+
row = {"symbol": symbol, "market": market}
|
|
157
|
+
try:
|
|
158
|
+
history = _resolve_fetcher(market).get_history(symbol, period="1y")
|
|
159
|
+
info = {}
|
|
160
|
+
if market != "crypto":
|
|
161
|
+
try:
|
|
162
|
+
import yfinance as yf
|
|
163
|
+
|
|
164
|
+
from ..utils.deadline import call_with_deadline
|
|
165
|
+
|
|
166
|
+
info = call_with_deadline(lambda: yf.Ticker(yahoo_symbol(symbol, market)).info, 15) or {}
|
|
167
|
+
except Exception:
|
|
168
|
+
info = {}
|
|
169
|
+
row.update(compute_snapshot(history, info))
|
|
170
|
+
row["name"] = info.get("shortName") or info.get("longName")
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
row["error"] = str(exc) or type(exc).__name__
|
|
173
|
+
return row
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_rows(targets, config, ai_top: int, analyze=None, snap=snapshot, mode: str = "standard") -> list[dict]:
|
|
177
|
+
"""Snapshot every stock (parallel), rank by attention, then run the AI
|
|
178
|
+
pipeline (at the schedule's analysis depth) on the top `ai_top` only."""
|
|
179
|
+
from .analysis_run import analyze_symbol
|
|
180
|
+
|
|
181
|
+
analyze = analyze or analyze_symbol
|
|
182
|
+
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
|
|
183
|
+
rows = list(pool.map(lambda t: snap(t.symbol, t.market), targets))
|
|
184
|
+
rows.sort(key=lambda r: r.get("attention", -1), reverse=True)
|
|
185
|
+
|
|
186
|
+
for row in [r for r in rows if "error" not in r][: max(0, ai_top)]:
|
|
187
|
+
result = analyze(row["symbol"], row["market"], mode, config)
|
|
188
|
+
dash = result.get("dashboard") or {}
|
|
189
|
+
# The AI's outlook replaces the computed reading; none, and the computed one stands.
|
|
190
|
+
if dash.get("outlook"):
|
|
191
|
+
row["action"] = {"bullish": "buy", "bearish": "sell"}.get(dash["outlook"], "hold")
|
|
192
|
+
row["ai"] = True
|
|
193
|
+
row["ai_summary"] = dash.get("analysis_summary")
|
|
194
|
+
return rows
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# A report shows what the indicators read, never a call (marketmyna/core/compliance.py).
|
|
198
|
+
READINGS = {"strong_buy": "positive", "buy": "positive", "hold": "mixed", "sell": "negative", "strong_sell": "negative"}
|
|
199
|
+
READING_LABELS = {"positive": "Bullish", "mixed": "Neutral", "negative": "Bearish"}
|
|
200
|
+
READING_ICONS = {"positive": "▲", "mixed": "◆", "negative": "▼"}
|
|
201
|
+
_NEUTRAL_REASONS = {"at its buy point": "near its support level", "at its sell point": "near its resistance level",
|
|
202
|
+
"oversold": "RSI below 30", "overbought": "RSI above 70"}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def without_calls(row: dict) -> dict:
|
|
206
|
+
"""The row with every call, level and stop removed and the computed
|
|
207
|
+
action turned into a reading of the indicators (positive / mixed /
|
|
208
|
+
negative). Verdict words (undervalued, at its buy point) go too."""
|
|
209
|
+
from ..core.compliance import CALL_FIELDS
|
|
210
|
+
|
|
211
|
+
if "error" in row:
|
|
212
|
+
return row
|
|
213
|
+
out = {k: v for k, v in row.items() if k not in CALL_FIELDS}
|
|
214
|
+
out["reading"] = READINGS.get(str(row.get("action") or "hold").lower(), "mixed")
|
|
215
|
+
out["reasons"] = [_NEUTRAL_REASONS.get(r, r) for r in row.get("reasons") or []]
|
|
216
|
+
out["valuation"] = None # "undervalued" / "overvalued" is a verdict; the P/E stays
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def reading_label(row: dict) -> str:
|
|
221
|
+
reading = row.get("reading") or "mixed"
|
|
222
|
+
return f"{READING_ICONS[reading]} {READING_LABELS[reading]}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def save_report(config, rows: list[dict], title: str, meta: dict) -> str:
|
|
226
|
+
from ..storage import DatabaseManager, ReportRunRecord, get_db_url
|
|
227
|
+
|
|
228
|
+
token = secrets.token_urlsafe(16)
|
|
229
|
+
db = DatabaseManager.instance(get_db_url(config=config))
|
|
230
|
+
with db.get_session() as session:
|
|
231
|
+
session.add(ReportRunRecord(token=token, title=title, stock_count=len(rows), rows=rows, meta=meta))
|
|
232
|
+
return token
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------- rendering
|
|
236
|
+
|
|
237
|
+
def _f(value, digits: int = 2) -> str:
|
|
238
|
+
v = _num(value)
|
|
239
|
+
if v is None:
|
|
240
|
+
return "—"
|
|
241
|
+
return f"{v:,.{digits}f}" if abs(v) >= 1 else f"{v:.4f}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def smart_message(rows: list[dict], prefs, lang: str, link: Optional[str], now: datetime) -> tuple[str, str]:
|
|
245
|
+
"""(title, body): only the top N, one line each, then the link."""
|
|
246
|
+
from .report import CURRENCY_SYMBOLS
|
|
247
|
+
from .report_i18n import has_builtin, t
|
|
248
|
+
|
|
249
|
+
lang_labels = lang if has_builtin(lang) else "en"
|
|
250
|
+
good = [r for r in rows if "error" not in r]
|
|
251
|
+
top = good[: prefs.smart_top_n]
|
|
252
|
+
lines = [
|
|
253
|
+
f"📊 {t(lang_labels, 'smart_top', n=len(top))} · {now:%d %b %Y}",
|
|
254
|
+
"",
|
|
255
|
+
]
|
|
256
|
+
for r in top:
|
|
257
|
+
cur = CURRENCY_SYMBOLS.get(r["market"], "")
|
|
258
|
+
reading = r.get("reading") or "mixed"
|
|
259
|
+
change = f" ({r['change_percent']:+.1f}%)" if _num(r.get("change_percent")) is not None else ""
|
|
260
|
+
lines.append(f"{READING_ICONS[reading]} {r['symbol']} {cur}{_f(r['price'])}{change} · "
|
|
261
|
+
f"{t(lang_labels, 'indicators')}: {t(lang_labels, 'reading_' + reading)}")
|
|
262
|
+
lines.append(f" {t(lang_labels, 'normal')} {cur}{_f(r['normal_low'])}–{_f(r['normal_high'])} · "
|
|
263
|
+
f"{t(lang_labels, 'week')} {cur}{_f(r['week_low'])}–{_f(r['week_high'])}")
|
|
264
|
+
why = r.get("ai_summary") or ", ".join(r.get("reasons") or [])
|
|
265
|
+
if why:
|
|
266
|
+
lines.append(f" {why[:140]}{'…' if len(why) > 140 else ''}")
|
|
267
|
+
lines.append("")
|
|
268
|
+
lines.append(f"📄 {t(lang_labels, 'full_report', n=len(rows))}" + (f": {link}" if link else ""))
|
|
269
|
+
lines.append(t(lang_labels, "reading_note"))
|
|
270
|
+
lines.append(t(lang_labels, "disclaimer"))
|
|
271
|
+
return t(lang_labels, "title"), "\n".join(lines)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def html_report(rows: list[dict], title: str, now: datetime, link: Optional[str]) -> str:
|
|
275
|
+
"""Self-contained HTML table of every stock (for email and the artifact):
|
|
276
|
+
the indicators' reading and the ranges, no call, level or stop, with a
|
|
277
|
+
note on how to read it."""
|
|
278
|
+
from .report import CURRENCY_SYMBOLS
|
|
279
|
+
|
|
280
|
+
esc = html_lib.escape
|
|
281
|
+
colors = {"positive": "#0b7a52", "mixed": "#8f5b00", "negative": "#c02f2f"}
|
|
282
|
+
head = ["Stock", "Price", "Day", "Indicators", "Normal range", "1-week range", "Trend", "RSI", "P/E", "What stands out"]
|
|
283
|
+
body = []
|
|
284
|
+
for r in rows:
|
|
285
|
+
if "error" in r:
|
|
286
|
+
body.append(f"<tr><td>{esc(r['symbol'])}</td><td colspan='9' style='color:#888'>no data: {esc(r['error'][:80])}</td></tr>")
|
|
287
|
+
continue
|
|
288
|
+
cur = CURRENCY_SYMBOLS.get(r["market"], "")
|
|
289
|
+
chg = _num(r.get("change_percent"))
|
|
290
|
+
cells = [
|
|
291
|
+
f"<b>{esc(r['symbol'])}</b><br><span style='color:#666;font-size:11px'>{esc(r.get('name') or '')}</span>",
|
|
292
|
+
f"{cur}{_f(r['price'])}",
|
|
293
|
+
f"<span style='color:{'#0b7a52' if (chg or 0) >= 0 else '#c02f2f'}'>{'' if chg is None else f'{chg:+.1f}%'}</span>",
|
|
294
|
+
f"<b style='color:{colors[r.get('reading') or 'mixed']}'>{esc(reading_label(r))}</b>",
|
|
295
|
+
f"{_f(r['normal_low'])}–{_f(r['normal_high'])}",
|
|
296
|
+
f"{_f(r['week_low'])}–{_f(r['week_high'])}",
|
|
297
|
+
esc(r["trend"]), f"{r['rsi']:.0f}", _f(r.get("pe"), 1),
|
|
298
|
+
esc((r.get("ai_summary") or ", ".join(r.get("reasons") or []))[:200]),
|
|
299
|
+
]
|
|
300
|
+
body.append("<tr>" + "".join(f"<td>{c}</td>" for c in cells) + "</tr>")
|
|
301
|
+
link_html = f"<p><a href='{esc(link)}'>Open the interactive report</a></p>" if link else ""
|
|
302
|
+
return f"""<!doctype html><html><body style="font-family:Segoe UI,Arial,sans-serif;color:#0f1522">
|
|
303
|
+
<h2 style="margin:0">{esc(title)}</h2>
|
|
304
|
+
<p style="color:#555;margin:4px 0 12px">{now:%d %b %Y, %H:%M %Z} · {len(rows)} stocks</p>
|
|
305
|
+
<p style="background:#eef3fb;border-left:4px solid #1f3a8a;padding:8px 12px;font-size:12px">{esc(READING_NOTE)}</p>
|
|
306
|
+
{link_html}
|
|
307
|
+
<table cellpadding="6" style="border-collapse:collapse;font-size:12px">
|
|
308
|
+
<thead><tr style="background:#eceff4">{''.join(f'<th align="left">{h}</th>' for h in head)}</tr></thead>
|
|
309
|
+
<tbody>{''.join(body)}</tbody></table>
|
|
310
|
+
<p style="color:#777;font-size:11px">Indicators = how trend, momentum (RSI), where the price sits in its range and
|
|
311
|
+
valuation lean today: bullish, neutral or bearish. Normal range = 20-day Bollinger band. 1-week range ≈
|
|
312
|
+
where ~2 of 3 weeks end, from recent volatility. Computed from market data.</p></body></html>"""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Shown on every general user's report: what the reading is and is not.
|
|
316
|
+
READING_NOTE = ("This report describes what the market data shows. It is not a recommendation to buy, sell or "
|
|
317
|
+
"hold any stock. \"Bullish\" or \"bearish\" means most of the indicators we "
|
|
318
|
+
"track lean that way today; they can change quickly and are often wrong. Do your own research or "
|
|
319
|
+
"speak to a SEBI-registered adviser before you invest.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def run_smart(targets, prefs, config, dispatcher=None, dry_run: bool = False, analyze=None, snap=snapshot):
|
|
323
|
+
"""Full smart run: rows -> saved report -> short messages (+ full HTML
|
|
324
|
+
email). Returns (reports by language, delivery, rows, token)."""
|
|
325
|
+
from ..notifiers.dispatcher import NotificationDispatcher
|
|
326
|
+
from .report import translate_report
|
|
327
|
+
from .report_i18n import normalize_language
|
|
328
|
+
|
|
329
|
+
rows = build_rows(targets, config, prefs.smart_ai_top, analyze=analyze, snap=snap,
|
|
330
|
+
mode=getattr(prefs, "mode", None) or "standard")
|
|
331
|
+
rows = [without_calls(r) for r in rows]
|
|
332
|
+
now = datetime.now(ZoneInfo(prefs.timezone))
|
|
333
|
+
title = f"MarketMyna · {len(rows)} stocks · {now:%d %b %Y}"
|
|
334
|
+
token = None
|
|
335
|
+
try:
|
|
336
|
+
token = save_report(config, rows, title, {"markets": sorted({t.market for t in targets}),
|
|
337
|
+
"compliance": "analytics"})
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
logger.warning("Could not save full report: %s", exc)
|
|
340
|
+
link = f"{prefs.report_base_url.rstrip('/')}/r/{token}" if prefs.report_base_url and token else None
|
|
341
|
+
|
|
342
|
+
dispatcher = dispatcher or NotificationDispatcher(config)
|
|
343
|
+
channels = dispatcher.configured_channels("report")
|
|
344
|
+
by_lang = prefs.languages_needed(channels)
|
|
345
|
+
languages = list(dict.fromkeys([normalize_language(prefs.language), *by_lang]))
|
|
346
|
+
|
|
347
|
+
reports = {}
|
|
348
|
+
for lang in languages:
|
|
349
|
+
msg_title, body = smart_message(rows, prefs, lang, link, now)
|
|
350
|
+
if lang != "en":
|
|
351
|
+
body = translate_report(body, lang, config) or body
|
|
352
|
+
reports[lang] = (msg_title, body)
|
|
353
|
+
|
|
354
|
+
full_html = html_report(rows, title, now, link) if prefs.smart_email_full else None
|
|
355
|
+
if dry_run or not channels:
|
|
356
|
+
return reports, {}, rows, token, full_html
|
|
357
|
+
|
|
358
|
+
# The report file (PDF or Excel, from the ticked sections) to the channels the user picked.
|
|
359
|
+
from .smart_file import report_file
|
|
360
|
+
|
|
361
|
+
wanted = [c for c in (getattr(prefs, "smart_file_channels", None) or []) if c in ("email", "telegram")]
|
|
362
|
+
report = report_file(rows, prefs, config, now, title) if wanted else None
|
|
363
|
+
files = {c: [report] for c in wanted} if report else {}
|
|
364
|
+
|
|
365
|
+
messages = {}
|
|
366
|
+
for lang, chs in by_lang.items():
|
|
367
|
+
for ch in chs:
|
|
368
|
+
msg_title, body = reports[lang]
|
|
369
|
+
messages[ch] = (msg_title, body, full_html) if ch == "email" and full_html else (msg_title, body)
|
|
370
|
+
return reports, dispatcher.send_each(messages, purpose="report", files=files), rows, token, full_html
|