Skip to Main Content

Code Examples

Annotated walkthrough of the official example_orders.py and example_market.py scripts shipped in the Shoonya_API_OAuth repo — the real reference implementation behind the Python SDK, not illustrative pseudocode.

Overview

The Shoonya_API_OAuth repo ships two runnable CLI scripts — example_orders.py and example_market.py — that exercise every major call through a simple input()-driven menu loop. They're the fastest way to confirm your credentials and see real request/response shapes before wiring anything into a strategy. Both scripts import the same wrapper class, ShoonyaApiPy, from api_helper.py.

This is the actual SDK surfaceEarlier pages on this site show a simplified/illustrative ShoonyaClient for readability. The real class is ShoonyaApiPy (also aliased NorenApiPy in some forks), and its method names — place_order, get_quotes, searchscrip, start_websocket — are what you'll actually import and call.

Shared setup: OAuth login

Unlike the password/TOTP-based fork, this repo authenticates via the OAuth flow documented on Manual Login (OAuth) — redirect to the authorize URL, capture the code, then exchange it for a token:

python
from api_helper import NorenApiPy

import logging

logging.basicConfig(level=logging.DEBUG)

api = NorenApiPy()

uid = "ABC123"          # your Shoonya user ID
client_id = "ABC123_U"
secret_key = ""     #your Secret_Code

# Step 1 — get the OAuth login URL, visit it, log in, capture 'code' from the redirect
oauth_url = api.getOAuthURL(client_id)
print("Visit and login:", oauth_url)
auth_code = input("Paste the 'code' from the redirect URL: ")

# Step 2 — exchange the auth code for an access token
acc_tok, usrid, ref_tok, actid = api.getAccessToken(auth_code, secret_key, client_id, uid)

print("Access Token:", acc_tok)
print("User ID:", usrid)
print("Refresh Token:", ref_tok)
print("Account ID:", actid)

# Step 3 — inject the token into the session, along with UID and AID
api.injectOAuthHeader(acc_tok, usrid, actid)

print("Login successful, session ready.")
No cred.yml in this forkThe password/TOTP fork stores credentials in a cred.yml file loaded at startup. This OAuth fork has no equivalent file — the only long-lived secret is secret_key, which should be loaded from an environment variable or secrets manager, never hardcoded as shown above. Treat it exactly like the checksum inputs on the Manual Login (OAuth) calculator.

example_orders.py — full script

A menu-driven loop covering the full order lifecycle. Reproduced with the OAuth login block collapsed (see above):

python
socket_opened = False

def event_handler_order_update(message):
    print("order event: " + str(message))

def event_handler_quote_update(message):
    print("quote event: " + str(message))

def open_callback():
    global socket_opened
    socket_opened = True
    print('app is connected')
    api.subscribe('NSE|22')
    # api.subscribe(['NSE|22', 'BSE|522032'])   # multiple tokens at once

# ... api = ShoonyaApiPy(); OAuth login via getOAuthURL/getAccessToken/injectOAuthHeader (see Setup section) ...

