Metadata-Version: 2.4
Name: kkunal
Version: 1.4.0
Summary: Kkunal - Python library for Choice FINX Trading API with built-in Technical Indicators
Author: Kkunal
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Requires-Dist: websockets>=11.0.3
Requires-Dist: websocket-client>=1.6.0
Requires-Dist: pandas>=1.3.0
Requires-Dist: numpy>=1.21.0
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# Kkunal

A Python library for the Choice FINX Trading API. Supports REST API, Interactive WebSockets (order/trade updates), and Live Price Feed WebSockets (FIX3.0 compressed data).

## Installation

```bash
pip install kkunal
```

All dependencies (`requests`, `websockets`, `pandas`) are installed automatically.

---

## Quick Start

```python
from choice_api import ChoiceClient, BASE_URL_OMNE, BASE_URL_FINX

# Default endpoint (https://finxomne.choiceindia.com)
client = ChoiceClient(
    vendor_id="YOUR_VENDOR_ID",
    api_key="YOUR_JWT_BEARER_TOKEN"
)

# Alternate endpoint (https://finx.choiceindia.com)
client = ChoiceClient(
    vendor_id="YOUR_VENDOR_ID",
    api_key="YOUR_JWT_BEARER_TOKEN",
    base_url=BASE_URL_FINX
)

# Login (TOTP flow is handled automatically)
session_id = client.login(mobile_no="1234567890")
print(f"Session ID: {session_id}")
```

Pass `session_file=` and the session is reused for the rest of the day instead of
logging in again — sessions expire daily, so this is safe to leave switched on:

```python
client.login(mobile_no="1234567890", session_file="session.json")
```

`ChoiceClient` is also a context manager, so the session is always logged off:

```python
with ChoiceClient(vendor_id="...", api_key="...") as client:
    client.login(mobile_no="1234567890", session_file="session.json")
    print(client.is_authenticated)   # True
    ...
# logged off automatically on exit
```

All HTTP calls use a 30 second timeout by default. Override it per client:

```python
client = ChoiceClient(vendor_id="...", api_key="...", timeout=60)
```

### Session Persistence

You can save and reload sessions to avoid logging in repeatedly during the same trading day:

```python
session_file = "my_session.json"

if client.load_session(session_file):
    print("Restored today's session.")
else:
    client.login(mobile_no="1234567890")
    client.save_session(session_file)
```

> **Note:** Sessions expire daily. `load_session` will return `False` if the saved session is from a previous day.

---

## Constants

Named constants replace the magic numbers and short codes the API expects:

```python
from choice_api import Segment, Side, OrderType, ProductType, Validity, Resolution, to_paisa

client.orders.place_order(
    segment_id=Segment.NSE_FO,          # 2
    token=48552,
    order_type=OrderType.LIMIT,         # 'RL_LIMIT'
    bs=Side.BUY,                        # 1
    qty=15,
    price=to_paisa(1300.50),            # 130050
    trigger_price=0,
    validity=Validity.DAY,              # 1
    product_type=ProductType.INTRADAY,  # 'M'
)
```

| Constant | Values |
|---|---|
| `Segment` | `NSE_CASH`, `NSE_FO`, `BSE_CASH`, `BSE_FO`, `NSE_CURRENCY`, `MCX` |
| `Side` | `BUY`, `SELL` |
| `OrderType` | `LIMIT`, `STOP_LOSS_LIMIT` |
| `ProductType` | `INTRADAY`, `DELIVERY` |
| `Validity` | `DAY`, `IOC` |
| `Resolution` | `MIN_1`, `MIN_3`, `MIN_5`, `MIN_10`, `MIN_15`, `MIN_30`, `HOUR_1`, `DAY`, `WEEK`, `MONTH` |

Prices are in **paisa**, not rupees. `to_paisa(1300.50)` → `130050`, and
`to_rupees(130050)` → `1300.50`.

---

## Error Handling

Every failure raises a typed exception deriving from `ChoiceAPIError`, so you can
tell an expired session apart from a network blip or a rejected order:

```python
from choice_api import ChoiceAPIError, AuthenticationError, NetworkError

try:
    client.orders.place_order(...)
except AuthenticationError:
    client.login(mobile_no="1234567890")   # session expired, log back in
except NetworkError:
    ...                                    # timeout or connection reset, safe to retry
except ChoiceAPIError as e:
    print(e.status_code, e.endpoint, e.response)
```

