Skip to main content

Overview

The OHLCV Data API retrieves historical and current-day daily candlestick data for NSE equities. It uses a hybrid approach combining historical tick data API with live market snapshots to provide complete OHLCV coverage including today’s data. Source File: fetch_all_ohlcv.py

Endpoint Details

Historical Tick Data API

URL
string
required
https://openweb-ticks.dhan.co/getDataH
Method
string
required
POST

Live Snapshot API

URL
string
required
https://ow-scanx-analytics.dhan.co/customscan/fetchdt
Method
string
required
POST

Request Headers

{
  "Content-Type": "application/json",
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
  "Accept": "application/json, text/plain, */*",
  "Origin": "https://scanx.dhan.co",
  "Referer": "https://scanx.dhan.co/"
}

Request Payloads

Historical OHLCV Request

EXCH
string
required
Exchange code (“NSE” for NSE equities)
SYM
string
required
Trading symbol
SEG
string
required
Segment (“E” for equities)
INST
string
required
Instrument type (“EQUITY”)
SEC_ID
string
required
Security ID (Sid from master_isin_map.json)
EXPCODE
integer
default:"0"
Expiry code (0 for equities)
INTERVAL
string
required
Timeframe - “D” for daily, “W” for weekly, “M” for monthly
START
integer
required
Start timestamp (Unix epoch seconds)
END
integer
required
End timestamp (Unix epoch seconds)

Example Historical Payload

{
  "EXCH": "NSE",
  "SYM": "RELIANCE",
  "SEG": "E",
  "INST": "EQUITY",
  "SEC_ID": "11536",
  "EXPCODE": 0,
  "INTERVAL": "D",
  "START": 1672531200,
  "END": 1705449600
}

Live Snapshot Request

{
  "data": {
    "sort": "Volume",
    "sorder": "desc",
    "count": 5000,
    "fields": ["Sym", "Open", "High", "Low", "Ltp", "Volume"],
    "params": [{"field": "Exch", "op": "", "val": "NSE"}]
  }
}

Example Requests

Historical OHLCV

curl -X POST https://openweb-ticks.dhan.co/getDataH \
  -H "Content-Type: application/json" \
  -H "Origin: https://scanx.dhan.co" \
  -H "Referer: https://scanx.dhan.co/" \
  -d '{
    "EXCH": "NSE",
    "SYM": "RELIANCE",
    "SEG": "E",
    "INST": "EQUITY",
    "SEC_ID": "11536",
    "EXPCODE": 0,
    "INTERVAL": "D",
    "START": 1672531200,
    "END": 1705449600
  }'

Live Snapshot

curl -X POST https://ow-scanx-analytics.dhan.co/customscan/fetchdt \
  -H "Content-Type: application/json" \
  -H "Origin: https://scanx.dhan.co" \
  -d '{
    "data": {
      "sort": "Volume",
      "sorder": "desc",
      "count": 5000,
      "fields": ["Sym", "Open", "High", "Low", "Ltp", "Volume"],
      "params": [{"field": "Exch", "op": "", "val": "NSE"}]
    }
  }'

Response Structure

Historical OHLCV Response

data.Time
array
Array of timestamps or date strings
data.o
array
Array of open prices
data.h
array
Array of high prices
data.l
array
Array of low prices
data.c
array
Array of close prices
data.v
array
Array of volumes

Example Historical Response

{
  "data": {
    "Time": ["2024-01-15", "2024-01-16", "2024-01-17"],
    "o": [2590.0, 2600.5, 2595.0],
    "h": [2610.0, 2615.5, 2608.0],
    "l": [2585.0, 2595.0, 2590.0],
    "c": [2605.0, 2598.5, 2602.0],
    "v": [5234567, 4876543, 5123456]
  }
}

Live Snapshot Response

{
  "data": [
    {
      "Sym": "RELIANCE",
      "Open": 2598.0,
      "High": 2615.5,
      "Low": 2590.0,
      "Ltp": 2605.0,
      "Volume": 5234567
    }
  ]
}

Implementation Details

Hybrid Multi-Chunk Strategy

import json
import requests
import csv
import time
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

TICK_API_URL = "https://openweb-ticks.dhan.co/getDataH"
SCANX_URL = "https://ow-scanx-analytics.dhan.co/customscan/fetchdt"
CHUNK_DAYS = 180  # Fetch in 6-month chunks
MAX_THREADS = 15

