Skip to content

32 - Master Contract Download

Overview

Master contracts contain symbol mappings between Tradeboard's standardized format and broker-specific formats. They are downloaded at most once per day, on broker login, and cached in the symtoken table plus an in-memory cache for fast symbol resolution.

Architecture Diagram

┌──────────────────────────────────────────────────────────────────────────────┐
│                    Master Contract Download Architecture                     │
└──────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────────┐
│                               Download Trigger                               │
│                                                                              │
│      ┌────────────────────────┐              ┌────────────────────────┐      │
│      │ On broker login        │              │ Force Download button  │      │
│      │ handle_auth_success()  │              │ POST /api/master-      │      │
│      │                        │              │ contract/download      │      │
│      └────────────┬───────────┘              └────────────┬───────────┘      │
│                   │                                       │                  │
│                   └───────────────────┬───────────────────┘                  │
│                                       │                                      │
│                                       ▼                                      │
│   ┌──────────────────────────────────────────────────────────────────────┐   │
│   │ should_download_master_contract() decides fresh download vs cache    │   │
│   │ then a daemon Thread runs async_master_contract_download() or        │   │
│   │ load_existing_master_contract()                                      │   │
│   └──────────────────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│                           Broker-Specific Download                           │
│                                                                              │
│  broker/{name}/database/master_contract_db.py                                │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │  1. Fetch from broker API or static URL                              │    │
│  │  2. Parse CSV/JSON format                                            │    │
│  │  3. Transform to Tradeboard format                                     │    │
│  │  4. Store in symtoken table                                          │    │
│  └─────────────────────────────────────────────────────────────────────┘     │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│                               Symbol Database                                │
│                                                                              │
│   ┌──────────────────────────────────────────────────────────────────────┐   │
│   │                            symtoken table                            │   │
│   │                                                                      │   │
│   │ symbol   │ brsymbol │ exchange  │ brexchange │ token  │ lotsize      │   │
│   │ ──────────────────────────────────────────────────────────────────── │   │
│   │ SBIN     │ SBIN-EQ  │ NSE       │ NSE        │ 779    │ 1            │   │
│   │ NIFTY    │ NIFTY    │ NSE_INDEX │ NSE        │ 26000  │ 1            │   │
│   │                                                                      │   │
│   │ Plus name, expiry, strike, instrumenttype, tick_size, contract_value │   │
│   └──────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

Download Process

1. Trigger on Broker Login

python
# utils/auth_utils.py - inside handle_auth_success(...)

def handle_auth_success(auth_token, user_session_key, broker, feed_token=None, user_id=None):
    init_broker_status(broker)

    # Smart download: skip the download if cached data is still valid for today
    should_download, reason = should_download_master_contract(broker)

    if should_download:
        Thread(target=async_master_contract_download, args=(broker,), daemon=True).start()
    else:
        Thread(target=load_existing_master_contract, args=(broker,), daemon=True).start()

2. Background Download

python
def async_master_contract_download(broker):
    """Run the broker's master contract download and record status/statistics"""
    update_status(broker, "downloading", "Master contract download in progress")

    module_path = f"broker.{broker}.database.master_contract_db"
    master_contract_module = importlib.import_module(module_path)

    master_contract_module.master_contract_download()

    # Normalise derivative underlyings, refresh the memory cache and record stats
    normalize_derivative_underlyings()
    update_status(broker, "success", "Master contract download completed successfully",
                  get_symbol_count())

3. Broker-Specific Download

Every broker plugin exposes the same entry point, master_contract_download().

python
# broker/zerodha/database/master_contract_db.py

def master_contract_download():
    """Download Zerodha master contract"""
    output_path = 'tmp/zerodha.csv'

    # Fetch https://api.kite.trade/instruments and save to output_path
    download_csv_zerodha_data(output_path)

    # Parse the CSV and map it into the Tradeboard symtoken schema
    token_df = process_zerodha_csv(output_path)
    delete_zerodha_temp_data(output_path)

    # Replace the existing symbol set with the freshly downloaded one
    delete_symtoken_table()
    copy_from_dataframe(token_df)

    return socketio.emit('master_contract_download',
                         {'status': 'success', 'message': 'Successfully Downloaded'})

Symbol Database Schema

symtoken Table