| Exception | Raised when |
|---|---|
| `AuthenticationError` | Login failed, or the session is missing/expired (401/403) |
| `APIResponseError` | The request reached the API but was rejected |
| `NetworkError` | Timeout, DNS failure or connection reset |
| `InvalidResponseError` | A 2xx response whose body was not valid JSON |
| `ScripMasterError` | The daily scrip master could not be downloaded or parsed |
| `WebSocketError` | A live feed or interactive socket failed |

---

## Scrip Master

The daily instrument file is cached on disk per day, so repeated runs reuse it
instead of re-downloading several megabytes. Pass `fetch(force=True)` to refresh,
or `ScripMaster(cache_dir=...)` to control where it lives.

### `resolve(symbol, segment=None)`

Resolves a symbol straight to the `(segment_id, token)` pair the other APIs want.

```python
segment_id, token = client.scrip_master.resolve("RELIANCE", segment=Segment.NSE_CASH)
# (1, 2885)
```

Raises `KeyError` if the symbol is not listed. With no `segment`, the first match is
returned and the other segments are logged.

The Scrip Master CSV is automatically downloaded when you log in. It maps instrument symbols to their tokens, lot sizes, and other metadata.

### `get_token(symbol, segment=None)`

Looks up tokens for a given symbol or description.

- **Without `segment`**: Returns a **list of dicts** for ALL matching rows across every segment (NSE, BSE, CDS, etc.). Each dict contains `Token`, `Exchange`, `Segment`, `Symbol`, `SecDesc`, `Series`, `MarketLot`.
- **With `segment`** (e.g., `"1"`, `"13"`): Returns a **single token string** for that specific segment, or `None` if not found.

```python
# Get all matches across all segments
matches = client.scrip_master.get_token("RELIANCE")
for m in matches:
    print(f"Segment: {m['Segment']} — Token: {m['Token']}, Symbol: {m['Symbol']}")
# Segment: 1 — Token: 2885, Symbol: RELIANCE
# Segment: 13 — Token: 500325, Symbol: RELIANCE
# ...

# Get specific segment token
nse_token = client.scrip_master.get_token("RELIANCE", segment="1")
nse_cds_token = client.scrip_master.get_token("RELIANCE", segment="13")
```

### `search(name)`

Case-insensitive fuzzy search: returns all rows where Symbol or SecDesc **contains** the given name.

```python
results = client.scrip_master.search("NIFTY")
for r in results:
    print(f"{r['Exchange']} | {r['Symbol']} | Token: {r['Token']}")
```

### `get_details(token)`

Returns all CSV row details for a given token as a dictionary.

```python
details = client.scrip_master.get_details("2885")
print(details)
```

### `get_lot_size(token)`

Returns the market lot size for a token.

```python
lot = client.scrip_master.get_lot_size("2885")
print(lot)  # 1 for equity, 250 for NIFTY futures, etc.
```

---

## Orders

> **Important:** Prices must be in **paisa** (multiply INR by 100). For F&O orders, `qty` must be in **total shares** (multiples of the lot size), not the number of lots.

### `client.orders.place_order(...)`

| Parameter | Type | Description |
|---|---|---|
| `segment_id` | `int` | `1` = NSE Cash, `2` = NSE F&O, `3` = BSE Cash |
| `token` | `int` | Instrument token from Scrip Master |
| `order_type` | `str` | `"RL_LIMIT"` = Limit, `"SL_LIMIT"` = Stop Loss Limit *(Note: Market orders are not supported via API)* |
| `bs` | `int` | `1` = Buy, `2` = Sell |
| `qty` | `int` | Total quantity in shares |
| `price` | `float` | Price in paisa (e.g., 1300 INR → `130000`) |
| `trigger_price` | `float` | Trigger price in paisa (0 for non-SL orders) |
| `validity` | `int` | `1` = Day |
| `product_type` | `str` | `"M"` = Intraday (Margin), `"D"` = Delivery/CarryForward |
| `disclosed_qty` | `int` | Optional. Disclosed quantity (default `0`) |
| `client_order_no` | `int` | Optional. Your own reference number, used later by `modify_order`/`cancel_order`. A unique one is generated if omitted. |

```python
response = client.orders.place_order(
    segment_id=1,
    token=2885,
    order_type="RL_LIMIT",
    bs=1,
    qty=1,
    price=130000,
    trigger_price=0,
    validity=1,
    product_type="D"
)
```

### `client.orders.modify_order(...)`