if ret != None:
    while True:
        print('p => place order')
        print('m => modify order')
        print('c => cancel order')
        print('y => order history')
        print('o => get order book')
        print('h => get holdings')
        print('l => get limits')
        print('k => get positions')
        print('d => get daily mtm')
        print('s => start_websocket')
        print('q => quit')
        prompt1 = input('what shall we do? ').lower()

        if prompt1 == 'p':
            ret = api.place_order(
                buy_or_sell='B', product_type='C',
                exchange='NSE', tradingsymbol='INFY-EQ',
                quantity=1, discloseqty=0,
                price_type='LMT', price=1500.00, trigger_price=None,
                retention='DAY', remarks='my_order_001',
            )
            print(ret)

        elif prompt1 == 'm':
            orderno = input('Enter orderno:').lower()
            ret = api.modify_order(
                exchange='NSE', tradingsymbol='INFY-EQ', orderno=orderno,
                newquantity=2, newprice_type='LMT', newprice=1505.00,
            )
            print(ret)

        elif prompt1 == 'c':
            orderno = input('Enter orderno:').lower()
            ret = api.cancel_order(orderno=orderno)
            print(ret)

        elif prompt1 == 'y':
            orderno = input('Enter orderno:').lower()
            ret = api.single_order_history(orderno=orderno)
            print(ret)

        elif prompt1 == 'o':
            ret = api.get_order_book()
            print(ret)

        elif prompt1 == 'h':
            ret = api.get_holdings()
            print(ret)

        elif prompt1 == 'l':
            ret = api.get_limits()
            print(ret)

        elif prompt1 == 'k':
            ret = api.get_positions()
            print(ret)

        elif prompt1 == 'd':
            # contributed by Aromal P Nair
            while True:
                ret = api.get_positions()
                mtm, pnl = 0, 0
                for i in ret:
                    mtm += float(i['urmtom'])
                    pnl += float(i['rpnl'])
                    day_m2m = mtm + pnl
                print(day_m2m)

        elif prompt1 == 's':
            if socket_opened:
                print('websocket already opened')
                continue
            ret = api.start_websocket(
                order_update_callback=event_handler_order_update,
                subscribe_callback=event_handler_quote_update,
                socket_open_callback=open_callback,
            )
            print(ret)

        else:
            print('Fin')
            break
Two things worth noticing
  • The 'd' (Daily MTM) branch is an infinite loop with no sleep or break condition — it's a quick-and-dirty demo, not something to run unmodified. Add a poll interval and an exit condition (see Daily MTM) before reusing it.
  • modify_order's success payload returns the order number under result, not norenordno — the same field-naming quirk documented on Modify Order. The example script just prints the raw dict, so this is easy to miss until you try to parse it.

place_order — real parameter mapping

The wrapper's Python keyword arguments don't share names with the raw JSON fields documented on Place Order. This is the actual mapping used by api_helper.py:

Python kwargRaw JSON fieldNotes
buy_or_selltrantypeB / S
product_typeprdC / M / I / H / B
exchangeexch
tradingsymboltsymURL-encode symbols with &, e.g. M&M
quantityqty
discloseqtydscqty
price_typeprctypWrapper-level enum includes MKT/SL-MKT; whether the broker's OMS actually accepts them is a separate, account-level restriction — see the warning on Place Order.
priceprc0.00 is valid only for MKT
trigger_pricetrgprcNone unless price_type is an SL variant
retentionretDAY / IOC / EOS
remarksremarksFree text — tag every order uniquely here for reconciliation, per Place Order best practices
uid, actid, ordersourceuid, actid, ordersourceFilled in automatically by the wrapper from the logged-in session — you never pass these yourself

example_market.py — full script

The market-data counterpart, covering symbol search, quotes, contract info, historical candles, and the option chain:

python
def get_time(time_string):
    data = time.strptime(time_string, '%d-%m-%Y %H:%M:%S')
    return time.mktime(data)

# ... api = ShoonyaApiPy(); OAuth login via getOAuthURL/getAccessToken/injectOAuthHeader ...