┌─────────────────────────────────────────────────────────────────────┐
│                           symtoken table                            │
├────────────────┬────────────┬───────────────────────────────────────┤
│ Column         │ Type       │ Description                           │
├────────────────┼────────────┼───────────────────────────────────────┤
│ id             │ Integer PK │ Auto-increment                        │
│ symbol         │ String     │ Tradeboard symbol, not null, indexed    │
│ brsymbol       │ String     │ Broker symbol, not null, indexed      │
│ name           │ String     │ Instrument name                       │
│ exchange       │ String     │ Tradeboard exchange code, indexed       │
│ brexchange     │ String     │ Broker exchange code, indexed         │
│ token          │ String     │ Broker instrument token, indexed      │
│ expiry         │ String     │ Expiry as DDMMMYY for F&O             │
│ strike         │ Float      │ Strike price for options              │
│ lotsize        │ Integer    │ Lot size                              │
│ instrumenttype │ String     │ EQ, FUT, CE, PE, INDEX                │
│ tick_size      │ Float      │ Minimum price tick                    │
│ contract_value │ Float      │ Contract multiplier, crypto contracts │
└────────────────┴────────────┴───────────────────────────────────────┘

Symbol Mapping

Tradeboard to Broker

python
from database.token_db import get_br_symbol

# Get broker-specific symbol
broker_symbol = get_br_symbol("SBIN", "NSE")
# Returns: "SBIN-EQ" (for Zerodha)

broker_symbol = get_br_symbol("NIFTY21JAN2521500CE", "NFO")
# Returns: "NIFTY 21JAN25 21500 CE" (for Zerodha)

Get Token

python
from database.token_db import get_token

# Get broker token for symbol
token = get_token("SBIN", "NSE")
# Returns: "779"

Get Symbol Info

python
from database.token_db import get_symbol_info

info = get_symbol_info("NIFTY", "NFO")
# Returns: {
#     "symbol": "NIFTY",
#     "lotsize": 65,
#     "tick_size": 0.05,
#     "expiry": "2025-01-30"
# }

Broker Implementations

Zerodha

python
# broker/zerodha/database/master_contract_db.py

URL = "https://api.kite.trade/instruments"
FORMAT = "CSV"

def master_contract_download():
    # CSV columns: instrument_token, exchange_token, tradingsymbol,
    #              name, last_price, expiry, strike, tick_size,
    #              lot_size, instrument_type, segment, exchange
    ...

Dhan

python
# broker/dhan/database/master_contract_db.py

URL = "https://images.dhan.co/api-data/api-scrip-master.csv"
FORMAT = "CSV"

Angel One

python
# broker/angel/database/master_contract_db.py

URL = "https://margincalculator.angelbroking.com/OpenAPI_File/files/OpenAPIScripMaster.json"
FORMAT = "JSON"

Caching Strategy

In-Memory Cache

database/token_db.py is a thin compatibility shim that re-exports everything from database/token_db_enhanced.py, which keeps the whole symbol set (100,000+ rows) in memory for O(1) lookups instead of expiring individual entries.

python
# database/token_db.py
from database.token_db_enhanced import (
    get_token, get_symbol, get_oa_symbol, get_br_symbol, get_brexchange,
    get_symbol_info, get_symbol_count, get_tokens_bulk, get_symbols_bulk,
    search_symbols, load_cache_for_broker, clear_cache, get_cache_stats,
)

After a successful download, database/master_contract_cache_hook.py (hook_into_master_contract_download) reloads the memory cache for the broker. Every lookup also has a *_dbquery twin (get_token_dbquery, get_symbol_dbquery, and so on) that bypasses the cache and hits the database directly.

Database Index

SymToken in database/symbol.py declares three composite indices, plus single column indices on symbol, brsymbol, exchange, brexchange and token.

sql
CREATE INDEX idx_symbol_exchange ON symtoken(symbol, exchange);
CREATE INDEX idx_symbol_name ON symtoken(symbol, name);
CREATE INDEX idx_brsymbol_exchange ON symtoken(brsymbol, exchange);

Status Tracking

master_contract_status Table