Modifies an existing order. Requires `client_order_no`, `exchange_order_no`, and `gateway_order_no` from the order book.

```python
response = client.orders.modify_order(
    client_order_no=123456,
    exchange_order_no="1234567890",
    gateway_order_no="1234567890",
    segment_id=1,
    token=2885,
    order_type="RL_LIMIT",
    bs=1,
    qty=1,
    price=130000,
    trigger_price=0,
    validity=1,
    product_type="D"
)
```

### `client.orders.cancel_order(...)`

Cancels an existing order. Same parameters as `modify_order` plus optional `exchange_order_time`.

### `client.orders.get_order_book()`

Returns all orders placed during the current session.

```python
order_book = client.orders.get_order_book()
```

### `client.orders.get_order_book_v2()`

Returns the order book (version 2 format).

### `client.orders.get_order_by_no(order_no)`

Returns details for a specific order number.

```python
order = client.orders.get_order_by_no(123456)
```

### `client.orders.get_trade_book()`

Returns all executed trades.

```python
trades = client.orders.get_trade_book()
```

### `client.orders.get_order_messages(req_id)`

Returns order-related messages for a given request ID.

### `client.orders.get_margin(...)` / `calculate_margin(...)`

Convenience method to calculate margin requirements before placing orders. Same syntax as [`client.funds.get_margin`](#clientfundsget_margin--calculatemargin).

```python
margin = client.orders.get_margin(segment_id=2, token=48552, qty=425)
```

---

## Portfolio

### `client.portfolio.get_holdings()`

Returns current holdings.

```python
holdings = client.portfolio.get_holdings()
```

### `client.portfolio.get_net_position()`

Returns net positions.

```python
positions = client.portfolio.get_net_position()
```

### `client.portfolio.position_conversion(...)`

Converts an open position from one product type to another (e.g., Intraday to Delivery).

| Parameter | Type | Description |
|---|---|---|
| `segment_id` | `int` | Exchange segment |
| `token` | `int` | Instrument token |
| `client_order_no` | `int` | Client order number |
| `buy_sell` | `int` | `1` = Buy, `2` = Sell |
| `quantity` | `int` | Quantity to convert |
| `product_type` | `str` | Target product type |
| `source_product_type` | `str` | Current product type |

### `client.portfolio.verify_dis(...)`

Verifies eDIS (Electronic Delivery Instruction Slip) for delivery sell orders.

### `client.portfolio.get_dis_status()`

Returns the current DIS verification status.

---

## Funds

### `client.funds.get_funds_view()`

Returns funds summary.

```python
funds = client.funds.get_funds_view()
```

### `client.funds.get_funds_view_new()`

Returns funds summary in the new format.

### `client.funds.get_margin(...)` / `calculate_margin(...)`

Calculates required margin for single or multiple contracts.

| Parameter | Type | Description |
|---|---|---|
| `segment_id` | `int` | `1` = NSE Cash, `2` = NSE F&O, `3` = BSE Cash |
| `token_qty` | `str` or `list` | Pipe-separated string (`"48552|425"`), tilde-separated string (`"48552|425~48553|100"`), or list of tuples `[(48552, 425), (48553, 100)]` / dicts `[{"token": 48552, "qty": 425}]` |
| `mode` | `int` | Optional. Mode integer (default `1`) |
| `device_id` | `str` | Optional. Device ID string (default `"MAC"`) |
| `token` | `int` / `str` | Optional. Single token shorthand (used with `qty`) |
| `qty` | `int` | Optional. Single quantity shorthand (used with `token`) |

```python
# Single contract using string
margin = client.funds.get_margin(segment_id=2, token_qty="48552|425")

# Single contract using token and qty shorthand
margin = client.funds.get_margin(segment_id=2, token=48552, qty=425)

# Multiple contracts using string
margin = client.funds.get_margin(segment_id=2, token_qty="48552|425~48553|100")

# Multiple contracts using list of tuples
margin = client.funds.get_margin(
    segment_id=2,
    token_qty=[(48552, 425), (48553, 100)]
)

print(margin)
# {"Status": "Success", "Response": { ... }, "Reason": ""}
```

*(Also accessible via `client.orders.get_margin(...)` or `calculate_margin`)*

### `client.funds.process_payout(amount, bank_acc_no, product_type=0)`

Initiates a fund withdrawal.

### `client.funds.payment_via_netbanking(amount, bank_acc_no, bank_ifsc_code, return_url, segment_id, product_type=0)`

Initiates a net banking payment.

### `client.funds.payment_via_hdfc_upi(amount, bank_acc_no, user_vpa, segment_id, product_type=0)`

Initiates a HDFC UPI payment.

### `client.funds.check_vpa(user_vpa)`

Validates a UPI VPA address.

### `client.funds.payment_via_razorpay(amount, bank_acc_no, bank_ifsc_code, upi_id, segment_id, payment_type=0, product_type=0)`

Initiates a RazorPay payment.

### `client.funds.payment_ack_response(transaction_id)`

Acknowledges a payment transaction.

---

## Market

### `client.market.get_market_status()`

Returns current market status across all segments.

```python
status = client.market.get_market_status()
```

### `client.market.get_user_profile()`

Returns the authenticated user's profile.

```python
profile = client.market.get_user_profile()
```

### `client.market.get_multiple_touchline(multiple_seg_token)`

Returns touchline data for multiple instruments.

```python
# Format: "SegmentId1,Token1|SegmentId2,Token2"
touchline = client.market.get_multiple_touchline("1@2885,1@11536")
```

---

## Historical Data

### `client.historical.get_by_symbol(symbol, from_date, to_date, resolution='D', segment=None, indicators=None)`

Fetches candles by symbol, resolving the token through the Scrip Master — no token
lookup needed.

```python
df = client.historical.get_by_symbol("RELIANCE", "2024-01-01", "2024-06-01")

# with indicators in the same call
df = client.historical.get_by_symbol(
    "RELIANCE", "2024-01-01", "2024-06-01",
    resolution=Resolution.DAY,
    indicators=["rsi", "macd", "supertrend"],
)
```

### `client.historical.get_historical_data(segment_id, token, from_date, to_date, resolution)`

Returns historical OHLCV data as a **Pandas DataFrame**.

| Parameter | Type | Description |
|---|---|---|
| `segment_id` | `int` | Exchange segment |
| `token` | `int` | Instrument token |
| `from_date` | `str` or `int` | Start date (`"YYYY-MM-DD"` or seconds from 1980) |
| `to_date` | `str` or `int` | End date (`"YYYY-MM-DD"` or seconds from 1980) |
| `resolution` | `str` | `"1"` = 1 min, `"5"` = 5 min, `"D"` = Daily |

```python
df = client.historical.get_historical_data(
    segment_id=1,
    token=2885,
    from_date="2024-01-01",
    to_date="2024-12-31",
    resolution="D"
)
print(df.head())
#                   Time     Open     High      Low    Close   Volume  OI
# 0  2024-01-01 00:00:00  2501.00  2520.50  2490.00  2515.30  1234567   0
```

The returned DataFrame has columns: `Time`, `Open`, `High`, `Low`, `Close`, `Volume`, `OI`. Prices are automatically adjusted using the `PriceDivisor` from the API response.

---

## Technical Indicators

`kkunal` includes a built-in vectorized Technical Analysis indicator engine based on `pandas` and `numpy`. No external C-dependencies required.

### Supported Indicators

| Category | Indicators |
|---|---|
| **Trend** | SMA, EMA, DEMA, TEMA, WMA, MACD, ADX, Supertrend, Parabolic SAR, Ichimoku Cloud |
| **Momentum** | RSI, Stochastic Oscillator (%K, %D), CCI, Williams %R |
| **Volatility** | Bollinger Bands (with %B), ATR, Donchian Channel |
| **Volume** | VWAP, OBV |
| **Utilities** | Crossover, Crossunder, Heikin Ashi, Pivot Points (Standard/Fibonacci/Camarilla) |

### Usage Methods

> **Warmup:** every indicator returns `NaN` until its lookback window is full, so a
> value only ever comes from a complete window. Use `df.dropna()` before feeding
> results into a strategy.

#### 1. Fetch Historical Data with Indicators in One Step
```python
# Every indicator (all 21)
df = client.historical.get_historical_data_with_indicators(
    segment_id=1, token=2885,
    from_date="2024-01-01", to_date="2024-12-31", resolution="D",
    indicators="all"
)

# Or select specific indicators
df = client.historical.get_historical_data_with_indicators(
    segment_id=1, token=2885,
    from_date="2024-01-01", to_date="2024-12-31", resolution="D",
    indicators=["rsi", "macd", "supertrend", "bb", "ichimoku", "pivot"]
)
```

`indicators` accepts `'all'` (every indicator), `'core'` (the nine most common ones),
or a list of names and shorthands such as `['rsi', 'macd', 'st', 'bb']`. An
unrecognised name raises `ValueError` rather than being silently skipped.

#### 2. Apply via `client.indicators`
```python
df = client.historical.get_historical_data(1, 2885, "2024-01-01", "2024-12-31", "D")

# Add specific indicators
df = client.indicators.add_rsi(df, period=14)
df = client.indicators.add_macd(df)
df = client.indicators.add_supertrend(df, period=10, multiplier=3.0)
df = client.indicators.add_bollinger_bands(df, period=20, std_dev=2.0)
df = client.indicators.add_ichimoku(df)
df = client.indicators.add_parabolic_sar(df)
df = client.indicators.add_pivot_points(df, method="fibonacci")
df = client.indicators.add_heikin_ashi(df)

# Pick indicators by name, chosen at runtime
df = client.indicators.add(df, "rsi", "macd", "supertrend")
df = client.indicators.add(df, "rsi", period=21)

# Or add every indicator at once (with customizable periods)
df_all = client.indicators.add_all(df, sma_period=50, ema_period=50, rsi_period=21)
```

#### 3. Standalone Indicator Functions
```python
from choice_api import rsi, macd, supertrend, bollinger_bands, ichimoku, pivot_points

rsi_series = rsi(df, period=14)
macd_df = macd(df, fast_period=12, slow_period=26, signal_period=9)
st_df = supertrend(df, period=10, multiplier=3.0)
bb_df = bollinger_bands(df, period=20, std_dev=2.0)  # Includes BB_PercentB
ichi_df = ichimoku(df)
pp_df = pivot_points(df, method="camarilla")
```

> **Notes**
> - `bollinger_bands` uses the **population** standard deviation (`ddof=0`), so bands line up with TradingView.
> - `pivot_points` derives each bar's levels from the **previous** bar's High/Low/Close, so they are known before the bar opens. The first row is therefore `NaN`.

#### 4. Signal Crossover Detection
```python
from choice_api import crossover, crossunder, ema

ema_9 = ema(df, period=9)
ema_21 = ema(df, period=21)

buy_signals = crossover(ema_9, ema_21)    # EMA 9 crosses above EMA 21
sell_signals = crossunder(ema_9, ema_21)   # EMA 9 crosses below EMA 21

print(f"Buy signals on dates: {df['Time'][buy_signals].tolist()}")
```

---



## Interactive WebSockets

Receives live order updates, trade confirmations, and market status events.

```python
import asyncio
from choice_api import InteractiveSocketClient

async def main():
    # token is the session_id obtained after login
    ws = InteractiveSocketClient(token=client.session_id)

    ws.on("ORD_NRML", lambda data: print(f"Order Update: {data}"))
    ws.on("TRD_MSG", lambda data: print(f"Trade: {data}"))
    ws.on("MKT_STAT", lambda data: print(f"Market Status: {data}"))

    await ws.connect()

# IMPORTANT: If running in a Jupyter Notebook, use `await main()` instead of `asyncio.run(main())`
if __name__ == "__main__":
    asyncio.run(main())
```

**Event types:** `ORD_NRML` (order updates), `TRD_MSG` (trade confirmations), `MKT_STAT` (market open/close).

---

## Price Feed WebSockets (FIX3.0)

Receives live Level 1 (Touchline) and Level 2 (Best Five / Depth) market data via TCP socket with Zlib compression.

This client is **synchronous** — it runs on a background thread, so no `asyncio` is needed.

```python
import time
from choice_api import PriceFeedSocketClient

feed = PriceFeedSocketClient(
    host=client.bcast_ip,
    port=client.bcast_port,
    vendor_id=client.vendor_id,
    access_token=client.access_token
)

# Register callback for live market data
feed.on_message(lambda data: print(f"Market Data: {data}"))

# Start the background thread (automatically sends login and reconnects on drop)
feed.start_websocket()
time.sleep(2)  # Give it a moment to connect

# Subscribe to touchline and best five data
feed.subscribe_touchline(client.session_id, segment_id=1, token=2885)
feed.subscribe_best_five(client.session_id, segment_id=1, token=2885)

try:
    time.sleep(3600)
finally:
    feed.stop_websocket()
```

> **Note:** prices in the feed are delivered in **paisa**, not rupees — divide by 100 yourself if you need rupees.

---

## Logoff

```python
client.logoff()
```
