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.
Files changed (344) hide show
  1. marketmyna/__init__.py +14 -0
  2. marketmyna/agent/__init__.py +8 -0
  3. marketmyna/agent/agents.py +543 -0
  4. marketmyna/agent/context.py +69 -0
  5. marketmyna/agent/dashboard.py +55 -0
  6. marketmyna/agent/orchestrator.py +153 -0
  7. marketmyna/agent/sentiment.py +150 -0
  8. marketmyna/agent/skills.py +119 -0
  9. marketmyna/ai/__init__.py +1 -0
  10. marketmyna/ai/portfolio_optimizer.py +330 -0
  11. marketmyna/analysis/__init__.py +6 -0
  12. marketmyna/analysis/fundamental.py +329 -0
  13. marketmyna/analysis/sip_optimizer.py +81 -0
  14. marketmyna/analysis/technical.py +321 -0
  15. marketmyna/api/__init__.py +1 -0
  16. marketmyna/api/app.py +209 -0
  17. marketmyna/api/deps.py +63 -0
  18. marketmyna/api/middlewares/__init__.py +0 -0
  19. marketmyna/api/middlewares/auth.py +116 -0
  20. marketmyna/api/middlewares/csrf.py +66 -0
  21. marketmyna/api/middlewares/error_handler.py +38 -0
  22. marketmyna/api/middlewares/slashes.py +27 -0
  23. marketmyna/api/v1/__init__.py +0 -0
  24. marketmyna/api/v1/endpoints/__init__.py +0 -0
  25. marketmyna/api/v1/endpoints/account.py +166 -0
  26. marketmyna/api/v1/endpoints/agent.py +114 -0
  27. marketmyna/api/v1/endpoints/bot.py +38 -0
  28. marketmyna/api/v1/endpoints/calendar.py +269 -0
  29. marketmyna/api/v1/endpoints/data_sources.py +174 -0
  30. marketmyna/api/v1/endpoints/health.py +10 -0
  31. marketmyna/api/v1/endpoints/listings.py +564 -0
  32. marketmyna/api/v1/endpoints/macro.py +163 -0
  33. marketmyna/api/v1/endpoints/portfolio.py +530 -0
  34. marketmyna/api/v1/endpoints/push.py +119 -0
  35. marketmyna/api/v1/endpoints/reports.py +49 -0
  36. marketmyna/api/v1/endpoints/scheduler.py +219 -0
  37. marketmyna/api/v1/endpoints/stocks.py +400 -0
  38. marketmyna/api/v1/endpoints/system_config.py +157 -0
  39. marketmyna/api/v1/endpoints/thesis.py +112 -0
  40. marketmyna/api/v1/endpoints/trade.py +709 -0
  41. marketmyna/api/v1/endpoints/updates.py +394 -0
  42. marketmyna/api/v1/endpoints/watchlists.py +179 -0
  43. marketmyna/api/v1/router.py +43 -0
  44. marketmyna/bot/__init__.py +14 -0
  45. marketmyna/bot/commands/__init__.py +0 -0
  46. marketmyna/bot/commands/analyze.py +40 -0
  47. marketmyna/bot/commands/ask.py +51 -0
  48. marketmyna/bot/commands/base.py +35 -0
  49. marketmyna/bot/commands/batch.py +53 -0
  50. marketmyna/bot/commands/common.py +84 -0
  51. marketmyna/bot/commands/help.py +43 -0
  52. marketmyna/bot/commands/markets.py +153 -0
  53. marketmyna/bot/commands/portfolio.py +145 -0
  54. marketmyna/bot/commands/research.py +323 -0
  55. marketmyna/bot/commands/status.py +48 -0
  56. marketmyna/bot/commands/trade.py +176 -0
  57. marketmyna/bot/dispatcher.py +95 -0
  58. marketmyna/bot/formatting.py +25 -0
  59. marketmyna/bot/models.py +27 -0
  60. marketmyna/bot/registry.py +50 -0
  61. marketmyna/bot/telegram_listener.py +227 -0
  62. marketmyna/cli/__init__.py +1 -0
  63. marketmyna/cli/scheduled_run.py +292 -0
  64. marketmyna/core/__init__.py +6 -0
  65. marketmyna/core/compliance.py +53 -0
  66. marketmyna/core/config.py +377 -0
  67. marketmyna/core/constants.py +69 -0
  68. marketmyna/core/home.py +22 -0
  69. marketmyna/core/numbers.py +28 -0
  70. marketmyna/core/secret_store.py +104 -0
  71. marketmyna/core/tenant.py +130 -0
  72. marketmyna/data/__init__.py +8 -0
  73. marketmyna/data/base.py +36 -0
  74. marketmyna/data/categories.py +272 -0
  75. marketmyna/data/china.py +360 -0
  76. marketmyna/data/crypto.py +200 -0
  77. marketmyna/data/ecb_fx.py +131 -0
  78. marketmyna/data/extra_sources.py +569 -0
  79. marketmyna/data/india.py +241 -0
  80. marketmyna/data/india_filings.py +343 -0
  81. marketmyna/data/listings.py +1314 -0
  82. marketmyna/data/mutual_funds.py +34 -0
  83. marketmyna/data/news.py +98 -0
  84. marketmyna/data/news_search.py +346 -0
  85. marketmyna/data/nse_deals.py +138 -0
  86. marketmyna/data/resilience.py +173 -0
  87. marketmyna/data/sec_facts.py +252 -0
  88. marketmyna/data/shariah.py +283 -0
  89. marketmyna/data/social/__init__.py +9 -0
  90. marketmyna/data/social/base.py +100 -0
  91. marketmyna/data/social/browser.py +92 -0
  92. marketmyna/data/social/hackernews.py +53 -0
  93. marketmyna/data/social/reddit.py +397 -0
  94. marketmyna/data/social/stocktwits.py +63 -0
  95. marketmyna/data/social/valuepickr.py +53 -0
  96. marketmyna/data/stocks.py +197 -0
  97. marketmyna/data/yahoo_price.py +48 -0
  98. marketmyna/launcher.py +71 -0
  99. marketmyna/llm/__init__.py +33 -0
  100. marketmyna/llm/backend_factory.py +51 -0
  101. marketmyna/llm/backend_registry.py +19 -0
  102. marketmyna/llm/channels.py +447 -0
  103. marketmyna/llm/json_reply.py +63 -0
  104. marketmyna/llm/litellm_backend.py +197 -0
  105. marketmyna/llm/model_catalog.py +166 -0
  106. marketmyna/llm/model_health.py +193 -0
  107. marketmyna/llm/protocol.py +111 -0
  108. marketmyna/llm/providers.py +128 -0
  109. marketmyna/llm/store.py +128 -0
  110. marketmyna/notifiers/__init__.py +35 -0
  111. marketmyna/notifiers/apprise_sender.py +43 -0
  112. marketmyna/notifiers/astrbot.py +47 -0
  113. marketmyna/notifiers/bark.py +24 -0
  114. marketmyna/notifiers/base.py +80 -0
  115. marketmyna/notifiers/dingtalk.py +54 -0
  116. marketmyna/notifiers/discord.py +39 -0
  117. marketmyna/notifiers/dispatcher.py +153 -0
  118. marketmyna/notifiers/email.py +99 -0
  119. marketmyna/notifiers/feishu.py +57 -0
  120. marketmyna/notifiers/googlechat.py +24 -0
  121. marketmyna/notifiers/gotify.py +36 -0
  122. marketmyna/notifiers/matrix.py +28 -0
  123. marketmyna/notifiers/ntfy.py +36 -0
  124. marketmyna/notifiers/pushbullet.py +24 -0
  125. marketmyna/notifiers/pushover.py +41 -0
  126. marketmyna/notifiers/pushplus.py +41 -0
  127. marketmyna/notifiers/serverchan3.py +40 -0
  128. marketmyna/notifiers/settings_schema.py +119 -0
  129. marketmyna/notifiers/signal_messenger.py +25 -0
  130. marketmyna/notifiers/slack.py +36 -0
  131. marketmyna/notifiers/teams.py +37 -0
  132. marketmyna/notifiers/telegram.py +81 -0
  133. marketmyna/notifiers/twilio.py +33 -0
  134. marketmyna/notifiers/webhook.py +44 -0
  135. marketmyna/notifiers/webpush.py +222 -0
  136. marketmyna/notifiers/wechat.py +40 -0
  137. marketmyna/notifiers/whatsapp.py +27 -0
  138. marketmyna/portfolio/__init__.py +6 -0
  139. marketmyna/portfolio/analytics.py +336 -0
  140. marketmyna/portfolio/manager.py +383 -0
  141. marketmyna/portfolio/models.py +155 -0
  142. marketmyna/portfolio/optimizers.py +88 -0
  143. marketmyna/portfolio/review.py +411 -0
  144. marketmyna/services/__init__.py +1 -0
  145. marketmyna/services/accounts.py +278 -0
  146. marketmyna/services/ai_access.py +94 -0
  147. marketmyna/services/analysis_run.py +491 -0
  148. marketmyna/services/big_trades.py +335 -0
  149. marketmyna/services/brokers/__init__.py +0 -0
  150. marketmyna/services/brokers/angel.py +271 -0
  151. marketmyna/services/brokers/finnhub.py +209 -0
  152. marketmyna/services/byok_detect.py +196 -0
  153. marketmyna/services/calendar.py +748 -0
  154. marketmyna/services/central_banks.py +202 -0
  155. marketmyna/services/commodities.py +134 -0
  156. marketmyna/services/compare.py +307 -0
  157. marketmyna/services/cot.py +148 -0
  158. marketmyna/services/country_macro.py +135 -0
  159. marketmyna/services/crude_sensitivity.py +239 -0
  160. marketmyna/services/crypto_mood.py +135 -0
  161. marketmyna/services/crypto_screens.py +226 -0
  162. marketmyna/services/data_export.py +91 -0
  163. marketmyna/services/dcf.py +220 -0
  164. marketmyna/services/dcf_model.py +194 -0
  165. marketmyna/services/delivery.py +142 -0
  166. marketmyna/services/discovery.py +146 -0
  167. marketmyna/services/dividends.py +95 -0
  168. marketmyna/services/earnings.py +305 -0
  169. marketmyna/services/econ_calendar.py +495 -0
  170. marketmyna/services/eia_stocks.py +115 -0
  171. marketmyna/services/estimates.py +109 -0
  172. marketmyna/services/etf.py +236 -0
  173. marketmyna/services/etf_rotation.py +193 -0
  174. marketmyna/services/etf_screens.py +196 -0
  175. marketmyna/services/financials.py +639 -0
  176. marketmyna/services/fund_ai.py +181 -0
  177. marketmyna/services/fund_analytics.py +371 -0
  178. marketmyna/services/fund_cost.py +147 -0
  179. marketmyna/services/fund_holdings.py +664 -0
  180. marketmyna/services/fund_peers.py +247 -0
  181. marketmyna/services/fund_rotation.py +206 -0
  182. marketmyna/services/fund_style.py +179 -0
  183. marketmyna/services/funds.py +70 -0
  184. marketmyna/services/hot_stocks.py +358 -0
  185. marketmyna/services/india_flows.py +263 -0
  186. marketmyna/services/india_rates.py +258 -0
  187. marketmyna/services/investor_lenses.py +271 -0
  188. marketmyna/services/leadership.py +165 -0
  189. marketmyna/services/live_market.py +497 -0
  190. marketmyna/services/live_quotes.py +295 -0
  191. marketmyna/services/macro.py +182 -0
  192. marketmyna/services/market_calendar.py +399 -0
  193. marketmyna/services/market_mood.py +168 -0
  194. marketmyna/services/market_valuation.py +231 -0
  195. marketmyna/services/mf_holdings.py +199 -0
  196. marketmyna/services/mf_insights.py +132 -0
  197. marketmyna/services/mf_lumpsum.py +587 -0
  198. marketmyna/services/mf_portfolio_ai.py +195 -0
  199. marketmyna/services/nse_events.py +98 -0
  200. marketmyna/services/overview.py +366 -0
  201. marketmyna/services/ownership.py +143 -0
  202. marketmyna/services/quality_scores.py +199 -0
  203. marketmyna/services/rates.py +335 -0
  204. marketmyna/services/reconcile.py +343 -0
  205. marketmyna/services/report.py +584 -0
  206. marketmyna/services/report_i18n.py +678 -0
  207. marketmyna/services/rotation.py +224 -0
  208. marketmyna/services/runtime_scheduler.py +548 -0
  209. marketmyna/services/schedule_config.py +251 -0
  210. marketmyna/services/schedule_time.py +39 -0
  211. marketmyna/services/screens.py +398 -0
  212. marketmyna/services/sec_mdna.py +173 -0
  213. marketmyna/services/shariah.py +246 -0
  214. marketmyna/services/sip.py +148 -0
  215. marketmyna/services/sip_days.py +362 -0
  216. marketmyna/services/smart_file.py +595 -0
  217. marketmyna/services/smart_pdf.py +425 -0
  218. marketmyna/services/smart_report.py +370 -0
  219. marketmyna/services/social_sentiment.py +511 -0
  220. marketmyna/services/thesis.py +328 -0
  221. marketmyna/services/translate.py +146 -0
  222. marketmyna/services/valuation.py +173 -0
  223. marketmyna/services/worker_pool.py +149 -0
  224. marketmyna/services/world_markets.py +208 -0
  225. marketmyna/storage.py +722 -0
  226. marketmyna/strategies/README.md +51 -0
  227. marketmyna/strategies/bottom_volume.yaml +49 -0
  228. marketmyna/strategies/box_oscillation.yaml +69 -0
  229. marketmyna/strategies/bull_trend.yaml +49 -0
  230. marketmyna/strategies/chan_theory.yaml +58 -0
  231. marketmyna/strategies/dragon_head.yaml +44 -0
  232. marketmyna/strategies/emotion_cycle.yaml +79 -0
  233. marketmyna/strategies/event_driven.yaml +54 -0
  234. marketmyna/strategies/expectation_repricing.yaml +54 -0
  235. marketmyna/strategies/growth_quality.yaml +54 -0
  236. marketmyna/strategies/hot_theme.yaml +54 -0
  237. marketmyna/strategies/ma_golden_cross.yaml +46 -0
  238. marketmyna/strategies/one_yang_three_yin.yaml +37 -0
  239. marketmyna/strategies/shrink_pullback.yaml +46 -0
  240. marketmyna/strategies/volume_breakout.yaml +47 -0
  241. marketmyna/strategies/wave_theory.yaml +64 -0
  242. marketmyna/trading/__init__.py +2 -0
  243. marketmyna/trading/alerts.py +356 -0
  244. marketmyna/trading/backtest.py +317 -0
  245. marketmyna/trading/charges.py +82 -0
  246. marketmyna/trading/chart.py +197 -0
  247. marketmyna/trading/indicators.py +303 -0
  248. marketmyna/trading/journal.py +74 -0
  249. marketmyna/trading/knn.py +164 -0
  250. marketmyna/trading/lots.py +109 -0
  251. marketmyna/trading/market_hours.py +66 -0
  252. marketmyna/trading/options.py +554 -0
  253. marketmyna/trading/pairs.py +94 -0
  254. marketmyna/trading/risk.py +170 -0
  255. marketmyna/trading/scanner.py +500 -0
  256. marketmyna/trading/volatility.py +163 -0
  257. marketmyna/utils/__init__.py +1 -0
  258. marketmyna/utils/deadline.py +36 -0
  259. marketmyna/utils/log_redact.py +52 -0
  260. marketmyna/utils/yahoo_gate.py +183 -0
  261. marketmyna/utils/yf_lock.py +12 -0
  262. marketmyna/web/apple-touch-icon.png +0 -0
  263. marketmyna/web/assets/Account-CA9NFDKO.js +1 -0
  264. marketmyna/web/assets/Analyze-uM0srB_U.js +7 -0
  265. marketmyna/web/assets/AreaChart-2i67V6Jf.js +1 -0
  266. marketmyna/web/assets/BarChart-_yngarVl.js +3 -0
  267. marketmyna/web/assets/Bot-E4gpf1qZ.js +1 -0
  268. marketmyna/web/assets/CandleChart-CioqTqMV.js +1 -0
  269. marketmyna/web/assets/Compare-D6_2CPl4.js +1 -0
  270. marketmyna/web/assets/ExportMenu-DaZ1xVmW.js +1 -0
  271. marketmyna/web/assets/LineChart-BaOkD1Ta.js +57 -0
  272. marketmyna/web/assets/Markets-BMw_M1sJ.js +1 -0
  273. marketmyna/web/assets/Portfolio-DQEdHVc2.js +1 -0
  274. marketmyna/web/assets/ReportView-BzhdcqSh.js +1 -0
  275. marketmyna/web/assets/Scheduler-I9PDKxTd.js +1 -0
  276. marketmyna/web/assets/Settings-DpXfHolW.js +3 -0
  277. marketmyna/web/assets/SipDays-C9q3bNSx.js +1 -0
  278. marketmyna/web/assets/SymbolSearch-CBOVPEk1.js +1 -0
  279. marketmyna/web/assets/Trade-_CM7HC4H.js +2 -0
  280. marketmyna/web/assets/TradeChart-ZjsycacC.js +1 -0
  281. marketmyna/web/assets/WatchButton-ComYW56_.js +1 -0
  282. marketmyna/web/assets/Watchlist-DAjLb0jd.js +1 -0
  283. marketmyna/web/assets/app-BP1o7WZS.js +1 -0
  284. marketmyna/web/assets/app-BkIUKdld.js +1 -0
  285. marketmyna/web/assets/app-CKeSYhEt.js +1 -0
  286. marketmyna/web/assets/app-CN1OERYX.js +1 -0
  287. marketmyna/web/assets/app-DIQllha5.js +1 -0
  288. marketmyna/web/assets/app-DeiAW0tX.js +1 -0
  289. marketmyna/web/assets/br-Cu5YU29T.svg +45 -0
  290. marketmyna/web/assets/browser-CR-ks2Fp.js +19 -0
  291. marketmyna/web/assets/browser-D5K_K_gj.js +1 -0
  292. marketmyna/web/assets/common-0wvpAVpd.js +1 -0
  293. marketmyna/web/assets/common-3ghy53Us.js +1 -0
  294. marketmyna/web/assets/common-Bib2unjX.js +1 -0
  295. marketmyna/web/assets/common-BvSJP0w_.js +1 -0
  296. marketmyna/web/assets/common-CSfyC0Sp.js +1 -0
  297. marketmyna/web/assets/common-C_t-iZPf.js +1 -0
  298. marketmyna/web/assets/compare-DMcsx70H.js +1 -0
  299. marketmyna/web/assets/es-d5m8M5h8.svg +544 -0
  300. marketmyna/web/assets/exporter-DiMg2O5d.js +3 -0
  301. marketmyna/web/assets/files-CpgiZRbN.js +1 -0
  302. marketmyna/web/assets/format-d8InM0Zw.js +9 -0
  303. marketmyna/web/assets/html2canvas-Bw0OShmD.js +5 -0
  304. marketmyna/web/assets/index-CKAnq-Or.js +12 -0
  305. marketmyna/web/assets/index-D4U8E6LH.css +2 -0
  306. marketmyna/web/assets/index.es-iBEw7jOv.js +5 -0
  307. marketmyna/web/assets/ir-cCIgaNf6.svg +219 -0
  308. marketmyna/web/assets/jspdf.es.min-C4285IdL.js +79 -0
  309. marketmyna/web/assets/jspdf.plugin.autotable-2eOJeXl-.js +2 -0
  310. marketmyna/web/assets/lightweight-charts.production-BF2noug5.js +1 -0
  311. marketmyna/web/assets/listings-BefgXpb1.js +1 -0
  312. marketmyna/web/assets/liveQuotes-CtSmCUFA.js +1 -0
  313. marketmyna/web/assets/markets-BI4AhI9W.js +1 -0
  314. marketmyna/web/assets/markets-BUHUdYpT.js +1 -0
  315. marketmyna/web/assets/markets-BqvN1Bvf.js +1 -0
  316. marketmyna/web/assets/markets-DPO40TTe.js +1 -0
  317. marketmyna/web/assets/markets-bsiPo-Tk.js +1 -0
  318. marketmyna/web/assets/markets-ff1e792u.js +1 -0
  319. marketmyna/web/assets/montecarlo-BsNlH2qe.js +1 -0
  320. marketmyna/web/assets/options-BYAZnwzW.js +1 -0
  321. marketmyna/web/assets/preload-helper-BLdHbfdN.js +1 -0
  322. marketmyna/web/assets/purify.es-Dqk5qvad.js +3 -0
  323. marketmyna/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  324. marketmyna/web/assets/sa-Dh79zbT9.svg +25 -0
  325. marketmyna/web/assets/toast-DDradTZw.js +1 -0
  326. marketmyna/web/assets/trade-fndw1Ql3.js +1 -0
  327. marketmyna/web/assets/ui-gsFb8B2m.js +1 -0
  328. marketmyna/web/assets/watchlists-B6YqwUIk.js +1 -0
  329. marketmyna/web/brand/logo-mark.webp +0 -0
  330. marketmyna/web/favicon.png +0 -0
  331. marketmyna/web/icon-192.png +0 -0
  332. marketmyna/web/icon-512.png +0 -0
  333. marketmyna/web/index.html +34 -0
  334. marketmyna/web/manifest.webmanifest +28 -0
  335. marketmyna/web/robots.txt +2 -0
  336. marketmyna/web/sw.js +40 -0
  337. marketmyna/web/theme-init.js +37 -0
  338. marketmyna-0.2.0.dist-info/METADATA +345 -0
  339. marketmyna-0.2.0.dist-info/RECORD +344 -0
  340. marketmyna-0.2.0.dist-info/WHEEL +5 -0
  341. marketmyna-0.2.0.dist-info/entry_points.txt +2 -0
  342. marketmyna-0.2.0.dist-info/licenses/LICENSE +202 -0
  343. marketmyna-0.2.0.dist-info/licenses/NOTICE +22 -0
  344. marketmyna-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,143 @@
