Fyers adapter for BrokerKit.
Project description
brokerkit-fyers
BrokerKit's Fyers adapter — wraps the official fyers-apiv3 SDK behind BrokerKit's broker-agnostic interfaces (auth, instruments, orders, portfolio, market data, historical candles, streaming).
Fyers is BrokerKit's primary data source (free market/historical/streaming data, unlike Groww which needs a paid subscription for the same) — market, historical, and streaming got the most scrutiny in this adapter — but the full Broker contract is implemented, no data-only shortcuts.
Install
pip install brokerkit-core brokerkit-fyers
Prerequisites (Fyers dashboard)
-
Create an API app at myapi.fyers.in/dashboard — you get a
client_id(App ID) andsecret_key(App Secret), and register aredirect_uri(a plainhttp://127.0.0.1:<port>/URL). -
Enable TOTP-based 2FA on your Fyers login and note the TOTP secret (same secret you'd scan into an authenticator app).
-
Know your Fyers ID (login username, e.g.
"XX00000") and your 4-digit trading PIN. -
Static IP registration (SEBI rule, same requirement as Groww) — needed for order placement. Without it,
orders.place_order()/modify()/cancel()fail; reads (instruments,market,historical,portfolio,orders.list_orders(),streaming) all work fine without it. -
One-time only, per app: activate the app via a real browser login —
from brokerkit_fyers import get_access_token get_access_token(client_id="...", secret_key="...", redirect_uri="http://127.0.0.1:5000/")
Opens a browser to log in, catches the redirect on a local server, and prints an
access_token— that's just proof the app + credentials work end-to-end, you don't need to save it. Confirmed necessary by testing: a brand-new app returns"invalid totp"on step 4 below until this has been done once; it worked immediately after. Exact mechanism unconfirmed (Fyers doesn't document this), but the fix is repeatable.
After that one-time step, everything is credential-only — no more browser, no token to copy-paste.
Auth, honestly
Fyers' officially documented auth is browser-redirect only: generate-authcode → you log in in a browser → Fyers redirects with an auth_code → validate-authcode exchanges it for an access_token. There's no SDK-wrapped way to skip that browser round-trip through the official flow alone — that's what get_access_token() above does, and per the testing above, a new app seems to need it run once regardless.
FyersAuth (used by create_broker("fyers", ...) for every login after that) instead drives the same internal endpoints Fyers' own web login uses (api-t2.fyers.in/vagator/v2/*) — TOTP for the OTP step, PIN for the next — to get an auth_code programmatically, then finishes with the official, documented exchange (SessionModel.generate_token()). This is a known community pattern, not something invented here; it was adopted after cross-verifying against a previously-working implementation of the same flow, not guessed. login() does this full sequence fresh every time — no persisted refresh_token to track, same ergonomics as GrowwAuth. FyersBroker._auto_refresh_loop() sleeps until the token's assumed expiry (Fyers gives no reliable expiry signal, unlike Groww's deterministic 6 AM IST) and re-logs in automatically. Verified live 2026-07-20 against a real account (auth + instrument fetch + quote).
Trade-off, stated plainly: the auth_code-acquisition part of FyersAuth isn't part of any Fyers SDK or public API contract — it plays back a sequence the Fyers website uses internally, not a documented integration surface. It could break without notice on a UI/security change on Fyers' end. If that ever happens, only brokerkit_fyers/auth.py needs to change — the rest of the adapter is unaffected, and get_access_token() (the official flow) still works as a fallback.
get_access_token() internals
Runs a tiny local Flask server on your redirect_uri to catch auth_code directly instead of asking you to copy-paste it from the browser address bar — manual copy-paste of that (very long) JWT is exactly what caused an "invalid auth code" failure while first building this. Can also be run from the command line: python -m brokerkit_fyers.login_helper <client_id> <secret_key> <redirect_uri>.
Quick start
import asyncio
from brokerkit import Exchange, Segment, create_broker
async def main():
broker = await create_broker(
"fyers",
client_id="XXXXXXXXXX-100",
secret_key="...",
redirect_uri="http://127.0.0.1:5000/",
fy_id="XX00000",
totp_secret="...",
pin="1234",
)
instruments = await broker.instruments.fetch_instruments()
reliance = next(
i for i in instruments
if i.symbol == "RELIANCE-EQ" and i.exchange == Exchange.NSE and i.segment == Segment.CASH
)
quote = await broker.market.get_quote(reliance)
print("LTP:", quote.last_price)
await broker.close()
asyncio.run(main())
What you get on broker
broker.instruments — InstrumentProvider
instruments = await broker.instruments.fetch_instruments() # list[Instrument]
Fyers has no single combined instrument file like Groww — this downloads and normalizes five public per-exchange/segment CSVs (NSE_CM, NSE_FO, BSE_CM, BSE_FO, MCX_COM; no auth needed) concurrently and concatenates them (~128k instruments as of 2026-07-20). Currency derivatives (NSE_CD) are deliberately excluded — core's Segment enum has no CURRENCY value.
broker.market — MarketDataProvider
await broker.market.get_quote(reliance) # Quote
await broker.market.get_ltp([reliance, tcs]) # dict[symbol, Decimal]
await broker.market.get_ohlc([reliance, tcs]) # dict[symbol, Ohlc]
get_ltp/get_ohlc batch up to 50 symbols per call (Fyers' own limit) and chunk automatically. Note: Fyers' /quotes doesn't return market depth or circuit limits (those live on a separate /depth endpoint this adapter doesn't call for get_quote) — the resulting Quote is narrower than Groww's equivalent on those fields; that's a real capability difference between the two brokers, not a bug here.
from datetime import date
chain = await broker.market.get_option_chain(nifty_index, expiry=date(2026, 7, 21), strike_count=10)
print(chain.underlying_ltp)
for s in chain.strikes:
print(s.strike, s.call.ltp if s.call else None, s.call.greeks.delta if s.call and s.call.greeks else None)
expiry is required (no "nearest expiry" convenience in v1 — caller must know a valid one, e.g. from Fyers' own app). timestamp is computed internally as 15:30:00 IST on that date, verified against real expiryData values, not guessed. Verified live 2026-07-20 against a real account (real NIFTY greeks: delta/gamma/theta/vega/iv; real bid/ask spreads for liquidity checks before trading). Fyers' greeks have no rho (OptionGreeks.rho is always None here) — Groww's do. Groww's contracts have no bid_price/ask_price at all (confirmed absent, not guessed) — Fyers' do.
broker.historical — HistoricalDataProvider
from datetime import datetime, timedelta
candles = await broker.historical.get_candles(
reliance, start=datetime.now() - timedelta(days=7), end=datetime.now(), interval_minutes=1440,
)
broker.streaming — StreamingProvider
async def on_tick(tick):
print(tick.symbol, tick.ltp)
await broker.streaming.subscribe_ltp([reliance], on_tick)
await broker.streaming.unsubscribe_ltp([reliance])
await broker.streaming.close()
Two Fyers SDK quirks worth knowing (verified from fyers_apiv3 source, not guessed):
FyersDataSocketis a process-wide singleton. A secondFyersStreamingin the same process (e.g. a second Fyers account viaBrokerManager) would silently hijack the first one's connection —FyersStreamingdetects this and raisesStreamingErrorinstead of letting that happen. Only one Fyers streaming connection per process for now.- Reconnects wipe the SDK's own subscription state.
FyersStreaminghooks the reconnect callback to automatically replay your tracked subscriptions, so a network blip doesn't silently kill the stream — but this hasn't been exercised against a real dropped connection yet.
open_interest on Tick is always None for Fyers — the SDK's own LTP feed explicitly strips that field before dispatching (confirmed in FyersWebsocket/data_ws.py), not an adapter gap.
broker.orders — OrderProvider
from decimal import Decimal
from brokerkit import OrderRequest, OrderType, Product, TransactionType
order = await broker.orders.place_order(OrderRequest(
instrument=reliance, transaction_type=TransactionType.BUY, order_type=OrderType.LIMIT,
quantity=1, product=Product.CNC, price=Decimal("2500"),
))
await broker.orders.modify(order.order_id, reliance.segment, price=Decimal("2510"))
await broker.orders.cancel(order.order_id, reliance.segment)
await broker.orders.get_order(order.order_id, reliance.segment)
await broker.orders.list_orders()
Fyers has no single-order lookup endpoint — get_order/list_orders fetch the whole orderbook and filter, same as the official SDK's own get_orders() helper does internally. No client-side pre-validation, same philosophy as the Groww adapter — Fyers rejects invalid orders and the reason comes back as Order.status_message.
place_order/modify/cancel need the static IP registered (see Prerequisites) — untested live for that reason. list_orders() (read-only) works without it and is live-verified.
broker.portfolio — PortfolioProvider
await broker.portfolio.holdings() # list[Holding]
await broker.portfolio.positions() # list[Position]
Fyers' holdings response carries no ISIN — Holding.isin is always None for this adapter.
Errors
Fyers' SDK never raises on an API-level failure — every call just returns {"s": "error", ...}, even for auth problems. This adapter inspects that (brokerkit_fyers/errors.py) and translates it into the same core exceptions Groww uses, so your code never needs to know the difference:
from brokerkit import BrokerKitError, OrderError
try:
await broker.orders.place_order(request)
except OrderError as e:
print("order failed:", e)
Live verification status
Verified live against a real account, 2026-07-20:
- Auth — TOTP+PIN auto-login (
FyersAuth), after the one-timeget_access_token()app-activation step. - Instruments —
fetch_instruments()against the real public CSVs (127,758 instruments across EQ/IDX/FUT/CE/PE,RELIANCE-EQresolves correctly with real ISIN/tick size). No auth needed for this one. - Market data —
get_quote(),get_ltp(),get_ohlc(),get_option_chain()(real NIFTY greeks confirmed). - Historical —
get_candles(). - Portfolio —
holdings(),positions(). - Streaming —
subscribe_ltp()against real ticks during market hours (single symbol and a 50-symbol batch). - Orders —
list_orders()(read-only) works.place_order/modify/cancelare code-complete and unit-checked against real captured response shapes, but not yet live-tested — blocked on static IP registration (see Prerequisites), same blocker Groww hit for the same SEBI rule.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file brokerkit_fyers-1.0.2.tar.gz.
File metadata
- Download URL: brokerkit_fyers-1.0.2.tar.gz
- Upload date:
- Size: 25.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d85c626918048b959588c253976713205ffb0b550674b1322741d867efa33d9
|
|
| MD5 |
3fa0250a661e563dc1aa238e0fce91ac
|
|
| BLAKE2b-256 |
88d341857b80de3d6ea15472fdaa560947e00a969827e00a23d791eac1578d2e
|
File details
Details for the file brokerkit_fyers-1.0.2-py3-none-any.whl.
File metadata
- Download URL: brokerkit_fyers-1.0.2-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c4682e7d4de82d8099b9bad771359a4f9f797be102cf0aa0b80b840279cbb3d
|
|
| MD5 |
f94eaf58d4251bf0b005d55e8db384da
|
|
| BLAKE2b-256 |
ea49e2391cfc0f422609f48878f075043e05da2b5341409a157fa138309e846b
|