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,497 @@
|
|
|
1
|
+
"""Live prices for the Market insights views (Market pulse, Hot stocks,
|
|
2
|
+
Market mood, Sector / ETF rotation, World markets, Commodities).
|
|
3
|
+
|
|
4
|
+
The listings store is a snapshot (India: the last session's exchange file;
|
|
5
|
+
US: hourly; China / HK: every 4 hours), and the rotation / mood maths run on
|
|
6
|
+
daily closes. While a market is open this module keeps fresh quotes for its
|
|
7
|
+
largest names, so the views recompute gainers, losers, breadth, sector moves,
|
|
8
|
+
mood readings and the leading / weakening / lagging / improving quadrants on
|
|
9
|
+
live prices:
|
|
10
|
+
|
|
11
|
+
- `market(name)`: quotes for the market's largest names (whole market where
|
|
12
|
+
one call covers it), refreshed in the background while someone is looking
|
|
13
|
+
and the market is open, at most every REFRESH seconds. Requests never wait
|
|
14
|
+
on the upstream source once something is cached.
|
|
15
|
+
- `latest(tickers)`: last price of index / futures / FX / ETF tickers from
|
|
16
|
+
one batched 1-minute download, cached LATEST_SECONDS.
|
|
17
|
+
- `merge(rows, quotes)` and `extend(series, price, day)` fold those into
|
|
18
|
+
listing rows and daily close series.
|
|
19
|
+
|
|
20
|
+
Sources: the broker feed for India when it is set up (else the regular feed),
|
|
21
|
+
the US screener (whole market, one call), the crypto exchange's tickers (whole
|
|
22
|
+
market, one call), batched exchange quotes for China A / Hong Kong.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from datetime import date, datetime
|
|
32
|
+
from typing import Optional
|
|
33
|
+
from zoneinfo import ZoneInfo
|
|
34
|
+
|
|
35
|
+
import pandas as pd
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Seconds between upstream refreshes per market while it is open.
|
|
40
|
+
REFRESH = {
|
|
41
|
+
"india": float(os.environ.get("LIVE_MARKET_REFRESH_INDIA", 60)),
|
|
42
|
+
"us": float(os.environ.get("LIVE_MARKET_REFRESH_US", 60)),
|
|
43
|
+
"china_a": float(os.environ.get("LIVE_MARKET_REFRESH_CHINA_A", 60)),
|
|
44
|
+
"hong_kong": float(os.environ.get("LIVE_MARKET_REFRESH_HONG_KONG", 60)),
|
|
45
|
+
"crypto": float(os.environ.get("LIVE_MARKET_REFRESH_CRYPTO", 15)),
|
|
46
|
+
}
|
|
47
|
+
# The regular feed is slow for hundreds of Indian names and lags anyway.
|
|
48
|
+
INDIA_FEED_REFRESH = float(os.environ.get("LIVE_MARKET_REFRESH_INDIA_FEED", 120))
|
|
49
|
+
# How many of the largest names get live prices (US and crypto: the whole
|
|
50
|
+
# market comes in one call, so everything).
|
|
51
|
+
TOP = {
|
|
52
|
+
"india": int(os.environ.get("LIVE_MARKET_TOP_INDIA", 500)),
|
|
53
|
+
"china_a": int(os.environ.get("LIVE_MARKET_TOP_CHINA_A", 1000)),
|
|
54
|
+
"hong_kong": int(os.environ.get("LIVE_MARKET_TOP_HONG_KONG", 600)),
|
|
55
|
+
}
|
|
56
|
+
WATCH_SECONDS = 120 # nobody asked for two minutes: stop refreshing
|
|
57
|
+
MAX_AGE = 18 * 3600 # older quotes are from an earlier session: not used
|
|
58
|
+
LATEST_SECONDS = 60
|
|
59
|
+
YAHOO_CHUNK = 150
|
|
60
|
+
TIMEZONES = {"india": "Asia/Kolkata", "us": "America/New_York", "china_a": "Asia/Shanghai",
|
|
61
|
+
"hong_kong": "Asia/Hong_Kong", "crypto": "UTC"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _now() -> float:
|
|
65
|
+
return time.time()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def market_day(market: str, epoch: Optional[float] = None) -> date:
|
|
69
|
+
"""The calendar date in the market's own time zone."""
|
|
70
|
+
tz = ZoneInfo(TIMEZONES.get(market, "UTC"))
|
|
71
|
+
return datetime.fromtimestamp(epoch if epoch is not None else _now(), tz).date()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------- market quotes
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _State:
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.quotes: dict[str, dict] = {} # symbol -> {price, change, change_percent, volume, time, day}
|
|
80
|
+
self.updated = 0.0 # wall clock of the last successful refresh
|
|
81
|
+
self.asked = 0.0 # wall clock of the last request
|
|
82
|
+
self.error: Optional[str] = None
|
|
83
|
+
self.source = ""
|
|
84
|
+
self.busy = False
|
|
85
|
+
self.version = 0
|
|
86
|
+
self.lock = threading.Lock()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_states: dict[str, _State] = {}
|
|
90
|
+
_states_lock = threading.Lock()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _state(name: str) -> _State:
|
|
94
|
+
with _states_lock:
|
|
95
|
+
return _states.setdefault(name, _State())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _broker_on() -> bool:
|
|
99
|
+
from . import live_quotes
|
|
100
|
+
|
|
101
|
+
return live_quotes._broker_live()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def refresh_seconds(name: str) -> float:
|
|
105
|
+
if name == "india" and not _broker_on():
|
|
106
|
+
return INDIA_FEED_REFRESH
|
|
107
|
+
return REFRESH.get(name, 60)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _source(name: str) -> str:
|
|
111
|
+
from . import live_quotes
|
|
112
|
+
|
|
113
|
+
return live_quotes.source(name)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def market(name: str) -> dict:
|
|
117
|
+
"""{"quotes": {symbol: quote}, "status": {...}} for one market. Starts a
|
|
118
|
+
background refresh when due; returns at once with what is cached."""
|
|
119
|
+
from ..trading.market_hours import is_open
|
|
120
|
+
|
|
121
|
+
s = _state(name)
|
|
122
|
+
now = _now()
|
|
123
|
+
open_now = is_open(name)
|
|
124
|
+
with s.lock:
|
|
125
|
+
s.asked = now
|
|
126
|
+
age = now - s.updated if s.updated else None
|
|
127
|
+
due = age is None or age >= MAX_AGE or (open_now and age >= refresh_seconds(name))
|
|
128
|
+
start = due and not s.busy and name in REFRESH
|
|
129
|
+
if start:
|
|
130
|
+
s.busy = True
|
|
131
|
+
fresh = age is not None and age < MAX_AGE
|
|
132
|
+
quotes = dict(s.quotes) if fresh else {}
|
|
133
|
+
# The tables' per-second cache (live_quotes) is fresher for the names on
|
|
134
|
+
# someone's screen: take those prices too.
|
|
135
|
+
if quotes:
|
|
136
|
+
quotes.update(_table_quotes(name, quotes))
|
|
137
|
+
with s.lock:
|
|
138
|
+
status = {
|
|
139
|
+
"open": open_now,
|
|
140
|
+
"live": bool(quotes),
|
|
141
|
+
"updated": s.updated or None,
|
|
142
|
+
"age_seconds": round(age, 1) if age is not None else None,
|
|
143
|
+
"refresh_seconds": refresh_seconds(name),
|
|
144
|
+
"covered": len(quotes),
|
|
145
|
+
"source": s.source or _source(name),
|
|
146
|
+
"error": s.error,
|
|
147
|
+
"version": s.version,
|
|
148
|
+
}
|
|
149
|
+
if start:
|
|
150
|
+
threading.Thread(target=_refresh, args=(name,), name=f"live-market-{name}", daemon=True).start()
|
|
151
|
+
return {"quotes": quotes, "status": status}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _table_quotes(name: str, quotes: dict[str, dict]) -> dict[str, dict]:
|
|
155
|
+
"""Quotes from the tables' cache that are newer than ours, with the
|
|
156
|
+
trading day added."""
|
|
157
|
+
from . import live_quotes
|
|
158
|
+
|
|
159
|
+
m = live_quotes._markets.get(name)
|
|
160
|
+
if m is None:
|
|
161
|
+
return {}
|
|
162
|
+
with m.lock:
|
|
163
|
+
table = dict(m.quotes)
|
|
164
|
+
out = {}
|
|
165
|
+
for sym, q in table.items():
|
|
166
|
+
mine = quotes.get(sym)
|
|
167
|
+
if mine is not None and q.get("price") and (q.get("time") or 0) > (mine.get("time") or 0):
|
|
168
|
+
out[sym] = {**q, "day": market_day(name, q.get("time"))}
|
|
169
|
+
return out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _universe(name: str) -> list[str]:
|
|
173
|
+
"""Symbols of the market's largest names, biggest first."""
|
|
174
|
+
from ..data.listings import get_store
|
|
175
|
+
|
|
176
|
+
if name not in TOP:
|
|
177
|
+
return []
|
|
178
|
+
rows = [r for r in get_store().rows(name) if r.get("type") == "stock" and r.get("market_cap")]
|
|
179
|
+
rows.sort(key=lambda r: -r["market_cap"])
|
|
180
|
+
return [r["symbol"] for r in rows[:TOP[name]]]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _refresh(name: str) -> None:
|
|
184
|
+
s = _state(name)
|
|
185
|
+
try:
|
|
186
|
+
fresh = FETCHERS[name](_universe(name))
|
|
187
|
+
if not fresh:
|
|
188
|
+
raise RuntimeError("no quotes returned")
|
|
189
|
+
error = None
|
|
190
|
+
except Exception as exc: # keep serving the last quotes
|
|
191
|
+
logger.warning("Live market prices for %s failed: %s", name, exc)
|
|
192
|
+
fresh, error = {}, "Live prices are temporarily unavailable; showing the last ones."
|
|
193
|
+
with s.lock:
|
|
194
|
+
if fresh:
|
|
195
|
+
s.quotes = fresh
|
|
196
|
+
s.updated = _now()
|
|
197
|
+
s.version += 1
|
|
198
|
+
s.error = error
|
|
199
|
+
s.source = _source(name)
|
|
200
|
+
s.busy = False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _fetch_us(_symbols: list[str]) -> dict[str, dict]:
|
|
204
|
+
"""The whole US market from the screener, one call."""
|
|
205
|
+
from ..data.listings import _float, _nasdaq_screener
|
|
206
|
+
from ..trading.market_hours import is_open
|
|
207
|
+
|
|
208
|
+
now = _now()
|
|
209
|
+
day = market_day("us", now) if is_open("us") else None
|
|
210
|
+
out = {}
|
|
211
|
+
for r in _nasdaq_screener("stocks"):
|
|
212
|
+
sym = (r.get("symbol") or "").strip().replace("/", "-").replace("^", "-P")
|
|
213
|
+
price = _float(r.get("lastsale"))
|
|
214
|
+
if not sym or not price:
|
|
215
|
+
continue
|
|
216
|
+
out[sym] = {"price": price, "change": _float(r.get("netchange")), "change_percent": _float(r.get("pctchange")),
|
|
217
|
+
"volume": _float(r.get("volume")), "time": now, "day": day}
|
|
218
|
+
return out
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _fetch_crypto(_symbols: list[str]) -> dict[str, dict]:
|
|
222
|
+
"""Every USDT pair's 24-hour ticker, one call."""
|
|
223
|
+
import requests
|
|
224
|
+
|
|
225
|
+
r = requests.get("https://api.binance.com/api/v3/ticker/24hr", timeout=15)
|
|
226
|
+
r.raise_for_status()
|
|
227
|
+
now = _now()
|
|
228
|
+
day = market_day("crypto", now)
|
|
229
|
+
out = {}
|
|
230
|
+
for t in r.json():
|
|
231
|
+
pair = t.get("symbol", "")
|
|
232
|
+
if not pair.endswith("USDT"):
|
|
233
|
+
continue
|
|
234
|
+
price = _num(t.get("lastPrice"))
|
|
235
|
+
if price:
|
|
236
|
+
out[f"{pair[:-4]}-USD"] = {"price": price, "change": _num(t.get("priceChange")),
|
|
237
|
+
"change_percent": _num(t.get("priceChangePercent")),
|
|
238
|
+
"volume": _num(t.get("quoteVolume")), "time": now, "day": day}
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _tencent(symbols: list[str], name: str) -> dict[str, dict]:
|
|
243
|
+
from ..data.extra_sources import tencent_symbol
|
|
244
|
+
from ..data.listings import _tencent_quote_lines
|
|
245
|
+
|
|
246
|
+
tz = ZoneInfo(TIMEZONES[name])
|
|
247
|
+
out = {}
|
|
248
|
+
for f in _tencent_quote_lines([tencent_symbol(c, name) for c in symbols]):
|
|
249
|
+
if len(f) < 33 or not f[2]:
|
|
250
|
+
continue
|
|
251
|
+
price = _num(f[3])
|
|
252
|
+
if not price:
|
|
253
|
+
continue
|
|
254
|
+
try:
|
|
255
|
+
stamp = datetime.strptime(f[30][:14], "%Y%m%d%H%M%S").replace(tzinfo=tz)
|
|
256
|
+
except (ValueError, IndexError):
|
|
257
|
+
stamp = datetime.now(tz)
|
|
258
|
+
vol = _num(f[6])
|
|
259
|
+
if vol is not None and name == "china_a":
|
|
260
|
+
vol *= 100 # lots of 100 shares
|
|
261
|
+
out[f[2]] = {"price": price, "change": _num(f[31]), "change_percent": _num(f[32]), "volume": vol,
|
|
262
|
+
"time": stamp.timestamp(), "day": stamp.date()}
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _fetch_india(symbols: list[str]) -> dict[str, dict]:
|
|
267
|
+
"""Broker feed when it is set up (live); the regular feed otherwise, or
|
|
268
|
+
for names the broker can't price."""
|
|
269
|
+
out: dict[str, dict] = {}
|
|
270
|
+
if _broker_on():
|
|
271
|
+
from . import live_quotes
|
|
272
|
+
from .brokers import angel
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
for sym, q in angel.client(live_quotes._broker_config()).quotes(symbols).items():
|
|
276
|
+
out[sym] = {**q, "day": market_day("india", q.get("time"))}
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
logger.warning("Broker quotes for the live market failed, using the regular feed: %s", exc)
|
|
279
|
+
rest = [s for s in symbols if s not in out]
|
|
280
|
+
if rest:
|
|
281
|
+
out.update(_yahoo_daily(rest, "india"))
|
|
282
|
+
return out
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _yahoo_daily(symbols: list[str], name: str) -> dict[str, dict]:
|
|
286
|
+
"""Today's bar so far (price, change vs the previous close, volume) for
|
|
287
|
+
many names: daily bars are far lighter than minute bars."""
|
|
288
|
+
import yfinance as yf
|
|
289
|
+
|
|
290
|
+
from ..utils.yf_lock import YF_DOWNLOAD_LOCK
|
|
291
|
+
from .analysis_run import yahoo_symbol
|
|
292
|
+
|
|
293
|
+
out: dict[str, dict] = {}
|
|
294
|
+
now = _now()
|
|
295
|
+
for i in range(0, len(symbols), YAHOO_CHUNK):
|
|
296
|
+
tickers = {yahoo_symbol(s, name): s for s in symbols[i:i + YAHOO_CHUNK]}
|
|
297
|
+
with YF_DOWNLOAD_LOCK:
|
|
298
|
+
df = yf.download(list(tickers), period="5d", interval="1d", group_by="ticker", progress=False,
|
|
299
|
+
threads=True, auto_adjust=False, timeout=20)
|
|
300
|
+
if df is None or df.empty:
|
|
301
|
+
continue
|
|
302
|
+
for yt, sym in tickers.items():
|
|
303
|
+
try:
|
|
304
|
+
frame = (df[yt] if isinstance(df.columns, pd.MultiIndex) else df).dropna(subset=["Close"])
|
|
305
|
+
except KeyError:
|
|
306
|
+
continue
|
|
307
|
+
if len(frame) < 2:
|
|
308
|
+
continue
|
|
309
|
+
price, prev = float(frame["Close"].iloc[-1]), float(frame["Close"].iloc[-2])
|
|
310
|
+
out[sym] = {"price": round(price, 4), "change": round(price - prev, 4),
|
|
311
|
+
"change_percent": round((price / prev - 1) * 100, 2) if prev else None,
|
|
312
|
+
"volume": _num(frame["Volume"].iloc[-1]) if "Volume" in frame else None,
|
|
313
|
+
"time": now, "day": pd.Timestamp(frame.index[-1]).date()}
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
FETCHERS = {
|
|
318
|
+
"india": _fetch_india,
|
|
319
|
+
"us": _fetch_us,
|
|
320
|
+
"crypto": _fetch_crypto,
|
|
321
|
+
"china_a": lambda symbols: _tencent(symbols, "china_a"),
|
|
322
|
+
"hong_kong": lambda symbols: _tencent(symbols, "hong_kong"),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def merge(rows: list[dict], quotes: dict[str, dict], only_live: bool = False) -> list[dict]:
|
|
327
|
+
"""Listing rows with live price, change and volume folded in; the 52-week
|
|
328
|
+
range widens when a live price breaks it and market cap moves with price.
|
|
329
|
+
`only_live` drops rows of the quoted kind that have no live quote, so
|
|
330
|
+
today's moves are never mixed with an earlier session's."""
|
|
331
|
+
if not quotes:
|
|
332
|
+
return rows
|
|
333
|
+
out = []
|
|
334
|
+
for r in rows:
|
|
335
|
+
q = quotes.get(r.get("symbol"))
|
|
336
|
+
if q is None:
|
|
337
|
+
if not (only_live and r.get("type") in ("stock", "crypto")):
|
|
338
|
+
out.append(r)
|
|
339
|
+
continue
|
|
340
|
+
new = dict(r)
|
|
341
|
+
price, old = q["price"], r.get("price")
|
|
342
|
+
new["price"] = price
|
|
343
|
+
for key in ("change", "change_percent", "volume"):
|
|
344
|
+
if q.get(key) is not None:
|
|
345
|
+
new[key] = q[key]
|
|
346
|
+
if r.get("market_cap") and old:
|
|
347
|
+
new["market_cap"] = r["market_cap"] * price / old
|
|
348
|
+
if r.get("high_52w"):
|
|
349
|
+
new["high_52w"] = max(r["high_52w"], price)
|
|
350
|
+
if r.get("low_52w"):
|
|
351
|
+
new["low_52w"] = min(r["low_52w"], price)
|
|
352
|
+
new["live"] = True
|
|
353
|
+
out.append(new)
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ------------------------------------------------------------ latest tickers
|
|
358
|
+
|
|
359
|
+
_latest: dict[str, tuple[float, dict]] = {}
|
|
360
|
+
_latest_lock = threading.Lock()
|
|
361
|
+
_latest_busy: set[str] = set()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def latest(tickers: list[str], wait: bool = True) -> dict[str, dict]:
|
|
365
|
+
"""ticker -> {price, day, time} from one batched 1-minute download.
|
|
366
|
+
Cached LATEST_SECONDS; stale tickers refresh in the background, and only
|
|
367
|
+
tickers never seen before are fetched inline (when `wait`)."""
|
|
368
|
+
now = _now()
|
|
369
|
+
tickers = list(dict.fromkeys(tickers))
|
|
370
|
+
with _latest_lock:
|
|
371
|
+
have = {t: v for t, (at, v) in _latest.items() if t in tickers}
|
|
372
|
+
stale = [t for t in tickers if t not in _latest or now - _latest[t][0] >= LATEST_SECONDS]
|
|
373
|
+
stale = [t for t in stale if t not in _latest_busy]
|
|
374
|
+
_latest_busy.update(stale)
|
|
375
|
+
missing = [t for t in stale if t not in have]
|
|
376
|
+
background = [t for t in stale if t in have]
|
|
377
|
+
if missing:
|
|
378
|
+
if wait:
|
|
379
|
+
_fill_latest(missing)
|
|
380
|
+
else:
|
|
381
|
+
background += missing
|
|
382
|
+
if background:
|
|
383
|
+
threading.Thread(target=_fill_latest, args=(background,), name="live-latest", daemon=True).start()
|
|
384
|
+
with _latest_lock:
|
|
385
|
+
return {t: _latest[t][1] for t in tickers if t in _latest}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _fill_latest(tickers: list[str]) -> None:
|
|
389
|
+
try:
|
|
390
|
+
got = _yahoo_latest(tickers)
|
|
391
|
+
except Exception as exc:
|
|
392
|
+
logger.info("Latest prices failed: %s", exc)
|
|
393
|
+
got = {}
|
|
394
|
+
now = _now()
|
|
395
|
+
with _latest_lock:
|
|
396
|
+
for t in tickers:
|
|
397
|
+
_latest_busy.discard(t)
|
|
398
|
+
if t in got:
|
|
399
|
+
_latest[t] = (now, got[t])
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _yahoo_latest(tickers: list[str]) -> dict[str, dict]:
|
|
403
|
+
import yfinance as yf
|
|
404
|
+
|
|
405
|
+
from ..utils.yf_lock import YF_DOWNLOAD_LOCK
|
|
406
|
+
|
|
407
|
+
with YF_DOWNLOAD_LOCK:
|
|
408
|
+
df = yf.download(tickers, period="5d", interval="1m", group_by="ticker", progress=False,
|
|
409
|
+
threads=True, auto_adjust=False, prepost=False, timeout=15)
|
|
410
|
+
out = {}
|
|
411
|
+
if df is None or df.empty:
|
|
412
|
+
return out
|
|
413
|
+
for t in tickers:
|
|
414
|
+
try:
|
|
415
|
+
frame = (df[t] if isinstance(df.columns, pd.MultiIndex) else df).dropna(subset=["Close"])
|
|
416
|
+
except KeyError:
|
|
417
|
+
continue
|
|
418
|
+
if frame.empty:
|
|
419
|
+
continue
|
|
420
|
+
stamp = pd.Timestamp(frame.index[-1])
|
|
421
|
+
out[t] = {"price": float(frame["Close"].iloc[-1]), "day": stamp.date(), "time": stamp.timestamp()}
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def extend(series: pd.Series, price: Optional[float], day: Optional[date]) -> pd.Series:
|
|
426
|
+
"""A daily close series with `price` as the close of `day`: replaces the
|
|
427
|
+
last bar when it is that day, appends one when the day is newer, and
|
|
428
|
+
leaves the series alone for an older day (or no price)."""
|
|
429
|
+
if price is None or day is None or series is None or not len(series):
|
|
430
|
+
return series
|
|
431
|
+
last = pd.Timestamp(series.index[-1])
|
|
432
|
+
if last.date() > day:
|
|
433
|
+
return series
|
|
434
|
+
s = series.copy()
|
|
435
|
+
if last.date() == day:
|
|
436
|
+
s.iloc[-1] = price
|
|
437
|
+
return s
|
|
438
|
+
stamp = pd.Timestamp(day)
|
|
439
|
+
if last.tzinfo is not None:
|
|
440
|
+
stamp = stamp.tz_localize(last.tzinfo)
|
|
441
|
+
s.loc[stamp] = price
|
|
442
|
+
return s
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def extend_all(closes: dict[str, pd.Series], prices: dict[str, dict]) -> dict[str, pd.Series]:
|
|
446
|
+
"""`extend` for every series that has a live {price, day}."""
|
|
447
|
+
out = dict(closes)
|
|
448
|
+
for t, q in prices.items():
|
|
449
|
+
if t in out and q:
|
|
450
|
+
out[t] = extend(out[t], q.get("price"), q.get("day"))
|
|
451
|
+
return out
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def for_tickers(name: str, quotes: dict[str, dict]) -> dict[str, dict]:
|
|
455
|
+
"""Market quotes keyed by the app's symbols -> keyed by Yahoo tickers,
|
|
456
|
+
for `extend_all` on daily close series."""
|
|
457
|
+
from .analysis_run import yahoo_symbol
|
|
458
|
+
|
|
459
|
+
return {yahoo_symbol(sym, name): q for sym, q in quotes.items()}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def status_for(prices: dict[str, dict], source: str = "Market data feed, can lag up to 15 min") -> dict:
|
|
463
|
+
"""The `live` block for views priced by `latest` (no market cache):
|
|
464
|
+
live while something in it traded in the last 15 minutes."""
|
|
465
|
+
times = [q["time"] for q in prices.values() if q.get("time")]
|
|
466
|
+
newest = max(times) if times else None
|
|
467
|
+
age = _now() - newest if newest else None
|
|
468
|
+
return {"open": age is not None and age < 900, "live": bool(prices), "updated": newest,
|
|
469
|
+
"age_seconds": round(age, 1) if age is not None else None, "refresh_seconds": LATEST_SECONDS,
|
|
470
|
+
"covered": len(prices), "source": source, "error": None, "version": None}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
_memo: dict[tuple, tuple[float, object]] = {}
|
|
474
|
+
_memo_lock = threading.Lock()
|
|
475
|
+
MEMO_SECONDS = 15
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def memo(key: tuple, fn, seconds: float = MEMO_SECONDS):
|
|
479
|
+
"""fn() shared by every request for `seconds`: many people polling a
|
|
480
|
+
view cost one recomputation per interval."""
|
|
481
|
+
now = _now()
|
|
482
|
+
with _memo_lock:
|
|
483
|
+
hit = _memo.get(key)
|
|
484
|
+
if hit and now - hit[0] < seconds:
|
|
485
|
+
return hit[1]
|
|
486
|
+
value = fn()
|
|
487
|
+
with _memo_lock:
|
|
488
|
+
_memo[key] = (now, value)
|
|
489
|
+
return value
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _num(value) -> Optional[float]:
|
|
493
|
+
try:
|
|
494
|
+
v = float(value)
|
|
495
|
+
except (TypeError, ValueError):
|
|
496
|
+
return None
|
|
497
|
+
return v if v == v else None # NaN -> None
|