if ret != None:
    while True:
        print('f => find symbol')
        print('m => get quotes')
        print('p => contract info n properties')
        print('v => get 1 min market data')
        print('t => get today 1 min market data')
        print('d => get daily data')
        print('o => get option chain')
        print('s => start_websocket')
        print('q => quit')
        prompt1 = input('what shall we do? ').lower()

        if prompt1 == 'v':
            ret = api.get_time_price_series(
                exchange='NSE', token='22',
                starttime=1642265814, endtime=1642438794, interval=240,
            )
            df = pd.DataFrame.from_dict(ret)
            print(df)

        elif prompt1 == 't':
            ret = api.get_time_price_series(exchange='NFO', token='71321')
            df = pd.DataFrame.from_dict(ret)
            print(df)

        elif prompt1 == 'f':
            exch, query = 'MCX', 'CRUDEOIL FEB'
            ret = api.searchscrip(exchange=exch, searchtext=query)
            print(ret)
            if ret != None:
                for symbol in ret['values']:
                    print('{0} token is {1}'.format(symbol['tsym'], symbol['token']))

        elif prompt1 == 'd':
            ret = api.get_daily_price_series(
                exchange='NSE', tradingsymbol='RELIANCE-EQ', startdate=0,
            )
            print(ret)

        elif prompt1 == 'p':
            ret = api.get_security_info(exchange='NSE', token='22')
            print(ret)

        elif prompt1 == 'm':
            ret = api.get_quotes(exchange='NSE', token='22')
            print(ret)

        elif prompt1 == 'o':
            exch, tsym = 'MCX', 'CRUDEOIL18FEB22'
            chain = api.get_option_chain(exchange=exch, tradingsymbol=tsym, strikeprice=4150, count=2)
            chainscrips = []
            for scrip in chain['values']:
                scripdata = api.get_quotes(exchange=scrip['exch'], token=scrip['token'])
                chainscrips.append(scripdata)
            print(chainscrips)

        elif prompt1 == 's':
            if socket_opened:
                print('websocket already opened')
                continue
            ret = api.start_websocket(
                order_update_callback=event_handler_order_update,
                subscribe_callback=event_handler_quote_update,
                socket_open_callback=open_callback,
            )
            print(ret)

        else:
            ret = api.logout()
            print(ret)
            print('Fin')
            break
The option chain branch makes N+1 callsThe 'o' handler fetches the chain once via get_option_chain, then loops and calls get_quotes per strike returned. For a chain with a real strike count (10–20+ per side), that's 10–20+ sequential REST calls purely for a live price refresh — exactly the polling pattern Rate Limits warns against. Resolve tokens once with get_option_chain, then subscribe to those tokens on the WebSocket feed instead of re-polling quotes per strike.

get_time_price_series — the two calling patterns

The script shows both an explicit-range call and a defaults-only call, and the difference matters:

CallBehavior
get_time_price_series(exchange, token, starttime, endtime, interval)Explicit epoch-second window and candle size in minutes — used by the 'v' branch above.
get_time_price_series(exchange, token)Omitting starttime/endtime/interval falls back to wrapper defaults (effectively "today, 1-minute candles") — used by the 't' branch.

The included helper get_time(time_string) converts a '%d-%m-%Y %H:%M:%S' string to epoch seconds via time.mktime — useful for building starttime/endtime without hand-computing timestamps. Note this uses local system time, not UTC/IST explicitly, so keep the host clock's timezone in mind (see the NTP note on TOTP Setup for a related clock-drift issue elsewhere in this API).

Best practices

  • Run these scripts against a small, disposable order (1 share, a liquid large-cap) before pointing any automation at your real strategy — they're unmodified from the repo and will place a live order the moment you hit p.
  • logging.basicConfig(level=logging.DEBUG) at the top of both scripts prints every raw HTTP request/response — keep this on while debugging, but turn it off (or route to a file) in anything long-running, since it will log your access token on every call.
  • The 'd' Daily MTM branch's unbounded while True loop is illustrative only — see Daily MTM for a bounded version with a recompute interval.
  • Both scripts call api.subscribe(...) from inside open_callback, never before — the socket has to confirm open before a subscribe frame is meaningful. Structure your own WebSocket code the same way; see Streaming Code Examples.
  • Never hardcode secret_key in the login block as shown for readability above — load it from an environment variable, matching the pattern warned about on Manual Login (OAuth).

Notes

Source: example_orders.py and example_market.py on GitHub. This fork swaps the TOTP/password login block used by other Shoonya SDK forks for getOAuthURLgetAccessTokeninjectOAuthHeader — see Manual Login (OAuth) for the underlying HTTP flow this wraps.