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,197 @@
|
|
|
1
|
+
"""Stock data fetcher using Yahoo Finance."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import yfinance as yf
|
|
8
|
+
|
|
9
|
+
from ..analysis.fundamental import yfinance_fundamentals
|
|
10
|
+
from ..core.constants import TimeFrame
|
|
11
|
+
from .base import DataFetcher
|
|
12
|
+
from .extra_sources import (
|
|
13
|
+
alphavantage_history,
|
|
14
|
+
alphavantage_key,
|
|
15
|
+
alphavantage_quote,
|
|
16
|
+
finnhub_key,
|
|
17
|
+
finnhub_quote,
|
|
18
|
+
nasdaq_history,
|
|
19
|
+
nasdaq_quote,
|
|
20
|
+
polygon_history,
|
|
21
|
+
polygon_key,
|
|
22
|
+
polygon_quote,
|
|
23
|
+
tiingo_history,
|
|
24
|
+
tiingo_key,
|
|
25
|
+
tiingo_quote,
|
|
26
|
+
twelvedata_history,
|
|
27
|
+
twelvedata_key,
|
|
28
|
+
twelvedata_quote,
|
|
29
|
+
)
|
|
30
|
+
from .resilience import FallbackChain
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _plain_us(symbol: str) -> bool:
|
|
34
|
+
return symbol.replace(".", "").replace("-", "").isalpha()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StockDataFetcher(DataFetcher):
|
|
38
|
+
"""Fetches stock data from Yahoo Finance (US & Global markets).
|
|
39
|
+
|
|
40
|
+
Yahoo first; plain US tickers then fall back to the keyed sources
|
|
41
|
+
configured (Finnhub, Twelve Data, Tiingo, Alpha Vantage, Polygon) and
|
|
42
|
+
Nasdaq, which needs no key. Index and foreign symbols (^GSPC,
|
|
43
|
+
RELIANCE.NS) have only Yahoo.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self._cache = {}
|
|
48
|
+
|
|
49
|
+
def _fetch_quote_yfinance(self, symbol: str) -> dict:
|
|
50
|
+
ticker = yf.Ticker(symbol)
|
|
51
|
+
info = ticker.fast_info
|
|
52
|
+
try:
|
|
53
|
+
price = info.last_price
|
|
54
|
+
except KeyError:
|
|
55
|
+
return {} # unknown symbol: empty, not a source failure (see india.py)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"symbol": symbol,
|
|
59
|
+
"price": price,
|
|
60
|
+
"previous_close": info.previous_close,
|
|
61
|
+
"open": info.open,
|
|
62
|
+
"day_high": info.day_high,
|
|
63
|
+
"day_low": info.day_low,
|
|
64
|
+
"volume": info.last_volume,
|
|
65
|
+
"market_cap": info.market_cap,
|
|
66
|
+
"change": info.last_price - info.previous_close,
|
|
67
|
+
"change_percent": ((info.last_price - info.previous_close) / info.previous_close) * 100
|
|
68
|
+
if info.previous_close
|
|
69
|
+
else 0,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def get_quote(self, symbol: str) -> dict:
|
|
73
|
+
"""Get current quote for a stock."""
|
|
74
|
+
chain = FallbackChain("stocks.get_quote").add("yfinance", self._fetch_quote_yfinance)
|
|
75
|
+
# Keyed backups for US tickers (index/foreign symbols like ^GSPC or
|
|
76
|
+
# RELIANCE.NS aren't covered by these APIs' free tiers).
|
|
77
|
+
if _plain_us(symbol):
|
|
78
|
+
if finnhub_key():
|
|
79
|
+
chain.add("finnhub", finnhub_quote)
|
|
80
|
+
if twelvedata_key():
|
|
81
|
+
chain.add("twelvedata", twelvedata_quote)
|
|
82
|
+
if tiingo_key():
|
|
83
|
+
chain.add("tiingo", tiingo_quote)
|
|
84
|
+
chain.add("nasdaq", nasdaq_quote)
|
|
85
|
+
if alphavantage_key():
|
|
86
|
+
chain.add("alphavantage", alphavantage_quote)
|
|
87
|
+
if polygon_key():
|
|
88
|
+
chain.add("polygon", polygon_quote)
|
|
89
|
+
return chain.call(symbol, is_empty=lambda d: not d or d.get("price") is None)
|
|
90
|
+
|
|
91
|
+
def _fetch_history_yfinance(
|
|
92
|
+
self,
|
|
93
|
+
symbol: str,
|
|
94
|
+
period: str = "1y",
|
|
95
|
+
interval: TimeFrame = TimeFrame.DAY_1,
|
|
96
|
+
start: Optional[datetime] = None,
|
|
97
|
+
end: Optional[datetime] = None,
|
|
98
|
+
) -> pd.DataFrame:
|
|
99
|
+
ticker = yf.Ticker(symbol)
|
|
100
|
+
|
|
101
|
+
if start and end:
|
|
102
|
+
df = ticker.history(start=start, end=end, interval=interval.value)
|
|
103
|
+
else:
|
|
104
|
+
df = ticker.history(period=period, interval=interval.value)
|
|
105
|
+
|
|
106
|
+
if df.empty:
|
|
107
|
+
return pd.DataFrame()
|
|
108
|
+
|
|
109
|
+
# Standardize column names
|
|
110
|
+
df.columns = [col.lower().replace(" ", "_") for col in df.columns]
|
|
111
|
+
return df
|
|
112
|
+
|
|
113
|
+
def get_history(
|
|
114
|
+
self,
|
|
115
|
+
symbol: str,
|
|
116
|
+
period: str = "1y",
|
|
117
|
+
interval: TimeFrame = TimeFrame.DAY_1,
|
|
118
|
+
start: Optional[datetime] = None,
|
|
119
|
+
end: Optional[datetime] = None,
|
|
120
|
+
) -> pd.DataFrame:
|
|
121
|
+
"""Get historical price data for a stock."""
|
|
122
|
+
chain = FallbackChain("stocks.get_history").add("yfinance", self._fetch_history_yfinance)
|
|
123
|
+
# Daily bars only: an intraday request skips these (they raise).
|
|
124
|
+
if _plain_us(symbol):
|
|
125
|
+
if twelvedata_key():
|
|
126
|
+
chain.add("twelvedata", twelvedata_history)
|
|
127
|
+
if tiingo_key():
|
|
128
|
+
chain.add("tiingo", tiingo_history)
|
|
129
|
+
if polygon_key():
|
|
130
|
+
chain.add("polygon", polygon_history)
|
|
131
|
+
chain.add("nasdaq", nasdaq_history)
|
|
132
|
+
if alphavantage_key():
|
|
133
|
+
chain.add("alphavantage", alphavantage_history)
|
|
134
|
+
return chain.call(symbol, period=period, interval=interval, start=start, end=end, is_empty=lambda df: df.empty)
|
|
135
|
+
|
|
136
|
+
def search(self, query: str) -> list[dict]:
|
|
137
|
+
"""Search for stocks matching query."""
|
|
138
|
+
try:
|
|
139
|
+
ticker = yf.Ticker(query)
|
|
140
|
+
info = ticker.info
|
|
141
|
+
if info and "symbol" in info:
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
"symbol": info.get("symbol", query),
|
|
145
|
+
"name": info.get("longName", info.get("shortName", "")),
|
|
146
|
+
"exchange": info.get("exchange", ""),
|
|
147
|
+
"type": info.get("quoteType", ""),
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
def get_info(self, symbol: str) -> dict:
|
|
155
|
+
"""Get detailed information about a stock."""
|
|
156
|
+
ticker = yf.Ticker(symbol)
|
|
157
|
+
info = ticker.info
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"symbol": info.get("symbol", symbol),
|
|
161
|
+
"name": info.get("longName", ""),
|
|
162
|
+
"sector": info.get("sector", ""),
|
|
163
|
+
"industry": info.get("industry", ""),
|
|
164
|
+
"description": info.get("longBusinessSummary", ""),
|
|
165
|
+
"website": info.get("website", ""),
|
|
166
|
+
"employees": info.get("fullTimeEmployees", 0),
|
|
167
|
+
"country": info.get("country", ""),
|
|
168
|
+
"currency": info.get("currency", "USD"),
|
|
169
|
+
# Valuation metrics
|
|
170
|
+
"market_cap": info.get("marketCap", 0),
|
|
171
|
+
"pe_ratio": info.get("trailingPE", 0),
|
|
172
|
+
"forward_pe": info.get("forwardPE", 0),
|
|
173
|
+
"peg_ratio": info.get("pegRatio", 0),
|
|
174
|
+
"price_to_book": info.get("priceToBook", 0),
|
|
175
|
+
"price_to_sales": info.get("priceToSalesTrailing12Months", 0),
|
|
176
|
+
# Financial metrics
|
|
177
|
+
"revenue": info.get("totalRevenue", 0),
|
|
178
|
+
"gross_profit": info.get("grossProfits", 0),
|
|
179
|
+
"ebitda": info.get("ebitda", 0),
|
|
180
|
+
"net_income": info.get("netIncomeToCommon", 0),
|
|
181
|
+
"eps": info.get("trailingEps", 0),
|
|
182
|
+
# Dividend info
|
|
183
|
+
"dividend_yield": info.get("dividendYield", 0),
|
|
184
|
+
"dividend_rate": info.get("dividendRate", 0),
|
|
185
|
+
"payout_ratio": info.get("payoutRatio", 0),
|
|
186
|
+
# Trading info
|
|
187
|
+
"52_week_high": info.get("fiftyTwoWeekHigh", 0),
|
|
188
|
+
"52_week_low": info.get("fiftyTwoWeekLow", 0),
|
|
189
|
+
"50_day_avg": info.get("fiftyDayAverage", 0),
|
|
190
|
+
"200_day_avg": info.get("twoHundredDayAverage", 0),
|
|
191
|
+
"avg_volume": info.get("averageVolume", 0),
|
|
192
|
+
"beta": info.get("beta", 0),
|
|
193
|
+
# Normalized ratios for FundamentalAnalyzer (overrides the raw
|
|
194
|
+
# yfinance-unit fields above, e.g. dividend_yield)
|
|
195
|
+
**yfinance_fundamentals(info),
|
|
196
|
+
}
|
|
197
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""A price from a Yahoo `info` dict that is safe to use.
|
|
2
|
+
|
|
3
|
+
Yahoo's quote fields go stale for some listings while their daily bars stay
|
|
4
|
+
current: Indian REITs and InvITs report a `regularMarketPrice` frozen in
|
|
5
|
+
2024 with no `currentPrice` at all. A price older than MAX_AGE is treated as
|
|
6
|
+
missing (or replaced by the latest daily close when a ticker is given), so
|
|
7
|
+
a yield or upside is never worked out from it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
MAX_AGE = 10 * 24 * 3600 # a week of holidays plus a weekend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _num(value: Any) -> Optional[float]:
|
|
20
|
+
try:
|
|
21
|
+
v = float(value)
|
|
22
|
+
except (TypeError, ValueError):
|
|
23
|
+
return None
|
|
24
|
+
return v if math.isfinite(v) and v > 0 else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def info_price(info: dict, now: Optional[float] = None) -> Optional[float]:
|
|
28
|
+
"""currentPrice, else a regularMarketPrice that is still fresh."""
|
|
29
|
+
price = _num(info.get("currentPrice"))
|
|
30
|
+
if price:
|
|
31
|
+
return price
|
|
32
|
+
price = _num(info.get("regularMarketPrice"))
|
|
33
|
+
stamp = _num(info.get("regularMarketTime"))
|
|
34
|
+
if price and stamp and (now or time.time()) - stamp > MAX_AGE:
|
|
35
|
+
return None
|
|
36
|
+
return price
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fresh_price(info: dict, ticker=None) -> Optional[float]:
|
|
40
|
+
"""info_price(), falling back to the ticker's latest daily close."""
|
|
41
|
+
price = info_price(info)
|
|
42
|
+
if price or ticker is None:
|
|
43
|
+
return price
|
|
44
|
+
try:
|
|
45
|
+
bars = ticker.history(period="5d", interval="1d")
|
|
46
|
+
return _num(bars["Close"].iloc[-1]) if not bars.empty else None
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
marketmyna/launcher.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Start MarketMyna: the `marketmyna` command (pip / pipx install) and start.py.
|
|
2
|
+
|
|
3
|
+
marketmyna # http://127.0.0.1:8001, opens your browser
|
|
4
|
+
marketmyna --port 9000 --no-browser
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import urllib.request
|
|
15
|
+
import webbrowser
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parser() -> argparse.ArgumentParser:
|
|
19
|
+
p = argparse.ArgumentParser(prog="marketmyna", description="Start MarketMyna.")
|
|
20
|
+
p.add_argument("--host", default="127.0.0.1",
|
|
21
|
+
help="address to listen on (default 127.0.0.1: only this computer can reach it)")
|
|
22
|
+
p.add_argument("--port", type=int, default=8001)
|
|
23
|
+
p.add_argument("--no-browser", action="store_true", help="don't open the browser")
|
|
24
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {_version()}")
|
|
25
|
+
return p
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _version() -> str:
|
|
29
|
+
from marketmyna import __version__
|
|
30
|
+
return __version__
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def open_when_ready(url: str) -> None:
|
|
34
|
+
for _ in range(120):
|
|
35
|
+
try:
|
|
36
|
+
with urllib.request.urlopen(f"{url}/health", timeout=2):
|
|
37
|
+
webbrowser.open(url)
|
|
38
|
+
return
|
|
39
|
+
except OSError:
|
|
40
|
+
time.sleep(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run(args: argparse.Namespace) -> None:
|
|
44
|
+
try:
|
|
45
|
+
import uvicorn
|
|
46
|
+
except ImportError:
|
|
47
|
+
sys.exit("Install the requirements first: pip install -r requirements-web.txt")
|
|
48
|
+
url = f"http://{'127.0.0.1' if args.host in ('0.0.0.0', '::') else args.host}:{args.port}"
|
|
49
|
+
print(f"MarketMyna: {url} (Ctrl+C to stop)")
|
|
50
|
+
if not args.no_browser:
|
|
51
|
+
threading.Thread(target=open_when_ready, args=(url,), daemon=True).start()
|
|
52
|
+
# One process: the scheduler, price alerts and the Telegram bot run inside it.
|
|
53
|
+
# Ctrl+C stops within a few seconds even while a slow data download is running.
|
|
54
|
+
try:
|
|
55
|
+
uvicorn.run("marketmyna.api.app:app", host=args.host, port=args.port, timeout_graceful_shutdown=5)
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
pass
|
|
58
|
+
# The app has shut down; don't wait minutes at exit for download threads
|
|
59
|
+
# (a first China / Hong Kong listing fetch) that nothing will read.
|
|
60
|
+
if any(t.is_alive() and not t.daemon for t in threading.enumerate() if t is not threading.main_thread()):
|
|
61
|
+
sys.stdout.flush()
|
|
62
|
+
sys.stderr.flush()
|
|
63
|
+
os._exit(0)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> None:
|
|
67
|
+
run(parser().parse_args(argv))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""LLM generation backend layer.
|
|
2
|
+
|
|
3
|
+
A GenerationBackend Protocol (marketmyna/llm/protocol.py) is the swappable
|
|
4
|
+
interface; backend_factory.create_generation_backend(config) builds a
|
|
5
|
+
concrete implementation chosen by backend_registry from Config, the
|
|
6
|
+
same "small pluggable backend" shape daily_stock_analysis uses for its
|
|
7
|
+
own litellm/local-CLI backends.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .backend_factory import create_generation_backend
|
|
13
|
+
from .backend_registry import LITELLM_BACKEND_ID, resolve_generation_backend_id
|
|
14
|
+
from .litellm_backend import LiteLLMGenerationBackend
|
|
15
|
+
from .protocol import (
|
|
16
|
+
GenerationBackend,
|
|
17
|
+
GenerationCapabilities,
|
|
18
|
+
GenerationError,
|
|
19
|
+
GenerationErrorCode,
|
|
20
|
+
GenerationResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"GenerationBackend",
|
|
25
|
+
"GenerationCapabilities",
|
|
26
|
+
"GenerationError",
|
|
27
|
+
"GenerationErrorCode",
|
|
28
|
+
"GenerationResult",
|
|
29
|
+
"LiteLLMGenerationBackend",
|
|
30
|
+
"LITELLM_BACKEND_ID",
|
|
31
|
+
"resolve_generation_backend_id",
|
|
32
|
+
"create_generation_backend",
|
|
33
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Builds a concrete GenerationBackend from Config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .backend_registry import LITELLM_BACKEND_ID, resolve_generation_backend_id
|
|
6
|
+
from .litellm_backend import LiteLLMGenerationBackend
|
|
7
|
+
from .protocol import GenerationBackend, GenerationError, GenerationErrorCode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_generation_backend(config, tier: str = "deep") -> GenerationBackend:
|
|
11
|
+
"""tier="deep": the main model chain, for decisions and idea picks.
|
|
12
|
+
tier="fast": the cheaper level-1 model first (commentary, news tone,
|
|
13
|
+
translation), falling back to the main chain. The deep backend also
|
|
14
|
+
exposes `.fast`, so agents can pick the right level per job.
|
|
15
|
+
|
|
16
|
+
Saved AI keys come first, then the config.json / env setup
|
|
17
|
+
(marketmyna/services/ai_access.py)."""
|
|
18
|
+
from ..services.ai_access import wrap_backend
|
|
19
|
+
|
|
20
|
+
return wrap_backend(config, _create_platform_backend, tier)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _create_platform_backend(config, tier: str = "deep") -> GenerationBackend:
|
|
24
|
+
backend_id = resolve_generation_backend_id(config)
|
|
25
|
+
|
|
26
|
+
if backend_id == LITELLM_BACKEND_ID:
|
|
27
|
+
from .channels import FailoverGenerationBackend, build_routes
|
|
28
|
+
from .store import LLMStore
|
|
29
|
+
|
|
30
|
+
routes = build_routes(config, tier=tier)
|
|
31
|
+
if not routes:
|
|
32
|
+
# Raises a clear "No llm_model configured" on first use.
|
|
33
|
+
return LiteLLMGenerationBackend(config)
|
|
34
|
+
try:
|
|
35
|
+
store = LLMStore(config)
|
|
36
|
+
except Exception: # cache/usage are optional extras
|
|
37
|
+
store = None
|
|
38
|
+
backend = FailoverGenerationBackend(config, routes, store=store, tier=tier)
|
|
39
|
+
if tier == "deep" and getattr(config, "llm_fast_model", None):
|
|
40
|
+
backend.fast = FailoverGenerationBackend(config, build_routes(config, tier="fast"), store=store,
|
|
41
|
+
tier="fast")
|
|
42
|
+
return backend
|
|
43
|
+
|
|
44
|
+
raise GenerationError(
|
|
45
|
+
GenerationErrorCode.BACKEND_NOT_CONFIGURED,
|
|
46
|
+
f"Unknown or unsupported llm_backend '{backend_id}'",
|
|
47
|
+
stage="setup",
|
|
48
|
+
retryable=False,
|
|
49
|
+
fallbackable=False,
|
|
50
|
+
backend=backend_id,
|
|
51
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Resolves which LLM backend id a Config selects.
|
|
2
|
+
|
|
3
|
+
Only "litellm" exists in this phase. daily_stock_analysis also has
|
|
4
|
+
local-CLI backends (codex_cli/claude_code_cli/opencode_cli - subprocess-
|
|
5
|
+
based, with allowlisted env, output caps, redaction) - deliberately not
|
|
6
|
+
built here yet: that's real extra scope (subprocess sandboxing is
|
|
7
|
+
security-sensitive to get right) rather than a small addition, so it's
|
|
8
|
+
cut from Phase 3a rather than half-implemented. backend_factory already
|
|
9
|
+
raises a clear, typed error for any backend id other than "litellm" so
|
|
10
|
+
adding one later doesn't require reshaping this function's callers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
LITELLM_BACKEND_ID = "litellm"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_generation_backend_id(config) -> str:
|
|
19
|
+
return getattr(config, "llm_backend", None) or LITELLM_BACKEND_ID
|