┌────────────────────────────────────────────────────────────────────────────────┐
│                          master_contract_status table                          │
├───────────────────────────┬───────────┬────────────────────────────────────────┤
│ Column                    │ Type      │ Description                            │
├───────────────────────────┼───────────┼────────────────────────────────────────┤
│ broker                    │ String PK │ Broker name, primary key               │
│ status                    │ String    │ pending, downloading, success, error   │
│ message                   │ String    │ Human readable status detail           │
│ last_updated              │ DateTime  │ Last status write                      │
│ total_symbols             │ String    │ Symbol count, defaults to "0"          │
│ is_ready                  │ Boolean   │ Symbols usable, defaults to false      │
│ last_download_time        │ DateTime  │ When a download last succeeded         │
│ download_date             │ Date      │ Trading day of the download            │
│ exchange_stats            │ Text      │ JSON, e.g. {"NSE": 2500, "NFO": 85000} │
│ download_duration_seconds │ Integer   │ Download wall time                     │
└───────────────────────────┴───────────┴────────────────────────────────────────┘

Stuck Download Detection

The system auto-detects stuck master contract downloads. If a download stays in 'downloading' state for more than DOWNLOAD_TIMEOUT_MINUTES (5 minutes, a module level constant, not an environment variable), it is automatically marked as an error, and the UI enables the "Force Download" button for manual retry.

python
# database/master_contract_status_db.py
DOWNLOAD_TIMEOUT_MINUTES = 5  # Module level constant

def get_status(broker):
    """
    Check download status with stuck detection.
    If status='downloading' and last_updated is older than DOWNLOAD_TIMEOUT_MINUTES,
    auto-mark as error and suggest Force Download.
    """
    status = MasterContractStatus.query.filter_by(broker=broker).first()
    if (status.status == "downloading"
            and datetime.now() - status.last_updated > timedelta(minutes=DOWNLOAD_TIMEOUT_MINUTES)):
        update_status(broker, "error",
            f"Download timed out (stuck for >{DOWNLOAD_TIMEOUT_MINUTES} minutes). "
            "Click Force Download to retry."
        )

Force Download is exposed by blueprints/master_contract_status.py at POST /api/master-contract/download. The same blueprint serves GET /api/master-contract/status, GET /api/master-contract/ready and GET /api/master-contract/smart-status.

Error Handling

Download Failures

python
def master_contract_download():
    try:
        response = client.get(url, timeout=60)
        response.raise_for_status()
        process_data(response.text)
        return socketio.emit('master_contract_download',
                             {'status': 'success', 'message': 'Successfully Downloaded'})
    except Exception as e:
        logger.error(f"Download failed: {e}")
        return socketio.emit('master_contract_download',
                             {'status': 'error', 'message': str(e)})

async_master_contract_download wraps this call and writes the outcome to the master_contract_status table with update_status(broker, "success"|"error", message).

Fallback to Cache

If download fails, use existing cached data:

python
def get_symbol(symbol, exchange):
    result = db_lookup(symbol, exchange)
    if result:
        return result

    # Log warning but don't fail
    logger.warning(f"Symbol not found: {symbol}:{exchange}")
    return None

Daily Refresh

There is no cron job. The refresh is login driven: should_download_master_contract(broker) in utils/auth_utils.py compares the last successful download against a per day cutoff time, so the first login after the cutoff downloads fresh data and every later login on the same day reuses the cached symbols.

Cutoff Configuration

Env VariableDefault in codeTimezoneApplies to
MASTER_CONTRACT_CUTOFF_TIME08:00ISTIndian exchange brokers
CRYPTO_MASTER_CONTRACT_CUTOFF_TIME00:00UTCCrypto brokers in CRYPTO_BROKERS

CRYPTO_BROKERS is defined in utils/constants.py and currently contains deltaexchange. The format for both variables is HH:MM in 24-hour form.

python
# utils/auth_utils.py
def should_download_master_contract(broker):
    """
    - Never downloaded before: download
    - Different calendar day in the reference timezone: download
    - Same day but downloaded before the cutoff: download
    - Same day and downloaded after the cutoff: skip and use cached symbols
    - A different broker downloaded more recently: download (symtoken is stale)
    """

When the download is skipped, load_existing_master_contract(broker) marks the status ready from the existing symtoken rows instead.

Key Files Reference

FilePurpose
broker/*/database/master_contract_db.pyBroker download (master_contract_download())
database/symbol.pySymToken model, symtoken table
database/token_db.pySymbol lookup (re-exports token_db_enhanced)
database/token_db_enhanced.pyFull memory symbol cache
database/master_contract_cache_hook.pyCache reload after download
database/master_contract_status_db.pyStatus tracking (MasterContractStatus)
blueprints/master_contract_status.pyStatus and Force Download routes
utils/auth_utils.pySmart download decision and async trigger