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,196 @@
|
|
|
1
|
+
"""Ready-made ETF screens. ETFs carry no earnings of their own, so like the
|
|
2
|
+
crypto screens (marketmyna/services/crypto_screens.py) these work on price and
|
|
3
|
+
volume: returns over 1 / 3 / 12 months, strength against the home index,
|
|
4
|
+
volatility, distance from the 52-week high, liquidity.
|
|
5
|
+
|
|
6
|
+
Universe per market (india, us, hong_kong, china_a): the hand-picked ETFs of
|
|
7
|
+
the ETF rotation view (marketmyna/services/etf_rotation.py) plus the market's most
|
|
8
|
+
traded ETFs by turnover in our listing, up to UNIVERSE. Leveraged and inverse
|
|
9
|
+
funds are left out: their returns are not comparable. A year of daily closes
|
|
10
|
+
from the bar cache. Same result shape as marketmyna/services/screens.py.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import math
|
|
17
|
+
import re
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
from typing import Callable, Optional
|
|
21
|
+
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from .crypto_screens import _passes, _ret
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
MARKETS = ("india", "us", "hong_kong", "china_a")
|
|
29
|
+
UNIVERSE = 120
|
|
30
|
+
CACHE_SECONDS = 3600
|
|
31
|
+
LEVERAGED = re.compile(r"\b(\d(\.\d)?x|ultra|ultrapro|inverse|bear|short|leveraged|daily (2|3)x)\b", re.I)
|
|
32
|
+
|
|
33
|
+
# Large US funds the rotation view leaves out (it groups by theme, not by size).
|
|
34
|
+
US_CORE = [
|
|
35
|
+
("SPY", "SPDR S&P 500"), ("VOO", "Vanguard S&P 500"), ("IVV", "iShares Core S&P 500"), ("VTI", "Vanguard Total Stock Market"),
|
|
36
|
+
("DIA", "SPDR Dow Jones"), ("VUG", "Vanguard Growth"), ("VTV", "Vanguard Value"), ("SCHD", "Schwab US Dividend Equity"),
|
|
37
|
+
("VEA", "Vanguard Developed Markets"), ("VWO", "Vanguard Emerging Markets"), ("IEMG", "iShares Core MSCI Emerging"),
|
|
38
|
+
("VNQ", "Vanguard Real Estate"), ("BND", "Vanguard Total Bond"), ("AGG", "iShares Core US Aggregate Bond"),
|
|
39
|
+
("IEF", "iShares 7-10 Year Treasury"), ("SHY", "iShares 1-3 Year Treasury"), ("LQD", "iShares Investment Grade Corporate"),
|
|
40
|
+
("SLV", "iShares Silver"), ("IAU", "iShares Gold"), ("XLK", "Technology Select Sector"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
LABELS = {
|
|
44
|
+
"volume": "Avg daily turnover", "ret_1m": "1M %", "ret_3m": "3M %", "ret_1y": "1Y %", "vs_index_3m": "vs index 3M pts",
|
|
45
|
+
"volatility": "Volatility %/yr", "from_high": "From 52w high %", "above_ma200": "Above 200d avg %",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# key: title, description, [(field, op, value)], (sort field, descending), columns
|
|
49
|
+
PRESETS = {
|
|
50
|
+
"most_traded": ("Most traded", "The most liquid ETFs by average daily turnover (20 days)",
|
|
51
|
+
[], ("volume", True), ["volume", "ret_1m", "ret_1y", "volatility"]),
|
|
52
|
+
"best_1y": ("Best 1-year return", "Highest price return over the past 12 months",
|
|
53
|
+
[("ret_1y", ">", 0)], ("ret_1y", True), ["ret_1y", "ret_3m", "volatility", "from_high"]),
|
|
54
|
+
"momentum": ("Strong momentum", "Up over 5% in 3 months and above the 200-day average",
|
|
55
|
+
[("ret_3m", ">", 5), ("above_ma200", ">", 0)], ("ret_3m", True), ["ret_3m", "ret_1m", "above_ma200", "volume"]),
|
|
56
|
+
"beating_index": ("Beating the index", "3-month return ahead of the home market index",
|
|
57
|
+
[("vs_index_3m", ">", 0)], ("vs_index_3m", True), ["vs_index_3m", "ret_3m", "ret_1y", "volatility"]),
|
|
58
|
+
"low_volatility": ("Low volatility", "Calmest price path over the past year (under 15%/yr)",
|
|
59
|
+
[("volatility", "<", 15)], ("volatility", False), ["volatility", "ret_1y", "from_high", "volume"]),
|
|
60
|
+
"near_high": ("Near 52-week high", "Within 3% of the 52-week closing high",
|
|
61
|
+
[("from_high", ">", -3)], ("from_high", True), ["from_high", "ret_3m", "ret_1y", "volume"]),
|
|
62
|
+
"drawdown": ("Deep drawdown", "More than 20% below the 52-week high",
|
|
63
|
+
[("from_high", "<", -20)], ("from_high", False), ["from_high", "ret_1y", "volatility", "volume"]),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ticker -> daily bars with "close" and (where the feed has it) "volume".
|
|
67
|
+
Fetch = Callable[[list[str]], dict[str, pd.DataFrame]]
|
|
68
|
+
|
|
69
|
+
_cache: dict[str, tuple[float, tuple[list[dict], dict]]] = {}
|
|
70
|
+
_lock = threading.Lock()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _bars(tickers: list[str]) -> dict[str, pd.DataFrame]:
|
|
74
|
+
from ..trading.scanner import _cache as bars
|
|
75
|
+
|
|
76
|
+
return bars.get(tickers, "1d", "1y")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def turnover(df: pd.DataFrame) -> Optional[float]:
|
|
80
|
+
"""Average daily traded value over the last 20 sessions, in the fund's currency."""
|
|
81
|
+
if "volume" not in df:
|
|
82
|
+
return None
|
|
83
|
+
value = (df["close"].astype(float) * df["volume"].astype(float)).dropna().tail(20)
|
|
84
|
+
return float(value.mean()) if len(value) and value.mean() > 0 else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _leveraged(r: dict, market: str = "") -> bool:
|
|
88
|
+
sym = r.get("symbol") or ""
|
|
89
|
+
# HKEX lists its leveraged and inverse products under stock codes 07200-07599.
|
|
90
|
+
if market == "hong_kong" and sym.isdigit() and 7200 <= int(sym) <= 7599:
|
|
91
|
+
return True
|
|
92
|
+
return r.get("category") == "Leveraged & Inverse" or bool(LEVERAGED.search(r.get("name") or ""))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def universe(market: str, rows: list[dict]) -> list[dict]:
|
|
96
|
+
"""[{symbol, name, ticker, ...listing row}] for the market, liquid first."""
|
|
97
|
+
from .analysis_run import yahoo_symbol
|
|
98
|
+
from .etf_rotation import UNIVERSES, app_symbol
|
|
99
|
+
|
|
100
|
+
etfs = {r["symbol"]: r for r in rows if r.get("type") == "etf" and not _leveraged(r, market)}
|
|
101
|
+
picked: dict[str, dict] = {}
|
|
102
|
+
curated = [pair for group in UNIVERSES.get(market, {}).get("views", {}).values() for pair in group]
|
|
103
|
+
if market == "us":
|
|
104
|
+
curated = US_CORE + curated
|
|
105
|
+
for ticker, name in curated:
|
|
106
|
+
sym = app_symbol(ticker, market)
|
|
107
|
+
picked.setdefault(sym, {**etfs.get(sym, {}), "symbol": sym, "name": (etfs.get(sym) or {}).get("name") or name, "ticker": ticker})
|
|
108
|
+
traded = sorted((r for r in etfs.values() if r.get("volume") and r.get("price")),
|
|
109
|
+
key=lambda r: -(r["volume"] * r["price"]))
|
|
110
|
+
for r in traded:
|
|
111
|
+
if len(picked) >= UNIVERSE:
|
|
112
|
+
break
|
|
113
|
+
picked.setdefault(r["symbol"], {**r, "ticker": yahoo_symbol(r["symbol"], market)})
|
|
114
|
+
return list(picked.values())[:UNIVERSE]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def metrics(s: pd.Series, index_3m: Optional[float]) -> dict:
|
|
118
|
+
s = s.dropna()
|
|
119
|
+
out = {"ret_1m": _ret(s, 21), "ret_3m": _ret(s, 63)}
|
|
120
|
+
# A year of bars is 240-252 sessions depending on the market's holidays: measure from the first one.
|
|
121
|
+
covers_year = len(s) > 1 and (s.index[-1] - s.index[0]).days >= 330
|
|
122
|
+
out["ret_1y"] = float(s.iloc[-1] / s.iloc[0] - 1) * 100 if covers_year else None
|
|
123
|
+
out["from_high"] = float(s.iloc[-1] / s.max() - 1) * 100 if len(s) else None
|
|
124
|
+
out["above_ma200"] = float(s.iloc[-1] / s.tail(200).mean() - 1) * 100 if len(s) >= 200 else None
|
|
125
|
+
daily = s.pct_change().dropna()
|
|
126
|
+
out["volatility"] = float(daily.std() * math.sqrt(252) * 100) if len(daily) > 60 else None
|
|
127
|
+
out["vs_index_3m"] = out["ret_3m"] - index_3m if out["ret_3m"] is not None and index_3m is not None else None
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _rule_text(rule) -> str:
|
|
132
|
+
field, op, value = rule
|
|
133
|
+
return f"{LABELS.get(field, field)} {op} {value:g}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def list_presets() -> list[dict]:
|
|
137
|
+
return [{"key": k, "title": p[0], "description": p[1], "large_only": False, "rules": [_rule_text(r) for r in p[2]]}
|
|
138
|
+
for k, p in PRESETS.items()]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _measured(market: str, rows: Optional[list[dict]], fetch: Fetch) -> tuple[list[dict], dict]:
|
|
142
|
+
from .analysis_run import MARKET_INDICES
|
|
143
|
+
|
|
144
|
+
if rows is None:
|
|
145
|
+
from ..data.listings import get_store
|
|
146
|
+
|
|
147
|
+
rows = get_store().rows(market)
|
|
148
|
+
funds = universe(market, rows)
|
|
149
|
+
index = MARKET_INDICES[market][0][0]
|
|
150
|
+
bars = fetch([f["ticker"] for f in funds] + [index])
|
|
151
|
+
closes = {t: df["close"].astype(float).dropna() for t, df in bars.items()}
|
|
152
|
+
index_3m = _ret(closes[index], 63) if index in closes else None
|
|
153
|
+
return funds, {f["symbol"]: {**metrics(closes[f["ticker"]], index_3m), "volume": turnover(bars[f["ticker"]])}
|
|
154
|
+
for f in funds if f["ticker"] in closes and len(closes[f["ticker"]]) > 60}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def run(key: str, market: str, fetch: Fetch = _bars, rows: Optional[list[dict]] = None, use_cache: bool = True) -> dict:
|
|
158
|
+
from .analysis_run import MARKET_INDICES
|
|
159
|
+
|
|
160
|
+
if market not in MARKETS:
|
|
161
|
+
raise ValueError("ETF screens cover India, US, Hong Kong and China A")
|
|
162
|
+
if key not in PRESETS:
|
|
163
|
+
raise ValueError(f"Unknown screen '{key}'")
|
|
164
|
+
title, desc, rules, (sort_field, desc_sort), columns = PRESETS[key]
|
|
165
|
+
hit = None
|
|
166
|
+
if use_cache:
|
|
167
|
+
with _lock:
|
|
168
|
+
hit = _cache.get(market)
|
|
169
|
+
if hit and time.time() - hit[0] < CACHE_SECONDS:
|
|
170
|
+
funds, ms = hit[1]
|
|
171
|
+
else:
|
|
172
|
+
funds, ms = _measured(market, rows, fetch)
|
|
173
|
+
if use_cache and ms:
|
|
174
|
+
with _lock:
|
|
175
|
+
_cache[market] = (time.time(), (funds, ms))
|
|
176
|
+
|
|
177
|
+
hits = []
|
|
178
|
+
for f in funds:
|
|
179
|
+
m = ms.get(f["symbol"])
|
|
180
|
+
if m is None:
|
|
181
|
+
continue
|
|
182
|
+
if _passes(m, rules):
|
|
183
|
+
hits.append({"symbol": f["symbol"], "name": f.get("name"), "market": market, "market_cap": None,
|
|
184
|
+
"volume": m["volume"], "price": f.get("price"), "currency": f.get("currency"),
|
|
185
|
+
"change_percent": f.get("change_percent"), "sector": f.get("category"),
|
|
186
|
+
**{col: m.get(col) for col in columns}})
|
|
187
|
+
hits.sort(key=lambda h: (h.get(sort_field) is None, -(h.get(sort_field) or 0) if desc_sort else (h.get(sort_field) or 0)))
|
|
188
|
+
return {
|
|
189
|
+
"key": key, "title": title, "description": desc, "market": market,
|
|
190
|
+
"rules": [_rule_text(r) for r in rules] or ["Ranked by turnover"],
|
|
191
|
+
"columns": [{"key": c, "label": LABELS.get(c, c)} for c in columns],
|
|
192
|
+
"universe": f"{len(funds)} large and most-traded ETFs (leveraged and inverse excluded), "
|
|
193
|
+
f"index: {MARKET_INDICES[market][0][1]}",
|
|
194
|
+
"scanned": len(ms),
|
|
195
|
+
"results": hits[:50],
|
|
196
|
+
}
|