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,149 @@
|
|
|
1
|
+
"""A pool of long-lived worker processes for scheduled-report analyses.
|
|
2
|
+
|
|
3
|
+
Starting a fresh Python process per stock costs ~5 s of imports (pandas,
|
|
4
|
+
yfinance, litellm...) before any work; a report wave of hundreds of stocks
|
|
5
|
+
would spend a fifth of its time just starting up. These workers import once
|
|
6
|
+
and then take one stock after another.
|
|
7
|
+
|
|
8
|
+
Safety, as with one process per stock:
|
|
9
|
+
- every task has a deadline; a worker past it (or one that died) is killed
|
|
10
|
+
and replaced, and only that task fails;
|
|
11
|
+
- each worker talks to the parent over its own pipe, so killing one never
|
|
12
|
+
corrupts the others' results;
|
|
13
|
+
- a worker is replaced after `max_tasks` tasks, so leaks can't build up.
|
|
14
|
+
|
|
15
|
+
One batch runs at a time (the scheduler's timer waves); the interactive
|
|
16
|
+
"Run now" keeps its own short-lived processes, so it never waits on a wave.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from multiprocessing.connection import wait
|
|
25
|
+
from typing import Callable, Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# (symbol, market, mode, llm_kwargs, sections, user_id, shared)
|
|
30
|
+
Task = tuple
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _worker_main(conn, run: Callable) -> None:
|
|
34
|
+
"""Child process: run tasks until told to stop (None) or the pipe closes."""
|
|
35
|
+
while True:
|
|
36
|
+
try:
|
|
37
|
+
msg = conn.recv()
|
|
38
|
+
except (EOFError, OSError):
|
|
39
|
+
return
|
|
40
|
+
if msg is None:
|
|
41
|
+
return
|
|
42
|
+
task_id, task = msg
|
|
43
|
+
try:
|
|
44
|
+
result = run(*task)
|
|
45
|
+
except Exception as exc: # noqa: BLE001 - always answer the parent
|
|
46
|
+
result = {"ok": False, "symbol": task[0], "market": task[1], "error": str(exc) or type(exc).__name__}
|
|
47
|
+
try:
|
|
48
|
+
conn.send((task_id, result))
|
|
49
|
+
except (EOFError, OSError):
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _Worker:
|
|
54
|
+
def __init__(self, ctx, run: Callable):
|
|
55
|
+
parent, child = ctx.Pipe()
|
|
56
|
+
self.conn = parent
|
|
57
|
+
self.process = ctx.Process(target=_worker_main, args=(child, run), daemon=True)
|
|
58
|
+
self.process.start()
|
|
59
|
+
child.close()
|
|
60
|
+
self.tasks = 0
|
|
61
|
+
self.busy: Optional[tuple[int, float]] = None # (task index, deadline)
|
|
62
|
+
|
|
63
|
+
def stop(self, kill: bool = False) -> None:
|
|
64
|
+
try:
|
|
65
|
+
if kill:
|
|
66
|
+
self.process.kill()
|
|
67
|
+
else:
|
|
68
|
+
self.conn.send(None)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
self.process.join(timeout=5)
|
|
72
|
+
if self.process.is_alive():
|
|
73
|
+
self.process.kill()
|
|
74
|
+
self.process.join(timeout=5)
|
|
75
|
+
try:
|
|
76
|
+
self.conn.close()
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class WorkerPool:
|
|
82
|
+
def __init__(self, ctx, run: Callable, size: int, task_timeout: float, max_tasks: int = 200):
|
|
83
|
+
self._ctx, self._run = ctx, run
|
|
84
|
+
self.size = max(1, size)
|
|
85
|
+
self.task_timeout = task_timeout
|
|
86
|
+
self.max_tasks = max_tasks
|
|
87
|
+
self._workers: list[_Worker] = []
|
|
88
|
+
self._lock = threading.Lock()
|
|
89
|
+
|
|
90
|
+
def _ensure(self) -> None:
|
|
91
|
+
self._workers = [w for w in self._workers if w.process.is_alive()]
|
|
92
|
+
while len(self._workers) < self.size:
|
|
93
|
+
self._workers.append(_Worker(self._ctx, self._run))
|
|
94
|
+
while len(self._workers) > self.size: # size lowered by the admin
|
|
95
|
+
self._workers.pop().stop()
|
|
96
|
+
|
|
97
|
+
def resize(self, size: int) -> None:
|
|
98
|
+
self.size = max(1, size)
|
|
99
|
+
|
|
100
|
+
def run_many(self, tasks: list[Task]) -> list[dict]:
|
|
101
|
+
"""Runs every task on the pool; results in task order, never raises."""
|
|
102
|
+
with self._lock:
|
|
103
|
+
self._ensure()
|
|
104
|
+
results: list[Optional[dict]] = [None] * len(tasks)
|
|
105
|
+
pending = list(range(len(tasks)))
|
|
106
|
+
pending.reverse()
|
|
107
|
+
while pending or any(w.busy for w in self._workers):
|
|
108
|
+
for w in self._workers:
|
|
109
|
+
if w.busy is None and pending:
|
|
110
|
+
i = pending.pop()
|
|
111
|
+
try:
|
|
112
|
+
w.conn.send((i, tasks[i]))
|
|
113
|
+
w.busy = (i, time.monotonic() + self.task_timeout)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
results[i] = self._failed(tasks[i], f"worker unavailable: {exc}")
|
|
116
|
+
busy = {w.conn: w for w in self._workers if w.busy}
|
|
117
|
+
for conn in wait(list(busy), timeout=0.5) if busy else []:
|
|
118
|
+
w = busy[conn]
|
|
119
|
+
try:
|
|
120
|
+
i, result = conn.recv()
|
|
121
|
+
except (EOFError, OSError):
|
|
122
|
+
continue # died: handled below
|
|
123
|
+
results[i] = result
|
|
124
|
+
w.busy = None
|
|
125
|
+
w.tasks += 1
|
|
126
|
+
now = time.monotonic()
|
|
127
|
+
for idx, w in enumerate(self._workers):
|
|
128
|
+
if w.busy and (now > w.busy[1] or not w.process.is_alive()):
|
|
129
|
+
i = w.busy[0]
|
|
130
|
+
reason = (f"watchdog timeout after {self.task_timeout:.0f}s" if w.process.is_alive()
|
|
131
|
+
else "worker process exited without a result")
|
|
132
|
+
logger.warning("Scheduled analysis of %s: %s; replacing the worker", tasks[i][0], reason)
|
|
133
|
+
results[i] = self._failed(tasks[i], reason)
|
|
134
|
+
w.stop(kill=True)
|
|
135
|
+
self._workers[idx] = _Worker(self._ctx, self._run)
|
|
136
|
+
elif not w.busy and w.tasks >= self.max_tasks:
|
|
137
|
+
w.stop()
|
|
138
|
+
self._workers[idx] = _Worker(self._ctx, self._run)
|
|
139
|
+
return [r if r is not None else self._failed(t, "no result") for r, t in zip(results, tasks)]
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _failed(task: Task, error: str) -> dict:
|
|
143
|
+
return {"ok": False, "symbol": task[0], "market": task[1], "error": error}
|
|
144
|
+
|
|
145
|
+
def close(self) -> None:
|
|
146
|
+
with self._lock:
|
|
147
|
+
for w in self._workers:
|
|
148
|
+
w.stop()
|
|
149
|
+
self._workers = []
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""World markets: major stock indices by region, currencies, US Treasury
|
|
2
|
+
yields and fear gauges, each with 1-day / 1-month / 1-year change and a
|
|
3
|
+
3-month sparkline.
|
|
4
|
+
|
|
5
|
+
Prices are Yahoo daily closes (no key); currency pairs Yahoo misses come from
|
|
6
|
+
the ECB reference rates (marketmyna/data/ecb_fx.py). The VIX family's latest value is
|
|
7
|
+
taken from CBOE's own delayed quotes when they answer (fresher than Yahoo
|
|
8
|
+
before the US open). Crypto mood is alternative.me's fear & greed index.
|
|
9
|
+
The VIX term structure is reported as a reading, not advice: a 1-month VIX
|
|
10
|
+
above the 3-month one (backwardation) has historically come with market stress.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
19
|
+
from typing import Callable, Optional
|
|
20
|
+
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
from .commodities import _change, _yahoo
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
CACHE_SECONDS = 15 * 60
|
|
29
|
+
|
|
30
|
+
# ticker, label, group, note
|
|
31
|
+
ITEMS = [
|
|
32
|
+
("^GSPC", "S&P 500", "americas", "US large caps"),
|
|
33
|
+
("^IXIC", "Nasdaq Composite", "americas", "US, tech-heavy"),
|
|
34
|
+
("^DJI", "Dow Jones", "americas", "30 US blue chips"),
|
|
35
|
+
("^GSPTSE", "S&P/TSX", "americas", "Canada"),
|
|
36
|
+
("^BVSP", "Bovespa", "americas", "Brazil"),
|
|
37
|
+
("^STOXX50E", "Euro Stoxx 50", "europe", "Eurozone blue chips"),
|
|
38
|
+
("^FTSE", "FTSE 100", "europe", "UK"),
|
|
39
|
+
("^GDAXI", "DAX", "europe", "Germany"),
|
|
40
|
+
("^FCHI", "CAC 40", "europe", "France"),
|
|
41
|
+
("^NSEI", "Nifty 50", "asia", "India"),
|
|
42
|
+
("^N225", "Nikkei 225", "asia", "Japan"),
|
|
43
|
+
("000001.SS", "Shanghai Composite", "asia", "China A-shares"),
|
|
44
|
+
("^HSI", "Hang Seng", "asia", "Hong Kong"),
|
|
45
|
+
("^KS11", "KOSPI", "asia", "South Korea"),
|
|
46
|
+
("^TWII", "Taiwan Weighted", "asia", "Taiwan"),
|
|
47
|
+
("^AXJO", "ASX 200", "asia", "Australia"),
|
|
48
|
+
("DX-Y.NYB", "US dollar index", "fx", "Dollar against six major currencies"),
|
|
49
|
+
("EURUSD=X", "EUR / USD", "fx", "Dollars per euro"),
|
|
50
|
+
("GBPUSD=X", "GBP / USD", "fx", "Dollars per pound"),
|
|
51
|
+
("USDJPY=X", "USD / JPY", "fx", "Yen per dollar"),
|
|
52
|
+
("USDCNY=X", "USD / CNY", "fx", "Yuan per dollar"),
|
|
53
|
+
("USDINR=X", "USD / INR", "fx", "Rupees per dollar"),
|
|
54
|
+
("AUDUSD=X", "AUD / USD", "fx", "Dollars per Australian dollar"),
|
|
55
|
+
("^IRX", "US 3-month", "yields", "Treasury bill yield, %"),
|
|
56
|
+
("^FVX", "US 5-year", "yields", "Treasury yield, %"),
|
|
57
|
+
("^TNX", "US 10-year", "yields", "Treasury yield, %"),
|
|
58
|
+
("^TYX", "US 30-year", "yields", "Treasury yield, %"),
|
|
59
|
+
("^VIX9D", "VIX 9-day", "volatility", "Expected S&P 500 volatility, next 9 days"),
|
|
60
|
+
("^VIX", "VIX", "volatility", "Expected S&P 500 volatility, next 30 days"),
|
|
61
|
+
("^VIX3M", "VIX 3-month", "volatility", "Expected S&P 500 volatility, next 3 months"),
|
|
62
|
+
("^VIX6M", "VIX 6-month", "volatility", "Expected S&P 500 volatility, next 6 months"),
|
|
63
|
+
("^INDIAVIX", "India VIX", "volatility", "Expected Nifty volatility, next 30 days"),
|
|
64
|
+
]
|
|
65
|
+
# Yields move in points, not percent: their changes are reported as differences.
|
|
66
|
+
POINT_CHANGE = {"yields", "volatility"}
|
|
67
|
+
|
|
68
|
+
Fetch = Callable[[list[str]], dict[str, pd.Series]]
|
|
69
|
+
|
|
70
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
71
|
+
_raw_cache: dict[str, tuple[float, tuple]] = {}
|
|
72
|
+
_lock = threading.Lock()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cboe_latest(symbols: list[str]) -> dict[str, float]:
|
|
76
|
+
"""Latest CBOE delayed values for VIX indices (^VIX -> _VIX.json)."""
|
|
77
|
+
def one(sym: str) -> tuple[str, Optional[float]]:
|
|
78
|
+
try:
|
|
79
|
+
r = requests.get(f"https://cdn.cboe.com/api/global/delayed_quotes/quotes/_{sym.lstrip('^')}.json",
|
|
80
|
+
headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
|
81
|
+
r.raise_for_status()
|
|
82
|
+
price = ((r.json() or {}).get("data") or {}).get("current_price")
|
|
83
|
+
return sym, float(price) if price else None
|
|
84
|
+
except Exception: # noqa: BLE001 - CBOE is only a fresher override
|
|
85
|
+
return sym, None
|
|
86
|
+
|
|
87
|
+
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
88
|
+
return {s: v for s, v in pool.map(one, symbols) if v}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fear_greed() -> Optional[dict]:
|
|
92
|
+
from ..data.extra_sources import crypto_fear_greed
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
rows = crypto_fear_greed(31)
|
|
96
|
+
except Exception as exc: # noqa: BLE001
|
|
97
|
+
logger.info("Crypto fear & greed unavailable: %s", exc)
|
|
98
|
+
return None
|
|
99
|
+
if not rows:
|
|
100
|
+
return None
|
|
101
|
+
month = rows[30]["value"] if len(rows) > 30 else None
|
|
102
|
+
return {"value": rows[0]["value"], "label": rows[0]["label"], "date": rows[0]["date"],
|
|
103
|
+
"week_ago": rows[7]["value"] if len(rows) > 7 else None, "month_ago": month,
|
|
104
|
+
"spark": [r["value"] for r in reversed(rows)], "source": "Crypto fear & greed index"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _diff(s: pd.Series, days: int) -> Optional[float]:
|
|
108
|
+
when = s.index[-1] - pd.Timedelta(days=days)
|
|
109
|
+
past = s[s.index <= when]
|
|
110
|
+
return float(s.iloc[-1] - past.iloc[-1]) if len(past) else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _row(ticker: str, label: str, group: str, note: str, s: pd.Series) -> dict:
|
|
114
|
+
spark = s[s.index >= s.index[-1] - pd.Timedelta(days=92)]
|
|
115
|
+
points = group in POINT_CHANGE
|
|
116
|
+
prev = s.iloc[-2] if len(s) > 1 else None
|
|
117
|
+
return {
|
|
118
|
+
"key": ticker, "label": label, "group": group, "note": note,
|
|
119
|
+
"price": float(s.iloc[-1]),
|
|
120
|
+
"as_of": str(pd.Timestamp(s.index[-1]).date()),
|
|
121
|
+
"change_unit": "points" if points else "%",
|
|
122
|
+
"change_1d": (float(s.iloc[-1] - prev) if points else float(s.iloc[-1] / prev - 1) * 100)
|
|
123
|
+
if prev else None,
|
|
124
|
+
"change_1m": _diff(s, 30) if points else _change(s, 30),
|
|
125
|
+
"change_1y": _diff(s, 365) if points else _change(s, 365),
|
|
126
|
+
"spark": [round(float(v), 4) for v in spark.to_numpy()],
|
|
127
|
+
"source": s.attrs.get("source"),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def term_structure(rows: list[dict]) -> Optional[dict]:
|
|
132
|
+
by = {r["key"]: r["price"] for r in rows}
|
|
133
|
+
vix, vix3m = by.get("^VIX"), by.get("^VIX3M")
|
|
134
|
+
if not vix or not vix3m:
|
|
135
|
+
return None
|
|
136
|
+
ratio = vix / vix3m
|
|
137
|
+
shape = "backwardation" if ratio > 1 else "contango"
|
|
138
|
+
return {"vix": vix, "vix3m": vix3m, "ratio": round(ratio, 3), "shape": shape,
|
|
139
|
+
"note": ("Near-term fear above longer-term: usually seen during sell-offs"
|
|
140
|
+
if shape == "backwardation" else "Calm near term: the usual shape")}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def tickers() -> list[str]:
|
|
144
|
+
return [t for t, *_ in ITEMS]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build(fetch: Fetch = _yahoo, cboe: Callable[[list[str]], dict[str, float]] = _cboe_latest,
|
|
148
|
+
fear_greed: Callable[[], Optional[dict]] = _fear_greed, use_cache: bool = True,
|
|
149
|
+
live: Optional[dict] = None) -> dict:
|
|
150
|
+
"""`live`: ticker -> {price, day} from live_market.latest, moving each
|
|
151
|
+
item's latest value; the history behind it is cached CACHE_SECONDS."""
|
|
152
|
+
from .live_market import extend_all
|
|
153
|
+
|
|
154
|
+
if use_cache and not live:
|
|
155
|
+
with _lock:
|
|
156
|
+
hit = _cache.get("all")
|
|
157
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
158
|
+
return hit[1]
|
|
159
|
+
|
|
160
|
+
raw = None
|
|
161
|
+
if use_cache:
|
|
162
|
+
with _lock:
|
|
163
|
+
hit = _raw_cache.get("all")
|
|
164
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
165
|
+
raw = hit[1]
|
|
166
|
+
if raw is None:
|
|
167
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
168
|
+
mood = pool.submit(fear_greed)
|
|
169
|
+
fresh = pool.submit(cboe, [t for t, *_ in ITEMS if t.startswith("^VIX")])
|
|
170
|
+
try:
|
|
171
|
+
data = fetch([t for t, *_ in ITEMS])
|
|
172
|
+
except Exception as exc: # network / Yahoo hiccup
|
|
173
|
+
logger.warning("World markets fetch failed: %s", exc)
|
|
174
|
+
data = {}
|
|
175
|
+
raw = (data, mood.result(), fresh.result())
|
|
176
|
+
if use_cache and raw[0]:
|
|
177
|
+
with _lock:
|
|
178
|
+
_raw_cache["all"] = (time.time(), raw)
|
|
179
|
+
data, crypto, latest = raw
|
|
180
|
+
data = dict(data)
|
|
181
|
+
|
|
182
|
+
today = pd.Timestamp.today().normalize()
|
|
183
|
+
for sym, value in latest.items(): # CBOE's value is today's; replace or append the last bar
|
|
184
|
+
s = data.get(sym)
|
|
185
|
+
if s is None or not len(s):
|
|
186
|
+
continue
|
|
187
|
+
s = s.copy()
|
|
188
|
+
if s.index[-1].normalize() >= today:
|
|
189
|
+
s.iloc[-1] = value
|
|
190
|
+
else:
|
|
191
|
+
s.loc[today] = value
|
|
192
|
+
data[sym] = s
|
|
193
|
+
if live:
|
|
194
|
+
data = extend_all(data, live)
|
|
195
|
+
|
|
196
|
+
rows = [_row(t, label, group, note, data[t]) for t, label, group, note in ITEMS
|
|
197
|
+
if t in data and len(data[t]) > 1]
|
|
198
|
+
failed = [label for t, label, *_ in ITEMS if not any(r["key"] == t for r in rows)]
|
|
199
|
+
source = "Daily closes from the market data feed; VIX from the exchange's delayed quotes; crypto fear & greed index"
|
|
200
|
+
backup = [r["label"] for r in rows if r["source"]]
|
|
201
|
+
if backup:
|
|
202
|
+
source += f"; central bank reference rates for {', '.join(backup)} (feed missing or stale)"
|
|
203
|
+
out = {"items": rows, "failed": failed, "vix_term": term_structure(rows), "crypto_mood": crypto,
|
|
204
|
+
"updated": time.strftime("%Y-%m-%dT%H:%M:%S"), "source": source}
|
|
205
|
+
if use_cache and rows and not live:
|
|
206
|
+
with _lock:
|
|
207
|
+
_cache["all"] = (time.time(), out)
|
|
208
|
+
return out
|