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,36 @@
|
|
|
1
|
+
"""Hard deadlines for third-party calls that can hang (Yahoo's `.info`,
|
|
2
|
+
option chains, batch downloads have no reliable timeout of their own).
|
|
3
|
+
|
|
4
|
+
The call runs on a daemon thread: if it overruns, the caller gets
|
|
5
|
+
TimeoutError right away and the stuck thread can't delay a reload or
|
|
6
|
+
process exit (a ThreadPoolExecutor's threads would)."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextvars
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Any, Callable, TypeVar
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def call_with_deadline(fn: Callable[..., T], seconds: float, *args: Any, **kwargs: Any) -> T:
|
|
18
|
+
box: dict[str, Any] = {}
|
|
19
|
+
done = threading.Event()
|
|
20
|
+
|
|
21
|
+
def run() -> None:
|
|
22
|
+
try:
|
|
23
|
+
box["value"] = fn(*args, **kwargs)
|
|
24
|
+
except BaseException as exc: # re-raised in the caller's thread
|
|
25
|
+
box["error"] = exc
|
|
26
|
+
finally:
|
|
27
|
+
done.set()
|
|
28
|
+
|
|
29
|
+
# Copy the caller's context so the per-user data scope (marketmyna/core/tenant.py) follows the call.
|
|
30
|
+
ctx = contextvars.copy_context()
|
|
31
|
+
threading.Thread(target=ctx.run, args=(run,), name=f"deadline:{getattr(fn, '__name__', 'call')}", daemon=True).start()
|
|
32
|
+
if not done.wait(seconds):
|
|
33
|
+
raise TimeoutError(f"{getattr(fn, '__name__', 'call')} took longer than {seconds:.0f}s")
|
|
34
|
+
if "error" in box:
|
|
35
|
+
raise box["error"]
|
|
36
|
+
return box["value"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Mask secrets in every log line, tracebacks included.
|
|
2
|
+
|
|
3
|
+
A failed Telegram call raises an error whose text carries the request URL,
|
|
4
|
+
and that URL holds the bot token (api.telegram.org/bot<token>/...); a plain
|
|
5
|
+
`logger.exception` then wrote the token to the server logs. The same goes for
|
|
6
|
+
AI keys echoed back by a provider. `install()` wraps the log record factory,
|
|
7
|
+
so every record from every logger is cleaned before any handler sees it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
SECRETS = re.compile(
|
|
17
|
+
r"(?<=/bot)\d{5,}:[\w-]{20,}" # Telegram bot token in an API URL
|
|
18
|
+
r"|\b\d{8,12}:AA[\w-]{30,}" # Telegram bot token on its own
|
|
19
|
+
r"|AIza[\w-]{10,}|gsk_\w{10,}|sk-[\w-]{10,}" # AI provider keys
|
|
20
|
+
r"|(?<=key=)[^&\s\"']+|(?<=Bearer )[\w.-]+"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_installed = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def redact(text: str) -> str:
|
|
27
|
+
return SECRETS.sub("***", text)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def install() -> None:
|
|
31
|
+
"""Idempotent; safe to call from every entry point."""
|
|
32
|
+
global _installed
|
|
33
|
+
if _installed:
|
|
34
|
+
return
|
|
35
|
+
make = logging.getLogRecordFactory()
|
|
36
|
+
|
|
37
|
+
def factory(*args, **kwargs):
|
|
38
|
+
record = make(*args, **kwargs)
|
|
39
|
+
try:
|
|
40
|
+
message = record.getMessage()
|
|
41
|
+
except Exception: # noqa: BLE001 - a bad format string is the handler's problem, not ours
|
|
42
|
+
return record
|
|
43
|
+
clean = redact(message)
|
|
44
|
+
if clean != message:
|
|
45
|
+
record.msg, record.args = clean, None
|
|
46
|
+
if record.exc_info and record.exc_info[0] is not None:
|
|
47
|
+
# Formatters reuse exc_text when it is set, so the raw traceback is never printed.
|
|
48
|
+
record.exc_text = redact("".join(traceback.format_exception(*record.exc_info)).rstrip("\n"))
|
|
49
|
+
return record
|
|
50
|
+
|
|
51
|
+
logging.setLogRecordFactory(factory)
|
|
52
|
+
_installed = True
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""One shared pace for every Yahoo Finance request on this server.
|
|
2
|
+
|
|
3
|
+
Yahoo throttles by IP, not by process. The web app, every scheduled-report
|
|
4
|
+
worker and the background jobs each run their own yfinance, so before this
|
|
5
|
+
gate a report wave of 4 workers fired requests in bursts and one 429 in one
|
|
6
|
+
worker said nothing to the others, which kept hammering and extended the ban.
|
|
7
|
+
|
|
8
|
+
- Pacing (scheduled-report workers only; web requests are never delayed):
|
|
9
|
+
a token bucket shared by all worker processes (a small state file under
|
|
10
|
+
a file lock). Up to `BURST` requests go straight through, so a page that
|
|
11
|
+
fetches dozens of tickers at once is not slowed; beyond that, requests
|
|
12
|
+
are released at `RATE` per second. (A strict gap between every request
|
|
13
|
+
queued page loads behind each other: 80 tickers took 16 s.)
|
|
14
|
+
- Shared cooldown: a 429 anywhere pauses Yahoo for every process for
|
|
15
|
+
`COOLDOWN` seconds. Scheduled workers (`patient()`) wait it out, up to
|
|
16
|
+
`MAX_WAIT`; web requests fail fast with yfinance's own YFRateLimitError, so
|
|
17
|
+
pages fall back to caches instead of hanging.
|
|
18
|
+
|
|
19
|
+
`install()` wraps yfinance's single request method (YfData._make_request), so
|
|
20
|
+
every Ticker / download / search call goes through the gate without touching
|
|
21
|
+
the call sites. Without fcntl (Windows) the gate works per process only.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import functools
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from marketmyna.core.home import app_home
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
RATE = float(os.environ.get("YAHOO_RATE", 10)) # sustained requests per second
|
|
38
|
+
BURST = float(os.environ.get("YAHOO_BURST", 60))
|
|
39
|
+
COOLDOWN = float(os.environ.get("YAHOO_COOLDOWN", 30))
|
|
40
|
+
MAX_WAIT = float(os.environ.get("YAHOO_MAX_WAIT", 90))
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import fcntl
|
|
44
|
+
except ImportError: # pragma: no cover - Windows
|
|
45
|
+
fcntl = None
|
|
46
|
+
try:
|
|
47
|
+
import msvcrt # Windows file locks
|
|
48
|
+
except ImportError:
|
|
49
|
+
msvcrt = None
|
|
50
|
+
|
|
51
|
+
_local_lock = threading.Lock()
|
|
52
|
+
_local_state = [0.0, 0.0] # bucket clock (see reserve), cooldown until (no-fcntl fallback)
|
|
53
|
+
_patient = False
|
|
54
|
+
_installed = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def state_path() -> Path:
|
|
58
|
+
return Path(os.environ.get("YAHOO_GATE_FILE") or app_home() / "yahoo_gate")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def patient(on: bool = True) -> None:
|
|
62
|
+
"""This process waits out a shared cooldown instead of failing fast
|
|
63
|
+
(scheduled-report workers: a late report beats a failed one)."""
|
|
64
|
+
global _patient
|
|
65
|
+
_patient = on
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _lock(f) -> None:
|
|
69
|
+
if fcntl is not None:
|
|
70
|
+
fcntl.flock(f, fcntl.LOCK_EX)
|
|
71
|
+
else: # Windows: lock the first byte (LK_LOCK retries for about 10 s, then raises OSError)
|
|
72
|
+
f.seek(0)
|
|
73
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _unlock(f) -> None:
|
|
77
|
+
if fcntl is not None:
|
|
78
|
+
fcntl.flock(f, fcntl.LOCK_UN)
|
|
79
|
+
else:
|
|
80
|
+
f.seek(0)
|
|
81
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _update(change):
|
|
85
|
+
"""Run change(clock, cooldown_until) -> (clock, cooldown_until, result)
|
|
86
|
+
under the cross-process lock and return result."""
|
|
87
|
+
if fcntl is not None or msvcrt is not None:
|
|
88
|
+
try:
|
|
89
|
+
path = state_path()
|
|
90
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
with open(path, "a+", encoding="utf-8") as f:
|
|
92
|
+
_lock(f)
|
|
93
|
+
try:
|
|
94
|
+
f.seek(0)
|
|
95
|
+
parts = f.read().split()
|
|
96
|
+
try:
|
|
97
|
+
nxt, cool = float(parts[0]), float(parts[1])
|
|
98
|
+
except (IndexError, ValueError):
|
|
99
|
+
nxt, cool = 0.0, 0.0
|
|
100
|
+
nxt, cool, result = change(nxt, cool)
|
|
101
|
+
f.seek(0)
|
|
102
|
+
f.truncate()
|
|
103
|
+
f.write(f"{nxt:.3f} {cool:.3f}")
|
|
104
|
+
f.flush()
|
|
105
|
+
return result
|
|
106
|
+
finally:
|
|
107
|
+
_unlock(f)
|
|
108
|
+
except OSError as exc:
|
|
109
|
+
logger.debug("Yahoo gate file unavailable, pacing this process only: %s", exc)
|
|
110
|
+
with _local_lock:
|
|
111
|
+
nxt, cool, result = change(*_local_state)
|
|
112
|
+
_local_state[:] = [nxt, cool]
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def reserve() -> float:
|
|
117
|
+
"""Book the next request slot; returns seconds to sleep before sending.
|
|
118
|
+
Raises YFRateLimitError while Yahoo is cooling down (unless patient)."""
|
|
119
|
+
now = time.time()
|
|
120
|
+
|
|
121
|
+
# The bucket is kept as one number, `clock`: the moment it would be full
|
|
122
|
+
# again. Each request pushes it 1/RATE later; a request may go once the
|
|
123
|
+
# clock is no more than BURST/RATE ahead of now.
|
|
124
|
+
def change(clock, cool):
|
|
125
|
+
if cool > now and (not _patient or cool - now > MAX_WAIT):
|
|
126
|
+
return clock, cool, -(cool - now)
|
|
127
|
+
if not _patient:
|
|
128
|
+
# Web requests are never paced (a page waits on them); only the
|
|
129
|
+
# scheduled-report workers, which burst for minutes, use the bucket.
|
|
130
|
+
return clock, cool, 0.0
|
|
131
|
+
start = max(now, cool)
|
|
132
|
+
clock = max(clock, start) + 1.0 / RATE
|
|
133
|
+
return clock, cool, max(0.0, clock - start - BURST / RATE) + (start - now)
|
|
134
|
+
|
|
135
|
+
wait = _update(change)
|
|
136
|
+
if wait < 0:
|
|
137
|
+
from yfinance.exceptions import YFRateLimitError
|
|
138
|
+
|
|
139
|
+
logger.info("Yahoo cooling down for another %.0fs after a 429; not sending", -wait)
|
|
140
|
+
raise YFRateLimitError()
|
|
141
|
+
return wait
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def record_limit() -> None:
|
|
145
|
+
"""Yahoo said 429: every process on this server pauses for COOLDOWN."""
|
|
146
|
+
until = time.time() + COOLDOWN
|
|
147
|
+
|
|
148
|
+
def change(clock, cool):
|
|
149
|
+
return clock, max(cool, until), None
|
|
150
|
+
|
|
151
|
+
_update(change)
|
|
152
|
+
logger.warning("Yahoo rate limit hit; pausing Yahoo requests for %.0fs on this server", COOLDOWN)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def install() -> None:
|
|
156
|
+
"""Route every yfinance HTTP request through the gate (idempotent)."""
|
|
157
|
+
global _installed
|
|
158
|
+
if _installed:
|
|
159
|
+
return
|
|
160
|
+
try:
|
|
161
|
+
from yfinance.data import YfData
|
|
162
|
+
from yfinance.exceptions import YFRateLimitError
|
|
163
|
+
except Exception as exc: # noqa: BLE001 - yfinance missing or reshaped: run ungated
|
|
164
|
+
logger.warning("Yahoo gate not installed: %s", exc)
|
|
165
|
+
return
|
|
166
|
+
original = YfData._make_request
|
|
167
|
+
|
|
168
|
+
@functools.wraps(original)
|
|
169
|
+
def paced(self, *args, **kwargs):
|
|
170
|
+
wait = reserve()
|
|
171
|
+
if wait > 0:
|
|
172
|
+
time.sleep(wait)
|
|
173
|
+
try:
|
|
174
|
+
response = original(self, *args, **kwargs)
|
|
175
|
+
except YFRateLimitError:
|
|
176
|
+
record_limit()
|
|
177
|
+
raise
|
|
178
|
+
if getattr(response, "status_code", None) == 429:
|
|
179
|
+
record_limit()
|
|
180
|
+
return response
|
|
181
|
+
|
|
182
|
+
YfData._make_request = paced
|
|
183
|
+
_installed = True
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""One process-wide lock for yfinance.download().
|
|
2
|
+
|
|
3
|
+
yf.download keeps its results in module-level state (yfinance.shared), so two
|
|
4
|
+
batch downloads running at once on different threads can hand each other's
|
|
5
|
+
rows back: a 1-year commodities fetch and a 2-year crude fetch overlapping
|
|
6
|
+
returned 1-year Brent to the 2-year caller. Every batch download goes through
|
|
7
|
+
this lock; single-ticker Ticker().history() calls do not need it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
YF_DOWNLOAD_LOCK = threading.Lock()
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{b as t,n,p as r,r as i}from"./format-d8InM0Zw.js";import{t as a}from"./toast-DDradTZw.js";import{D as o,_ as s,a as c,c as l,f as u,g as d,i as f,o as p,p as m,r as h}from"./ui-gsFb8B2m.js";import{C as g,D as _,_ as v,b as y,g as b,j as x,y as S}from"./index-CKAnq-Or.js";var C=e(t(),1),w=r(),T=[{value:`profile`,label:`Profile`},{value:`data`,label:`Your data`}];function E(){let e=b(e=>e.me),t=b(e=>e.setMe),[n,r]=x(),i=T,a=i.some(e=>e.value===n.get(`tab`))?n.get(`tab`):`profile`;return(0,C.useEffect)(()=>{g().then(t).catch(()=>{})},[t]),e?(0,w.jsxs)(`div`,{className:`space-y-6`,children:[(0,w.jsx)(m,{title:`Account settings`,description:`Your name, time zone and data.`}),(0,w.jsx)(d,{options:i,value:a,onChange:e=>{let t=new URLSearchParams(n);t.set(`tab`,e),r(t,{replace:!0})}}),a===`profile`&&(0,w.jsx)(D,{}),a===`data`&&(0,w.jsx)(j,{})]}):null}function D(){let e=b(e=>e.me),t=b(e=>e.setMe),[r,o]=(0,C.useState)(e.name),d=e.timezone??`Asia/Kolkata`,m=e.timezone_auto!==!1,[g,y]=(0,C.useState)(d),[x,T]=(0,C.useState)(m),[E,D]=(0,C.useState)(!1),A=S(),j=x&&A?A:g,M=(0,C.useMemo)(()=>{let e=typeof Intl.supportedValuesOf==`function`?Intl.supportedValuesOf(`timeZone`).map(v):[];return[...new Set([g,A,...e])].filter(Boolean).sort()},[g,A]);async function N(e){e.preventDefault(),D(!0);try{t(await _({name:r,timezone:j,timezone_auto:x})),a.success(`Profile saved`)}catch(e){a.error(n(e))}finally{D(!1)}}return(0,w.jsxs)(f,{children:[(0,w.jsx)(p,{title:`Profile`,icon:`user`}),(0,w.jsxs)(c,{className:`space-y-5`,children:[(0,w.jsxs)(`div`,{className:`flex items-center gap-4`,children:[(0,w.jsx)(`span`,{className:`flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-accent-soft text-lg font-semibold text-accent-fg ring-1 ring-accent/30`,children:i(e.name,e.email)}),(0,w.jsx)(`div`,{className:`min-w-0`,children:(0,w.jsx)(`div`,{className:`truncate text-base font-semibold`,children:e.name||`No name yet`})})]}),(0,w.jsxs)(`form`,{onSubmit:N,className:`grid gap-3 sm:grid-cols-[1fr_1fr_auto] sm:items-start`,children:[(0,w.jsx)(l,{label:`Name`,hint:`Shown in the menu.`,children:(0,w.jsx)(u,{value:r,maxLength:80,autoComplete:`name`,onChange:e=>o(e.target.value)})}),(0,w.jsx)(l,{label:(0,w.jsxs)(`span`,{className:`flex items-center justify-between gap-2`,children:[`Time zone`,(0,w.jsx)(k,{checked:x,disabled:!A,onChange:e=>{T(e),!e&&A&&y(A)}})]}),hint:x?`From this device's clock: now ${O(j)}. Changes when you travel.`:`Scheduled report times are in this zone: now ${O(j)}.`,children:x?(0,w.jsx)(u,{value:j.replace(/_/g,` `),readOnly:!0,"aria-readonly":!0,className:`cursor-default text-muted`}):(0,w.jsx)(s,{value:g,onChange:e=>y(e.target.value),options:M.map(e=>({value:e,label:e.replace(/_/g,` `)}))})}),(0,w.jsx)(h,{type:`submit`,variant:`secondary`,className:`sm:mt-[calc(var(--text-xs--line-height)+0.375rem)]`,loading:E,disabled:r.trim()===e.name.trim()&&j===d&&x===m,children:`Save`})]})]})]})}function O(e){try{return new Intl.DateTimeFormat(void 0,{timeZone:e,hour:`numeric`,minute:`2-digit`,timeZoneName:`short`}).format(new Date)}catch{return`—`}}function k({checked:e,disabled:t,onChange:n}){return(0,w.jsxs)(`button`,{type:`button`,role:`switch`,"aria-checked":e,disabled:t,title:t?`This browser doesn't share its time zone`:`Follow this device's time zone`,onClick:t=>{t.preventDefault(),n(!e)},className:`inline-flex items-center gap-1.5 text-xs font-normal text-muted hover:text-fg disabled:opacity-50`,children:[`Set automatically`,(0,w.jsx)(`span`,{className:o(`relative h-4 w-7 shrink-0 rounded-full transition-colors`,e?`bg-accent`:`bg-line-strong`),children:(0,w.jsx)(`span`,{className:o(`absolute top-0.5 start-0 h-3 w-3 rounded-full bg-white transition-transform`,e?`translate-x-3.5`:`translate-x-0.5`)})})]})}function A(){let[e,t]=(0,C.useState)(!1);async function r(){t(!0);try{await y(),a.success(`Your data file is downloading.`)}catch(e){a.error(n(e))}finally{t(!1)}}return(0,w.jsx)(h,{variant:`secondary`,icon:`download`,loading:e,onClick:r,children:`Download my data`})}function j(){return(0,w.jsxs)(f,{children:[(0,w.jsx)(p,{title:`Your data`,icon:`download`,description:`Download a copy of everything MarketMyna stores about you, whenever you like.`}),(0,w.jsxs)(c,{className:`space-y-3`,children:[(0,w.jsx)(`p`,{className:`text-sm text-muted`,children:`One JSON file with your profile, plan and payments, portfolios and transactions, watchlists, alerts, scheduled report settings and usage history. Passwords, login tokens and your saved AI keys are never included.`}),(0,w.jsx)(A,{})]})]})}export{E as default};
|