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,81 @@
|
|
|
1
|
+
"""SIP (Systematic Investment Plan) Optimization Module."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from marketmyna.data.mutual_funds import MutualFundDataFetcher
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class StepUpSIPProjection:
|
|
11
|
+
"""Step-up SIP projection."""
|
|
12
|
+
base_amount: float
|
|
13
|
+
annual_increase_pct: float
|
|
14
|
+
duration_years: int
|
|
15
|
+
expected_return_pct: float
|
|
16
|
+
total_invested: float
|
|
17
|
+
projected_corpus: float
|
|
18
|
+
projected_gain: float
|
|
19
|
+
yearly_breakdown: list[dict]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SIPOptimizer:
|
|
24
|
+
"""SIP optimization and analysis tools."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, data_fetcher: Optional[MutualFundDataFetcher] = None):
|
|
27
|
+
self.data_fetcher = data_fetcher or MutualFundDataFetcher()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def calculate_step_up_sip(
|
|
33
|
+
self,
|
|
34
|
+
base_amount: float,
|
|
35
|
+
annual_increase_pct: float,
|
|
36
|
+
duration_years: int,
|
|
37
|
+
expected_return_pct: float,
|
|
38
|
+
) -> StepUpSIPProjection:
|
|
39
|
+
"""
|
|
40
|
+
Calculate step-up SIP projection where SIP amount increases annually.
|
|
41
|
+
"""
|
|
42
|
+
monthly_rate = expected_return_pct / 100 / 12
|
|
43
|
+
|
|
44
|
+
total_invested = 0
|
|
45
|
+
corpus = 0
|
|
46
|
+
yearly_breakdown = []
|
|
47
|
+
current_amount = base_amount
|
|
48
|
+
|
|
49
|
+
for year in range(1, duration_years + 1):
|
|
50
|
+
year_invested = 0
|
|
51
|
+
year_start_corpus = corpus
|
|
52
|
+
|
|
53
|
+
for month in range(12):
|
|
54
|
+
# Add this month's investment
|
|
55
|
+
corpus = corpus * (1 + monthly_rate) + current_amount
|
|
56
|
+
year_invested += current_amount
|
|
57
|
+
total_invested += current_amount
|
|
58
|
+
|
|
59
|
+
yearly_breakdown.append({
|
|
60
|
+
'year': year,
|
|
61
|
+
'monthly_sip': current_amount,
|
|
62
|
+
'year_invested': year_invested,
|
|
63
|
+
'year_end_corpus': corpus,
|
|
64
|
+
'year_growth': corpus - year_start_corpus - year_invested,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# Increase for next year
|
|
68
|
+
current_amount = current_amount * (1 + annual_increase_pct / 100)
|
|
69
|
+
|
|
70
|
+
projected_gain = corpus - total_invested
|
|
71
|
+
|
|
72
|
+
return StepUpSIPProjection(
|
|
73
|
+
base_amount=base_amount,
|
|
74
|
+
annual_increase_pct=annual_increase_pct,
|
|
75
|
+
duration_years=duration_years,
|
|
76
|
+
expected_return_pct=expected_return_pct,
|
|
77
|
+
total_invested=total_invested,
|
|
78
|
+
projected_corpus=corpus,
|
|
79
|
+
projected_gain=projected_gain,
|
|
80
|
+
yearly_breakdown=yearly_breakdown,
|
|
81
|
+
)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Technical analysis indicators and signals."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Signal(Enum):
|
|
11
|
+
"""Trading signal types."""
|
|
12
|
+
STRONG_BUY = "strong_buy"
|
|
13
|
+
BUY = "buy"
|
|
14
|
+
NEUTRAL = "neutral"
|
|
15
|
+
SELL = "sell"
|
|
16
|
+
STRONG_SELL = "strong_sell"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class IndicatorResult:
|
|
21
|
+
"""Result of a technical indicator calculation."""
|
|
22
|
+
name: str
|
|
23
|
+
value: float
|
|
24
|
+
signal: Signal
|
|
25
|
+
description: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TechnicalAnalyzer:
|
|
29
|
+
"""Technical analysis calculator."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, df: pd.DataFrame):
|
|
32
|
+
"""
|
|
33
|
+
Initialize with OHLCV dataframe.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
df: DataFrame with columns: open, high, low, close, volume
|
|
37
|
+
"""
|
|
38
|
+
self.df = df.copy()
|
|
39
|
+
self._ensure_columns()
|
|
40
|
+
|
|
41
|
+
def _ensure_columns(self):
|
|
42
|
+
"""Ensure required columns exist with proper names."""
|
|
43
|
+
col_map = {
|
|
44
|
+
"Open": "open",
|
|
45
|
+
"High": "high",
|
|
46
|
+
"Low": "low",
|
|
47
|
+
"Close": "close",
|
|
48
|
+
"Volume": "volume",
|
|
49
|
+
}
|
|
50
|
+
for old, new in col_map.items():
|
|
51
|
+
if old in self.df.columns and new not in self.df.columns:
|
|
52
|
+
self.df[new] = self.df[old]
|
|
53
|
+
|
|
54
|
+
# Moving Averages
|
|
55
|
+
def sma(self, period: int = 20, column: str = "close") -> pd.Series:
|
|
56
|
+
"""Simple Moving Average."""
|
|
57
|
+
return self.df[column].rolling(window=period).mean()
|
|
58
|
+
|
|
59
|
+
def ema(self, period: int = 20, column: str = "close") -> pd.Series:
|
|
60
|
+
"""Exponential Moving Average."""
|
|
61
|
+
return self.df[column].ewm(span=period, adjust=False).mean()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Momentum Indicators
|
|
65
|
+
def rsi(self, period: int = 14) -> pd.Series:
|
|
66
|
+
"""Relative Strength Index, Wilder smoothing - the same formula as
|
|
67
|
+
broker charts and the rest of the app (marketmyna/trading/indicators.py)."""
|
|
68
|
+
from ..trading.indicators import rsi
|
|
69
|
+
|
|
70
|
+
return rsi(self.df["close"].astype(float), period)
|
|
71
|
+
|
|
72
|
+
def macd(
|
|
73
|
+
self, fast: int = 12, slow: int = 26, signal: int = 9
|
|
74
|
+
) -> tuple[pd.Series, pd.Series, pd.Series]:
|
|
75
|
+
"""
|
|
76
|
+
MACD - Moving Average Convergence Divergence.
|
|
77
|
+
|
|
78
|
+
Returns: (macd_line, signal_line, histogram)
|
|
79
|
+
"""
|
|
80
|
+
ema_fast = self.ema(fast)
|
|
81
|
+
ema_slow = self.ema(slow)
|
|
82
|
+
macd_line = ema_fast - ema_slow
|
|
83
|
+
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
|
|
84
|
+
histogram = macd_line - signal_line
|
|
85
|
+
|
|
86
|
+
return macd_line, signal_line, histogram
|
|
87
|
+
|
|
88
|
+
def stochastic(
|
|
89
|
+
self, k_period: int = 14, d_period: int = 3
|
|
90
|
+
) -> tuple[pd.Series, pd.Series]:
|
|
91
|
+
"""
|
|
92
|
+
Stochastic Oscillator.
|
|
93
|
+
|
|
94
|
+
Returns: (k_line, d_line)
|
|
95
|
+
"""
|
|
96
|
+
low_min = self.df["low"].rolling(window=k_period).min()
|
|
97
|
+
high_max = self.df["high"].rolling(window=k_period).max()
|
|
98
|
+
|
|
99
|
+
k = 100 * (self.df["close"] - low_min) / (high_max - low_min)
|
|
100
|
+
d = k.rolling(window=d_period).mean()
|
|
101
|
+
|
|
102
|
+
return k, d
|
|
103
|
+
|
|
104
|
+
def williams_r(self, period: int = 14) -> pd.Series:
|
|
105
|
+
"""Williams %R."""
|
|
106
|
+
high_max = self.df["high"].rolling(window=period).max()
|
|
107
|
+
low_min = self.df["low"].rolling(window=period).min()
|
|
108
|
+
|
|
109
|
+
return -100 * (high_max - self.df["close"]) / (high_max - low_min)
|
|
110
|
+
|
|
111
|
+
def cci(self, period: int = 20) -> pd.Series:
|
|
112
|
+
"""Commodity Channel Index."""
|
|
113
|
+
typical_price = (self.df["high"] + self.df["low"] + self.df["close"]) / 3
|
|
114
|
+
sma_tp = typical_price.rolling(window=period).mean()
|
|
115
|
+
mean_dev = typical_price.rolling(window=period).apply(
|
|
116
|
+
lambda x: np.abs(x - x.mean()).mean()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return (typical_price - sma_tp) / (0.015 * mean_dev)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Volatility Indicators
|
|
123
|
+
def bollinger_bands(
|
|
124
|
+
self, period: int = 20, std_dev: float = 2.0
|
|
125
|
+
) -> tuple[pd.Series, pd.Series, pd.Series]:
|
|
126
|
+
"""
|
|
127
|
+
Bollinger Bands.
|
|
128
|
+
|
|
129
|
+
Returns: (upper_band, middle_band, lower_band)
|
|
130
|
+
"""
|
|
131
|
+
middle = self.sma(period)
|
|
132
|
+
std = self.df["close"].rolling(window=period).std()
|
|
133
|
+
upper = middle + (std * std_dev)
|
|
134
|
+
lower = middle - (std * std_dev)
|
|
135
|
+
|
|
136
|
+
return upper, middle, lower
|
|
137
|
+
|
|
138
|
+
def atr(self, period: int = 14) -> pd.Series:
|
|
139
|
+
"""Average True Range."""
|
|
140
|
+
high = self.df["high"]
|
|
141
|
+
low = self.df["low"]
|
|
142
|
+
close = self.df["close"]
|
|
143
|
+
|
|
144
|
+
tr1 = high - low
|
|
145
|
+
tr2 = abs(high - close.shift())
|
|
146
|
+
tr3 = abs(low - close.shift())
|
|
147
|
+
|
|
148
|
+
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
149
|
+
return tr.rolling(window=period).mean()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Volume Indicators
|
|
153
|
+
def obv(self) -> pd.Series:
|
|
154
|
+
"""On-Balance Volume."""
|
|
155
|
+
obv = [0]
|
|
156
|
+
for i in range(1, len(self.df)):
|
|
157
|
+
if self.df["close"].iloc[i] > self.df["close"].iloc[i - 1]:
|
|
158
|
+
obv.append(obv[-1] + self.df["volume"].iloc[i])
|
|
159
|
+
elif self.df["close"].iloc[i] < self.df["close"].iloc[i - 1]:
|
|
160
|
+
obv.append(obv[-1] - self.df["volume"].iloc[i])
|
|
161
|
+
else:
|
|
162
|
+
obv.append(obv[-1])
|
|
163
|
+
return pd.Series(obv, index=self.df.index)
|
|
164
|
+
|
|
165
|
+
def vwap(self) -> pd.Series:
|
|
166
|
+
"""Volume Weighted Average Price."""
|
|
167
|
+
typical_price = (self.df["high"] + self.df["low"] + self.df["close"]) / 3
|
|
168
|
+
return (typical_price * self.df["volume"]).cumsum() / self.df["volume"].cumsum()
|
|
169
|
+
|
|
170
|
+
def mfi(self, period: int = 14) -> pd.Series:
|
|
171
|
+
"""Money Flow Index."""
|
|
172
|
+
typical_price = (self.df["high"] + self.df["low"] + self.df["close"]) / 3
|
|
173
|
+
money_flow = typical_price * self.df["volume"]
|
|
174
|
+
|
|
175
|
+
positive_flow = money_flow.where(typical_price > typical_price.shift(), 0)
|
|
176
|
+
negative_flow = money_flow.where(typical_price < typical_price.shift(), 0)
|
|
177
|
+
|
|
178
|
+
positive_mf = positive_flow.rolling(window=period).sum()
|
|
179
|
+
negative_mf = negative_flow.rolling(window=period).sum()
|
|
180
|
+
|
|
181
|
+
mfi = 100 - (100 / (1 + positive_mf / negative_mf))
|
|
182
|
+
return mfi
|
|
183
|
+
|
|
184
|
+
# Trend Indicators
|
|
185
|
+
def adx(self, period: int = 14) -> pd.Series:
|
|
186
|
+
"""Average Directional Index."""
|
|
187
|
+
high = self.df["high"]
|
|
188
|
+
low = self.df["low"]
|
|
189
|
+
|
|
190
|
+
plus_dm = high.diff()
|
|
191
|
+
minus_dm = low.diff().abs()
|
|
192
|
+
|
|
193
|
+
plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0)
|
|
194
|
+
minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0)
|
|
195
|
+
|
|
196
|
+
atr_val = self.atr(period)
|
|
197
|
+
|
|
198
|
+
plus_di = 100 * (plus_dm.rolling(window=period).mean() / atr_val)
|
|
199
|
+
minus_di = 100 * (minus_dm.rolling(window=period).mean() / atr_val)
|
|
200
|
+
|
|
201
|
+
dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
|
|
202
|
+
adx = dx.rolling(window=period).mean()
|
|
203
|
+
|
|
204
|
+
return adx
|
|
205
|
+
|
|
206
|
+
def supertrend(
|
|
207
|
+
self, period: int = 10, multiplier: float = 3.0
|
|
208
|
+
) -> tuple[pd.Series, pd.Series]:
|
|
209
|
+
"""
|
|
210
|
+
Supertrend indicator.
|
|
211
|
+
|
|
212
|
+
Returns: (supertrend, direction) direction: 1=uptrend, -1=downtrend
|
|
213
|
+
"""
|
|
214
|
+
atr_val = self.atr(period)
|
|
215
|
+
hl2 = (self.df["high"] + self.df["low"]) / 2
|
|
216
|
+
|
|
217
|
+
upper_band = hl2 + (multiplier * atr_val)
|
|
218
|
+
lower_band = hl2 - (multiplier * atr_val)
|
|
219
|
+
|
|
220
|
+
supertrend = pd.Series(index=self.df.index, dtype=float)
|
|
221
|
+
direction = pd.Series(index=self.df.index, dtype=float)
|
|
222
|
+
|
|
223
|
+
for i in range(period, len(self.df)):
|
|
224
|
+
if self.df["close"].iloc[i] > upper_band.iloc[i - 1]:
|
|
225
|
+
direction.iloc[i] = 1
|
|
226
|
+
elif self.df["close"].iloc[i] < lower_band.iloc[i - 1]:
|
|
227
|
+
direction.iloc[i] = -1
|
|
228
|
+
else:
|
|
229
|
+
direction.iloc[i] = direction.iloc[i - 1]
|
|
230
|
+
|
|
231
|
+
if direction.iloc[i] == 1:
|
|
232
|
+
supertrend.iloc[i] = lower_band.iloc[i]
|
|
233
|
+
else:
|
|
234
|
+
supertrend.iloc[i] = upper_band.iloc[i]
|
|
235
|
+
|
|
236
|
+
return supertrend, direction
|
|
237
|
+
|
|
238
|
+
# Analysis Summary
|
|
239
|
+
def get_signals(self) -> dict[str, IndicatorResult]:
|
|
240
|
+
"""Get trading signals from multiple indicators."""
|
|
241
|
+
signals = {}
|
|
242
|
+
close = self.df["close"].iloc[-1]
|
|
243
|
+
|
|
244
|
+
# RSI Signal
|
|
245
|
+
rsi_val = self.rsi().iloc[-1]
|
|
246
|
+
if rsi_val < 30:
|
|
247
|
+
rsi_signal = Signal.BUY
|
|
248
|
+
elif rsi_val > 70:
|
|
249
|
+
rsi_signal = Signal.SELL
|
|
250
|
+
else:
|
|
251
|
+
rsi_signal = Signal.NEUTRAL
|
|
252
|
+
signals["rsi"] = IndicatorResult("RSI", rsi_val, rsi_signal)
|
|
253
|
+
|
|
254
|
+
# MACD Signal
|
|
255
|
+
macd_line, signal_line, _ = self.macd()
|
|
256
|
+
macd_val = macd_line.iloc[-1]
|
|
257
|
+
if macd_line.iloc[-1] > signal_line.iloc[-1] and macd_line.iloc[-2] <= signal_line.iloc[-2]:
|
|
258
|
+
macd_signal = Signal.BUY
|
|
259
|
+
elif macd_line.iloc[-1] < signal_line.iloc[-1] and macd_line.iloc[-2] >= signal_line.iloc[-2]:
|
|
260
|
+
macd_signal = Signal.SELL
|
|
261
|
+
else:
|
|
262
|
+
macd_signal = Signal.NEUTRAL
|
|
263
|
+
signals["macd"] = IndicatorResult("MACD", macd_val, macd_signal)
|
|
264
|
+
|
|
265
|
+
# Moving Average Signal
|
|
266
|
+
sma20 = self.sma(20).iloc[-1]
|
|
267
|
+
sma50 = self.sma(50).iloc[-1]
|
|
268
|
+
if close > sma20 > sma50:
|
|
269
|
+
ma_signal = Signal.BUY
|
|
270
|
+
elif close < sma20 < sma50:
|
|
271
|
+
ma_signal = Signal.SELL
|
|
272
|
+
else:
|
|
273
|
+
ma_signal = Signal.NEUTRAL
|
|
274
|
+
signals["ma"] = IndicatorResult("Moving Averages", sma20, ma_signal)
|
|
275
|
+
|
|
276
|
+
# Bollinger Bands Signal
|
|
277
|
+
upper, middle, lower = self.bollinger_bands()
|
|
278
|
+
bb_position = (close - lower.iloc[-1]) / (upper.iloc[-1] - lower.iloc[-1])
|
|
279
|
+
if bb_position < 0.2:
|
|
280
|
+
bb_signal = Signal.BUY
|
|
281
|
+
elif bb_position > 0.8:
|
|
282
|
+
bb_signal = Signal.SELL
|
|
283
|
+
else:
|
|
284
|
+
bb_signal = Signal.NEUTRAL
|
|
285
|
+
signals["bollinger"] = IndicatorResult("Bollinger Bands", bb_position * 100, bb_signal)
|
|
286
|
+
|
|
287
|
+
# Stochastic Signal
|
|
288
|
+
k, d = self.stochastic()
|
|
289
|
+
stoch_val = k.iloc[-1]
|
|
290
|
+
if stoch_val < 20:
|
|
291
|
+
stoch_signal = Signal.BUY
|
|
292
|
+
elif stoch_val > 80:
|
|
293
|
+
stoch_signal = Signal.SELL
|
|
294
|
+
else:
|
|
295
|
+
stoch_signal = Signal.NEUTRAL
|
|
296
|
+
signals["stochastic"] = IndicatorResult("Stochastic", stoch_val, stoch_signal)
|
|
297
|
+
|
|
298
|
+
return signals
|
|
299
|
+
|
|
300
|
+
def get_summary(self) -> dict:
|
|
301
|
+
"""Get overall analysis summary."""
|
|
302
|
+
signals = self.get_signals()
|
|
303
|
+
|
|
304
|
+
buy_count = sum(1 for s in signals.values() if s.signal in [Signal.BUY, Signal.STRONG_BUY])
|
|
305
|
+
sell_count = sum(1 for s in signals.values() if s.signal in [Signal.SELL, Signal.STRONG_SELL])
|
|
306
|
+
neutral_count = sum(1 for s in signals.values() if s.signal == Signal.NEUTRAL)
|
|
307
|
+
|
|
308
|
+
if buy_count > sell_count + neutral_count:
|
|
309
|
+
overall = Signal.BUY
|
|
310
|
+
elif sell_count > buy_count + neutral_count:
|
|
311
|
+
overall = Signal.SELL
|
|
312
|
+
else:
|
|
313
|
+
overall = Signal.NEUTRAL
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"overall_signal": overall,
|
|
317
|
+
"buy_signals": buy_count,
|
|
318
|
+
"sell_signals": sell_count,
|
|
319
|
+
"neutral_signals": neutral_count,
|
|
320
|
+
"indicators": {name: {"value": r.value, "signal": r.signal.value} for name, r in signals.items()},
|
|
321
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI backend for MarketMyna (self-hosted web app)."""
|
marketmyna/api/app.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""FastAPI application factory.
|
|
2
|
+
|
|
3
|
+
Auth: single-user build, every request runs as the local owner
|
|
4
|
+
(marketmyna/api/middlewares/auth.py).
|
|
5
|
+
|
|
6
|
+
Phase 5 adds a lifespan: RuntimeSchedulerService is started on app
|
|
7
|
+
startup and stopped on shutdown, stashed on app.state.scheduler so
|
|
8
|
+
endpoints can reach the one running instance (marketmyna/api/deps.py's
|
|
9
|
+
get_scheduler_service pulls it from there, matching the same
|
|
10
|
+
"shared services live on app.state" pattern this whole API follows).
|
|
11
|
+
|
|
12
|
+
Phase 6 adds a second background service to the same lifespan:
|
|
13
|
+
TelegramListener (marketmyna/bot/telegram_listener.py), started only if
|
|
14
|
+
config.telegram_bot_token is set (a no-op, logged, not an error,
|
|
15
|
+
otherwise), stashed on app.state.telegram_listener and app.state.bot_dispatcher.
|
|
16
|
+
|
|
17
|
+
The lifespan also warms the market listing cache (marketmyna/data/listings.py)
|
|
18
|
+
on a daemon thread so the Markets page and symbol search are instant.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from contextlib import asynccontextmanager
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from fastapi import FastAPI, HTTPException
|
|
30
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
31
|
+
from fastapi.responses import FileResponse
|
|
32
|
+
|
|
33
|
+
from marketmyna import __version__
|
|
34
|
+
from marketmyna.api.deps import get_config_dep
|
|
35
|
+
from marketmyna.api.middlewares.auth import AuthMiddleware, api_docs_enabled, check_auth_config
|
|
36
|
+
from marketmyna.api.middlewares.auth import router as auth_router
|
|
37
|
+
from marketmyna.api.middlewares.csrf import CSRFMiddleware
|
|
38
|
+
from marketmyna.api.middlewares.error_handler import add_error_handlers
|
|
39
|
+
from marketmyna.api.middlewares.slashes import StripTrailingSlashMiddleware
|
|
40
|
+
from marketmyna.api.v1.endpoints.account import router as account_router
|
|
41
|
+
from marketmyna.api.v1.router import router as api_v1_router
|
|
42
|
+
from marketmyna.bot.registry import build_default_dispatcher
|
|
43
|
+
from marketmyna.bot.telegram_listener import TelegramListener
|
|
44
|
+
from marketmyna.core import tenant
|
|
45
|
+
from marketmyna.data.listings import get_store
|
|
46
|
+
from marketmyna.services.runtime_scheduler import RuntimeSchedulerService
|
|
47
|
+
from marketmyna.trading.alerts import AlertMonitor
|
|
48
|
+
|
|
49
|
+
# The server keeps times as naive UTC (datetime.now() in the UTC container) and
|
|
50
|
+
# the frontend's serverTime() reads them as UTC; pin the zone so a dev machine
|
|
51
|
+
# in another zone writes the same times as the server.
|
|
52
|
+
os.environ["TZ"] = "UTC"
|
|
53
|
+
if hasattr(time, "tzset"):
|
|
54
|
+
time.tzset()
|
|
55
|
+
else: # Windows: no time.tzset(); the C runtime re-reads TZ ("UTC0": UTC, no daylight saving) on _tzset()
|
|
56
|
+
os.environ["TZ"] = "UTC0"
|
|
57
|
+
try:
|
|
58
|
+
import ctypes
|
|
59
|
+
|
|
60
|
+
ctypes.CDLL("ucrtbase")._tzset()
|
|
61
|
+
except (OSError, AttributeError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
DEFAULT_CORS_ORIGINS = [
|
|
65
|
+
"http://localhost:5174",
|
|
66
|
+
"http://localhost:3000",
|
|
67
|
+
"http://127.0.0.1:5174",
|
|
68
|
+
"http://127.0.0.1:3000",
|
|
69
|
+
]
|
|
70
|
+
# Extra allowed origins for a frontend on another domain that calls the API
|
|
71
|
+
# directly (comma-separated). Not needed normally: the API serves the built
|
|
72
|
+
# frontend itself (serve_frontend below), so both share one origin.
|
|
73
|
+
EXTRA_CORS_ORIGINS = [o.strip() for o in os.environ.get("CORS_ORIGINS", "").split(",") if o.strip()]
|
|
74
|
+
|
|
75
|
+
# The built frontend: frontend/dist in a git checkout (cd frontend && npm run build),
|
|
76
|
+
# or marketmyna/web inside the installed package.
|
|
77
|
+
_PACKAGE = Path(__file__).resolve().parent.parent
|
|
78
|
+
FRONTEND_DIST = next((d for d in (_PACKAGE.parent / "frontend" / "dist", _PACKAGE / "web")
|
|
79
|
+
if (d / "index.html").is_file()), _PACKAGE / "web")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@asynccontextmanager
|
|
83
|
+
async def app_lifespan(app: FastAPI):
|
|
84
|
+
config = get_config_dep()
|
|
85
|
+
if getattr(config, "_plaintext_secrets", False):
|
|
86
|
+
# One-time upgrade: re-save so older plain-text keys are encrypted on disk.
|
|
87
|
+
config.save()
|
|
88
|
+
|
|
89
|
+
scheduler = RuntimeSchedulerService(config)
|
|
90
|
+
scheduler.start()
|
|
91
|
+
app.state.scheduler = scheduler
|
|
92
|
+
|
|
93
|
+
dispatcher = build_default_dispatcher(config, scheduler_running=scheduler.is_running)
|
|
94
|
+
app.state.bot_dispatcher = dispatcher
|
|
95
|
+
|
|
96
|
+
telegram_listener = TelegramListener(config, dispatcher)
|
|
97
|
+
telegram_listener.start()
|
|
98
|
+
app.state.telegram_listener = telegram_listener
|
|
99
|
+
|
|
100
|
+
# Trade section: price alerts, checked every ALERT_POLL_SECONDS while markets are open.
|
|
101
|
+
alert_monitor = AlertMonitor(config, poll_seconds=int(os.environ.get("ALERT_POLL_SECONDS", "60")))
|
|
102
|
+
alert_monitor.start()
|
|
103
|
+
app.state.alert_monitor = alert_monitor
|
|
104
|
+
|
|
105
|
+
threading.Thread(target=get_store().rows, args=("all",), name="listings-warmup", daemon=True).start()
|
|
106
|
+
|
|
107
|
+
def _warm_funds():
|
|
108
|
+
import time as _t
|
|
109
|
+
|
|
110
|
+
_t.sleep(60) # after the listings warm-up
|
|
111
|
+
from marketmyna.services.fund_rotation import warm
|
|
112
|
+
|
|
113
|
+
warm()
|
|
114
|
+
|
|
115
|
+
threading.Thread(target=_warm_funds, name="fund-rotation-warmup", daemon=True).start()
|
|
116
|
+
|
|
117
|
+
from marketmyna.services.fund_holdings import refresh_loop
|
|
118
|
+
|
|
119
|
+
threading.Thread(target=refresh_loop, name="fund-portfolios", daemon=True).start()
|
|
120
|
+
|
|
121
|
+
from marketmyna.services.sip_days import refresh_loop as sip_days_loop
|
|
122
|
+
|
|
123
|
+
threading.Thread(target=sip_days_loop, name="sip-days", daemon=True).start()
|
|
124
|
+
|
|
125
|
+
from marketmyna.services.market_valuation import snapshot_loop as valuation_loop
|
|
126
|
+
|
|
127
|
+
threading.Thread(target=valuation_loop, name="market-valuation", daemon=True).start()
|
|
128
|
+
|
|
129
|
+
from marketmyna.services.india_flows import snapshot_loop as flows_loop
|
|
130
|
+
|
|
131
|
+
threading.Thread(target=flows_loop, name="india-flows", daemon=True).start()
|
|
132
|
+
|
|
133
|
+
from marketmyna.services.mf_lumpsum import alert_loop as fund_dip_loop
|
|
134
|
+
|
|
135
|
+
threading.Thread(target=fund_dip_loop, args=(config,), name="fund-dip-alerts", daemon=True).start()
|
|
136
|
+
|
|
137
|
+
from marketmyna.api.v1.endpoints.updates import check_loop as updates_loop
|
|
138
|
+
|
|
139
|
+
# Looks for a new MarketMyna release / newer code on GitHub (Settings -> Updates).
|
|
140
|
+
threading.Thread(target=updates_loop, name="update-check", daemon=True).start()
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
yield
|
|
144
|
+
finally:
|
|
145
|
+
telegram_listener.stop()
|
|
146
|
+
alert_monitor.stop()
|
|
147
|
+
scheduler.stop()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def create_app() -> FastAPI:
|
|
151
|
+
check_auth_config()
|
|
152
|
+
# Every Yahoo Finance request on this server shares one pace and 429 cooldown.
|
|
153
|
+
from marketmyna.utils import yahoo_gate
|
|
154
|
+
|
|
155
|
+
yahoo_gate.install()
|
|
156
|
+
# Scope every owned-table query to the signed-in user.
|
|
157
|
+
tenant.enable(bool(get_config_dep().accounts_enabled))
|
|
158
|
+
docs = api_docs_enabled()
|
|
159
|
+
app = FastAPI(title="MarketMyna API", version=__version__, lifespan=app_lifespan, redirect_slashes=False,
|
|
160
|
+
docs_url="/docs" if docs else None, redoc_url="/redoc" if docs else None,
|
|
161
|
+
openapi_url="/openapi.json" if docs else None)
|
|
162
|
+
|
|
163
|
+
# Added before CORS so CORS ends up outermost and 401s still carry CORS headers.
|
|
164
|
+
app.add_middleware(AuthMiddleware)
|
|
165
|
+
# Cross-site writes are refused before any session lookup (marketmyna/api/middlewares/csrf.py).
|
|
166
|
+
app.add_middleware(CSRFMiddleware, allowed_origins=DEFAULT_CORS_ORIGINS + EXTRA_CORS_ORIGINS)
|
|
167
|
+
app.add_middleware(
|
|
168
|
+
CORSMiddleware,
|
|
169
|
+
allow_origins=DEFAULT_CORS_ORIGINS + EXTRA_CORS_ORIGINS,
|
|
170
|
+
allow_credentials=True,
|
|
171
|
+
allow_methods=["*"],
|
|
172
|
+
allow_headers=["*"],
|
|
173
|
+
)
|
|
174
|
+
app.add_middleware(StripTrailingSlashMiddleware)
|
|
175
|
+
|
|
176
|
+
add_error_handlers(app)
|
|
177
|
+
|
|
178
|
+
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
|
|
179
|
+
app.include_router(account_router, prefix="/api/auth/account", tags=["account"])
|
|
180
|
+
app.include_router(api_v1_router, prefix="/api/v1")
|
|
181
|
+
|
|
182
|
+
@app.get("/health")
|
|
183
|
+
@app.get("/api/health")
|
|
184
|
+
def health() -> dict:
|
|
185
|
+
return {"status": "ok"}
|
|
186
|
+
|
|
187
|
+
serve_frontend(app)
|
|
188
|
+
return app
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def serve_frontend(app: FastAPI, dist: Path = FRONTEND_DIST) -> None:
|
|
192
|
+
"""Serves the built frontend at /, so one uvicorn process is the whole
|
|
193
|
+
app. Paths that are not files get index.html for the React router;
|
|
194
|
+
unknown /api/* paths stay 404s. Does nothing before the first build."""
|
|
195
|
+
dist = dist.resolve()
|
|
196
|
+
if not (dist / "index.html").is_file():
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
@app.get("/{path:path}", include_in_schema=False)
|
|
200
|
+
def frontend(path: str):
|
|
201
|
+
if path == "api" or path.startswith("api/"):
|
|
202
|
+
raise HTTPException(status_code=404)
|
|
203
|
+
file = (dist / path).resolve()
|
|
204
|
+
if path and file.is_file() and file.is_relative_to(dist):
|
|
205
|
+
return FileResponse(file)
|
|
206
|
+
return FileResponse(dist / "index.html")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
app = create_app()
|
marketmyna/api/deps.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""FastAPI dependency-injection functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Iterator
|
|
7
|
+
|
|
8
|
+
from fastapi import Depends, Request
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
from marketmyna.core.config import Config
|
|
12
|
+
from marketmyna.storage import DatabaseManager, get_db_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@lru_cache(maxsize=1)
|
|
16
|
+
def get_config_dep() -> Config:
|
|
17
|
+
"""Process-wide Config, loaded once per process."""
|
|
18
|
+
return Config.load()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_database_manager() -> DatabaseManager:
|
|
22
|
+
config = get_config_dep()
|
|
23
|
+
return DatabaseManager.instance(get_db_url(config=config))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_db_session(config: Config = Depends(get_config_dep)) -> Iterator[Session]:
|
|
27
|
+
"""A request-scoped session: committed on success, rolled back on error."""
|
|
28
|
+
session = DatabaseManager.instance(get_db_url(config=config)).SessionLocal()
|
|
29
|
+
try:
|
|
30
|
+
yield session
|
|
31
|
+
session.commit()
|
|
32
|
+
except Exception:
|
|
33
|
+
session.rollback()
|
|
34
|
+
raise
|
|
35
|
+
finally:
|
|
36
|
+
session.close()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_scheduler_service(request: Request):
|
|
40
|
+
"""The RuntimeSchedulerService started in marketmyna/api/app.py's lifespan, off
|
|
41
|
+
app.state - lazily created if somehow missing (e.g. a test app built
|
|
42
|
+
without going through the real lifespan) rather than 500ing."""
|
|
43
|
+
scheduler = getattr(request.app.state, "scheduler", None)
|
|
44
|
+
if scheduler is None:
|
|
45
|
+
from marketmyna.services.runtime_scheduler import RuntimeSchedulerService
|
|
46
|
+
|
|
47
|
+
scheduler = RuntimeSchedulerService(get_config_dep())
|
|
48
|
+
scheduler.start()
|
|
49
|
+
request.app.state.scheduler = scheduler
|
|
50
|
+
return scheduler
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_bot_dispatcher(request: Request):
|
|
54
|
+
"""The CommandDispatcher built in marketmyna/api/app.py's lifespan, off
|
|
55
|
+
app.state - lazily created if somehow missing, same fallback shape
|
|
56
|
+
as get_scheduler_service."""
|
|
57
|
+
dispatcher = getattr(request.app.state, "bot_dispatcher", None)
|
|
58
|
+
if dispatcher is None:
|
|
59
|
+
from marketmyna.bot.registry import build_default_dispatcher
|
|
60
|
+
|
|
61
|
+
dispatcher = build_default_dispatcher(get_config_dep())
|
|
62
|
+
request.app.state.bot_dispatcher = dispatcher
|
|
63
|
+
return dispatcher
|
|
File without changes
|