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.
Files changed (344) hide show
  1. marketmyna/__init__.py +14 -0
  2. marketmyna/agent/__init__.py +8 -0
  3. marketmyna/agent/agents.py +543 -0
  4. marketmyna/agent/context.py +69 -0
  5. marketmyna/agent/dashboard.py +55 -0
  6. marketmyna/agent/orchestrator.py +153 -0
  7. marketmyna/agent/sentiment.py +150 -0
  8. marketmyna/agent/skills.py +119 -0
  9. marketmyna/ai/__init__.py +1 -0
  10. marketmyna/ai/portfolio_optimizer.py +330 -0
  11. marketmyna/analysis/__init__.py +6 -0
  12. marketmyna/analysis/fundamental.py +329 -0
  13. marketmyna/analysis/sip_optimizer.py +81 -0
  14. marketmyna/analysis/technical.py +321 -0
  15. marketmyna/api/__init__.py +1 -0
  16. marketmyna/api/app.py +209 -0
  17. marketmyna/api/deps.py +63 -0
  18. marketmyna/api/middlewares/__init__.py +0 -0
  19. marketmyna/api/middlewares/auth.py +116 -0
  20. marketmyna/api/middlewares/csrf.py +66 -0
  21. marketmyna/api/middlewares/error_handler.py +38 -0
  22. marketmyna/api/middlewares/slashes.py +27 -0
  23. marketmyna/api/v1/__init__.py +0 -0
  24. marketmyna/api/v1/endpoints/__init__.py +0 -0
  25. marketmyna/api/v1/endpoints/account.py +166 -0
  26. marketmyna/api/v1/endpoints/agent.py +114 -0
  27. marketmyna/api/v1/endpoints/bot.py +38 -0
  28. marketmyna/api/v1/endpoints/calendar.py +269 -0
  29. marketmyna/api/v1/endpoints/data_sources.py +174 -0
  30. marketmyna/api/v1/endpoints/health.py +10 -0
  31. marketmyna/api/v1/endpoints/listings.py +564 -0
  32. marketmyna/api/v1/endpoints/macro.py +163 -0
  33. marketmyna/api/v1/endpoints/portfolio.py +530 -0
  34. marketmyna/api/v1/endpoints/push.py +119 -0
  35. marketmyna/api/v1/endpoints/reports.py +49 -0
  36. marketmyna/api/v1/endpoints/scheduler.py +219 -0
  37. marketmyna/api/v1/endpoints/stocks.py +400 -0
  38. marketmyna/api/v1/endpoints/system_config.py +157 -0
  39. marketmyna/api/v1/endpoints/thesis.py +112 -0
  40. marketmyna/api/v1/endpoints/trade.py +709 -0
  41. marketmyna/api/v1/endpoints/updates.py +394 -0
  42. marketmyna/api/v1/endpoints/watchlists.py +179 -0
  43. marketmyna/api/v1/router.py +43 -0
  44. marketmyna/bot/__init__.py +14 -0
  45. marketmyna/bot/commands/__init__.py +0 -0
  46. marketmyna/bot/commands/analyze.py +40 -0
  47. marketmyna/bot/commands/ask.py +51 -0
  48. marketmyna/bot/commands/base.py +35 -0
  49. marketmyna/bot/commands/batch.py +53 -0
  50. marketmyna/bot/commands/common.py +84 -0
  51. marketmyna/bot/commands/help.py +43 -0
  52. marketmyna/bot/commands/markets.py +153 -0
  53. marketmyna/bot/commands/portfolio.py +145 -0
  54. marketmyna/bot/commands/research.py +323 -0
  55. marketmyna/bot/commands/status.py +48 -0
  56. marketmyna/bot/commands/trade.py +176 -0
  57. marketmyna/bot/dispatcher.py +95 -0
  58. marketmyna/bot/formatting.py +25 -0
  59. marketmyna/bot/models.py +27 -0
  60. marketmyna/bot/registry.py +50 -0
  61. marketmyna/bot/telegram_listener.py +227 -0
  62. marketmyna/cli/__init__.py +1 -0
  63. marketmyna/cli/scheduled_run.py +292 -0
  64. marketmyna/core/__init__.py +6 -0
  65. marketmyna/core/compliance.py +53 -0
  66. marketmyna/core/config.py +377 -0
  67. marketmyna/core/constants.py +69 -0
  68. marketmyna/core/home.py +22 -0
  69. marketmyna/core/numbers.py +28 -0
  70. marketmyna/core/secret_store.py +104 -0
  71. marketmyna/core/tenant.py +130 -0
  72. marketmyna/data/__init__.py +8 -0
  73. marketmyna/data/base.py +36 -0
  74. marketmyna/data/categories.py +272 -0
  75. marketmyna/data/china.py +360 -0
  76. marketmyna/data/crypto.py +200 -0
  77. marketmyna/data/ecb_fx.py +131 -0
  78. marketmyna/data/extra_sources.py +569 -0
  79. marketmyna/data/india.py +241 -0
  80. marketmyna/data/india_filings.py +343 -0
  81. marketmyna/data/listings.py +1314 -0
  82. marketmyna/data/mutual_funds.py +34 -0
  83. marketmyna/data/news.py +98 -0
  84. marketmyna/data/news_search.py +346 -0
  85. marketmyna/data/nse_deals.py +138 -0
  86. marketmyna/data/resilience.py +173 -0
  87. marketmyna/data/sec_facts.py +252 -0
  88. marketmyna/data/shariah.py +283 -0
  89. marketmyna/data/social/__init__.py +9 -0
  90. marketmyna/data/social/base.py +100 -0
  91. marketmyna/data/social/browser.py +92 -0
  92. marketmyna/data/social/hackernews.py +53 -0
  93. marketmyna/data/social/reddit.py +397 -0
  94. marketmyna/data/social/stocktwits.py +63 -0
  95. marketmyna/data/social/valuepickr.py +53 -0
  96. marketmyna/data/stocks.py +197 -0
  97. marketmyna/data/yahoo_price.py +48 -0
  98. marketmyna/launcher.py +71 -0
  99. marketmyna/llm/__init__.py +33 -0
  100. marketmyna/llm/backend_factory.py +51 -0
  101. marketmyna/llm/backend_registry.py +19 -0
  102. marketmyna/llm/channels.py +447 -0
  103. marketmyna/llm/json_reply.py +63 -0
  104. marketmyna/llm/litellm_backend.py +197 -0
  105. marketmyna/llm/model_catalog.py +166 -0
  106. marketmyna/llm/model_health.py +193 -0
  107. marketmyna/llm/protocol.py +111 -0
  108. marketmyna/llm/providers.py +128 -0
  109. marketmyna/llm/store.py +128 -0
  110. marketmyna/notifiers/__init__.py +35 -0
  111. marketmyna/notifiers/apprise_sender.py +43 -0
  112. marketmyna/notifiers/astrbot.py +47 -0
  113. marketmyna/notifiers/bark.py +24 -0
  114. marketmyna/notifiers/base.py +80 -0
  115. marketmyna/notifiers/dingtalk.py +54 -0
  116. marketmyna/notifiers/discord.py +39 -0
  117. marketmyna/notifiers/dispatcher.py +153 -0
  118. marketmyna/notifiers/email.py +99 -0
  119. marketmyna/notifiers/feishu.py +57 -0
  120. marketmyna/notifiers/googlechat.py +24 -0
  121. marketmyna/notifiers/gotify.py +36 -0
  122. marketmyna/notifiers/matrix.py +28 -0
  123. marketmyna/notifiers/ntfy.py +36 -0
  124. marketmyna/notifiers/pushbullet.py +24 -0
  125. marketmyna/notifiers/pushover.py +41 -0
  126. marketmyna/notifiers/pushplus.py +41 -0
  127. marketmyna/notifiers/serverchan3.py +40 -0
  128. marketmyna/notifiers/settings_schema.py +119 -0
  129. marketmyna/notifiers/signal_messenger.py +25 -0
  130. marketmyna/notifiers/slack.py +36 -0
  131. marketmyna/notifiers/teams.py +37 -0
  132. marketmyna/notifiers/telegram.py +81 -0
  133. marketmyna/notifiers/twilio.py +33 -0
  134. marketmyna/notifiers/webhook.py +44 -0
  135. marketmyna/notifiers/webpush.py +222 -0
  136. marketmyna/notifiers/wechat.py +40 -0
  137. marketmyna/notifiers/whatsapp.py +27 -0
  138. marketmyna/portfolio/__init__.py +6 -0
  139. marketmyna/portfolio/analytics.py +336 -0
  140. marketmyna/portfolio/manager.py +383 -0
  141. marketmyna/portfolio/models.py +155 -0
  142. marketmyna/portfolio/optimizers.py +88 -0
  143. marketmyna/portfolio/review.py +411 -0
  144. marketmyna/services/__init__.py +1 -0
  145. marketmyna/services/accounts.py +278 -0
  146. marketmyna/services/ai_access.py +94 -0
  147. marketmyna/services/analysis_run.py +491 -0
  148. marketmyna/services/big_trades.py +335 -0
  149. marketmyna/services/brokers/__init__.py +0 -0
  150. marketmyna/services/brokers/angel.py +271 -0
  151. marketmyna/services/brokers/finnhub.py +209 -0
  152. marketmyna/services/byok_detect.py +196 -0
  153. marketmyna/services/calendar.py +748 -0
  154. marketmyna/services/central_banks.py +202 -0
  155. marketmyna/services/commodities.py +134 -0
  156. marketmyna/services/compare.py +307 -0
  157. marketmyna/services/cot.py +148 -0
  158. marketmyna/services/country_macro.py +135 -0
  159. marketmyna/services/crude_sensitivity.py +239 -0
  160. marketmyna/services/crypto_mood.py +135 -0
  161. marketmyna/services/crypto_screens.py +226 -0
  162. marketmyna/services/data_export.py +91 -0
  163. marketmyna/services/dcf.py +220 -0
  164. marketmyna/services/dcf_model.py +194 -0
  165. marketmyna/services/delivery.py +142 -0
  166. marketmyna/services/discovery.py +146 -0
  167. marketmyna/services/dividends.py +95 -0
  168. marketmyna/services/earnings.py +305 -0
  169. marketmyna/services/econ_calendar.py +495 -0
  170. marketmyna/services/eia_stocks.py +115 -0
  171. marketmyna/services/estimates.py +109 -0
  172. marketmyna/services/etf.py +236 -0
  173. marketmyna/services/etf_rotation.py +193 -0
  174. marketmyna/services/etf_screens.py +196 -0
  175. marketmyna/services/financials.py +639 -0
  176. marketmyna/services/fund_ai.py +181 -0
  177. marketmyna/services/fund_analytics.py +371 -0
  178. marketmyna/services/fund_cost.py +147 -0
  179. marketmyna/services/fund_holdings.py +664 -0
  180. marketmyna/services/fund_peers.py +247 -0
  181. marketmyna/services/fund_rotation.py +206 -0
  182. marketmyna/services/fund_style.py +179 -0
  183. marketmyna/services/funds.py +70 -0
  184. marketmyna/services/hot_stocks.py +358 -0
  185. marketmyna/services/india_flows.py +263 -0
  186. marketmyna/services/india_rates.py +258 -0
  187. marketmyna/services/investor_lenses.py +271 -0
  188. marketmyna/services/leadership.py +165 -0
  189. marketmyna/services/live_market.py +497 -0
  190. marketmyna/services/live_quotes.py +295 -0
  191. marketmyna/services/macro.py +182 -0
  192. marketmyna/services/market_calendar.py +399 -0
  193. marketmyna/services/market_mood.py +168 -0
  194. marketmyna/services/market_valuation.py +231 -0
  195. marketmyna/services/mf_holdings.py +199 -0
  196. marketmyna/services/mf_insights.py +132 -0
  197. marketmyna/services/mf_lumpsum.py +587 -0
  198. marketmyna/services/mf_portfolio_ai.py +195 -0
  199. marketmyna/services/nse_events.py +98 -0
  200. marketmyna/services/overview.py +366 -0
  201. marketmyna/services/ownership.py +143 -0
  202. marketmyna/services/quality_scores.py +199 -0
  203. marketmyna/services/rates.py +335 -0
  204. marketmyna/services/reconcile.py +343 -0
  205. marketmyna/services/report.py +584 -0
  206. marketmyna/services/report_i18n.py +678 -0
  207. marketmyna/services/rotation.py +224 -0
  208. marketmyna/services/runtime_scheduler.py +548 -0
  209. marketmyna/services/schedule_config.py +251 -0
  210. marketmyna/services/schedule_time.py +39 -0
  211. marketmyna/services/screens.py +398 -0
  212. marketmyna/services/sec_mdna.py +173 -0
  213. marketmyna/services/shariah.py +246 -0
  214. marketmyna/services/sip.py +148 -0
  215. marketmyna/services/sip_days.py +362 -0
  216. marketmyna/services/smart_file.py +595 -0
  217. marketmyna/services/smart_pdf.py +425 -0
  218. marketmyna/services/smart_report.py +370 -0
  219. marketmyna/services/social_sentiment.py +511 -0
  220. marketmyna/services/thesis.py +328 -0
  221. marketmyna/services/translate.py +146 -0
  222. marketmyna/services/valuation.py +173 -0
  223. marketmyna/services/worker_pool.py +149 -0
  224. marketmyna/services/world_markets.py +208 -0
  225. marketmyna/storage.py +722 -0
  226. marketmyna/strategies/README.md +51 -0
  227. marketmyna/strategies/bottom_volume.yaml +49 -0
  228. marketmyna/strategies/box_oscillation.yaml +69 -0
  229. marketmyna/strategies/bull_trend.yaml +49 -0
  230. marketmyna/strategies/chan_theory.yaml +58 -0
  231. marketmyna/strategies/dragon_head.yaml +44 -0
  232. marketmyna/strategies/emotion_cycle.yaml +79 -0
  233. marketmyna/strategies/event_driven.yaml +54 -0
  234. marketmyna/strategies/expectation_repricing.yaml +54 -0
  235. marketmyna/strategies/growth_quality.yaml +54 -0
  236. marketmyna/strategies/hot_theme.yaml +54 -0
  237. marketmyna/strategies/ma_golden_cross.yaml +46 -0
  238. marketmyna/strategies/one_yang_three_yin.yaml +37 -0
  239. marketmyna/strategies/shrink_pullback.yaml +46 -0
  240. marketmyna/strategies/volume_breakout.yaml +47 -0
  241. marketmyna/strategies/wave_theory.yaml +64 -0
  242. marketmyna/trading/__init__.py +2 -0
  243. marketmyna/trading/alerts.py +356 -0
  244. marketmyna/trading/backtest.py +317 -0
  245. marketmyna/trading/charges.py +82 -0
  246. marketmyna/trading/chart.py +197 -0
  247. marketmyna/trading/indicators.py +303 -0
  248. marketmyna/trading/journal.py +74 -0
  249. marketmyna/trading/knn.py +164 -0
  250. marketmyna/trading/lots.py +109 -0
  251. marketmyna/trading/market_hours.py +66 -0
  252. marketmyna/trading/options.py +554 -0
  253. marketmyna/trading/pairs.py +94 -0
  254. marketmyna/trading/risk.py +170 -0
  255. marketmyna/trading/scanner.py +500 -0
  256. marketmyna/trading/volatility.py +163 -0
  257. marketmyna/utils/__init__.py +1 -0
  258. marketmyna/utils/deadline.py +36 -0
  259. marketmyna/utils/log_redact.py +52 -0
  260. marketmyna/utils/yahoo_gate.py +183 -0
  261. marketmyna/utils/yf_lock.py +12 -0
  262. marketmyna/web/apple-touch-icon.png +0 -0
  263. marketmyna/web/assets/Account-CA9NFDKO.js +1 -0
  264. marketmyna/web/assets/Analyze-uM0srB_U.js +7 -0
  265. marketmyna/web/assets/AreaChart-2i67V6Jf.js +1 -0
  266. marketmyna/web/assets/BarChart-_yngarVl.js +3 -0
  267. marketmyna/web/assets/Bot-E4gpf1qZ.js +1 -0
  268. marketmyna/web/assets/CandleChart-CioqTqMV.js +1 -0
  269. marketmyna/web/assets/Compare-D6_2CPl4.js +1 -0
  270. marketmyna/web/assets/ExportMenu-DaZ1xVmW.js +1 -0
  271. marketmyna/web/assets/LineChart-BaOkD1Ta.js +57 -0
  272. marketmyna/web/assets/Markets-BMw_M1sJ.js +1 -0
  273. marketmyna/web/assets/Portfolio-DQEdHVc2.js +1 -0
  274. marketmyna/web/assets/ReportView-BzhdcqSh.js +1 -0
  275. marketmyna/web/assets/Scheduler-I9PDKxTd.js +1 -0
  276. marketmyna/web/assets/Settings-DpXfHolW.js +3 -0
  277. marketmyna/web/assets/SipDays-C9q3bNSx.js +1 -0
  278. marketmyna/web/assets/SymbolSearch-CBOVPEk1.js +1 -0
  279. marketmyna/web/assets/Trade-_CM7HC4H.js +2 -0
  280. marketmyna/web/assets/TradeChart-ZjsycacC.js +1 -0
  281. marketmyna/web/assets/WatchButton-ComYW56_.js +1 -0
  282. marketmyna/web/assets/Watchlist-DAjLb0jd.js +1 -0
  283. marketmyna/web/assets/app-BP1o7WZS.js +1 -0
  284. marketmyna/web/assets/app-BkIUKdld.js +1 -0
  285. marketmyna/web/assets/app-CKeSYhEt.js +1 -0
  286. marketmyna/web/assets/app-CN1OERYX.js +1 -0
  287. marketmyna/web/assets/app-DIQllha5.js +1 -0
  288. marketmyna/web/assets/app-DeiAW0tX.js +1 -0
  289. marketmyna/web/assets/br-Cu5YU29T.svg +45 -0
  290. marketmyna/web/assets/browser-CR-ks2Fp.js +19 -0
  291. marketmyna/web/assets/browser-D5K_K_gj.js +1 -0
  292. marketmyna/web/assets/common-0wvpAVpd.js +1 -0
  293. marketmyna/web/assets/common-3ghy53Us.js +1 -0
  294. marketmyna/web/assets/common-Bib2unjX.js +1 -0
  295. marketmyna/web/assets/common-BvSJP0w_.js +1 -0
  296. marketmyna/web/assets/common-CSfyC0Sp.js +1 -0
  297. marketmyna/web/assets/common-C_t-iZPf.js +1 -0
  298. marketmyna/web/assets/compare-DMcsx70H.js +1 -0
  299. marketmyna/web/assets/es-d5m8M5h8.svg +544 -0
  300. marketmyna/web/assets/exporter-DiMg2O5d.js +3 -0
  301. marketmyna/web/assets/files-CpgiZRbN.js +1 -0
  302. marketmyna/web/assets/format-d8InM0Zw.js +9 -0
  303. marketmyna/web/assets/html2canvas-Bw0OShmD.js +5 -0
  304. marketmyna/web/assets/index-CKAnq-Or.js +12 -0
  305. marketmyna/web/assets/index-D4U8E6LH.css +2 -0
  306. marketmyna/web/assets/index.es-iBEw7jOv.js +5 -0
  307. marketmyna/web/assets/ir-cCIgaNf6.svg +219 -0
  308. marketmyna/web/assets/jspdf.es.min-C4285IdL.js +79 -0
  309. marketmyna/web/assets/jspdf.plugin.autotable-2eOJeXl-.js +2 -0
  310. marketmyna/web/assets/lightweight-charts.production-BF2noug5.js +1 -0
  311. marketmyna/web/assets/listings-BefgXpb1.js +1 -0
  312. marketmyna/web/assets/liveQuotes-CtSmCUFA.js +1 -0
  313. marketmyna/web/assets/markets-BI4AhI9W.js +1 -0
  314. marketmyna/web/assets/markets-BUHUdYpT.js +1 -0
  315. marketmyna/web/assets/markets-BqvN1Bvf.js +1 -0
  316. marketmyna/web/assets/markets-DPO40TTe.js +1 -0
  317. marketmyna/web/assets/markets-bsiPo-Tk.js +1 -0
  318. marketmyna/web/assets/markets-ff1e792u.js +1 -0
  319. marketmyna/web/assets/montecarlo-BsNlH2qe.js +1 -0
  320. marketmyna/web/assets/options-BYAZnwzW.js +1 -0
  321. marketmyna/web/assets/preload-helper-BLdHbfdN.js +1 -0
  322. marketmyna/web/assets/purify.es-Dqk5qvad.js +3 -0
  323. marketmyna/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  324. marketmyna/web/assets/sa-Dh79zbT9.svg +25 -0
  325. marketmyna/web/assets/toast-DDradTZw.js +1 -0
  326. marketmyna/web/assets/trade-fndw1Ql3.js +1 -0
  327. marketmyna/web/assets/ui-gsFb8B2m.js +1 -0
  328. marketmyna/web/assets/watchlists-B6YqwUIk.js +1 -0
  329. marketmyna/web/brand/logo-mark.webp +0 -0
  330. marketmyna/web/favicon.png +0 -0
  331. marketmyna/web/icon-192.png +0 -0
  332. marketmyna/web/icon-512.png +0 -0
  333. marketmyna/web/index.html +34 -0
  334. marketmyna/web/manifest.webmanifest +28 -0
  335. marketmyna/web/robots.txt +2 -0
  336. marketmyna/web/sw.js +40 -0
  337. marketmyna/web/theme-init.js +37 -0
  338. marketmyna-0.2.0.dist-info/METADATA +345 -0
  339. marketmyna-0.2.0.dist-info/RECORD +344 -0
  340. marketmyna-0.2.0.dist-info/WHEEL +5 -0
  341. marketmyna-0.2.0.dist-info/entry_points.txt +2 -0
  342. marketmyna-0.2.0.dist-info/licenses/LICENSE +202 -0
  343. marketmyna-0.2.0.dist-info/licenses/NOTICE +22 -0
  344. 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