bdshare 1.2.2__tar.gz → 1.2.3__tar.gz

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 (42) hide show
  1. {bdshare-1.2.2 → bdshare-1.2.3}/CHANGELOG.md +17 -0
  2. {bdshare-1.2.2/bdshare.egg-info → bdshare-1.2.3}/PKG-INFO +165 -6
  3. {bdshare-1.2.2 → bdshare-1.2.3}/README.md +143 -5
  4. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/__init__.py +1 -5
  5. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/_version.py +1 -1
  6. bdshare-1.2.3/bdshare/indicators.py +100 -0
  7. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/mcp_server.py +26 -1
  8. bdshare-1.2.3/bdshare/portfolio.py +138 -0
  9. bdshare-1.2.3/bdshare/stream.py +161 -0
  10. {bdshare-1.2.2 → bdshare-1.2.3/bdshare.egg-info}/PKG-INFO +165 -6
  11. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare.egg-info/SOURCES.txt +6 -0
  12. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare.egg-info/entry_points.txt +1 -0
  13. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare.egg-info/requires.txt +6 -0
  14. {bdshare-1.2.2 → bdshare-1.2.3}/setup.cfg +6 -1
  15. bdshare-1.2.3/tests/test_indicators.py +89 -0
  16. bdshare-1.2.3/tests/test_portfolio.py +110 -0
  17. bdshare-1.2.3/tests/test_stream.py +131 -0
  18. {bdshare-1.2.2 → bdshare-1.2.3}/LICENSE +0 -0
  19. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/stock/__init__.py +0 -0
  20. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/stock/market.py +0 -0
  21. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/stock/news.py +0 -0
  22. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/stock/trading.py +0 -0
  23. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/__init__.py +0 -0
  24. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/cache.py +0 -0
  25. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/cons.py +0 -0
  26. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/helper.py +0 -0
  27. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/proxy.py +0 -0
  28. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/sectigo_dv_r36.pem +0 -0
  29. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/session.py +0 -0
  30. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/store.py +0 -0
  31. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/tickers.py +0 -0
  32. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/upass.py +0 -0
  33. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare/util/vars.py +0 -0
  34. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare.egg-info/dependency_links.txt +0 -0
  35. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare.egg-info/not-zip-safe +0 -0
  36. {bdshare-1.2.2 → bdshare-1.2.3}/bdshare.egg-info/top_level.txt +0 -0
  37. {bdshare-1.2.2 → bdshare-1.2.3}/pyproject.toml +0 -0
  38. {bdshare-1.2.2 → bdshare-1.2.3}/tests/__init__.py +0 -0
  39. {bdshare-1.2.2 → bdshare-1.2.3}/tests/test_market.py +0 -0
  40. {bdshare-1.2.2 → bdshare-1.2.3}/tests/test_news.py +0 -0
  41. {bdshare-1.2.2 → bdshare-1.2.3}/tests/test_polars.py +0 -0
  42. {bdshare-1.2.2 → bdshare-1.2.3}/tests/test_trading.py +0 -0
@@ -6,6 +6,23 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
  Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
