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,148 @@
|
|
|
1
|
+
"""Commitments of Traders: how hedge funds and producers are positioned in the
|
|
2
|
+
main US commodity futures, from the CFTC's disaggregated futures-only report
|
|
3
|
+
(publicreporting.cftc.gov, no key).
|
|
4
|
+
|
|
5
|
+
Positions are as of each Tuesday and published the following Friday. For each
|
|
6
|
+
market this gives managed money (hedge funds, CTAs) net long contracts, the
|
|
7
|
+
weekly change, net as a share of open interest, producers' / merchants' net
|
|
8
|
+
(commercial hedgers) and a 3-year position index: where this week's managed
|
|
9
|
+
money net sits between its 3-year low (0) and high (100). The reading is a
|
|
10
|
+
description of positioning, not a forecast. Brent is not covered: it trades on
|
|
11
|
+
ICE Futures Europe, outside the CFTC's report.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from datetime import date, timedelta
|
|
20
|
+
from typing import Callable, Optional
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
API = "https://publicreporting.cftc.gov/resource/72hh-3qpy.json"
|
|
27
|
+
CACHE_SECONDS = 6 * 3600
|
|
28
|
+
YEARS = 3
|
|
29
|
+
|
|
30
|
+
# Yahoo ticker on the Commodities card, label, CFTC contract market code
|
|
31
|
+
MARKETS = [
|
|
32
|
+
("GC=F", "Gold", "088691"),
|
|
33
|
+
("SI=F", "Silver", "084691"),
|
|
34
|
+
("HG=F", "Copper", "085692"),
|
|
35
|
+
("CL=F", "WTI crude", "067651"),
|
|
36
|
+
("NG=F", "Natural gas", "023651"),
|
|
37
|
+
("ZW=F", "Wheat", "001602"),
|
|
38
|
+
("ZC=F", "Corn", "002602"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
Fetch = Callable[[list[str], date], list[dict]]
|
|
42
|
+
|
|
43
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
44
|
+
_lock = threading.Lock()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _fetch(codes: list[str], since: date) -> list[dict]:
|
|
48
|
+
where = (f"cftc_contract_market_code in({','.join(repr(c) for c in codes)}) "
|
|
49
|
+
f"AND report_date_as_yyyy_mm_dd >= '{since.isoformat()}T00:00:00'")
|
|
50
|
+
params = {
|
|
51
|
+
"$select": ("cftc_contract_market_code,report_date_as_yyyy_mm_dd,open_interest_all,"
|
|
52
|
+
"m_money_positions_long_all,m_money_positions_short_all,"
|
|
53
|
+
"prod_merc_positions_long,prod_merc_positions_short"),
|
|
54
|
+
"$where": where,
|
|
55
|
+
"$order": "report_date_as_yyyy_mm_dd",
|
|
56
|
+
"$limit": 5000,
|
|
57
|
+
}
|
|
58
|
+
r = requests.get(API, params=params, timeout=30)
|
|
59
|
+
r.raise_for_status()
|
|
60
|
+
return r.json()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _int(row: dict, key: str) -> int:
|
|
64
|
+
return int(float(row.get(key) or 0))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def reading(index: Optional[float]) -> str:
|
|
68
|
+
if index is None:
|
|
69
|
+
return ""
|
|
70
|
+
if index >= 90:
|
|
71
|
+
return "Funds near their most long in 3 years"
|
|
72
|
+
if index <= 10:
|
|
73
|
+
return "Funds near their most short in 3 years"
|
|
74
|
+
if index >= 70:
|
|
75
|
+
return "Funds longer than usual"
|
|
76
|
+
if index <= 30:
|
|
77
|
+
return "Funds shorter than usual"
|
|
78
|
+
return "Fund positioning mid-range"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def summarise(rows: list[dict]) -> Optional[dict]:
|
|
82
|
+
"""One market's weekly rows (oldest first) -> latest positioning."""
|
|
83
|
+
weeks = []
|
|
84
|
+
for r in rows:
|
|
85
|
+
weeks.append({
|
|
86
|
+
"date": (r.get("report_date_as_yyyy_mm_dd") or "")[:10],
|
|
87
|
+
"oi": _int(r, "open_interest_all"),
|
|
88
|
+
"mm_net": _int(r, "m_money_positions_long_all") - _int(r, "m_money_positions_short_all"),
|
|
89
|
+
"mm_long": _int(r, "m_money_positions_long_all"),
|
|
90
|
+
"mm_short": _int(r, "m_money_positions_short_all"),
|
|
91
|
+
"prod_net": _int(r, "prod_merc_positions_long") - _int(r, "prod_merc_positions_short"),
|
|
92
|
+
})
|
|
93
|
+
weeks = sorted({w["date"]: w for w in weeks if w["date"]}.values(), key=lambda w: w["date"])
|
|
94
|
+
if not weeks:
|
|
95
|
+
return None
|
|
96
|
+
last = weeks[-1]
|
|
97
|
+
nets = [w["mm_net"] for w in weeks]
|
|
98
|
+
lo, hi = min(nets), max(nets)
|
|
99
|
+
index = round((last["mm_net"] - lo) / (hi - lo) * 100) if hi > lo and len(weeks) >= 52 else None
|
|
100
|
+
return {
|
|
101
|
+
"as_of": last["date"],
|
|
102
|
+
"open_interest": last["oi"],
|
|
103
|
+
"managed_money_long": last["mm_long"],
|
|
104
|
+
"managed_money_short": last["mm_short"],
|
|
105
|
+
"managed_money_net": last["mm_net"],
|
|
106
|
+
"net_change_1w": last["mm_net"] - weeks[-2]["mm_net"] if len(weeks) > 1 else None,
|
|
107
|
+
"net_pct_oi": round(last["mm_net"] / last["oi"] * 100, 1) if last["oi"] else None,
|
|
108
|
+
"producer_net": last["prod_net"],
|
|
109
|
+
"index_3y": index,
|
|
110
|
+
"reading": reading(index),
|
|
111
|
+
"history": [w["mm_net"] for w in weeks[-52:]],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build(fetch: Fetch = _fetch, use_cache: bool = True, today: Optional[date] = None) -> dict:
|
|
116
|
+
if use_cache:
|
|
117
|
+
with _lock:
|
|
118
|
+
hit = _cache.get("all")
|
|
119
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
120
|
+
return hit[1]
|
|
121
|
+
|
|
122
|
+
since = (today or date.today()) - timedelta(days=365 * YEARS + 7)
|
|
123
|
+
try:
|
|
124
|
+
raw = fetch([code for *_, code in MARKETS], since)
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
logger.warning("CFTC commitments of traders fetch failed: %s", exc)
|
|
127
|
+
raw = []
|
|
128
|
+
|
|
129
|
+
by_code: dict[str, list[dict]] = {}
|
|
130
|
+
for r in raw:
|
|
131
|
+
by_code.setdefault(r.get("cftc_contract_market_code", "").strip(), []).append(r)
|
|
132
|
+
|
|
133
|
+
items = []
|
|
134
|
+
for ticker, label, code in MARKETS:
|
|
135
|
+
s = summarise(by_code.get(code, []))
|
|
136
|
+
if s:
|
|
137
|
+
items.append({"key": ticker, "label": label, **s})
|
|
138
|
+
|
|
139
|
+
out = {
|
|
140
|
+
"items": items,
|
|
141
|
+
"as_of": max((i["as_of"] for i in items), default=None),
|
|
142
|
+
"source": "CFTC Commitments of Traders (disaggregated, futures only); positions as of Tuesday, "
|
|
143
|
+
"published Friday. Managed money = hedge funds and commodity trading advisers",
|
|
144
|
+
}
|
|
145
|
+
if use_cache and items:
|
|
146
|
+
with _lock:
|
|
147
|
+
_cache["all"] = (time.time(), out)
|
|
148
|
+
return out
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Country macro from the IMF World Economic Outlook (DataMapper API, no key).
|
|
2
|
+
|
|
3
|
+
Real GDP growth, inflation, unemployment, government debt and fiscal balance,
|
|
4
|
+
current account, GDP and GDP per capita for any of about 200 economies, as
|
|
5
|
+
last year / this year / next year, where this year and later are IMF
|
|
6
|
+
projections. One request per indicator returns every country, so the whole
|
|
7
|
+
set is fetched at most once a day and kept on disk; a failed refresh serves
|
|
8
|
+
the saved copy marked stale.
|
|
9
|
+
|
|
10
|
+
India's figures are fiscal years (April to March), as the IMF reports them.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
20
|
+
from datetime import date
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Callable, Optional
|
|
23
|
+
|
|
24
|
+
from marketmyna.core.home import app_home
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
API = "https://www.imf.org/external/datamapper/api/v1"
|
|
29
|
+
REFRESH_SECONDS = 24 * 3600
|
|
30
|
+
|
|
31
|
+
# IMF code, label, unit, note
|
|
32
|
+
INDICATORS = [
|
|
33
|
+
("NGDP_RPCH", "GDP growth", "%", "Real GDP, change on the year before"),
|
|
34
|
+
("PCPIPCH", "Inflation", "%", "Average consumer prices, change on the year before"),
|
|
35
|
+
("LUR", "Unemployment", "%", "Share of the labour force"),
|
|
36
|
+
("GGXWDG_NGDP", "Govt debt", "% of GDP", "General government gross debt"),
|
|
37
|
+
("GGXCNL_NGDP", "Fiscal balance", "% of GDP", "General government net lending (+) / borrowing (-)"),
|
|
38
|
+
("BCA_NGDPD", "Current account", "% of GDP", "Surplus (+) or deficit (-) with the rest of the world"),
|
|
39
|
+
("NGDPD", "GDP", "US$ bn", "Nominal GDP in current US dollars"),
|
|
40
|
+
("NGDPDPC", "GDP per person", "US$", "Nominal GDP per capita in current US dollars"),
|
|
41
|
+
]
|
|
42
|
+
DEFAULT_COUNTRIES = ["IND", "USA", "CHN", "JPN", "DEU", "GBR", "FRA", "KOR", "IDN", "BRA", "SAU", "ARE", "AUS", "CAN"]
|
|
43
|
+
FISCAL_YEAR = {"IND": "April-March fiscal years"}
|
|
44
|
+
|
|
45
|
+
Fetch = Callable[[str], dict]
|
|
46
|
+
|
|
47
|
+
_lock = threading.Lock()
|
|
48
|
+
_state: dict[str, object] = {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _dir() -> Path:
|
|
52
|
+
path = app_home() / "imf"
|
|
53
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
return path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get(path: str) -> dict:
|
|
58
|
+
# The IMF's bot filter answers 403 to some clients (browser-like user
|
|
59
|
+
# agents from scripts); curl_cffi (installed with yfinance) presents a
|
|
60
|
+
# real browser handshake and is let through.
|
|
61
|
+
from curl_cffi import requests as cffi
|
|
62
|
+
|
|
63
|
+
r = cffi.get(f"{API}/{path}", timeout=30, impersonate="chrome")
|
|
64
|
+
r.raise_for_status()
|
|
65
|
+
return r.json() or {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _download(fetch: Fetch) -> dict:
|
|
69
|
+
def one(code: str):
|
|
70
|
+
return code, ((fetch(code).get("values") or {}).get(code)) or {}
|
|
71
|
+
|
|
72
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
73
|
+
values = dict(pool.map(one, [c for c, *_ in INDICATORS]))
|
|
74
|
+
if not any(values.values()):
|
|
75
|
+
raise ValueError("IMF returned no data")
|
|
76
|
+
names = {k: v.get("label") for k, v in ((fetch("countries").get("countries")) or {}).items() if v.get("label")}
|
|
77
|
+
return {"values": values, "countries": names, "fetched": time.time()}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _data(fetch: Fetch, now: Optional[float] = None) -> tuple[dict, bool]:
|
|
81
|
+
"""(data, stale). Memory, then disk, then the IMF once a day."""
|
|
82
|
+
now = now or time.time()
|
|
83
|
+
with _lock:
|
|
84
|
+
data = _state.get("data")
|
|
85
|
+
if data is None:
|
|
86
|
+
try:
|
|
87
|
+
data = json.loads((_dir() / "weo.json").read_text(encoding="utf-8"))
|
|
88
|
+
except (OSError, ValueError):
|
|
89
|
+
data = None
|
|
90
|
+
if data is not None and now - data.get("fetched", 0) < REFRESH_SECONDS:
|
|
91
|
+
with _lock:
|
|
92
|
+
_state["data"] = data
|
|
93
|
+
return data, False
|
|
94
|
+
try:
|
|
95
|
+
fresh = _download(fetch)
|
|
96
|
+
except Exception as exc: # noqa: BLE001 - keep serving the saved copy
|
|
97
|
+
logger.warning("IMF DataMapper fetch failed: %s", exc)
|
|
98
|
+
if data is None:
|
|
99
|
+
raise
|
|
100
|
+
return data, True
|
|
101
|
+
try:
|
|
102
|
+
(_dir() / "weo.json").write_text(json.dumps(fresh), encoding="utf-8")
|
|
103
|
+
except OSError as exc:
|
|
104
|
+
logger.warning("Could not save IMF data: %s", exc)
|
|
105
|
+
with _lock:
|
|
106
|
+
_state["data"] = fresh
|
|
107
|
+
return fresh, False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build(countries: Optional[list[str]] = None, fetch: Fetch = _get, today: Optional[date] = None) -> dict:
|
|
111
|
+
year = (today or date.today()).year
|
|
112
|
+
years = [year - 1, year, year + 1]
|
|
113
|
+
data, stale = _data(fetch)
|
|
114
|
+
names: dict = data.get("countries") or {}
|
|
115
|
+
codes = [c.upper() for c in (countries or DEFAULT_COUNTRIES) if c.upper() in names][:30]
|
|
116
|
+
rows = []
|
|
117
|
+
for code in codes:
|
|
118
|
+
row = {"code": code, "name": names[code], "fiscal_year": FISCAL_YEAR.get(code)}
|
|
119
|
+
for ind, *_ in INDICATORS:
|
|
120
|
+
series = (data["values"].get(ind) or {}).get(code) or {}
|
|
121
|
+
row[ind] = [series.get(str(y)) for y in years]
|
|
122
|
+
# a 5-year look back for the growth / inflation sparkline
|
|
123
|
+
row[f"{ind}_trend"] = [{"year": y, "value": series.get(str(y))} for y in range(year - 6, year + 2)
|
|
124
|
+
if series.get(str(y)) is not None]
|
|
125
|
+
rows.append(row)
|
|
126
|
+
return {
|
|
127
|
+
"years": years,
|
|
128
|
+
"projection_from": year,
|
|
129
|
+
"indicators": [{"key": c, "label": label, "unit": unit, "note": note} for c, label, unit, note in INDICATORS],
|
|
130
|
+
"rows": rows,
|
|
131
|
+
"countries": sorted(({"code": k, "name": v} for k, v in names.items()), key=lambda c: c["name"]),
|
|
132
|
+
"stale": stale,
|
|
133
|
+
"as_of": time.strftime("%Y-%m-%d", time.localtime(data.get("fetched", time.time()))),
|
|
134
|
+
"source": "IMF World Economic Outlook (DataMapper)",
|
|
135
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""How oil-linked stocks react to crude - India, US, Hong Kong, China A.
|
|
2
|
+
|
|
3
|
+
For each stock, weekly returns over two years are regressed on the local
|
|
4
|
+
index and on crude in local currency together (India: Nifty 50 and Brent
|
|
5
|
+
in rupees; US: S&P 500 and WTI; HK: Hang Seng and Brent; China: Shanghai Composite
|
|
6
|
+
and Brent in yuan):
|
|
7
|
+
|
|
8
|
+
r_stock = a + b_mkt * r_index + b_crude * r_crude_local + e
|
|
9
|
+
|
|
10
|
+
b_crude is the crude sensitivity net of the market: a b_crude of -0.3 means a
|
|
11
|
+
10% rise in rupee Brent has gone with the stock doing about 3% worse than its
|
|
12
|
+
market exposure alone would suggest. Weekly bars sidestep the time-zone gap
|
|
13
|
+
between NSE and ICE closes. Plain numpy OLS with ordinary standard errors.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from typing import Callable, Optional
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
CACHE_SECONDS = 6 * 3600
|
|
29
|
+
BRENT, FX, NIFTY = "BZ=F", "USDINR=X", "^NSEI"
|
|
30
|
+
|
|
31
|
+
# group, why it is linked to crude, [(symbol, name)]
|
|
32
|
+
GROUPS = [
|
|
33
|
+
("Oil marketing (OMCs)", "Buy crude, sell fuel at prices the government leans on: costlier crude squeezes margins",
|
|
34
|
+
[("BPCL", "Bharat Petroleum"), ("HINDPETRO", "Hindustan Petroleum"), ("IOC", "Indian Oil")]),
|
|
35
|
+
("Upstream producers", "Sell the crude they pump: higher prices lift revenue (windfall taxes cap it)",
|
|
36
|
+
[("ONGC", "ONGC"), ("OIL", "Oil India")]),
|
|
37
|
+
("Refining & petchem", "Margins track the spread between crude and products",
|
|
38
|
+
[("RELIANCE", "Reliance Industries"), ("MRPL", "MRPL"), ("CHENNPETRO", "Chennai Petroleum")]),
|
|
39
|
+
("Paints", "Many raw materials are crude derivatives",
|
|
40
|
+
[("ASIANPAINT", "Asian Paints"), ("BERGEPAINT", "Berger Paints"), ("KANSAINER", "Kansai Nerolac")]),
|
|
41
|
+
("Aviation", "Jet fuel is the largest cost line",
|
|
42
|
+
[("INDIGO", "InterGlobe Aviation")]),
|
|
43
|
+
("Tyres", "Synthetic rubber and carbon black come from crude",
|
|
44
|
+
[("MRF", "MRF"), ("APOLLOTYRE", "Apollo Tyres"), ("CEATLTD", "CEAT")]),
|
|
45
|
+
("Gas", "City gas buys LNG priced partly off crude and sells at regulated prices; GAIL also trades it",
|
|
46
|
+
[("IGL", "Indraprastha Gas"), ("MGL", "Mahanagar Gas"), ("GAIL", "GAIL")]),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
US_GROUPS = [
|
|
50
|
+
("Integrated majors", "Pump, refine and sell: revenue rides the crude price, refining softens it",
|
|
51
|
+
[("XOM", "Exxon Mobil"), ("CVX", "Chevron")]),
|
|
52
|
+
("Producers (E&P)", "Sell the crude they pump: the purest upside to higher oil",
|
|
53
|
+
[("COP", "ConocoPhillips"), ("EOG", "EOG Resources"), ("OXY", "Occidental"), ("DVN", "Devon Energy")]),
|
|
54
|
+
("Oil services", "Drilling budgets follow the oil price with a lag",
|
|
55
|
+
[("SLB", "SLB"), ("HAL", "Halliburton"), ("BKR", "Baker Hughes")]),
|
|
56
|
+
("Refiners", "Margins track the spread between crude and fuel, not crude itself",
|
|
57
|
+
[("VLO", "Valero"), ("MPC", "Marathon Petroleum"), ("PSX", "Phillips 66")]),
|
|
58
|
+
("Airlines", "Jet fuel is one of the two biggest costs",
|
|
59
|
+
[("DAL", "Delta"), ("UAL", "United"), ("AAL", "American"), ("LUV", "Southwest")]),
|
|
60
|
+
("Cruise lines", "Bunker fuel is a big cost and higher pump prices squeeze customers",
|
|
61
|
+
[("CCL", "Carnival"), ("RCL", "Royal Caribbean"), ("NCLH", "Norwegian Cruise")]),
|
|
62
|
+
("Chemicals", "Feedstock is oil and gas derivatives",
|
|
63
|
+
[("DOW", "Dow"), ("LYB", "LyondellBasell")]),
|
|
64
|
+
("Parcel & trucking", "Diesel and jet fuel costs, partly passed on through surcharges",
|
|
65
|
+
[("UPS", "UPS"), ("FDX", "FedEx"), ("ODFL", "Old Dominion")]),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
HK_GROUPS = [
|
|
69
|
+
("Oil majors", "China's big three: upstream profits rise with crude, refining and retail cushion it",
|
|
70
|
+
[("00857", "PetroChina"), ("00386", "Sinopec"), ("00883", "CNOOC")]),
|
|
71
|
+
("Airlines", "Jet fuel is one of the largest costs",
|
|
72
|
+
[("00293", "Cathay Pacific"), ("00753", "Air China"), ("00670", "China Eastern"), ("01055", "China Southern")]),
|
|
73
|
+
("Shipping", "Bunker fuel costs; freight rates often move with energy demand",
|
|
74
|
+
[("01919", "COSCO Shipping"), ("00316", "OOIL"), ("02866", "COSCO Shipping Dev")]),
|
|
75
|
+
("City gas", "Buy gas priced partly off oil, sell at regulated tariffs",
|
|
76
|
+
[("00003", "HK & China Gas"), ("01193", "China Resources Gas"), ("02688", "ENN Energy")]),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
CHINA_GROUPS = [
|
|
80
|
+
("Oil majors", "Upstream profits rise with crude; state-set fuel prices cap the refining side",
|
|
81
|
+
[("601857", "PetroChina"), ("600028", "Sinopec"), ("600938", "CNOOC")]),
|
|
82
|
+
("Oilfield services", "Spending follows the oil price",
|
|
83
|
+
[("600871", "Sinopec Oilfield"), ("601808", "China Oilfield Services")]),
|
|
84
|
+
("Airlines", "Jet fuel is one of the largest costs",
|
|
85
|
+
[("601111", "Air China"), ("600115", "China Eastern"), ("600029", "China Southern")]),
|
|
86
|
+
("Chemicals & refining", "Crude is the feedstock",
|
|
87
|
+
[("600309", "Wanhua Chemical"), ("002493", "Rongsheng Petrochem"), ("600346", "Hengli Petrochem")]),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# market -> index, crude contract, FX to local (None = priced in USD), labels.
|
|
91
|
+
# Group symbols are the app's own (00857, 601857); yahoo_symbol maps them.
|
|
92
|
+
MARKETS = {
|
|
93
|
+
"india": {"index": NIFTY, "index_name": "Nifty 50", "crude": BRENT, "crude_name": "Brent", "fx": FX,
|
|
94
|
+
"currency": "₹", "groups": GROUPS},
|
|
95
|
+
"us": {"index": "^GSPC", "index_name": "S&P 500", "crude": "CL=F", "crude_name": "WTI", "fx": None,
|
|
96
|
+
"currency": "$", "groups": US_GROUPS},
|
|
97
|
+
"hong_kong": {"index": "^HSI", "index_name": "Hang Seng", "crude": BRENT, "crude_name": "Brent", "fx": "HKD=X",
|
|
98
|
+
"currency": "HK$", "groups": HK_GROUPS},
|
|
99
|
+
"china_a": {"index": "000001.SS", "index_name": "Shanghai Composite", "crude": BRENT, "crude_name": "Brent", "fx": "CNY=X",
|
|
100
|
+
"currency": "¥", "groups": CHINA_GROUPS},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Fetch = Callable[[list[str]], dict[str, pd.Series]]
|
|
104
|
+
|
|
105
|
+
_cache: dict[str, tuple[float, dict]] = {}
|
|
106
|
+
_lock = threading.Lock()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _yahoo(tickers: list[str]) -> dict[str, pd.Series]:
|
|
110
|
+
import yfinance as yf
|
|
111
|
+
|
|
112
|
+
from ..data import ecb_fx
|
|
113
|
+
from ..utils.yf_lock import YF_DOWNLOAD_LOCK
|
|
114
|
+
|
|
115
|
+
def download() -> dict[str, pd.Series]:
|
|
116
|
+
with YF_DOWNLOAD_LOCK:
|
|
117
|
+
df = yf.download(tickers, period="2y", interval="1d", progress=False, auto_adjust=True, threads=True)
|
|
118
|
+
closes = df["Close"]
|
|
119
|
+
return {t: closes[t].dropna() for t in tickers if t in closes and closes[t].notna().sum()}
|
|
120
|
+
|
|
121
|
+
return ecb_fx.guarded(download, tickers)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _weekly(s: pd.Series) -> pd.Series:
|
|
125
|
+
s = s.copy()
|
|
126
|
+
s.index = pd.DatetimeIndex(s.index).tz_localize(None) if getattr(s.index, "tz", None) else pd.DatetimeIndex(s.index)
|
|
127
|
+
return s.resample("W-FRI").last()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def regress(stock: pd.Series, nifty: pd.Series, crude: pd.Series) -> Optional[dict]:
|
|
131
|
+
"""Weekly two-factor OLS. Inputs are daily closes."""
|
|
132
|
+
df = pd.concat([_weekly(stock), _weekly(nifty), _weekly(crude)], axis=1, keys=["s", "m", "c"]).pct_change().dropna()
|
|
133
|
+
if len(df) < 40:
|
|
134
|
+
return None
|
|
135
|
+
y = df["s"].to_numpy()
|
|
136
|
+
x = np.column_stack([np.ones(len(df)), df["m"].to_numpy(), df["c"].to_numpy()])
|
|
137
|
+
beta, *_ = np.linalg.lstsq(x, y, rcond=None)
|
|
138
|
+
resid = y - x @ beta
|
|
139
|
+
dof = len(y) - x.shape[1]
|
|
140
|
+
s2 = float(resid @ resid) / dof
|
|
141
|
+
se = np.sqrt(np.diag(s2 * np.linalg.inv(x.T @ x)))
|
|
142
|
+
tss = float(((y - y.mean()) ** 2).sum())
|
|
143
|
+
r2 = 1 - float(resid @ resid) / tss if tss else None
|
|
144
|
+
# R2 of the market-only model, to show what crude adds
|
|
145
|
+
xm = x[:, :2]
|
|
146
|
+
bm, *_ = np.linalg.lstsq(xm, y, rcond=None)
|
|
147
|
+
rm = y - xm @ bm
|
|
148
|
+
r2_mkt = 1 - float(rm @ rm) / tss if tss else None
|
|
149
|
+
t = float(beta[2] / se[2]) if se[2] else 0.0
|
|
150
|
+
return {
|
|
151
|
+
"weeks": len(df),
|
|
152
|
+
"beta_market": float(beta[1]),
|
|
153
|
+
"beta_crude": float(beta[2]),
|
|
154
|
+
"t_crude": t,
|
|
155
|
+
"significant": abs(t) >= 2,
|
|
156
|
+
"corr_crude": float(np.corrcoef(df["s"], df["c"])[0, 1]),
|
|
157
|
+
"r2": r2,
|
|
158
|
+
"r2_added": (r2 - r2_mkt) if r2 is not None and r2_mkt is not None else None,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _change(s: pd.Series, days: int) -> Optional[float]:
|
|
163
|
+
s = s.dropna()
|
|
164
|
+
if s.empty:
|
|
165
|
+
return None
|
|
166
|
+
idx = pd.DatetimeIndex(s.index).tz_localize(None) if getattr(s.index, "tz", None) else pd.DatetimeIndex(s.index)
|
|
167
|
+
s = pd.Series(s.to_numpy(), index=idx)
|
|
168
|
+
past = s[s.index <= s.index[-1] - pd.Timedelta(days=days)]
|
|
169
|
+
return float(s.iloc[-1] / past.iloc[-1] - 1) * 100 if len(past) else None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build(fetch: Fetch = _yahoo, use_cache: bool = True, market: str = "india") -> dict:
|
|
173
|
+
cfg = MARKETS.get(market)
|
|
174
|
+
if cfg is None:
|
|
175
|
+
return {"groups": [], "market": market, "error": "Oil sensitivity is not available for this market"}
|
|
176
|
+
if use_cache:
|
|
177
|
+
with _lock:
|
|
178
|
+
hit = _cache.get(market)
|
|
179
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
180
|
+
return hit[1]
|
|
181
|
+
|
|
182
|
+
index, crude_t, fx = cfg["index"], cfg["crude"], cfg["fx"]
|
|
183
|
+
from .analysis_run import yahoo_symbol
|
|
184
|
+
|
|
185
|
+
tickers = [yahoo_symbol(sym, market) for _, _, members in cfg["groups"] for sym, _ in members]
|
|
186
|
+
tickers += [crude_t, index] + ([fx] if fx else [])
|
|
187
|
+
try:
|
|
188
|
+
data = fetch(tickers)
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
logger.warning("Crude sensitivity fetch failed: %s", exc)
|
|
191
|
+
return {"groups": [], "market": market, "error": "Price data unavailable right now"}
|
|
192
|
+
if crude_t not in data or index not in data or (fx and fx not in data):
|
|
193
|
+
return {"groups": [], "market": market, "error": f"{cfg['crude_name']}, FX or {cfg['index_name']} history unavailable"}
|
|
194
|
+
|
|
195
|
+
if fx:
|
|
196
|
+
both = pd.concat([data[crude_t], data[fx]], axis=1, keys=["b", "fx"]).ffill().dropna()
|
|
197
|
+
crude_local = both["b"] * both["fx"]
|
|
198
|
+
else:
|
|
199
|
+
crude_local = data[crude_t]
|
|
200
|
+
crude_1m = _change(crude_local, 30)
|
|
201
|
+
|
|
202
|
+
groups = []
|
|
203
|
+
for name, why, members in cfg["groups"]:
|
|
204
|
+
rows = []
|
|
205
|
+
for sym, label in members:
|
|
206
|
+
s = data.get(yahoo_symbol(sym, market))
|
|
207
|
+
if s is None:
|
|
208
|
+
continue
|
|
209
|
+
r = regress(s, data[index], crude_local)
|
|
210
|
+
if r is None:
|
|
211
|
+
continue
|
|
212
|
+
r.update(symbol=sym, name=label,
|
|
213
|
+
change_1m=_change(s, 30),
|
|
214
|
+
# what crude's last month implies on its own, holding the market flat
|
|
215
|
+
crude_implied_1m=r["beta_crude"] * crude_1m if crude_1m is not None else None)
|
|
216
|
+
rows.append(r)
|
|
217
|
+
if rows:
|
|
218
|
+
groups.append({"name": name, "why": why,
|
|
219
|
+
"avg_beta_crude": float(np.mean([r["beta_crude"] for r in rows])),
|
|
220
|
+
"stocks": rows})
|
|
221
|
+
local = "" if not fx else {"india": " in rupees", "hong_kong": " in HK dollars", "china_a": " in yuan"}[market]
|
|
222
|
+
crude_label = f"{cfg['crude_name']}{local}"
|
|
223
|
+
out = {
|
|
224
|
+
"market": market,
|
|
225
|
+
"groups": groups,
|
|
226
|
+
"crude_label": crude_label,
|
|
227
|
+
"currency": cfg["currency"],
|
|
228
|
+
"index_name": cfg["index_name"],
|
|
229
|
+
"crude": float(crude_local.iloc[-1]),
|
|
230
|
+
"crude_change_1m": crude_1m,
|
|
231
|
+
"crude_change_1y": _change(crude_local, 365),
|
|
232
|
+
"method": f"Weekly returns over 2 years, regressed on the {cfg['index_name']} and {crude_label} together.",
|
|
233
|
+
}
|
|
234
|
+
if market == "india": # older field names, kept for existing clients
|
|
235
|
+
out.update(brent_inr=out["crude"], brent_inr_change_1m=crude_1m, brent_inr_change_1y=out["crude_change_1y"])
|
|
236
|
+
if use_cache and groups:
|
|
237
|
+
with _lock:
|
|
238
|
+
_cache[market] = (time.time(), out)
|
|
239
|
+
return out
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Crypto market mood: the stock gauge's idea (marketmyna/services/market_mood.py)
|
|
2
|
+
for coins, which have no 52-week history in our feed, no index and no VIX.
|
|
3
|
+
|
|
4
|
+
Universe: the most-traded coins of the crypto screens (stablecoins dropped),
|
|
5
|
+
six months of daily closes shared with them. Readings, each 0 (fear) - 100 (greed):
|
|
6
|
+
|
|
7
|
+
- Advancing vs declining today (24h change of each coin)
|
|
8
|
+
- Share above the 50-day average
|
|
9
|
+
- Share above the 100-day average
|
|
10
|
+
- Near 90-day highs vs lows (within 5% of either; share that are highs)
|
|
11
|
+
- Bitcoin vs its 125-day average (-20% -> 0, +20% -> 100; coins swing more than indices)
|
|
12
|
+
- Bitcoin's 30-day volatility vs its past months (calm = high score)
|
|
13
|
+
- Perpetual funding (BTC and ETH, annualised): about 11%/yr is the usual
|
|
14
|
+
baseline; crowded longs push it up (greed), crowded shorts below zero (fear)
|
|
15
|
+
|
|
16
|
+
Describes how the market behaves now; not a forecast or advice.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import math
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from typing import Callable, Optional
|
|
26
|
+
|
|
27
|
+
import pandas as pd
|
|
28
|
+
|
|
29
|
+
from . import crypto_screens
|
|
30
|
+
from .market_mood import _clip, label
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
CACHE_SECONDS = 300
|
|
35
|
+
FUNDING_BASELINE = 11.0 # % / yr: 0.01% per 8h, the exchanges' default
|
|
36
|
+
|
|
37
|
+
_cache: dict[str, tuple[float, Optional[float]]] = {}
|
|
38
|
+
_lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _funding() -> Optional[float]:
|
|
42
|
+
"""Average annualised BTC / ETH perpetual funding in %, or None."""
|
|
43
|
+
from .rates import _funding_binance, _funding_bybit
|
|
44
|
+
|
|
45
|
+
for fn in (_funding_binance, _funding_bybit):
|
|
46
|
+
try:
|
|
47
|
+
rows = [r for r in fn() if r["coin"] in ("BTC", "ETH")]
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
logger.warning("Funding for crypto mood via %s failed: %s", fn.__name__, exc)
|
|
50
|
+
continue
|
|
51
|
+
if rows:
|
|
52
|
+
return sum(r["rate_8h"] for r in rows) / len(rows) * 3 * 365 * 100
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def readings(coins: dict[str, pd.Series], changes: list[float], btc: Optional[pd.Series],
|
|
57
|
+
funding: Optional[float]) -> list[dict]:
|
|
58
|
+
out = []
|
|
59
|
+
adv = sum(1 for c in changes if c > 0)
|
|
60
|
+
dec = sum(1 for c in changes if c < 0)
|
|
61
|
+
if adv + dec:
|
|
62
|
+
out.append({"key": "breadth", "label": "Advancing vs declining (24h)", "score": _clip(adv / (adv + dec) * 100),
|
|
63
|
+
"value": f"{adv} up, {dec} down"})
|
|
64
|
+
|
|
65
|
+
for days in (50, 100):
|
|
66
|
+
ok = [s for s in coins.values() if len(s) >= days]
|
|
67
|
+
if len(ok) >= 20:
|
|
68
|
+
above = sum(1 for s in ok if s.iloc[-1] > s.iloc[-days:].mean())
|
|
69
|
+
out.append({"key": f"above_{days}", "label": f"Above the {days}-day average", "score": _clip(above / len(ok) * 100),
|
|
70
|
+
"value": f"{above} of {len(ok)} ({above / len(ok):.0%})"})
|
|
71
|
+
|
|
72
|
+
ok = [s for s in coins.values() if len(s) >= 90]
|
|
73
|
+
if len(ok) >= 20:
|
|
74
|
+
highs = sum(1 for s in ok if s.iloc[-1] >= s.iloc[-90:].max() * 0.95)
|
|
75
|
+
lows = sum(1 for s in ok if s.iloc[-1] <= s.iloc[-90:].min() * 1.05)
|
|
76
|
+
out.append({"key": "highs_lows", "label": "Near 90-day highs vs lows",
|
|
77
|
+
"score": _clip(highs / (highs + lows) * 100) if highs + lows else 50.0,
|
|
78
|
+
"value": f"{highs} near highs, {lows} near lows" if highs + lows else "none near a high or a low"})
|
|
79
|
+
|
|
80
|
+
if btc is not None and len(btc) >= 125:
|
|
81
|
+
gap = float(btc.iloc[-1] / btc.iloc[-125:].mean() - 1)
|
|
82
|
+
out.append({"key": "momentum", "label": "Bitcoin vs its 125-day average", "score": _clip(50 + gap * 250),
|
|
83
|
+
"value": f"{gap:+.1%}"})
|
|
84
|
+
|
|
85
|
+
if btc is not None and len(btc) >= 90:
|
|
86
|
+
vol = btc.pct_change().rolling(30).std().dropna() * math.sqrt(365) * 100
|
|
87
|
+
if len(vol) >= 30:
|
|
88
|
+
now = float(vol.iloc[-1])
|
|
89
|
+
rank = float((vol < now).mean())
|
|
90
|
+
out.append({"key": "volatility", "label": "Bitcoin volatility vs recent months", "score": _clip((1 - rank) * 100),
|
|
91
|
+
"value": f"{now:.0f}%/yr, higher than {rank:.0%} of the period"})
|
|
92
|
+
|
|
93
|
+
if funding is not None:
|
|
94
|
+
out.append({"key": "funding", "label": "Funding rates (BTC, ETH)", "score": _clip(50 + (funding - FUNDING_BASELINE) * 2.5),
|
|
95
|
+
"value": f"{funding:+.1f}% / yr"})
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _funding_cached(funding: Callable[[], Optional[float]], use_cache: bool) -> Optional[float]:
|
|
100
|
+
if use_cache:
|
|
101
|
+
with _lock:
|
|
102
|
+
hit = _cache.get("funding")
|
|
103
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
104
|
+
return hit[1]
|
|
105
|
+
value = funding()
|
|
106
|
+
if use_cache and value is not None:
|
|
107
|
+
with _lock:
|
|
108
|
+
_cache["funding"] = (time.time(), value)
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build(rows: Optional[list[dict]] = None, fetch: Callable = crypto_screens._fetch,
|
|
113
|
+
funding: Callable[[], Optional[float]] = _funding, use_cache: bool = True) -> dict:
|
|
114
|
+
"""`rows` = the crypto listing, ideally with live prices merged in: today's
|
|
115
|
+
breadth follows them on every call; closes and funding are cached."""
|
|
116
|
+
if rows is None:
|
|
117
|
+
from ..data.listings import get_store
|
|
118
|
+
|
|
119
|
+
rows = get_store().rows("crypto")
|
|
120
|
+
coins = crypto_screens.universe(rows)
|
|
121
|
+
closes = crypto_screens.history(coins, fetch, use_cache)
|
|
122
|
+
series = {c["symbol"]: closes[c["symbol"]] for c in coins if c["symbol"] in closes}
|
|
123
|
+
changes = [float(c["change_percent"]) for c in coins if c.get("change_percent") is not None]
|
|
124
|
+
parts = readings(series, changes, closes.get("BTC-USD"), _funding_cached(funding, use_cache))
|
|
125
|
+
score = round(sum(p["score"] for p in parts) / len(parts), 1) if parts else None
|
|
126
|
+
for p in parts:
|
|
127
|
+
p["score"] = round(p["score"], 1)
|
|
128
|
+
p["label_text"] = label(p["score"])
|
|
129
|
+
out = {
|
|
130
|
+
"market": "crypto", "score": score, "label": label(score), "readings": parts,
|
|
131
|
+
"universe": f"the {len(series)} most-traded coins (stablecoins excluded)",
|
|
132
|
+
"index": "Bitcoin",
|
|
133
|
+
"source": "Daily closes from crypto exchange data; today's moves and funding from live exchange data",
|
|
134
|
+
}
|
|
135
|
+
return out
|