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,94 @@
|
|
|
1
|
+
"""Pairs test: are two prices cointegrated, and how stretched is the spread now?
|
|
2
|
+
|
|
3
|
+
Engle-Granger two-step, the method behind OpenBB's econometrics cointegration
|
|
4
|
+
command: regress log(A) on log(B) for the hedge ratio, then run an augmented
|
|
5
|
+
Dickey-Fuller test on the residual spread. A stationary spread means the pair
|
|
6
|
+
tends to drift back together, the premise of pairs trading. Plain numpy, so no
|
|
7
|
+
statsmodels dependency; critical values are MacKinnon's (2010) for two series
|
|
8
|
+
with a constant.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
# MacKinnon (2010) asymptotic critical values, Engle-Granger, N=2, constant, no trend
|
|
19
|
+
EG_CRITICAL = {"1%": -3.90, "5%": -3.34, "10%": -3.04}
|
|
20
|
+
MIN_BARS = 120
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ols(y: np.ndarray, x: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
24
|
+
beta, *_ = np.linalg.lstsq(x, y, rcond=None)
|
|
25
|
+
return beta, y - x @ beta
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def adf(series: np.ndarray, max_lag: int | None = None) -> dict:
|
|
29
|
+
"""ADF t-statistic (constant, lag order by BIC) for a residual series.
|
|
30
|
+
|
|
31
|
+
BIC rather than AIC: daily closes of two listings carry bid-ask and timing
|
|
32
|
+
noise that AIC answers with many lags, which drains the test of power."""
|
|
33
|
+
y = np.asarray(series, dtype=float)
|
|
34
|
+
n = len(y)
|
|
35
|
+
if max_lag is None:
|
|
36
|
+
max_lag = int(12 * (n / 100) ** 0.25)
|
|
37
|
+
dy = np.diff(y)
|
|
38
|
+
best = None
|
|
39
|
+
for lag in range(max_lag + 1):
|
|
40
|
+
rows = len(dy) - max_lag # same sample for every lag so BIC is comparable
|
|
41
|
+
target = dy[max_lag:]
|
|
42
|
+
cols = [np.ones(rows), y[max_lag:-1]]
|
|
43
|
+
cols += [dy[max_lag - k:len(dy) - k] for k in range(1, lag + 1)]
|
|
44
|
+
x = np.column_stack(cols)
|
|
45
|
+
beta, resid = _ols(target, x)
|
|
46
|
+
sigma2 = float(resid @ resid) / rows
|
|
47
|
+
bic = rows * math.log(sigma2) + x.shape[1] * math.log(rows)
|
|
48
|
+
if best is None or bic < best[0]:
|
|
49
|
+
dof = rows - x.shape[1]
|
|
50
|
+
s2 = float(resid @ resid) / dof
|
|
51
|
+
se = math.sqrt(s2 * np.linalg.inv(x.T @ x)[1, 1])
|
|
52
|
+
best = (bic, float(beta[1] / se), lag)
|
|
53
|
+
return {"stat": best[1], "lag": best[2]}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def analyze(a: pd.Series, b: pd.Series) -> dict:
|
|
57
|
+
"""a, b: close prices indexed by date. Raises ValueError on too little overlap."""
|
|
58
|
+
df = pd.concat([a, b], axis=1, keys=["a", "b"]).dropna()
|
|
59
|
+
df = df[(df > 0).all(axis=1)]
|
|
60
|
+
if len(df) < MIN_BARS:
|
|
61
|
+
raise ValueError(f"Need at least {MIN_BARS} shared trading days, got {len(df)}")
|
|
62
|
+
la, lb = np.log(df["a"].to_numpy()), np.log(df["b"].to_numpy())
|
|
63
|
+
(alpha, hedge), spread = _ols(la, np.column_stack([np.ones(len(lb)), lb]))
|
|
64
|
+
test = adf(spread)
|
|
65
|
+
|
|
66
|
+
# mean-reversion speed: d(spread) = k * spread(t-1) + e -> half-life = -ln2 / k
|
|
67
|
+
k = float(np.polyfit(spread[:-1], np.diff(spread), 1)[0])
|
|
68
|
+
half_life = -math.log(2) / k if k < 0 else None
|
|
69
|
+
z = (spread - spread.mean()) / spread.std()
|
|
70
|
+
ret = df.pct_change().dropna()
|
|
71
|
+
|
|
72
|
+
level = next((lvl for lvl, cv in EG_CRITICAL.items() if test["stat"] < cv), None)
|
|
73
|
+
z_now = float(z[-1])
|
|
74
|
+
if level is None:
|
|
75
|
+
verdict = "not_cointegrated"
|
|
76
|
+
elif abs(z_now) >= 2:
|
|
77
|
+
verdict = "stretched"
|
|
78
|
+
else:
|
|
79
|
+
verdict = "cointegrated"
|
|
80
|
+
return {
|
|
81
|
+
"days": len(df),
|
|
82
|
+
"start": str(df.index[0].date()),
|
|
83
|
+
"end": str(df.index[-1].date()),
|
|
84
|
+
"hedge_ratio": float(hedge),
|
|
85
|
+
"correlation": float(ret["a"].corr(ret["b"])),
|
|
86
|
+
"adf_stat": test["stat"],
|
|
87
|
+
"adf_lag": test["lag"],
|
|
88
|
+
"critical": EG_CRITICAL,
|
|
89
|
+
"significance": level,
|
|
90
|
+
"half_life_days": half_life,
|
|
91
|
+
"z_now": z_now,
|
|
92
|
+
"verdict": verdict,
|
|
93
|
+
"spread": [{"time": str(d.date()), "z": round(float(v), 3)} for d, v in zip(df.index, z)],
|
|
94
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Daily risk rules: loss limit, open risk, trades per day.
|
|
2
|
+
|
|
3
|
+
Status "stop" means today's net P&L (realized + open positions) has hit
|
|
4
|
+
the daily loss limit: the Trade page shows a stop banner and one alert is
|
|
5
|
+
sent. Nothing is blocked - MarketMyna never touches orders."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Callable, Optional
|
|
12
|
+
|
|
13
|
+
from .journal import trade_pnl
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
DEFAULTS = {
|
|
18
|
+
"market": "india",
|
|
19
|
+
"capital": 100000.0,
|
|
20
|
+
"daily_loss_limit_pct": 2.0,
|
|
21
|
+
"max_open_risk_pct": 5.0,
|
|
22
|
+
"max_trades_per_day": 5,
|
|
23
|
+
"notify": True,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _db(config):
|
|
28
|
+
from ..storage import DatabaseManager, get_db_url
|
|
29
|
+
|
|
30
|
+
return DatabaseManager.instance(get_db_url(config=config))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_settings(config) -> dict:
|
|
34
|
+
from ..storage import TradeRiskSettingsRecord
|
|
35
|
+
|
|
36
|
+
with _db(config).get_session() as s:
|
|
37
|
+
rec = s.query(TradeRiskSettingsRecord).order_by(TradeRiskSettingsRecord.id).first() # one row per user
|
|
38
|
+
if rec is None:
|
|
39
|
+
return {**DEFAULTS, "notified_day": None}
|
|
40
|
+
return {c.name: getattr(rec, c.name) for c in rec.__table__.columns if c.name not in ("id", "user_id")}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_settings(config, **changes) -> dict:
|
|
44
|
+
from ..storage import TradeRiskSettingsRecord
|
|
45
|
+
|
|
46
|
+
with _db(config).get_session() as s:
|
|
47
|
+
rec = s.query(TradeRiskSettingsRecord).order_by(TradeRiskSettingsRecord.id).first() # one row per user
|
|
48
|
+
if rec is None:
|
|
49
|
+
rec = TradeRiskSettingsRecord(**DEFAULTS)
|
|
50
|
+
s.add(rec)
|
|
51
|
+
for k, v in changes.items():
|
|
52
|
+
setattr(rec, k, v)
|
|
53
|
+
return load_settings(config)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compute(trades: list[dict], settings: dict, quotes: dict[str, float], now: datetime) -> dict:
|
|
57
|
+
"""Pure: today's risk picture from journal trades (dicts) and live prices."""
|
|
58
|
+
day = now.date()
|
|
59
|
+
mine = [t for t in trades if t["market"] == settings["market"]]
|
|
60
|
+
closed_today = [t for t in mine if t.get("exit_price") is not None and t.get("exit_at") and t["exit_at"].date() == day]
|
|
61
|
+
opened_today = [t for t in mine if t.get("entry_at") and t["entry_at"].date() == day]
|
|
62
|
+
open_trades = [t for t in mine if t.get("exit_price") is None]
|
|
63
|
+
|
|
64
|
+
realized = sum(trade_pnl(t) for t in closed_today)
|
|
65
|
+
unrealized, open_risk, no_stop, positions = 0.0, 0.0, [], []
|
|
66
|
+
for t in open_trades:
|
|
67
|
+
sign = -1 if t.get("side") == "short" else 1
|
|
68
|
+
price = quotes.get(t["symbol"])
|
|
69
|
+
pnl = (price - t["entry_price"]) * t["quantity"] * sign if price is not None else None
|
|
70
|
+
if pnl is not None:
|
|
71
|
+
unrealized += pnl
|
|
72
|
+
if t.get("stop"):
|
|
73
|
+
# Risk left = distance from the current price (or entry) to the stop.
|
|
74
|
+
ref = price if price is not None else t["entry_price"]
|
|
75
|
+
open_risk += max(0.0, (ref - t["stop"]) * sign) * t["quantity"]
|
|
76
|
+
else:
|
|
77
|
+
no_stop.append(t["symbol"])
|
|
78
|
+
positions.append({"id": t["id"], "symbol": t["symbol"], "side": t.get("side"), "quantity": t["quantity"],
|
|
79
|
+
"entry_price": t["entry_price"], "stop": t.get("stop"), "price": price,
|
|
80
|
+
"pnl": round(pnl, 2) if pnl is not None else None})
|
|
81
|
+
|
|
82
|
+
capital = settings["capital"] or 0
|
|
83
|
+
limit = capital * (settings["daily_loss_limit_pct"] or 0) / 100
|
|
84
|
+
max_open = capital * (settings["max_open_risk_pct"] or 0) / 100
|
|
85
|
+
day_pnl = realized + unrealized
|
|
86
|
+
reasons, status = [], "ok"
|
|
87
|
+
if limit and day_pnl <= -limit:
|
|
88
|
+
status = "stop"
|
|
89
|
+
reasons.append(f"Daily loss limit hit: {day_pnl:,.2f} vs limit -{limit:,.2f}. Stop trading for today.")
|
|
90
|
+
else:
|
|
91
|
+
if limit and day_pnl <= -0.75 * limit:
|
|
92
|
+
reasons.append(f"{abs(day_pnl) / limit * 100:.0f}% of today's loss limit used.")
|
|
93
|
+
if max_open and open_risk > max_open:
|
|
94
|
+
reasons.append(f"Open risk {open_risk:,.2f} is above the {settings['max_open_risk_pct']}% cap ({max_open:,.2f}).")
|
|
95
|
+
if settings["max_trades_per_day"] and len(opened_today) >= settings["max_trades_per_day"]:
|
|
96
|
+
reasons.append(f"{len(opened_today)} trades today: at the daily maximum of {settings['max_trades_per_day']}.")
|
|
97
|
+
if no_stop:
|
|
98
|
+
reasons.append(f"No stop set on {', '.join(no_stop)}.")
|
|
99
|
+
if reasons:
|
|
100
|
+
status = "warn"
|
|
101
|
+
return {
|
|
102
|
+
"status": status,
|
|
103
|
+
"reasons": reasons,
|
|
104
|
+
"day_pnl": round(day_pnl, 2),
|
|
105
|
+
"realized": round(realized, 2),
|
|
106
|
+
"unrealized": round(unrealized, 2),
|
|
107
|
+
"loss_limit": round(limit, 2),
|
|
108
|
+
"limit_used_pct": round(max(0.0, -day_pnl) / limit * 100, 1) if limit else None,
|
|
109
|
+
"open_risk": round(open_risk, 2),
|
|
110
|
+
"open_risk_pct": round(open_risk / capital * 100, 2) if capital else None,
|
|
111
|
+
"max_open_risk": round(max_open, 2),
|
|
112
|
+
"trades_today": len(opened_today),
|
|
113
|
+
"closed_today": len(closed_today),
|
|
114
|
+
"positions": positions,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _trades(config) -> list[dict]:
|
|
119
|
+
from ..storage import TradeRecord
|
|
120
|
+
|
|
121
|
+
with _db(config).get_session() as s:
|
|
122
|
+
return [{c.name: getattr(r, c.name) for c in r.__table__.columns} for r in s.query(TradeRecord)]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _default_quote(symbol: str, market: str) -> Optional[float]:
|
|
126
|
+
from ..agent.agents import _resolve_fetcher
|
|
127
|
+
from ..utils.deadline import call_with_deadline
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
return (call_with_deadline(_resolve_fetcher(market).get_quote, 15, symbol) or {}).get("price")
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.warning("Quote for %s failed: %s", symbol, exc)
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def status(config, now: Optional[datetime] = None, quote_fn: Callable[[str, str], Optional[float]] = _default_quote) -> dict:
|
|
137
|
+
settings = load_settings(config)
|
|
138
|
+
trades = _trades(config)
|
|
139
|
+
open_symbols = {t["symbol"] for t in trades if t["market"] == settings["market"] and t.get("exit_price") is None}
|
|
140
|
+
quotes = {sym: q for sym in open_symbols if (q := quote_fn(sym, settings["market"])) is not None}
|
|
141
|
+
return {"settings": settings, **compute(trades, settings, quotes, now or datetime.now())}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def check_daily_limit(config, now: Optional[datetime] = None, dispatcher=None, quote_fn=_default_quote) -> Optional[dict]:
|
|
145
|
+
"""Monitor hook: one alert per day when the loss limit is hit (only
|
|
146
|
+
while the market is open and there is trading activity)."""
|
|
147
|
+
from .market_hours import is_open
|
|
148
|
+
|
|
149
|
+
now = now or datetime.now()
|
|
150
|
+
settings = load_settings(config)
|
|
151
|
+
if not settings.get("notify"):
|
|
152
|
+
return None
|
|
153
|
+
aware = now.astimezone() if now.tzinfo is None else now
|
|
154
|
+
if not is_open(settings["market"], aware):
|
|
155
|
+
return None
|
|
156
|
+
today = now.strftime("%Y-%m-%d")
|
|
157
|
+
if settings.get("notified_day") == today:
|
|
158
|
+
return None
|
|
159
|
+
result = status(config, now.replace(tzinfo=None), quote_fn)
|
|
160
|
+
if result["status"] != "stop":
|
|
161
|
+
return result
|
|
162
|
+
try:
|
|
163
|
+
from ..notifiers.dispatcher import NotificationDispatcher
|
|
164
|
+
|
|
165
|
+
dispatcher = dispatcher or NotificationDispatcher(config)
|
|
166
|
+
dispatcher.send_all("🛑 " + result["reasons"][0], title="Daily loss limit", purpose="alert")
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
logger.error("Risk alert failed: %s", exc)
|
|
169
|
+
save_settings(config, notified_day=today)
|
|
170
|
+
return result
|