+ ## [1.2.3] - 2026-07-22
10
+
11
+ ### Added
12
+ - `bdshare/indicators.py` — technical indicator helpers (`add_sma`, `add_ema`, `add_rsi`, `add_macd`, `add_bollinger_bands`, `add_indicators`) built on the `ta` library, operating on the OHLCV DataFrames returned by `get_basic_historical_data()`; install with `pip install bdshare[ta]`
13
+ - `bdshare/portfolio.py` — `Portfolio`/`Position` classes for tracking cost basis and valuing holdings against live prices (`holdings()`, `valuation()`, `summary()`); one live call for all instruments regardless of portfolio size
14
+ - `bdshare/stream.py` — polling-based WebSocket tick streaming (`stream_ticks()`, `TickServer`); broadcasts changed rows from `get_current_trade_data()` to connected clients so another program can subscribe over `ws://` instead of polling itself. DSE has no push API, so this is poll-and-diff under a WebSocket-shaped interface, not true push. Install with `pip install bdshare[stream]`, run with `bdshare-stream`
15
+ - `demo/streamlit/streamlit_app.py` — full-feature Streamlit dashboard showcasing every public feature in one app: live trading data, historical charts with `bdshare.indicators` overlays, market movers, news, `bdshare.portfolio` tracking, live-tick polling, and an interactive AI-agent (MCP) tool explorer. Runs via `docker compose up --build streamlit` (`demo/streamlit/Dockerfile`, builds bdshare from local source so unreleased features are included) or locally with `pip install -e ".[ta,stream]"` + `streamlit run demo/streamlit/streamlit_app.py`
16
+ - `demo/` reorganized into `demo/flask/`, `demo/streamlit/`, and `demo/node/`, all built from `demo/docker-compose.yml` with a repo-root build context; documented in `demo/README.md` (previously an unrelated generic Flask/Plotly readme with no mention of bdshare or Docker)
17
+ - `bdshare-mcp --transport streamable-http --host 0.0.0.0 --port 8000` — the MCP server can now run over the network instead of only stdio, for clients that can't spawn it as a local child process (a different language, another container). Binding beyond loopback auto-disables DNS-rebinding protection, since every legitimate client then arrives with a non-localhost `Host` header
18
+ - `demo/node/` — a four-service showcase of consuming bdshare from **outside Python**: an Express UI (`demo/node/server`, never imports bdshare) talking to a FastAPI backend (`demo/node/api`, bdshare functions as JSON REST) over REST, to `bdshare-mcp --transport streamable-http` over MCP via the official `@modelcontextprotocol/sdk`, and to `bdshare-stream` over WebSocket (relayed to the browser). Run via `docker compose up --build node node-api mcp stream` or locally per `demo/README.md`
19
+
20
+ ### Fixed
21
+ - `bdshare.BDShareError` (the publicly exported/documented exception) was a **different class** from `bdshare.util.helper.BDShareError`, the one every scraping function actually raises — `except bdshare.BDShareError` silently never caught real errors. `bdshare/__init__.py` now re-exports the same class instead of redefining it
22
+ - `demo/app.py`: `convert_to_native_types()` called `pd.isna(obj)` before checking whether `obj` was a list/dict, crashing with `ValueError: The truth value of an array... is ambiguous` on any chart request; container types are now checked first
23
+ - `demo/app.py`: `get_available_tickers()` checked for a `trading_code` column that doesn't exist on `get_current_trade_data()`'s output (`symbol` does), so it always silently fell back to the hardcoded stock list
24
+ - `demo/Dockerfile`: base image `python:3.11-slim-buster` is EOL (Debian Buster archived), so `apt-get update` would fail on a fresh build; switched to `python:3.11-slim`
25
+
9
26
  ## [1.2.2] - 2026-07-22
10
27
 
11
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bdshare
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: A utility for crawling historical and Real-time Quotes of DSE(Dhaka Stock Exchange)
5
5
  Author: Raisul Islam
6
6
  Author-email: raisul.me@gmail.com
@@ -20,6 +20,10 @@ Provides-Extra: polars
20
20
  Requires-Dist: polars; extra == "polars"
21
21
  Provides-Extra: mcp
22
22
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
23
+ Provides-Extra: ta
24
+ Requires-Dist: ta; extra == "ta"
25
+ Provides-Extra: stream
26
+ Requires-Dist: websockets>=12.0; extra == "stream"
23
27
  Dynamic: license-file
24
28
 
25
29
  # bdshare
@@ -38,6 +42,7 @@ Dynamic: license-file
38
42
 
