wiz-trader 0.41.0
A Python SDK for connecting to the Wizzer.
License: MIT
Requires Python: >=3.6
Keywords: finance, trading, sdk

# WizTrader SDK Documentation

## Table of Contents

1. [Introduction](#introduction)
2. [Installation](#installation)
3. [Authentication](#authentication)
4. [Quotes Client](#quotes-client)
    - [Initialization](#quotes-client-initialization)
    - [Connection Methods](#connection-methods)
    - [Callbacks](#callbacks)
    - [Subscribing to Instruments](#subscribing-to-instruments)
    - [Unsubscribing from Instruments](#unsubscribing-from-instruments)
    - [Account Events](#account-events)
    - [Corporate Events](#corporate-events)
    - [Handling WebSocket Connection](#handling-websocket-connection)
    - [Complete Examples](#quotes-client-examples)
5. [Wizzer Client](#wizzer-client)
    - [Initialization](#wizzer-client-initialization)
    - [Constants](#constants)
    - [Data Hub Methods](#data-hub-methods)
    - [Order Management](#order-management)
    - [Portfolio Management](#portfolio-management)
    - [Basket Management](#basket-management)
    - [Instrument Management](#instrument-management)
    - [Instrument Screener](#instrument-screener)
    - [Analytics API](#analytics-api)
    - [Complete Examples](#wizzer-client-examples)
6. [Common Use Cases](#common-use-cases)
7. [Error Handling](#error-handling)
8. [Troubleshooting](#troubleshooting)
9. [API Reference](#api-reference)

## Introduction

WizTrader is a Python SDK for connecting to the Wizzer trading platform. It provides two main client classes:

- **QuotesClient**: For real-time market data streaming via WebSockets
- **WizzerClient**: For REST API access to trading, order management, and market data, including data pipelines and classification APIs.

This documentation covers all the ways to use the SDK, with examples for each endpoint and common use cases.

## Installation

You can install the package directly from PyPI:

```bash
pip install wiz_trader
```

To install the development version from GitHub:

```bash
pip install git+https://github.com/wizzer-tech/wiz_trader.git
```

## Authentication

There are two ways to provide authentication credentials to the SDK:

1. **Direct parameter passing** (Recommended for most use cases)
2. **Environment variables** (Recommended for production deployments)

### Direct Parameter Passing

Simply provide the required parameters directly when initializing the clients:

```python
from wiz_trader import QuotesClient, WizzerClient

quotes_client = QuotesClient(
    base_url="wss://websocket-url/quotes",
    token="your-jwt-token",
    log_level="info"
)

wizzer_client = WizzerClient(
    base_url="https://api-url.in",
    token="your-jwt-token",
    strategy_id="str_01hxgwgfxgfrxb48bzchnve7gm",  # Optional
    log_level="info"
)
```

### Environment Variables

Set the following environment variables:

```bash
export WZ__QUOTES_BASE_URL="wss://websocket-url/quotes"
export WZ__API_BASE_URL="https://api-url.in"
export WZ__TOKEN="your-jwt-token"
export WZ__STRATEGY_ID="str_01hxgwgfxgfrxb48bzchnve7gm"  # Optional
```

Then initialize the clients without parameters:

```python
from wiz_trader import QuotesClient, WizzerClient

quotes_client = QuotesClient(log_level="info")
wizzer_client = WizzerClient(log_level="info")
```

You can also use a hybrid approach, providing some parameters directly and letting others fall back to environment variables:

```python
quotes_client = QuotesClient(log_level="debug")  # Only override log level
```

## Quotes Client

The `QuotesClient` enables you to connect to Wizzer's WebSocket server for real‑time market data using **plain synchronous** callbacks—no `async def` required.

### Quotes Client Initialization

You can initialize the `QuotesClient` in several ways:

```python
from wiz_trader import QuotesClient

# Method 1: All parameters provided directly
client = QuotesClient(
    base_url="wss://websocket-url/quotes",
    token="your-jwt-token",
    log_level="info",         # Options: "error", "info", "debug"
    max_message_size=10 * 1024 * 1024,  # Optional: default 10MB
    batch_size=20             # Optional: default 20 instruments per batch
)

# Method 2: Using environment variables
# (Requires WZ__QUOTES_BASE_URL and WZ__TOKEN set)
client = QuotesClient(log_level="info")

# Method 3: Mixed approach
client = QuotesClient(
    base_url="wss://custom-url/quotes",
    log_level="debug"         # token from WZ__TOKEN env var
)
```

### Connection Methods

#### Blocking Connection

Blocks your main thread, similar to Zerodha’s KiteTicker:

```python
client.connect()
```

#### Non‑Blocking Connection

Run alongside your own `asyncio` code:

```python
client.connect_async()
# … do other work …
client.stop()
```

### Callbacks

All callbacks are **plain `def`** functions.  Inside them you can call `subscribe(...)`, which under the hood schedules the actual async work—so you never `await` in your callbacks.

The QuotesClient routes messages based on their `type` field:
- Messages with `type: 'ticks'` are sent to the `on_tick` callback
- Messages with `type: 'greeks'` are sent to the `on_stats` callback
- Messages without a `type` field are sent to `on_tick` for backward compatibility

```python
def on_tick(ws, tick):
    print("Tick:", tick)

def on_stats(ws, stats):
    print("Stats:", stats)
    # Stats/Greek messages contain: {'type': 'greeks', 'identifier': '...', 'rho': 0.1, 'theta': 9.8, ...}

def on_connect(ws):
    print("Connected!")
    # Subscribe with different modes
    ws.subscribe(["NSE:SBIN:3045"], mode="ticks")      # Will receive tick messages
    ws.subscribe(["NSE:NIFTY25JULFUT:53216"], mode="greeks") # Will receive greek messages
    ws.subscribe(["NSE:RELIANCE:2885"], mode="full")   # Will receive both types

def on_close(ws, code, reason):
    print(f"Connection closed [{code}]: {reason}")
    ws.stop()   # to prevent auto‑reconnect

def on_error(ws, error):
    print("Error:", error)

client.on_tick    = on_tick
client.on_stats   = on_stats
client.on_connect = on_connect
client.on_close   = on_close
client.on_error   = on_error
```

### Subscribing to Instruments

Call `ws.subscribe([...])` directly; the SDK will batch large lists and send them over the socket:

```python
def on_connect(ws):
    # subscribe without await
    ws.subscribe([
        "NSE:SBIN:3045",
        "NSE:ICICIBANK:4963",
        "NSE:RELIANCE:2885",
    ])
```

### Unsubscribing from Instruments

Use the `unsubscribe` method to stop receiving real-time data for specific instruments. This helps manage bandwidth and reduces unnecessary data processing.

```python
# Unsubscribe from specific instruments
ws.unsubscribe(["NSE:SBIN:3045", "NSE:ICICIBANK:4963"])

# You can also unsubscribe from a single instrument
ws.unsubscribe(["NSE:RELIANCE:2885"])
```

**Key Features:**
- Immediately stops tick data for the specified instruments
- Reduces network bandwidth and processing overhead
- Can be called multiple times for different instrument sets
- No callback fired for unsubscribed instruments

### Account Events

The QuotesClient now supports automatic reception of account-related events (orders, trades, positions, holdings) from the quotes server. These events are automatically pushed to SDK users via dedicated callback handlers based on the event type.

#### Event Handlers

Register event handlers to receive real-time updates for your account activity:

```python
def on_order(ws, order):
    print(f"Order event: {order}")

def on_trade(ws, trade):
    print(f"Trade event: {trade}")

def on_position(ws, position):
    print(f"Position event: {position}")

def on_holding(ws, holding):
    print(f"Holding event: {holding}")

# Register event handlers
client.on_order = on_order
client.on_trade = on_trade
client.on_position = on_position
client.on_holding = on_holding
```

#### Key Features

- **Automatic Event Reception**: No explicit subscription needed - events are automatically sent based on your JWT token's account ID and strategy ID
- **Optional Handlers**: Events are silently dropped if no handler is registered - you only receive what you need
- **Message Normalization**: The SDK automatically handles different message formats and delivers clean, flattened event data to your handlers
- **Error Resilience**: Handler exceptions are caught and logged without breaking the event flow
- **Backward Compatible**: Existing tick and Greeks functionality remains unchanged

#### Event Data Structure

All event handlers receive flattened event data with the event type preserved:

```python
# Example order event data
{
    "_id": "order_01k10np7weemkv812nx55bppw5",
    "accountEntityType": "client",
    "accountId": "user_01j9brznsffphvzqn9gzyj1frw",
    "avgPrice": 72.65,
    "broker": "wizzer",
    "brokerOrderId": "dssorder_175344314711184",
    "brokerTimestamp": "2025-07-25T11:32:28.147Z",
    "cancelledQty": 0,
    "completedAt": "2025-07-25T11:32:28.147Z",
    "createdAt": "2025-07-25T11:32:27.722Z",
    "disclosedQty": 0,
    "exchange": "NSE",
    "exchangeOrderId": "dssx_1753443147",
    "exchangeToken": 11184,
    "exchangeUpdateTimestamp": "2025-07-25T11:32:28.147Z",
    "executionType": "regular",
    "filledQty": 1,
    "guid": "t0lVueivgBgcEh2k_BP",
    "instrumentType": "EQLC",
    "isDealer": false,
    "isOrderModified": false,
    "legs": [],
    "marginUsed": 0,
    "meta": {
        "dss": {"orderId": "order_01k10np7weemkv812nx55bppw5"},
        "source": "wizzer.dss",
        "wizzer": {"mode": "paper_trading"}
    },
    "orderType": "MARKET",
    "parentOrderId": "",
    "pendingQty": 1,
    "placedBy": "user_01j9brznsffphvzqn9gzyj1frw",
    "price": 0,
    "product": "CNC",
    "qty": 1,
    "remarks": "",
    "segment": "NSECM",
    "slPrice": 0,
    "slTriggerPrice": 0,
    "sqOffHoldingId": "",
    "sqOffPositionId": "",
    "status": "COMPLETED",
    "stoploss": 0,
    "stoplossOrderId": "",
    "strategy": {
        "id": "stgy_01jtn7t3erf8ss78spqy40k8en",
        "identifier": "S-608377",
        "name": "Test 1122"
    },
    "target": 0,
    "targetOrderId": "",
    "tradingSymbol": "IDFCFIRSTB",
    "transactionType": "BUY",
    "triggerId": "",
    "triggerPrice": 0,
    "updatedAt": "2025-07-25T11:32:28.254Z",
    "userId": "user_01j9brznsffphvzqn9gzyj1frw",
    "validity": "DAY",
    "variety": "REGULAR"
}

# Example trade event data
{
    "_id": "trade_01k10np8rqef793zr93ctqj0wt",
    "accountId": "user_01j9brznsffphvzqn9gzyj1frw",
    "avgPrice": 72.65,
    "broker": "wizzer",
    "brokerOrderId": "dssorder_175344314711184",
    "createdAt": "2025-07-25T11:32:28.567Z",
    "exchange": "NSE",
    "exchangeOrderId": "dssx_1753443147",
    "exchangeTimestamp": "2025-07-25T11:32:28.147Z",
    "exchangeToken": 11184,
    "isDividend": false,
    "meta": {
        "dss": {"orderId": "order_01k10np7weemkv812nx55bppw5"},
        "source": "wizzer.dss",
        "wizzer": {"mode": "paper_trading"}
    },
    "orderId": "order_01k10np7weemkv812nx55bppw5",
    "orderType": "MARKET",
    "parentOrderId": "",
    "parentTradeId": "",
    "product": "CNC",
    "qty": 1,
    "segment": "NSECM",
    "strategy": {
        "id": "stgy_01jtn7t3erf8ss78spqy40k8en",
        "identifier": "S-608377",
        "name": "Test 1122"
    },
    "tradeId": "dsstrade_50PccSfWvsCg-Cr5dQE",
    "tradingSymbol": "IDFCFIRSTB",
    "transactionType": "BUY",
    "updatedAt": "2025-07-25T11:32:28.567Z",
    "userId": "user_01j9brznsffphvzqn9gzyj1frw"
}

# Example position event data
{
    "_id": "pos_01k10np8t7e0fs63q52hacbx8m",
    "accountEntityType": "client",
    "accountId": "user_01j9brznsffphvzqn9gzyj1frw",
    "bep": 0,
    "broker": "wizzer",
    "buyAvgPrice": 72.65,
    "buyOrders": ["order_01k10np7weemkv812nx55bppw5"],
    "buyQty": 1,
    "buyTrades": ["trade_01k10np8rqef793zr93ctqj0wt"],
    "buyValue": 72.65,
    "closePrice": 0,
    "createdAt": "2025-07-25T11:32:28.614Z",
    "exchange": "NSE",
    "exchangeToken": 11184,
    "instrumentType": "EQLC",
    "lotSize": 1,
    "meta": {
        "source": "wizzer.dss",
        "wizzer": {"mode": "paper_trading"}
    },
    "netQty": 1,
    "pnlPercentage": 0,
    "product": "CNC",
    "realisedPnlToday": 0,
    "segment": "NSECM",
    "sellAvgPrice": 0,
    "sellOrders": [],
    "sellQty": 0,
    "sellTrades": [],
    "sellValue": 0,
    "slPrice": null,
    "slTriggerPrice": null,
    "status": "open",
    "stoploss": 0,
    "strategy": {
        "id": "stgy_01jtn7t3erf8ss78spqy40k8en",
        "identifier": "S-608377",
        "name": "Test 1122"
    },
    "target": 0,
    "tradingSymbol": "IDFCFIRSTB",
    "unrealisedPnlToday": 0,
    "updatedAt": "2025-07-25T11:32:28.614Z",
    "userId": "user_01j9brznsffphvzqn9gzyj1frw"
}

# Example holding event data
{
    "_id": "holding_01k10np8t7e0fs63q52hacbx8m",
    "accountEntityType": "client",
    "accountId": "user_01j9brznsffphvzqn9gzyj1frw",
    "bep": 0,
    "broker": "wizzer",
    "buyAvgPrice": 72.65,
    "buyOrders": ["order_01k10np7weemkv812nx55bppw5"],
    "buyQty": 1,
    "buyTrades": ["trade_01k10np8rqef793zr93ctqj0wt"],
    "buyValue": 72.65,
    "closePrice": 0,
    "createdAt": "2025-07-25T11:32:28.614Z",
    "exchange": "NSE",
    "exchangeToken": 11184,
    "instrumentType": "EQLC",
    "lotSize": 1,
    "meta": {
        "source": "wizzer.dss",
        "wizzer": {"mode": "paper_trading"}
    },
    "netQty": 1,
    "pnlPercentage": 0,
    "product": "CNC",
    "realisedPnlToday": 0,
    "segment": "NSECM",
    "sellAvgPrice": 0,
    "sellOrders": [],
    "sellQty": 0,
    "sellTrades": [],
    "sellValue": 0,
    "slPrice": null,
    "slTriggerPrice": null,
    "status": "open",
    "stoploss": 0,
    "strategy": {
        "id": "stgy_01jtn7t3erf8ss78spqy40k8en",
        "identifier": "S-608377",
        "name": "Test 1122"
    },
    "target": 0,
    "tradingSymbol": "IDFCFIRSTB",
    "unrealisedPnlToday": 0,
    "updatedAt": "2025-07-25T11:32:28.614Z",
    "userId": "user_01j9brznsffphvzqn9gzyj1frw"
}
```

#### Complete Event Handling Example

```python
from wiz_trader import QuotesClient

# Initialize client
client = QuotesClient(
    base_url="wss://websocket-url/quotes",
    token="your-jwt-token",
    log_level="info"
)

def on_order(ws, order):    
    # React to order status changes
    if order['status'] == ORDER_STATUS_COMPLETED:
        print(f"Order {order['_id']} completed!")
    elif order['status'] == ORDER_STATUS_REJECTED:
        print(f"Order {order['_id']} rejected: {order.get('remarks', 'Unknown')}")

def on_trade(ws, trade):
    print(f"Trade executed: {trade['qty']} x {trade['tradingSymbol']} @ {trade['avgPrice']}")

def on_position(ws, position):
    print(f"Position update: {position['tradingSymbol']} = {position['netQty']} (PnL: {position.get('unrealisedPnlToday', 0)})")

def on_holding(ws, holding):
    print(f"Holding update: {holding['tradingSymbol']} = {holding['netQty']}")

def on_connect(ws):
    print("Connected! Ready to receive events.")
    # Also subscribe to some ticks if needed
    ws.subscribe(["NSE:SBIN:3045"], mode="ticks")

# Register all handlers
client.on_order = on_order
client.on_trade = on_trade
client.on_position = on_position
client.on_holding = on_holding
client.on_connect = on_connect

# Connect and receive events
client.connect()
```

#### Event-Driven Trading Strategy Example

```python
class EventDrivenStrategy:
    def __init__(self):
        self.orders = {}
        self.positions = {}
        self.pending_orders = set()
    
    def on_order_event(self, ws, order):
        order_id = order['_id']
        self.orders[order_id] = order
        
        # Track pending orders
        if order['status'] == ORDER_STATUS_OPEN:
            self.pending_orders.add(order_id)
        elif order_id in self.pending_orders:
            self.pending_orders.remove(order_id)
        
        # Log order lifecycle
        print(f"Order {order_id}: {order['status']}")
        
        # React to rejections
        if order['status'] == ORDER_STATUS_REJECTED:
            print(f"Order rejected! Reason: {order.get('remarks', 'Unknown')}")
            # Implement retry logic or alternative strategy
    
    def on_position_event(self, ws, position):
        symbol = position['tradingSymbol']
        self.positions[symbol] = position
        
        # Monitor position changes
        print(f"Position {symbol}: Qty={position['netQty']}, PnL={position.get('unrealisedPnlToday', 0)}")
        
        # Implement stop-loss or take-profit logic
        pnl = position.get('unrealisedPnlToday', 0)
        if pnl < -100:  # Stop loss at -100
            print(f"Stop loss triggered for {symbol}")
            # Place exit order
        elif pnl > 200:  # Take profit at 200
            print(f"Take profit triggered for {symbol}")
            # Place exit order
    
    def on_trade_event(self, ws, trade):
        print(f"Trade executed: {trade['qty']} @ {trade['avgPrice']}")
        # Update internal trade log, calculate VWAP, etc.

# Initialize strategy
strategy = EventDrivenStrategy()

# Set up client with strategy handlers
client = QuotesClient(base_url="wss://...", token="...")
client.on_order = strategy.on_order_event
client.on_position = strategy.on_position_event
client.on_trade = strategy.on_trade_event

client.connect()
```

### Corporate Events

The QuotesClient supports automatic reception of corporate events from the server. These include corporate actions (dividends, stock splits, bonus issues) and corporate announcements (earnings reports, AGM notices, company announcements).

#### Corporate Event Handlers

```python
from wiz_trader import QuotesClient

def handle_corporate_action(client, event):
    """Handle corporate actions like dividends, splits, bonus issues"""
    print(f"Corporate Action: {event['data']['eventText']}")
    print(f"Symbol: {event['data']['tradingSymbol']}")
    print(f"Date: {event['data']['date']}")
    
    # Handle specific action categories
    action_category = event['data'].get('actionCategory', '').lower()
    if action_category == 'dividend':
        amount = event['data'].get('primaryAmount')
        print(f"Dividend Amount: ₹{amount} per share")
    elif action_category == 'split':
        print(f"Stock Split - Face Value Change from ₹{event['data'].get('oldFaceValue')} to ₹{event['data'].get('newFaceValue')}")
    elif action_category == 'bonus':
        ratio_num = event['data'].get('ratioNumerator')
        ratio_den = event['data'].get('ratioDenominator')
        if ratio_num and ratio_den:
            print(f"Bonus Shares: {ratio_num}:{ratio_den} ratio")

def handle_corporate_announcement(client, event):
    """Handle corporate announcements like earnings, AGM notices"""
    print(f"Announcement: {event['data']['announcement']}")
    print(f"Symbol: {event['data']['tradingSymbol']}")
    print(f"Date: {event['data']['date']}")
    print(f"Event Description: {event['data']['eventsDescriptions']}")
    
    # Check for attachment file
    if 'attachmentFile' in event['data']:
        print(f"Attachment: {event['data']['attachmentFile']}")

# Register corporate event handlers
client.on_corporate_action = handle_corporate_action
client.on_corporate_announcement = handle_corporate_announcement
```

#### Corporate Event Types

**Corporate Actions** (`type: 'events.corporate.actions'`):
- **Dividends**: Cash dividends, special dividends
- **Stock Splits**: Forward splits, reverse splits  
- **Bonus Issues**: Bonus share distributions
- **Rights Issues**: Rights share offerings
- **Mergers & Acquisitions**: Corporate restructuring events

**Corporate Announcements** (`type: 'events.corporate.announcements'`):
- **Earnings Reports**: Quarterly and annual results
- **AGM/EGM Notices**: Annual and extraordinary general meetings
- **Board Meetings**: Board meeting announcements
- **Company Updates**: Material announcements, regulatory filings

#### Example Event Data Structures

**Corporate Action Example**:
```json
{
    "data": {
        "actionCategory": "dividend",
        "actionSubcategory": "",
        "assetClass": "sme",
        "date": "19-08-2025",
        "eventText": "DIVIDEND - RS 2 PER SHARE",
        "events": "corporate_actions.other_corporate_action",
        "eventsDescriptions": "OTHER CORPORATE ACTION",
        "faceValue": 10,
        "hasAgm": false,
        "hasBonus": false,
        "hasDividend": true,
        "newFaceValue": null,
        "oldFaceValue": null,
        "percentageValue": null,
        "primaryAmount": 2,
        "ratioDenominator": null,
        "ratioNumerator": null,
        "secondaryAmount": null,
        "series": "SM",
        "tradingSymbol": "SBIN"
    },
    "timestamp": "2025-08-19T19:04:47.789Z",
    "type": "events.corporate.actions"
}
```

**Corporate Announcement Example**:
```json
{
    "data": {
        "announcement": "Manglam Infra & Engineering Limited has informed the Exchange about award of Additional Work from BRO under Project Deepak, Himachal Pradesh.",
        "assetClass": "sme",
        "attachmentFile": "https://nsearchives.nseindia.com/corporate/MANGLAMINFRA_19082025133231_addition.pdf",
        "date": "19-08-2025",
        "events": "corporate_announcements.general_updates",
        "eventsDescriptions": "General Updates",
        "hasXbrl": false,
        "industry": "",
        "tradingSymbol": "SBIN"
    },
    "timestamp": "2025-08-19T19:04:30.511Z",
    "type": "events.corporate.announcements"
}
```

#### Complete Corporate Events Example

```python
from wiz_trader import QuotesClient
from datetime import datetime

# Initialize client
client = QuotesClient(
    base_url="url",
    token="your-jwt-token",
    log_level="info"
)

def handle_corporate_action(client, event):
    """Comprehensive corporate action handler"""
    print(f"\n🏢 CORPORATE ACTION - {datetime.now()}")
    data = event.get('data', {})
    
    print(f"Symbol: {data.get('tradingSymbol')}")
    print(f"Action: {data.get('eventText')}")
    print(f"Date: {data.get('date')}")
    print(f"Event: {data.get('eventsDescriptions')}")
    print(f"Asset Class: {data.get('assetClass')}")
    print(f"Face Value: ₹{data.get('faceValue')}")
    
    # Action-specific handling based on actionCategory
    action_category = data.get('actionCategory', '').lower()
    if action_category == 'dividend' and data.get('hasDividend'):
        amount = data.get('primaryAmount')
        print(f"💰 Dividend: ₹{amount} per share")
        # Implement dividend tracking logic
    elif action_category == 'bonus' and data.get('hasBonus'):
        ratio_num = data.get('ratioNumerator')
        ratio_den = data.get('ratioDenominator')
        if ratio_num and ratio_den:
            print(f"🎁 Bonus Shares: {ratio_num}:{ratio_den} ratio")
        # Update portfolio for bonus shares
    elif action_category == 'split':
        old_fv = data.get('oldFaceValue')
        new_fv = data.get('newFaceValue')
        if old_fv and new_fv:
            print(f"📊 Stock Split: Face value changed from ₹{old_fv} to ₹{new_fv}")
        # Adjust position quantities

def handle_corporate_announcement(client, event):
    """Comprehensive corporate announcement handler"""
    print(f"\n📢 CORPORATE ANNOUNCEMENT - {datetime.now()}")
    data = event.get('data', {})
    
    print(f"Symbol: {data.get('tradingSymbol')}")
    print(f"Announcement: {data.get('announcement')}")
    print(f"Date: {data.get('date')}")
    print(f"Event: {data.get('eventsDescriptions')}")
    print(f"Asset Class: {data.get('assetClass')}")
    
    # Check for attachment file
    if 'attachmentFile' in data:
        print(f"📎 Attachment: {data['attachmentFile']}")
    
    # Event-specific handling based on events field
    events = data.get('events', '').lower()
    if 'earnings' in events or 'results' in events:
        print("📈 Earnings/Results announcement detected")
        # Implement earnings analysis
    elif 'general_updates' in events:
        print("📋 General company update")
        # Handle general announcements

# Register handlers
client.on_corporate_action = handle_corporate_action
client.on_corporate_announcement = handle_corporate_announcement

# Optional: Subscribe to market data as well
instruments = ["NSE:RELIANCE-EQ", "NSE:TCS-EQ"]
client.subscribe(instruments, mode="ticks")

# Start listening for events
client.connect()
```

### Complete Examples

#### Blocking Example

```python
import logging
from wiz_trader import QuotesClient

logging.basicConfig(level=logging.INFO)

client = QuotesClient(
    base_url="wss://websocket-url/quotes",
    token="your-jwt-token"
)

def on_tick(ws, tick):
    logging.debug("Tick: %s", tick)

def on_stats(ws, stats):
    logging.debug("Stats: %s", stats)

def on_connect(ws):
    logging.info("Connected.")
    ws.subscribe(["NSE:SBIN:3045"], mode="ticks")
    ws.subscribe(["NSE:NIFTY24JUN20100CE:20100"], mode="greeks")

def on_close(ws, code, reason):
    logging.warning("Closed: %s", reason)
    ws.stop()

def on_error(ws, error):
    logging.error("Error: %s", error)

client.on_tick    = on_tick
client.on_stats   = on_stats
client.on_connect = on_connect
client.on_close   = on_close
client.on_error   = on_error

try:
    client.connect()
except KeyboardInterrupt:
    client.stop()
```

#### Non‑Blocking Example

```python
import asyncio
import logging
from wiz_trader import QuotesClient

async def main():
    logging.basicConfig(level=logging.INFO)
    client = QuotesClient(
        base_url="wss://websocket-url/quotes",
        token="your-jwt-token"
    )
    client.on_tick    = lambda ws, tick: logging.info(tick)
    client.on_connect = lambda ws: ws.subscribe(["NSE:SBIN:3045"])
    client.connect_async()

    # Your other async work here...
    await asyncio.sleep(60)
    client.stop()

asyncio.run(main())
```

#### Message Data Structures

##### Ticks structure
Messages with `type: 'ticks'` contain market data:
- `ts`: tick timestamp
- `lastTradedTs`: timestamp of the last executed trade in this instrument
- `oi`: Current open interest (for futures and options)
- `oiDayHigh`: Day's highest open interest value  
- `oiDayLow`: Day's lowest open interest value

```json
{
  "type": "ticks",
  "identifier": "NSE:RELIANCE:2885",
  "tradingSymbol": "RELIANCE",
  "exchange": "NSE",
  "segment": "NSECM",
  "exchangeToken": 2885,
  "bids": [{"volume": 1722, "price": 1295.5, "orders": 43}, ...],
  "offers": [{"volume": 0, "price": 0, "orders": 0}, ...],
  "ltp": 1295.5,
  "lastTradedQty": 10,
  "buyQty": 1722,
  "sellQty": 0,
  "volume": 10429964,
  "avgPrice": 1291.46,
  "netChange": 0,
  "ohlc": {"open": 1270, "high": 1300.9, "low": 1267, "close": 1295.5},
  "oi": 1234567,
  "oiDayHigh": 1250000,
  "oiDayLow": 1200000,
  "lastTradedTs": "2025-04-21T10:29:33Z",
  "ts": "2025-04-21T10:29:46Z"
}
```

##### Greeks structure
Messages with `type: 'greeks'` contain options Greeks data:

```json
{
  "type": "greeks",
  "identifier": "NSE:NIFTY25JULFUT:53216",
  "tradingSymbol": "NIFTY25JULFUT",
  "iv": 0.0,
  "delta": 0.0,
  "gamma": 0.0,
  "theta": 0.0,
  "vega": 0.0,
  "rho": 0.0,
  "ts": "2025-04-21T10:29:46Z"
}
```

## Wizzer Client

The `WizzerClient` provides access to Wizzer's REST APIs for trading, portfolio management, and market data.

### Wizzer Client Initialization

Initialize the client with your authentication details:

```python
from wiz_trader import WizzerClient

# Method 1: All parameters provided directly
client = WizzerClient(
    base_url="https://api-url.in",
    token="your-jwt-token",
    strategy_id="str_01hxgwgfxgfrxb48bzchnve7gm",  # Optional: Default strategy ID
    log_level="info"  # Options: "error", "info", "debug"
)

# Method 2: Using environment variables
# (Requires WZ__API_BASE_URL and WZ__TOKEN to be set)
client = WizzerClient(log_level="info")
```

### Constants

The `WizzerClient` provides constants for various parameter values, making your code more readable and type-safe:

```python
from wiz_trader import WizzerClient

client = WizzerClient(...)

# Use constants for transaction types
client.place_order(
    exchange=client.EXCHANGE_NSE,
    trading_symbol="SBIN",
    transaction_type=client.TRANSACTION_TYPE_BUY,
    quantity=10,
    order_type=client.ORDER_TYPE_MARKET,
    product=client.PRODUCT_CNC
)
```

Available constants:

- Transaction types: `TRANSACTION_TYPE_BUY`, `TRANSACTION_TYPE_SELL`
- Product types: `PRODUCT_CNC`, `PRODUCT_MIS`, `PRODUCT_NRML`
- Order types: `ORDER_TYPE_MARKET`, `ORDER_TYPE_LIMIT`, `ORDER_TYPE_SL`, `ORDER_TYPE_SLM`
- Order status: `ORDER_STATUS_OPEN`, `ORDER_STATUS_CANCELLED`, `ORDER_STATUS_REJECTED`, `ORDER_STATUS_PENDING`, `ORDER_STATUS_COMPLETED`
- Validity types: `VALIDITY_DAY`, `VALIDITY_IOC`, `VALIDITY_GTT`
- Variety types: `VARIETY_REGULAR`, `VARIETY_AMO`, `VARIETY_BO`, `VARIETY_CO`
- Exchanges: `EXCHANGE_NSE`, `EXCHANGE_BSE`, `EXCHANGE_WZR`
- Segments: `SEGMENT_NSE_CM`, `SEGMENT_BSE_CM`, `SEGMENT_NSE_FO`, `SEGMENT_WZREQ`
- Instrument types: `INSTRUMENT_TYPE_EQ`, `INSTRUMENT_TYPE_EQLC`, `INSTRUMENT_TYPE_EQMCC`, `INSTRUMENT_TYPE_EQSCC`, `INSTRUMENT_TYPE_BASKET`
- - Weightage schemes: `WEIGHTAGE_SCHEME_EQUI_WEIGHTED`, `WEIGHTAGE_SCHEME_QUANTITY_WEIGHTED`, `WEIGHTAGE_SCHEME_PRICE_WEIGHTED`, `WEIGHTAGE_SCHEME_MARKET_CAP_WEIGHTED`, `WEIGHTAGE_SCHEME_FLOAT_ADJUSTED_MARKET_CAP_WEIGHTED`, `WEIGHTAGE_SCHEME_FUNDAMENTAL_WEIGHTED`, `WEIGHTAGE_SCHEME_CUSTOM_WEIGHTED`

### Data Hub Methods

#### Get Indices

Fetch available indices from an exchange:

```python
# Get all indices from NSE
indices = client.get_indices(exchange="NSE")
print(f"Found {len(indices)} indices")

# Get a specific index by trading symbol
nifty_50 = client.get_indices(trading_symbol="NIFTY 50", exchange="NSE")
```

#### Get Index Components

Fetch components (stocks) in a specific index:

```python
# Get components of NIFTY 50
components = client.get_index_components(trading_symbol="NIFTY 50", exchange="NSE")
print(f"Found {len(components)} components in NIFTY 50")
for component in components[:5]:
    print(f"{component['tradingSymbol']} - {component.get('weightage', 'N/A')}%")
```

#### List Classification Types

Retrieve all available classification types.

```python
classification_types = client.get_classification_types()
# Response: ['basicIndustry', 'industry', 'macroEconomicSector', 'sector']
```

#### List Classification Values

Retrieve all values for a specific classification type.

```python
basic_industry_values = client.get_classifications("basicIndustry")
# Response: ['Aerospace & Defense', 'Agricultural Food & other Products', ...]
```

#### Filter Indices by Classifications

Filter indices based on multiple classification criteria. The `match_all` parameter controls the filter logic:
- `True`: AND logic (all filters must match)
- `False`: OR logic (any filter can match)

```python
# Example: Find indices in the 'Commodities' macro-economic sector OR 'Aerospace & Defense' industry
filters = {
    "macroEconomicSector": ["Commodities"],
    "industry": ["Aerospace & Defense"]
}
or_logic_indices = client.filter_indices(filters=filters, match_all=False)

# Example: Find indices that are in BOTH 'Housing Finance Company' and 'Financial Services'
filters = {
    "basicIndustry": ["Housing Finance Company"],
    "sector": ["Financial Services"]
}
and_logic_indices = client.filter_indices(filters=filters, match_all=True)
```

#### Filter Instruments by Classifications

Filter instruments based on classification criteria.

```python
# Example: Find instruments in the 'Technology' sector
filters = {"sector": ["Technology"]}
tech_instruments = client.filter_instruments(filters=filters, match_all=True)
```

#### Filter Instruments by Classifications and Index

Filter instruments that belong to both specific classifications and a given index.

```python
# Example: Find 'Financial Services' instruments within the 'NIFTY NEXT 50' index
filters = {
    "basicIndustry": ["Housing Finance Company"],
    "sector": ["Financial Services"]
}
filtered_instruments = client.filter_instruments(
    index_identifier="NSE:NIFTY NEXT 50:26054",
    filters=filters,
    match_all=False
)
```

#### Filter Instruments by Derivatives

Filter instruments based on whether they have associated futures or options contracts.

```python
# Find all instruments that have options
optionable_stocks = client.filter_instruments_by_derivatives(hasOptions=True)

# Find all instruments that have futures
futures_stocks = client.filter_instruments_by_derivatives(hasFutures=True)

# Find all instruments that have both options and futures
fno_stocks = client.filter_instruments_by_derivatives(hasOptions=True, hasFutures=True)

# Find all instruments that have either options or futures
all_derivative_stocks = client.filter_instruments_by_derivatives()
```

### Instrument Screener

The SDK provides a powerful instrument screener to filter and sort instruments based on a wide range of criteria using a simple and expressive Domain Specific Language (DSL).

> **📋 Comprehensive Screener Documentation**: For detailed information about all available screener fields, practical use cases, and advanced filtering examples, see **[screener.md](screener.md)**.

#### `get_screener_fields()`

This method returns a list of all available filterable fields and their supported operations.

```python
# Get all available screener fields
fields = client.get_screener_fields()
print(json.dumps(fields, indent=2))
```

#### `run_screener(filters, sort=None, limit=None, as_of=None)`

This is the main method for the instrument screener. It accepts a dictionary with a `filters` object to define the screening criteria.

**Arguments:**

*   `filters` (dict, required): An object containing the filter conditions.
*   `sort` (list, optional): A list of objects to define the sort order.
*   `limit` (int, optional): The maximum number of results to return.

#### Filter DSL (Domain Specific Language)

The `filters` object uses a simple DSL to define the screening criteria.

##### Operators

| Operator  | Description              | Example                               |
| :-------- | :----------------------- | :------------------------------------ |
| `$eq`     | Equal to                 | `{ "sector": { "$eq": "Finance" } }`  |
| `$ne`     | Not equal to             | `{ "sector": { "$ne": "Finance" } }`  |
| `$gt`     | Greater than             | `{ "roe": { "$gt": 0.15 } }`          |
| `$gte`    | Greater than or equal to | `{ "roe": { "$gte": 0.15 } }`         |
| `$lt`     | Less than                | `{ "roe": { "$lt": 0.15 } }`          |
| `$lte`    | Less than or equal to    | `{ "roe": { "$lte": 0.15 } }`         |
| `$in`     | In a list of values    | `{ "sector": { "$in": ["Information Technology", "Finance"] } }` |
| `$nin`    | Not in a list of values| `{ "sector": { "$nin": ["Information Technology", "Finance"] } }` |
| `$like`   | Like (case-sensitive)    | `{ "companyName": { "$like": "%Bank%" } }` |
| `$nlike`  | Not like (case-sensitive)| `{ "companyName": { "$nlike": "%Bank%" } }` |
| `$ilike`  | Like (case-insensitive)  | `{ "companyName": { "$ilike": "%bank%" } }` |
| `$between`| Between two values       | `{ "roe": { "$between": [0.10, 0.20] } }` |

##### Logical Operators

*   `$and`: A list of filter conditions that must all be true.
*   `$or`: A list of filter conditions where at least one must be true.

#### Screener Examples

##### Basic Filters

1.  **Find all instruments in the 'Finance' sector.**
    ```python
    client.run_screener(filters={"sector": {"$eq": "Finance"}})
    ```

2.  **Find all instruments on the 'NSE' exchange.**
    ```python
    client.run_screener(filters={"exchange": {"$eq": "NSE"}})
    ```

3.  **Find all instruments with a Return on Equity (ROE) greater than 15%.**
    ```python
    client.run_screener(filters={"roe": {"$gt": 0.15}})
    ```

##### Set and Range Operators

4.  **Find all instruments in the 'Energy' or 'Healthcare' sectors.**
    ```python
    client.run_screener(filters={"sector": {"$in": ["Energy", "Healthcare"]}})
    ```

5.  **Find instruments with a PAT Margin between 5% and 10%.**
    ```python
    client.run_screener(filters={"patMargin": {"$between": [0.05, 0.10]}})
    ```

##### Pattern Matching

6.  **Find all instruments with 'Bank' in their name (case-insensitive).**
    ```python
    client.run_screener(filters={"companyName": {"$ilike": "%bank%"}})
    ```

##### Logical Combinations

7.  **Find instruments in the 'IT' sector with a ROE greater than 20%.**
    ```python
    client.run_screener(
        filters={
            "$and": [
                {"sector": {"$eq": "Information Technology"}},
                {"roe": {"$gt": 0.20}}
            ]
        }
    )
    ```

8.  **Complex Nested Query:** Find instruments in the 'Automobile' sector that have (a ROE > 18% AND a low Debt/Equity) OR have a high Asset Turnover.
    ```python
    client.run_screener(
        filters={
            "$and": [
                {"sector": {"$eq": "Automobile"}},
                {
                    "$or": [
                        {
                            "$and": [
                                {"roe": {"$gt": 0.18}},
                                {"debtEquity": {"$lt": 0.3}}
                            ]
                        },
                        {"assetTurnover": {"$gt": 1.5}}
                    ]
                }
            ]
        }
    )
    ```

##### Sort and Limit

9.  **Find the top 10 companies by highest ROE.**
    ```python
    client.run_screener(
        filters={},
        sort=[{"roe": -1}],
        limit=10
    )
    ```

10. **Find companies in the 'Pharmaceuticals' sector, sort them by the highest PAT margin, and then by the lowest Debt-to-Equity ratio.**
    ```python
    client.run_screener(
        filters={"sector": {"$eq": "Pharmaceuticals"}},
        sort=[
            {"patMargin": -1},
            {"debtEquity": 1}
        ]
    )
    ```

#### Get Historical OHLCV Data
## Overview

The `get_historical_ohlcv` method supports multiple time intervals for historical data retrieval, from 1-minute intraday data to monthly aggregated data.

## Supported Intervals

### Intraday Intervals
- `1m` - 1 minute
- `2m` - 2 minutes  
- `3m` - 3 minutes
- `5m` - 5 minutes
- `10m` - 10 minutes
- `15m` - 15 minutes
- `30m` - 30 minutes
- `45m` - 45 minutes
- `1h` - 1 hour

### Daily/Monthly Intervals
- `1d` - 1 day (default)
- `1M` - 1 month (last trading day of month)

## Date Format Requirements

### For Daily and Monthly Intervals (`1d`, `1M`)
- **Format**: `YYYY-MM-DD`
- **Example**: `"2024-01-15"`

### For Intraday Intervals (all minute/hour intervals)
- **Format**: `YYYY-MM-DD HH:MM:SS`
- **Example**: `"2024-01-15 09:30:00"`

**New Features in Historical Data API:**

- **Open Interest (`oi`)**: Now supported in the `ohlcv` parameter for futures instruments
- **Continuous Data**: The `continuous` parameter (for futures only) provides seamless data across multiple contract months when your date range spans multiple months

## Using Constants

The client provides constants for all intervals:

```python
from wiz_trader import WizzerClient

client = WizzerClient(...)

# Intraday intervals
client.INTERVAL_1_MINUTE     # "1m"
client.INTERVAL_2_MINUTES    # "2m"
client.INTERVAL_3_MINUTES    # "3m"
client.INTERVAL_5_MINUTES    # "5m"
client.INTERVAL_10_MINUTES   # "10m"
client.INTERVAL_15_MINUTES   # "15m"
client.INTERVAL_30_MINUTES   # "30m"
client.INTERVAL_45_MINUTES   # "45m"
client.INTERVAL_1_HOUR       # "1h"

# Daily/Monthly intervals
client.INTERVAL_1_DAY        # "1d"
client.INTERVAL_1_MONTH      # "1M"
```

## Usage Examples

### 1. Get 5-minute intraday data

```python
# Get 5-minute candles for a trading session
intraday_data = client.get_historical_ohlcv(
    instruments=["NSE:SBIN:3045"],
    start_date="2024-01-15 09:15:00",  # Market open
    end_date="2024-01-15 15:30:00",    # Market close
    ohlcv=["open", "high", "low", "close", "volume"],
    interval=client.IN