def get_live_snapshots():
    """Fetch live OHLCV snapshot for all stocks (Today's data)"""
    payload = {
        "data": {
            "sort": "Volume", "sorder": "desc", "count": 5000,
            "fields": ["Sym", "Open", "High", "Low", "Ltp", "Volume"],
            "params": [{"field": "Exch", "op": "", "val": "NSE"}]
        }
    }
    try:
        response = requests.post(SCANX_URL, json=payload, headers=headers, timeout=15)
        if response.status_code == 200:
            return {i['Sym']: i for i in response.json().get('data', [])}
    except:
        pass
    return {}

def fetch_history_chunk(payload):
    """Fetch a single chunk of historical data"""
    try:
        response = requests.post(TICK_API_URL, json=payload, headers=headers, timeout=15)
        if response.status_code == 200:
            data = response.json().get("data", {})
            times = data.get("Time", [])
            if times:
                o, h, l, c, v = data.get("o", []), data.get("h", []), data.get("l", []), data.get("c", []), data.get("v", [])
                rows = []
                for i in range(len(times)):
                    t = times[i]
                    dt_str = t if isinstance(t, str) else datetime.fromtimestamp(t).strftime("%Y-%m-%d")
                    rows.append({
                        'Date': dt_str,
                        'Open': o[i],
                        'High': h[i],
                        'Low': l[i],
                        'Close': c[i],
                        'Volume': v[i]
                    })
                return rows
    except:
        pass
    return []

def fetch_single_stock(sym, details, live_snapshot=None):
    output_path = f"ohlcv_data/{sym}.csv"
    today_str = datetime.now().strftime("%Y-%m-%d")
    
    # Determine starting point
    global_start_ts = int(time.time()) - (2 * 365 * 86400)  # 2 years back
    target_start = global_start_ts
    
    existing_rows = []
    if os.path.exists(output_path):
        try:
            with open(output_path, "r") as f:
                existing_rows = list(csv.DictReader(f))
                if existing_rows:
                    last_date = existing_rows[-1]["Date"]
                    last_dt = datetime.strptime(last_date, "%Y-%m-%d")
                    target_start = int(last_dt.timestamp()) + 86400  # Next day
        except:
            pass
    
    # Fetch missing history in chunks
    new_rows = []
    current_end = int(time.time())
    
    if target_start < current_end - 86400:
        chunk_ptr = current_end
        while chunk_ptr > target_start:
            c_start = max(target_start, chunk_ptr - (CHUNK_DAYS * 86400))
            payload = {
                "EXCH": details["Exch"],
                "SYM": sym,
                "SEG": details["Seg"],
                "INST": details["Inst"],
                "SEC_ID": details["Sid"],
                "EXPCODE": 0,
                "INTERVAL": "D",
                "START": int(c_start),
                "END": int(chunk_ptr)
            }
            chunk_rows = fetch_history_chunk(payload)
            if chunk_rows:
                new_rows.extend(chunk_rows)
            
            chunk_ptr = c_start - 86400
    
    # Add today's data from live snapshot
    if live_snapshot:
        s = live_snapshot
        today_row = {
            'Date': today_str,
            'Open': s.get('Open', 0),
            'High': s.get('High', 0),
            'Low': s.get('Low', 0),
            'Close': s.get('Ltp', 0),
            'Volume': s.get('Volume', 0)
        }
        new_rows.append(today_row)
    
    if not new_rows:
        return "uptodate"
    
    # Merge and deduplicate
    merged = {r['Date']: r for r in existing_rows + new_rows}
    final_rows = sorted(merged.values(), key=lambda x: x['Date'])
    
    if not final_rows:
        return "uptodate"
    
    # Save to CSV
    with open(output_path, "w", newline='') as f:
        writer = csv.DictWriter(f, fieldnames=['Date', 'Open', 'High', 'Low', 'Close', 'Volume'])
        writer.writeheader()
        writer.writerows(final_rows)
    
    return "success"

Configuration

Max Threads
integer
default:"15"
Concurrent threads for parallel processing
Chunk Size
integer
default:"180"
Days per chunk (6 months)
Default History
string
2 years back from today (ensures 200-day MA calculation)
Timeout
integer
default:"15"
Request timeout in seconds
Input File
string
required
dhan_data_response.json (requires Sid field)
Output Directory
string
ohlcv_data/{SYMBOL}.csv