39
43
  - [Installation](#installation)
40
44
  - [Quick Start](#quick-start)
45
+ - [Demo App](#demo-app)
41
46
  - [Core Concepts](#core-concepts)
42
47
  - [Usage Guide](#usage-guide)
43
48
  - [Live Trading Data](#live-trading-data)
@@ -49,6 +54,10 @@ Dynamic: license-file
49
54
  - [Error Handling](#error-handling)
50
55
  - [API Reference](#api-reference)
51
56
  - [Examples](#examples)
57
+ - [Advanced Features](#advanced-features)
58
+ - [Technical Indicators](#technical-indicators)
59
+ - [Portfolio Tracking](#portfolio-tracking)
60
+ - [Real-Time Streaming (WebSocket)](#real-time-streaming-websocket)
52
61
  - [Using bdshare with AI Agents (MCP Server)](#using-bdshare-with-ai-agents-mcp-server)
53
62
  - [Contributing](#contributing)
54
63
  - [Roadmap](#roadmap)
@@ -100,6 +109,29 @@ with BDShare() as bd:
100
109
 
101
110
  ---
102
111
 
112
+ ## Demo App
113
+
114
+ Three full-feature demos live in [`demo/`](demo/) — live prices, historical charts with
115
+ technical indicators, market movers, news, a portfolio tracker, live tick polling, and
116
+ an interactive AI-agent (MCP) tool explorer:
117
+
118
+ ```bash
119
+ cd demo
120
+ docker compose up --build streamlit # pure Python — open http://localhost:8501
121
+ docker compose up --build node node-api mcp stream # cross-language via REST/MCP/WebSocket — open http://localhost:3000
122
+ docker compose up --build app # Flask/Plotly candlestick-only demo — open http://localhost:9999
123
+ ```
124
+
125
+ The Node.js demo is the interesting one if you're integrating bdshare from **outside
126
+ Python**: it's an Express UI that never imports bdshare — it talks to a FastAPI backend
127
+ over REST, to `bdshare-mcp` over HTTP using the official MCP SDK, and to
128
+ `bdshare-stream` over WebSocket, showing three different ways another program can
129
+ consume this library.
130
+
131
+ See [`demo/README.md`](demo/README.md) for local (non-Docker) setup for each.
132
+
133
+ ---
134
+
103
135
  ## Core Concepts
104
136
 
105
137
  | Concept | Details |
@@ -448,6 +480,100 @@ print(big_movers[['symbol', 'close', 'change']])
448
480
 
449
481
  ---
450
482
 
483
+ ## Advanced Features
484
+
485
+ ### Technical Indicators
486
+
487
+ Wraps the [`ta`](https://github.com/bukosabino/ta) library to add indicator columns
488
+ directly onto bdshare's OHLCV DataFrames.
489
+
490
+ ```bash
491
+ pip install "bdshare[ta]"
492
+ ```
493
+
494
+ ```python
495
+ from bdshare import get_basic_historical_data
496
+ from bdshare.indicators import add_indicators, add_rsi
497
+
498
+ df = get_basic_historical_data('2024-01-01', '2024-06-30', 'GP')
499
+
500
+ # Add everything (sma_20, ema_20, rsi_14, macd/macd_signal/macd_diff, bb_high/bb_mid/bb_low)
501
+ df = add_indicators(df)
502
+
503
+ # Or just one, with custom parameters
504
+ df = add_rsi(df, window=21)
505
+ ```
506
+
507
+ Also available: `add_sma()`, `add_ema()`, `add_macd()`, `add_bollinger_bands()`.
508
+ Each returns a new DataFrame (the input is never mutated).
509
+
510
+ ### Portfolio Tracking
511
+
512
+ `Portfolio` tracks cost basis and values holdings against live prices — no extra
513
+ dependency required (pure pandas), and `valuation()` makes exactly **one** live call
514
+ for all instruments regardless of how many positions you hold.
515
+
516
+ ```python
517
+ from bdshare.portfolio import Portfolio
518
+
519
+ pf = Portfolio()
520
+ pf.add_position('GP', quantity=100, avg_cost=450.50)
521
+ pf.add_position('ACI', quantity=50, avg_cost=225.75)
522
+
523
+ print(pf.holdings().to_string()) # cost basis only, no network call
524
+ print(pf.valuation().to_string()) # + ltp, market_value, pnl, pnl_pct per position
525
+ print(pf.summary()) # {'positions': 2, 'total_cost': ..., 'total_pnl': ...}
526
+ ```
527
+
528
+ Adding to an existing position blends the cost basis like a real buy; a negative
529
+ `quantity` reduces it, and netting to zero drops the position. Unknown/delisted
530
+ symbols get `None` valuation fields instead of raising, so one bad symbol doesn't
531
+ block valuing the rest.
532
+
533
+ ### Real-Time Streaming (WebSocket)
534
+
535
+ DSE has no public push/streaming API — this polls `get_current_trade_data()` on an
536
+ interval and broadcasts **changed** rows over WebSocket, so another program can
537
+ subscribe instead of polling bdshare itself.
538
+
539
+ ```bash
540
+ pip install "bdshare[stream]"
541
+ ```
542
+
543
+ Run the bundled server:
544
+
545
+ ```bash
546
+ bdshare-stream --symbols GP,ACI --interval 5
547
+ ```
548
+
549
+ Any other program connects as a plain WebSocket client:
550
+
551
+ ```python
552
+ import asyncio, json, websockets
553
+
554
+ async def main():
555
+ async with websockets.connect('ws://localhost:8765') as ws:
556
+ async for message in ws:
557
+ print(json.loads(message)) # {"type": "ticks", "data": [...]}
558
+
559
+ asyncio.run(main())
560
+ ```
561
+
562
+ Or skip the server and use the polling generator directly inside your own asyncio
563
+ program:
564
+
565
+ ```python
566
+ from bdshare.stream import stream_ticks
567
+
568
+ async def main():
569
+ async for changed in stream_ticks(symbols=['GP', 'ACI'], interval=5.0):
570
+ print(changed)
571
+
572
+ asyncio.run(main())
573
+ ```
574
+
575
+ ---
576
+
451
577
  ## Using bdshare with AI Agents (MCP Server)
452
578
 
453
579
  bdshare ships an [MCP](https://modelcontextprotocol.io/) server so AI agents running in
@@ -493,6 +619,23 @@ claude mcp add bdshare -- bdshare-mcp
493
619
  Any other MCP client is configured the same way — point it at the `bdshare-mcp` command
494
620
  (or `python -m bdshare.mcp_server`), stdio transport.
495
621
 
622
+ ### Running it over the network instead of stdio
623
+
624
+ Stdio only works when the client can spawn the server as a local child process (Claude
625
+ Desktop/Code). For a program in **another container, another machine, or another
626
+ language entirely** — nothing that speaks Python — run it over HTTP instead:
627
+
628
+ ```bash
629
+ bdshare-mcp --transport streamable-http --host 0.0.0.0 --port 8000
630
+ ```
631
+
632
+ Any MCP client library can connect to `http://host:8000/mcp`; see
633
+ [`demo/node/`](demo/node/) for a full example using the official
634
+ `@modelcontextprotocol/sdk` from Node.js. Binding beyond `127.0.0.1`/`localhost`
635
+ automatically disables DNS-rebinding protection, since every legitimate client then
636
+ arrives with a non-localhost `Host` header — only do this on a trusted network (an
637
+ internal Docker network, not the public internet).
638
+
496
639
  ### What the agent gets
497
640
 
498
641
  15 tools covering the same data this README documents — `market_status`,
@@ -535,11 +678,10 @@ Please open an issue before submitting a pull request for significant changes. S
535
678
 
536
679
  ## Roadmap
537
680
 
538
- - [ ] Chittagong Stock Exchange (CSE) support
539
- - [ ] WebSocket streaming for real-time ticks
540
- - [ ] Built-in technical indicators (`ta` integration)
541
- - [ ] Portfolio management helpers
542
- - [ ] Docker demo examples
681
+ - [x] WebSocket streaming for real-time ticks (polling-based — see [Real-Time Streaming](#real-time-streaming-websocket))
682
+ - [x] Built-in technical indicators (`ta` integration)
683
+ - [x] Portfolio management helpers
684
+ - [x] Docker demo examples
543
685
  - [x] Shared session with exponential back-off
544
686
  - [x] `lxml`-based fast parsing
545
687
  - [x] `BDShareError` for clean error handling
@@ -572,6 +714,23 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
572
714
  Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
573
715
 
574
716
  ---
717
+ ## [1.2.3] - 2026-07-22
718
+
719
+ ### Added
720
+ - `bdshare/indicators.py` — technical indicator helpers (`add_sma`, `add_ema`, `add_rsi`, `add_macd`, `add_bollinger_bands`, `add_indicators`) built on the `ta` library, operating on the OHLCV DataFrames returned by `get_basic_historical_data()`; install with `pip install bdshare[ta]`
721
+ - `bdshare/portfolio.py` — `Portfolio`/`Position` classes for tracking cost basis and valuing holdings against live prices (`holdings()`, `valuation()`, `summary()`); one live call for all instruments regardless of portfolio size
722
+ - `bdshare/stream.py` — polling-based WebSocket tick streaming (`stream_ticks()`, `TickServer`); broadcasts changed rows from `get_current_trade_data()` to connected clients so another program can subscribe over `ws://` instead of polling itself. DSE has no push API, so this is poll-and-diff under a WebSocket-shaped interface, not true push. Install with `pip install bdshare[stream]`, run with `bdshare-stream`
723
+ - `demo/streamlit/streamlit_app.py` — full-feature Streamlit dashboard showcasing every public feature in one app: live trading data, historical charts with `bdshare.indicators` overlays, market movers, news, `bdshare.portfolio` tracking, live-tick polling, and an interactive AI-agent (MCP) tool explorer. Runs via `docker compose up --build streamlit` (`demo/streamlit/Dockerfile`, builds bdshare from local source so unreleased features are included) or locally with `pip install -e ".[ta,stream]"` + `streamlit run demo/streamlit/streamlit_app.py`
724
+ - `demo/` reorganized into `demo/flask/`, `demo/streamlit/`, and `demo/node/`, all built from `demo/docker-compose.yml` with a repo-root build context; documented in `demo/README.md` (previously an unrelated generic Flask/Plotly readme with no mention of bdshare or Docker)
725
+ - `bdshare-mcp --transport streamable-http --host 0.0.0.0 --port 8000` — the MCP server can now run over the network instead of only stdio, for clients that can't spawn it as a local child process (a different language, another container). Binding beyond loopback auto-disables DNS-rebinding protection, since every legitimate client then arrives with a non-localhost `Host` header
726
+ - `demo/node/` — a four-service showcase of consuming bdshare from **outside Python**: an Express UI (`demo/node/server`, never imports bdshare) talking to a FastAPI backend (`demo/node/api`, bdshare functions as JSON REST) over REST, to `bdshare-mcp --transport streamable-http` over MCP via the official `@modelcontextprotocol/sdk`, and to `bdshare-stream` over WebSocket (relayed to the browser). Run via `docker compose up --build node node-api mcp stream` or locally per `demo/README.md`
727
+
728
+ ### Fixed
729
+ - `bdshare.BDShareError` (the publicly exported/documented exception) was a **different class** from `bdshare.util.helper.BDShareError`, the one every scraping function actually raises — `except bdshare.BDShareError` silently never caught real errors. `bdshare/__init__.py` now re-exports the same class instead of redefining it
730
+ - `demo/app.py`: `convert_to_native_types()` called `pd.isna(obj)` before checking whether `obj` was a list/dict, crashing with `ValueError: The truth value of an array... is ambiguous` on any chart request; container types are now checked first
731
+ - `demo/app.py`: `get_available_tickers()` checked for a `trading_code` column that doesn't exist on `get_current_trade_data()`'s output (`symbol` does), so it always silently fell back to the hardcoded stock list
732
+ - `demo/Dockerfile`: base image `python:3.11-slim-buster` is EOL (Debian Buster archived), so `apt-get update` would fail on a fresh build; switched to `python:3.11-slim`
733
+
575
734
  ## [1.2.2] - 2026-07-22
576
735
 
577
736
  ### Added
@@ -14,6 +14,7 @@
14
14
 
15
15
  - [Installation](#installation)
16
16
  - [Quick Start](#quick-start)
17
+ - [Demo App](#demo-app)
17
18
  - [Core Concepts](#core-concepts)
18
19
  - [Usage Guide](#usage-guide)
19
20
  - [Live Trading Data](#live-trading-data)
@@ -25,6 +26,10 @@
25
26
  - [Error Handling](#error-handling)
26
27
  - [API Reference](#api-reference)
27
28
  - [Examples](#examples)
29
+ - [Advanced Features](#advanced-features)
30
+ - [Technical Indicators](#technical-indicators)
31
+ - [Portfolio Tracking](#portfolio-tracking)
32
+ - [Real-Time Streaming (WebSocket)](#real-time-streaming-websocket)
28
33
  - [Using bdshare with AI Agents (MCP Server)](#using-bdshare-with-ai-agents-mcp-server)
29
34
  - [Contributing](#contributing)
30
35
  - [Roadmap](#roadmap)
@@ -76,6 +81,29 @@ with BDShare() as bd:
76
81
 
77
82
  ---
78
83
 
84
+ ## Demo App
85
+
86
+ Three full-feature demos live in [`demo/`](demo/) — live prices, historical charts with
87
+ technical indicators, market movers, news, a portfolio tracker, live tick polling, and
88
+ an interactive AI-agent (MCP) tool explorer:
89
+
90
+ ```bash
91
+ cd demo
92
+ docker compose up --build streamlit # pure Python — open http://localhost:8501
93
+ docker compose up --build node node-api mcp stream # cross-language via REST/MCP/WebSocket — open http://localhost:3000
94
+ docker compose up --build app # Flask/Plotly candlestick-only demo — open http://localhost:9999
95
+ ```
96
+
97
+ The Node.js demo is the interesting one if you're integrating bdshare from **outside
98
+ Python**: it's an Express UI that never imports bdshare — it talks to a FastAPI backend
99
+ over REST, to `bdshare-mcp` over HTTP using the official MCP SDK, and to
100
+ `bdshare-stream` over WebSocket, showing three different ways another program can
101
+ consume this library.
102
+
103
+ See [`demo/README.md`](demo/README.md) for local (non-Docker) setup for each.
104
+
105
+ ---
106
+
79
107
  ## Core Concepts
80
108
 
81
109
  | Concept | Details |
@@ -424,6 +452,100 @@ print(big_movers[['symbol', 'close', 'change']])
424
452
 
425
453
  ---
426
454
 
455
+ ## Advanced Features
456
+
457
+ ### Technical Indicators
458
+
459
+ Wraps the [`ta`](https://github.com/bukosabino/ta) library to add indicator columns
460
+ directly onto bdshare's OHLCV DataFrames.
461
+
462
+ ```bash
463
+ pip install "bdshare[ta]"
464
+ ```
465
+
466
+ ```python
467
+ from bdshare import get_basic_historical_data
468
+ from bdshare.indicators import add_indicators, add_rsi
469
+
470
+ df = get_basic_historical_data('2024-01-01', '2024-06-30', 'GP')
471
+
472
+ # Add everything (sma_20, ema_20, rsi_14, macd/macd_signal/macd_diff, bb_high/bb_mid/bb_low)
473
+ df = add_indicators(df)
474
+
475
+ # Or just one, with custom parameters
476
+ df = add_rsi(df, window=21)
477
+ ```
478
+
479
+ Also available: `add_sma()`, `add_ema()`, `add_macd()`, `add_bollinger_bands()`.
480
+ Each returns a new DataFrame (the input is never mutated).
481
+
482
+ ### Portfolio Tracking
483
+
484
+ `Portfolio` tracks cost basis and values holdings against live prices — no extra
485
+ dependency required (pure pandas), and `valuation()` makes exactly **one** live call
486
+ for all instruments regardless of how many positions you hold.
487
+
488
+ ```python
489
+ from bdshare.portfolio import Portfolio
490
+
491
+ pf = Portfolio()
492
+ pf.add_position('GP', quantity=100, avg_cost=450.50)
493
+ pf.add_position('ACI', quantity=50, avg_cost=225.75)
494
+
495
+ print(pf.holdings().to_string()) # cost basis only, no network call
496
+ print(pf.valuation().to_string()) # + ltp, market_value, pnl, pnl_pct per position
497
+ print(pf.summary()) # {'positions': 2, 'total_cost': ..., 'total_pnl': ...}
498
+ ```
499
+
500
+ Adding to an existing position blends the cost basis like a real buy; a negative
501
+ `quantity` reduces it, and netting to zero drops the position. Unknown/delisted
502
+ symbols get `None` valuation fields instead of raising, so one bad symbol doesn't
503
+ block valuing the rest.
504
+
505
+ ### Real-Time Streaming (WebSocket)
506
+
507
+ DSE has no public push/streaming API — this polls `get_current_trade_data()` on an
508
+ interval and broadcasts **changed** rows over WebSocket, so another program can
509
+ subscribe instead of polling bdshare itself.
510
+
511
+ ```bash
512
+ pip install "bdshare[stream]"
513
+ ```
514
+
515
+ Run the bundled server:
516
+
517
+ ```bash
518
+ bdshare-stream --symbols GP,ACI --interval 5
519
+ ```
520
+
521
+ Any other program connects as a plain WebSocket client:
522
+
523
+ ```python
524
+ import asyncio, json, websockets
525
+
526
+ async def main():
527
+ async with websockets.connect('ws://localhost:8765') as ws:
528
+ async for message in ws:
529
+ print(json.loads(message)) # {"type": "ticks", "data": [...]}
530
+
531
+ asyncio.run(main())
532
+ ```
533
+
534
+ Or skip the server and use the polling generator directly inside your own asyncio
535
+ program:
536
+
537
+ ```python
538
+ from bdshare.stream import stream_ticks
539
+
540
+ async def main():
541
+ async for changed in stream_ticks(symbols=['GP', 'ACI'], interval=5.0):
542
+ print(changed)
543
+
544
+ asyncio.run(main())
545
+ ```
546
+
547
+ ---
548
+
427
549
  ## Using bdshare with AI Agents (MCP Server)
428
550
 
429
551
  bdshare ships an [MCP](https://modelcontextprotocol.io/) server so AI agents running in
@@ -469,6 +591,23 @@ claude mcp add bdshare -- bdshare-mcp
469
591
  Any other MCP client is configured the same way — point it at the `bdshare-mcp` command
470
592
  (or `python -m bdshare.mcp_server`), stdio transport.
471
593
 
594
+ ### Running it over the network instead of stdio
595
+
596
+ Stdio only works when the client can spawn the server as a local child process (Claude
597
+ Desktop/Code). For a program in **another container, another machine, or another
598
+ language entirely** — nothing that speaks Python — run it over HTTP instead:
599
+
600
+ ```bash
601
+ bdshare-mcp --transport streamable-http --host 0.0.0.0 --port 8000
602
+ ```
603
+
604
+ Any MCP client library can connect to `http://host:8000/mcp`; see
605
+ [`demo/node/`](demo/node/) for a full example using the official
606
+ `@modelcontextprotocol/sdk` from Node.js. Binding beyond `127.0.0.1`/`localhost`
607
+ automatically disables DNS-rebinding protection, since every legitimate client then
608
+ arrives with a non-localhost `Host` header — only do this on a trusted network (an
609
+ internal Docker network, not the public internet).
610
+
472
611
  ### What the agent gets
473
612
 
474
613
  15 tools covering the same data this README documents — `market_status`,
@@ -511,11 +650,10 @@ Please open an issue before submitting a pull request for significant changes. S
511
650
 
512
651
  ## Roadmap
513
652
 
514
- - [ ] Chittagong Stock Exchange (CSE) support
515
- - [ ] WebSocket streaming for real-time ticks
516
- - [ ] Built-in technical indicators (`ta` integration)
517
- - [ ] Portfolio management helpers
518
- - [ ] Docker demo examples
653
+ - [x] WebSocket streaming for real-time ticks (polling-based — see [Real-Time Streaming](#real-time-streaming-websocket))
654
+ - [x] Built-in technical indicators (`ta` integration)
655
+ - [x] Portfolio management helpers
656
+ - [x] Docker demo examples
519
657
  - [x] Shared session with exponential back-off
520
658
  - [x] `lxml`-based fast parsing
521
659
  - [x] `BDShareError` for clean error handling
@@ -52,7 +52,7 @@ from bdshare.util import (
52
52
  configure_proxy,
53
53
  )
54
54
  from bdshare.util.cache import _TTLCache
55
- from bdshare.util.helper import deprecated
55
+ from bdshare.util.helper import deprecated, BDShareError
56
56
 
57
57
  # ---------------------------------------------------------------------------
58
58
  # Type aliases
@@ -87,10 +87,6 @@ def _validate_date_range(start_date: str, end_date: str) -> None:
87
87
  raise ValueError("Date range cannot exceed 5 years.")
88
88
 
89
89
 
90
- class BDShareError(Exception):
91
- """Top-level client exception."""
92
-
93
-
94
90
  # ---------------------------------------------------------------------------
95
91
  # Rate limiter
96
92
  # ---------------------------------------------------------------------------
@@ -1,2 +1,2 @@
1
- version_info = (1, 2, 2)
1
+ version_info = (1, 2, 3)
2
2
  __version__ = ".".join(map(str, version_info))
@@ -0,0 +1,100 @@
1
+ """
2
+ bdshare.indicators
3
+ ~~~~~~~~~~~~~~~~~~~
4
+ Optional technical-indicator helpers built on the `ta` library, for use with
5
+ the OHLCV DataFrames returned by get_basic_historical_data() and friends.
6
+
7
+ Requires the optional 'ta' extra:
8
+ pip install bdshare[ta]
9
+ """
10
+ from typing import Iterable, Optional
11
+
12
+ import pandas as pd
13
+
14
+ try:
15
+ from ta.momentum import RSIIndicator
16
+ from ta.trend import EMAIndicator, MACD, SMAIndicator
17
+ from ta.volatility import BollingerBands
18
+ except ImportError as exc:
19
+ raise ImportError(
20
+ "Technical indicators require the 'ta' package. "
21
+ "Install it with: pip install bdshare[ta]"
22
+ ) from exc
23
+
24
+
25
+ def add_sma(df: pd.DataFrame, window: int = 20, column: str = "close") -> pd.DataFrame:
26
+ """Add a Simple Moving Average column named ``sma_{window}``."""
27
+ out = df.copy()
28
+ out[f"sma_{window}"] = SMAIndicator(out[column], window=window).sma_indicator()
29
+ return out
30
+
31
+
32
+ def add_ema(df: pd.DataFrame, window: int = 20, column: str = "close") -> pd.DataFrame:
33
+ """Add an Exponential Moving Average column named ``ema_{window}``."""
34
+ out = df.copy()
35
+ out[f"ema_{window}"] = EMAIndicator(out[column], window=window).ema_indicator()
36
+ return out
37
+
38
+
39
+ def add_rsi(df: pd.DataFrame, window: int = 14, column: str = "close") -> pd.DataFrame:
40
+ """Add a Relative Strength Index column named ``rsi_{window}``."""
41
+ out = df.copy()
42
+ out[f"rsi_{window}"] = RSIIndicator(out[column], window=window).rsi()
43
+ return out
44
+
45
+
46
+ def add_macd(
47
+ df: pd.DataFrame,
48
+ window_fast: int = 12,
49
+ window_slow: int = 26,
50
+ window_sign: int = 9,
51
+ column: str = "close",
52
+ ) -> pd.DataFrame:
53
+ """Add ``macd``, ``macd_signal``, and ``macd_diff`` (histogram) columns."""
54
+ out = df.copy()
55
+ macd = MACD(
56
+ out[column], window_fast=window_fast, window_slow=window_slow, window_sign=window_sign
57
+ )
58
+ out["macd"] = macd.macd()
59
+ out["macd_signal"] = macd.macd_signal()
60
+ out["macd_diff"] = macd.macd_diff()
61
+ return out
62
+
63
+
64
+ def add_bollinger_bands(
65
+ df: pd.DataFrame, window: int = 20, window_dev: int = 2, column: str = "close"
66
+ ) -> pd.DataFrame:
67
+ """Add ``bb_high``, ``bb_mid``, and ``bb_low`` Bollinger Band columns."""
68
+ out = df.copy()
69
+ bb = BollingerBands(out[column], window=window, window_dev=window_dev)
70
+ out["bb_high"] = bb.bollinger_hband()
71
+ out["bb_mid"] = bb.bollinger_mavg()
72
+ out["bb_low"] = bb.bollinger_lband()
73
+ return out
74
+
75
+
76
+ _INDICATORS = {
77
+ "sma": add_sma,
78
+ "ema": add_ema,
79
+ "rsi": add_rsi,
80
+ "macd": add_macd,
81
+ "bbands": add_bollinger_bands,
82
+ }
83
+
84
+
85
+ def add_indicators(df: pd.DataFrame, indicators: Optional[Iterable[str]] = None) -> pd.DataFrame:
86
+ """Add several indicators at once, each with its default parameters.
87
+
88
+ For custom parameters (e.g. a different SMA window), call the individual
89
+ ``add_*`` function directly instead.
90
+
91
+ :param indicators: Names from 'sma', 'ema', 'rsi', 'macd', 'bbands'.
92
+ Defaults to all of them.
93
+ """
94
+ names = list(indicators) if indicators is not None else list(_INDICATORS)
95
+ out = df
96
+ for name in names:
97
+ if name not in _INDICATORS:
98
+ raise ValueError(f"Unknown indicator '{name}'. Choose from: {list(_INDICATORS)}")
99
+ out = _INDICATORS[name](out)
100
+ return out
@@ -11,6 +11,11 @@ Run directly:
11
11
  Or via the installed console script:
12
12
  bdshare-mcp
13
13
 
14
+ Defaults to stdio transport (what Claude Desktop/Code expect). For a
15
+ network-reachable server — e.g. so a non-Python program in another
16
+ container can connect — pass --transport streamable-http --host 0.0.0.0:
17
+ bdshare-mcp --transport streamable-http --host 0.0.0.0 --port 8000
18
+
14
19
  Requires the optional 'mcp' extra:
15
20
  pip install bdshare[mcp]
16
21
  """
@@ -190,7 +195,27 @@ def agm_news() -> list:
190
195
 
191
196
 
192
197
  def main() -> None:
193
- mcp.run()
198
+ import argparse
199
+
200
+ parser = argparse.ArgumentParser(description="bdshare MCP server.")
201
+ parser.add_argument(
202
+ "--transport", choices=["stdio", "sse", "streamable-http"], default="stdio",
203
+ help="MCP transport (default: stdio, for desktop/CLI agent clients like Claude Desktop/Code)",
204
+ )
205
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind for sse/streamable-http")
206
+ parser.add_argument("--port", type=int, default=8000, help="Port to bind for sse/streamable-http")
207
+ args = parser.parse_args()
208
+
209
+ if args.transport != "stdio":
210
+ mcp.settings.host = args.host
211
+ mcp.settings.port = args.port
212
+ if args.host not in ("127.0.0.1", "localhost"):
213
+ # Binding beyond loopback (e.g. inside a container network) means every
214
+ # legitimate client arrives with a non-localhost Host header — DNS-rebinding
215
+ # protection would otherwise reject all of them.
216
+ mcp.settings.transport_security.enable_dns_rebinding_protection = False
217
+
218
+ mcp.run(transport=args.transport)
194
219
 
195
220
 
196
221
  if __name__ == "__main__":