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,564 @@
|
|
|
1
|
+
"""Market-wide listing endpoints: browse/search every stock, ETF and mutual
|
|
2
|
+
fund per market (screener-style), backed by marketmyna/data/listings.py."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from marketmyna.api.deps import get_config_dep
|
|
12
|
+
from marketmyna.data.listings import MARKET_LABELS, get_store, query
|
|
13
|
+
from marketmyna.data.mutual_funds import MutualFundDataFetcher
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
_MARKETS = set(MARKET_LABELS) | {"all", "global"}
|
|
18
|
+
_TYPES = {"all", "stock", "etf", "mf", "crypto"}
|
|
19
|
+
|
|
20
|
+
_mf = MutualFundDataFetcher()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _market(market: str) -> str:
|
|
24
|
+
market = market.lower()
|
|
25
|
+
if market not in _MARKETS:
|
|
26
|
+
raise HTTPException(status_code=400, detail=f"Unknown market '{market}'")
|
|
27
|
+
return market
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _csv(value: str) -> list[str]:
|
|
31
|
+
return [v.strip() for v in value.split(",") if v.strip()]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get("/listings")
|
|
35
|
+
def list_instruments(
|
|
36
|
+
market: str = Query(default="india"),
|
|
37
|
+
type: str = Query(default="all", description="all | stock | etf | mf | crypto"),
|
|
38
|
+
q: str = Query(default="", max_length=100),
|
|
39
|
+
sector: str = Query(default=""),
|
|
40
|
+
exchange: str = Query(default=""),
|
|
41
|
+
plan: str = Query(default="", description="Mutual funds: Direct | Regular"),
|
|
42
|
+
option: str = Query(default="", description="Mutual funds: Growth | IDCW"),
|
|
43
|
+
min_cap: Optional[float] = Query(default=None),
|
|
44
|
+
max_cap: Optional[float] = Query(default=None),
|
|
45
|
+
category: str = Query(default="", description="Comma-separated broad categories, e.g. Technology,Agriculture"),
|
|
46
|
+
cap: str = Query(default="", description="Comma-separated size bands: large,mid,small,micro"),
|
|
47
|
+
shariah: str = Query(default="", description="Comma-separated Shariah screen statuses: halal,doubtful,haram,unknown"),
|
|
48
|
+
price_min: Optional[float] = Query(default=None, ge=0),
|
|
49
|
+
price_max: Optional[float] = Query(default=None, ge=0),
|
|
50
|
+
change_min: Optional[float] = Query(default=None, description="Day change %, lower bound"),
|
|
51
|
+
change_max: Optional[float] = Query(default=None, description="Day change %, upper bound"),
|
|
52
|
+
near_high: Optional[float] = Query(default=None, ge=0, le=100, description="Within N% of 52-week high"),
|
|
53
|
+
near_low: Optional[float] = Query(default=None, ge=0, le=100, description="Within N% of 52-week low"),
|
|
54
|
+
min_volume: Optional[float] = Query(default=None, ge=0),
|
|
55
|
+
max_volume: Optional[float] = Query(default=None, ge=0),
|
|
56
|
+
markets: str = Query(default="", description="All-markets view: comma-separated markets to keep"),
|
|
57
|
+
sip_edge: str = Query(default="", description="Funds / ETFs: consistent | no_edge | short (historically best SIP day)"),
|
|
58
|
+
sort: str = Query(default="market_cap"),
|
|
59
|
+
order: str = Query(default="desc", pattern="^(asc|desc)$"),
|
|
60
|
+
offset: int = Query(default=0, ge=0),
|
|
61
|
+
limit: int = Query(default=50, ge=1, le=500),
|
|
62
|
+
) -> dict:
|
|
63
|
+
market = _market(market)
|
|
64
|
+
if type not in _TYPES:
|
|
65
|
+
raise HTTPException(status_code=400, detail=f"Unknown type '{type}'")
|
|
66
|
+
store = get_store()
|
|
67
|
+
from marketmyna.services import sip_days
|
|
68
|
+
|
|
69
|
+
rows = store.rows(market)
|
|
70
|
+
sip_days.annotate(rows, market)
|
|
71
|
+
if sip_edge:
|
|
72
|
+
rows = [r for r in rows if r.get("sip_edge") in set(_csv(sip_edge))]
|
|
73
|
+
result = query(
|
|
74
|
+
rows,
|
|
75
|
+
q=q,
|
|
76
|
+
type_=type,
|
|
77
|
+
sector=sector,
|
|
78
|
+
exchange=exchange,
|
|
79
|
+
plan=plan,
|
|
80
|
+
option=option,
|
|
81
|
+
min_cap=min_cap,
|
|
82
|
+
max_cap=max_cap,
|
|
83
|
+
categories=_csv(category),
|
|
84
|
+
cap_sizes=_csv(cap),
|
|
85
|
+
shariah=_csv(shariah),
|
|
86
|
+
price_min=price_min,
|
|
87
|
+
price_max=price_max,
|
|
88
|
+
change_min=change_min,
|
|
89
|
+
change_max=change_max,
|
|
90
|
+
near_high=near_high,
|
|
91
|
+
near_low=near_low,
|
|
92
|
+
min_volume=min_volume,
|
|
93
|
+
max_volume=max_volume,
|
|
94
|
+
markets=_csv(markets),
|
|
95
|
+
sort=sort,
|
|
96
|
+
order=order,
|
|
97
|
+
offset=offset,
|
|
98
|
+
limit=limit,
|
|
99
|
+
)
|
|
100
|
+
result["sources"] = [s for s in store.status() if s["source"] in store.keys_for(market)]
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Typeahead answers with the markets loaded by then; slower ones join later searches.
|
|
105
|
+
SEARCH_WAIT = 8.0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.get("/listings/search")
|
|
109
|
+
def search_instruments(
|
|
110
|
+
q: str = Query(..., min_length=1, max_length=100),
|
|
111
|
+
market: str = Query(default="all"),
|
|
112
|
+
type: str = Query(default="all"),
|
|
113
|
+
limit: int = Query(default=10, ge=1, le=50),
|
|
114
|
+
) -> list[dict]:
|
|
115
|
+
"""Typeahead: best matches by relevance, then size."""
|
|
116
|
+
market = _market(market)
|
|
117
|
+
rows = get_store().rows(market, wait=SEARCH_WAIT)
|
|
118
|
+
return query(rows, q=q, type_=type if type in _TYPES else "all", limit=limit)["rows"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.get("/listings/discovery")
|
|
122
|
+
def market_discovery(
|
|
123
|
+
market: str = Query(default="india"),
|
|
124
|
+
size: int = Query(default=500, ge=20, le=3000, description="Rank the N largest names"),
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Index snapshot, breadth, gainers / losers / most active, 52-week extremes, sectors."""
|
|
127
|
+
from marketmyna.services import live_market
|
|
128
|
+
from marketmyna.services.discovery import DISCOVERY_MARKETS, build
|
|
129
|
+
|
|
130
|
+
market = market.lower()
|
|
131
|
+
if market not in DISCOVERY_MARKETS:
|
|
132
|
+
raise HTTPException(status_code=400, detail=f"Discovery not available for '{market}'")
|
|
133
|
+
return build(market, size, live=live_market.market(market))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@router.get("/listings/hot")
|
|
137
|
+
def hot_stocks(
|
|
138
|
+
market: str = Query(default="india"),
|
|
139
|
+
area: str = Query(default="trader", pattern="^(trader|investor)$"),
|
|
140
|
+
) -> dict:
|
|
141
|
+
"""Hot stocks for the login area: what is moving today (trader) or the
|
|
142
|
+
businesses being re-priced with fundamentals behind them (investor)."""
|
|
143
|
+
from marketmyna.services import hot_stocks as hot
|
|
144
|
+
from marketmyna.services import live_market
|
|
145
|
+
from marketmyna.services.discovery import DISCOVERY_MARKETS
|
|
146
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
147
|
+
|
|
148
|
+
market = market.lower()
|
|
149
|
+
if market not in DISCOVERY_MARKETS:
|
|
150
|
+
raise HTTPException(status_code=400, detail=f"Hot stocks not available for '{market}'")
|
|
151
|
+
live = live_market.market(market)
|
|
152
|
+
if area == "trader":
|
|
153
|
+
return hot.trader(market, live=live)
|
|
154
|
+
return hot.with_live_prices(call_with_deadline(hot.investor, 90, market), live)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.get("/listings/rotation")
|
|
158
|
+
def sector_rotation(market: str = Query(default="india")) -> dict:
|
|
159
|
+
"""Relative rotation of sector baskets vs the market index (RRG), with
|
|
160
|
+
today's live prices moving the current week's point."""
|
|
161
|
+
from marketmyna.services import live_market
|
|
162
|
+
from marketmyna.services.analysis_run import MARKET_INDICES
|
|
163
|
+
from marketmyna.services.rotation import ROTATION_MARKETS, build
|
|
164
|
+
|
|
165
|
+
market = market.lower()
|
|
166
|
+
if market not in ROTATION_MARKETS:
|
|
167
|
+
raise HTTPException(status_code=400, detail=f"Sector rotation not available for '{market}'")
|
|
168
|
+
|
|
169
|
+
def run() -> dict:
|
|
170
|
+
live = live_market.market(market)
|
|
171
|
+
prices = live_market.for_tickers(market, live["quotes"])
|
|
172
|
+
if market != "crypto":
|
|
173
|
+
prices.update(live_market.latest([MARKET_INDICES[market][0][0]]))
|
|
174
|
+
return {**build(market, live=prices), "live": live["status"]}
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
return live_market.memo(("rotation", market), run)
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
raise HTTPException(status_code=503, detail=str(exc))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.get("/listings/deals")
|
|
183
|
+
def nse_deals(days: int = Query(default=1, ge=1, le=90)) -> dict:
|
|
184
|
+
"""NSE bulk and block deals over the last N days (history builds up daily)."""
|
|
185
|
+
from marketmyna.data.nse_deals import recent
|
|
186
|
+
|
|
187
|
+
return recent(days)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.get("/listings/live")
|
|
191
|
+
def live_quotes(market: str, symbols: str = Query(..., max_length=2000)) -> dict:
|
|
192
|
+
"""Latest prices for the rows on screen; the page asks every second.
|
|
193
|
+
Served from a shared cache that refreshes upstream every few seconds
|
|
194
|
+
(marketmyna/services/live_quotes.py), so polling stays cheap."""
|
|
195
|
+
from marketmyna.services import live_quotes as live
|
|
196
|
+
|
|
197
|
+
if market not in MARKET_LABELS:
|
|
198
|
+
raise HTTPException(status_code=400, detail=f"Unknown market '{market}'")
|
|
199
|
+
return live.get(market, symbols.split(","))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
_KNOWN: dict[str, tuple[float, frozenset]] = {}
|
|
203
|
+
KNOWN_TTL = 600.0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _listed(market: str, symbol: str) -> bool:
|
|
207
|
+
"""Whether `symbol` is a listing of `market` (set rebuilt every 10 min).
|
|
208
|
+
Unlisted text never joins the live watch list, where every extra symbol
|
|
209
|
+
slows the shared per-second refresh. With no listings loaded, allow."""
|
|
210
|
+
import time
|
|
211
|
+
|
|
212
|
+
hit = _KNOWN.get(market)
|
|
213
|
+
if hit is None or time.monotonic() - hit[0] > KNOWN_TTL:
|
|
214
|
+
hit = (time.monotonic(), frozenset(str(r.get("symbol", "")).upper() for r in get_store().rows(market)))
|
|
215
|
+
_KNOWN[market] = hit
|
|
216
|
+
return not hit[1] or symbol in hit[1]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@router.get("/listings/activity")
|
|
220
|
+
def trading_activity(market: str, symbol: str = Query(..., max_length=40)) -> dict:
|
|
221
|
+
"""Live trading activity for one stock page: price and volume from the
|
|
222
|
+
shared live cache, plus for India the broker's pending buy / sell
|
|
223
|
+
quantity and best five bids / offers, and last session's delivery %."""
|
|
224
|
+
from marketmyna.services import live_quotes as live
|
|
225
|
+
|
|
226
|
+
if market not in MARKET_LABELS:
|
|
227
|
+
raise HTTPException(status_code=400, detail=f"Unknown market '{market}'")
|
|
228
|
+
sym = symbol.strip().upper()
|
|
229
|
+
if market == "india":
|
|
230
|
+
sym = sym.removesuffix(".NS").removesuffix(".BO")
|
|
231
|
+
elif market == "hong_kong":
|
|
232
|
+
sym = sym.removesuffix(".HK").zfill(5) # listings use 00700
|
|
233
|
+
elif market == "china_a":
|
|
234
|
+
sym = sym.removesuffix(".SS").removesuffix(".SZ")
|
|
235
|
+
quotes = live.get(market, [sym] if _listed(market, sym) else [], depth=True)
|
|
236
|
+
out = {k: quotes[k] for k in ("market", "open", "age_seconds", "refresh_seconds", "source", "error")}
|
|
237
|
+
out["symbol"], out["quote"] = sym, quotes["quotes"].get(sym)
|
|
238
|
+
if market == "india":
|
|
239
|
+
from marketmyna.services import delivery
|
|
240
|
+
|
|
241
|
+
out["delivery"] = delivery.get(sym)
|
|
242
|
+
return out
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@router.get("/listings/etf-rotation")
|
|
246
|
+
def etf_rotation(market: str = Query(default="us"), view: Optional[str] = Query(default=None, max_length=60)) -> dict:
|
|
247
|
+
"""ETF rotation vs the home market (us | hong_kong | china_a): the
|
|
248
|
+
non-India counterpart of the mutual-fund rotation."""
|
|
249
|
+
from marketmyna.services import etf_rotation as er
|
|
250
|
+
from marketmyna.services import live_market
|
|
251
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
252
|
+
|
|
253
|
+
market = market.lower()
|
|
254
|
+
|
|
255
|
+
def run() -> dict:
|
|
256
|
+
prices = live_market.latest(er.tickers(market, view)) if market in er.UNIVERSES else {}
|
|
257
|
+
out = call_with_deadline(er.build, 90, market, view, live=prices)
|
|
258
|
+
return {**out, "live": live_market.status_for(prices)}
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
return live_market.memo(("etf-rotation", market, view), run)
|
|
262
|
+
except ValueError as exc:
|
|
263
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
264
|
+
except TimeoutError:
|
|
265
|
+
raise HTTPException(status_code=503, detail="Still loading ETF prices - try again in a minute.")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TranslateRequest(BaseModel):
|
|
269
|
+
market: str = "china_a"
|
|
270
|
+
names: list[dict] = Field(default_factory=list, max_length=200) # [{"symbol", "name"}]
|
|
271
|
+
texts: list[str] = Field(default_factory=list, max_length=300)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@router.post("/translate")
|
|
275
|
+
def translate(req: TranslateRequest) -> dict:
|
|
276
|
+
"""English for Chinese stock names (official English name from Yahoo,
|
|
277
|
+
cached) and free text such as broker branches (glossary + pinyin)."""
|
|
278
|
+
from marketmyna.services import translate as tr
|
|
279
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
280
|
+
|
|
281
|
+
items = [(str(n.get("symbol", ""))[:20], str(n.get("name", ""))[:200]) for n in req.names
|
|
282
|
+
if n.get("symbol") and tr.has_cjk(str(n.get("name", "")))]
|
|
283
|
+
try:
|
|
284
|
+
names = call_with_deadline(tr.english_names, 60, items, req.market.lower()) if items else {}
|
|
285
|
+
except TimeoutError:
|
|
286
|
+
names = {}
|
|
287
|
+
texts = {t: tr.english_text(t) for t in dict.fromkeys(req.texts) if tr.has_cjk(t)}
|
|
288
|
+
return {"names": names, "texts": texts}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@router.get("/listings/big-trades")
|
|
292
|
+
def big_trades(market: str = Query(default="us"), days: int = Query(default=1, ge=1, le=30)) -> dict:
|
|
293
|
+
"""Big trades outside India: US insider Form 4 trades, China block trades,
|
|
294
|
+
HK southbound Stock Connect, crypto big-trader positioning."""
|
|
295
|
+
from marketmyna.services import big_trades as bt
|
|
296
|
+
from marketmyna.services.sec_mdna import SecNotConfigured
|
|
297
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
return call_with_deadline(bt.build, 120, market, days)
|
|
301
|
+
except SecNotConfigured as exc:
|
|
302
|
+
raise HTTPException(status_code=409, detail=str(exc))
|
|
303
|
+
except ValueError as exc:
|
|
304
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
305
|
+
except TimeoutError:
|
|
306
|
+
raise HTTPException(status_code=503, detail="Still collecting trades - try again in a minute.")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@router.get("/listings/fund-rotation")
|
|
310
|
+
def fund_rotation(by: str = Query(default="category", pattern="^(category|amc)$"),
|
|
311
|
+
group: Optional[str] = Query(default=None, max_length=120)) -> dict:
|
|
312
|
+
"""Leading / weakening / lagging / improving for equity fund categories or
|
|
313
|
+
fund houses vs the Nifty 500; with `group`, each fund inside it."""
|
|
314
|
+
from marketmyna.services import fund_rotation as fr
|
|
315
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
return call_with_deadline(fr.build, 120, by, group)
|
|
319
|
+
except ValueError as exc:
|
|
320
|
+
raise HTTPException(status_code=404 if group else 503, detail=str(exc))
|
|
321
|
+
except TimeoutError:
|
|
322
|
+
raise HTTPException(status_code=503, detail="Still gathering fund NAVs - try again in a minute.")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@router.post("/listings/refresh")
|
|
326
|
+
def refresh_listings(market: str = Query(default="all")) -> list[dict]:
|
|
327
|
+
market = _market(market)
|
|
328
|
+
store = get_store()
|
|
329
|
+
store.rows(market, force=True)
|
|
330
|
+
return [s for s in store.status() if s["source"] in store.keys_for(market)]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@router.get("/listings/mf/{scheme_code}/history")
|
|
334
|
+
def mutual_fund_history(scheme_code: int, days: int = Query(default=365, ge=7, le=3650)) -> list[dict]:
|
|
335
|
+
df = _mf.get_historical_nav(scheme_code, days=days)
|
|
336
|
+
if df.empty:
|
|
337
|
+
raise HTTPException(status_code=502, detail=f"No NAV history for scheme {scheme_code}")
|
|
338
|
+
return [{"date": d.strftime("%Y-%m-%d"), "nav": float(v)} for d, v in df["nav"].items()]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@router.get("/listings/mf/{scheme_code}/holdings")
|
|
342
|
+
def mutual_fund_holdings(scheme_code: int) -> dict:
|
|
343
|
+
"""Stocks (and other assets) a fund holds, from its fund house's official
|
|
344
|
+
monthly portfolio file; index funds fall back to the NSE index list."""
|
|
345
|
+
from marketmyna.services import fund_holdings as fh
|
|
346
|
+
|
|
347
|
+
return fh.for_fund(str(scheme_code))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@router.get("/listings/mf/{scheme_code}/peers")
|
|
351
|
+
def mutual_fund_peers(scheme_code: int) -> dict:
|
|
352
|
+
"""The fund against its competitors: same SEBI category and plan, one
|
|
353
|
+
scheme per fund house, ranked on returns and risk from NAV history."""
|
|
354
|
+
from marketmyna.services import fund_peers
|
|
355
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
return call_with_deadline(fund_peers.compare, 90, str(scheme_code))
|
|
359
|
+
except TimeoutError as exc:
|
|
360
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@router.get("/listings/mf/{scheme_code}/style")
|
|
364
|
+
def mutual_fund_style(scheme_code: int) -> dict:
|
|
365
|
+
"""Style (value / blend / growth, size) from the holdings, and SEBI category rule checks."""
|
|
366
|
+
from marketmyna.services import fund_style
|
|
367
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
return call_with_deadline(fund_style.build, 90, str(scheme_code))
|
|
371
|
+
except TimeoutError as exc:
|
|
372
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@router.get("/listings/mf/{scheme_code}/cost")
|
|
376
|
+
def mutual_fund_cost(scheme_code: int) -> dict:
|
|
377
|
+
"""Direct vs Regular plan cost (from both plans' NAVs) and how gains are taxed."""
|
|
378
|
+
from marketmyna.services import fund_cost
|
|
379
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
return call_with_deadline(fund_cost.build, 60, str(scheme_code))
|
|
383
|
+
except TimeoutError as exc:
|
|
384
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@router.get("/listings/mf/{scheme_code}/overlap")
|
|
388
|
+
def mutual_fund_overlap(scheme_code: int, other: int = Query(...)) -> dict:
|
|
389
|
+
"""Stocks two funds hold in common and how much of the money that is."""
|
|
390
|
+
from marketmyna.services import fund_holdings as fh
|
|
391
|
+
|
|
392
|
+
return fh.overlap(str(scheme_code), str(other))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@router.get("/listings/mf/{scheme_code}/analytics")
|
|
396
|
+
def mutual_fund_analytics(scheme_code: int) -> dict:
|
|
397
|
+
"""Rolling returns, risk against the category benchmark (total return),
|
|
398
|
+
and drawdowns with recovery times, from the fund's daily NAV history."""
|
|
399
|
+
from marketmyna.services import fund_analytics
|
|
400
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
return call_with_deadline(fund_analytics.build, 90, str(scheme_code))
|
|
404
|
+
except TimeoutError as exc:
|
|
405
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@router.post("/listings/mf/{scheme_code}/ai-review")
|
|
409
|
+
def mutual_fund_ai_review(scheme_code: int, refresh: bool = Query(default=False), config=Depends(get_config_dep)) -> dict:
|
|
410
|
+
"""AI read of any fund from the figures on its page (an explanation for
|
|
411
|
+
public users, a view for the owner; see marketmyna/services/fund_ai.py)."""
|
|
412
|
+
from marketmyna.llm.channels import llm_configured
|
|
413
|
+
from marketmyna.llm.protocol import GenerationError
|
|
414
|
+
from marketmyna.services import fund_ai
|
|
415
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
416
|
+
|
|
417
|
+
if not llm_configured(config):
|
|
418
|
+
raise HTTPException(status_code=409, detail="No AI model is set up. Add an AI key under Settings → AI keys.")
|
|
419
|
+
try:
|
|
420
|
+
out = call_with_deadline(lambda: fund_ai.review(str(scheme_code), config, use_cache=not refresh), 180)
|
|
421
|
+
except LookupError as exc:
|
|
422
|
+
raise HTTPException(status_code=404, detail=str(exc))
|
|
423
|
+
except GenerationError as exc:
|
|
424
|
+
raise HTTPException(status_code=402, detail=str(exc))
|
|
425
|
+
except TimeoutError as exc:
|
|
426
|
+
raise HTTPException(status_code=504, detail=str(exc))
|
|
427
|
+
if not out:
|
|
428
|
+
raise HTTPException(status_code=502, detail="The AI model did not return a usable review. Try again.")
|
|
429
|
+
return out
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@router.get("/listings/stock/{symbol}/funds")
|
|
433
|
+
def funds_holding_stock(symbol: str) -> dict:
|
|
434
|
+
"""Mutual fund schemes holding an NSE stock (loaded fund houses only)."""
|
|
435
|
+
from marketmyna.services import fund_holdings as fh
|
|
436
|
+
|
|
437
|
+
return fh.funds_holding(symbol)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@router.get("/listings/mf/{scheme_code}/sip")
|
|
441
|
+
def mutual_fund_sip(
|
|
442
|
+
scheme_code: int,
|
|
443
|
+
monthly: float = Query(default=5000, gt=0, le=10_000_000),
|
|
444
|
+
years: int = Query(default=5, ge=1, le=30),
|
|
445
|
+
day: int = Query(default=5, ge=1, le=28),
|
|
446
|
+
step_up: float = Query(default=10, ge=0, le=100),
|
|
447
|
+
expected: float = Query(default=12, ge=-50, le=50),
|
|
448
|
+
horizon: int = Query(default=10, ge=1, le=40),
|
|
449
|
+
) -> dict:
|
|
450
|
+
"""SIP backtest on real NAV (vs lumpsum, by SIP day) plus a step-up projection."""
|
|
451
|
+
from marketmyna.services import sip
|
|
452
|
+
|
|
453
|
+
df = _mf.get_historical_nav(scheme_code, days=100_000)
|
|
454
|
+
if df.empty:
|
|
455
|
+
raise HTTPException(status_code=502, detail=f"No NAV history for scheme {scheme_code}")
|
|
456
|
+
return {"backtest": sip.backtest(df["nav"], monthly, years, day),
|
|
457
|
+
"step_up": sip.step_up(monthly, step_up, horizon, expected)}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@router.get("/listings/{market}/{kind}/{symbol}/sip-days")
|
|
461
|
+
def best_sip_day(market: str, kind: str, symbol: str, years: int = Query(default=10, ge=3, le=10)) -> dict:
|
|
462
|
+
"""Historically best SIP day for one mutual fund or ETF: the stored
|
|
463
|
+
3 / 5 / 10-year summary (computed fresh if missing) plus the XIRR of
|
|
464
|
+
every day 1-28 over `years`."""
|
|
465
|
+
from marketmyna.services import sip_days
|
|
466
|
+
|
|
467
|
+
market = _market(market)
|
|
468
|
+
if kind not in ("mf", "etf") or (kind == "mf" and market != "india") or market not in ("india", *sip_days.ETF_MARKETS):
|
|
469
|
+
raise HTTPException(status_code=400, detail="Best SIP day covers Indian mutual funds and ETFs in India, US and Hong Kong")
|
|
470
|
+
series = sip_days.mf_nav(symbol) if kind == "mf" else sip_days.etf_prices(symbol, market)
|
|
471
|
+
if series is None or series.empty:
|
|
472
|
+
raise HTTPException(status_code=502, detail=f"No price history for {symbol}")
|
|
473
|
+
summary = sip_days.analyze(series)
|
|
474
|
+
if summary is None:
|
|
475
|
+
return {"summary": None, "curve": [], "years": years, "note": "Needs at least 3 years of prices."}
|
|
476
|
+
span = (series.index.max() - series.index.min()).days / 365.25
|
|
477
|
+
years = min(years, max(3, int(span)))
|
|
478
|
+
return {"summary": summary, "curve": sip_days.curve(series, years), "years": years}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@router.get("/screens")
|
|
482
|
+
def screen_presets(market: str = Query(default="india"), asset: str = Query(default="stock")) -> list[dict]:
|
|
483
|
+
"""Ready-made screens: fundamental for stock markets, price-based for crypto and ETFs."""
|
|
484
|
+
if asset == "etf":
|
|
485
|
+
from marketmyna.services.etf_screens import list_presets as etf_presets
|
|
486
|
+
|
|
487
|
+
return etf_presets()
|
|
488
|
+
if market.lower() == "crypto":
|
|
489
|
+
from marketmyna.services.crypto_screens import list_presets as crypto_presets
|
|
490
|
+
|
|
491
|
+
return crypto_presets()
|
|
492
|
+
from marketmyna.services.screens import list_presets
|
|
493
|
+
|
|
494
|
+
return list_presets()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@router.get("/screens/metrics")
|
|
498
|
+
def screen_metrics() -> list[dict]:
|
|
499
|
+
"""Metrics a custom screen can filter on, in groups."""
|
|
500
|
+
from marketmyna.services.screens import catalogue
|
|
501
|
+
|
|
502
|
+
return catalogue()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class ScreenRuleIn(BaseModel):
|
|
506
|
+
field: str = Field(max_length=40)
|
|
507
|
+
op: str = Field(max_length=20)
|
|
508
|
+
value: Optional[float] = None
|
|
509
|
+
value2: Optional[float] = None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class CustomScreenIn(BaseModel):
|
|
513
|
+
market: str = "india"
|
|
514
|
+
rules: list[ScreenRuleIn] = Field(default_factory=list, max_length=12)
|
|
515
|
+
sectors: list[Annotated[str, Field(max_length=80)]] = Field(default_factory=list, max_length=40)
|
|
516
|
+
sort: Optional[str] = Field(default=None, max_length=40)
|
|
517
|
+
descending: bool = True
|
|
518
|
+
columns: list[Annotated[str, Field(max_length=40)]] = Field(default_factory=list, max_length=20)
|
|
519
|
+
large_only: bool = False
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@router.post("/screens/custom")
|
|
523
|
+
def run_custom_screen(body: CustomScreenIn) -> dict:
|
|
524
|
+
"""Run the user's own rules over the largest stocks of a market."""
|
|
525
|
+
from marketmyna.services import screens
|
|
526
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
527
|
+
|
|
528
|
+
market = _market(body.market)
|
|
529
|
+
if market not in ("india", "us", "hong_kong", "china_a"):
|
|
530
|
+
raise HTTPException(status_code=400, detail="Custom screens cover India, US, Hong Kong and China A")
|
|
531
|
+
try:
|
|
532
|
+
return call_with_deadline(screens.run_custom, 90, market, [r.model_dump() for r in body.rules], body.sectors,
|
|
533
|
+
body.sort, body.descending, body.columns, body.large_only)
|
|
534
|
+
except screens.RuleError as exc:
|
|
535
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@router.get("/screens/{key}")
|
|
539
|
+
def run_screen(key: str, market: str = Query(default="india"), asset: str = Query(default="stock")) -> dict:
|
|
540
|
+
"""Run one preset over the largest stocks (or most-traded coins, or ETFs) of a market."""
|
|
541
|
+
from marketmyna.utils.deadline import call_with_deadline
|
|
542
|
+
|
|
543
|
+
market = _market(market)
|
|
544
|
+
if asset == "etf":
|
|
545
|
+
from marketmyna.services import etf_screens
|
|
546
|
+
|
|
547
|
+
if market not in etf_screens.MARKETS:
|
|
548
|
+
raise HTTPException(status_code=400, detail="ETF screens cover India, US, Hong Kong and China A")
|
|
549
|
+
if key not in etf_screens.PRESETS:
|
|
550
|
+
raise HTTPException(status_code=404, detail=f"Unknown screen '{key}'")
|
|
551
|
+
return call_with_deadline(etf_screens.run, 90, key, market)
|
|
552
|
+
if market == "crypto":
|
|
553
|
+
from marketmyna.services import crypto_screens
|
|
554
|
+
|
|
555
|
+
if key not in crypto_screens.PRESETS:
|
|
556
|
+
raise HTTPException(status_code=404, detail=f"Unknown screen '{key}'")
|
|
557
|
+
return call_with_deadline(crypto_screens.run, 90, key)
|
|
558
|
+
from marketmyna.services.screens import PRESETS, run
|
|
559
|
+
|
|
560
|
+
if key not in PRESETS:
|
|
561
|
+
raise HTTPException(status_code=404, detail=f"Unknown screen '{key}'")
|
|
562
|
+
if market not in ("india", "us", "hong_kong", "china_a"):
|
|
563
|
+
raise HTTPException(status_code=400, detail="Screens cover India, US, Hong Kong, China A and crypto")
|
|
564
|
+
return call_with_deadline(run, 90, key, market)
|