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,130 @@
|
|
|
1
|
+
"""Per-user data scoping for accounts mode.
|
|
2
|
+
|
|
3
|
+
When accounts mode is on (`enable()` is called at app start), every ORM
|
|
4
|
+
SELECT / UPDATE / DELETE on an owned model gets `user_id = <current user>`
|
|
5
|
+
added automatically, and every new owned row gets the current user's id on
|
|
6
|
+
flush. The current user lives in a context variable that AuthMiddleware sets
|
|
7
|
+
per request, so routers and services don't have to pass user ids around.
|
|
8
|
+
|
|
9
|
+
Scopes:
|
|
10
|
+
- a user id (int): only that user's rows.
|
|
11
|
+
- SYSTEM: no filter. Only for code that must see every user's rows
|
|
12
|
+
(admin counts, account deletion, background jobs that loop over users).
|
|
13
|
+
- unset: fail closed. Queries match nothing and inserts raise, so code that
|
|
14
|
+
forgot to pick a scope can't read or write another user's data.
|
|
15
|
+
|
|
16
|
+
With accounts off (CLI runs, one local user) nothing is filtered.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import contextlib
|
|
22
|
+
import logging
|
|
23
|
+
from contextvars import ContextVar
|
|
24
|
+
from typing import Iterator, Optional, Union
|
|
25
|
+
|
|
26
|
+
from sqlalchemy import event
|
|
27
|
+
from sqlalchemy.orm import ORMExecuteState, Session, with_loader_criteria
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _System:
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return "SYSTEM"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
SYSTEM = _System()
|
|
38
|
+
Scope = Union[int, _System, None]
|
|
39
|
+
|
|
40
|
+
_current: ContextVar[Scope] = ContextVar("marketmyna_user_scope", default=None)
|
|
41
|
+
_enabled = False
|
|
42
|
+
_owned: tuple[type, ...] = ()
|
|
43
|
+
# Owned for inserts (user_id is filled in) but readable across users: the
|
|
44
|
+
# admin reads all feedback and usage, the platform scheduler / alert config
|
|
45
|
+
# rows belong to the platform, not one user.
|
|
46
|
+
_insert_only: tuple[type, ...] = ()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def configure(owned: tuple[type, ...], insert_only: tuple[type, ...] = ()) -> None:
|
|
50
|
+
global _owned, _insert_only
|
|
51
|
+
_owned, _insert_only = owned, insert_only
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def owned_models() -> tuple[type, ...]:
|
|
55
|
+
"""Models scoped to one user (each has a user_id column)."""
|
|
56
|
+
return _owned
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def enable(on: bool = True) -> None:
|
|
60
|
+
global _enabled
|
|
61
|
+
_enabled = on
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def enabled() -> bool:
|
|
65
|
+
return _enabled
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def current() -> Scope:
|
|
69
|
+
return _current.get()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def current_user_id() -> Optional[int]:
|
|
73
|
+
scope = _current.get()
|
|
74
|
+
return scope if isinstance(scope, int) else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def set_scope(scope: Scope):
|
|
78
|
+
"""Sets the scope for the rest of this context; returns a reset token."""
|
|
79
|
+
return _current.set(scope)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reset_scope(token) -> None:
|
|
83
|
+
_current.reset(token)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@contextlib.contextmanager
|
|
87
|
+
def user_scope(scope: Scope) -> Iterator[None]:
|
|
88
|
+
token = _current.set(scope)
|
|
89
|
+
try:
|
|
90
|
+
yield
|
|
91
|
+
finally:
|
|
92
|
+
_current.reset(token)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def system_scope():
|
|
96
|
+
return user_scope(SYSTEM)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@event.listens_for(Session, "do_orm_execute")
|
|
100
|
+
def _scope_queries(state: ORMExecuteState) -> None:
|
|
101
|
+
if not _enabled or not _owned:
|
|
102
|
+
return
|
|
103
|
+
if not (state.is_select or state.is_update or state.is_delete):
|
|
104
|
+
return
|
|
105
|
+
if state.is_column_load or state.is_relationship_load:
|
|
106
|
+
return # the parent row was already scoped
|
|
107
|
+
scope = _current.get()
|
|
108
|
+
if scope is SYSTEM:
|
|
109
|
+
return
|
|
110
|
+
# No user id is ever -1, so an unset scope matches nothing.
|
|
111
|
+
uid = scope if isinstance(scope, int) else -1
|
|
112
|
+
state.statement = state.statement.options(
|
|
113
|
+
*(with_loader_criteria(model, model.user_id == uid, include_aliases=True) for model in _owned)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@event.listens_for(Session, "before_flush")
|
|
118
|
+
def _stamp_owner(session: Session, flush_context, instances) -> None:
|
|
119
|
+
if not _enabled:
|
|
120
|
+
return
|
|
121
|
+
scope = _current.get()
|
|
122
|
+
for obj in session.new:
|
|
123
|
+
if not isinstance(obj, _owned + _insert_only):
|
|
124
|
+
continue
|
|
125
|
+
if getattr(obj, "user_id", None) is not None:
|
|
126
|
+
continue
|
|
127
|
+
if isinstance(scope, int):
|
|
128
|
+
obj.user_id = scope
|
|
129
|
+
elif scope is None and isinstance(obj, _owned):
|
|
130
|
+
raise RuntimeError(f"Refusing to create {type(obj).__name__} with no user scope")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Data fetching module for market data."""
|
|
2
|
+
|
|
3
|
+
from .base import DataFetcher
|
|
4
|
+
from .crypto import CryptoDataFetcher
|
|
5
|
+
from .india import IndiaDataFetcher
|
|
6
|
+
from .stocks import StockDataFetcher
|
|
7
|
+
|
|
8
|
+
__all__ = ["DataFetcher", "StockDataFetcher", "CryptoDataFetcher", "IndiaDataFetcher"]
|
marketmyna/data/base.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Base data fetcher interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ..core.constants import TimeFrame
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataFetcher(ABC):
|
|
13
|
+
"""Abstract base class for data fetchers."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_quote(self, symbol: str) -> dict:
|
|
17
|
+
"""Get current quote for a symbol."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_history(
|
|
21
|
+
self,
|
|
22
|
+
symbol: str,
|
|
23
|
+
period: str = "1y",
|
|
24
|
+
interval: TimeFrame = TimeFrame.DAY_1,
|
|
25
|
+
start: Optional[datetime] = None,
|
|
26
|
+
end: Optional[datetime] = None,
|
|
27
|
+
) -> pd.DataFrame:
|
|
28
|
+
"""Get historical price data."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def search(self, query: str) -> list[dict]:
|
|
32
|
+
"""Search for symbols matching query."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def get_info(self, symbol: str) -> dict:
|
|
36
|
+
"""Get detailed information about a symbol."""
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""One broad category list shared by every market, so a single filter
|
|
2
|
+
("Technology", "Agriculture", "Natural Resources", ...) works whether the
|
|
3
|
+
row came from NSE, NASDAQ or anywhere else.
|
|
4
|
+
|
|
5
|
+
Resolution order for a row:
|
|
6
|
+
1. industry keywords (the most specific signal, e.g. US "Farming/Seeds/Milling"),
|
|
7
|
+
2. the source's own sector label (NSE industry / US sector),
|
|
8
|
+
3. keywords in the company name - the only signal for most small NSE/SME
|
|
9
|
+
stocks, which no free bulk source classifies. Those rows are flagged
|
|
10
|
+
``category_inferred`` so the UI can say so.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
TECHNOLOGY = "Technology"
|
|
19
|
+
FINANCIALS = "Financials"
|
|
20
|
+
HEALTHCARE = "Healthcare"
|
|
21
|
+
CONSUMER = "Consumer"
|
|
22
|
+
AUTOMOBILE = "Automobile"
|
|
23
|
+
INDUSTRIALS = "Industrials"
|
|
24
|
+
ENERGY = "Energy"
|
|
25
|
+
NATURAL_RESOURCES = "Natural Resources"
|
|
26
|
+
AGRICULTURE = "Agriculture"
|
|
27
|
+
MATERIALS = "Chemicals & Materials"
|
|
28
|
+
REAL_ESTATE = "Real Estate & Construction"
|
|
29
|
+
TRUSTS = "REITs & InvITs"
|
|
30
|
+
TELECOM_MEDIA = "Telecom & Media"
|
|
31
|
+
UTILITIES = "Utilities"
|
|
32
|
+
TEXTILES = "Textiles"
|
|
33
|
+
SERVICES = "Services"
|
|
34
|
+
OTHER = "Other"
|
|
35
|
+
|
|
36
|
+
# Extra groups used only for ETFs (and index funds).
|
|
37
|
+
BROAD_MARKET = "Broad Market"
|
|
38
|
+
BONDS = "Bonds"
|
|
39
|
+
COMMODITIES = "Commodities"
|
|
40
|
+
LEVERAGED = "Leveraged & Inverse"
|
|
41
|
+
INTERNATIONAL = "International"
|
|
42
|
+
|
|
43
|
+
CATEGORIES = [
|
|
44
|
+
TECHNOLOGY, FINANCIALS, HEALTHCARE, CONSUMER, AUTOMOBILE, INDUSTRIALS, ENERGY,
|
|
45
|
+
NATURAL_RESOURCES, AGRICULTURE, MATERIALS, REAL_ESTATE, TRUSTS, TELECOM_MEDIA,
|
|
46
|
+
UTILITIES, TEXTILES, SERVICES, BROAD_MARKET, BONDS, COMMODITIES, INTERNATIONAL, LEVERAGED, OTHER,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
_ETF_RULES: list[tuple[str, re.Pattern]] = [
|
|
50
|
+
(LEVERAGED, re.compile(r"\b[23]x\b|ultra ?pro|ultrashort|inverse|\bbull\b|\bbear\b|daily .*(long|short)|leveraged", re.I)),
|
|
51
|
+
(COMMODITIES, re.compile(r"gold|silver|commodit|platinum|copper|oil fund|natural gas|crude", re.I)),
|
|
52
|
+
(BONDS, re.compile(
|
|
53
|
+
r"bond|treasury|gilt|t-bill|income|credit|debt|fixed|municipal|muni|aggregate|"
|
|
54
|
+
r"liquid|money market|floating rate|high yield|duration|mortgage|sdl|g-sec|bharat bond", re.I)),
|
|
55
|
+
(INTERNATIONAL, re.compile(
|
|
56
|
+
r"international|emerging|ex[- ]u\.?s|global|world|europe|japan|china|india|brazil|"
|
|
57
|
+
r"korea|taiwan|germany|canada|mexico|spain|kuwait|saudi|asia|pacific|latin|frontier|"
|
|
58
|
+
r"developed markets|hang seng|msci [a-z]+ (etf|index)", re.I)),
|
|
59
|
+
(BROAD_MARKET, re.compile(
|
|
60
|
+
r"s&p 500|s&p500|total (stock )?market|total world|nifty 50\b|nifty next|sensex|"
|
|
61
|
+
r"russell|nasdaq[- ]100|dow jones industrial|msci (world|acwi|eafe|emerging)|all[- ]world|"
|
|
62
|
+
r"large[- ]?cap|mid[- ]?cap|small[- ]?cap|micro[- ]?cap|nifty 100|nifty 200|nifty 500|"
|
|
63
|
+
r"broad|total market|equal weight|dividend|value|growth|momentum|quality|low vol", re.I)),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Source sector labels (NSE industries + NASDAQ sectors), lower-cased.
|
|
67
|
+
_SECTOR_MAP = {
|
|
68
|
+
# Listed trusts (NSE series RR / IV, see listings.TRUST_SERIES)
|
|
69
|
+
"reit": TRUSTS,
|
|
70
|
+
"invit": TRUSTS,
|
|
71
|
+
# NSE
|
|
72
|
+
"information technology": TECHNOLOGY,
|
|
73
|
+
"financial services": FINANCIALS,
|
|
74
|
+
"healthcare": HEALTHCARE,
|
|
75
|
+
"fast moving consumer goods": CONSUMER,
|
|
76
|
+
"consumer durables": CONSUMER,
|
|
77
|
+
"consumer services": SERVICES,
|
|
78
|
+
"automobile and auto components": AUTOMOBILE,
|
|
79
|
+
"capital goods": INDUSTRIALS,
|
|
80
|
+
"oil gas & consumable fuels": ENERGY,
|
|
81
|
+
"power": ENERGY,
|
|
82
|
+
"metals & mining": NATURAL_RESOURCES,
|
|
83
|
+
"forest materials": NATURAL_RESOURCES,
|
|
84
|
+
"chemicals": MATERIALS,
|
|
85
|
+
"construction materials": MATERIALS,
|
|
86
|
+
"construction": REAL_ESTATE,
|
|
87
|
+
"realty": REAL_ESTATE,
|
|
88
|
+
"telecommunication": TELECOM_MEDIA,
|
|
89
|
+
"media entertainment & publication": TELECOM_MEDIA,
|
|
90
|
+
"utilities": UTILITIES,
|
|
91
|
+
"textiles": TEXTILES,
|
|
92
|
+
"services": SERVICES,
|
|
93
|
+
"diversified": OTHER,
|
|
94
|
+
# NASDAQ
|
|
95
|
+
"technology": TECHNOLOGY,
|
|
96
|
+
"finance": FINANCIALS,
|
|
97
|
+
"health care": HEALTHCARE,
|
|
98
|
+
"consumer discretionary": CONSUMER,
|
|
99
|
+
"consumer staples": CONSUMER,
|
|
100
|
+
"industrials": INDUSTRIALS,
|
|
101
|
+
"energy": ENERGY,
|
|
102
|
+
"basic materials": MATERIALS,
|
|
103
|
+
"real estate": REAL_ESTATE,
|
|
104
|
+
"telecommunications": TELECOM_MEDIA,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Checked against industry first, then the company name. Order matters:
|
|
108
|
+
# the first matching rule wins, so specific themes sit above generic ones.
|
|
109
|
+
_KEYWORD_RULES: list[tuple[str, re.Pattern]] = [
|
|
110
|
+
(AGRICULTURE, re.compile(
|
|
111
|
+
r"\b(agri\w*|agro\w*|farm\w*|seeds?|fertili[sz]\w*|crop\w*|sugars?|tea|teas|coffee|"
|
|
112
|
+
r"plantation\w*|dairy|poultry|rice|flour|milling|feeds?|fisher\w*|aqua\w*|"
|
|
113
|
+
r"edible oils?|solvent|cattle|kisan|krishi|grains?)\b", re.I)),
|
|
114
|
+
(NATURAL_RESOURCES, re.compile(
|
|
115
|
+
r"\b(mining|mines?|minerals?|metals?|steel|iron|ore|alloys?|copper|zinc|"
|
|
116
|
+
r"alumin\w*|gold|silver|precious|coal|granite|marble|forest\w*|timber|"
|
|
117
|
+
r"paper|pulp|quarry\w*|ferro\w*|sponge)\b", re.I)),
|
|
118
|
+
(ENERGY, re.compile(
|
|
119
|
+
r"\b(oil|gas|petro\w*|refin\w*|energ\w*|power\w*|solar|wind|renewable\w*|"
|
|
120
|
+
r"hydro\w*|fuels?|lng|drilling)\b", re.I)),
|
|
121
|
+
(HEALTHCARE, re.compile(
|
|
122
|
+
r"\b(pharma\w*|health\w*|hospitals?|medic\w*|drugs?|bio\w*|labs?|"
|
|
123
|
+
r"laborator\w*|diagnostic\w*|life ?sciences?|remedies|clinic\w*|surgical|"
|
|
124
|
+
r"therapeutic\w*|formulations?)\b", re.I)),
|
|
125
|
+
(TECHNOLOGY, re.compile(
|
|
126
|
+
r"\b(software|tech\w*|infotech|info ?systems?|computer\w*|digital|data|"
|
|
127
|
+
r"cyber\w*|semiconductor\w*|electronics?|it services|infoway|internet|"
|
|
128
|
+
r"cloud|ai|analytics|netsol)\b", re.I)),
|
|
129
|
+
(FINANCIALS, re.compile(
|
|
130
|
+
r"\b(bank\w*|financ\w*|fincorp|fincap|capital|invest\w*|credit|leasing|"
|
|
131
|
+
r"securities|insurance|assurance|asset management|holdings?|fintech|"
|
|
132
|
+
r"money|broking|stock ?brokers?|nbfc)\b", re.I)),
|
|
133
|
+
(TEXTILES, re.compile(
|
|
134
|
+
r"\b(tex\w*|textiles?|spinning|spinners|cotton|yarns?|fabrics?|garments?|apparel|"
|
|
135
|
+
r"weav\w*|knit\w*|denim|silk|jute|mills?)\b", re.I)),
|
|
136
|
+
(AUTOMOBILE, re.compile(r"\b(auto\w*|motors?|tyres?|tires?|vehicles?|forg\w*|mobility|ev|wheels?)\b", re.I)),
|
|
137
|
+
(MATERIALS, re.compile(
|
|
138
|
+
r"\b(chemicals?|chem\w*|paints?|plastics?|polymers?|pipes?|cement|glass|"
|
|
139
|
+
r"ceramics?|packaging|rubber|dyes?|pigments?|fertichem|laminates?)\b", re.I)),
|
|
140
|
+
(REAL_ESTATE, re.compile(
|
|
141
|
+
r"\b(realty|real estate|estates?|housing|homes|builders?|construct\w*|"
|
|
142
|
+
r"infra\w*|developers?|properties|projects)\b", re.I)),
|
|
143
|
+
(TELECOM_MEDIA, re.compile(
|
|
144
|
+
r"\b(telecom\w*|communications?|media|entertainment|broadcast\w*|films?|"
|
|
145
|
+
r"studios?|publish\w*|news\w*|cable|networks?)\b", re.I)),
|
|
146
|
+
(UTILITIES, re.compile(r"\b(water|sewer|utilit\w*|electric utilit\w*)\b", re.I)),
|
|
147
|
+
(CONSUMER, re.compile(
|
|
148
|
+
r"\b(foods?|beverages?|consumer|retail\w*|jewel\w*|fashion|footwear|"
|
|
149
|
+
r"cosmetics?|brands?|appliances?|furniture|breweries|distiller\w*|"
|
|
150
|
+
r"spirits|tobacco|restaurants?)\b", re.I)),
|
|
151
|
+
(SERVICES, re.compile(
|
|
152
|
+
r"\b(hotels?|resorts?|travels?|tours?|logistics|shipping|transport\w*|"
|
|
153
|
+
r"airlines?|aviation|education\w*|services|hospitality|courier|trading|traders|"
|
|
154
|
+
r"impex|exports?|overseas|ventures)\b", re.I)),
|
|
155
|
+
(INDUSTRIALS, re.compile(
|
|
156
|
+
r"\b(engineer\w*|machin\w*|equipments?|electricals?|cables?|industrial\w*|"
|
|
157
|
+
r"tools?|pumps?|valves?|bearings?|defen[cs]e|aerospace|castings?|"
|
|
158
|
+
r"fabricat\w*|compressors?|transformers?|turbines?)\b", re.I)),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Eastmoney's (Chinese) industry names for China A / Hong Kong. Substring
|
|
163
|
+
# match, first hit wins, so e.g. 电力设备 (Industrials) precedes 电力 (Energy).
|
|
164
|
+
_ZH_RULES: list[tuple[str, tuple[str, ...]]] = [
|
|
165
|
+
# Financials first: 农业银行 / 农商行 are banks, not farms.
|
|
166
|
+
(FINANCIALS, ("银行", "证券", "保险", "金融", "信托", "期货", "农商行")),
|
|
167
|
+
(AGRICULTURE, ("农", "种植", "养殖", "饲料", "渔业", "林业", "动物保健", "畜")),
|
|
168
|
+
(NATURAL_RESOURCES, ("煤炭", "有色", "金属", "钢", "贵金属", "黄金", "冶", "焦炭", "矿", "造纸", "非金属材料")),
|
|
169
|
+
(INDUSTRIALS, ("电力设备", "电网设备", "光伏设备", "风电设备", "电源设备", "电池", "电机", "机械", "专用设备",
|
|
170
|
+
"通用设备", "自动化设备", "工程机械", "轨交", "航空装备", "航天", "航海装备", "地面兵装",
|
|
171
|
+
"军工", "环保设备", "照明设备", "工业")),
|
|
172
|
+
(ENERGY, ("石油", "油气", "油服", "炼化", "电力", "燃气", "能源", "光伏", "风电", "新能源")),
|
|
173
|
+
(TECHNOLOGY, ("半导体", "软件", "IT服务", "计算机", "电子", "元件", "光学", "通信设备", "互联网", "科技", "数字")),
|
|
174
|
+
(HEALTHCARE, ("医", "药", "生物", "医疗")),
|
|
175
|
+
(AUTOMOBILE, ("汽车", "乘用车", "商用车", "摩托车")),
|
|
176
|
+
(MATERIALS, ("化学", "化工", "塑料", "橡胶", "玻璃", "水泥", "建材", "化纤", "农化", "包装")),
|
|
177
|
+
(REAL_ESTATE, ("房地产", "地产", "建筑", "基础建设", "工程", "装修", "房屋")),
|
|
178
|
+
(TELECOM_MEDIA, ("通信服务", "电信", "传媒", "广告", "游戏", "影视", "出版", "电视", "媒体")),
|
|
179
|
+
(UTILITIES, ("水务", "公用", "环境治理")),
|
|
180
|
+
(TEXTILES, ("纺织", "服装", "家纺")),
|
|
181
|
+
(CONSUMER, ("食品", "饮料", "乳", "酒", "家电", "家居", "零售", "化妆品", "个护", "饰品", "文娱", "调味",
|
|
182
|
+
"休闲", "电商", "连锁", "美容")),
|
|
183
|
+
(SERVICES, ("物流", "航运", "港口", "机场", "铁路", "公路", "旅游", "酒店", "餐饮", "教育", "贸易", "服务", "体育")),
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _match_zh(text: str) -> Optional[str]:
|
|
188
|
+
for category, words in _ZH_RULES:
|
|
189
|
+
if any(w in text for w in words):
|
|
190
|
+
return category
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _match(text: str) -> Optional[str]:
|
|
195
|
+
for category, pattern in _KEYWORD_RULES:
|
|
196
|
+
if pattern.search(text):
|
|
197
|
+
return category
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def classify(row: dict) -> tuple[str, bool]:
|
|
202
|
+
"""(category, inferred) for a stock/ETF row."""
|
|
203
|
+
industry = row.get("industry") or ""
|
|
204
|
+
sector = (row.get("sector") or "").strip()
|
|
205
|
+
|
|
206
|
+
# US industries carry themes their broad sector hides (Farming sits
|
|
207
|
+
# under Consumer Staples, Steel under Industrials, ...).
|
|
208
|
+
if industry:
|
|
209
|
+
hit = _match(industry)
|
|
210
|
+
if hit in (AGRICULTURE, NATURAL_RESOURCES, ENERGY, UTILITIES, TECHNOLOGY):
|
|
211
|
+
return hit, False
|
|
212
|
+
|
|
213
|
+
mapped = _SECTOR_MAP.get(sector.lower())
|
|
214
|
+
if mapped:
|
|
215
|
+
return mapped, False
|
|
216
|
+
if sector and row.get("market") in ("china_a", "hong_kong"):
|
|
217
|
+
zh = _match_zh(sector)
|
|
218
|
+
if zh:
|
|
219
|
+
return zh, False
|
|
220
|
+
|
|
221
|
+
if row.get("type") == "etf":
|
|
222
|
+
text = f"{row.get('symbol', '')} {row.get('name', '')} {row.get('underlying') or ''}"
|
|
223
|
+
# Sector themes first ("Technology Select Sector"), then asset class.
|
|
224
|
+
themed = _match(text)
|
|
225
|
+
if themed and themed not in (FINANCIALS, SERVICES):
|
|
226
|
+
return themed, True
|
|
227
|
+
for category, pattern in _ETF_RULES:
|
|
228
|
+
if pattern.search(text):
|
|
229
|
+
return category, True
|
|
230
|
+
return themed or OTHER, True
|
|
231
|
+
|
|
232
|
+
name = row.get("name") or ""
|
|
233
|
+
return _match(name) or _match_zh(row.get("name_local") or name) or OTHER, True
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def fund_category(row: dict) -> str:
|
|
237
|
+
"""Mutual funds: AMFI scheme group, cleaned into a short label."""
|
|
238
|
+
group = (row.get("fund_group") or "").lower()
|
|
239
|
+
sub = (row.get("sector") or "").lower()
|
|
240
|
+
if "index" in sub or "etf" in sub:
|
|
241
|
+
return "Index / ETF"
|
|
242
|
+
if "fund of funds" in sub or "fof" in sub:
|
|
243
|
+
return "Fund of Funds"
|
|
244
|
+
if "elss" in sub or "elss" in group:
|
|
245
|
+
return "ELSS (Tax Saving)"
|
|
246
|
+
for key, label in (
|
|
247
|
+
("equity", "Equity"),
|
|
248
|
+
("debt", "Debt"),
|
|
249
|
+
("income", "Debt"),
|
|
250
|
+
("hybrid", "Hybrid"),
|
|
251
|
+
("solution", "Solution Oriented"),
|
|
252
|
+
("children", "Solution Oriented"),
|
|
253
|
+
("retirement", "Solution Oriented"),
|
|
254
|
+
("growth", "Equity"),
|
|
255
|
+
):
|
|
256
|
+
if key in group:
|
|
257
|
+
return label
|
|
258
|
+
return "Other"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def enrich(rows: list[dict]) -> list[dict]:
|
|
262
|
+
"""Stamp ``category`` (+ ``category_inferred``) on every row in place."""
|
|
263
|
+
for row in rows:
|
|
264
|
+
if row.get("type") == "mf":
|
|
265
|
+
row["category"] = fund_category(row)
|
|
266
|
+
row["category_inferred"] = False
|
|
267
|
+
elif row.get("type") == "crypto":
|
|
268
|
+
row["category"] = "Crypto"
|
|
269
|
+
row["category_inferred"] = False
|
|
270
|
+
else:
|
|
271
|
+
row["category"], row["category_inferred"] = classify(row)
|
|
272
|
+
return rows
|