
Dhanhq
- 718 installs
- 28 repo stars
- Updated June 30, 2026
- dhan-oss/dhanhq-skills
Place and manage stock, F&O, and commodity orders and fetch market data and option chains on Indian exchanges via the DhanHQ Python SDK.
About
Wraps the DhanHQ Python SDK to trade on Indian exchanges (NSE, BSE, MCX), covering order placement, portfolio and positions, market data, option chains with Greeks, and live WebSocket feeds. A developer uses it to build trading automation for Indian markets on Dhan.
- Place, modify, cancel orders and fetch holdings/positions on NSE/BSE/MCX
- Live and historical market data, option chains with Greeks, WebSocket feeds
Dhanhq by the numbers
- 718 all-time installs (skills.sh)
- Ranked #189 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dhan-oss/dhanhq-skills --skill dhanhqAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 718 |
|---|---|
| repo stars | ★ 28 |
| Last updated | June 30, 2026 |
| Repository | dhan-oss/dhanhq-skills ↗ |
What it does
Place and manage stock, F&O, and commodity orders and fetch market data and option chains on Indian exchanges via the DhanHQ Python SDK.
Files
DhanHQ — Indian Market Trading Skill
Setup
Stable install:
pip install dhanhqUse the current SDK branch when you need newer v2 capabilities such as 200-level depth or the latest helper coverage:
pip install --upgrade dhanhqMinimal initialization:
from dhanhq import DhanContext, dhanhq
dhan_context = DhanContext("YOUR_CLIENT_ID", "YOUR_ACCESS_TOKEN")
dhan = dhanhq(dhan_context)Environment-variable setup:
import os
from dhanhq import DhanContext, dhanhq
dhan_context = DhanContext(
os.environ["DHAN_CLIENT_ID"],
os.environ["DHAN_ACCESS_TOKEN"],
)
dhan = dhanhq(dhan_context)If generating scripts for this repo, prefer:
from scripts.dhan_helpers import get_client
dhan, dhan_context = get_client()Safety Rules — Always Enforce
1. Confirm before placing live orders. 2. Show a readable order preview before execution. 3. Default to LIMIT orders unless the user explicitly wants MARKET. 4. Warn when notional exceeds Rs. 50,000. 5. For F&O, validate lot size before placement. 6. Never use CNC or MTF for F&O, commodity, or currency segments. 7. Never hardcode credentials in generated code. 8. Ask for confirmation before modify_order, cancel_order, kill_switch, or any multi-leg live execution.
Access Checks Before Live Use
Before using the account for live work, verify:
1. Access token is valid. 2. dhan_login.user_profile(...) or GET /profile shows the needed account setup. 3. dataPlan is active for quote/history/feed/option-chain use. 4. Static IP is configured for order placement, order modification, order cancellation, super orders, and forever orders.
Useful profile fields:
tokenValidityactiveSegmentddpimtfdataPlandataValidity
Current SDK Constants
| Category | Constant | Value |
|---|---|---|
| Exchange | dhanhq.NSE | NSE_EQ |
dhanhq.BSE | BSE_EQ | |
dhanhq.NSE_FNO | NSE_FNO | |
dhanhq.BSE_FNO | BSE_FNO | |
dhanhq.MCX | MCX_COMM | |
dhanhq.CUR | NSE_CURRENCY | |
dhanhq.INDEX | IDX_I | |
| Transaction | dhanhq.BUY | BUY |
dhanhq.SELL | SELL | |
| Order Type | dhanhq.LIMIT | LIMIT |
dhanhq.MARKET | MARKET | |
dhanhq.SL | STOP_LOSS | |
dhanhq.SLM | STOP_LOSS_MARKET | |
| Product | dhanhq.CNC | CNC |
dhanhq.INTRA | INTRADAY | |
dhanhq.MARGIN | MARGIN | |
dhanhq.MTF | MTF | |
| Validity | dhanhq.DAY | DAY |
dhanhq.IOC | IOC |
Current SDK Methods To Prefer
| Task | Method |
|---|---|
| Place order | dhan.place_order() |
| Slice large order | dhan.place_slice_order() |
| Modify order | dhan.modify_order() |
| Cancel order | dhan.cancel_order() |
| Order book | dhan.get_order_list() |
| Order by ID | dhan.get_order_by_id() |
| Order by correlation ID | dhan.get_order_by_correlationID() |
| Trade book | dhan.get_trade_book() |
| Trade history | dhan.get_trade_history() |
| Ledger | dhan.ledger_report() |
| Super orders | place_super_order(), modify_super_order(), cancel_super_order(), get_super_order_list() |
| Forever orders | place_forever(), modify_forever(), cancel_forever(), get_forever() |
| Holdings | dhan.get_holdings() |
| Positions | dhan.get_positions() |
| Convert position | dhan.convert_position() |
| eDIS | dhan.generate_tpin(), dhan.open_browser_for_tpin(), dhan.edis_inquiry() |
| Fund limits | dhan.get_fund_limits() |
| Margin calculator | dhan.margin_calculator() |
| Daily history | dhan.historical_daily_data() |
| Minute history | dhan.intraday_minute_data() |
| Expired options data | dhan.expired_options_data() |
| Market quote snapshot | dhan.ticker_data(), dhan.ohlc_data(), dhan.quote_data() |
| Expiry list | dhan.expiry_list() |
| Option chain | dhan.option_chain() |
| Security master | dhanhq.fetch_security_list() |
| Live market feed | MarketFeed |
| Live order updates | OrderUpdate |
| Full market depth | FullDepth |
| Kill switch | dhan.kill_switch(), dhan.status_kill_switch() |
High-Value Gotchas
- The SDK wraps HTTP responses as
{"status": "success"|"failure", "remarks": ..., "data": ...}. Response shapes vary by endpoint — success payloads differ significantly (arrays, flat objects, nested dicts) depending on the API. - Repo helpers add a normalization layer. Fields like
ce_ltp,ce_oi,ce_ivare repo-defined names — not raw Dhan field names. intraday_minute_data(...)is the current SDK method. Do not referencehistorical_minute_data().- Historical timestamps are epoch values. Convert them explicitly.
- The SDK currently validates
expiry_codewith[0, 1, 2, 3], but Dhan's v2 annexure documents0,1,2. Prefer the documented values unless Dhan updates the API docs. - Quote APIs are rate-limited to
1 request/sec. - Option-chain REST data is keyed by strike string under
data["oc"]. Use repo helpers for analysis-friendly rows. - Market orders via API are currently converted by Dhan into limit orders with MPP.
- Order placement APIs require static IP whitelisting.
- Trading APIs are free for Dhan users; Data APIs require an active data plan.
- Lot sizes and freeze quantities change. Treat hardcoded values as fallback only.
Product-Type Rules
| Segment | Allowed Product Types |
|---|---|
NSE_EQ, BSE_EQ | CNC, INTRADAY, MARGIN, MTF |
NSE_FNO, BSE_FNO, MCX_COMM, NSE_CURRENCY, BSE_CURRENCY | INTRADAY, MARGIN |
Instrument Resolution Rules
Use the security master as the primary source for:
security_idlot_sizetick_size- expiry
- strike
- derivative contract lookup
Quick-reference index underlyings:
| Underlying | security_id | Underlying Segment |
|---|---|---|
| NIFTY 50 | 13 | IDX_I |
| BANK NIFTY | 25 | IDX_I |
| FINNIFTY | 27 | IDX_I |
| MIDCPNIFTY | 442 | IDX_I |
| SENSEX | 51 | IDX_I |
Preferred Helper Layer
When generating scripts in this repo, prefer:
get_client()for SDK bootstrappingresolve_symbol()for cash-market lookupresolve_derivative()for contract lookupfetch_chain_df()for option-chain normalizationfind_atm_row()for ATM selectioncheck_margin()for pre-flight margin checkspreview_order()for readable confirmation
Core Patterns
1. Check account access before data calls
from dhanhq import DhanLogin
dhan_login = DhanLogin("YOUR_CLIENT_ID")
profile = dhan_login.user_profile("YOUR_ACCESS_TOKEN")
print(profile["dataPlan"])
print(profile["dataValidity"])2. Fetch historical data with epoch conversion
data = dhan.historical_daily_data(
security_id="2885",
exchange_segment=dhanhq.NSE,
instrument_type="EQUITY",
from_date="2024-01-01",
to_date="2024-12-31",
)
if data["status"] == "success":
candles = data["data"]
timestamps = [dhan.convert_to_date_time(ts) for ts in candles["timestamp"]]3. Normalize option-chain data for analysis
from scripts.dhan_helpers import fetch_chain_df, find_atm_row
chain_df, spot = fetch_chain_df(dhan, under_security_id=13, expiry="2025-03-27")
atm = find_atm_row(chain_df, spot)
print(spot)
print(atm["strike"])
print(atm["ce_security_id"], atm["ce_ltp"])4. Margin check before live order placement
from scripts.dhan_helpers import check_margin
margin = check_margin(
dhan,
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=10,
product_type=dhanhq.CNC,
price=2450.0,
)
print(margin["sufficient"], margin["total_margin"], margin["available_balance"])5. Live market feed
from dhanhq import MarketFeed
instruments = [
(MarketFeed.NSE, "2885", MarketFeed.Ticker),
(MarketFeed.NSE_FNO, "49081", MarketFeed.Full),
]
feed = MarketFeed(dhan_context, instruments, "v2")
feed.run_forever()
print(feed.get_data())Rate Limits
| API Category | Per Second | Per Minute | Per Hour | Per Day |
|---|---|---|---|---|
| Order APIs | 10 | 250 | 1000 | 7000 |
| Data APIs | 5 | - | - | 100000 |
| Quote APIs | 1 | Unlimited | Unlimited | Unlimited |
| Non-Trading APIs | 20 | Unlimited | Unlimited | Unlimited |
Reference Files
Dhan APIs cover execution, quotes, OHLC, option chain, and portfolio. For fundamental data (PE, EPS, revenue), technical indicators (RSI, MACD), or shareholding patterns not available via Dhan, use ScanX — see references/scanx-data.md.
| Need | File |
|---|---|
| Orders, super orders, forever orders | references/orders.md |
| Holdings, positions, eDIS | references/portfolio.md |
| Daily/minute history, quotes, expired options | references/market-data.md |
| Option-chain usage and normalization | references/option-chain.md |
| Fund limits and margin checks | references/funds.md |
| Live feeds and depth | references/live-feed.md |
| Error handling and subscription troubleshooting | references/error-codes.md |
| Instrument resolution | references/instruments.md |
| Multi-step execution patterns | references/common-workflows.md |
| Options analytics | references/options-analysis-patterns.md |
| Backtesting patterns | references/backtesting-with-dhan.md |
| PE ratio, RSI, financials, screeners — data Dhan does not provide | references/scanx-data.md |
Data API Subscription Invalid
If the user gets DH-902 or 806:
1. Log in to web.dhan.co 2. Open My Profile -> Access DhanHQ APIs 3. Verify that dataPlan is active 4. Activate the Data API plan if needed 5. Generate a fresh access token 6. Re-test with ticker_data() or ohlc_data() 7. If order APIs still fail, check static IP separately
"""Fetch and display the Nifty option chain using the repo helper layer."""
from scripts.dhan_helpers import fetch_chain_df, find_atm_row, get_client
dhan, _ = get_client()
expiries = dhan.expiry_list(under_security_id=13, under_exchange_segment="IDX_I")
nearest_expiry = expiries["data"][0]
print(f"Using expiry: {nearest_expiry}")
chain_df, spot = fetch_chain_df(dhan, under_security_id=13, expiry=nearest_expiry)
atm = find_atm_row(chain_df, spot)
print(f"Nifty Spot: {spot}")
print(f"ATM Strike: {atm['strike']}")
view = chain_df[
["strike", "ce_ltp", "ce_oi", "ce_iv", "pe_ltp", "pe_oi", "pe_iv"]
].copy()
nearby = view[(view["strike"] >= atm["strike"] - 500) & (view["strike"] <= atm["strike"] + 500)]
print("\nOption Chain (ATM ± 500 points):\n")
print(nearby.to_string(index=False))
"""Place GTT (Good Till Triggered) Forever Orders via DhanHQ.
Demonstrates:
- Single trigger GTT (buy on dip)
- OCO (One Cancels Other) for target + stop loss
- Listing and cancelling forever orders
"""
from dhanhq import dhanhq
from scripts.dhan_helpers import get_client
dhan, _ = get_client()
# Example 1: Single GTT — Buy Reliance if it dips to ₹2300
print("--- GTT Single: Buy RELIANCE on dip ---")
# response = dhan.place_forever(
# security_id="2885",
# exchange_segment=dhanhq.NSE,
# transaction_type=dhanhq.BUY,
# product_type=dhanhq.CNC, # Equity delivery
# order_type=dhanhq.LIMIT,
# quantity=5,
# price=2300.00, # Limit price
# trigger_Price=2305.00, # Trigger price (note: capital P)
# order_flag="SINGLE",
# validity=dhanhq.DAY,
# tag="gtt_buy_dip",
# )
# print(f"GTT placed: {response}")
# Example 2: OCO — Sell Reliance at ₹2700 (target) OR ₹2200 (stop loss)
print("\n--- GTT OCO: Target + Stop Loss for RELIANCE holding ---")
# response = dhan.place_forever(
# security_id="2885",
# exchange_segment=dhanhq.NSE,
# transaction_type=dhanhq.SELL,
# product_type=dhanhq.CNC, # Selling from holdings
# order_type=dhanhq.LIMIT,
# quantity=5,
# price=2700.00, # Target price
# trigger_Price=2695.00, # Target trigger (capital P!)
# price1=2200.00, # Stop loss price
# trigger_Price1=2205.00, # Stop loss trigger (capital P!)
# order_flag="OCO", # One Cancels Other
# validity=dhanhq.DAY,
# )
# print(f"OCO placed: {response}")
# Example 3: List all active forever orders
print("\n--- Active Forever Orders ---")
forever_orders = dhan.get_forever()
if forever_orders["status"] == "success" and forever_orders["data"]:
for order in forever_orders["data"]:
print(f" ID: {order.get('orderId', 'N/A')} | "
f"{order.get('tradingSymbol', 'N/A')} | "
f"Type: {order.get('orderFlag', 'N/A')} | "
f"Trigger: ₹{order.get('triggerPrice', 'N/A')}")
else:
print(" No active forever orders")
# Example 4: Cancel a forever order
# dhan.cancel_forever(order_id="YOUR_ORDER_ID")
"""Fetch historical OHLCV data from DhanHQ and perform basic analysis."""
from datetime import datetime, timedelta
import pandas as pd
from scripts.dhan_helpers import get_client
dhan, _ = get_client()
to_date = datetime.now().strftime("%Y-%m-%d")
from_date = (datetime.now() - timedelta(days=180)).strftime("%Y-%m-%d")
response = dhan.historical_daily_data(
security_id="2885",
exchange_segment="NSE_EQ",
instrument_type="EQUITY",
from_date=from_date,
to_date=to_date,
)
if response["status"] != "success":
raise SystemExit(response["remarks"])
df = pd.DataFrame(response["data"])
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s", utc=True).dt.tz_convert("Asia/Kolkata")
df.set_index("timestamp", inplace=True)
df["SMA_20"] = df["close"].rolling(20).mean()
df["SMA_50"] = df["close"].rolling(50).mean()
df["returns"] = df["close"].pct_change()
print("=== RELIANCE — Last 6 Months ===\n")
print(f"Period: {df.index[0].date()} to {df.index[-1].date()}")
print(f"Trading Days: {len(df)}")
print(f"Start Price: Rs. {df['close'].iloc[0]:,.2f}")
print(f"End Price: Rs. {df['close'].iloc[-1]:,.2f}")
print(f"High: Rs. {df['high'].max():,.2f}")
print(f"Low: Rs. {df['low'].min():,.2f}")
print(f"Total Return: {(df['close'].iloc[-1] / df['close'].iloc[0] - 1):.2%}")
print(f"Avg Daily Vol: {df['volume'].mean():,.0f}")
print(f"Volatility: {df['returns'].std() * (252 ** 0.5):.2%} (annualized)")
latest = df.iloc[-1]
print(f"\nSMA 20: Rs. {latest['SMA_20']:,.2f}")
print(f"SMA 50: Rs. {latest['SMA_50']:,.2f}")
print("Signal: Bullish (SMA 20 > SMA 50)" if latest["SMA_20"] > latest["SMA_50"] else "Signal: Bearish (SMA 20 < SMA 50)")
"""Build and analyze a Nifty iron condor from normalized option-chain data."""
import numpy as np
from scripts.dhan_helpers import fetch_chain_df, get_client
dhan, _ = get_client()
expiries = dhan.expiry_list(under_security_id=13, under_exchange_segment="IDX_I")
nearest_expiry = expiries["data"][0]
chain_df, spot = fetch_chain_df(dhan, under_security_id=13, expiry=nearest_expiry)
print(f"Nifty Spot: {spot}, Expiry: {nearest_expiry}")
strike_prices = sorted(chain_df["strike"].tolist())
sell_ce_strike = min(strike_prices, key=lambda x: abs(x - (spot + 200)))
buy_ce_strike = sell_ce_strike + 200
sell_pe_strike = min(strike_prices, key=lambda x: abs(x - (spot - 200)))
buy_pe_strike = sell_pe_strike - 200
def get_row(target_strike):
match = chain_df[chain_df["strike"] == target_strike]
return None if match.empty else match.iloc[0]
sell_ce = get_row(sell_ce_strike)
buy_ce = get_row(buy_ce_strike)
sell_pe = get_row(sell_pe_strike)
buy_pe = get_row(buy_pe_strike)
if any(row is None for row in [sell_ce, buy_ce, sell_pe, buy_pe]):
raise SystemExit("Could not find all required strikes. Try different offsets.")
lot_size = 65 # fallback — prefer get_lot_size("NIFTY") from security master
legs = [
{
"label": f"Sell {int(sell_pe_strike)} PE",
"type": "PE",
"strike": sell_pe_strike,
"premium": float(sell_pe["pe_ltp"]),
"qty": -1,
"sid": sell_pe["pe_security_id"],
},
{
"label": f"Buy {int(buy_pe_strike)} PE",
"type": "PE",
"strike": buy_pe_strike,
"premium": float(buy_pe["pe_ltp"]),
"qty": 1,
"sid": buy_pe["pe_security_id"],
},
{
"label": f"Sell {int(sell_ce_strike)} CE",
"type": "CE",
"strike": sell_ce_strike,
"premium": float(sell_ce["ce_ltp"]),
"qty": -1,
"sid": sell_ce["ce_security_id"],
},
{
"label": f"Buy {int(buy_ce_strike)} CE",
"type": "CE",
"strike": buy_ce_strike,
"premium": float(buy_ce["ce_ltp"]),
"qty": 1,
"sid": buy_ce["ce_security_id"],
},
]
net_premium = sum(-leg["qty"] * leg["premium"] for leg in legs)
spot_range = np.arange(spot - 1000, spot + 1000, 10)
payoff = np.zeros_like(spot_range, dtype=float)
for leg in legs:
if leg["type"] == "CE":
intrinsic = np.maximum(spot_range - leg["strike"], 0)
else:
intrinsic = np.maximum(leg["strike"] - spot_range, 0)
payoff += (intrinsic - leg["premium"]) * leg["qty"] * lot_size
max_profit = payoff.max()
max_loss = payoff.min()
sign_changes = np.where(np.diff(np.sign(payoff)))[0]
breakevens = spot_range[sign_changes]
print(f"\n{'=' * 55}")
print(f" NIFTY IRON CONDOR — Expiry: {nearest_expiry}")
print(f"{'=' * 55}")
print("\n Legs:")
for leg in legs:
action = "SELL" if leg["qty"] < 0 else "BUY "
print(f" {action} 1 lot {leg['label']} @ Rs. {leg['premium']:.1f}")
print(f"\n Analysis (1 lot = {lot_size} qty):")
print(f" Net Premium: Rs. {net_premium * lot_size:>8,.0f} ({'credit' if net_premium > 0 else 'debit'})")
print(f" Max Profit: Rs. {max_profit:>8,.0f}")
print(f" Max Loss: Rs. {max_loss:>8,.0f}")
print(f" Breakevens: {', '.join(f'{b:.0f}' for b in breakevens)}")
print(f" Risk/Reward: 1:{abs(max_profit / max_loss):.1f}" if max_loss != 0 else "")
print("\n Orders to place after confirmation:")
for leg in legs:
action = "SELL" if leg["qty"] < 0 else "BUY"
print(f" {action} {lot_size} qty | SID: {leg['sid']} | Rs. {leg['premium']:.1f}")
"""Set up a live market data feed using DhanHQ WebSocket."""
from dhanhq import MarketFeed
from scripts.dhan_helpers import get_client
_, dhan_context = get_client()
# Define instruments to subscribe
# Format: (exchange_segment, security_id, subscription_mode)
instruments = [
(MarketFeed.NSE, "2885", MarketFeed.Ticker), # RELIANCE — LTP only
(MarketFeed.NSE, "1333", MarketFeed.Quote), # HDFCBANK — OHLC + Volume
(MarketFeed.NSE, "11536", MarketFeed.Full), # TCS — Full packet
]
def on_connect(instance):
"""Called when WebSocket connection is established."""
print("Connected to DhanHQ MarketFeed")
def on_message(instance, message):
"""Called on every tick update."""
print(f"Tick: {message}")
def on_close(instance):
"""Called when WebSocket connection is closed."""
print("Disconnected")
# Create and start the feed
feed = MarketFeed(
dhan_context,
instruments,
"v2",
on_connect=on_connect,
on_message=on_message,
on_close=on_close,
)
print("Starting live market feed... (Ctrl+C to stop)")
try:
feed.run_forever()
except KeyboardInterrupt:
feed.close_connection()
print("\nFeed stopped.")
"""Check margin requirements before placing an order via DhanHQ."""
from dhanhq import dhanhq
from scripts.dhan_helpers import check_margin, fetch_chain_df, find_atm_row, get_client
dhan, _ = get_client()
funds = dhan.get_fund_limits()
available = funds["data"]["availabelBalance"]
print(f"Available Balance: Rs. {available:,.2f}")
expiries = dhan.expiry_list(under_security_id=13, under_exchange_segment="IDX_I")
nearest_expiry = expiries["data"][0]
chain_df, spot = fetch_chain_df(dhan, under_security_id=13, expiry=nearest_expiry)
atm = find_atm_row(chain_df, spot)
print("\n--- Margin Check: Buy 1 Lot Nifty CE (INTRADAY) ---")
option_margin = check_margin(
dhan,
security_id=atm["ce_security_id"],
exchange_segment=dhanhq.NSE_FNO,
transaction_type=dhanhq.BUY,
quantity=75,
product_type=dhanhq.INTRA,
price=float(atm["ce_ltp"]),
)
print(option_margin)
print("\n--- Margin Check: Buy 10 RELIANCE (CNC Delivery) ---")
equity_margin = check_margin(
dhan,
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=10,
product_type=dhanhq.CNC,
price=2450.0,
)
print(equity_margin)
"""Complete order lifecycle: place, monitor, modify, cancel via DhanHQ.
Demonstrates:
- Placing an order
- Checking order status
- Modifying a pending order
- Cancelling an order
- Viewing the order book and trade book
"""
import time
import os
from dhanhq import dhanhq
from scripts.dhan_helpers import get_client, preview_order
dhan, _ = get_client()
security_id = "2885"
price = 2000.0
quantity = 1
print(
preview_order(
security_id=security_id,
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=quantity,
order_type=dhanhq.LIMIT,
product_type=dhanhq.CNC,
price=price,
trading_symbol="RELIANCE",
)
)
if os.environ.get("RUN_LIVE_EXAMPLE") != "1":
raise SystemExit("Set RUN_LIVE_EXAMPLE=1 to place, modify, and cancel a live demo order.")
# Step 1: Place a limit order (well below market for demo — won't fill)
print("Step 1: Placing limit buy order for RELIANCE...")
response = dhan.place_order(
security_id=security_id,
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=quantity,
order_type=dhanhq.LIMIT,
product_type=dhanhq.CNC,
price=price, # Below market — will stay pending
validity=dhanhq.DAY,
tag="lifecycle_demo",
)
if response["status"] != "success":
raise SystemExit(f"Order failed: {response['remarks']}")
order_id = response["data"]["orderId"]
print(f"Order placed: {order_id}")
# Step 2: Check order status
print("\nStep 2: Checking order status...")
time.sleep(1)
order = dhan.get_order_by_id(order_id=order_id)
status = order["data"]["orderStatus"]
print(f"Status: {status}")
print(f" Security: {order['data'].get('tradingSymbol', 'N/A')}")
print(f" Qty: {order['data']['quantity']}")
print(f" Price: ₹{order['data']['price']}")
print(f" Filled: {order['data'].get('filledQty', 0)}")
# Step 3: Modify the order (change price)
if status == "PENDING":
print("\nStep 3: Modifying order price to ₹2050...")
mod_response = dhan.modify_order(
order_id=order_id,
order_type=dhanhq.LIMIT,
leg_name=None, # None for regular orders
quantity=quantity,
price=2050.00,
trigger_price=0,
disclosed_quantity=0,
validity=dhanhq.DAY,
)
print(f"Modify result: {mod_response['status']}")
# Step 4: Cancel the order
print("\nStep 4: Cancelling order...")
cancel_response = dhan.cancel_order(order_id=order_id)
print(f"Cancel result: {cancel_response['status']}")
# Step 5: View order book
print("\nStep 5: Today's order book:")
orders = dhan.get_order_list()
if orders["data"]:
for o in orders["data"][-5:]: # Last 5 orders
print(f" {o.get('orderId', 'N/A')[:12]} | "
f"{o.get('tradingSymbol', 'N/A'):>12} | "
f"{o.get('transactionType', ''):>4} | "
f"{o.get('orderStatus', 'N/A'):>12} | "
f"₹{o.get('price', 0):>8.2f}")
# Step 6: View trade book
print("\nStep 6: Today's trade book:")
trades = dhan.get_trade_book()
if trades["data"]:
for t in trades["data"][-5:]:
print(f" {t.get('tradingSymbol', 'N/A'):>12} | "
f"{t.get('transactionType', ''):>4} | "
f"Qty: {t.get('tradedQuantity', 0):>5} | "
f"₹{t.get('tradedPrice', 0):>8.2f}")
else:
print(" No trades today")
"""Prepare a simple equity delivery order on NSE via DhanHQ."""
from dhanhq import dhanhq
from scripts.dhan_helpers import get_client, preview_order
dhan, _ = get_client()
security_id = "2885" # RELIANCE
price = 2450.0
quantity = 1
print(
preview_order(
security_id=security_id,
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=quantity,
order_type=dhanhq.LIMIT,
product_type=dhanhq.CNC,
price=price,
trading_symbol="RELIANCE",
)
)
# Uncomment after confirmation:
# response = dhan.place_order(
# security_id=security_id,
# exchange_segment=dhanhq.NSE,
# transaction_type=dhanhq.BUY,
# quantity=quantity,
# order_type=dhanhq.LIMIT,
# product_type=dhanhq.CNC,
# price=price,
# validity=dhanhq.DAY,
# tag="example_order",
# )
# print(response)
"""Prepare a Nifty option order using current option-chain data."""
from dhanhq import dhanhq
from scripts.dhan_helpers import (
check_margin,
fetch_chain_df,
find_atm_row,
get_client,
get_lot_size,
preview_order,
)
dhan, _ = get_client()
expiries = dhan.expiry_list(under_security_id=13, under_exchange_segment="IDX_I")
nearest_expiry = expiries["data"][0]
print(f"Nearest expiry: {nearest_expiry}")
chain_df, spot = fetch_chain_df(dhan, under_security_id=13, expiry=nearest_expiry)
atm = find_atm_row(chain_df, spot)
ce_security_id = atm["ce_security_id"]
ce_ltp = float(atm["ce_ltp"])
lot_size = get_lot_size(underlying="NIFTY") or 75
quantity = lot_size
print(f"Nifty spot: {spot}")
print(f"ATM strike: {atm['strike']}")
print(f"CE security ID: {ce_security_id}, LTP: Rs. {ce_ltp:,.2f}")
print()
print(
preview_order(
security_id=ce_security_id,
exchange_segment=dhanhq.NSE_FNO,
transaction_type=dhanhq.BUY,
quantity=quantity,
order_type=dhanhq.LIMIT,
product_type=dhanhq.INTRA,
price=ce_ltp,
trading_symbol=f"NIFTY {int(atm['strike'])} CE",
)
)
margin = check_margin(
dhan,
security_id=ce_security_id,
exchange_segment=dhanhq.NSE_FNO,
transaction_type=dhanhq.BUY,
quantity=quantity,
product_type=dhanhq.INTRA,
price=ce_ltp,
)
print(
f"Margin check: sufficient={margin['sufficient']} "
f"required=Rs. {margin['total_margin']:,.2f} "
f"available=Rs. {margin['available_balance']:,.2f}"
)
# Uncomment only after confirmation:
# response = dhan.place_order(
# security_id=ce_security_id,
# exchange_segment=dhanhq.NSE_FNO,
# transaction_type=dhanhq.BUY,
# quantity=quantity,
# order_type=dhanhq.LIMIT,
# product_type=dhanhq.INTRA,
# price=ce_ltp,
# validity=dhanhq.DAY,
# )
# print(response)
"""Fetch and display a portfolio summary from DhanHQ."""
from scripts.dhan_helpers import format_pnl_report, get_client
dhan, _ = get_client()
holdings_resp = dhan.get_holdings()
positions_resp = dhan.get_positions()
funds_resp = dhan.get_fund_limits()
trades_resp = dhan.get_trade_book()
if not all(resp["status"] == "success" for resp in [holdings_resp, positions_resp, funds_resp, trades_resp]):
raise SystemExit("One or more portfolio calls failed.")
holdings = holdings_resp["data"]
positions = positions_resp["data"]
funds = funds_resp["data"]
trades = trades_resp["data"]
summary = format_pnl_report(holdings_resp, positions_resp)
print("=" * 50)
print(" PORTFOLIO SUMMARY")
print("=" * 50)
print(f"\nHoldings count: {summary['holdings_count']}")
print(f"Positions count: {summary['positions_count']}")
print(f"Current value: Rs. {summary['current_value']:>12,.2f}")
print(f"Total P&L: Rs. {summary['total_pnl']:>12,.2f}")
print(f"Day P&L: Rs. {summary['day_pnl']:>12,.2f}")
print("\nFUNDS")
print(f" Available: Rs. {funds['availabelBalance']:>12,.2f}")
print(f" Utilized: Rs. {funds['utilizedAmount']:>12,.2f}")
print(f" Collateral: Rs. {funds['collateralAmount']:>12,.2f}")
print(f" Withdrawable: Rs. {funds['withdrawableBalance']:>12,.2f}")
if holdings:
print("\nTOP HOLDINGS")
for holding in sorted(holdings, key=lambda row: row.get("totalQty", 0), reverse=True)[:5]:
print(
f" {holding['tradingSymbol']:<15} "
f"qty={holding['totalQty']:>5} "
f"available={holding['availableQty']:>5}"
)
open_positions = [row for row in positions if row["netQty"] != 0]
if open_positions:
print("\nOPEN POSITIONS")
for position in open_positions[:5]:
pnl = position.get("realizedProfit", 0) + position.get("unrealizedProfit", 0)
print(
f" {position['tradingSymbol']:<20} "
f"netQty={position['netQty']:>5} "
f"pnl=Rs. {pnl:>8,.0f}"
)
print(f"\nTrades today: {len(trades)}")
print("=" * 50)
"""Prepare a super order with target and trailing stop loss."""
from dhanhq import OrderUpdate, dhanhq
from scripts.dhan_helpers import get_client
dhan, dhan_context = get_client()
ltp_data = dhan.ticker_data({"NSE_EQ": [2885]})
if ltp_data["status"] != "success":
raise SystemExit(ltp_data["remarks"])
reliance_ltp = float(ltp_data["data"]["NSE_EQ"]["2885"]["last_price"])
print(f"Reliance LTP: Rs. {reliance_ltp:,.2f}")
entry_price = reliance_ltp
target_price = round(entry_price * 1.02, 2)
sl_price = round(entry_price * 0.99, 2)
trailing_jump = 5.0
print("\n--- Super Order Preview ---")
print("Action: BUY 1 share of RELIANCE")
print(f"Entry Price: Rs. {entry_price:,.2f}")
print(f"Target: Rs. {target_price:,.2f}")
print(f"Stop Loss: Rs. {sl_price:,.2f}")
print(f"Trailing Jump: Rs. {trailing_jump:,.2f}")
print("Product: INTRADAY")
# Uncomment after confirmation:
# response = dhan.place_super_order(
# security_id="2885",
# exchange_segment=dhanhq.NSE,
# transaction_type=dhanhq.BUY,
# quantity=1,
# order_type=dhanhq.LIMIT,
# product_type=dhanhq.INTRA,
# price=entry_price,
# targetPrice=target_price,
# stopLossPrice=sl_price,
# trailingJump=trailing_jump,
# tag="super_example",
# )
#
# if response["status"] == "success":
# print(response["data"])
#
# def on_update(data):
# print(f"Update: {data}")
#
# order_ws = OrderUpdate(dhan_context)
# order_ws.on_update = on_update
# order_ws.connect_to_dhan_websocket_sync()
Backtesting With Dhan Data
Daily Equity Backtest Skeleton
import pandas as pd
response = dhan.historical_daily_data(
security_id="2885",
exchange_segment=dhanhq.NSE,
instrument_type="EQUITY",
from_date="2023-01-01",
to_date="2024-12-31",
)
if response["status"] == "success":
data = response["data"]
df = pd.DataFrame(data)
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s", utc=True).dt.tz_convert("Asia/Kolkata")
df.set_index("timestamp", inplace=True)Typical next steps:
- create signals
- shift positions to avoid look-ahead bias
- apply transaction costs
- compute CAGR / drawdown / Sharpe / win rate
Minute-Level Backtest Skeleton
response = dhan.intraday_minute_data(
security_id="2885",
exchange_segment=dhanhq.NSE,
instrument_type="EQUITY",
from_date="2024-09-11 09:30:00",
to_date="2024-09-15 13:00:00",
interval=5,
oi=False,
)
if response["status"] == "success":
minute_data = response["data"]Practical note:
- current v2 API docs describe minute data for active instruments over up to last 5 years
Expired Options Backtest Skeleton
response = dhan.expired_options_data(
security_id=13,
exchange_segment=dhanhq.NSE_FNO,
instrument_type="OPTIDX",
expiry_flag="MONTH",
expiry_code=1,
strike="ATM",
drv_option_type="CALL",
required_data=["open", "high", "low", "close", "volume", "oi", "spot"],
from_date="2021-08-01",
to_date="2021-08-31",
interval=1,
)
if response["status"] == "success":
ce = response["data"]["ce"]Current raw output shape:
- arrays live under
response["data"]["ce"]orresponse["data"]["pe"] - timestamps are epoch integers
Cost Model Reminders
At minimum consider:
- brokerage
- STT
- transaction charges
- GST
- stamp duty
- SEBI charges
- slippage
Use a single cost function and keep it explicit in the notebook/script.
Guidance
- Do not assume timestamps are ISO strings.
- Do not mix raw option-chain parsing with expired-options data shapes.
- For derivative strategies, separate:
- live contract discovery
- historical rolling data analysis
- Paper trade before live deployment.
Common Workflows — Agent Playbooks
Portfolio Rebalance
Recommended sequence:
1. Fetch holdings and funds 2. Compute target deltas 3. Resolve symbols and quantities 4. Preview all proposed orders 5. Confirm with the user 6. Place live orders only after confirmation
Skeleton:
holdings_resp = dhan.get_holdings()
funds_resp = dhan.get_fund_limits()
if holdings_resp["status"] == "success" and funds_resp["status"] == "success":
holdings = holdings_resp["data"]
funds = funds_resp["data"]
available_cash = funds["availabelBalance"]Guardrails:
- do not assume all holdings are NSE equities
- use current market data or a user-provided limit price
- do not submit all orders blindly without a preview
Delivery Sell With eDIS
Use this flow for selling demat holdings:
1. Fetch holdings and identify ISIN 2. Generate TPIN 3. Open the authorization form 4. Check edis_inquiry(isin) 5. Only after approval, place the sell order
Skeleton:
dhan.generate_tpin()
dhan.open_browser_for_tpin(isin="INE002A01018", qty=5, exchange="NSE")
status = dhan.edis_inquiry("INE002A01018")Do not use this flow for:
- intraday equity sells
- F&O
- commodity
- currency
Single-Leg F&O Execution
Recommended sequence:
1. Resolve current contract from option chain or security master 2. Resolve lot size 3. Validate quantity 4. Check margin 5. Preview 6. Confirm 7. Place live order
Skeleton:
from scripts.dhan_helpers import fetch_chain_df, find_atm_row, check_margin
chain_df, spot = fetch_chain_df(dhan, 13, "2025-03-27")
atm = find_atm_row(chain_df, spot)
margin = check_margin(
dhan,
security_id=atm["ce_security_id"],
exchange_segment=dhanhq.NSE_FNO,
transaction_type=dhanhq.BUY,
quantity=75,
product_type=dhanhq.INTRA,
price=float(atm["ce_ltp"]),
)Multi-Leg Option Strategy
Recommended sequence:
1. Fetch option chain 2. Normalize with fetch_chain_df() 3. Build the strategy legs 4. Check live contract IDs and lot sizes 5. Check margin impact 6. Preview the complete basket 7. Confirm 8. Place buy-protection legs first where relevant 9. Monitor fills with OrderUpdate
Practical rules:
- use normalized helper output, not raw
ocparsing in every script - never hardcode current derivative security IDs
- for naked-risk strategies, be explicit about user confirmation
Daily P&L Summary
Recommended sequence:
1. Fetch holdings 2. Fetch positions 3. Fetch funds 4. Aggregate P&L and capital snapshot 5. Present a concise summary
Skeleton:
from scripts.dhan_helpers import format_pnl_report
holdings_resp = dhan.get_holdings()
positions_resp = dhan.get_positions()
summary = format_pnl_report(holdings_resp, positions_resp)Data API Subscription Invalid
Do not re-explain the full flow here.
Point the user to:
references/error-codes.md
Minimal workflow: 1. check dataPlan 2. activate data subscription if needed 3. refresh token 4. retry a simple data endpoint
Error Codes — Complete Reference
SDK note:
- on HTTP failure, the current Python SDK maps raw Dhan error payloads into:
{
"status": "failure",
"remarks": {
"error_code": "...",
"error_type": "...",
"error_message": "..."
},
"data": ""
}- on success,
response["data"]contains the raw endpoint payload.
Trading API Errors
From the current v2 annexure:
| Type | Code | Meaning |
|---|---|---|
| Invalid Authentication | DH-901 | Client ID or access token is invalid or expired |
| Invalid Access | DH-902 | User does not have required Data API or Trading API access |
| User Account | DH-903 | Account setup issue, segment activation, or related account requirement |
| Rate Limit | DH-904 | Rate limit exceeded |
| Input Exception | DH-905 | Missing or invalid request fields |
| Order Error | DH-906 | Order request cannot be processed |
| Data Error | DH-907 | Data unavailable or parameters invalid |
| Internal Server Error | DH-908 | Server-side failure |
| Network Error | DH-909 | Backend communication failure |
| Others | DH-910 | Other failure reason |
| Invalid IP | DH-911 | Static IP invalid or not whitelisted |
Data API Errors
From the current v2 annexure:
| Code | Meaning |
|---|---|
800 | Internal Server Error |
804 | Requested number of instruments exceeds limit |
805 | Too many requests or connections |
806 | Data APIs not subscribed |
807 | Access token is expired |
808 | Authentication failed - client ID or access token invalid |
809 | Access token is invalid |
810 | Client ID is invalid |
811 | Invalid expiry date |
812 | Invalid date format |
813 | Invalid security ID |
814 | Invalid request |
Decision Tree
If the error is 807, 808, 809, or 810
Fix authentication first:
- refresh or regenerate token
- verify client ID
- retry only after credentials are corrected
If the error is 806 or DH-902
Treat it as access/subscription first:
- check
dataPlan - check
dataValidity - verify the account has the right API access
If the error is DH-911
Treat it as a trading infrastructure issue:
- check static IP setup
- verify the request is coming from the whitelisted IP
If the error is DH-905 or DH-906
Treat it as a request-shape or order-validation problem:
- product type
- lot size
- trigger/price fields
- segment
- security ID
Data API Subscription Invalid — User Playbook
When the user gets DH-902 or 806, do this:
1. Log in to web.dhan.co 2. Open My Profile -> Access DhanHQ APIs 3. Check whether dataPlan is active 4. If not active, subscribe/activate the Data API plan 5. Generate a fresh access token 6. Verify dataValidity 7. Re-test a simple snapshot call such as ticker_data() or ohlc_data()
If order APIs still fail after that:
- this is a separate issue
- check static IP setup next
Retry Guidance
Safe to retry automatically:
DH-904800805- rare transient
DH-908/DH-909
Do not blindly retry:
DH-901DH-902DH-905DH-906DH-911806807808809810811812813814
Rate Limits
Current documented rate limits:
| API Category | Per Second | Per Minute | Per Hour | Per Day |
|---|---|---|---|---|
| Order APIs | 10 | 250 | 1000 | 7000 |
| Data APIs | 5 | - | - | 100000 |
| Quote APIs | 1 | Unlimited | Unlimited | Unlimited |
| Non-Trading APIs | 20 | Unlimited | Unlimited | Unlimited |
Practical Rule
Never use the error code alone without endpoint context.
Examples:
805on WebSocket can mean too many live connections805on data calls can mean request throttlingDH-902can surface when the user expects quotes/history but only has trading access
Funds & Margin — Complete Reference
The installed dhanhq SDK exposes get_fund_limits() and single-order margin_calculator().
Fund Limits
SDK method:
response = dhan.get_fund_limits()Example:
if response["status"] == "success":
funds = response["data"]
print(funds["availabelBalance"])
print(funds["utilizedAmount"])Current fund-limit fields:
dhanClientIdavailabelBalancesodLimitcollateralAmountreceiveableAmountutilizedAmountblockedPayoutAmountwithdrawableBalance
Important:
availabelBalanceis Dhan's actual field spelling.
Margin Calculator — Single Order
SDK signature:
dhan.margin_calculator(
security_id,
exchange_segment,
transaction_type,
quantity,
product_type,
price,
trigger_price=0,
)Example:
response = dhan.margin_calculator(
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=10,
product_type=dhanhq.CNC,
price=2450.0,
trigger_price=0,
)
if response["status"] == "success":
margin = response["data"]
print(margin["totalMargin"])
print(margin["availableBalance"])
print(margin["brokerage"])Current single-order margin fields:
totalMarginspanMarginexposureMarginavailableBalancevariableMargininsufficientBalancebrokerageleverage
Multi-Order Margin
Current v2 REST supports:
POST /margincalculator/multi
Current installed SDK status:
- the local
dhanhq2.2.0 install does not expose a first-classmargin_calculator_multi()method
Practical guidance:
- if you only need a pre-trade check, use the single-order SDK method
- if you need true portfolio-style multi-leg margin from this repo, either:
- call the raw REST endpoint directly, or
- extend the SDK wrapper in a controlled way
Do not document or generate calls to margin_calculator_multi() unless you add that wrapper explicitly.
Recommended Agent Pattern
Use the repo helper:
from scripts.dhan_helpers import check_margin
margin = check_margin(
dhan,
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=10,
product_type=dhanhq.CNC,
price=2450.0,
)
if margin["sufficient"]:
print("Order can be funded")
else:
print("Insufficient balance", margin["shortfall"])Derivative Margin Notes
- Resolve lot size from the security master or current contract metadata.
- Do not hardcode lot sizes in margin examples as if they are permanent.
- Use
INTRADAYorMARGINfor F&O, commodity, and currency segments. - Never use
CNCorMTFfor derivative products.
Troubleshooting
Margin call fails immediately
Check:
security_idexchange_segmentproduct_typetrigger_pricefor SL/SLM
Available cash seems lower than expected
Inspect:
utilizedAmountcollateralAmountblockedPayoutAmountwithdrawableBalance
User wants a multi-leg strategy
Do this in order: 1. Resolve live contract IDs from security master or option chain 2. Check margin impact leg by leg if no multi-order wrapper exists 3. Preview the sequence 4. Confirm with the user before any live execution
Instruments — Complete Reference
Use the security master as the primary source for:
security_id- lot size
- expiry
- strike
- tick size
- display symbol
Do not treat hardcoded derivative IDs as stable.
Preferred SDK Entry Point
Current SDK static method:
from dhanhq import dhanhq
df = dhanhq.fetch_security_list("compact")The installed SDK downloads the CSV locally and returns a pandas DataFrame.
Official instrument sources:
- compact CSV:
https://images.dhan.co/api-data/api-scrip-master.csv - detailed CSV:
https://images.dhan.co/api-data/api-scrip-master-detailed.csv
Key Columns
| Column | Meaning |
|---|---|
SEM_SMST_SECURITY_ID | Security ID |
SEM_EXM_EXCH_ID | Exchange (NSE, BSE, MCX) |
SEM_INSTRUMENT_NAME | Instrument type (EQUITY, OPTIDX, OPTSTK, etc.) |
SEM_TRADING_SYMBOL | Exchange trading symbol |
SEM_CUSTOM_SYMBOL | Dhan display name |
SEM_LOT_UNITS | Lot size |
SEM_TICK_SIZE | Tick size |
SEM_EXPIRY_DATE | Expiry date |
SEM_STRIKE_PRICE | Strike price |
SEM_OPTION_TYPE | CE or PE |
SEM_EXPIRY_FLAG | W or M |
Recommended Resolution Flow
1. Exact trading-symbol match 2. Exact custom-symbol match 3. Filter by exchange and instrument type 4. Only then fall back to contains-search and disambiguation
Prefer the helper layer:
from scripts.dhan_helpers import resolve_symbol, resolve_derivative, get_lot_size
cash = resolve_symbol("RELIANCE", exchange_segment="NSE_EQ")
contract = resolve_derivative("NIFTY", strike=24000, option_type="CE", expiry="2025-03-27")
lot_size = get_lot_size(underlying="NIFTY")Cash-Market Lookup Example
df = dhanhq.fetch_security_list("compact")
match = df[
(df["SEM_EXM_EXCH_ID"] == "NSE")
& (df["SEM_INSTRUMENT_NAME"] == "EQUITY")
& (df["SEM_TRADING_SYMBOL"] == "RELIANCE")
]
security_id = str(match.iloc[0]["SEM_SMST_SECURITY_ID"])Derivative Lookup Example
df = dhanhq.fetch_security_list("compact")
contract = df[
(df["SEM_EXM_EXCH_ID"] == "NSE")
& (df["SEM_INSTRUMENT_NAME"] == "OPTIDX")
& (df["SEM_CUSTOM_SYMBOL"] == "NIFTY")
& (df["SEM_STRIKE_PRICE"] == 24000.0)
& (df["SEM_OPTION_TYPE"] == "CE")
& (df["SEM_EXPIRY_DATE"] == "2025-03-27")
]
security_id = str(contract.iloc[0]["SEM_SMST_SECURITY_ID"])
lot_size = int(contract.iloc[0]["SEM_LOT_UNITS"])Quick-Reference Fallback IDs
These are convenience references only. Re-check the security master if there is any ambiguity.
Index Underlyings
| Underlying | security_id | Underlying Segment |
|---|---|---|
| NIFTY 50 | 13 | IDX_I |
| BANK NIFTY | 25 | IDX_I |
| FINNIFTY | 27 | IDX_I |
| MIDCPNIFTY | 442 | IDX_I |
| SENSEX | 51 | IDX_I |
Common NSE Equities
| Symbol | security_id |
|---|---|
| RELIANCE | 2885 |
| HDFCBANK | 1333 |
| TCS | 11536 |
| INFY | 1594 |
| ICICIBANK | 4963 |
| SBIN | 3045 |
Practical Rules
- Equity security IDs are relatively stable.
- Derivative contract IDs are not stable across expiries.
- Resolve derivative IDs fresh for live trading.
- Use lot size from the security master, not from stale constants.
Troubleshooting
No symbol match
Check:
- exchange
- instrument type
- expiry
- strike
- option type
Too many matches
Add filters for:
SEM_EXM_EXCH_IDSEM_INSTRUMENT_NAMESEM_EXPIRY_DATESEM_OPTION_TYPE
Contract not found
Possible causes:
- contract expired
- wrong expiry date
- wrong exchange segment
- stale assumption about current listed contracts
Live Feed — Complete Reference
SDK classes: MarketFeed, OrderUpdate, FullDepth.
MarketFeed
Current SDK signature:
MarketFeed(
dhan_context,
instruments,
version="v2",
on_connect=None,
on_message=None,
on_close=None,
on_error=None,
on_ticks=None,
)Example:
from dhanhq import DhanContext, MarketFeed
dhan_context = DhanContext("client_id", "access_token")
instruments = [
(MarketFeed.NSE, "2885", MarketFeed.Ticker),
(MarketFeed.NSE, "1333", MarketFeed.Quote),
(MarketFeed.NSE_FNO, "49081", MarketFeed.Full),
]
def on_message(instance, message):
print(message)
feed = MarketFeed(
dhan_context,
instruments,
version="v2",
on_message=on_message,
)
feed.run_forever()Current SDK Constants
Exchange constants:
MarketFeed.IDX = 0MarketFeed.NSE = 1MarketFeed.NSE_FNO = 2MarketFeed.NSE_CURR = 3MarketFeed.BSE = 4MarketFeed.MCX = 5MarketFeed.BSE_CURR = 7MarketFeed.BSE_FNO = 8
Subscription constants:
MarketFeed.Ticker = 15MarketFeed.Quote = 17MarketFeed.Depth = 19MarketFeed.Full = 21
Use version="v2" when you need full packet mode.
Connection Limits
From the current v2 API docs:
- up to 5 concurrent websockets per user
- up to 5000 instruments per connection
- up to 100 instruments per subscription message
SDK helper methods:
feed.subscribe_symbols(symbols)
feed.unsubscribe_symbols(symbols)
feed.close_connection()
feed.disconnect()Parsed Packet Shapes From The Installed SDK
Representative Ticker packet:
{
"type": "Ticker Data",
"exchange_segment": 1,
"security_id": 2885,
"LTP": "2450.00",
"LTT": "2025-01-15 10:30:00+00:00"
}Representative Quote packet:
{
"type": "Quote Data",
"exchange_segment": 1,
"security_id": 2885,
"LTP": "2450.00",
"LTQ": 10,
"LTT": "...",
"avg_price": "2445.50",
"volume": 1234567,
"total_sell_quantity": 450000,
"total_buy_quantity": 500000,
"open": "2430.00",
"close": "2440.00",
"high": "2465.00",
"low": "2425.00"
}Representative Full packet:
{
"type": "Full Data",
"exchange_segment": 2,
"security_id": 49081,
"LTP": "368.15",
"LTQ": 50,
"LTT": "...",
"avg_price": "365.00",
"volume": 10000,
"total_sell_quantity": 2500,
"total_buy_quantity": 3000,
"OI": 1250000,
"oi_day_high": 1265000,
"oi_day_low": 1210000,
"open": "360.00",
"close": "355.00",
"high": "372.00",
"low": "352.00",
"depth": [
{
"bid_quantity": 100,
"ask_quantity": 75,
"bid_orders": 2,
"ask_orders": 1,
"bid_price": "368.10",
"ask_price": "368.20"
}
]
}OrderUpdate
Current SDK signature:
OrderUpdate(dhan_context)Minimal usage:
from dhanhq import OrderUpdate
order_client = OrderUpdate(dhan_context)
def on_order_update(order_data):
print(order_data)
order_client.on_update = on_order_update
order_client.connect_to_dhan_websocket_sync()Use this for:
- live order-status monitoring
- fill confirmations
- multi-leg execution monitoring
FullDepth
Current SDK signature:
FullDepth(dhan_context, instruments, depth_level=20)Example:
from dhanhq import FullDepth
depth = FullDepth(
dhan_context,
instruments=[(FullDepth.NSE, "2885")],
depth_level=20,
)
depth.run_forever()
print(depth.get_data())Current SDK constants:
FullDepth.NSE = 1FullDepth.NSE_FNO = 2
Current API limits:
- 20-level depth: up to 50 instruments per connection
- 200-level depth: 1 instrument per connection
- only
NSE_EQandNSE_FNOare supported for full depth
SDK methods:
depth.subscribe_symbols(symbols)
depth.unsubscribe_symbols(symbols)
depth.close_connection()
depth.disconnect()Parsed Depth Output
FullDepth receives bid and ask packets separately and the SDK formats them into a combined representation. Treat the output as SDK-parsed depth data, not raw binary packet layout.
When To Use What
- Use
ticker_data(),ohlc_data(), orquote_data()for snapshots. - Use
MarketFeedfor live monitoring of LTP, quote, or full packets. - Use
OrderUpdatefor execution tracking. - Use
FullDepthonly when you genuinely need deeper order-book visibility because it is heavier and more restrictive than regular live market feed usage.
Market Data — Complete Reference
Historical timestamps are epoch integers inside response["data"]["timestamp"] — always convert explicitly.
Historical Daily Data
SDK signature:
dhan.historical_daily_data(
security_id,
exchange_segment,
instrument_type,
from_date,
to_date,
expiry_code=0,
oi=False,
)Example:
response = dhan.historical_daily_data(
security_id="2885",
exchange_segment=dhanhq.NSE,
instrument_type="EQUITY",
from_date="2024-01-01",
to_date="2024-12-31",
expiry_code=0,
oi=False,
)
if response["status"] == "success":
candles = response["data"]
timestamps = [dhan.convert_to_date_time(ts) for ts in candles["timestamp"]]Raw API payload fields:
securityIdexchangeSegmentinstrumentexpiryCodeoifromDatetoDate
Documented instrument values from the v2 annexure:
INDEXFUTIDXOPTIDXEQUITYFUTSTKOPTSTKFUTCOMOPTFUTFUTCUROPTCUR
Response data fields:
openhighlowclosevolumetimestampopen_interestwhenoi=True
Important:
- The raw API docs currently document
expiryCodevalues0,1,2. - The installed SDK validation still accepts
3. - Prefer the documented values unless Dhan updates the API docs.
Intraday Minute Data
SDK signature:
dhan.intraday_minute_data(
security_id,
exchange_segment,
instrument_type,
from_date,
to_date,
interval=1,
oi=False,
)Example:
response = dhan.intraday_minute_data(
security_id="2885",
exchange_segment=dhanhq.NSE,
instrument_type="EQUITY",
from_date="2024-09-11 09:30:00",
to_date="2024-09-15 13:00:00",
interval=1,
oi=False,
)
if response["status"] == "success":
minute_data = response["data"]
minute_times = [dhan.convert_to_date_time(ts) for ts in minute_data["timestamp"]]Current API truth:
- Intraday data is ranged minute data, not "today only".
- The v2 historical-data page documents last 5 years for active instruments.
- The installed SDK docstring still says "last 5 trading day". Prefer the current v2 API docs when planning data windows.
Supported intervals:
15152560
Market Quote Snapshots
SDK methods:
dhan.ticker_data(securities)
dhan.ohlc_data(securities)
dhan.quote_data(securities)Request format:
securities = {
"NSE_EQ": [2885, 1333],
"NSE_FNO": [49081],
}Ticker Data
response = dhan.ticker_data({"NSE_EQ": [2885]})
if response["status"] == "success":
ltp = response["data"]["NSE_EQ"]["2885"]["last_price"]OHLC Data
response = dhan.ohlc_data({"NSE_EQ": [2885]})
if response["status"] == "success":
ohlc = response["data"]["NSE_EQ"]["2885"]["ohlc"]Quote Data
response = dhan.quote_data({"NSE_FNO": [49081]})
if response["status"] == "success":
quote = response["data"]["NSE_FNO"]["49081"]
print(quote["last_price"], quote["oi"], quote["volume"])Current raw fields exposed by the v2 quote endpoint include:
last_priceaverage_pricebuy_quantitysell_quantitydepth.buy[]depth.sell[]last_quantitylast_trade_timelower_circuit_limitupper_circuit_limitnet_changevolumeoioi_day_highoi_day_lowohlc.openohlc.closeohlc.highohlc.low
Quote API limits:
- up to 1000 instruments per request
1 request/sec
Expired Options Data
SDK signature:
dhan.expired_options_data(
security_id,
exchange_segment,
instrument_type,
expiry_flag,
expiry_code,
strike,
drv_option_type,
required_data,
from_date,
to_date,
interval=1,
)Example:
response = dhan.expired_options_data(
security_id=13,
exchange_segment=dhanhq.NSE_FNO,
instrument_type="OPTIDX",
expiry_flag="MONTH",
expiry_code=1,
strike="ATM",
drv_option_type="CALL",
required_data=["open", "high", "low", "close", "volume", "oi", "spot"],
from_date="2021-08-01",
to_date="2021-08-31",
interval=1,
)
if response["status"] == "success":
ce = response["data"]["ce"]
timestamps = [dhan.convert_to_date_time(ts) for ts in ce["timestamp"]]Current v2 API notes:
- rolling expired options data is available for up to last 5 years
- fetch up to 30 days per call
strikesupportsATM,ATM+N,ATM-N- near-expiry index options go up to
ATM+10 / ATM-10 - other contracts go up to
ATM+3 / ATM-3
Allowed required_data values:
openhighlowcloseivvolumestrikeoispot
Timestamp Conversion
Prefer explicit conversion instead of assuming ISO strings:
timestamps = [dhan.convert_to_date_time(ts) for ts in response["data"]["timestamp"]]Or with pandas:
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s", utc=True).dt.tz_convert("Asia/Kolkata")Practical Guidance
- Use historical APIs for candles and backtests.
- Use quote APIs for point-in-time snapshots.
- Use
MarketFeedfor live monitoring instead of polling quote endpoints aggressively. - Use the security master to validate derivative instrument type, expiry, and lot size before mixing historical data with live execution.
Option Chain — Complete Reference
For analysis code use the normalized helper layer from scripts/dhan_helpers.py. See raw payload shape below when you need to parse the response directly.
Expiry List
SDK signature:
dhan.expiry_list(under_security_id, under_exchange_segment)Example:
response = dhan.expiry_list(
under_security_id=13,
under_exchange_segment="IDX_I",
)
if response["status"] == "success":
expiries = response["data"]Option Chain
SDK signature:
dhan.option_chain(under_security_id, under_exchange_segment, expiry)Index example:
response = dhan.option_chain(
under_security_id=13,
under_exchange_segment="IDX_I",
expiry="2025-03-27",
)Equity-underlying example:
response = dhan.option_chain(
under_security_id=2885,
under_exchange_segment="NSE_EQ",
expiry="2025-03-27",
)Rate limit:
- one unique option-chain request every 3 seconds
Raw Payload Shape
Raw response structure from Dhan v2:
{
"data": {
"last_price": 25642.8,
"oc": {
"25650.000000": {
"ce": {
"security_id": 12345,
"last_price": 146.99,
"average_price": 146.99,
"oi": 1250000,
"oi_change": 50000,
"implied_volatility": 12.5,
"top_bid_price": 146.9,
"top_bid_quantity": 50,
"top_ask_price": 147.0,
"top_ask_quantity": 75,
"volume": 85000,
"greeks": {
"delta": 0.65,
"gamma": 0.002,
"theta": -15.2,
"vega": 28.5
}
},
"pe": {...}
}
}
}
}Important:
ocis keyed by strike string, not a list.- Raw field names are
last_price,oi,implied_volatility,top_bid_price,top_ask_price, and nestedgreeks.
Repo-Normalized Analysis Layer
For analysis code, prefer:
from scripts.dhan_helpers import fetch_chain_df, find_atm_row
chain_df, spot = fetch_chain_df(
dhan,
under_security_id=13,
expiry="2025-03-27",
under_exchange_segment="IDX_I",
)
atm = find_atm_row(chain_df, spot)
print(spot, atm["strike"], atm["ce_security_id"], atm["ce_ltp"])Normalized helper columns:
strikece_security_id,pe_security_idce_ltp,pe_ltpce_oi,pe_oice_oi_change,pe_oi_changece_volume,pe_volumece_iv,pe_ivce_bid_price,pe_bid_pricece_ask_price,pe_ask_pricece_delta,pe_deltace_gamma,pe_gammace_theta,pe_thetace_vega,pe_vega
These normalized names are repo-defined conveniences. They are not raw Dhan field names.
Practical Patterns
Get ATM row
atm = find_atm_row(chain_df, spot)
print(atm["strike"])
print(atm["ce_ltp"], atm["pe_ltp"])Filter nearby strikes
nearby = chain_df[(chain_df["strike"] >= spot - 500) & (chain_df["strike"] <= spot + 500)]Find a contract security ID
row = chain_df[chain_df["strike"] == 24000].iloc[0]
ce_security_id = row["ce_security_id"]
pe_security_id = row["pe_security_id"]Compute simple OI totals
total_ce_oi = chain_df["ce_oi"].fillna(0).sum()
total_pe_oi = chain_df["pe_oi"].fillna(0).sum()Guidance
- Use index examples first for Nifty/BankNifty workflows.
- Cover equity underlyings only when the user explicitly needs stock options.
- For current liquid contracts, option chain is a fast way to get security IDs.
- For robust contract resolution across expiries and underlyings, fall back to the security master.
Options Analysis Patterns
Use the normalized helper output from scripts/dhan_helpers.py for analysis code.
from scripts.dhan_helpers import fetch_chain_df, find_atm_row
chain_df, spot = fetch_chain_df(dhan, 13, "2025-03-27")
atm = find_atm_row(chain_df, spot)Put-Call Ratio (PCR)
total_ce_oi = chain_df["ce_oi"].fillna(0).sum()
total_pe_oi = chain_df["pe_oi"].fillna(0).sum()
pcr = total_pe_oi / total_ce_oi if total_ce_oi else 0
print(f"PCR: {pcr:.2f}")OI Support / Resistance
ce_walls = chain_df[["strike", "ce_oi"]].dropna().sort_values("ce_oi", ascending=False).head(3)
pe_walls = chain_df[["strike", "pe_oi"]].dropna().sort_values("pe_oi", ascending=False).head(3)Interpretation:
- highest CE OI often acts like resistance
- highest PE OI often acts like support
IV Skew
otm_puts = chain_df[chain_df["strike"] < spot].nlargest(3, "strike")
otm_calls = chain_df[chain_df["strike"] > spot].nsmallest(3, "strike")
put_iv = otm_puts["pe_iv"].dropna().mean()
call_iv = otm_calls["ce_iv"].dropna().mean()
skew = put_iv - call_ivMax Pain
def calculate_max_pain(df):
strikes = df["strike"].tolist()
pain = {}
for test_price in strikes:
total = 0
for _, row in df.iterrows():
strike = row["strike"]
ce_oi = row.get("ce_oi") or 0
pe_oi = row.get("pe_oi") or 0
total += max(0, test_price - strike) * ce_oi
total += max(0, strike - test_price) * pe_oi
pain[test_price] = total
return min(pain, key=pain.get)Contract Selection
ATM contract lookup:
atm = find_atm_row(chain_df, spot)
ce_security_id = atm["ce_security_id"]
pe_security_id = atm["pe_security_id"]Nearby strikes:
nearby = chain_df[(chain_df["strike"] >= spot - 500) & (chain_df["strike"] <= spot + 500)]Guidance
- Use option chain for current listed option contracts.
- Use the security master when you need broader derivative resolution logic.
- Treat helper fields like
ce_ltp,pe_oi,ce_iv, etc. as repo-defined normalized fields, not raw Dhan field names.
Orders — Complete Reference
Critical API rule:
- order placement, modification, cancellation, super orders, and forever orders require static IP whitelisting
Current API note:
- Dhan's current order docs say API market orders are converted to limit orders with MPP
Regular Orders
Place Order
Current SDK signature:
dhan.place_order(
security_id,
exchange_segment,
transaction_type,
quantity,
order_type,
product_type,
price,
trigger_price=0,
disclosed_quantity=0,
after_market_order=False,
validity="DAY",
amo_time="OPEN",
bo_profit_value=None,
bo_stop_loss_Value=None,
tag=None,
should_slice=False,
)Recommended pattern:
response = dhan.place_order(
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=10,
order_type=dhanhq.LIMIT,
product_type=dhanhq.CNC,
price=2450.0,
validity=dhanhq.DAY,
tag="rebalance_001",
)
if response["status"] == "success":
print(response["data"]["orderId"], response["data"]["orderStatus"])Validation rules to enforce before placement:
pricerequired forLIMITandSTOP_LOSStrigger_pricerequired forSTOP_LOSSandSTOP_LOSS_MARKET- derivatives require lot-size multiples
- derivatives only allow
INTRADAYorMARGIN - quote market structure first if you need a sensible limit price
Slice Order
Use should_slice=True or the explicit SDK helper:
response = dhan.place_slice_order(
security_id="49081",
exchange_segment=dhanhq.NSE_FNO,
transaction_type=dhanhq.BUY,
quantity=2500,
order_type=dhanhq.LIMIT,
product_type=dhanhq.INTRA,
price=150.0,
validity=dhanhq.DAY,
)Use this only after checking current freeze-quantity requirements.
Modify Order
Current SDK signature:
dhan.modify_order(order_id, order_type, leg_name, quantity, price, trigger_price, disclosed_quantity, validity)Example:
response = dhan.modify_order(
order_id="112111182198",
order_type=dhanhq.LIMIT,
leg_name=None,
quantity=10,
price=2455.0,
trigger_price=0,
disclosed_quantity=0,
validity=dhanhq.DAY,
)The SDK and release notes expect full placed quantity in modification requests, not pending quantity.
Cancel Order
response = dhan.cancel_order(order_id="112111182198")Order Retrieval
order = dhan.get_order_by_id("112111182198")
order_by_tag = dhan.get_order_by_correlationID("my_tag")
orders = dhan.get_order_list()
trades = dhan.get_trade_book()
single_trade = dhan.get_trade_book(order_id="112111182198")
history = dhan.get_trade_history("2025-01-01", "2025-01-31", page_number=0)
ledger = dhan.ledger_report("2025-01-01", "2025-01-31")Super Orders
Current SDK support exists for:
place_super_ordermodify_super_ordercancel_super_orderget_super_order_list
Place Super Order
Current SDK signature:
dhan.place_super_order(
security_id,
exchange_segment,
transaction_type,
quantity,
order_type,
product_type,
price,
targetPrice=0.0,
stopLossPrice=0.0,
trailingJump=0.0,
tag=None,
)Example:
response = dhan.place_super_order(
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=1,
order_type=dhanhq.LIMIT,
product_type=dhanhq.INTRA,
price=2450.0,
targetPrice=2500.0,
stopLossPrice=2420.0,
trailingJump=10.0,
)Modify Super Order
Current SDK signature:
dhan.modify_super_order(order_id, order_type, leg_name, quantity=0, price=0.0, targetPrice=0.0, stopLossPrice=0.0, trailingJump=0.0)Key rule from the API docs:
ENTRY_LEGcan modify the whole structure while the entry order isPENDINGorPART_TRADED- after entry is
TRADED, onlyTARGET_LEGandSTOP_LOSS_LEGchanges remain
Cancel Super Order
response = dhan.cancel_super_order(order_id="...", order_leg="ENTRY_LEG")Super Order Book
response = dhan.get_super_order_list()Forever Orders
Current SDK support exists for:
place_forevermodify_forevercancel_foreverget_forever
Place Forever Order
Current SDK signature:
dhan.place_forever(
security_id,
exchange_segment,
transaction_type,
product_type,
order_type,
quantity,
price,
trigger_Price,
order_flag="SINGLE",
disclosed_quantity=0,
validity="DAY",
price1=0,
trigger_Price1=0,
quantity1=0,
tag=None,
symbol="",
)Single trigger example:
response = dhan.place_forever(
security_id="2885",
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
product_type=dhanhq.CNC,
order_type=dhanhq.LIMIT,
quantity=1,
price=2400.0,
trigger_Price=2405.0,
)Important:
- the SDK parameter is
trigger_Price - OCO fields use
price1,trigger_Price1,quantity1
Order Validation Checklist
Before live placement, confirm: 1. account access token is valid 2. required segment is active 3. data plan is active if quotes are needed 4. static IP is configured for trading APIs 5. product type matches segment 6. quantity matches lot size for derivatives 7. trigger/price fields are consistent with order type 8. limit price is reasonable for the market state
Troubleshooting
Order rejected with access/IP issue
Check:
- static IP whitelisting
DH-911invalid IP
Order rejected with product-type issue
Check:
CNC/MTFonly for eligible equity flowsINTRADAY/MARGINonly for derivative segments
User thinks a data error is an order error
Separate the failure domain:
806/DH-902usually points to Data API access- trading placement failures often point to static IP or order validation
Destructive actions
Always preview and confirm before:
place_ordermodify_ordercancel_ordercancel_super_ordercancel_foreverkill_switch
Portfolio And Positions — Complete Reference
Holdings
SDK method:
response = dhan.get_holdings()Example:
if response["status"] == "success":
holdings = response["data"]
for holding in holdings:
print(holding["tradingSymbol"], holding["securityId"], holding["availableQty"])Useful holding fields from the v2 API:
exchangetradingSymbolsecurityIdisintotalQtydpQtyt1QtyavailableQtycollateralQtyavgCostPrice
Positions
SDK method:
response = dhan.get_positions()Example:
if response["status"] == "success":
positions = response["data"]
open_positions = [p for p in positions if p["netQty"] != 0]Useful position fields from the v2 API:
tradingSymbolsecurityIdpositionTypeexchangeSegmentproductTypebuyAvgbuyQtysellAvgsellQtynetQtyrealizedProfitunrealizedProfitdrvExpiryDatedrvOptionTypedrvStrikePrice
Convert Position
SDK signature:
dhan.convert_position(
from_product_type,
exchange_segment,
position_type,
security_id,
convert_qty,
to_product_type,
)Example:
response = dhan.convert_position(
from_product_type=dhanhq.INTRA,
exchange_segment=dhanhq.NSE,
position_type="LONG",
security_id="2885",
convert_qty=1,
to_product_type=dhanhq.CNC,
)Exit All Positions
Raw v2 API:
DELETE /positions
Current installed SDK status:
- the local
dhanhq2.2.0 install does not expose a first-classexit_all_positions()method
Practical rule:
- do not document or generate
dhan.exit_all_positions()unless you add an explicit wrapper - if you add raw REST support for this in the repo later, always require confirmation first
This is account-wide and high-risk.
eDIS Authorization
Use eDIS for selling delivery holdings.
Do not use eDIS for:
- intraday trades
- F&O positions
- non-holdings sell flows
Step 1: Generate TPIN
response = dhan.generate_tpin()Step 2: Open the authorization form
SDK signature:
dhan.open_browser_for_tpin(isin, qty, exchange, segment="EQ", bulk=False)Example:
dhan.open_browser_for_tpin(
isin="INE002A01018",
qty=5,
exchange="NSE",
segment="EQ",
)Step 3: Check approval status
SDK signature:
dhan.edis_inquiry(isin)Example:
response = dhan.edis_inquiry("INE002A01018")
if response["status"] == "success":
status = response["data"]
print(status["status"], status["aprvdQty"], status["remarks"])Current inquiry fields from the v2 API:
clientIdisintotalQtyaprvdQtystatusremarks
You can also pass ALL to inspect eDIS status more broadly when needed.
Agent Guardrails
- If the user wants to sell CNC holdings and eDIS status is unclear, stop and confirm authorization first.
- Do not place the delivery sell order until approval is confirmed.
- For holdings sell, use the ISIN from holdings data rather than guessing it.
Troubleshooting
Holdings or positions look empty
Check:
- current trading day state
- correct account
- segment activation on the Dhan account
eDIS inquiry is not approved
Do this: 1. Re-run TPIN flow 2. Re-open the browser authorization form 3. Re-check edis_inquiry(isin)
User wants account-wide position exit
Treat this as a destructive account action: 1. summarize open positions first 2. ask for explicit confirmation 3. only then use a raw REST wrapper if you add one
ScanX — Fundamental and Technical Data
Use ScanX when Dhan APIs do not cover the needed data. Dhan provides execution, quotes, OHLC, option chain, and portfolio. ScanX provides fundamentals, technical indicators, shareholding, and screeners.
Capability Gap
| Data needed | Use |
|---|---|
| PE ratio, EPS, Book Value, PB Ratio | ScanX |
| Revenue, Net Profit, EBITDA | ScanX |
| Debt-to-equity, Return on Equity | ScanX |
| RSI(14), MACD(12,26), ADX(14), ATR(14) | ScanX |
| Promoter %, FII %, DII %, Public % | ScanX |
| Quarterly results history (2015–present) | ScanX |
| Balance Sheet, Cash Flows | ScanX |
| Stock screeners (fundamental/technical) | ScanX |
| Live quotes, OHLC, option chain | Dhan |
| Order execution, portfolio | Dhan |
Company Page URL Pattern
https://scanx.trade/company/{slug}
Slug rules:
- Lowercase the full registered company name
- Replace spaces with hyphens
- Include "ltd" if part of the official name
| Company | URL slug |
|---|---|
| Reliance Industries Ltd | reliance-industries-ltd |
| HDFC Bank | hdfc-bank |
| Infosys | infosys |
| TCS | tata-consultancy-services-ltd |
| State Bank of India | state-bank-of-india |
| ICICI Bank | icici-bank |
If unsure: derive the slug, fetch the URL, and verify the page returns company data. If it 404s, try a shorter form (drop "ltd", use abbreviation, or search https://scanx.trade).
Data Per Page Section
Overview tab (loaded by default):
- Current price, day change %, 52-week range
- Market Cap, PE Ratio, EPS, PB Ratio, Book Value, Dividend Yield
- EBITDA, Revenue, Net Profit, Debt-to-equity, ROE
Technicals tab:
- RSI(14) — with signal: Overbought / Neutral / Oversold
- MACD(12,26) — with signal: Bullish / Bearish
- ADX(14) — with signal: Strong Trend / Weak Trend
- ATR(14) — with volatility label
Shareholding tab:
- Promoter %, DII %, FII %, Public %
Financials tab:
- Quarterly results back to 2015
- Revenue, EBITDA, Net Profit trends
Balance Sheet / Cash Flows tabs:
- Annual statements back to 2015
Fetching Data
Fetch the company URL and extract the relevant section from the rendered page:
https://scanx.trade/company/{slug}The Overview tab data (fundamentals) loads on the default page. Technical indicators require the Technicals tab (append #technicals or navigate to it).
Combined Workflow: Analyze on ScanX → Execute on Dhan
# Step 1: fetch ScanX page for fundamentals/technicals
# → https://scanx.trade/company/reliance-industries-ltd
# → extract: PE=18.38, RSI=42.75 (Neutral), Revenue=10,57,220 Cr
# Step 2: resolve security_id from Dhan security master
from scripts.dhan_helpers import resolve_symbol, get_client
dhan, _ = get_client()
row = resolve_symbol("RELIANCE", exchange_segment="NSE_EQ")
security_id = str(row["SEM_SMST_SECURITY_ID"]) # e.g. "2885"
# Step 3: get live quote from Dhan
from dhanhq import dhanhq
response = dhan.ticker_data({"NSE_EQ": [int(security_id)]})
ltp = response["data"]["NSE_EQ"][security_id]["last_price"]
# Step 4: place order via Dhan
response = dhan.place_order(
security_id=security_id,
exchange_segment=dhanhq.NSE,
transaction_type=dhanhq.BUY,
quantity=1,
order_type=dhanhq.LIMIT,
product_type=dhanhq.CNC,
price=ltp,
)Guidance
- ScanX does not have a documented public API — data is accessed via the web page.
- The Overview tab data is available without authentication.
- For screener results, use
https://scanx.trade/screener— 1500+ community screeners covering fundamental, technical, intraday, and sector-based filters. - ScanX integrates with Dhan for order execution via the web UI, but programmatic execution still goes through Dhan APIs directly.
"""Composable helper functions for DhanHQ trading workflows.
The DhanHQ Python SDK wraps HTTP responses as:
{"status": "success|failure", "remarks": ..., "data": ...}
The helper functions in this file keep that SDK wrapper explicit while also
providing a small set of repo-defined normalization utilities for analysis.
Most notably, option-chain normalization lives here so the docs can stay
clear about what is raw Dhan payload and what is repo-defined convenience.
"""
from __future__ import annotations
import json
import os
from typing import Any
import pandas as pd
from dhanhq import DhanContext, dhanhq
def _load_config(path: str) -> dict[str, Any]:
with open(path, encoding="utf-8") as handle:
return json.load(handle)
def get_client(config_path: str | None = None):
"""Initialize a DhanHQ client from config or environment variables.
Resolution order:
1. Explicit ``config_path``
2. ``DHAN_CONFIG_PATH`` environment variable
3. ``config.json`` in the current working directory
4. ``DHAN_CLIENT_ID`` + ``DHAN_ACCESS_TOKEN`` environment variables
"""
client_id = None
access_token = None
paths_to_try = [
config_path,
os.environ.get("DHAN_CONFIG_PATH"),
"config.json",
]
for path in paths_to_try:
if not path or not os.path.exists(path):
continue
config = _load_config(path)
client_id = config.get("client_id") or client_id
access_token = config.get("access_token") or access_token
if client_id and access_token:
break
client_id = client_id or os.environ.get("DHAN_CLIENT_ID")
access_token = access_token or os.environ.get("DHAN_ACCESS_TOKEN")
if not client_id or not access_token:
raise ValueError(
"Credentials not found. Set DHAN_CLIENT_ID and DHAN_ACCESS_TOKEN, "
"or point DHAN_CONFIG_PATH to a config file."
)
context = DhanContext(client_id, access_token)
return dhanhq(context), context
def unwrap_sdk_data(response: dict[str, Any]) -> Any:
"""Return the ``data`` field from a successful SDK response.
Raises:
ValueError: if the SDK response is not successful.
"""
if response.get("status") != "success":
raise ValueError(response.get("remarks") or "Dhan SDK call failed")
return response.get("data")
_security_master_cache: pd.DataFrame | None = None
def get_security_master(mode: str = "compact") -> pd.DataFrame:
"""Fetch and cache the Dhan security master.
The SDK downloads the CSV locally and returns a DataFrame.
"""
global _security_master_cache
if _security_master_cache is None:
_security_master_cache = dhanhq.fetch_security_list(mode)
if _security_master_cache is None:
raise ValueError("Unable to fetch the Dhan security master")
return _security_master_cache
def resolve_symbol(
symbol: str,
exchange_segment: str = "NSE_EQ",
instrument_name: str = "EQUITY",
) -> dict[str, Any] | None:
"""Resolve a cash-market symbol to a security ID using the security master."""
df = get_security_master()
query = symbol.upper().strip()
exchange = exchange_segment.split("_")[0]
exact = df[
(df["SEM_EXM_EXCH_ID"].astype(str).str.upper() == exchange)
& (df["SEM_INSTRUMENT_NAME"].astype(str).str.upper() == instrument_name.upper())
& (df["SEM_TRADING_SYMBOL"].astype(str).str.upper() == query)
]
if exact.empty:
exact = df[
(df["SEM_EXM_EXCH_ID"].astype(str).str.upper() == exchange)
& (df["SEM_INSTRUMENT_NAME"].astype(str).str.upper() == instrument_name.upper())
& (df["SEM_CUSTOM_SYMBOL"].astype(str).str.upper().str.contains(query, na=False))
]
if exact.empty:
return None
row = exact.iloc[0]
return {
"security_id": str(row["SEM_SMST_SECURITY_ID"]),
"trading_symbol": str(row["SEM_TRADING_SYMBOL"]),
"display_name": str(row.get("SEM_CUSTOM_SYMBOL", "")),
"exchange_segment": exchange_segment,
"instrument_name": str(row["SEM_INSTRUMENT_NAME"]),
}
def resolve_derivative(
underlying: str,
*,
instrument_names: tuple[str, ...] = ("OPTIDX", "OPTSTK", "FUTIDX", "FUTSTK"),
strike: float | None = None,
option_type: str | None = None,
expiry: str | None = None,
exchange: str = "NSE",
) -> dict[str, Any] | None:
"""Resolve a derivative contract from the security master."""
df = get_security_master()
mask = (
(df["SEM_EXM_EXCH_ID"].astype(str).str.upper() == exchange.upper())
& (df["SEM_INSTRUMENT_NAME"].isin(instrument_names))
& (df["SEM_CUSTOM_SYMBOL"].astype(str).str.upper() == underlying.upper())
)
if strike is not None:
mask &= df["SEM_STRIKE_PRICE"].astype(float) == float(strike)
if option_type is not None:
mask &= df["SEM_OPTION_TYPE"].astype(str).str.upper() == option_type.upper()
if expiry is not None:
mask &= df["SEM_EXPIRY_DATE"].astype(str) == expiry
matches = df[mask].sort_values(["SEM_EXPIRY_DATE", "SEM_TRADING_SYMBOL"])
if matches.empty:
return None
row = matches.iloc[0]
return {
"security_id": str(row["SEM_SMST_SECURITY_ID"]),
"trading_symbol": str(row["SEM_TRADING_SYMBOL"]),
"lot_size": int(row["SEM_LOT_UNITS"]),
"tick_size": float(row["SEM_TICK_SIZE"]),
"expiry": str(row.get("SEM_EXPIRY_DATE", "")),
"instrument_name": str(row["SEM_INSTRUMENT_NAME"]),
}
def get_lot_size(
*,
security_id: str | None = None,
trading_symbol: str | None = None,
underlying: str | None = None,
) -> int | None:
"""Return lot size from the security master when possible."""
df = get_security_master()
if security_id is not None:
match = df[df["SEM_SMST_SECURITY_ID"].astype(str) == str(security_id)]
if not match.empty:
return int(match.iloc[0]["SEM_LOT_UNITS"])
if trading_symbol is not None:
match = df[df["SEM_TRADING_SYMBOL"].astype(str).str.upper() == trading_symbol.upper()]
if not match.empty:
return int(match.iloc[0]["SEM_LOT_UNITS"])
if underlying is not None:
match = df[
(df["SEM_CUSTOM_SYMBOL"].astype(str).str.upper() == underlying.upper())
& (df["SEM_INSTRUMENT_NAME"].isin(["OPTIDX", "OPTSTK", "FUTIDX", "FUTSTK"]))
]
if not match.empty:
return int(match.iloc[0]["SEM_LOT_UNITS"])
return None
def preview_order(
security_id: str,
exchange_segment: str,
transaction_type: str,
quantity: int,
order_type: str,
product_type: str,
*,
price: float = 0.0,
trading_symbol: str | None = None,
) -> str:
"""Build a human-readable order preview for confirmation."""
notional = price * quantity if price else 0
lines = [
"--- ORDER PREVIEW ---",
f"Security: {trading_symbol or security_id}",
f"Exchange: {exchange_segment}",
f"Action: {transaction_type}",
f"Quantity: {quantity}",
f"Order Type: {order_type}",
f"Product Type: {product_type}",
f"Price: {'MARKET / MPP' if order_type == 'MARKET' else f'Rs. {price:,.2f}'}",
]
if notional:
lines.append(f"Notional: Rs. {notional:,.2f}")
if notional > 50000:
lines.append("Warning: Notional exceeds Rs. 50,000")
lines.append("---------------------")
return "\n".join(lines)
def normalize_option_chain(response: dict[str, Any]) -> tuple[float, list[dict[str, Any]]]:
"""Normalize raw option-chain data into analysis-friendly rows.
Raw REST response:
- underlying spot is ``data.last_price``
- strikes are keyed under ``data.oc`` as strings like ``"25650.000000"``
Normalized row fields are repo-defined conveniences like:
- ``strike``
- ``ce_ltp`` / ``pe_ltp``
- ``ce_oi`` / ``pe_oi``
- ``ce_iv`` / ``pe_iv``
- ``ce_delta`` / ``pe_delta``
"""
data = unwrap_sdk_data(response)
spot = float(data["last_price"])
option_chain = data.get("oc", {}) or {}
rows: list[dict[str, Any]] = []
for strike_key, strike_payload in sorted(option_chain.items(), key=lambda item: float(item[0])):
row: dict[str, Any] = {"strike": float(strike_key)}
for side in ("ce", "pe"):
leg = strike_payload.get(side) or {}
greeks = leg.get("greeks") or {}
row[f"{side}_security_id"] = str(leg["security_id"]) if leg.get("security_id") is not None else None
row[f"{side}_ltp"] = leg.get("last_price")
row[f"{side}_avg_price"] = leg.get("average_price")
row[f"{side}_oi"] = leg.get("oi")
row[f"{side}_oi_change"] = leg.get("oi_change")
row[f"{side}_volume"] = leg.get("volume")
row[f"{side}_iv"] = leg.get("implied_volatility")
row[f"{side}_bid_price"] = leg.get("top_bid_price")
row[f"{side}_bid_qty"] = leg.get("top_bid_quantity")
row[f"{side}_ask_price"] = leg.get("top_ask_price")
row[f"{side}_ask_qty"] = leg.get("top_ask_quantity")
row[f"{side}_delta"] = greeks.get("delta")
row[f"{side}_gamma"] = greeks.get("gamma")
row[f"{side}_theta"] = greeks.get("theta")
row[f"{side}_vega"] = greeks.get("vega")
rows.append(row)
return spot, rows
def fetch_chain_df(
dhan_client,
under_security_id: int,
expiry: str,
under_exchange_segment: str = "IDX_I",
) -> tuple[pd.DataFrame, float]:
"""Fetch option-chain data and return a normalized DataFrame plus spot."""
response = dhan_client.option_chain(
under_security_id=under_security_id,
under_exchange_segment=under_exchange_segment,
expiry=expiry,
)
spot, rows = normalize_option_chain(response)
return pd.DataFrame(rows), spot
def find_atm_row(chain_df: pd.DataFrame, spot: float) -> pd.Series:
"""Return the nearest strike row to the provided spot value."""
return chain_df.iloc[(chain_df["strike"] - spot).abs().argsort().iloc[0]]
def format_pnl_report(holdings_response: dict[str, Any], positions_response: dict[str, Any]) -> dict[str, Any]:
"""Generate a small structured P&L summary from SDK responses."""
holdings = unwrap_sdk_data(holdings_response)
positions = unwrap_sdk_data(positions_response)
report = {
"total_investment": 0.0,
"current_value": 0.0,
"total_pnl": 0.0,
"day_pnl": 0.0,
"holdings_count": len(holdings or []),
"positions_count": len(positions or []),
}
for holding in holdings or []:
total_qty = holding.get("totalQty", 0)
report["total_investment"] += holding.get("avgCostPrice", 0) * total_qty
report["current_value"] += holding.get("marketValue", 0)
report["total_pnl"] += holding.get("pnl", 0)
report["day_pnl"] += holding.get("dayPnl", 0)
for position in positions or []:
report["total_pnl"] += position.get("realizedProfit", 0) + position.get("unrealizedProfit", 0)
return report
def check_margin(
dhan_client,
*,
security_id: str,
exchange_segment: str,
transaction_type: str,
quantity: int,
product_type: str,
price: float,
trigger_price: float = 0,
) -> dict[str, Any]:
"""Run a single-order margin check against the current SDK."""
margin_response = dhan_client.margin_calculator(
security_id=security_id,
exchange_segment=exchange_segment,
transaction_type=transaction_type,
quantity=quantity,
product_type=product_type,
price=price,
trigger_price=trigger_price,
)
funds_response = dhan_client.get_fund_limits()
margin = unwrap_sdk_data(margin_response)
funds = unwrap_sdk_data(funds_response)
total_margin = margin.get("totalMargin", 0.0)
available_balance = funds.get("availabelBalance", 0.0)
return {
"total_margin": total_margin,
"available_balance": available_balance,
"brokerage": margin.get("brokerage", 0.0),
"leverage": margin.get("leverage"),
"sufficient": available_balance >= total_margin,
"shortfall": max(0.0, total_margin - available_balance),
}
#!/usr/bin/env python3
"""Resolve human-readable instruments to Dhan security IDs.
Usage:
python scripts/resolve_security.py RELIANCE
python scripts/resolve_security.py HDFC Bank
python scripts/resolve_security.py NIFTY 24000 CE 2025-03-27
This script uses the current SDK security-master path
(`dhanhq.fetch_security_list`) instead of downloading the CSV directly.
"""
from __future__ import annotations
import sys
import pandas as pd
from dhanhq import dhanhq
def load_security_master(mode: str = "compact") -> pd.DataFrame:
"""Fetch the Dhan security master through the SDK."""
print("Loading security master via dhanhq.fetch_security_list()...")
security_master = dhanhq.fetch_security_list(mode)
if security_master is None or security_master.empty:
raise SystemExit("Unable to fetch the Dhan security master.")
return security_master
def search_equity(df: pd.DataFrame, query: str, limit: int = 10) -> pd.DataFrame:
"""Return deterministic equity matches for NSE/BSE cash instruments."""
query_upper = query.upper().strip()
base_mask = (
df["SEM_INSTRUMENT_NAME"].astype(str).str.upper() == "EQUITY"
) & df["SEM_EXM_EXCH_ID"].astype(str).str.upper().isin(["NSE", "BSE"])
exact = df[
base_mask
& (df["SEM_TRADING_SYMBOL"].astype(str).str.upper() == query_upper)
]
if not exact.empty:
return exact[
[
"SEM_SMST_SECURITY_ID",
"SEM_TRADING_SYMBOL",
"SEM_CUSTOM_SYMBOL",
"SEM_EXM_EXCH_ID",
"SEM_INSTRUMENT_NAME",
]
].head(limit)
contains = df[
base_mask
& df["SEM_CUSTOM_SYMBOL"].astype(str).str.upper().str.contains(query_upper, na=False)
]
return contains[
[
"SEM_SMST_SECURITY_ID",
"SEM_TRADING_SYMBOL",
"SEM_CUSTOM_SYMBOL",
"SEM_EXM_EXCH_ID",
"SEM_INSTRUMENT_NAME",
]
].head(limit)
def search_derivative(
df: pd.DataFrame,
underlying: str,
*,
strike: float | None = None,
option_type: str | None = None,
expiry: str | None = None,
limit: int = 20,
) -> pd.DataFrame:
"""Return derivative matches for options and futures."""
base_mask = (
df["SEM_CUSTOM_SYMBOL"].astype(str).str.upper() == underlying.upper().strip()
) & df["SEM_INSTRUMENT_NAME"].astype(str).str.upper().isin(
["OPTIDX", "OPTSTK", "FUTIDX", "FUTSTK"]
)
if option_type == "FUT":
base_mask &= df["SEM_INSTRUMENT_NAME"].astype(str).str.upper().isin(["FUTIDX", "FUTSTK"])
elif option_type in {"CE", "PE"}:
base_mask &= df["SEM_OPTION_TYPE"].astype(str).str.upper() == option_type
if strike is not None:
base_mask &= df["SEM_STRIKE_PRICE"].astype(float) == float(strike)
if expiry is not None:
base_mask &= df["SEM_EXPIRY_DATE"].astype(str) == expiry
results = df[base_mask].sort_values(
by=["SEM_EXPIRY_DATE", "SEM_TRADING_SYMBOL"],
na_position="last",
)
return results[
[
"SEM_SMST_SECURITY_ID",
"SEM_TRADING_SYMBOL",
"SEM_CUSTOM_SYMBOL",
"SEM_EXM_EXCH_ID",
"SEM_INSTRUMENT_NAME",
"SEM_STRIKE_PRICE",
"SEM_OPTION_TYPE",
"SEM_EXPIRY_DATE",
"SEM_LOT_UNITS",
"SEM_TICK_SIZE",
]
].head(limit)
def parse_query(query: str) -> dict[str, str | float | None]:
"""Parse a free-form equity or derivative query."""
parts = query.upper().split()
if len(parts) <= 2 and not any(part in {"CE", "PE", "FUT", "FUTURE"} for part in parts):
return {"type": "equity", "name": query}
underlying = parts[0]
strike = None
option_type = None
expiry = None
for part in parts[1:]:
if part in {"CE", "PE"}:
option_type = part
elif part in {"FUT", "FUTURE"}:
option_type = "FUT"
elif "-" in part and len(part) == 10:
expiry = part
else:
try:
strike = float(part)
except ValueError:
underlying += f" {part}"
return {
"type": "fno",
"underlying": underlying,
"strike": strike,
"option_type": option_type,
"expiry": expiry,
}
def main() -> None:
if len(sys.argv) < 2:
print("Usage: python scripts/resolve_security.py <query>")
print('Examples: "RELIANCE", "HDFC Bank", "NIFTY 24000 CE 2025-03-27"')
raise SystemExit(1)
query = " ".join(sys.argv[1:])
df = load_security_master()
parsed = parse_query(query)
if parsed["type"] == "equity":
results = search_equity(df, str(parsed["name"]))
else:
results = search_derivative(
df,
str(parsed["underlying"]),
strike=parsed["strike"],
option_type=str(parsed["option_type"]) if parsed["option_type"] else None,
expiry=str(parsed["expiry"]) if parsed["expiry"] else None,
)
if results.empty:
print(f"No instruments found for: {query}")
return
print(f"\nResults for: {query}\n")
print(results.to_string(index=False))
if __name__ == "__main__":
main()
"""Trade journal for logging and reviewing DhanHQ orders.
Persists trade data to ${CLAUDE_PLUGIN_DATA}/trades.jsonl so history
survives across sessions and skill upgrades.
Usage:
from trade_logger import log_order, get_today_orders, get_trade_history
"""
import os
import json
from datetime import datetime, timedelta
def _get_log_path():
"""Get the stable path for trade log storage."""
plugin_data = os.environ.get("CLAUDE_PLUGIN_DATA")
if plugin_data:
os.makedirs(plugin_data, exist_ok=True)
return os.path.join(plugin_data, "trades.jsonl")
# Fallback to skill directory (may not persist across upgrades)
return os.path.join(os.path.dirname(__file__), "..", "data", "trades.jsonl")
def log_order(order_params, response, notes=""):
"""Append an order record to the trade journal.
Args:
order_params: dict of parameters passed to place_order()
response: dict response from the DhanHQ API
notes: Optional user-facing notes about this trade
"""
log_path = _get_log_path()
os.makedirs(os.path.dirname(log_path), exist_ok=True)
record = {
"timestamp": datetime.now().isoformat(),
"order_params": order_params,
"response": response,
"order_id": response.get("data", {}).get("orderId") if isinstance(response.get("data"), dict) else None,
"status": response.get("status"),
"notes": notes,
}
with open(log_path, "a") as f:
f.write(json.dumps(record) + "\n")
return record
def _read_all_records():
"""Read all records from the trade log."""
log_path = _get_log_path()
if not os.path.exists(log_path):
return []
records = []
with open(log_path) as f:
for line in f:
line = line.strip()
if line:
records.append(json.loads(line))
return records
def get_today_orders():
"""Get all orders logged today.
Returns:
list of order records from today
"""
today = datetime.now().date().isoformat()
return [r for r in _read_all_records() if r["timestamp"].startswith(today)]
def get_trade_history(days=7):
"""Get order history for the last N days.
Args:
days: Number of days to look back (default 7)
Returns:
list of order records, newest first
"""
cutoff = (datetime.now() - timedelta(days=days)).isoformat()
records = [r for r in _read_all_records() if r["timestamp"] >= cutoff]
return sorted(records, key=lambda r: r["timestamp"], reverse=True)
def get_trade_summary(days=7):
"""Generate a summary of recent trading activity.
Returns:
dict with total_orders, successful, failed, buy_count, sell_count,
instruments_traded (unique set)
"""
records = get_trade_history(days)
summary = {
"period_days": days,
"total_orders": len(records),
"successful": sum(1 for r in records if r.get("status") == "success"),
"failed": sum(1 for r in records if r.get("status") != "success"),
"buy_count": 0,
"sell_count": 0,
"instruments_traded": set(),
}
for r in records:
params = r.get("order_params", {})
txn = params.get("transaction_type", "")
if txn == "BUY":
summary["buy_count"] += 1
elif txn == "SELL":
summary["sell_count"] += 1
sid = params.get("security_id") or params.get("trading_symbol")
if sid:
summary["instruments_traded"].add(str(sid))
summary["instruments_traded"] = list(summary["instruments_traded"])
return summary
def print_today_orders():
"""Print today's orders in a human-readable format."""
orders = get_today_orders()
if not orders:
print("No orders placed today.")
return
print(f"--- Today's Orders ({len(orders)} total) ---")
for r in orders:
params = r.get("order_params", {})
status = r.get("status", "unknown")
oid = r.get("order_id", "N/A")
txn = params.get("transaction_type", "?")
sym = params.get("trading_symbol") or params.get("security_id", "?")
qty = params.get("quantity", "?")
price = params.get("price", "MKT")
time = r["timestamp"].split("T")[1][:8]
print(f" [{time}] {status.upper():8s} | {txn:4s} {qty}x {sym} @ {price} | ID: {oid}")
if __name__ == "__main__":
print_today_orders()
#!/usr/bin/env python3
"""Pre-flight order validation for DhanHQ orders.
This validator is intentionally conservative. It checks obvious SDK/order-rule
issues before an order is placed, while treating hardcoded lot sizes and freeze
quantities as fallback heuristics only.
"""
from __future__ import annotations
from datetime import datetime
# These are fallback heuristics only. Prefer security-master-derived values.
LOT_SIZES = {
"NIFTY": 75,
"BANKNIFTY": 15,
"FINNIFTY": 25,
"MIDCPNIFTY": 50,
"SENSEX": 10,
}
# Fallback freeze-quantity heuristics only.
FREEZE_QTY = {
"NIFTY": 1800,
"BANKNIFTY": 900,
"FINNIFTY": 1000,
"MIDCPNIFTY": 2800,
"SENSEX": 500,
}
VALID_EXCHANGE_SEGMENTS = {
"NSE_EQ",
"BSE_EQ",
"NSE_FNO",
"BSE_FNO",
"MCX_COMM",
"NSE_CURRENCY",
"BSE_CURRENCY",
}
EQUITY_SEGMENTS = {"NSE_EQ", "BSE_EQ"}
DERIVATIVE_SEGMENTS = {"NSE_FNO", "BSE_FNO", "MCX_COMM", "NSE_CURRENCY", "BSE_CURRENCY"}
EQUITY_PRODUCT_TYPES = {"CNC", "INTRADAY", "MARGIN", "MTF"}
DERIVATIVE_PRODUCT_TYPES = {"INTRADAY", "MARGIN"}
VALID_ORDER_TYPES = {"LIMIT", "MARKET", "STOP_LOSS", "STOP_LOSS_MARKET"}
VALID_TRANSACTION_TYPES = {"BUY", "SELL"}
VALID_VALIDITY = {"DAY", "IOC"}
NOTIONAL_WARNING_THRESHOLD = 50000
def _infer_lot_size(trading_symbol: str | None) -> int | None:
if not trading_symbol:
return None
symbol_upper = trading_symbol.upper()
for name, lot_size in LOT_SIZES.items():
if name in symbol_upper:
return lot_size
return None
def _infer_freeze_qty(trading_symbol: str | None) -> int | None:
if not trading_symbol:
return None
symbol_upper = trading_symbol.upper()
for name, freeze_qty in FREEZE_QTY.items():
if name in symbol_upper:
return freeze_qty
return None
def validate_order(
*,
security_id: str | None = None,
exchange_segment: str | None = None,
transaction_type: str | None = None,
quantity: int | None = None,
order_type: str | None = None,
product_type: str | None = None,
price: float = 0,
trigger_price: float = 0,
validity: str = "DAY",
after_market_order: bool = False,
trading_symbol: str | None = None,
lot_size: int | None = None,
) -> dict[str, object]:
"""Validate common DhanHQ order parameters before placement."""
errors: list[str] = []
warnings: list[str] = []
exchange_segment = exchange_segment.upper() if exchange_segment else exchange_segment
transaction_type = transaction_type.upper() if transaction_type else transaction_type
order_type = order_type.upper() if order_type else order_type
product_type = product_type.upper() if product_type else product_type
validity = validity.upper() if validity else validity
if not security_id:
errors.append("security_id is required")
if not exchange_segment:
errors.append("exchange_segment is required")
if not transaction_type:
errors.append("transaction_type is required")
if quantity is None or quantity <= 0:
errors.append("quantity must be a positive integer")
if not order_type:
errors.append("order_type is required")
if not product_type:
errors.append("product_type is required")
if exchange_segment and exchange_segment not in VALID_EXCHANGE_SEGMENTS:
errors.append(f"Invalid exchange_segment: {exchange_segment}")
if transaction_type and transaction_type not in VALID_TRANSACTION_TYPES:
errors.append(f"Invalid transaction_type: {transaction_type}")
if order_type and order_type not in VALID_ORDER_TYPES:
errors.append(f"Invalid order_type: {order_type}")
if validity and validity not in VALID_VALIDITY:
errors.append(f"Invalid validity: {validity}")
if order_type in {"LIMIT", "STOP_LOSS"} and price <= 0:
errors.append(f"price is required for {order_type} orders")
if order_type in {"STOP_LOSS", "STOP_LOSS_MARKET"} and trigger_price <= 0:
errors.append(f"trigger_price is required for {order_type} orders")
if exchange_segment in EQUITY_SEGMENTS and product_type and product_type not in EQUITY_PRODUCT_TYPES:
errors.append(
f"Invalid product_type '{product_type}' for equity segment '{exchange_segment}'. "
f"Valid values: {sorted(EQUITY_PRODUCT_TYPES)}"
)
if exchange_segment in DERIVATIVE_SEGMENTS and product_type and product_type not in DERIVATIVE_PRODUCT_TYPES:
errors.append(
f"Invalid product_type '{product_type}' for derivative segment '{exchange_segment}'. "
f"Valid values: {sorted(DERIVATIVE_PRODUCT_TYPES)}"
)
if order_type == "MARKET":
warnings.append(
"Dhan's current order docs say API market orders are converted to limit orders with MPP."
)
effective_lot_size = lot_size or _infer_lot_size(trading_symbol)
if exchange_segment in DERIVATIVE_SEGMENTS and quantity:
if effective_lot_size is not None and quantity % effective_lot_size != 0:
errors.append(
f"Derivative quantity must be a multiple of lot size {effective_lot_size}. Got {quantity}."
)
elif effective_lot_size is None:
warnings.append(
"Could not resolve a lot size from the provided data. Confirm lot size from the security master before placing."
)
freeze_qty = _infer_freeze_qty(trading_symbol)
if freeze_qty is not None and quantity > freeze_qty:
warnings.append(
f"Quantity {quantity} exceeds fallback freeze quantity {freeze_qty}. "
"Consider place_slice_order() after verifying the latest exchange freeze limits."
)
if price and quantity:
notional = price * quantity
if notional > NOTIONAL_WARNING_THRESHOLD:
warnings.append(
f"High notional value: Rs. {notional:,.2f} exceeds the Rs. 50,000 warning threshold."
)
if not after_market_order:
now = datetime.now()
if now.weekday() >= 5:
warnings.append("Market is closed on weekends. Use AMO only if that is intentional.")
elif now.hour < 9 or (now.hour == 9 and now.minute < 15):
warnings.append("Regular market is not yet open.")
elif now.hour > 15 or (now.hour == 15 and now.minute > 30):
warnings.append("Regular market is closed. Use AMO only if that is intentional.")
return {
"valid": not errors,
"errors": errors,
"warnings": warnings,
}
def print_validation(result: dict[str, object]) -> None:
"""Pretty-print validation output."""
if result["valid"]:
print("Order validation: PASS")
else:
print("Order validation: FAIL")
for error in result["errors"]:
print(f" ERROR: {error}")
for warning in result["warnings"]:
print(f" WARNING: {warning}")
if __name__ == "__main__":
sample = validate_order(
security_id="2885",
exchange_segment="NSE_EQ",
transaction_type="BUY",
quantity=10,
order_type="LIMIT",
product_type="CNC",
price=2450,
)
print_validation(sample)