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,224 @@
|
|
|
1
|
+
"""Relative rotation (RRG): where each sector sits against the market index on
|
|
2
|
+
relative strength (RS-Ratio) and its momentum (RS-Momentum), with a weekly tail.
|
|
3
|
+
|
|
4
|
+
Yahoo lacks most NSE sector indices, so every sector is a market-cap-weighted
|
|
5
|
+
basket of its largest stocks (by our listings category), priced from the
|
|
6
|
+
scanner's batched daily bars. The JdK-style normalisation follows OpenBB's
|
|
7
|
+
technical.relative_rotation (Apache 2.0).
|
|
8
|
+
|
|
9
|
+
Quadrants: leading (strong, rising) -> weakening -> lagging -> improving.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import Callable, Optional
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
ROTATION_MARKETS = ("india", "us", "china_a", "hong_kong", "crypto")
|
|
25
|
+
|
|
26
|
+
# Crypto has no sectors in the listings; these are the usual narrative
|
|
27
|
+
# groups, each an equal-weight basket measured against Bitcoin.
|
|
28
|
+
CRYPTO_GROUPS = {
|
|
29
|
+
"Smart-contract L1s": ["ETH", "SOL", "ADA", "AVAX", "DOT", "NEAR", "APT", "SUI", "TON", "TRX"],
|
|
30
|
+
"Layer 2s": ["ARB", "OP", "POL", "STRK", "IMX", "MNT"],
|
|
31
|
+
"DeFi": ["UNI", "AAVE", "LDO", "CRV", "COMP", "PENDLE", "JUP", "RAY"],
|
|
32
|
+
"Payments & legacy": ["XRP", "LTC", "BCH", "XLM", "ETC"],
|
|
33
|
+
"Exchange tokens": ["BNB", "OKB", "CRO"],
|
|
34
|
+
"Oracles & infra": ["LINK", "FIL", "GRT", "QNT", "HBAR"],
|
|
35
|
+
"AI": ["FET", "RENDER", "TAO", "WLD", "AR"],
|
|
36
|
+
"Memecoins": ["DOGE", "SHIB", "PEPE", "BONK", "WIF", "FLOKI"],
|
|
37
|
+
"Gaming & metaverse": ["SAND", "MANA", "AXS", "GALA"],
|
|
38
|
+
}
|
|
39
|
+
MEMBERS = 8
|
|
40
|
+
WINDOW = 10 # weeks used to normalise
|
|
41
|
+
TAIL = 8 # weeks of trail shown
|
|
42
|
+
CACHE_SECONDS = 1800
|
|
43
|
+
|
|
44
|
+
Fetch = Callable[[list[str]], dict[str, pd.Series]]
|
|
45
|
+
|
|
46
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
47
|
+
_closes_cache: dict[str, tuple[float, dict[str, pd.Series], set[str]]] = {}
|
|
48
|
+
_lock = threading.Lock()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _closes(tickers: list[str]) -> dict[str, pd.Series]:
|
|
52
|
+
from ..trading.scanner import _cache as bars
|
|
53
|
+
|
|
54
|
+
return {t: df["close"].astype(float) for t, df in bars.get(tickers, "1d", "1y").items()}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _weekly(s: pd.Series) -> pd.Series:
|
|
58
|
+
s = s.dropna().copy()
|
|
59
|
+
s.index = pd.DatetimeIndex(s.index).tz_localize(None)
|
|
60
|
+
return s.resample("W-FRI").last().dropna()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def rrg(series: pd.Series, bench: pd.Series, window: int = WINDOW) -> pd.DataFrame:
|
|
64
|
+
"""Weekly RS-Ratio / RS-Momentum (both centred on 100) of `series` vs `bench`."""
|
|
65
|
+
both = pd.concat([series, bench], axis=1).dropna()
|
|
66
|
+
rs = both.iloc[:, 0] / both.iloc[:, 1]
|
|
67
|
+
z = lambda s: (s - s.rolling(window).mean()) / s.rolling(window).std() # noqa: E731
|
|
68
|
+
ratio = 100 + z(rs)
|
|
69
|
+
momentum = 100 + z(ratio.pct_change() * 100)
|
|
70
|
+
return pd.DataFrame({"ratio": ratio, "momentum": momentum}).dropna()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def quadrant(ratio: float, momentum: float) -> str:
|
|
74
|
+
if ratio >= 100:
|
|
75
|
+
return "leading" if momentum >= 100 else "weakening"
|
|
76
|
+
return "improving" if momentum >= 100 else "lagging"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def baskets(rows: list[dict], members: int = MEMBERS) -> dict[str, list[dict]]:
|
|
80
|
+
"""category -> its largest priced stocks (with market cap)."""
|
|
81
|
+
out: dict[str, list[dict]] = {}
|
|
82
|
+
for r in sorted((r for r in rows if r.get("type") == "stock" and r.get("market_cap")), key=lambda r: -r["market_cap"]):
|
|
83
|
+
cat = r.get("category")
|
|
84
|
+
if not cat or cat == "Other":
|
|
85
|
+
continue
|
|
86
|
+
group = out.setdefault(cat, [])
|
|
87
|
+
if len(group) < members:
|
|
88
|
+
group.append(r)
|
|
89
|
+
return {k: v for k, v in out.items() if len(v) >= 3}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _crypto_closes(tickers: list[str]) -> dict[str, pd.Series]:
|
|
93
|
+
from .crypto_screens import _fetch
|
|
94
|
+
|
|
95
|
+
return _fetch(tickers, days=400)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cached_closes(key: str, tickers: list[str], fetch: Fetch, use_cache: bool) -> dict[str, pd.Series]:
|
|
99
|
+
"""Daily closes, kept CACHE_SECONDS: live prices only move the last bar,
|
|
100
|
+
so the year of history is fetched once and re-used."""
|
|
101
|
+
if use_cache:
|
|
102
|
+
with _lock:
|
|
103
|
+
hit = _closes_cache.get(key)
|
|
104
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS and set(tickers) <= hit[2]:
|
|
105
|
+
return hit[1]
|
|
106
|
+
closes = fetch(tickers)
|
|
107
|
+
if use_cache and closes:
|
|
108
|
+
with _lock:
|
|
109
|
+
_closes_cache[key] = (time.time(), closes, set(tickers))
|
|
110
|
+
return closes
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_crypto(fetch: Fetch = _crypto_closes, use_cache: bool = True, live: Optional[dict] = None) -> dict:
|
|
114
|
+
"""Coin groups vs Bitcoin, same output shape as build(). `live`: ticker ->
|
|
115
|
+
{price, day} moving today's close."""
|
|
116
|
+
from .live_market import extend_all
|
|
117
|
+
|
|
118
|
+
if use_cache and not live:
|
|
119
|
+
with _lock:
|
|
120
|
+
hit = _cache.get("crypto")
|
|
121
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
122
|
+
return hit[1]
|
|
123
|
+
tickers = ["BTC-USD"] + [f"{c}-USD" for coins in CRYPTO_GROUPS.values() for c in coins]
|
|
124
|
+
closes = _cached_closes("crypto", list(dict.fromkeys(tickers)), fetch, use_cache)
|
|
125
|
+
if live:
|
|
126
|
+
closes = extend_all(closes, live)
|
|
127
|
+
if "BTC-USD" not in closes:
|
|
128
|
+
raise ValueError("Bitcoin price history unavailable")
|
|
129
|
+
bench = _weekly(closes["BTC-USD"])
|
|
130
|
+
sectors = []
|
|
131
|
+
for group, coins in CRYPTO_GROUPS.items():
|
|
132
|
+
parts, members = [], []
|
|
133
|
+
for c in coins:
|
|
134
|
+
s = closes.get(f"{c}-USD")
|
|
135
|
+
if s is None or len(s.dropna()) < 120:
|
|
136
|
+
continue
|
|
137
|
+
w = _weekly(s)
|
|
138
|
+
parts.append(w / w.iloc[0])
|
|
139
|
+
members.append(f"{c}-USD")
|
|
140
|
+
if len(parts) < 2:
|
|
141
|
+
continue
|
|
142
|
+
basket = pd.concat(parts, axis=1).ffill().dropna().mean(axis=1) # equal weight
|
|
143
|
+
path = rrg(basket, bench)
|
|
144
|
+
if path.empty:
|
|
145
|
+
continue
|
|
146
|
+
tail = path.tail(TAIL)
|
|
147
|
+
last = tail.iloc[-1]
|
|
148
|
+
sectors.append({
|
|
149
|
+
"sector": group, "members": members,
|
|
150
|
+
"ratio": float(last["ratio"]), "momentum": float(last["momentum"]),
|
|
151
|
+
"quadrant": quadrant(float(last["ratio"]), float(last["momentum"])),
|
|
152
|
+
"return_4w": float(basket.iloc[-1] / basket.iloc[-5] - 1) * 100 if len(basket) > 5 else None,
|
|
153
|
+
"tail": [{"week": str(i.date()), "ratio": float(v["ratio"]), "momentum": float(v["momentum"])} for i, v in tail.iterrows()],
|
|
154
|
+
})
|
|
155
|
+
order = {"leading": 0, "improving": 1, "weakening": 2, "lagging": 3}
|
|
156
|
+
sectors.sort(key=lambda s: (order[s["quadrant"]], -s["ratio"]))
|
|
157
|
+
out = {"market": "crypto", "benchmark": {"symbol": "BTC-USD", "name": "Bitcoin"},
|
|
158
|
+
"as_of": str(bench.index[-1].date()), "window_weeks": WINDOW, "sectors": sectors}
|
|
159
|
+
if use_cache and sectors and not live:
|
|
160
|
+
with _lock:
|
|
161
|
+
_cache["crypto"] = (time.time(), out)
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build(market: str, rows: Optional[list[dict]] = None, fetch: Fetch = _closes, live: Optional[dict] = None) -> dict:
|
|
166
|
+
"""`live`: Yahoo ticker -> {price, day} for the basket members and the
|
|
167
|
+
benchmark; today's close moves with it, so the current week's point (and
|
|
168
|
+
its quadrant) follows the session."""
|
|
169
|
+
if market == "crypto":
|
|
170
|
+
return build_crypto(live=live)
|
|
171
|
+
from ..data.listings import get_store
|
|
172
|
+
from .analysis_run import MARKET_INDICES, yahoo_symbol
|
|
173
|
+
from .live_market import extend_all
|
|
174
|
+
|
|
175
|
+
if rows is None and not live:
|
|
176
|
+
with _lock:
|
|
177
|
+
hit = _cache.get(market)
|
|
178
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
179
|
+
return hit[1]
|
|
180
|
+
groups = baskets(rows if rows is not None else get_store().rows(market))
|
|
181
|
+
bench_ticker, bench_name = MARKET_INDICES[market][0]
|
|
182
|
+
tickers = {r["symbol"]: yahoo_symbol(r["symbol"], market) for g in groups.values() for r in g}
|
|
183
|
+
closes = _cached_closes(market, list(dict.fromkeys([*tickers.values(), bench_ticker])), fetch, rows is None)
|
|
184
|
+
if live:
|
|
185
|
+
closes = extend_all(closes, live)
|
|
186
|
+
if bench_ticker not in closes:
|
|
187
|
+
raise ValueError(f"Benchmark {bench_name} unavailable")
|
|
188
|
+
bench = _weekly(closes[bench_ticker])
|
|
189
|
+
|
|
190
|
+
sectors = []
|
|
191
|
+
for cat, members in groups.items():
|
|
192
|
+
parts, weights = [], []
|
|
193
|
+
for r in members:
|
|
194
|
+
s = closes.get(tickers[r["symbol"]])
|
|
195
|
+
if s is None or len(s.dropna()) < 120:
|
|
196
|
+
continue
|
|
197
|
+
w = _weekly(s)
|
|
198
|
+
parts.append(w / w.iloc[0])
|
|
199
|
+
weights.append(float(r["market_cap"]))
|
|
200
|
+
if len(parts) < 3:
|
|
201
|
+
continue
|
|
202
|
+
frame = pd.concat(parts, axis=1).ffill().dropna()
|
|
203
|
+
basket = frame.mul(np.array(weights) / sum(weights), axis=1).sum(axis=1)
|
|
204
|
+
path = rrg(basket, bench)
|
|
205
|
+
if path.empty:
|
|
206
|
+
continue
|
|
207
|
+
tail = path.tail(TAIL)
|
|
208
|
+
last = tail.iloc[-1]
|
|
209
|
+
sectors.append({
|
|
210
|
+
"sector": cat,
|
|
211
|
+
"members": [r["symbol"] for r in members],
|
|
212
|
+
"ratio": float(last["ratio"]), "momentum": float(last["momentum"]),
|
|
213
|
+
"quadrant": quadrant(float(last["ratio"]), float(last["momentum"])),
|
|
214
|
+
"return_4w": float(basket.iloc[-1] / basket.iloc[-5] - 1) * 100 if len(basket) > 5 else None,
|
|
215
|
+
"tail": [{"week": str(i.date()), "ratio": float(v["ratio"]), "momentum": float(v["momentum"])} for i, v in tail.iterrows()],
|
|
216
|
+
})
|
|
217
|
+
order = {"leading": 0, "improving": 1, "weakening": 2, "lagging": 3}
|
|
218
|
+
sectors.sort(key=lambda s: (order[s["quadrant"]], -s["ratio"]))
|
|
219
|
+
out = {"market": market, "benchmark": {"symbol": bench_ticker, "name": bench_name},
|
|
220
|
+
"as_of": str(bench.index[-1].date()), "window_weeks": WINDOW, "sectors": sectors}
|
|
221
|
+
if rows is None and not live:
|
|
222
|
+
with _lock:
|
|
223
|
+
_cache[market] = (time.time(), out)
|
|
224
|
+
return out
|