Metadata-Version: 2.5
Name: glimpse-markets
Version: 0.1.0
Summary: Official Python client for the Glimpse Nmarket prediction-market API
Project-URL: Homepage, https://docs.glimpse.markets
Project-URL: Documentation, https://docs.glimpse.markets
Project-URL: Repository, https://github.com/BIRKELAND-GLIMPSE/python-package
Project-URL: Issues, https://github.com/BIRKELAND-GLIMPSE/python-package/issues
Author-email: Ritankar Saha <ritankar.saha786@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api-client,glimpse,lmsr,prediction-markets,trading,trading-bot
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: click>=8.1
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: websockets>=13
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# glimpse-markets

The official Python client for [Glimpse](https://docs.glimpse.markets)'s
Nmarket prediction-market API — for people building forecasting algorithms
and trading bots on Glimpse without writing HTTP plumbing by hand.

```bash
pip install glimpse-markets
```

> **Status:** every read endpoint, every trade endpoint, the `glimpse` CLI,
> an async client, real-time streaming, and a small bot-building layer are
> all implemented.

## Contents

- [Features](#features)
- [Quickstart](#quickstart)
- [Trading](#trading)
- [Paper trading (dry run)](#paper-trading-dry-run)
- [Async client](#async-client)
- [Real-time streaming](#real-time-streaming)
- [Building a bot](#building-a-bot)
- [CLI](#cli)
- [Units: millisats vs. price](#units-millisats-vs-price)
- [Error handling](#error-handling)
- [Development](#development)
- [License](#license)

## Features

- **Sync and async clients** with an identical method surface — every
  market-data, portfolio, and trading endpoint the public API exposes.
- **Typed responses.** Every call returns a [Pydantic](https://docs.pydantic.dev)
  model, not a raw dict — autocomplete and validation instead of
  `resp["message"]["..."]`.
- **Built-in paper trading.** Glimpse has no sandbox environment — every API
  key is a live key. `dry_run=True` simulates trades client-side through the
  free `/trades/estimate` endpoint, so you can test a strategy against real
  live prices without risking real funds.
- **Real-time market data** over Glimpse's WebSocket feed, with no polling
  loop to write yourself.
- **A minimal bot-building layer** (`Strategy` / `StrategyRunner`) for
  wiring strategy logic up to the live feed without hand-rolling the
  connect/dispatch/reconnect plumbing.
- **A `glimpse` CLI** for one-off calls from the terminal — check a
  balance, get a quote, place a trade — without writing any code.
- **Safety around the sharp edges.** The API has no idempotency key, so a
  network failure mid-trade is surfaced as a distinct
  `GlimpseAmbiguousTradeStateError` instead of being silently retried (which
  could double-execute a real trade) or silently swallowed.

## Quickstart

```python
from glimpse_markets import Client

with Client(api_key="glp_live_...") as client:
    print(client.wallet_balance())

    batch = client.batches().batches[0]
    print(client.batch_active_markets(batch.batch_id))

# Market-data endpoints (batches, markets, quotes, stats) are public --
# no API key needed:
with Client() as client:
    print(client.market_quotes(topic_id=6674))
```

Or configure from the environment (`GLIMPSE_API_KEY`, `GLIMPSE_BASE_URL`),
optionally via a `.env` file in your working directory:

```python
from glimpse_markets import Client

with Client.from_env() as client:
    print(client.portfolio_summary())
```

Generate an API key from your Glimpse account under **Settings → Developer
API Keys**.

## Trading

```python
from glimpse_markets import Client, TradeLeg, EnterMultiTopicLegGroup

with Client.from_env() as client:
    # Always check cost and price impact first -- free, no API key required.
    estimate = client.estimate_trade(6674, "buy", [TradeLeg(option_id=500, contracts=10)])
    print(estimate)

    topics = [EnterMultiTopicLegGroup(topic_id=6674, legs=[TradeLeg(option_id=500, contracts=10)])]
    result = client.enter_multi_topic_multi_leg(topics)
    print(result)

    client.exit_consolidated(topic_id=6674, option_id=500)  # exits the full position
```

Entering a position is buy-only, by design of the underlying API — exit an
existing position to realize a "sell." `exit_consolidated_multi`,
`exit_multi_topic_multi_leg`, and `exit_batch` cover multi-leg and
whole-batch exits.

## Paper trading (dry run)

Glimpse has no sandbox environment — every API key trades with real funds.
Pass `dry_run=True` (client-wide, or per call via `dry_run=...` on any trade
method) to paper-trade instead: every `enter`/`exit` call is priced through
the side-effect-free `/trades/estimate` endpoint rather than placing a real
order, and returns a `DryRunTradeResult` — a distinct type from a real
response, so a simulated fill can never be mistaken for a real one:

```python
with Client.from_env(dry_run=True) as client:
    result = client.enter_multi_topic_multi_leg(topics)
    assert result.simulated is True  # no order was placed
```

A network failure while an `enter`/`exit` call is genuinely in flight
raises `GlimpseAmbiguousTradeStateError` rather than being retried — the API
has no idempotency key, so the client can't safely guess whether the trade
went through. Check `client.portfolio_active()` before resubmitting.

## Async client

`AsyncClient` mirrors `Client`'s entire method surface — same names, same
signatures, `await` in front — built on `httpx.AsyncClient`, for bots
already running an asyncio event loop:

```python
import asyncio
from glimpse_markets import AsyncClient, TradeLeg

async def main():
    async with AsyncClient.from_env() as client:
        print(await client.wallet_balance())
        estimate = await client.estimate_trade(6674, "buy", [TradeLeg(option_id=500, contracts=10)])
        print(estimate)

asyncio.run(main())
```

## Real-time streaming

Glimpse pushes live quote updates over a public WebSocket feed — no API key
required, no polling loop to write:

```python
import asyncio
from glimpse_markets import AsyncClient

async def main():
    async with AsyncClient() as client:
        async with client.stream_market_updates(topic_id=6674) as stream:
            async for update in stream:
                print(update.data.quotes or update.data.binary_quotes)

asyncio.run(main())
```

Pass `topic_id` and/or `batch_id` to filter the feed to one market or batch
(call `stream.subscribe(...)` again later to change the filter without
reconnecting); pass neither to receive every market's updates. There is
**no separate "market resolved" event on this feed** — detect resolution by
polling `quote_mode` via `market_quotes()` or `batch_active_markets_page()`
instead.

`MarketStream` also works standalone: `from glimpse_markets import MarketStream`.

## Building a bot

`Strategy` and `StrategyRunner` wire a `MarketStream` up to your logic
without you writing the connect/dispatch/reconnect plumbing yourself:

```python
import asyncio
from glimpse_markets import AsyncClient, MarketUpdate, Strategy, StrategyRunner

class MyStrategy(Strategy):
    async def on_quote(self, update: MarketUpdate) -> None:
        # called for every market_update from the stream
        print(update.topic_id, update.data.quotes)

    async def on_tick(self) -> None:
        # called every `tick_interval` seconds, independent of quote events
        positions = await self.positions.get()  # cached portfolio_active()
        print(f"{len(positions)} open positions")

async def main():
    client = AsyncClient.from_env(dry_run=True)  # paper-trade by default
    runner = StrategyRunner(MyStrategy(), client=client, topic_id=6674, tick_interval=5.0)
    await runner.run()

asyncio.run(main())
```

`self.client` (the `AsyncClient`) and `self.positions` (a `PositionTracker`)
are available inside any hook — place trades with the former, check current
positions with the latter without re-fetching your whole portfolio on every
quote tick. `PositionTracker` caches `portfolio_active()` and refreshes at
most once every few seconds; call `self.positions.invalidate()` right after
placing a trade to force a fresh read.

An exception raised from `on_quote` or `on_tick` stops the runner and
propagates out of `run()` — a strategy bug fails loud instead of getting
silently swallowed. See [`examples/dry_run_strategy.py`](./examples/dry_run_strategy.py)
for a complete, runnable example.

## CLI

The `glimpse` command covers the same ground as the Python client, for
one-off calls from the terminal. Config comes from `GLIMPSE_API_KEY` /
`GLIMPSE_BASE_URL`, read from the environment or a `.env` file in the
current directory.

```bash
glimpse balance

glimpse portfolio active
glimpse portfolio summary
glimpse portfolio ended [--limit N --offset N]
glimpse portfolio resolved [--limit N --offset N]

glimpse batches                       # list all batches
glimpse batches --batch-id <id>       # active markets in a batch
glimpse quotes --topic-id <id>

glimpse estimate --topic-id <id> --type buy --leg 500:10 [--leg 501:5]
glimpse execute --topic-id <id> --leg 500:10 [--dry-run]
glimpse exit --topic-id <id> --option-id 500 [--shares 5] [--dry-run]
glimpse exit-batch --batch-id <id> [--dry-run]
```

`--dry-run` works the same way it does in the Python client. `execute` and
a partial `exit --shares N` never need an API key in dry-run mode, since
both are priced entirely through the public estimate endpoint. A
full-position `exit` (no `--shares`) or `exit-batch` still needs a key even
in dry-run mode, since pricing them requires looking up your current
positions first.

## Units: millisats vs. price

The API mixes two numeric scales that are easy to confuse:

- **millisats** (`*_millisats` fields, wallet balance) — real money.
  1 satoshi = 1000 millisatoshis.
- **price** (`yes_price` / `no_price` / `odds` from quotes) — a 0–100
  LS-LMSR pricing scale, only converted to millisats at trade/settlement
  time.

`glimpse_markets.money` exposes `Millisats` and `PriceUnits` as distinct
types, plus `millisats_to_sats()` / `sats_to_millisats()` helpers, so it's
harder to accidentally treat a price of `62.1` as `62.1` millisats.

## Error handling

Every non-2xx response raises a subclass of `GlimpseAPIError`
(`GlimpseAuthenticationError`, `GlimpseForbiddenError`,
`GlimpseTradingNotEligibleError`, `GlimpseNotFoundError`,
`GlimpseValidationError`, `GlimpseRateLimitError`, `GlimpseServerError`),
each carrying `.status_code`, `.error`, `.reason`, `.message`, and the raw
response body:

```python
from glimpse_markets import GlimpseAPIError

try:
    client.wallet_balance()
except GlimpseAPIError as e:
    print(e.status_code, e.error, e.reason)
```

Both clients also throttle themselves client-side to stay under Glimpse's
60-requests-per-60-seconds-per-key limit, so a naive loop doesn't
immediately trip a 429.

## Development

```bash
git clone <repo-url>
cd python-package

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

ruff check .
mypy src
pytest
```

## License

MIT — see [LICENSE](./LICENSE).
