Metadata-Version: 2.4
Name: nautilus-fyers
Version: 0.3.0
Summary: Fyers (Indian broker) adapter for NautilusTrader — REST + WebSocket bridge for NSE/BSE live data and execution
Author: Sunil Sala
License: MIT
Project-URL: Homepage, https://github.com/sunilsala88/nautilus-fyers
Project-URL: Issues, https://github.com/sunilsala88/nautilus-fyers/issues
Keywords: trading,nautilus,fyers,nse,bse,algotrading
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: certifi
Provides-Extra: live
Requires-Dist: websockets>=12; extra == "live"
Provides-Extra: stream
Requires-Dist: fyers-apiv3>=3.1; extra == "stream"
Provides-Extra: nautilus
Requires-Dist: nautilus_trader>=1.200; extra == "nautilus"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Dynamic: license-file

# nautilus-fyers

A [NautilusTrader](https://nautilustrader.io)-style adapter for **Fyers** (Indian broker, API v3),
bridging Fyers REST + WebSocket APIs for live market data and execution on NSE/BSE instruments.

Built following the phased plan in [fyers_nautilus_adapter_plan.md](fyers_nautilus_adapter_plan.md).

## Architecture

```
Python layer  nautilus_fyers/   framework-neutral clients, config, auth
Rust core     crates/adapters/fyers/            wire models, enums, parsing, protocol state
```

The Python layer is fully functional standalone (stdlib only; `websockets` needed for live
streaming). The Rust crate mirrors the NautilusTrader adapter crate layout (models/enums/parse
per module, no async runtime dependency) so it can later be vendored into the
`nautilus_trader` workspace and bound via PyO3.

| Component | Module | Status |
|---|---|---|
| Instrument provider (symbol master, positional + header CSV) | `providers.py` | ✅ tested |
| OAuth2 auth flow + daily token refresh | `auth.py` | ✅ tested |
| REST client (quotes, depth, history, orders, positions, holdings, funds, tradebook) | `http.py` | ✅ tested |
| Rate limiting (token bucket, 10 req/s) + retries with backoff | `http.py` | ✅ tested |
| **Real-time streaming** (official SDK decoder, asyncio bridge, `[stream]` extra) | `streaming.py`, `data.py` | ✅ live-verified |
| Data socket protocol state (modes, reconnect + subscription restore) | `transport.py`, `ws.py` | ✅ tested |
| Order socket (order/trade/position updates, replay dedup) | `transport.py`, `execution.py` | ✅ tested |
| Order lifecycle (submit/modify/cancel/cancel-all) + reconciliation reports | `execution.py` | ✅ tested |
| **NautilusTrader TradingNode integration** (factories, live clients, Equity/Option/Future instruments, reconciliation reports, account state) | `nautilus.py` | ✅ tested against nautilus_trader 1.230 |
| Rust core (models, enums, parse, protocol state) | `crates/adapters/fyers` | ✅ tested |

## Quick start

### 1. Get an access token (daily)

```bash
export FYERS_CLIENT_ID="ABCD1234-100"
export FYERS_SECRET_KEY="..."
python examples/generate_access_token.py
export FYERS_ACCESS_TOKEN="<printed token>"
```

### 2. Stream live quotes

```bash
pip install "nautilus-fyers[stream]"   # real-time streaming (official SDK decoder)
python examples/basic_data_subscription.py
```

```python
from nautilus_fyers import FyersDataClientConfig, QuoteEvent, create_data_client

client = create_data_client(FyersDataClientConfig.from_env())
client.on(QuoteEvent, my_async_handler)
await client.subscribe_quotes(["NSE:RELIANCE-EQ"])
await client.connect()
```

### 3. Trade

```python
from nautilus_fyers import FyersExecClientConfig, create_execution_client

exec_client = create_execution_client(FyersExecClientConfig.from_env())
await exec_client.connect()   # order socket: real-time order/trade/position updates
response = await exec_client.submit_order(
    "NSE:SBIN-EQ", "BUY", 1, order_type="LIMIT", limit_price=795.0,
)
await exec_client.cancel_order(response["id"])
state = await exec_client.reconcile()  # orders + positions + trades
```

See [examples/](examples/) for complete scripts, including a safe live order-lifecycle
test that uses a far-from-market limit order (Fyers has no paper-trading sandbox).

### 4. Run a NautilusTrader TradingNode

```bash
pip install "nautilus-fyers[nautilus,live]"
```

```python
from nautilus_trader.config import TradingNodeConfig
from nautilus_trader.live.node import TradingNode
from nautilus_fyers.nautilus import (
    FyersDataClientConfig, FyersExecClientConfig,
    FyersLiveDataClientFactory, FyersLiveExecClientFactory,
)

config = TradingNodeConfig(
    trader_id="FYERS-TRADER-001",
    data_clients={"FYERS": FyersDataClientConfig(client_id=..., access_token=...)},
    exec_clients={"FYERS": FyersExecClientConfig(client_id=..., access_token=...)},
)
node = TradingNode(config=config)
node.add_data_client_factory("FYERS", FyersLiveDataClientFactory)
node.add_exec_client_factory("FYERS", FyersLiveExecClientFactory)
node.build()
node.run()
```

Strategies address instruments as `NSE:SBIN-EQ.FYERS`. Execution updates
(accepted/filled/cancelled/rejected, partial fills, account state, reconciliation
reports) flow through the real-time order socket; quote ticks are synthesized by
REST polling (configurable `quote_poll_interval`, default 1s) until the binary
HSM market-data codec is implemented.

**→ Full guides:**
- [docs/nautilus_integration.md](docs/nautilus_integration.md) — live trading:
  instrument loading, order-type mapping, product types, restarts/reconciliation,
  and a complete SMA crossover strategy
  ([examples/sma_crossover_live.py](examples/sma_crossover_live.py))
- [docs/backtesting.md](docs/backtesting.md) — backtesting: Fyers history →
  Nautilus `Bar`s → `BacktestEngine`, with history-endpoint limits and a
  validated runnable example ([examples/backtest_sma.py](examples/backtest_sma.py))

### 5. Load instruments

```python
from nautilus_fyers import FyersInstrumentProvider

provider = FyersInstrumentProvider()
await provider.load_all_async({"segment": ["CM", "FO"]})  # downloads the daily symbol master
instrument = provider.get("NSE:RELIANCE-EQ.FYERS")
```

## Development

```bash
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest                      # Python tests
cargo test --manifest-path crates/adapters/fyers/Cargo.toml   # Rust tests
```

All test fixtures live under [test_data/](test_data/) and mirror real Fyers API payload shapes
(`http_*.json` for REST, `ws_*.json` for WebSocket frames, plus a positional symbol-master CSV).

## Implementation notes & deviations from the plan

- **Authorization header**: Fyers v3 expects `Authorization: <client_id>:<access_token>`,
  not `Bearer <token>` as sketched in the plan. `FyersConfig.authorization()` builds the
  correct form.
- **Modify order** uses HTTP `PATCH` (matching the official `fyers-apiv3` SDK), not `PUT`.
- **Endpoint paths**: trading endpoints live under `/api/v3/...` and market data under
  `/data/...` on `api-t1.fyers.in`; the plan's single `/orders`, `/data/quotes` paths were
  normalized accordingly.
- **WebSocket protocol** (verified against the official `fyers-apiv3` SDK source): auth
  travels on the websocket *handshake* as an `authorization: <client_id>:<token>` header —
  there is no JSON "CONNECT" frame. The order socket is `wss://socket.fyers.in/trade/v3`
  (JSON), the data socket is `wss://socket.fyers.in/hsm/v1-5/prod` (binary "HSM" protocol
  that first requires symbol→HSM-token conversion via `/data/symbol-token`), and keepalive
  is a literal `"ping"` text frame. Raw order-socket frames use short field names
  (`qty_filled`, `price_limit`, `tran_side`, ...) which the event parsers accept alongside
  the mapped names.
- **Binary data frames (HSM)**: solved by wrapping the official `fyers-apiv3` SDK — the
  broker's own maintained decoder — as an optional streaming backend
  (`pip install "nautilus-fyers[stream]"`), the same pattern NautilusTrader's IBKR
  adapter uses with `ibapi`. With the extra installed, `subscribe_quotes()` delivers
  true streaming ticks; without it, quotes fall back to 1s REST polling. A native
  asyncio HSM codec remains a possible future replacement.
- **Native Nautilus objects**: NautilusTrader is an optional dependency. The core adapter
  emits neutral event dataclasses (`QuoteEvent`, `OrderEvent`, ...); `nautilus_fyers.nautilus`
  (imported only when NautilusTrader is installed) provides full `TradingNode` wiring —
  `LiveMarketDataClient`/`LiveExecutionClient` subclasses, Equity/Option/Future instrument
  conversion, order lifecycle events with partial-fill deltas, reconciliation reports, and
  account state from `/funds`.
- **Rust core**: the crate contains the domain layer (models, enums, parsing, subscription
  and dedup state) with fixture-driven unit tests, but no HTTP/WebSocket I/O runtime — that
  arrives when the crate is vendored into the NautilusTrader workspace where
  `nautilus-network` provides the clients.

## Restarts & carryforward

Order ownership survives restarts through three mechanisms:

1. Every order is submitted with a Fyers `orderTag` carrying a sanitized copy of the
   Nautilus `ClientOrderId`, and the tag round-trips in order-book rows and socket frames.
2. On connect, the execution client seeds its venue-order map and *cumulative fill
   counters* from the venue order book — so socket replays of already-filled orders never
   double-fill, and fills that happened while you were offline reconcile as deltas.
3. Reconciliation reports resolve `ClientOrderId` from the returned tag by matching
   against cached orders.

For step 3 to work across a process restart, enable Nautilus cache persistence so cached
orders survive (otherwise recovered orders are adopted as external/venue orders — safe,
but not attributed to your strategy):

```python
from nautilus_trader.config import CacheConfig, DatabaseConfig

config = TradingNodeConfig(
    ...,
    cache=CacheConfig(database=DatabaseConfig(type="redis", host="localhost", port=6379)),
)
```

Positions carried overnight (CNC) are recovered via position status reports on every start.

## Symbol format

Fyers symbols look like `NSE:RELIANCE-EQ`, `NSE:NIFTY25JUL25000CE`, `BSE:HDFCBANK-A`.
Instrument ids append the venue: `NSE:RELIANCE-EQ.FYERS`.

## Rate limits & tokens

- REST: ~10 requests/second (enforced client-side by a token bucket).
- Access tokens expire daily (~6 AM IST). Use `TokenManager` with a refresh callback, or
  re-run `examples/generate_access_token.py`. Refresh tokens last ~15 days.
