Metadata-Version: 2.4
Name: bdshare
Version: 1.2.7
Summary: A utility for crawling historical and Real-time Quotes of DSE(Dhaka Stock Exchange)
Author: Raisul Islam
Author-email: raisul.me@gmail.com
License: MIT
Keywords: Crawling,DSE,Financial Data
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests
Requires-Dist: certifi
Requires-Dist: beautifulsoup4
Requires-Dist: html5lib
Requires-Dist: pandas
Requires-Dist: lxml
Provides-Extra: polars
Requires-Dist: polars; extra == "polars"
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.0.0; extra == "mcp"
Provides-Extra: ta
Requires-Dist: ta; extra == "ta"
Provides-Extra: stream
Requires-Dist: websockets>=12.0; extra == "stream"
Dynamic: license-file

# bdshare

![StyleCI](https://github.styleci.io/repos/253465924/shield?branch=main)
[![Documentation Status](https://readthedocs.org/projects/bdshare/badge/?version=latest)](https://bdshare.readthedocs.io/en/latest/?badge=latest)
![PyPI](https://img.shields.io/pypi/v/bdshare)
![Python](https://img.shields.io/pypi/pyversions/bdshare)
![License](https://img.shields.io/github/license/rochi88/bdshare)

**bdshare** is a Python library for fetching live and historical market data from the Dhaka Stock Exchange (DSE). It handles scraping, retries, caching, and rate limiting so you can focus on your analysis.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Demo App](#demo-app)
- [Core Concepts](#core-concepts)
- [Usage Guide](#usage-guide)
  - [Live Trading Data](#live-trading-data)
  - [Historical Data](#historical-data)
  - [Market & Index Data](#market--index-data)
  - [News & Announcements](#news--announcements)
  - [Saving to CSV](#saving-to-csv)
- [OOP Client (BDShare)](#oop-client-bdshare)
- [Error Handling](#error-handling)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Advanced Features](#advanced-features)
  - [Technical Indicators](#technical-indicators)
  - [Portfolio Tracking](#portfolio-tracking)
  - [Real-Time Streaming (WebSocket)](#real-time-streaming-websocket)
- [Using bdshare with AI Agents (MCP Server)](#using-bdshare-with-ai-agents-mcp-server)
- [Contributing](#contributing)
- [Roadmap](#roadmap)
- [Disclaimer](#disclaimer)

---

## Installation

**Requirements:** Python 3.7+

```bash
pip install bdshare
```

Install from source (latest development version):

```bash
pip install -U git+https://github.com/rochi88/bdshare.git
```

Dependencies installed automatically: `pandas`, `requests`, `beautifulsoup4`, `lxml`

---

## Quick Start

```python
from bdshare import get_current_trade_data, get_historical_data

# Live prices for all instruments
df = get_current_trade_data()
print(df.head())

# Historical data for a specific symbol
df = get_historical_data('2024-01-01', '2024-01-31', 'GP')
print(df.head())
```

Or use the object-oriented client:

```python
from bdshare import BDShare

with BDShare() as bd:
    print(bd.get_market_summary())
    print(bd.get_current_trades('ACI'))
```

---

## Demo App

Three full-feature demos live in [`demo/`](demo/) — live prices, historical charts with
technical indicators, market movers, news, a portfolio tracker, live tick polling, and
an interactive AI-agent (MCP) tool explorer:

```bash
cd demo
docker compose up --build streamlit   # pure Python — open http://localhost:8501
docker compose up --build node node-api mcp stream  # cross-language via REST/MCP/WebSocket — open http://localhost:3000
docker compose up --build app         # Flask/Plotly candlestick-only demo — open http://localhost:9999
```

The Node.js demo is the interesting one if you're integrating bdshare from **outside
Python**: it's an Express UI that never imports bdshare — it talks to a FastAPI backend
over REST, to `bdshare-mcp` over HTTP using the official MCP SDK, and to
`bdshare-stream` over WebSocket, showing three different ways another program can
consume this library.

See [`demo/README.md`](demo/README.md) for local (non-Docker) setup for each.

---

## Core Concepts

| Concept | Details |
|---|---|
| **Retries** | All network calls retry up to 3 times with exponential back-off |
| **Fallback URL** | Every request has a primary and an alternate DSE endpoint |
| **Two DSE sites** | Data comes from the current dsebd.org JSON API first; if that fails, bdshare falls back to the legacy site (old.dsebd.org) with the same columns. Set `BDSHARE_SOURCE=legacy` or `BDSHARE_SOURCE=new` to use only one site. `get_agm_news()` and `get_company_info()` work only on the legacy site |
| **Caching** | The `BDShare` client caches responses automatically (configurable TTL) |
| **Rate limiting** | Built-in sliding-window limiter (5 calls/second) prevents being blocked |
| **Errors** | All failures raise `BDShareError` — never silent |

---

## Usage Guide

### Live Trading Data

```python
from bdshare import get_current_trade_data, get_dsex_data, get_current_trading_code

# All instruments — returns columns: symbol, ltp, high, low, close, ycp, change, trade, value, volume
df = get_current_trade_data()

# Single instrument (case-insensitive)
df = get_current_trade_data('GP')

# DSEX index entries
df = get_dsex_data()

# Just the list of tradeable symbols
codes = get_current_trading_code()
print(codes['symbol'].tolist())
```

### Historical Data

```python
from bdshare import get_historical_data, get_basic_historical_data, get_close_price_data
import datetime as dt

start = '2024-01-01'
end   = '2024-03-31'

# Full historical data (ltp, open, high, low, close, volume, trade, value…)
# Indexed by date, sorted newest-first
df = get_historical_data(start, end, 'ACI')

# Simplified OHLCV — sorted oldest-first, ready for TA libraries
df = get_basic_historical_data(start, end, 'ACI')

# Set date as index explicitly
df = get_basic_historical_data(start, end, 'ACI', index='date')

# Rolling 2-year window
end   = dt.date.today()
start = end - dt.timedelta(days=2 * 365)
df    = get_basic_historical_data(str(start), str(end), 'GP')

# Close prices only
df = get_close_price_data(start, end, 'ACI')
```

> **Column order note:** `get_basic_historical_data` intentionally returns OHLCV in standard order
> (`open`, `high`, `low`, `close`, `volume`) to be compatible with libraries like `ta`, `pandas-ta`, and `backtrader`.
>
> **Deprecated aliases:** `get_hist_data()` and `get_basic_hist_data()` still work but emit a
> `DeprecationWarning` — migrate to `get_historical_data()` / `get_basic_historical_data()`.

### Market & Index Data

```python
from bdshare import (
    get_market_status,
    get_market_info,
    get_market_info_more_data,
    get_market_depth_data,
    get_latest_pe,
    get_top_ten_gainers_losers,
    get_top_twenty_shares,
    get_company_info,
)

# Current market status: Open, Closed, Holiday, etc.
status = get_market_status()

# Last 30 days of market summary (DSEX, DSES, DS30, DGEN, volumes, market cap)
df = get_market_info()

# Historical market summary between two dates
df = get_market_info_more_data('2024-01-01', '2024-03-31')

# Order book (buy/sell depth) for a symbol
df = get_market_depth_data('ACI')

# P/E ratios for all listed companies
df = get_latest_pe()

# Top 10 gainers and losers (adjust limit as needed)
df = get_top_ten_gainers_losers(limit=10)

# Top 20 shares by traded volume (adjust limit as needed)
df = get_top_twenty_shares(limit=20)

# Detailed company profile
tables = get_company_info('GP')
```

### News & Announcements

```python
from bdshare import get_news, get_agm_news, get_all_news

# Unified dispatcher — news_type: 'all' | 'agm' | 'corporate' | 'psn'
df = get_news(news_type='all')
df = get_news(news_type='agm')
df = get_news(news_type='corporate', code='GP')
df = get_news(news_type='psn', code='ACI')   # price-sensitive news

# Direct function calls
df = get_agm_news()                          # AGM / dividend declarations
df = get_all_news(code='BEXIMCO')            # All news for one symbol
df = get_all_news('2024-01-01', '2024-03-31', 'GP')  # Filtered by date + symbol
```

### Saving to CSV

```python
from bdshare import get_basic_historical_data, Store
import datetime as dt

end   = dt.date.today()
start = end - dt.timedelta(days=365)

df = get_basic_historical_data(str(start), str(end), 'GP')
Store(df).save()   # saves to current directory as a CSV
```

---

## OOP Client (BDShare)

The `BDShare` class wraps all functions with automatic caching and rate limiting.

```python
from bdshare import BDShare

bd = BDShare(cache_enabled=True)   # cache_enabled=True is the default
```

### Context manager (auto-cleans cache and session)

```python
with BDShare() as bd:
    data = bd.get_current_trades('GP')
```

### Market methods

```python
bd.get_market_summary()                    # DSEX/DSES/DS30 indices + stats  (1-min TTL)
bd.get_company_profile('ACI')             # Company profile                  (1-hr TTL)
bd.get_latest_pe_ratios()                 # All P/E ratios                   (1-hr TTL)
bd.get_top_movers(limit=10)               # Top gainers/losers               (5-min TTL)
```

`get_market_status()` and `get_top_twenty_shares()` don't have `BDShare` wrapper methods yet —
call the module-level functions directly (see [API Reference](#api-reference)).

### Trading methods

```python
bd.get_current_trades()                   # All live prices                  (30-sec TTL)
bd.get_current_trades('GP')               # Single symbol
bd.get_dsex_index()                       # DSEX index entries               (1-min TTL)
bd.get_trading_codes()                    # All tradeable symbols            (24-hr TTL)
bd.get_historical_data('GP', '2024-01-01', '2024-03-31')    # OHLCV history
```

### News methods

```python
bd.get_news(news_type='all')              # All news                         (5-min TTL)
bd.get_news(news_type='corporate', code='GP')
bd.get_news(news_type='psn')             # Price-sensitive news
```

### Utility methods

```python
bd.clear_cache()                          # Flush all cached data
bd.configure(proxy_url='http://proxy:8080')
print(bd.version)                         # Package version string
```

---

## Error Handling

All failures raise `BDShareError`. Never catch bare `Exception` — you'll miss bugs.

```python
from bdshare import BDShare, BDShareError

bd = BDShare()

try:
    df = bd.get_historical_data('INVALID', '2024-01-01', '2024-01-31')
except BDShareError as e:
    print(f"DSE error: {e}")
    # safe fallback logic here
```

Common causes of `BDShareError`:
- Symbol not found in the response table
- DSE site returned a non-200 status after all retries
- Table structure changed on the DSE page (report as a bug)
- Network timeout

---

## API Reference

### Trading Functions

| Function | Parameters | Returns | Description |
|---|---|---|---|
| `get_current_trade_data(symbol?)` | `symbol: str` | DataFrame | Live prices (all or one symbol) |
| `get_dsex_data(symbol?)` | `symbol: str` | DataFrame | DSEX index entries |
| `get_current_trading_code()` | — | DataFrame | All tradeable symbols |
| `get_historical_data(start, end, code?)` | `str, str, str` | DataFrame | Full historical OHLCV |
| `get_basic_historical_data(start, end, code?, index?)` | `str, str, str, str` | DataFrame | Simplified OHLCV (TA-ready) |
| `get_close_price_data(start, end, code?)` | `str, str, str` | DataFrame | Close + prior close |
| `get_last_trade_price_data()` | — | DataFrame | Last trade from DSE text file |

Deprecated aliases (removed in 2.0.0): `get_hist_data()` → `get_historical_data()`,
`get_basic_hist_data()` → `get_basic_historical_data()`.

### Market Functions

| Function | Parameters | Returns | Description |
|---|---|---|---|
| `get_market_status()` | — | str | Current market status (Open, Closed, Holiday, etc.) |
| `get_market_info()` | — | DataFrame | 30-day market summary |
| `get_market_info_more_data(start, end)` | `str, str` | DataFrame | Historical market summary |
| `get_market_depth_data(symbol)` | `str` | DataFrame | Order book (buy/sell depth) |
| `get_latest_pe()` | — | DataFrame | P/E ratios for all companies |
| `get_company_info(symbol)` | `str` | list[DataFrame] | Detailed company tables |
| `get_top_ten_gainers_losers(limit?)` | `int` (default 10) | DataFrame | Top movers by price change |
| `get_top_twenty_shares(limit?)` | `int` (default 20) | DataFrame | Top shares by traded volume |

### News Functions

| Function | Parameters | Returns | Description |
|---|---|---|---|
| `get_news(news_type?, code?)` | `str, str` | DataFrame | Unified news dispatcher |
| `get_agm_news()` | — | DataFrame | AGM / dividend declarations |
| `get_all_news(start?, end?, code?)` | `str, str, str` | DataFrame | All DSE news |
| `get_corporate_announcements(code?)` | `str` | DataFrame | Corporate actions |
| `get_price_sensitive_news(code?)` | `str` | DataFrame | Price-sensitive news |

### `get_news` `news_type` values

| Value | Equivalent direct function |
|---|---|
| `'all'` | `get_all_news()` |
| `'agm'` | `get_agm_news()` |
| `'corporate'` | `get_corporate_announcements()` |
| `'psn'` | `get_price_sensitive_news()` |

---

## Examples

### Stock performance summary

```python
import datetime as dt
from bdshare import BDShare, BDShareError

def summarize(symbol: str, days: int = 30) -> dict:
    end   = dt.date.today()
    start = end - dt.timedelta(days=days)

    with BDShare() as bd:
        try:
            df = bd.get_historical_data(symbol, str(start), str(end))
        except BDShareError as e:
            print(f"Could not fetch data: {e}")
            return {}

    return {
        'symbol':       symbol,
        'current':      df['close'].iloc[0],
        'period_high':  df['high'].max(),
        'period_low':   df['low'].min(),
        'avg_volume':   df['volume'].mean(),
        'change_pct':   (df['close'].iloc[0] - df['close'].iloc[-1])
                        / df['close'].iloc[-1] * 100,
    }

result = summarize('GP', days=30)
print(f"{result['symbol']}: {result['change_pct']:.2f}% over 30 days")
```

### Simple portfolio tracker

```python
from bdshare import BDShare, BDShareError

PORTFOLIO = {
    'GP':      {'qty': 100, 'cost': 450.50},
    'ACI':     {'qty':  50, 'cost': 225.75},
    'BEXIMCO': {'qty': 200, 'cost': 125.25},
}

with BDShare() as bd:
    total_cost = total_value = 0

    for symbol, pos in PORTFOLIO.items():
        try:
            row = bd.get_current_trades(symbol).iloc[0]
            price        = row['ltp']
            market_value = pos['qty'] * price
            cost         = pos['qty'] * pos['cost']
            pnl          = market_value - cost

            print(f"{symbol:10s}  price={price:8.2f}  P&L={pnl:+10.2f}")
            total_cost  += cost
            total_value += market_value

        except BDShareError as e:
            print(f"{symbol}: fetch error — {e}")

    print(f"\nPortfolio P&L: {total_value - total_cost:+.2f} "
          f"({(total_value/total_cost - 1)*100:+.2f}%)")
```

### Fetch and screen top gainers above 5 %

```python
from bdshare import get_top_ten_gainers_losers

df = get_top_ten_gainers_losers(limit=20)
big_movers = df[df['change'] > 5]
print(big_movers[['symbol', 'close', 'change']])
```

---

## Advanced Features

### Technical Indicators

Wraps the [`ta`](https://github.com/bukosabino/ta) library to add indicator columns
directly onto bdshare's OHLCV DataFrames.

```bash
pip install "bdshare[ta]"
```

```python
from bdshare import get_basic_historical_data
from bdshare.indicators import add_indicators, add_rsi

df = get_basic_historical_data('2024-01-01', '2024-06-30', 'GP')

# Add everything (sma_20, ema_20, rsi_14, macd/macd_signal/macd_diff, bb_high/bb_mid/bb_low)
df = add_indicators(df)

# Or just one, with custom parameters
df = add_rsi(df, window=21)
```

Also available: `add_sma()`, `add_ema()`, `add_macd()`, `add_bollinger_bands()`.
Each returns a new DataFrame (the input is never mutated).

### Portfolio Tracking

`Portfolio` tracks cost basis and values holdings against live prices — no extra
dependency required (pure pandas), and `valuation()` makes exactly **one** live call
for all instruments regardless of how many positions you hold.

```python
from bdshare.portfolio import Portfolio

pf = Portfolio()
pf.add_position('GP', quantity=100, avg_cost=450.50)
pf.add_position('ACI', quantity=50, avg_cost=225.75)

print(pf.holdings().to_string())    # cost basis only, no network call
print(pf.valuation().to_string())   # + ltp, market_value, pnl, pnl_pct per position
print(pf.summary())                 # {'positions': 2, 'total_cost': ..., 'total_pnl': ...}
```

Adding to an existing position blends the cost basis like a real buy; a negative
`quantity` reduces it, and netting to zero drops the position. Unknown/delisted
symbols get `None` valuation fields instead of raising, so one bad symbol doesn't
block valuing the rest.

### Real-Time Streaming (WebSocket)

DSE has no public push/streaming API — this polls `get_current_trade_data()` on an
interval and broadcasts **changed** rows over WebSocket, so another program can
subscribe instead of polling bdshare itself.

```bash
pip install "bdshare[stream]"
```

Run the bundled server:

```bash
bdshare-stream --symbols GP,ACI --interval 5
```

Any other program connects as a plain WebSocket client:

```python
import asyncio, json, websockets

async def main():
    async with websockets.connect('ws://localhost:8765') as ws:
        async for message in ws:
            print(json.loads(message))   # {"type": "ticks", "data": [...]}

asyncio.run(main())
```

Or skip the server and use the polling generator directly inside your own asyncio
program:

```python
from bdshare.stream import stream_ticks

async def main():
    async for changed in stream_ticks(symbols=['GP', 'ACI'], interval=5.0):
        print(changed)

asyncio.run(main())
```

---

## Using bdshare with AI Agents (MCP Server)

bdshare ships an [MCP](https://modelcontextprotocol.io/) server so AI agents running in
**another program** — Claude Desktop, Claude Code, or any other MCP-compatible client —
can call live DSE data as tools, without you writing any glue code.

### Install

```bash
pip install "bdshare[mcp]"
```

### Run

```bash
bdshare-mcp
# or
python -m bdshare.mcp_server
```

By default it speaks MCP over stdio, which is what desktop/CLI agent clients expect.

### Connect it to a client

**Claude Code:**

```bash
claude mcp add bdshare -- bdshare-mcp
```

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "bdshare": {
      "command": "bdshare-mcp"
    }
  }
}
```

Any other MCP client is configured the same way — point it at the `bdshare-mcp` command
(or `python -m bdshare.mcp_server`), stdio transport.

### Running it over the network instead of stdio

Stdio only works when the client can spawn the server as a local child process (Claude
Desktop/Code). For a program in **another container, another machine, or another
language entirely** — nothing that speaks Python — run it over HTTP instead:

```bash
bdshare-mcp --transport streamable-http --host 0.0.0.0 --port 8000
```

Any MCP client library can connect to `http://host:8000/mcp`; see
[`demo/node/`](demo/node/) for a full example using the official
`@modelcontextprotocol/sdk` from Node.js. Binding beyond `127.0.0.1`/`localhost`
automatically disables DNS-rebinding protection, since every legitimate client then
arrives with a non-localhost `Host` header — only do this on a trusted network (an
internal Docker network, not the public internet).

### What the agent gets

15 tools covering the same data this README documents — `market_status`,
`market_summary`, `market_summary_range`, `market_depth`, `latest_pe_ratios`,
`top_ten_gainers_losers`, `top_twenty_shares`, `company_info`, `current_trades`,
`dsex_index`, `trading_codes`, `historical_data`, `basic_historical_data`, `news`,
`agm_news`. Each tool returns JSON — `DataFrame`s are converted to lists of row
records — and a `BDShareError` (bad symbol, DSE outage, parse failure) surfaces as a
clean tool error message instead of a raw traceback.

### Notes for agent use

- **No caching in the MCP server itself.** Every tool call scrapes dsebd.org live. If
  your agent calls the same tool repeatedly in one turn (e.g. checking a price several
  times), consider fronting it with the `BDShare` client's caching in a custom
  wrapper — the bundled server intentionally stays stateless and simple.
- **Rate limits are the agent's responsibility.** The `BDShare` OOP client has a
  built-in 5 calls/second limiter; the MCP tools call the plain module-level functions,
  which don't. Avoid tight loops of tool calls.
- Source: [`bdshare/mcp_server.py`](bdshare/mcp_server.py) — a plain
  [`FastMCP`](https://github.com/modelcontextprotocol/python-sdk) server, easy to fork
  if you want a different tool surface.

---

## Contributing

Contributions are welcome! To get started:

```bash
git clone https://github.com/rochi88/bdshare.git
cd bdshare
pip install -e ".[dev]"
pytest
```

Please open an issue before submitting a pull request for significant changes. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide.

---

## Roadmap

- [x] WebSocket streaming for real-time ticks (polling-based — see [Real-Time Streaming](#real-time-streaming-websocket))
- [x] Built-in technical indicators (`ta` integration)
- [x] Portfolio management helpers
- [x] Docker demo examples
- [x] Shared session with exponential back-off
- [x] `lxml`-based fast parsing
- [x] `BDShareError` for clean error handling
- [x] Unified `get_news()` dispatcher
- [x] Rate limiter and response caching
- [x] MCP server for AI agent integration

---

## Support

- **Docs:** [bdshare.readthedocs.io](https://bdshare.readthedocs.io/)
- **Bugs / Features:** [GitHub Issues](https://github.com/rochi88/bdshare/issues)
- **Discussion:** [GitHub Discussions](https://github.com/rochi88/bdshare/discussions)

---

## License

MIT — see [LICENSE](LICENSE) for details.

## Disclaimer

bdshare is intended for educational and research use. Always respect DSE's terms of service. The authors are not responsible for financial decisions made using this library.
# Change log

All notable changes to **bdshare** are documented here.

Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---
## [1.2.7] - 2026-09-25

### Added
- Support for the redesigned **dsebd.org** site. The new site no longer serves the legacy `.php` pages (they return 404) and loads its data from a JSON API instead. Every data function now tries the dsebd.org API first and falls back to the legacy site (old.dsebd.org) if that fails, returning the same columns either way: current trade data, trading codes, DSEX data, historical/basic historical/close price data, market status, market info (and extended market info), latest P/E, market depth, top gainers, top twenty shares, and news (all, corporate, price-sensitive).
- `BDSHARE_SOURCE` environment variable (or `bdshare.util.vars.DSE_SOURCE` at runtime) to choose the site: `auto` (default: dsebd.org, then legacy), `new`, or `legacy`.

### Changed
- `DSE_URL` / `DSE_ALT_URL` in `bdshare/util/vars.py` now hold the current site (`https://dsebd.org/`, `https://dse.com.bd/`). The legacy hosts moved to the new `DSE_LEGACY_URL` / `DSE_LEGACY_ALT_URL`. Code that read `DSE_URL` expecting the legacy site should switch to `DSE_LEGACY_URL`.
- `get_agm_news()` and `get_company_info()` work only on the legacy site, because dsebd.org has no equivalent data.
- Fetching all instruments' history from dsebd.org is slower than from the legacy site. The API returns at most 500 rows per request, and one day has about 650 instruments, so bdshare fetches the instruments past the cutoff one at a time (in parallel).

### Fixed
- `_safe_num()` removed the minus sign from negative numbers (`"-1.20"` → `1.2`), so falling prices showed up as positive changes, for example in the `change` column of `get_dsex_data()`. Negative values now keep their sign. Cells containing only dashes (`"-"`, `"--"`) still parse as `None`.
- `get_top_twenty_shares()` read the wrong columns: `trade` held the close price and `volume` held the trade count. It now reads the TRADE and VOLUME columns.

## [1.2.6] - 2026-09-25

### Fixed
- Pointed `DSE_URL` and `DSE_ALT_URL` in `bdshare/util/vars.py` at the legacy DSE sites (`https://old.dsebd.org/` and `https://old.dse.com.bd/`). The legacy pages (`latest_share_price_scroll_l.php`, `day_end_archive.php`, etc.) that the scrapers parse are now served from these hosts.

## [1.2.5] - 2026-09-08

### Fixed
- Pinned the optional `mcp` extra to `>=1.0.0,<2` so installs keep using the v1 API expected by `bdshare/mcp_server.py`; this avoids the `mcp` 2.x `FastMCP` → `MCPServer` breakage that caused `bdshare-mcp` to fail immediately after installation.

## [1.2.3] - 2026-07-22

### Added
- `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]`
- `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
- `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`
- `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`
- `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)
- `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
- `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`

### Fixed
- `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
- `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
- `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
- `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`

## [1.2.2] - 2026-07-22

### Added
- Optional `as_polars=True` parameter across all data-fetching functions to return `polars.DataFrame` instead of `pandas.DataFrame` (requires `pip install bdshare[polars]`)
- `get_market_status()` — current market status (Open, Closed, Holiday, etc.)
- `get_top_twenty_shares()` — top twenty shares by traded volume
- MCP ([Model Context Protocol](https://modelcontextprotocol.io/)) server (`bdshare/mcp_server.py`) exposing 15 tools so AI agents (Claude Desktop, Claude Code, etc.) can call live DSE data directly; install with `pip install bdshare[mcp]`, run with `bdshare-mcp`

### Changed
- Renamed `get_top_gainers_losers()` → `get_top_ten_gainers_losers()`; returned columns changed from `symbol, ltp, change` to `symbol, close, high, low, ycp, change`

### Removed
- `get_sector_performance()` (module function and `BDShare.get_sector_performance()`) — no aliased replacement; use `get_top_twenty_shares()` instead

### Fixed
- Bundled the missing Sectigo DV R36 intermediate certificate so requests to dsebd.org verify correctly instead of relying on an incomplete chain
- Corrected `DSE_ALT_URL` fallback domain to `dse.com.bd`
- `get_corporate_announcements()` and `get_price_sensitive_news()` always raised `BDShareError` — the row parser assumed each news item was one `<tr>` with 4 `<td>`s, but DSE renders each field (`Trading Code:`, `News Title:`, `News:`, `Post Date:`) as its own `<th>`/`<td>` row pair
- `as_polars=True` could crash with `unexpected value while building Series of type String; found value of type Float64: NaN` on tables mixing strings and missing values (e.g. `get_company_info()`); NaN now converts to a proper polars null instead
- Corrected `usage.rst`/README documentation that had `get_historical_data()`/`get_basic_historical_data()` mislabeled as the deprecated aliases — `get_hist_data()`/`get_basic_hist_data()` are the deprecated ones

## [1.2.1] - 2026-02-22

### Added
- Added code filter on market index 

## [1.2.0] - 2026-02-21

### Changed
- Updated 

## [1.1.6] - 2026-02-20

### Changed
- Updated `readthedocs` structure

## [1.1.5] - 2026-02-20

### Changed
- Renamed `get_hist_data()` → `get_historical_data()` (improved readability)
- Renamed `get_basic_hist_data()` → `get_basic_historical_data()` (improved readability)
- Renamed `get_market_inf()` → `get_market_info()` (improved readability)
- Renamed `get_market_inf_more_data()` → `get_market_info_more_data()` (improved readability)
- Renamed `get_company_inf()` → `get_company_info()` (improved readability)

### Deprecated
- `get_hist_data()` — still callable but emits `DeprecationWarning`; will be removed in 2.0.0
- `get_basic_hist_data()` — still callable but emits `DeprecationWarning`; will be removed in 2.0.0
- `get_market_inf()` — still callable but emits `DeprecationWarning`; will be removed in 2.0.0
- `get_market_inf_more_data()` — still callable but emits `DeprecationWarning`; will be removed in 2.0.0
- `get_company_inf()` — still callable but emits `DeprecationWarning`; will be removed in 2.0.0

### Added
- `BDShareError` custom exception — all network and scraping failures now raise this instead of silently printing and returning `None`
- Shared `requests.Session` (`_session`) across all modules — reuses TCP connections for significantly faster repeated calls
- Exponential back-off retry logic in `safe_get()` — pauses 0.2 s → 0.4 s → 0.8 s between attempts before raising `BDShareError`
- Fallback URL support in `safe_get()` — primary and alternate DSE endpoints tried within the same retry attempt
- `lxml`-based HTML parsing in `_fetch_table()` with `html.parser` fallback — replaces `html5lib` (~10× faster)
- `_safe_num()` helper — all scraped values now returned as typed numerics (`float`/`int`) instead of raw strings
- `_parse_trade_rows()` and `_filter_symbol()` internal helpers in `trading.py` — eliminate duplicated parsing logic between `get_current_trade_data()` and `get_dsex_data()`
- `get_news()` unified dispatcher — accepts `news_type` of `'all'`, `'agm'`, `'corporate'`, or `'psn'`
- `get_corporate_announcements()` and `get_price_sensitive_news()` — previously missing functions now fully implemented
- Column count guards (`len(cols) < N`) across all table parsers — malformed rows are skipped rather than raising `IndexError`
- Backward-compatibility aliases for all renamed functions with `DeprecationWarning`
- Type hints throughout all public functions
- Comprehensive docstrings with parameter and return documentation

### Fixed
- `get_agm_news()`: corrected field name typo `agmData` → `agmDate`
- `get_agm_news()`: corrected field name typo `vanue` → `venue`
- `get_market_depth_data()`: no longer creates a new `requests.Session` on every retry iteration
- `get_basic_hist_data()`: redundant double `sort_index()` call removed
- `get_hist_data()` and `get_close_price_data()`: no longer silently return `None` on empty results
- `RateLimiter` in `__init__.py`: switched from `time.time()` to `time.monotonic()` for reliable elapsed-time measurement

### Removed
- `html5lib` as the default parser — replaced by `lxml` with `html.parser` fallback
- Silent `print(e)` error handling — all error paths now raise `BDShareError`
- Dead `timeout` parameter from `BDShare.configure()` — it had no effect

## [1.1.4] - 2025-09-16

### Added

- Enhanced error handling and robustness across all functions
- Improved parameter handling for news functions
- Better file path resolution for utility functions
- Comprehensive fallback mechanisms for network issues

### Changed

- Fixed get_all_news() function to support date range parameters as documented
- Enhanced market info functions with better error handling
- Improved Store utility with proper file saving mechanism
- Fixed Tickers utility with correct file path resolution

### Fixed

- All major function issues identified in testing (18/18 functions now working)
- Parameter signature mismatches in news functions
- HTML parsing errors in market data functions
- File saving issues in Store utility
- Missing tickers.json file dependency

## [1.1.2] - 2024-12-31

### Added

- n/a

### Changed

- update tests

### Fixed

- n/a

## [1.1.1] - 2024-12-31

### Added

- n/a

### Changed

- update runner

### Fixed

- n/a

## [1.1.0] - 2024-12-31

### Added

- new function for getting company info

### Changed

- n/a

### Fixed

- n/a

## [1.0.4] - 2024-12-30

### Added

- n/a

### Changed

- changed lint

### Fixed

- fixed typo

## [1.0.3] - 2024-07-29

### Added

- n/a

### Changed

- n/a

### Fixed

- check fix for latest P/E url [#6]

## [1.0.2] - 2024-07-29

### Added

- n/a

### Changed

- n/a

### Fixed

- fixed latest P/E url [#6]

## [1.0.0] - 2024-03-04

### Added

- Updated docs

### Changed

- n/a

## [0.7.2] - 2024-03-04

### Added

- Updated docs

### Changed

- n/a

## [0.7.1] - 2024-03-04

### Added

- n/a

### Changed

- fixed market depth data api

## [0.7.0] - 2024-03-04

### Added

- n/a

### Changed

- n/a

## [0.6.0] - 2024-03-03

### Added

- n/a

### Changed

- n/a

## [0.5.1] - 2024-02-29

### Added

- n/a

### Changed

- n/a

## [0.5.0] - 2024-02-29

### Added

- fixed store datafrave to csv file method

### Changed

- n/a

## [0.4.0] - 2023-03-12

### Added

- n/a

### Changed

- changed package manager

## [0.3.2] - 2022-10-10

### Added

- n/a

### Changed

- n/a

## [0.3.1] - 2022-06-15

### Added

- n/a

### Changed

- n/a

## [0.2.1] - 2021-08-01

### Added

-

### Changed

- `get_current_trading_code()`

## [0.2.0] - 2021-06-01

### Added

- added get_market_depth_data
- added get_dsex_data
- added 'dse.com.bd' as redundant

### Changed

- Changed documentation
- changed get_agm_news
- changed get_all_news

## [0.1.4] - 2020-08-22

### Added

- added get_market_inf_more_data

### Changed

- Changed documentation

## [0.1.3] - 2020-08-20

### Added

- html5lib
- added get params

### Changed

- post request to get

## [0.1.2] - 2020-05-21

### Added

- modified index declaration

## [0.1.1] - 2020-05-20

### Added

- modified index declaration

## [0.1.0] - 2020-04-08

### Added

- added git tag
- `VERSION.txt`

### Changed

- `setup.py`
- `HISTORY.md` to `CHANGELOG.md`

## [0.0.1] - 2020-04-06

### Added

- `get_hist_data(), get_current_trade_data()`
- `HISTORY.md`
