tse-client 0.3.0

A client for fetching stock data from the Tehran Stock Exchange (TSETMC). Rust port of the tse-client npm package.
Documentation
# tse-client (Rust)

<p align="center">
<img src="assets/tsetmc.png" alt="TSETMC" width="150">
</p>

A Rust library crate and port of the [`tse-client`](https://www.npmjs.com/package/tse-client) npm package, providing a client for accessing data from the Tehran Stock Exchange Technology Management Company (TSETMC). The crate supports fetching OHLC (Open/High/Low/Close) candlestick data, intraday trading data, market indices, and the complete list of tradable instruments across all major Iranian capital markets, including the Tehran Stock Exchange (TSE / بورس تهران), Iran Fara Bourse (IFB / فرابورس ایران), and the Base Market (بازار پایه). Supported indices include the Total Index (شاخص کل) and IFB Total Index (شاخص کل فرابورس).

> [!NOTE]
> This crate must be called on a server located **inside Iran**. TSE data
> servers are not accessible from outside the country.

## Features

- 📈 Fetch OHLC candlestick data for any TSE instrument
- 📋 Get the full list of TSE-listed instruments
- 🕯️ Supports Daily, Weekly, and Monthly timeframes with automatic resampling
- ⚡ Fetch intraday trading data
- 📅 Jalali (Solar Hijri) calendar support for correct weekly/monthly grouping
- 🔧 Adjusted prices support
- 🦀 High-performance Rust implementation and port of `tse-client`

## Public API

Mirrors the JS module:

```rust
use tse_client::{Client, PriceSettings, IntradaySettings};

#[tokio::main]
async fn main() -> tse_client::Result<()> {
    let client = Client::new()?;

    // getPrices
    let res = client
        .get_prices(&["فملی".to_string()], &PriceSettings::default())
        .await?;
    println!("{:#?}", res.data);

    // getInstruments
    let instruments = client.get_instruments().await?;
    println!("{} instruments", instruments.len());

    // getIntraday
    let itd = client
        .get_intraday(&["فملی".to_string()], &IntradaySettings::default())
        .await?;
    println!("{:#?}", itd.error);

    Ok(())
}
```

### Weekly / monthly timeframe (Jalali calendar)

`get_prices` can resample daily data into weekly or monthly OHLCV bars using
Jalali (Solar Hijri) calendar boundaries. Weeks run Saturday–Friday (the Iranian
trading week); months follow the Jalali calendar. Set the `period` field on
`PriceSettings`:

```rust
use tse_client::{Client, Period, PriceSettings};

# async fn run() -> tse_client::Result<()> {
let client = Client::new()?;
let settings = PriceSettings {
    period: Period::Weekly, // or Period::Monthly / Period::Daily (default)
    ..PriceSettings::default()
};
let res = client.get_prices(&["فملی".to_string()], &settings).await?;
# Ok(())
# }
```

You can also group already-parsed `ClosingPrice` rows directly with the free
function:

```rust
use tse_client::{group, ClosingPrice, Period};

// `daily` is a slice of ClosingPrice rows sorted ascending by date.
let daily: Vec<ClosingPrice> = /* ... */ vec![];

let weekly = group(&daily, Period::Weekly);
let monthly = group(&daily, Period::Monthly);
let unchanged = group(&daily, Period::Daily); // returns the input as-is
```

Aggregation per group: open = first open, close = last close, high = max high,
low = min low, and volume/value/count are summed. See
[`examples/group_prices.rs`](examples/group_prices.rs) for a runnable demo.

Configuration that used to be mutable globals (`PRICES_UPDATE_CHUNK`,
`INTRADAY_URL`, etc.) now lives on `Client::config_mut()` via the `Config`
struct.

## Notable differences from the JS version (bug / leak fixes)

The original JS kept several **module-level mutable maps** that were _never
cleared between calls_:

- `storedPrices` — accumulated every instrument's full price history ever
  fetched, for the lifetime of the process.
- `lastdevens` — accumulated last-update markers.
- `stored` (intraday) — accumulated every intraday record ever fetched.

In a short-lived CLI run this is harmless, but in a long-running process (a
server importing the module) it is an unbounded memory leak: memory grew with
every distinct symbol requested and was never reclaimed.

In this port, all of that working state is **owned locally per call**
(`PricesState` in `client.rs`, and the `stored` map in `intraday.rs`) and is
dropped when the call returns. No cross-call accumulation, no leak.

Other cleanups:

- Mutable URL/config globals replaced by an owned `Config` (no `static mut`).
- The `setTimeout`/`poll` callback state machines (`pricesUpdateManager`,
  `itdUpdateManager`) are expressed as plain bounded `async`/`await` loops with
  explicit retry rounds, removing the timer/`Map<id, timeout>` bookkeeping.
- Price adjustment uses `rust_decimal` with `MidpointNearestEven` rounding to
  match `decimal.js`'s `ROUND_HALF_EVEN`.

## Build & test

```shell
cargo build
cargo test
cargo clippy --all-targets
```