Output Structure

Each stock gets its own CSV file:
ohlcv_data/
├── RELIANCE.csv
├── TCS.csv
└── HDFCBANK.csv

CSV Format

Date,Open,High,Low,Close,Volume
2024-01-15,2590.0,2610.0,2585.0,2605.0,5234567
2024-01-16,2600.5,2615.5,2595.0,2598.5,4876543
2024-01-17,2595.0,2608.0,2590.0,2602.0,5123456

Performance Metrics

  • Total Stocks: ~2,775
  • Threads: 15 concurrent requests
  • History Depth: 2 years (default)
  • Chunk Size: 180 days
  • Time per Stock: 5-10 seconds (initial), 1-2 seconds (incremental)
  • Total Time: ~30-45 minutes (initial full sync), ~5-10 minutes (daily updates)
  • Success Rate: >95%
  • Output Size: ~300-500 KB per stock (2 years daily data)

Progress Tracking

Syncing OHLCV for 2775 stocks (Hybrid Multi-Chunk Mode)...
Done! Updated: 2650 | UpToDate: 100 | Errors: 25

Incremental Updates

The implementation supports efficient incremental updates:
  1. Check Existing File: Read last date from CSV
  2. Calculate Gap: Determine missing date range
  3. Fetch Gap Only: Request only missing data
  4. Add Today: Append live snapshot for current day
  5. Merge & Save: Combine with existing data

Incremental Example

# Existing file has data up to 2024-01-10
# Today is 2024-01-17
# Script fetches only 2024-01-11 to 2024-01-16 from tick API
# Adds 2024-01-17 from live snapshot
# Merges all and saves

Use Cases

  1. Technical Analysis: Calculate moving averages, RSI, MACD
  2. Backtesting: Test trading strategies on historical data
  3. Pattern Recognition: Identify chart patterns
  4. Volatility Analysis: Calculate ATR, Bollinger Bands
  5. Volume Analysis: Track volume trends and spikes
  6. Price Action: Study candlestick patterns
  7. Data Exports: Build custom analytics dashboards

Data Quality

Adjustments

  • Data is NOT adjusted for splits/bonuses by default
  • Manual adjustment required for corporate actions
  • Use corporate actions API to identify adjustment dates

Gaps

  • Market holidays: No data (expected gaps)
  • Stock suspensions: No data during suspension
  • Newly listed: Data starts from listing date
  • Delisted: Data ends at delisting date

Technical Indicators Examples

Simple Moving Average (SMA)

import pandas as pd

df = pd.read_csv("ohlcv_data/RELIANCE.csv")
df['SMA_50'] = df['Close'].rolling(window=50).mean()
df['SMA_200'] = df['Close'].rolling(window=200).mean()

RSI (Relative Strength Index)

def calculate_rsi(df, period=14):
    delta = df['Close'].diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    rs = gain / loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

df['RSI_14'] = calculate_rsi(df)

Volume Analysis

# Average volume
df['VolMA_20'] = df['Volume'].rolling(window=20).mean()

# Volume spikes
df['VolSpike'] = df['Volume'] > (df['VolMA_20'] * 2)

Error Handling

Missing Sid

if not sid:
    return "error"  # Skip stocks without Sid

API Timeout

try:
    response = requests.post(url, json=payload, headers=headers, timeout=15)
except requests.Timeout:
    return "error"  # Timeout after 15 seconds

Empty Response

if not times:
    return []  # No data for this chunk (market holiday, etc.)

Notes

  • Sid (Security ID) is mandatory - stocks without Sid are skipped
  • Data includes both historical and current-day (live) candles
  • Current day data updates during market hours (9:15 AM - 3:30 PM IST)
  • Post-market, current day becomes historical next trading day
  • Chunk size of 180 days balances API limits and efficiency
  • CSV format allows easy import into Excel, pandas, or TA-Lib
  • Date format is YYYY-MM-DD for universal compatibility
  • Volume is in number of shares (not value)
  • All prices are in INR
  • Run daily after market close for up-to-date data
  • Initial sync takes 30-45 minutes; incremental updates take 5-10 minutes
  • Consider running incrementally (only fetch new data) for daily updates

Build docs developers (and LLMs) love