1
+ """Who owns a stock and who is trading it: insider / institution split, top
2
+ holders, recent insider buys and sells, and short interest (US).
3
+
4
+ Source: Yahoo (via yfinance), which mirrors SEC Form 4 / 13F / FINRA short
5
+ interest for US listings and exchange disclosures, more sparsely, elsewhere.
6
+ Covers OpenBB's equity.ownership and equity.shorts endpoints (Apache 2.0).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import math
13
+ import threading
14
+ import time
15
+ from datetime import date, datetime, timedelta
16
+ from typing import Optional
17
+
18
+ import pandas as pd
19
+
20
+ from ..core.numbers import finite as _f
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ CACHE_SECONDS = 6 * 3600
25
+ INSIDER_DAYS = 180
26
+
27
+ _cache: dict[str, tuple[float, dict]] = {}
28
+ _lock = threading.Lock()
29
+
30
+
31
+ def _day(v) -> Optional[str]:
32
+ if v is None or (isinstance(v, float) and math.isnan(v)):
33
+ return None
34
+ if isinstance(v, (int, float)):
35
+ return datetime.fromtimestamp(v).date().isoformat()
36
+ try:
37
+ return pd.Timestamp(v).date().isoformat()
38
+ except (ValueError, TypeError):
39
+ return None
40
+
41
+
42
+ def classify(text: str, transaction: str = "") -> str:
43
+ """buy | sell | other, from Yahoo's free-text insider row."""
44
+ t = f"{transaction} {text}".lower()
45
+ if "sale" in t or "sell" in t or "sold" in t:
46
+ return "sell"
47
+ if "purchase" in t or "buy" in t or "bought" in t:
48
+ return "buy"
49
+ return "other"
50
+
51
+
52
+ def _holders(df) -> list[dict]:
53
+ if df is None or getattr(df, "empty", True):
54
+ return []
55
+ out = []
56
+ for _, r in df.head(10).iterrows():
57
+ out.append({"holder": str(r.get("Holder")), "pct_held": _f(r.get("pctHeld")), "shares": _f(r.get("Shares")),
58
+ "value": _f(r.get("Value")), "pct_change": _f(r.get("pctChange")), "reported": _day(r.get("Date Reported"))})
59
+ return out
60
+
61
+
62
+ def insider_activity(df, today: Optional[date] = None, days: int = INSIDER_DAYS) -> dict:
63
+ today = today or date.today()
64
+ since = today - timedelta(days=days)
65
+ rows = []
66
+ if df is not None and not getattr(df, "empty", True):
67
+ for _, r in df.iterrows():
68
+ when = _day(r.get("Start Date"))
69
+ if not when or date.fromisoformat(when) < since:
70
+ continue
71
+ rows.append({"date": when, "insider": str(r.get("Insider") or ""), "position": str(r.get("Position") or ""),
72
+ "side": classify(str(r.get("Text") or ""), str(r.get("Transaction") or "")),
73
+ "shares": _f(r.get("Shares")), "value": _f(r.get("Value")), "text": str(r.get("Text") or "")})
74
+ rows.sort(key=lambda x: x["date"], reverse=True)
75
+ total = lambda side, k: sum((x[k] or 0) for x in rows if x["side"] == side) # noqa: E731
76
+ buys, sells = sum(x["side"] == "buy" for x in rows), sum(x["side"] == "sell" for x in rows)
77
+ bias = None
78
+ if buys or sells:
79
+ bias = "net buying" if total("buy", "value") > total("sell", "value") else "net selling"
80
+ return {"days": days, "buys": buys, "sells": sells, "buy_value": total("buy", "value"),
81
+ "sell_value": total("sell", "value"), "bias": bias, "recent": rows[:12]}
82
+
83
+
84
+ def short_interest(info: dict) -> Optional[dict]:
85
+ shares = _f(info.get("sharesShort"))
86
+ if shares is None:
87
+ return None
88
+ prior = _f(info.get("sharesShortPriorMonth"))
89
+ return {"shares": shares, "prior_month": prior,
90
+ "change_pct": (shares / prior - 1) * 100 if prior else None,
91
+ "pct_of_float": (_f(info.get("shortPercentOfFloat")) or 0) * 100 or None,
92
+ "days_to_cover": _f(info.get("shortRatio")), "as_of": _day(info.get("dateShortInterest"))}
93
+
94
+
95
+ def _deals(symbol: str) -> Optional[dict]:
96
+ """NSE bulk / block deals in the stock over the last 90 days."""
97
+ try:
98
+ from ..data.nse_deals import recent
99
+
100
+ return recent(90, symbol)
101
+ except Exception as exc:
102
+ logger.info("NSE deals for %s unavailable: %s", symbol, exc)
103
+ return None
104
+
105
+
106
+ def build(symbol: str, market: str, ticker=None) -> dict:
107
+ from .analysis_run import yahoo_symbol
108
+
109
+ yf_symbol = yahoo_symbol(symbol, market)
110
+ live = ticker is None
111
+ if live:
112
+ with _lock:
113
+ hit = _cache.get(yf_symbol)
114
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
115
+ return hit[1]
116
+ import yfinance as yf
117
+
118
+ ticker = yf.Ticker(yf_symbol)
119
+
120
+ def safe(attr):
121
+ try:
122
+ return getattr(ticker, attr)
123
+ except Exception as exc:
124
+ logger.info("%s %s unavailable: %s", yf_symbol, attr, exc)
125
+ return None
126
+
127
+ info = safe("info") or {}
128
+ out = {
129
+ "symbol": symbol, "market": market,
130
+ "breakdown": {"insiders_pct": (_f(info.get("heldPercentInsiders")) or 0) * 100 or None,
131
+ "institutions_pct": (_f(info.get("heldPercentInstitutions")) or 0) * 100 or None,
132
+ "float_shares": _f(info.get("floatShares")),
133
+ "shares_outstanding": _f(info.get("sharesOutstanding"))},
134
+ "institutions": _holders(safe("institutional_holders")),
135
+ "funds": _holders(safe("mutualfund_holders")),
136
+ "insiders": insider_activity(safe("insider_transactions")),
137
+ "short": short_interest(info),
138
+ "deals": _deals(symbol) if market == "india" else None,
139
+ }
140
+ if live:
141
+ with _lock:
142
+ _cache[yf_symbol] = (time.time(), out)
143
+ return out
@@ -0,0 +1,199 @@
1
+ """Accounting quality scores from a company's last three annual statements,
2
+ rebuilt from the published papers:
3
+
4
+ - Piotroski F-score (Piotroski 2000): nine pass / fail tests on profitability,
5
+ leverage / liquidity and efficiency, latest year against the one before.
6
+ - Altman Z-score (Altman 1968) for manufacturers, which needs the market
7
+ value of equity; Z'' (Altman 1995) for everyone else, on book equity.
8
+ Banks and insurers are skipped by all three scores: lending shows up as
9
+ operating cash flow, and they have no gross margin or current ratio.
10
+ - Beneish M-score (Beneish 1999, 8 ratios): how far the statements look like
11
+ those of companies later found to have overstated earnings. Above -1.78 is
12
+ the paper's flag. A ratio that cannot be worked out is set to 1 (no change)
13
+ and listed.
14
+
15
+ Input is financials.py's {key: [value per year, newest first]} for the
16
+ income statement, balance sheet and cash flow. The scores describe the
17
+ filings; they are not a rating or advice.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Optional
23
+
24
+ from ..core.numbers import ratio as _div
25
+
26
+ # Sectors scored with Altman's original model (built on listed manufacturers,
27
+ # needs market value); the rest (services, real estate, utilities, telecom,
28
+ # media) get Z'' on book equity
29
+ MANUFACTURING = {"Industrials", "Basic Materials", "Consumer Cyclical", "Consumer Defensive", "Energy",
30
+ "Healthcare", "Technology"}
31
+
32
+
33
+ def _v(series: dict, key: str, i: int) -> Optional[float]:
34
+ vals = series.get(key) or []
35
+ return vals[i] if i < len(vals) else None
36
+
37
+
38
+ def _r(x: Optional[float], digits: int = 3) -> Optional[float]:
39
+ return round(x, digits) if x is not None else None
40
+
41
+
42
+ # ------------------------------------------------------------------ Piotroski
43
+
44
+ def piotroski(inc: dict, bal: dict, cf: dict, financial: bool = False) -> Optional[dict]:
45
+ if financial:
46
+ return None
47
+ ni0, ni1 = _v(inc, "net_income", 0), _v(inc, "net_income", 1)
48
+ ta0, ta1, ta2 = (_v(bal, "total_assets", i) for i in range(3))
49
+ if ni0 is None or ta0 is None or ta1 is None:
50
+ return None
51
+ cfo = _v(cf, "operating_cf", 0)
52
+ # ROA on assets at the start of the year (the paper's definition)
53
+ roa0 = _div(ni0, ta1)
54
+ roa1 = _div(ni1, ta2 if ta2 is not None else ta1)
55
+
56
+ def ratio(num_key: str, den_key: str, i: int):
57
+ return _div(_v(bal, num_key, i), _v(bal, den_key, i))
58
+
59
+ lev0, lev1 = ratio("total_debt", "total_assets", 0), ratio("total_debt", "total_assets", 1)
60
+ cr0, cr1 = ratio("current_assets", "current_liabilities", 0), ratio("current_assets", "current_liabilities", 1)
61
+ sh0, sh1 = _v(bal, "shares", 0), _v(bal, "shares", 1)
62
+ gm0 = _div(_v(inc, "gross_profit", 0), _v(inc, "revenue", 0))
63
+ gm1 = _div(_v(inc, "gross_profit", 1), _v(inc, "revenue", 1))
64
+ if gm0 is not None and gm1 is not None and gm0 >= 0.999 and gm1 >= 0.999:
65
+ gm0 = gm1 = None # no cost of goods reported (services): margin untestable
66
+ at0 = _div(_v(inc, "revenue", 0), ta1)
67
+ at1 = _div(_v(inc, "revenue", 1), ta2 if ta2 is not None else ta1)
68
+
69
+ def test(key, label, ok, detail):
70
+ return {"key": key, "label": label, "pass": ok, "detail": detail}
71
+
72
+ def pct(x):
73
+ return f"{x * 100:.1f}%" if x is not None else "n/a"
74
+
75
+ tests = [
76
+ test("roa", "Profitable (return on assets above 0)", None if roa0 is None else roa0 > 0, f"ROA {pct(roa0)}"),
77
+ test("cfo", "Cash from operations above 0", None if cfo is None else cfo > 0, "positive" if cfo and cfo > 0 else "zero or negative" if cfo is not None else "n/a"),
78
+ test("roa_up", "Return on assets improved", None if roa0 is None or roa1 is None else roa0 > roa1, f"{pct(roa1)} to {pct(roa0)}"),
79
+ test("accruals", "Cash from operations above net income", None if cfo is None else cfo > ni0, "earnings backed by cash" if cfo is not None and cfo > ni0 else "earnings ahead of cash" if cfo is not None else "n/a"),
80
+ test("leverage", "Debt to assets fell", None if lev0 is None or lev1 is None else lev0 <= lev1, f"{pct(lev1)} to {pct(lev0)}"),
81
+ test("liquidity", "Current ratio improved", None if cr0 is None or cr1 is None else cr0 > cr1,
82
+ f"{cr1:.2f} to {cr0:.2f}" if cr0 is not None and cr1 is not None else "n/a"),
83
+ test("dilution", "No new shares issued", None if sh0 is None or sh1 is None else sh0 <= sh1 * 1.001,
84
+ f"{(sh0 / sh1 - 1) * 100:+.1f}% shares" if sh0 and sh1 else "n/a"),
85
+ test("margin", "Gross margin improved", None if gm0 is None or gm1 is None else gm0 > gm1, f"{pct(gm1)} to {pct(gm0)}"),
86
+ test("turnover", "Asset turnover improved", None if at0 is None or at1 is None else at0 > at1,
87
+ f"{at1:.2f}x to {at0:.2f}x" if at0 is not None and at1 is not None else "n/a"),
88
+ ]
89
+ tested = [t for t in tests if t["pass"] is not None]
90
+ if len(tested) < 5:
91
+ return None
92
+ score = sum(1 for t in tested if t["pass"])
93
+ if len(tested) == 9:
94
+ label = "strong" if score >= 7 else "weak" if score <= 3 else "average"
95
+ else:
96
+ share = score / len(tested)
97
+ label = "strong" if share >= 0.75 else "weak" if share <= 0.35 else "average"
98
+ return {"score": score, "out_of": len(tested), "label": label, "tests": tests}
99
+
100
+
101
+ # ------------------------------------------------------------------ Altman
102
+
103
+ def altman(inc: dict, bal: dict, sector: Optional[str], market_cap: Optional[float],
104
+ financial: bool = False) -> Optional[dict]:
105
+ if financial:
106
+ return None
107
+ ta = _v(bal, "total_assets", 0)
108
+ tl = _v(bal, "total_liabilities", 0)
109
+ ca, cl = _v(bal, "current_assets", 0), _v(bal, "current_liabilities", 0)
110
+ re_ = _v(bal, "retained_earnings", 0)
111
+ ebit = _v(inc, "operating_income", 0)
112
+ if ebit is None and _v(inc, "pretax_income", 0) is not None:
113
+ ebit = _v(inc, "pretax_income", 0) + (abs(_v(inc, "interest_expense", 0) or 0))
114
+ sales = _v(inc, "revenue", 0)
115
+ equity = _v(bal, "equity", 0)
116
+ if not ta or not tl or ca is None or cl is None or re_ is None or ebit is None:
117
+ return None
118
+ x1, x2, x3 = (ca - cl) / ta, re_ / ta, ebit / ta
119
+
120
+ if sector in MANUFACTURING and market_cap and sales is not None:
121
+ x4, x5 = market_cap / tl, sales / ta
122
+ z = 1.2 * x1 + 1.4 * x2 + 3.3 * x3 + 0.6 * x4 + 1.0 * x5
123
+ model, safe, distress = "Z (manufacturers)", 2.99, 1.81
124
+ parts = {"working_capital": x1, "retained_earnings": x2, "ebit": x3, "market_value_to_liabilities": x4, "sales": x5}
125
+ else:
126
+ if equity is None:
127
+ return None
128
+ x4 = equity / tl
129
+ z = 6.56 * x1 + 3.26 * x2 + 6.72 * x3 + 1.05 * x4
130
+ model, safe, distress = "Z'' (non-manufacturers, book equity)", 2.6, 1.1
131
+ parts = {"working_capital": x1, "retained_earnings": x2, "ebit": x3, "book_equity_to_liabilities": x4}
132
+ zone = "safe" if z > safe else "distress" if z < distress else "grey"
133
+ return {"score": round(z, 2), "model": model, "zone": zone, "safe_above": safe, "distress_below": distress,
134
+ "parts": {k: _r(v) for k, v in parts.items()}}
135
+
136
+
137
+ # ------------------------------------------------------------------ Beneish
138
+
139
+ def beneish(inc: dict, bal: dict, cf: dict, financial: bool = False) -> Optional[dict]:
140
+ if financial:
141
+ return None
142
+ s0, s1 = _v(inc, "revenue", 0), _v(inc, "revenue", 1)
143
+ ta0, ta1 = _v(bal, "total_assets", 0), _v(bal, "total_assets", 1)
144
+ ni0, cfo0 = _v(inc, "net_income", 0), _v(cf, "operating_cf", 0)
145
+ if not s0 or not s1 or not ta0 or not ta1 or ni0 is None or cfo0 is None:
146
+ return None
147
+
148
+ def at(key, i, src=bal):
149
+ return _v(src, key, i)
150
+
151
+ rec0, rec1 = at("receivables", 0), at("receivables", 1)
152
+ gm0, gm1 = _div(at("gross_profit", 0, inc), s0), _div(at("gross_profit", 1, inc), s1)
153
+ ppe0, ppe1 = at("ppe", 0), at("ppe", 1)
154
+ ca0, ca1 = at("current_assets", 0), at("current_assets", 1)
155
+ dep0, dep1 = abs(at("depreciation", 0, cf) or 0) or None, abs(at("depreciation", 1, cf) or 0) or None
156
+ sga0, sga1 = at("sga", 0, inc), at("sga", 1, inc)
157
+ tl0, tl1 = at("total_liabilities", 0), at("total_liabilities", 1)
158
+
159
+ def quality(ca, ppe, ta):
160
+ return 1 - (ca + ppe) / ta if ca is not None and ppe is not None else None
161
+
162
+ def dep_rate(dep, ppe):
163
+ return dep / (dep + ppe) if dep is not None and ppe is not None and dep + ppe else None
164
+
165
+ ratios = {
166
+ "DSRI": _div(_div(rec0, s0), _div(rec1, s1)), # receivables growing faster than sales
167
+ "GMI": _div(gm1, gm0), # gross margin falling
168
+ "AQI": _div(quality(ca0, ppe0, ta0), quality(ca1, ppe1, ta1)), # more soft assets
169
+ "SGI": s0 / s1, # sales growth
170
+ "DEPI": _div(dep_rate(dep1, ppe1), dep_rate(dep0, ppe0)), # slower depreciation
171
+ "SGAI": _div(_div(sga0, s0), _div(sga1, s1)), # overheads per sale
172
+ "LVGI": _div(_div(tl0, ta0), _div(tl1, ta1)), # more leverage
173
+ "TATA": (ni0 - cfo0) / ta0, # accruals
174
+ }
175
+ if ratios["DSRI"] is None:
176
+ return None
177
+ neutral = [k for k, v in ratios.items() if v is None]
178
+ vals = {k: (1.0 if v is None else v) for k, v in ratios.items()}
179
+ m = (-4.84 + 0.920 * vals["DSRI"] + 0.528 * vals["GMI"] + 0.404 * vals["AQI"] + 0.892 * vals["SGI"]
180
+ + 0.115 * vals["DEPI"] - 0.172 * vals["SGAI"] + 4.679 * vals["TATA"] - 0.327 * vals["LVGI"])
181
+ return {"score": round(m, 2), "flag": m > -1.78, "threshold": -1.78,
182
+ "ratios": {k: _r(v) for k, v in ratios.items()}, "neutral": neutral}
183
+
184
+
185
+ def compute(inc: dict, bal: dict, cf: dict, *, sector: Optional[str] = None,
186
+ market_cap: Optional[float] = None, financial: bool = False) -> Optional[dict]:
187
+ out = {
188
+ "piotroski": piotroski(inc, bal, cf, financial),
189
+ "altman": altman(inc, bal, sector, market_cap, financial),
190
+ "beneish": beneish(inc, bal, cf, financial),
191
+ }
192
+ if financial:
193
+ return {**out, "financial_sector": True,
194
+ "note": "Not scored: these models are built for non-financial companies. Banks and insurers "
195
+ "book lending as operating cash flow and have no gross margin or current ratio."}
196
+ if not any(out.values()):
197
+ return None
198
+ out["financial_sector"] = False
199
+ return out
@@ -0,0 +1,335 @@
1
+ """Rates & bullion for markets other than India (India has its own,
2
+ richer card: marketmyna/services/india_rates.py).
3
+
4
+ - us: Fed funds, SOFR, Treasury 3M/2Y/10Y/30Y, 10y-2y, 30y mortgage
5
+ (FRED) + gold / silver in dollars (Yahoo futures).
6
+ - hong_kong: HKMA base rate, HIBOR overnight / 1M / 3M (HKMA open API),
7
+ USD/HKD peg + gold / silver in HK dollars.
8
+ - china_a: China 3M interbank (FRED/OECD, monthly), USD/CNY + gold per
9
+ gram and silver per kg in yuan.
10
+ - crypto: perpetual-futures funding rates (Binance, Bybit fallback),
11
+ annualised - crypto's closest thing to an interest rate - plus
12
+ Bitcoin priced in gold.
13
+
14
+ Every block is fetched independently; a failed block is reported and the
15
+ rest still render. Cached for an hour per market.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import threading
22
+ import time
23
+ from concurrent.futures import ThreadPoolExecutor
24
+ from datetime import date, timedelta
25
+ from typing import Callable, Optional
26
+
27
+ import pandas as pd
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ CACHE_SECONDS = 3600
32
+ TROY_OZ_G = 31.1034768
33
+ TAEL_OZ = 1.20337 # 1 HK tael (37.429 g) in troy ounces
34
+ HKMA = "https://api.hkma.gov.hk/public/market-data-and-statistics"
35
+ FUNDING_COINS = ["BTC", "ETH", "SOL", "XRP", "DOGE", "BNB"]
36
+
37
+ _cache: dict[str, tuple[float, dict]] = {}
38
+ _lock = threading.Lock()
39
+
40
+
41
+ def _get_json(url: str, params: Optional[dict] = None) -> dict:
42
+ import requests
43
+
44
+ r = requests.get(url, params=params, timeout=20, headers={"User-Agent": "Mozilla/5.0 MarketMyna"})
45
+ r.raise_for_status()
46
+ return r.json()
47
+
48
+
49
+ def _fred(series_id: str, start: date) -> pd.Series:
50
+ from .macro import _fred as fred
51
+
52
+ return fred(series_id, start)
53
+
54
+
55
+ def _yahoo(tickers: list[str]) -> dict[str, pd.Series]:
56
+ import yfinance as yf
57
+
58
+ from ..data import ecb_fx
59
+ from ..utils.yf_lock import YF_DOWNLOAD_LOCK
60
+
61
+ def download() -> dict[str, pd.Series]:
62
+ with YF_DOWNLOAD_LOCK:
63
+ df = yf.download(tickers, period="13mo", interval="1d", progress=False, auto_adjust=True, threads=True)
64
+ closes = df["Close"]
65
+ if isinstance(closes, pd.Series):
66
+ closes = closes.to_frame(tickers[0])
67
+ return {t: closes[t].dropna() for t in tickers if t in closes and closes[t].notna().sum()}
68
+
69
+ return ecb_fx.guarded(download, tickers, days=400)
70
+
71
+
72
+ # ---------------------------------------------------------------- helpers
73
+
74
+
75
+ def _naive(s: pd.Series) -> pd.Series:
76
+ idx = pd.DatetimeIndex(s.index)
77
+ return pd.Series(s.to_numpy(), index=idx.tz_localize(None) if idx.tz is not None else idx).sort_index()
78
+
79
+
80
+ def _ago(s: pd.Series, days: int) -> Optional[float]:
81
+ past = s[s.index <= s.index[-1] - pd.Timedelta(days=days)]
82
+ return float(past.iloc[-1]) if len(past) else None
83
+
84
+
85
+ def item(label: str, s: Optional[pd.Series], unit: str, note: str = "", scale: float = 1.0,
86
+ change: str = "points", digits: int = 2) -> Optional[dict]:
87
+ """One stat from a series: latest value, 1-month and 1-year change.
88
+
89
+ change="points" for rates (difference), "pct" for prices (% change)."""
90
+ if s is None or s.dropna().empty:
91
+ return None
92
+ s = _naive(s.dropna()) * scale
93
+ now = float(s.iloc[-1])
94
+
95
+ def delta(days: int) -> Optional[float]:
96
+ past = _ago(s, days)
97
+ if past is None:
98
+ return None
99
+ return now - past if change == "points" else (now / past - 1) * 100 if past else None
100
+
101
+ return {"label": label, "value": round(now, digits), "unit": unit, "note": note,
102
+ "change_1m": delta(30), "change_1y": delta(365), "change_kind": change,
103
+ "as_of": str(s.index[-1].date()),
104
+ "spark": [round(float(v), digits) for v in s.resample("W").last().dropna().tail(52)]}
105
+
106
+
107
+ def _section(title: str, items: list[Optional[dict]], note: str = "", source: str = "") -> Optional[dict]:
108
+ items = [i for i in items if i]
109
+ return {"title": title, "note": note, "source": source, "items": items} if items else None
110
+
111
+
112
+ def _run(blocks: dict[str, Callable[[], Optional[dict]]]) -> tuple[list[dict], list[str]]:
113
+ sections, errors = [], []
114
+ with ThreadPoolExecutor(max_workers=len(blocks)) as pool:
115
+ futures = {name: pool.submit(fn) for name, fn in blocks.items()}
116
+ for name, fut in futures.items():
117
+ try:
118
+ sec = fut.result()
119
+ except Exception as exc:
120
+ logger.warning("Rates block %s failed: %s", name, exc)
121
+ errors.append(f"{name} unavailable right now")
122
+ continue
123
+ if sec:
124
+ sections.append(sec)
125
+ else:
126
+ errors.append(f"{name} unavailable right now")
127
+ return sections, errors
128
+
129
+
130
+ def _bullion(currency: str, fx: Optional[str], unit_note: str, *, per: str = "oz",
131
+ yahoo: Callable = _yahoo) -> Optional[dict]:
132
+ tickers = ["GC=F", "SI=F"] + ([fx] if fx else [])
133
+ data = yahoo(tickers)
134
+ gold, silver = data.get("GC=F"), data.get("SI=F")
135
+ if gold is None:
136
+ return None
137
+ gold, silver = _naive(gold), _naive(silver) if silver is not None else None
138
+ if fx:
139
+ rate = _naive(data[fx]) if fx in data else None
140
+ if rate is None:
141
+ return None
142
+ gold = pd.concat([gold, rate], axis=1).ffill().dropna().prod(axis=1)
143
+ silver = pd.concat([silver, rate], axis=1).ffill().dropna().prod(axis=1) if silver is not None else None
144
+ if per == "g":
145
+ g_item = item(f"Gold ({currency}/g)", gold, currency, unit_note, scale=1 / TROY_OZ_G, change="pct")
146
+ s_item = item(f"Silver ({currency}/kg)", silver, currency, "", scale=1000 / TROY_OZ_G, change="pct", digits=0)
147
+ elif per == "tael":
148
+ g_item = item(f"Gold ({currency}/tael)", gold, currency, unit_note, scale=TAEL_OZ, change="pct", digits=0)
149
+ s_item = item(f"Silver ({currency}/oz)", silver, currency, "", change="pct")
150
+ else:
151
+ g_item = item(f"Gold ({currency}/oz)", gold, currency, unit_note, change="pct", digits=0)
152
+ s_item = item(f"Silver ({currency}/oz)", silver, currency, "", change="pct")
153
+ ratio = None
154
+ if silver is not None:
155
+ both = pd.concat([gold, silver], axis=1, keys=["g", "s"]).ffill().dropna()
156
+ ratio = item("Gold / silver ratio", both["g"] / both["s"], "x",
157
+ "Ounces of silver one ounce of gold buys; high = silver cheap vs gold", change="points", digits=1)
158
+ return _section("Bullion", [g_item, s_item, ratio], source="COMEX futures, market data feed")
159
+
160
+
161
+ # ---------------------------------------------------------------- markets
162
+
163
+
164
+ def _us(fred=_fred, yahoo=_yahoo) -> dict:
165
+ start = date.today() - timedelta(days=400)
166
+
167
+ def series(*ids):
168
+ return {i: fred(i, start) for i in ids}
169
+
170
+ def policy():
171
+ s = series("DFF", "SOFR", "MORTGAGE30US")
172
+ return _section("Policy & money market", [
173
+ item("Fed funds (effective)", s["DFF"], "%", "Overnight rate the Fed steers"),
174
+ item("SOFR", s["SOFR"], "%", "Secured overnight rate; benchmark for most US loans"),
175
+ item("30-year mortgage", s["MORTGAGE30US"], "%", "Freddie Mac weekly average"),
176
+ ], source="Official US rates data")
177
+
178
+ def treasuries():
179
+ s = series("DGS3MO", "DGS2", "DGS10", "DGS30", "T10Y2Y")
180
+ return _section("Treasury yields", [
181
+ item("3-month", s["DGS3MO"], "%", "Cash-like yield"),
182
+ item("2-year", s["DGS2"], "%", "Tracks expected Fed moves"),
183
+ item("10-year", s["DGS10"], "%", "Benchmark for valuations and mortgages"),
184
+ item("30-year", s["DGS30"], "%"),
185
+ item("10y − 2y spread", s["T10Y2Y"], "pts", "Below 0 (inverted) has preceded most recessions"),
186
+ ], note="Full curve: see the Macro tab.", source="Official US Treasury constant-maturity yields")
187
+
188
+ sections, errors = _run({"Policy rates": policy, "Treasuries": treasuries,
189
+ "Bullion": lambda: _bullion("$", None, "COMEX front-month", yahoo=yahoo)})
190
+ return {"sections": sections, "errors": errors}
191
+
192
+
193
+ def _hkma_series(path: str, fields: dict[str, str], date_key: str, pages: int = 400) -> dict[str, pd.Series]:
194
+ data = _get_json(f"{HKMA}/{path}", {"pagesize": pages, "sortby": date_key, "sortorder": "desc"})
195
+ records = (data.get("result") or {}).get("records") or []
196
+ out = {}
197
+ for key in fields:
198
+ pts = {pd.Timestamp(r[date_key]): r[key] for r in records if r.get(key) is not None}
199
+ if pts:
200
+ out[key] = pd.Series(pts).sort_index().astype(float)
201
+ return out
202
+
203
+
204
+ def _hong_kong(yahoo=_yahoo) -> dict:
205
+ def money():
206
+ daily = _hkma_series("daily-monetary-statistics/daily-figures-interbank-liquidity",
207
+ {"disc_win_base_rate": "", "hibor_overnight": "", "hibor_fixing_1m": ""}, "end_of_date")
208
+ curve = _hkma_series("monthly-statistical-bulletin/er-ir/hk-interbank-ir-daily",
209
+ {"ir_3m": "", "ir_12m": ""}, "end_of_day")
210
+ return _section("HKMA & HIBOR", [
211
+ item("Base rate", daily.get("disc_win_base_rate"), "%", "HKMA discount-window base rate; follows the Fed under the peg"),
212
+ item("HIBOR overnight", daily.get("hibor_overnight"), "%", "Swings with HK dollar liquidity"),
213
+ item("HIBOR 1M", daily.get("hibor_fixing_1m"), "%", "Prices most HK mortgages (HIBOR-linked plans)"),
214
+ item("HIBOR 3M", curve.get("ir_3m"), "%"),
215
+ item("HIBOR 12M", curve.get("ir_12m"), "%"),
216
+ ], source="HKMA open API")
217
+
218
+ def peg():
219
+ data = yahoo(["HKD=X"])
220
+ return _section("Currency peg", [
221
+ item("USD / HKD", data.get("HKD=X"), "HK$", "Pegged in the 7.75 – 7.85 band; near 7.85 = weak side, HKMA drains liquidity",
222
+ change="points", digits=4),
223
+ ], source="Market data feed")
224
+
225
+ sections, errors = _run({"HKMA rates": money, "Currency peg": peg,
226
+ "Bullion": lambda: _bullion("HK$", "HKD=X", "Spot in HK dollars", per="tael", yahoo=yahoo)})
227
+ return {"sections": sections, "errors": errors}
228
+
229
+
230
+ def _china(fred=_fred, yahoo=_yahoo) -> dict:
231
+ start = date.today() - timedelta(days=800)
232
+
233
+ def money():
234
+ return _section("Money market", [
235
+ item("3-month interbank", fred("IR3TIB01CNM156N", start), "%", "Monthly (OECD); lags a few months"),
236
+ ], source="Official central bank and OECD statistics")
237
+
238
+ def fx():
239
+ data = yahoo(["CNY=X"])
240
+ return _section("Currency", [
241
+ item("USD / CNY", data.get("CNY=X"), "¥", "Higher = weaker yuan; PBoC manages it in a daily band",
242
+ change="points", digits=4),
243
+ ], source="Market data feed")
244
+
245
+ sections, errors = _run({"Money market": money, "Currency": fx,
246
+ "Bullion": lambda: _bullion("¥", "CNY=X", "Spot converted to yuan", per="g", yahoo=yahoo)})
247
+ return {"sections": sections, "errors": errors}
248
+
249
+
250
+ def _funding_binance() -> list[dict]:
251
+ rows = _get_json("https://fapi.binance.com/fapi/v1/premiumIndex")
252
+ by = {r["symbol"]: r for r in rows if isinstance(r, dict)}
253
+ out = []
254
+ for coin in FUNDING_COINS:
255
+ r = by.get(f"{coin}USDT")
256
+ if r and r.get("lastFundingRate") not in (None, ""):
257
+ out.append({"coin": coin, "rate_8h": float(r["lastFundingRate"]), "mark": float(r["markPrice"]),
258
+ "next": int(r.get("nextFundingTime") or 0)})
259
+ return out
260
+
261
+
262
+ def _funding_bybit() -> list[dict]:
263
+ data = _get_json("https://api.bybit.com/v5/market/tickers", {"category": "linear"})
264
+ by = {r["symbol"]: r for r in (data.get("result") or {}).get("list") or []}
265
+ out = []
266
+ for coin in FUNDING_COINS:
267
+ r = by.get(f"{coin}USDT")
268
+ if r and r.get("fundingRate") not in (None, ""):
269
+ out.append({"coin": coin, "rate_8h": float(r["fundingRate"]), "mark": float(r["markPrice"]),
270
+ "next": int(r.get("nextFundingTime") or 0)})
271
+ return out
272
+
273
+
274
+ def _crypto(yahoo=_yahoo, funding_sources=(_funding_binance, _funding_bybit)) -> dict:
275
+ def funding():
276
+ rows, source = [], ""
277
+ for fn in funding_sources:
278
+ try:
279
+ rows = fn()
280
+ except Exception as exc:
281
+ logger.warning("Funding rates via %s failed: %s", fn.__name__, exc)
282
+ continue
283
+ if rows:
284
+ source = "Crypto exchange futures data"
285
+ break
286
+ items = []
287
+ for r in rows:
288
+ annual = r["rate_8h"] * 3 * 365 * 100
289
+ items.append({
290
+ "label": f"{r['coin']} funding", "value": round(annual, 1), "unit": "% / yr",
291
+ "note": f"{r['rate_8h'] * 100:.4f}% per 8h · "
292
+ + ("longs pay shorts (bullish leverage)" if r["rate_8h"] > 0 else "shorts pay longs (bearish leverage)"),
293
+ "change_1m": None, "change_1y": None, "change_kind": "points", "as_of": str(date.today()), "spark": [],
294
+ "tone": "up" if annual > 0 else "down",
295
+ })
296
+ return _section("Perpetual funding rates", items,
297
+ note="What leveraged longs pay shorts, annualised. Very high = crowded longs; negative = crowded shorts.",
298
+ source=f"{source} USDT perpetuals" if source else "")
299
+
300
+ def btc_gold():
301
+ data = yahoo(["BTC-USD", "GC=F"])
302
+ if "BTC-USD" not in data or "GC=F" not in data:
303
+ return None
304
+ both = pd.concat([_naive(data["BTC-USD"]), _naive(data["GC=F"])], axis=1, keys=["b", "g"]).ffill().dropna()
305
+ return _section("Bitcoin vs gold", [
306
+ item("BTC in gold ounces", both["b"] / both["g"], "oz", "How many ounces of gold one bitcoin buys", change="pct", digits=1),
307
+ item("Gold ($/oz)", data["GC=F"], "$", "COMEX front-month", change="pct", digits=0),
308
+ ], source="Market data feed")
309
+
310
+ sections, errors = _run({"Funding rates": funding, "Bitcoin vs gold": btc_gold})
311
+ return {"sections": sections, "errors": errors}
312
+
313
+
314
+ BUILDERS: dict[str, Callable[[], dict]] = {
315
+ "us": _us, "hong_kong": _hong_kong, "china_a": _china, "crypto": _crypto,
316
+ }
317
+ TITLES = {"us": "US rates & bullion", "hong_kong": "Hong Kong rates & bullion",
318
+ "china_a": "China rates & bullion", "crypto": "Funding rates & gold"}
319
+
320
+
321
+ def build(market: str, use_cache: bool = True) -> dict:
322
+ market = market.lower()
323
+ builder = BUILDERS.get(market)
324
+ if builder is None:
325
+ return {"market": market, "sections": [], "errors": ["Not available for this market"]}
326
+ if use_cache:
327
+ with _lock:
328
+ hit = _cache.get(market)
329
+ if hit and time.time() - hit[0] < CACHE_SECONDS:
330
+ return hit[1]
331
+ out = {"market": market, "title": TITLES[market], **builder()}
332
+ if use_cache and out["sections"]:
333
+ with _lock:
334
+ _cache[market] = (time.time(), out)
335
+ return out