diff --git a/interactive_examples/README.md b/interactive_examples/README.md index 5d48cf2..6caf61c 100644 --- a/interactive_examples/README.md +++ b/interactive_examples/README.md @@ -1,6 +1,6 @@ # Upstox Python Interactive Examples -> **47 working examples** showcasing Upstox API features — **Instrument Search**, **Analytics Token**, and **Market Data** — across futures spreads, options strategies, arbitrage, historical analysis, live market data, and more. +> **55 working examples** showcasing Upstox API features — **Instrument Search**, **Analytics Token**, **Market Data**, and **Fundamentals** — across futures spreads, options strategies, arbitrage, historical analysis, live market data, fundamentals analysis, and more. [![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/) [![upstox-python-sdk](https://img.shields.io/pypi/v/upstox-python-sdk?label=upstox-python-sdk)](https://pypi.org/project/upstox-python-sdk/) @@ -22,7 +22,7 @@ python instrument_search/search_equity.py --token --query RELIANCE A Streamlit web app wraps every example with a UI — paste your token and run: -> **[▶ Open Live App](https://upstox-python-examples.streamlit.app)** *(coming soon — deploy steps below)* +> **[▶ Open Live App](https://upstox-interactive-python.streamlit.app/)** Or run locally: @@ -225,10 +225,58 @@ python market_data/live_depth_mcx.py --token # Ctrl-C to stop --- +### Fundamentals Analysis +*Uses the [Upstox Fundamentals API](https://upstox.com/developer/api-documentation/fundamentals). Pass `--symbol` with a stock name — the ISIN is resolved automatically.* + +| Script | What it does | +|---|---| +| `fundamentals/company_profile.py` | Sector, industry, market cap, employees and business overview | +| `fundamentals/key_ratios.py` | P/E, P/B, ROE, ROCE, D/E and more vs sector average | +| `fundamentals/balance_sheet.py` | Historical total assets, liabilities and derived equity | +| `fundamentals/income_statement.py` | Revenue, operating profit, net profit and EPS over time | +| `fundamentals/cash_flow.py` | Operating, investing and financing cash flows across periods | +| `fundamentals/corporate_actions.py` | Dividends, stock splits, bonuses — sorted most-recent first | +| `fundamentals/share_holdings.py` | Quarterly promoter / FII / DII / public shareholding | +| `fundamentals/competitors.py` | Peer companies in the same sector with market cap comparison | + +```bash +python fundamentals/company_profile.py --token --symbol RELIANCE +python fundamentals/key_ratios.py --token --symbol TCS +python fundamentals/balance_sheet.py --token --symbol HDFCBANK +python fundamentals/income_statement.py --token --symbol INFY +python fundamentals/cash_flow.py --token --symbol WIPRO +python fundamentals/corporate_actions.py --token --symbol HDFCBANK +python fundamentals/share_holdings.py --token --symbol RELIANCE +python fundamentals/competitors.py --token --symbol TCS +``` + +### Market Information +*Uses the [Upstox Market Information API](https://upstox.com/developer/api-documentation/market-information). Note: **Market Holidays**, **Market Timings**, and **Exchange Status** live under `market_data/` above.* + +| Script | What it does | +|---|---| +| `market_information/fii_data.py` | FII buy / sell / OI activity by segment (cash, futures, options) and interval | +| `market_information/dii_data.py` | DII buy / sell flow for the NSE cash market | +| `market_information/oi_data.py` | Per-strike call / put open interest for an underlying + expiry | +| `market_information/change_oi.py` | Per-strike change in OI over a configurable lookback | +| `market_information/max_pain.py` | Max pain strike + intraday max-pain vs spot | +| `market_information/pcr_data.py` | Overall PCR + intraday PCR / spot data points | + +```bash +python market_information/fii_data.py --token --data-type "NSE_EQ|CASH" --interval 1D +python market_information/dii_data.py --token --interval 1M +python market_information/oi_data.py --token --expiry 2026-05-29 +python market_information/change_oi.py --token --expiry 2026-05-29 --interval 5 +python market_information/max_pain.py --token --expiry 2026-05-29 --bucket-interval 60 +python market_information/pcr_data.py --token --expiry 2026-05-29 --bucket-interval 60 +``` + +--- + ## 🌐 Deploy the Streamlit App -The `streamlit_app.py` wraps all 39 examples in a browser UI with interactive inputs and charts. +The `streamlit_app.py` wraps all 47 examples in a browser UI with interactive inputs and charts (including Plotly charts for fundamentals). ### Streamlit Cloud (free, ~5 minutes) @@ -260,7 +308,9 @@ interactive_examples/ ├── arbitrage/ # 3 scripts ├── historical_analysis/ # 7 scripts ├── portfolio_screening/ # 3 scripts -└── market_data/ # 8 scripts +├── market_data/ # 8 scripts +├── fundamentals/ # 8 scripts +└── market_information/ # 6 scripts ``` --- diff --git a/interactive_examples/fundamentals/balance_sheet.py b/interactive_examples/fundamentals/balance_sheet.py new file mode 100644 index 0000000..763b199 --- /dev/null +++ b/interactive_examples/fundamentals/balance_sheet.py @@ -0,0 +1,148 @@ +""" +Balance Sheet — historical total assets, liabilities and equity over time. + +Fetches balance sheet history from the Upstox Fundamentals API and displays +assets vs liabilities for each reporting period. + +Usage: + python fundamentals/balance_sheet.py --token + python fundamentals/balance_sheet.py --token --symbol HDFCBANK +""" + +import argparse +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +RESET = "\033[0m" + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _val(obj, key, default="—"): + if obj is None: + return default + if isinstance(obj, dict): + v = obj.get(key) + else: + v = getattr(obj, key, None) + return v if v is not None else default + + +def _fmt(v): + if v in (None, "—", ""): + return "—" + try: + return f"{float(v):>16,.2f}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="Balance Sheet via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + parser.add_argument("--type", default="consolidated", + choices=("consolidated", "standalone"), + help="Statement type (default: consolidated)") + parser.add_argument("--fs", default="false", choices=("true", "false"), + help="Full statement toggle — include detailed line-item breakdown (default: false)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, _ = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching balance sheet for {args.symbol.upper()} (ISIN: {isin})" + f" — type={args.type}, fs={args.fs}...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_balance_sheet(isin, type=args.type, fs=args.fs) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + raw = data.to_dict() if hasattr(data, "to_dict") else (data if isinstance(data, dict) else vars(data)) + + units = raw.get("units_in") or "" + period = raw.get("time_period") or "" + history = raw.get("history") or [] + + units_label = f" (in {units})" if units else "" + print(f" Period type : {period or '—'}") + print(f" Units : {units or '—'}") + print() + + if history: + col_w = 14 + print(f" {BOLD}{'Period':<{col_w}} {'Total Assets':>18} {'Total Liabilities':>20} {'Equity':>18}{RESET}") + print(" " + "─" * 72) + + for entry in history: + if hasattr(entry, "to_dict"): + entry = entry.to_dict() + elif not isinstance(entry, dict): + entry = vars(entry) if hasattr(entry, "__dict__") else {} + + p = str(_val(entry, "period", "—")) + ta = _val(entry, "total_asset") + tl = _val(entry, "total_liability") + eq = None + try: + if ta not in (None, "—") and tl not in (None, "—"): + eq = float(ta) - float(tl) + except (TypeError, ValueError): + pass + + ta_s = _fmt(ta).strip() + tl_s = _fmt(tl).strip() + eq_s = _fmt(eq).strip() if eq is not None else "—" + + print(f" {CYAN}{p:<{col_w}}{RESET} {ta_s:>18} {tl_s:>20} {GREEN}{eq_s:>18}{RESET}") + else: + # fall back to full_statement if history is empty + full = raw.get("full_statement") or [] + if not full: + die("No balance sheet data in response.") + + items = full if isinstance(full, list) else [full] + print(f" {BOLD}{'Particular':<40} {'Value':>18}{RESET}") + print(" " + "─" * 62) + for entry in items: + if hasattr(entry, "to_dict"): + entry = entry.to_dict() + elif not isinstance(entry, dict): + entry = vars(entry) if hasattr(entry, "__dict__") else {} + particular = str(entry.get("particular") or "—") + hist_vals = entry.get("history") or [] + last = hist_vals[-1] if hist_vals else None + if isinstance(last, dict): + val = last.get("value") + elif hasattr(last, "to_dict"): + val = last.to_dict().get("value") + else: + val = last + print(f" {CYAN}{particular:<40}{RESET} {_fmt(val).strip():>18}") + + print() + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/cash_flow.py b/interactive_examples/fundamentals/cash_flow.py new file mode 100644 index 0000000..04a7c7c --- /dev/null +++ b/interactive_examples/fundamentals/cash_flow.py @@ -0,0 +1,154 @@ +""" +Cash Flow — operating, investing and financing cash flows across periods. + +Fetches the cash flow statement from the Upstox Fundamentals API and displays +operating / investing / financing activities alongside net cash flow over time. + +Usage: + python fundamentals/cash_flow.py --token + python fundamentals/cash_flow.py --token --symbol WIPRO +""" + +import argparse +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +RESET = "\033[0m" + +PRIORITY_ITEMS = ( + "operating", "cash from operations", "net cash from operating", + "investing", "cash from investing", "net cash from investing", + "financing", "cash from financing", "net cash from financing", + "net cash", "net change", +) + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _fmt(v): + if v in (None, "", "—"): + return "—" + try: + f = float(v) + color = GREEN if f > 0 else (RED if f < 0 else "") + return f"{color}{f:>14,.2f}{RESET}" + except (TypeError, ValueError): + return str(v)[:14] + + +def _is_priority(name: str) -> bool: + nl = name.lower() + return any(k in nl for k in PRIORITY_ITEMS) + + +def main(): + parser = argparse.ArgumentParser(description="Cash Flow via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + parser.add_argument("--type", default="consolidated", + choices=("consolidated", "standalone"), + help="Statement type (default: consolidated)") + parser.add_argument("--fs", default="false", choices=("true", "false"), + help="Full statement toggle — include detailed line-item breakdown (default: false)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, _ = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching cash flow statement for {args.symbol.upper()} (ISIN: {isin})" + f" — type={args.type}, fs={args.fs}...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_cash_flow(isin, type=args.type, fs=args.fs) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + raw = data.to_dict() if hasattr(data, "to_dict") else (data if isinstance(data, dict) else vars(data)) + + units = raw.get("units_in") or "" + period = raw.get("time_period") or "" + stmts = raw.get("cash_flow") or raw.get("full_statement") or [] + + print(f" Period type : {period or '—'}") + print(f" Units : {units or '—'}") + print() + + if not stmts: + die("No cash flow entries found.") + + items = stmts if isinstance(stmts, list) else [stmts] + + def _flat(hist): + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): + h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + entries = [] + periods = [] + max_cols = 6 + + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("category") or item.get("particular") or item.get("name") or "—") + flat = _flat(item.get("history")) + if not periods and flat: + periods = [p if p is not None else f"P{i+1}" for i, (p, _) in enumerate(flat)] + entries.append((name, [v for _, v in flat])) + + if not entries: + die("No line items found.") + + periods = periods[:max_cols] + col_w = 16 + label_w = 38 + + header_periods = " ".join(f"{str(p)[:col_w]:>{col_w}}" for p in periods) + print(f" {BOLD}{'Particular':<{label_w}} {header_periods}{RESET}") + print(" " + "─" * (label_w + (col_w + 2) * len(periods) + 12)) + + priority = [(n, h) for n, h in entries if _is_priority(n)] + rest = [(n, h) for n, h in entries if not _is_priority(n)] + + for name, hist in priority + rest: + vals = (hist or [])[:max_cols] + while len(vals) < len(periods): + vals.append(None) + row_vals = " ".join(f"{_fmt(v):>{col_w + 9}}" for v in vals) + color = CYAN if _is_priority(name) else "" + print(f" {color}{str(name):<{label_w}}{RESET} {row_vals}") + + print() + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/company_profile.py b/interactive_examples/fundamentals/company_profile.py new file mode 100644 index 0000000..8929d25 --- /dev/null +++ b/interactive_examples/fundamentals/company_profile.py @@ -0,0 +1,119 @@ +""" +Company Profile — business description, sector, and sector market cap. + +Fetches company profile data from the Upstox Fundamentals API using the +instrument's ISIN resolved from the given stock symbol. + +Usage: + python fundamentals/company_profile.py --token + python fundamentals/company_profile.py --token --symbol INFY +""" + +import argparse +import sys +import os +import textwrap + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def resolve_symbol(client, symbol: str): + """Return (isin, instrument_key, name) for the first NSE EQ match.""" + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + h = hits[0] + return h.get("isin", ""), h.get("instrument_key", ""), h.get("name", "") + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_mcap(mcap: dict) -> str: + if not mcap: + return "—" + formatted = mcap.get("formatted") + if formatted: + return str(formatted) + value = mcap.get("value") + unit = mcap.get("unit") or "" + if value is None: + return "—" + return f"{value:,.2f} {unit}".strip() + + +def main(): + parser = argparse.ArgumentParser(description="Company Profile via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, instrument_key, sym_name = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching company profile for {args.symbol.upper()} (ISIN: {isin})...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_company_profile(isin) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + raw = _as_dict(data) + + description = raw.get("company_profile") or "" + sector = raw.get("sector") or "—" + mcap_inr = _as_dict(raw.get("sector_market_cap_inr")) + mcap_usd = _as_dict(raw.get("sector_market_cap_usd")) + + print(f" {BOLD}{'Field':<22} Value{RESET}") + print(" " + "─" * 70) + fields = [ + ("Symbol", args.symbol.upper()), + ("Name", sym_name or "—"), + ("ISIN", isin), + ("Instrument Key", instrument_key), + ("Sector", str(sector)), + ("Sector Market Cap INR", _fmt_mcap(mcap_inr)), + ("Sector Market Cap USD", _fmt_mcap(mcap_usd)), + ] + for label, value in fields: + print(f" {CYAN}{label:<22}{RESET} {value}") + + if description: + print() + print(f" {BOLD}Business Description:{RESET}") + wrapped = textwrap.wrap(str(description), width=92) + for line in wrapped: + print(f" {line}") + + print() + print(f" {DIM}Note: sector market cap is the aggregate for the '{sector}' sector, " + f"not the company's own market cap.{RESET}") + print() + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/competitors.py b/interactive_examples/fundamentals/competitors.py new file mode 100644 index 0000000..5d42a90 --- /dev/null +++ b/interactive_examples/fundamentals/competitors.py @@ -0,0 +1,152 @@ +""" +Competitors — peer companies in the same sector with market cap data. + +Fetches the competitor list from the Upstox Fundamentals API using the +instrument_key resolved from the given stock symbol and displays each peer's +sector, sector market capitalisation and business description. + +Usage: + python fundamentals/competitors.py --token + python fundamentals/competitors.py --token --symbol TCS +""" + +import argparse +import sys +import os +import textwrap + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _instrument_to_label(ikey: str) -> str: + """NSE_EQ|INE242A01010 → INE242A01010 (helps eyeball the symbol).""" + if not ikey: + return "—" + return ikey.split("|", 1)[-1] + + +def _lookup_name(client, ikey: str): + """Resolve a peer's trading symbol + company name from its instrument_key.""" + if not ikey or "|" not in ikey: + return "—", "—" + isin = ikey.split("|", 1)[-1] + try: + resp = search_instrument(client, isin, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if hits: + h = hits[0] + return (h.get("trading_symbol") or h.get("symbol") or "—", + h.get("name") or "—") + except Exception: + pass + return "—", "—" + + +def main(): + parser = argparse.ArgumentParser(description="Competitors via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + args = parser.parse_args() + + client = get_api_client(args.token) + _, instrument_key = resolve_symbol(client, args.symbol) + if not instrument_key: + die(f"Could not resolve instrument key for '{args.symbol}'.") + + print(f"\nFetching competitors for {args.symbol.upper()} ({instrument_key})...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_competitors(instrument_key) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + items = data if isinstance(data, list) else [data] + + rows = [] + for item in items: + item = _as_dict(item) + ikey = str(item.get("instrument_key") or "—") + descr = str(item.get("company_profile") or "") + sector = str(item.get("sector") or "—") + mcap_inr = _as_dict(item.get("sector_market_cap_inr")) + mcap_usd = _as_dict(item.get("sector_market_cap_usd")) + mcap_value = mcap_inr.get("value") + sym, name = _lookup_name(client, ikey) + rows.append({ + "symbol": sym, + "name": name, + "ikey": ikey, + "descr": descr, + "sector": sector, + "mcap_inr_formatted": str(mcap_inr.get("formatted") or "—"), + "mcap_usd_formatted": str(mcap_usd.get("formatted") or "—"), + "mcap_value": mcap_value, + }) + + if not rows: + die("No competitor data found.") + + # Sort by sector market cap descending (when numeric) + def _sort_mcap(r): + try: + return float(r["mcap_value"]) + except (TypeError, ValueError): + return -1 + rows.sort(key=_sort_mcap, reverse=True) + + print(f" {BOLD}{'Symbol':<14} {'Company':<34} {'Sector':<18} {'Sector Mkt Cap (INR)':>22} {'(USD)':<10} Instrument Key{RESET}") + print(" " + "─" * 130) + for i, r in enumerate(rows): + color = GREEN if i == 0 else CYAN + name = (r["name"][:32] + "…") if len(r["name"]) > 33 else r["name"] + print(f" {color}{r['symbol']:<14}{RESET} {name:<34} {r['sector']:<18} {r['mcap_inr_formatted']:>22} {r['mcap_usd_formatted']:<10} {r['ikey']}") + + print() + for i, r in enumerate(rows, 1): + if not r["descr"]: + continue + header = r["name"] if r["name"] != "—" else r["ikey"] + print(f" {BOLD}{i}. {header}{RESET} {DIM}({r['symbol']} · {r['sector']}){RESET}") + wrapped = textwrap.wrap(r["descr"], width=92) + for line in wrapped[:4]: + print(f" {line}") + if len(wrapped) > 4: + print(f" {DIM}...{RESET}") + print() + + print(f" Total: {len(rows)} competitor(s)\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/corporate_actions.py b/interactive_examples/fundamentals/corporate_actions.py new file mode 100644 index 0000000..3f07c34 --- /dev/null +++ b/interactive_examples/fundamentals/corporate_actions.py @@ -0,0 +1,119 @@ +""" +Corporate Actions — dividends, splits, bonuses and other events. + +Fetches the corporate action history from the Upstox Fundamentals API and +displays each event with its date, amount or ratio. + +Usage: + python fundamentals/corporate_actions.py --token + python fundamentals/corporate_actions.py --token --symbol HDFCBANK +""" + +import argparse +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +RESET = "\033[0m" + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _val(obj, key, default="—"): + if obj is None: + return default + if isinstance(obj, dict): + v = obj.get(key) + else: + v = getattr(obj, key, None) + return str(v) if v is not None else default + + +def main(): + parser = argparse.ArgumentParser(description="Corporate Actions via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, _ = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching corporate actions for {args.symbol.upper()} (ISIN: {isin})...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_corporate_actions(isin) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + items = data if isinstance(data, list) else ([data] if data else []) + + if not items: + die("No corporate action data found.") + + rows = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + + name = _val(item, "name") + date = _val(item, "expiry_date") or _val(item, "date") or _val(item, "ex_date") + amount = _val(item, "amount") + ratio = _val(item, "ratio") + + # flatten event_details if present + details = item.get("event_details") or [] + detail_str = "" + if details: + if isinstance(details, list): + parts = [] + for d in details: + if hasattr(d, "to_dict"): + d = d.to_dict() + elif not isinstance(d, dict): + d = vars(d) if hasattr(d, "__dict__") else {} + dn = d.get("name") or "" + dv = d.get("value") or "" + if dn or dv: + parts.append(f"{dn}: {dv}" if dn else str(dv)) + detail_str = " | ".join(parts) + else: + detail_str = str(details) + + rows.append((name, date, amount, ratio, detail_str)) + + # Sort by date descending (most recent first) + rows.sort(key=lambda r: r[1], reverse=True) + + print(f" {BOLD}{'Action':<28} {'Ex-Date':<14} {'Amount':>12} {'Ratio':>12} Details{RESET}") + print(" " + "─" * 90) + + for name, date, amount, ratio, details in rows: + detail_col = details[:35] if details and details != "—" else "" + print(f" {CYAN}{name:<28}{RESET} {date:<14} {amount:>12} {ratio:>12} {detail_col}") + + print(f"\n Total: {len(rows)} corporate action(s)\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/income_statement.py b/interactive_examples/fundamentals/income_statement.py new file mode 100644 index 0000000..6777447 --- /dev/null +++ b/interactive_examples/fundamentals/income_statement.py @@ -0,0 +1,161 @@ +""" +Income Statement — revenue, profit and EPS trends across reporting periods. + +Fetches the income statement from the Upstox Fundamentals API and displays +key line items (revenue, operating profit, net profit, EPS) over time. + +Usage: + python fundamentals/income_statement.py --token + python fundamentals/income_statement.py --token --symbol TCS +""" + +import argparse +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +RESET = "\033[0m" + +# Line items to highlight at the top of the table +PRIORITY_ITEMS = ( + "revenue", "net revenue", "total revenue", "net sales", "total income", + "operating profit", "ebitda", "ebit", + "profit before tax", "pbt", + "net profit", "pat", "profit after tax", + "eps", "earnings per share", +) + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _fmt(v): + if v in (None, "", "—"): + return "—" + try: + return f"{float(v):>14,.2f}" + except (TypeError, ValueError): + return str(v)[:14] + + +def _is_priority(name: str) -> bool: + nl = name.lower() + return any(k in nl for k in PRIORITY_ITEMS) + + +def _print_table(entries, periods): + max_cols = 6 + periods = periods[:max_cols] + col_w = 14 + label_w = 38 + + header_periods = " ".join(f"{str(p)[:col_w]:>{col_w}}" for p in periods) + print(f" {BOLD}{'Particular':<{label_w}} {header_periods}{RESET}") + print(" " + "─" * (label_w + (col_w + 2) * len(periods))) + + for name, hist in entries: + vals = (hist or [])[:max_cols] + while len(vals) < len(periods): + vals.append(None) + row_vals = " ".join(f"{_fmt(v).strip():>{col_w}}" for v in vals) + color = CYAN if _is_priority(name) else "" + print(f" {color}{str(name):<{label_w}}{RESET} {row_vals}") + + +def main(): + parser = argparse.ArgumentParser(description="Income Statement via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + parser.add_argument("--type", default="consolidated", + choices=("consolidated", "standalone"), + help="Statement type (default: consolidated)") + parser.add_argument("--time-period", dest="time_period", default="yearly", + choices=("yearly", "quarterly"), + help="Reporting period (default: yearly)") + parser.add_argument("--fs", default="false", choices=("true", "false"), + help="Full statement toggle — include detailed line-item breakdown (default: false)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, _ = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching income statement for {args.symbol.upper()} (ISIN: {isin})" + f" — type={args.type}, period={args.time_period}, fs={args.fs}...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_income_statement(isin, type=args.type, + time_period=args.time_period, fs=args.fs) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + raw = data.to_dict() if hasattr(data, "to_dict") else (data if isinstance(data, dict) else vars(data)) + + units = raw.get("units_in") or "" + period = raw.get("time_period") or "" + stmts = raw.get("income_statement") or raw.get("full_statement") or [] + + print(f" Period type : {period or '—'}") + print(f" Units : {units or '—'}") + print() + + if not stmts: + die("No income statement entries found.") + + items = stmts if isinstance(stmts, list) else [stmts] + + def _flat(hist): + """Convert history items ({period,value} dicts or raw scalars) to (period, value) tuples.""" + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): + h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + entries = [] + periods = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("category") or item.get("particular") or item.get("name") or "—") + flat = _flat(item.get("history")) + if not periods and flat: + periods = [p if p is not None else f"P{i+1}" for i, (p, _) in enumerate(flat)] + entries.append((name, [v for _, v in flat])) + + if not entries: + die("No line items found.") + + # Sort: priority items first + priority = [(n, h) for n, h in entries if _is_priority(n)] + rest = [(n, h) for n, h in entries if not _is_priority(n)] + + _print_table(priority + rest, periods) + print() + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/key_ratios.py b/interactive_examples/fundamentals/key_ratios.py new file mode 100644 index 0000000..ac48efc --- /dev/null +++ b/interactive_examples/fundamentals/key_ratios.py @@ -0,0 +1,121 @@ +""" +Key Ratios — P/E, P/B, ROE, ROCE, D/E and more vs sector peers. + +Fetches key financial ratios from the Upstox Fundamentals API and prints +the company value alongside the sector average for quick comparison. + +Usage: + python fundamentals/key_ratios.py --token + python fundamentals/key_ratios.py --token --symbol TCS +""" + +import argparse +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +RESET = "\033[0m" + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _val(obj, key): + if obj is None: + return None + if isinstance(obj, dict): + return obj.get(key) + return getattr(obj, key, None) + + +def _fmt(v): + if v is None: + return "—" + try: + return f"{float(v):,.2f}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="Key Ratios via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, _ = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching key ratios for {args.symbol.upper()} (ISIN: {isin})...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_key_ratios(isin) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + # data is a list of KeyRatioData or plain dicts + items = data if isinstance(data, list) else ([data] if data else []) + + rows = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + if isinstance(item, dict): + rows.append((_val(item, "name"), _val(item, "company_value"), _val(item, "sector_value"))) + else: + rows.append((getattr(item, "name", ""), getattr(item, "company_value", None), getattr(item, "sector_value", None))) + + if not rows: + die("No ratio data found.") + + col_w = [36, 18, 18] + header = f" {BOLD}{'Ratio':<{col_w[0]}} {'Company':>{col_w[1]}} {'Sector Avg':>{col_w[2]}}{RESET}" + print(header) + print(" " + "─" * (col_w[0] + col_w[1] + col_w[2] + 4)) + + for name, company_val, sector_val in rows: + name_str = str(name) if name else "—" + c_str = _fmt(company_val) + s_str = _fmt(sector_val) + + # highlight if company beats sector on common ratios + color = "" + try: + cv = float(company_val) if company_val is not None else None + sv = float(sector_val) if sector_val is not None else None + if cv is not None and sv is not None and sv != 0: + name_lower = name_str.lower() + # lower is better for D/E, P/E, P/B in value investing context + if any(k in name_lower for k in ("debt", "d/e", "pe", "p/e", "p/b")): + color = GREEN if cv <= sv else RED + elif any(k in name_lower for k in ("roe", "roce", "profit", "margin", "growth")): + color = GREEN if cv >= sv else RED + except (TypeError, ValueError): + pass + + print(f" {CYAN}{name_str:<{col_w[0]}}{RESET} {color}{c_str:>{col_w[1]}}{RESET} {s_str:>{col_w[2]}}") + + print() + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/fundamentals/share_holdings.py b/interactive_examples/fundamentals/share_holdings.py new file mode 100644 index 0000000..bcb40c5 --- /dev/null +++ b/interactive_examples/fundamentals/share_holdings.py @@ -0,0 +1,134 @@ +""" +Share Holdings — promoter, FII, DII and public shareholding over quarters. + +Fetches shareholding pattern history from the Upstox Fundamentals API +and displays the quarterly breakdown across holder categories. + +Usage: + python fundamentals/share_holdings.py --token + python fundamentals/share_holdings.py --token --symbol INFY +""" + +import argparse +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, search_instrument, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +RESET = "\033[0m" + +CATEGORY_ORDER = ["promoter", "fii", "dii", "public", "others"] + + +def resolve_symbol(client, symbol: str): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + die(f"No NSE equity instrument found for '{symbol}'.") + return hits[0].get("isin", ""), hits[0].get("instrument_key", "") + + +def _sort_key(cat_name: str) -> int: + nl = cat_name.lower() + for i, k in enumerate(CATEGORY_ORDER): + if k in nl: + return i + return len(CATEGORY_ORDER) + + +def _fmt(v): + if v in (None, "", "—"): + return "—" + try: + return f"{float(v):>8.2f}%" + except (TypeError, ValueError): + return str(v)[:9] + + +def main(): + parser = argparse.ArgumentParser(description="Share Holdings via Fundamentals API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--symbol", default="RELIANCE", help="Stock symbol (default: RELIANCE)") + args = parser.parse_args() + + client = get_api_client(args.token) + isin, _ = resolve_symbol(client, args.symbol) + if not isin: + die(f"Could not resolve ISIN for '{args.symbol}'.") + + print(f"\nFetching share holdings for {args.symbol.upper()} (ISIN: {isin})...\n") + + api = upstox_client.FundamentalsApi(client) + try: + response = api.get_share_holdings(isin) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + items = data if isinstance(data, list) else ([data] if data else []) + + def _flat(hist): + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): + h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + categories = [] + all_periods = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + cat = str(item.get("category") or "—") + flat = _flat(item.get("history")) + if not all_periods: + all_periods = [p for p, _ in flat] + categories.append((cat, [v for _, v in flat])) + + if not categories: + die("No shareholding data found.") + + # Sort by standard category order + categories.sort(key=lambda x: _sort_key(x[0])) + + max_cols = 8 + n_periods = max(len(h) for _, h in categories) if categories else 0 + n_cols = min(n_periods, max_cols) + periods = [str(all_periods[i]) if i < len(all_periods) and all_periods[i] else f"Q{i+1}" + for i in range(n_cols)] + + col_w = 10 + label_w = 26 + + header_periods = " ".join(f"{p:>{col_w}}" for p in periods) + print(f" {BOLD}{'Category':<{label_w}} {header_periods}{RESET}") + print(" " + "─" * (label_w + (col_w + 1) * len(periods))) + + for cat, hist in categories: + vals = (hist or [])[:n_cols] + while len(vals) < n_cols: + vals.append(None) + row_vals = " ".join(f"{_fmt(v):>{col_w}}" for v in vals) + print(f" {CYAN}{cat:<{label_w}}{RESET} {row_vals}") + + if n_periods > max_cols: + print(f"\n (showing latest {max_cols} of {n_periods} quarters)") + print() + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/market_information/change_oi.py b/interactive_examples/market_information/change_oi.py new file mode 100644 index 0000000..2b71bcd --- /dev/null +++ b/interactive_examples/market_information/change_oi.py @@ -0,0 +1,119 @@ +""" +Change in OI — open-interest deltas across strikes over a configurable lookback. + +Fetches per-strike call/put OI change data from the Upstox Market API. + +Usage: + python market_information/change_oi.py --token --expiry 2026-05-29 + python market_information/change_oi.py --token --expiry 2026-05-29 --interval 5 +""" + +import argparse +import sys +import os +from datetime import date + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_delta(v): + if v in (None, "", "—"): + return "—" + try: + f = float(v) + if f > 0: + return f"{GREEN}{int(f):>+14,}{RESET}" + if f < 0: + return f"{RED}{int(f):>+14,}{RESET}" + return f"{int(f):>+14,}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="Change in OI via Market API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--instrument-key", default="NSE_INDEX|Nifty 50", + help='Underlying instrument key (default: "NSE_INDEX|Nifty 50")') + parser.add_argument("--expiry", required=True, help="Expiry date YYYY-MM-DD") + parser.add_argument("--date", dest="_date", default=date.today().isoformat(), + help="Data date YYYY-MM-DD (default: today)") + parser.add_argument("--interval", default="5", + help="Lookback in days (default: 5)") + args = parser.parse_args() + + client = get_api_client(args.token) + api = upstox_client.MarketApi(client) + + print(f"\nFetching Change-in-OI for {args.instrument_key} expiry={args.expiry} " + f"date={args._date} interval={args.interval}d...\n") + + try: + response = api.get_change_oi_data(args.instrument_key, args.expiry, args._date, args.interval) + except Exception as e: + die(f"API error: {e}") + + data = _as_dict(response.data) + if not data: + die("No data returned.") + + total_calls = data.get("total_calls") + total_puts = data.get("total_puts") + spot = data.get("spot_closing_price") + strikes = data.get("call_put_oi_data_list") or [] + + print(f" {CYAN}Spot Close {RESET} {spot if spot is not None else '—'}") + print(f" {CYAN}Total Δ Calls OI {RESET} {_fmt_delta(total_calls)}") + print(f" {CYAN}Total Δ Puts OI {RESET} {_fmt_delta(total_puts)}") + print() + + if not strikes: + die("No per-strike change-in-OI data.") + + col_w = 16 + print(f" {BOLD}{'Strike':>{col_w}} {'Δ Call OI':>{col_w}} {'Δ Put OI':>{col_w}}{RESET}") + print(" " + "─" * ((col_w + 1) * 3)) + + rows = [] + for s in strikes: + s = _as_dict(s) + try: + strike = float(s.get("strike") or s.get("strike_price") or 0) + except (TypeError, ValueError): + strike = 0 + rows.append((strike, s.get("call_oi"), s.get("put_oi"))) + + rows.sort(key=lambda r: r[0]) + for strike, call_oi, put_oi in rows: + marker = "" + try: + if spot is not None and abs(float(strike) - float(spot)) < 1e-6: + marker = f" {DIM}← spot{RESET}" + except (TypeError, ValueError): + pass + print(f" {strike:>{col_w},.2f} {_fmt_delta(call_oi):>{col_w}} {_fmt_delta(put_oi):>{col_w}}{marker}") + + print(f"\n {len(rows)} strikes.\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/market_information/dii_data.py b/interactive_examples/market_information/dii_data.py new file mode 100644 index 0000000..919a331 --- /dev/null +++ b/interactive_examples/market_information/dii_data.py @@ -0,0 +1,146 @@ +""" +DII Data — Domestic Institutional Investor activity for NSE Cash market. + +Fetches DII buy / sell data from the Upstox Market API for the requested +interval. + +Usage: + python market_information/dii_data.py --token + python market_information/dii_data.py --token --interval 1M + python market_information/dii_data.py --token --interval 1D --from 2026-04-01 +""" + +import argparse +import sys +import os +from datetime import datetime, timezone, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +RESET = "\033[0m" + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_amt(v): + if v in (None, "", "—"): + return "—" + try: + f = float(v) + color = GREEN if f > 0 else (RED if f < 0 else "") + return f"{color}{f:>16,.2f}{RESET}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="DII Activity Data via Market API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--data-type", default="NSE_EQ|CASH", + help="Segment (default: NSE_EQ|CASH — only supported value)") + parser.add_argument("--interval", default="1D", choices=("1D", "1M"), + help="Interval (default: 1D)") + parser.add_argument("--from", dest="_from", default=None, + help="Optional start date YYYY-MM-DD") + args = parser.parse_args() + + client = get_api_client(args.token) + api = upstox_client.MarketApi(client) + + print(f"\nFetching DII data — data_type={args.data_type}, interval={args.interval}" + f"{f', from={args._from}' if args._from else ''}...\n") + + try: + if args._from: + response = api.get_dii_data(args.data_type, args.interval, _from=args._from) + else: + response = api.get_dii_data(args.data_type, args.interval) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + IST = timezone(timedelta(hours=5, minutes=30)) + + def _ts(ms): + try: + return datetime.fromtimestamp(int(ms) / 1000, tz=IST).strftime("%Y-%m-%d") + except (TypeError, ValueError): + return str(ms) + + flat_rows = [] + if isinstance(data, dict): + for seg, rows in data.items(): + if not isinstance(rows, list): + rows = [rows] + for r in rows: + d = _as_dict(r) + d["segment"] = seg + flat_rows.append(d) + elif isinstance(data, list): + for r in data: + d = _as_dict(r) + d.setdefault("segment", args.data_type) + flat_rows.append(d) + else: + d = _as_dict(data) + d.setdefault("segment", args.data_type) + flat_rows.append(d) + + if not flat_rows: + die("No DII rows.") + + flat_rows.sort(key=lambda r: r.get("time_stamp") or 0) + + DATE_COLS = ("date", "trade_date", "_date", "time_stamp", "timestamp") + PREFERRED_COLS = ["segment", "time_stamp", "date", "trade_date", "_date", + "buy_amount", "sell_amount", + "buy_contracts", "sell_contracts"] + seen = set(); cols = [] + for r in flat_rows: + for c in PREFERRED_COLS: + if c in r and c not in seen: + cols.append(c); seen.add(c) + if not cols: + cols = list(flat_rows[0].keys()) + + col_w = 22 + header = " ".join(f"{c:>{col_w}}" for c in cols) + print(f" {BOLD}{header}{RESET}") + print(" " + "─" * (len(cols) * (col_w + 1))) + + for r in flat_rows: + cells = [] + for c in cols: + v = r.get(c) + if c == "time_stamp": + cells.append(f"{_ts(v):>{col_w}}") + elif c == "segment": + cells.append(f"{CYAN}{str(v or '—'):>{col_w}}{RESET}") + elif c in DATE_COLS: + cells.append(f"{str(v) if v is not None else '—':>{col_w}}") + else: + cells.append(f"{_fmt_amt(v):>{col_w + 9}}") + print(" " + " ".join(cells)) + + print(f"\n Rows: {len(flat_rows)}\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/market_information/fii_data.py b/interactive_examples/market_information/fii_data.py new file mode 100644 index 0000000..e120843 --- /dev/null +++ b/interactive_examples/market_information/fii_data.py @@ -0,0 +1,166 @@ +""" +FII Data — Foreign Institutional Investor activity across market segments. + +Fetches FII buy / sell / open-interest data from the Upstox Market API for the +requested segment and interval. + +Usage: + python market_information/fii_data.py --token + python market_information/fii_data.py --token --data-type "NSE_FO|INDEX_OPTIONS" --interval 1D + python market_information/fii_data.py --token --interval 1M --from 2026-01-01 +""" + +import argparse +import sys +import os +from datetime import datetime, timezone, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + +ALLOWED_DATA_TYPES = ( + "NSE_EQ|CASH", + "NSE_FO|INDEX_FUTURES", + "NSE_FO|STOCK_FUTURES", + "NSE_FO|INDEX_OPTIONS", + "NSE_FO|STOCK_OPTIONS", +) + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_amt(v): + if v in (None, "", "—"): + return "—" + try: + f = float(v) + color = GREEN if f > 0 else (RED if f < 0 else "") + return f"{color}{f:>16,.2f}{RESET}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="FII Activity Data via Market API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--data-type", default="NSE_EQ|CASH", + help=f"Segment (default: NSE_EQ|CASH). Allowed: {', '.join(ALLOWED_DATA_TYPES)}") + parser.add_argument("--interval", default="1D", choices=("1D", "1M"), + help="Interval (default: 1D)") + parser.add_argument("--from", dest="_from", default=None, + help="Optional start date YYYY-MM-DD") + args = parser.parse_args() + + client = get_api_client(args.token) + api = upstox_client.MarketApi(client) + + print(f"\nFetching FII data — data_type={args.data_type}, interval={args.interval}" + f"{f', from={args._from}' if args._from else ''}...\n") + + try: + if args._from: + response = api.get_fii_data(args.data_type, args.interval, _from=args._from) + else: + response = api.get_fii_data(args.data_type, args.interval) + except Exception as e: + die(f"API error: {e}") + + data = response.data + if not data: + die("No data returned.") + + IST = timezone(timedelta(hours=5, minutes=30)) + + def _ts(ms): + try: + return datetime.fromtimestamp(int(ms) / 1000, tz=IST).strftime("%Y-%m-%d") + except (TypeError, ValueError): + return str(ms) + + # The API returns either a list of rows OR a dict keyed by segment whose + # values are lists of rows. Flatten both into one row stream, tagging the + # segment when present. + flat_rows = [] + if isinstance(data, dict): + for seg, rows in data.items(): + if not isinstance(rows, list): + rows = [rows] + for r in rows: + d = _as_dict(r) + d["segment"] = seg + flat_rows.append(d) + elif isinstance(data, list): + for r in data: + d = _as_dict(r) + d.setdefault("segment", args.data_type) + flat_rows.append(d) + else: + d = _as_dict(data) + d.setdefault("segment", args.data_type) + flat_rows.append(d) + + if not flat_rows: + die("No FII rows.") + + # Sort by time_stamp if present + flat_rows.sort(key=lambda r: r.get("time_stamp") or 0) + + DATE_COLS = ("date", "trade_date", "_date", "time_stamp", "timestamp") + PREFERRED_COLS = [ + "segment", "time_stamp", "date", "trade_date", "_date", + "buy_amount", "sell_amount", + "buy_contracts", "sell_contracts", + "oi_contracts", "oi_amount", + "total_long_contracts", "total_short_contracts", + "total_call_long_contracts", "total_put_long_contracts", + "total_call_short_contracts", "total_put_short_contracts", + ] + seen = set(); cols = [] + for r in flat_rows: + for c in PREFERRED_COLS: + if c in r and c not in seen: + cols.append(c); seen.add(c) + if not cols: + cols = list(flat_rows[0].keys()) + + col_w = 22 + header = " ".join(f"{c:>{col_w}}" for c in cols) + print(f" {BOLD}{header}{RESET}") + print(" " + "─" * (len(cols) * (col_w + 1))) + + for r in flat_rows: + cells = [] + for c in cols: + v = r.get(c) + if c == "time_stamp": + cells.append(f"{_ts(v):>{col_w}}") + elif c == "segment": + cells.append(f"{CYAN}{str(v or '—'):>{col_w}}{RESET}") + elif c in DATE_COLS: + cells.append(f"{str(v) if v is not None else '—':>{col_w}}") + else: + # _fmt_amt adds ANSI color codes (~9 chars) — pad accordingly + cells.append(f"{_fmt_amt(v):>{col_w + 9}}") + print(" " + " ".join(cells)) + + print(f"\n Rows: {len(flat_rows)}\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/market_information/max_pain.py b/interactive_examples/market_information/max_pain.py new file mode 100644 index 0000000..73ef393 --- /dev/null +++ b/interactive_examples/market_information/max_pain.py @@ -0,0 +1,103 @@ +""" +Max Pain — strike level at which option writers experience least loss. + +Fetches the Max Pain value for the requested underlying + expiry + date plus +intraday insights bucketed at the requested interval. + +Usage: + python market_information/max_pain.py --token --expiry 2026-05-29 + python market_information/max_pain.py --token --instrument-key "NSE_INDEX|Nifty Bank" --expiry 2026-05-29 --bucket-interval 30 +""" + +import argparse +import sys +import os +from datetime import date + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_num(v, prec=2): + if v in (None, "", "—"): + return "—" + try: + return f"{float(v):,.{prec}f}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="Max Pain via Market API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--instrument-key", default="NSE_INDEX|Nifty 50", + help='Underlying instrument key (default: "NSE_INDEX|Nifty 50")') + parser.add_argument("--expiry", required=True, help="Expiry YYYY-MM-DD") + parser.add_argument("--date", dest="_date", default=date.today().isoformat(), + help="Data date YYYY-MM-DD (default: today)") + parser.add_argument("--bucket-interval", default="60", + help="Intraday bucket size in minutes (default: 60)") + args = parser.parse_args() + + client = get_api_client(args.token) + api = upstox_client.MarketApi(client) + + print(f"\nFetching Max Pain for {args.instrument_key} expiry={args.expiry} " + f"date={args._date} bucket={args.bucket_interval}m...\n") + + try: + response = api.get_max_pain_data( + args.instrument_key, args.expiry, args._date, args.bucket_interval + ) + except Exception as e: + die(f"API error: {e}") + + data = _as_dict(response.data) + if not data: + die("No data returned.") + + max_pain = data.get("max_pain") + spot = data.get("spot_closing_price") + insights = data.get("insights") or [] + + print(f" {CYAN}Max Pain Strike {RESET} {GREEN}{_fmt_num(max_pain)}{RESET}") + print(f" {CYAN}Spot Close {RESET} {_fmt_num(spot)}") + print() + + if not insights: + print(f" {DIM}No intraday insights returned.{RESET}\n") + return + + col_w = 14 + print(f" {BOLD}{'Timestamp':<24} {'Max Pain':>{col_w}} {'Spot':>{col_w}}{RESET}") + print(" " + "─" * (24 + (col_w + 1) * 2)) + + for ins in insights: + ins = _as_dict(ins) + ts = ins.get("timestamp") or ins.get("time") or "—" + mp = ins.get("max_pain") + sp = ins.get("spot_price") + print(f" {str(ts):<24} {_fmt_num(mp):>{col_w}} {_fmt_num(sp):>{col_w}}") + + print(f"\n {len(insights)} data points.\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/market_information/oi_data.py b/interactive_examples/market_information/oi_data.py new file mode 100644 index 0000000..9836a88 --- /dev/null +++ b/interactive_examples/market_information/oi_data.py @@ -0,0 +1,119 @@ +""" +OI Data — Open Interest across all strikes for an underlying + expiry. + +Fetches per-strike call/put OI from the Upstox Market API. + +Usage: + python market_information/oi_data.py --token --expiry 2026-05-29 + python market_information/oi_data.py --token --instrument-key "NSE_INDEX|Nifty Bank" --expiry 2026-05-29 --date 2026-05-14 +""" + +import argparse +import sys +import os +from datetime import date + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_int(v): + if v in (None, "", "—"): + return "—" + try: + return f"{int(float(v)):>14,}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="OI Data via Market API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--instrument-key", default="NSE_INDEX|Nifty 50", + help='Underlying instrument key (default: "NSE_INDEX|Nifty 50")') + parser.add_argument("--expiry", required=True, help="Expiry date YYYY-MM-DD") + parser.add_argument("--date", dest="_date", default=date.today().isoformat(), + help="Data date YYYY-MM-DD (default: today)") + args = parser.parse_args() + + client = get_api_client(args.token) + api = upstox_client.MarketApi(client) + + print(f"\nFetching OI data for {args.instrument_key} expiry={args.expiry} date={args._date}...\n") + + try: + response = api.get_oi_data(args.instrument_key, args.expiry, args._date) + except Exception as e: + die(f"API error: {e}") + + data = _as_dict(response.data) + if not data: + die("No data returned.") + + total_calls = data.get("total_calls") + total_puts = data.get("total_puts") + spot = data.get("spot_closing_price") + strikes = data.get("call_put_oi_data_list") or [] + + pcr = None + try: + if total_calls and float(total_calls) > 0: + pcr = float(total_puts) / float(total_calls) + except (TypeError, ValueError): + pass + + print(f" {CYAN}Spot Close {RESET} {spot if spot is not None else '—'}") + print(f" {CYAN}Total Calls OI {RESET} {_fmt_int(total_calls)}") + print(f" {CYAN}Total Puts OI {RESET} {_fmt_int(total_puts)}") + print(f" {CYAN}PCR (Puts/Calls){RESET} {pcr:.3f}" if pcr is not None else f" {CYAN}PCR{RESET} —") + print() + + if not strikes: + die("No per-strike OI data.") + + col_w = 16 + print(f" {BOLD}{'Strike':>{col_w}} {'Call OI':>{col_w}} {'Put OI':>{col_w}}{RESET}") + print(" " + "─" * ((col_w + 1) * 3)) + + rows = [] + for s in strikes: + s = _as_dict(s) + try: + strike = float(s.get("strike") or s.get("strike_price") or 0) + except (TypeError, ValueError): + strike = 0 + rows.append((strike, s.get("call_oi"), s.get("put_oi"))) + + rows.sort(key=lambda r: r[0]) + for strike, call_oi, put_oi in rows: + marker = "" + try: + if spot is not None and abs(float(strike) - float(spot)) < 1e-6: + marker = f" {DIM}← spot{RESET}" + except (TypeError, ValueError): + pass + print(f" {strike:>{col_w},.2f} {_fmt_int(call_oi):>{col_w}} {_fmt_int(put_oi):>{col_w}}{marker}") + + print(f"\n {len(rows)} strikes.\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/market_information/pcr_data.py b/interactive_examples/market_information/pcr_data.py new file mode 100644 index 0000000..66b13f7 --- /dev/null +++ b/interactive_examples/market_information/pcr_data.py @@ -0,0 +1,115 @@ +""" +PCR Data — Put-Call Ratio for an underlying + expiry with intraday insights. + +Fetches overall PCR plus intraday PCR / spot data points at the requested +bucket interval. + +Usage: + python market_information/pcr_data.py --token --expiry 2026-05-29 + python market_information/pcr_data.py --token --instrument-key "NSE_INDEX|Nifty Bank" --expiry 2026-05-29 --bucket-interval 15 +""" + +import argparse +import sys +import os +from datetime import date + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +from utils import get_api_client, die +import upstox_client + +BOLD = "\033[1m" +GREEN = "\033[32m" +RED = "\033[31m" +CYAN = "\033[36m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def _as_dict(obj): + if obj is None: + return {} + if isinstance(obj, dict): + return obj + if hasattr(obj, "to_dict"): + return obj.to_dict() + return vars(obj) if hasattr(obj, "__dict__") else {} + + +def _fmt_num(v, prec=2): + if v in (None, "", "—"): + return "—" + try: + return f"{float(v):,.{prec}f}" + except (TypeError, ValueError): + return str(v) + + +def _fmt_pcr(v): + if v in (None, "", "—"): + return "—" + try: + f = float(v) + color = RED if f < 0.7 else (GREEN if f > 1.3 else CYAN) + return f"{color}{f:>8.3f}{RESET}" + except (TypeError, ValueError): + return str(v) + + +def main(): + parser = argparse.ArgumentParser(description="PCR via Market API") + parser.add_argument("--token", required=True, help="Upstox access or analytics token") + parser.add_argument("--instrument-key", default="NSE_INDEX|Nifty 50", + help='Underlying instrument key (default: "NSE_INDEX|Nifty 50")') + parser.add_argument("--expiry", required=True, help="Expiry YYYY-MM-DD") + parser.add_argument("--date", dest="_date", default=date.today().isoformat(), + help="Data date YYYY-MM-DD (default: today)") + parser.add_argument("--bucket-interval", default="60", + help="Intraday bucket size in minutes (default: 60)") + args = parser.parse_args() + + client = get_api_client(args.token) + api = upstox_client.MarketApi(client) + + print(f"\nFetching PCR for {args.instrument_key} expiry={args.expiry} " + f"date={args._date} bucket={args.bucket_interval}m...\n") + + try: + response = api.get_pcr_data( + args.instrument_key, args.expiry, args._date, args.bucket_interval + ) + except Exception as e: + die(f"API error: {e}") + + data = _as_dict(response.data) + if not data: + die("No data returned.") + + pcr = data.get("pcr") or data.get("put_call_ratio") + spot = data.get("spot_closing_price") + insights = data.get("insights") or [] + + print(f" {CYAN}Overall PCR {RESET} {_fmt_pcr(pcr)}") + print(f" {CYAN}Spot Close {RESET} {_fmt_num(spot)}") + print() + + if not insights: + print(f" {DIM}No intraday insights returned.{RESET}\n") + return + + col_w = 12 + print(f" {BOLD}{'Timestamp':<24} {'PCR':>{col_w}} {'Spot':>{col_w}}{RESET}") + print(" " + "─" * (24 + (col_w + 1) * 2)) + + for ins in insights: + ins = _as_dict(ins) + ts = ins.get("timestamp") or ins.get("time") or "—" + p = ins.get("pcr") or ins.get("put_call_ratio") + sp = ins.get("spot_price") + print(f" {str(ts):<24} {_fmt_pcr(p):>{col_w + 9}} {_fmt_num(sp):>{col_w}}") + + print(f"\n {len(insights)} data points.\n") + + +if __name__ == "__main__": + main() diff --git a/interactive_examples/streamlit_app.py b/interactive_examples/streamlit_app.py index 9fc18f0..4b5a5da 100644 --- a/interactive_examples/streamlit_app.py +++ b/interactive_examples/streamlit_app.py @@ -107,6 +107,24 @@ "Live Depth MCX", "Live Depth USDINR", ], + "📊 Market Information": [ + "FII Data", + "DII Data", + "OI", + "Change in OI", + "Max Pain", + "PCR", + ], + "🔬 Fundamentals Analysis": [ + "Company Profile", + "Key Ratios", + "Balance Sheet", + "Income Statement", + "Cash Flow", + "Corporate Actions", + "Share Holdings", + "Competitors", + ], } category = st.selectbox("Category", list(CATEGORIES.keys())) @@ -2180,5 +2198,1342 @@ def render_usdinr(col, inst, exchange): st.rerun() +# ── Fundamentals Analysis ───────────────────────────────────────────────────── + +elif example == "Company Profile": + client = require_client() + + symbol = st.text_input("Stock Symbol", value="RELIANCE") + + if st.button("▶ Get Company Profile", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity instrument found for '{symbol}'."); st.stop() + + isin = hits[0].get("isin", "") + instrument_key = hits[0].get("instrument_key", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching company profile…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_company_profile(isin) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + raw = _as_dict(data) + description = raw.get("company_profile") or "" + sector = raw.get("sector") or "—" + mcap_inr = _as_dict(raw.get("sector_market_cap_inr")) + mcap_usd = _as_dict(raw.get("sector_market_cap_usd")) + + sym_name = hits[0].get("name", "") or symbol.upper() + st.subheader(sym_name) + + c1, c2, c3 = st.columns(3) + c1.metric("Sector", str(sector)) + c2.metric("Sector Mkt Cap (INR)", str(mcap_inr.get("formatted") or "—")) + c3.metric("Sector Mkt Cap (USD)", str(mcap_usd.get("formatted") or "—")) + + if description: + st.markdown("**Business Description**") + st.write(description) + + rows = { + "Symbol": symbol.upper(), + "Name": sym_name, + "ISIN": isin, + "Instrument Key": instrument_key, + "Sector": str(sector), + "Sector Mkt Cap INR": str(mcap_inr.get("formatted") or "—"), + "Sector Mkt Cap USD": str(mcap_usd.get("formatted") or "—"), + } + df = pd.DataFrame(list(rows.items()), columns=["Field", "Value"]) + st.dataframe(df, use_container_width=True, hide_index=True) + st.caption("Note: sector market cap is the aggregate for the sector, not the company's own market cap.") + + +elif example == "Key Ratios": + client = require_client() + + symbol = st.text_input("Stock Symbol", value="RELIANCE") + + if st.button("▶ Get Key Ratios", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + isin = hits[0].get("isin", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching key ratios…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_key_ratios(isin) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + items = data if isinstance(data, list) else ([data] if data else []) + rows = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + name = item.get("name") or "—" + cv = item.get("company_value") + sv = item.get("sector_value") + rows.append({"Ratio": name, "Company": cv, "Sector Avg": sv}) + + if not rows: + st.warning("No ratio data found."); st.stop() + + df = pd.DataFrame(rows) + st.dataframe(df, use_container_width=True, hide_index=True) + + # Chart: bar of numeric ratios — values may include "%" suffix + def _num(v): + if v is None: return None + try: + return float(str(v).replace("%", "").replace(",", "").strip()) + except (TypeError, ValueError): + return None + chart_df = df.copy() + chart_df["Company"] = chart_df["Company"].apply(_num) + chart_df["Sector Avg"] = chart_df["Sector Avg"].apply(_num) + chart_df = chart_df.dropna(subset=["Company"]) + + if not chart_df.empty: + fig = go.Figure() + fig.add_trace(go.Bar( + name="Company", x=chart_df["Ratio"], y=chart_df["Company"], + marker_color="#3498db", + )) + if chart_df["Sector Avg"].notna().any(): + fig.add_trace(go.Bar( + name="Sector Avg", x=chart_df["Ratio"], y=chart_df["Sector Avg"], + marker_color="#e67e22", + )) + fig.update_layout( + barmode="group", + title=f"{symbol.upper()} — Key Ratios vs Sector Average", + xaxis_title="Ratio", yaxis_title="Value", + template="plotly_dark", + height=400, + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Company value (blue) vs sector average (orange) for each ratio.") + + +elif example == "Balance Sheet": + client = require_client() + + c1, c2, c3 = st.columns(3) + symbol = c1.text_input("Stock Symbol", value="RELIANCE") + stmt_type = c2.selectbox("Type", ["consolidated", "standalone"]) + fs_flag = c3.selectbox("Full Statement", ["false", "true"]) + + if st.button("▶ Get Balance Sheet", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + isin = hits[0].get("isin", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching balance sheet…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_balance_sheet(isin, type=stmt_type, fs=fs_flag) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + raw = data.to_dict() if hasattr(data, "to_dict") else (data if isinstance(data, dict) else vars(data)) + units = raw.get("units_in") or "" + history = raw.get("history") or [] + + if history: + rows = [] + for entry in history: + if hasattr(entry, "to_dict"): + entry = entry.to_dict() + elif not isinstance(entry, dict): + entry = vars(entry) if hasattr(entry, "__dict__") else {} + period = entry.get("period", "—") + ta = entry.get("total_asset") + tl = entry.get("total_liability") + try: + eq = float(ta) - float(tl) if ta is not None and tl is not None else None + except (TypeError, ValueError): + eq = None + rows.append({"Period": period, "Total Assets": ta, "Total Liabilities": tl, "Equity": eq}) + + df = pd.DataFrame(rows) + for col in ["Total Assets", "Total Liabilities", "Equity"]: + df[col] = pd.to_numeric(df[col], errors="coerce") + + st.dataframe(df, use_container_width=True, hide_index=True) + if units: + st.caption(f"Values in {units}") + + chart_df = df.dropna(subset=["Total Assets"]) + if not chart_df.empty: + fig = go.Figure() + fig.add_trace(go.Bar(name="Total Assets", x=chart_df["Period"], y=chart_df["Total Assets"], marker_color="#3498db")) + fig.add_trace(go.Bar(name="Total Liabilities", x=chart_df["Period"], y=chart_df["Total Liabilities"], marker_color="#e74c3c")) + if chart_df["Equity"].notna().any(): + fig.add_trace(go.Bar(name="Equity", x=chart_df["Period"], y=chart_df["Equity"], marker_color="#27ae60")) + fig.update_layout( + barmode="group", + title=f"{symbol.upper()} — Balance Sheet History", + xaxis_title="Period", yaxis_title=f"Value ({units})" if units else "Value", + template="plotly_dark", height=420, + ) + st.plotly_chart(fig, use_container_width=True) + else: + full = raw.get("full_statement") or [] + if not full: + st.warning("No balance sheet data in response."); st.stop() + items = full if isinstance(full, list) else [full] + rows = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + particular = item.get("particular") or "—" + hist_vals = item.get("history") or [] + last = hist_vals[-1] if hist_vals else None + if hasattr(last, "to_dict"): + last = last.to_dict() + if isinstance(last, dict): + period = last.get("period", "—") + value = last.get("value") + else: + period = "—" + value = last + rows.append({"Particular": particular, "Latest Period": period, "Latest Value": value}) + st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True) + + if fs_flag == "true": + full = raw.get("full_statement") or [] + if full: + def _flat_hist(hist): + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + fs_entries, fs_periods = [], [] + for item in full: + if hasattr(item, "to_dict"): item = item.to_dict() + elif not isinstance(item, dict): item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("particular") or item.get("category") or item.get("name") or "—") + flat = _flat_hist(item.get("history")) + if len(flat) > len(fs_periods): + fs_periods = [p for p, _ in flat] + fs_entries.append((name, [v for _, v in flat])) + + fs_cols = [str(p) if p is not None else f"P{i+1}" for i, p in enumerate(fs_periods)] + fs_rows = [] + for name, vals in fs_entries: + row = {"Particular": name} + for i, c in enumerate(fs_cols): + row[c] = vals[i] if i < len(vals) else None + fs_rows.append(row) + + st.subheader("Full Statement (detailed line items)") + st.dataframe(pd.DataFrame(fs_rows), use_container_width=True, hide_index=True) + if units: + st.caption(f"Values in {units}") + + +elif example == "Income Statement": + client = require_client() + + c1, c2, c3, c4 = st.columns(4) + symbol = c1.text_input("Stock Symbol", value="RELIANCE") + stmt_type = c2.selectbox("Type", ["consolidated", "standalone"]) + period = c3.selectbox("Period", ["yearly", "quarterly"]) + fs_flag = c4.selectbox("Full Statement", ["false", "true"]) + + if st.button("▶ Get Income Statement", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + isin = hits[0].get("isin", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching income statement…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_income_statement(isin, type=stmt_type, time_period=period, fs=fs_flag) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + raw = data.to_dict() if hasattr(data, "to_dict") else (data if isinstance(data, dict) else vars(data)) + units = raw.get("units_in") or "" + stmts = raw.get("income_statement") or raw.get("full_statement") or [] + + if not stmts: + st.warning("No income statement data found."); st.stop() + + def _flat_hist(hist): + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): + h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + items_list = stmts if isinstance(stmts, list) else [stmts] + entries, period_labels = [], [] + for item in items_list: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("category") or item.get("particular") or item.get("name") or "—") + flat = _flat_hist(item.get("history")) + if len(flat) > len(period_labels): + period_labels = [p for p, _ in flat] + entries.append((name, [v for _, v in flat])) + + cols = [str(p) if p is not None else f"P{i+1}" for i, p in enumerate(period_labels)] + table_rows = [] + for name, vals in entries: + row = {"Particular": name} + for i, col in enumerate(cols): + row[col] = vals[i] if i < len(vals) else None + table_rows.append(row) + + df = pd.DataFrame(table_rows) + st.dataframe(df, use_container_width=True, hide_index=True) + if units: + st.caption(f"Values in {units}") + + PRIORITY_KEYS = ("revenue", "net revenue", "total revenue", "net sales", "total income", + "operating profit", "operating_profit", "ebitda", "ebit", + "net profit", "net_profit", "pat", "profit after tax") + + chart_rows = [(n, h) for n, h in entries if any(k in n.lower() for k in PRIORITY_KEYS)] + if chart_rows: + fig = go.Figure() + for name, vals in chart_rows: + y_vals = [float(v) if v is not None else None for v in vals] + fig.add_trace(go.Scatter( + x=cols[:len(y_vals)], y=y_vals, mode="lines+markers", + name=name.replace("_", " ").title(), + )) + fig.update_layout( + title=f"{symbol.upper()} — Income Statement Trends", + xaxis_title="Period", yaxis_title=f"Value ({units})" if units else "Value", + template="plotly_dark", height=420, + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Revenue, profit and other key line items over time.") + + if fs_flag == "true": + full = raw.get("full_statement") or [] + if full and full is not stmts: + fs_entries, fs_periods = [], [] + for item in full: + if hasattr(item, "to_dict"): item = item.to_dict() + elif not isinstance(item, dict): item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("particular") or item.get("category") or item.get("name") or "—") + flat = _flat_hist(item.get("history")) + if len(flat) > len(fs_periods): + fs_periods = [p for p, _ in flat] + fs_entries.append((name, [v for _, v in flat])) + + fs_cols = [str(p) if p is not None else f"P{i+1}" for i, p in enumerate(fs_periods)] + fs_rows = [] + for name, vals in fs_entries: + row = {"Particular": name} + for i, c in enumerate(fs_cols): + row[c] = vals[i] if i < len(vals) else None + fs_rows.append(row) + + st.subheader("Full Statement (detailed line items)") + st.dataframe(pd.DataFrame(fs_rows), use_container_width=True, hide_index=True) + if units: + st.caption(f"Values in {units}") + + +elif example == "Cash Flow": + client = require_client() + + c1, c2, c3 = st.columns(3) + symbol = c1.text_input("Stock Symbol", value="RELIANCE") + stmt_type = c2.selectbox("Type", ["consolidated", "standalone"]) + fs_flag = c3.selectbox("Full Statement", ["false", "true"]) + + if st.button("▶ Get Cash Flow", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + isin = hits[0].get("isin", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching cash flow statement…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_cash_flow(isin, type=stmt_type, fs=fs_flag) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + raw = data.to_dict() if hasattr(data, "to_dict") else (data if isinstance(data, dict) else vars(data)) + units = raw.get("units_in") or "" + stmts = raw.get("cash_flow") or raw.get("full_statement") or [] + + if not stmts: + st.warning("No cash flow data found."); st.stop() + + def _flat_hist(hist): + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): + h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + items_list = stmts if isinstance(stmts, list) else [stmts] + entries, period_labels = [], [] + for item in items_list: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("category") or item.get("particular") or item.get("name") or "—") + flat = _flat_hist(item.get("history")) + if len(flat) > len(period_labels): + period_labels = [p for p, _ in flat] + entries.append((name, [v for _, v in flat])) + + cols = [str(p) if p is not None else f"P{i+1}" for i, p in enumerate(period_labels)] + table_rows = [] + for name, vals in entries: + row = {"Particular": name} + for i, col in enumerate(cols): + row[col] = vals[i] if i < len(vals) else None + table_rows.append(row) + + df = pd.DataFrame(table_rows) + st.dataframe(df, use_container_width=True, hide_index=True) + if units: + st.caption(f"Values in {units}") + + CF_KEYS = ("operating", "investing", "financing", "net cash", "net change") + chart_rows = [(n, h) for n, h in entries if any(k in n.lower() for k in CF_KEYS)] + if chart_rows: + fig = go.Figure() + colors = ["#27ae60", "#e74c3c", "#3498db", "#f39c12"] + for i, (name, vals) in enumerate(chart_rows): + y_vals = [] + for v in vals: + try: + y_vals.append(float(v)) + except (TypeError, ValueError): + y_vals.append(None) + fig.add_trace(go.Bar( + name=name.replace("_", " ").title(), + x=cols[:len(y_vals)], + y=y_vals, + marker_color=colors[i % len(colors)], + )) + fig.update_layout( + barmode="group", + title=f"{symbol.upper()} — Cash Flow Statement", + xaxis_title="Period", yaxis_title=f"Value ({units})" if units else "Value", + template="plotly_dark", height=420, + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Operating / Investing / Financing cash flows per period.") + + if fs_flag == "true": + full = raw.get("full_statement") or [] + if full and full is not stmts: + fs_entries, fs_periods = [], [] + for item in full: + if hasattr(item, "to_dict"): item = item.to_dict() + elif not isinstance(item, dict): item = vars(item) if hasattr(item, "__dict__") else {} + name = str(item.get("particular") or item.get("category") or item.get("name") or "—") + flat = _flat_hist(item.get("history")) + if len(flat) > len(fs_periods): + fs_periods = [p for p, _ in flat] + fs_entries.append((name, [v for _, v in flat])) + + fs_cols = [str(p) if p is not None else f"P{i+1}" for i, p in enumerate(fs_periods)] + fs_rows = [] + for name, vals in fs_entries: + row = {"Particular": name} + for i, c in enumerate(fs_cols): + row[c] = vals[i] if i < len(vals) else None + fs_rows.append(row) + + st.subheader("Full Statement (detailed line items)") + st.dataframe(pd.DataFrame(fs_rows), use_container_width=True, hide_index=True) + if units: + st.caption(f"Values in {units}") + + +elif example == "Corporate Actions": + client = require_client() + + symbol = st.text_input("Stock Symbol", value="RELIANCE") + + if st.button("▶ Get Corporate Actions", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + isin = hits[0].get("isin", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching corporate actions…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_corporate_actions(isin) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + items = data if isinstance(data, list) else ([data] if data else []) + rows = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + name = item.get("name") or "—" + date_ = item.get("expiry_date") or item.get("date") or item.get("ex_date") or "—" + amount = item.get("amount") + ratio = item.get("ratio") + row = {"Action": name, "Ex-Date": date_, "Amount": amount, "Ratio": ratio} + for d in (item.get("event_details") or []): + if hasattr(d, "to_dict"): + d = d.to_dict() + elif not isinstance(d, dict): + d = vars(d) if hasattr(d, "__dict__") else {} + dn = (d.get("name") or "").strip() + dv = d.get("value") + if dn: + row[dn] = dv + rows.append(row) + + if not rows: + st.warning("No corporate action data found."); st.stop() + + df = pd.DataFrame(rows) + df["Ex-Date"] = pd.to_datetime(df["Ex-Date"], errors="coerce") + df = df.sort_values("Ex-Date", ascending=False) + + # Drop columns that are empty across all rows for readability + df = df.dropna(axis=1, how="all") + for col in list(df.columns): + if df[col].apply(lambda v: v is None or (isinstance(v, str) and not v.strip())).all(): + df = df.drop(columns=[col]) + + # Put core columns first; push event_details fields to the right + core = [c for c in ["Action", "Ex-Date", "Amount", "Ratio"] if c in df.columns] + extras = [c for c in df.columns if c not in core] + df = df[core + extras] + + st.metric("Total Actions", len(df)) + st.dataframe(df, use_container_width=True, hide_index=True) + + # Timeline scatter: plot numeric actions (dividends) over time + div_df = df[df["Amount"].notna()].copy() + div_df["Amount"] = pd.to_numeric(div_df["Amount"], errors="coerce") + div_df = div_df.dropna(subset=["Amount", "Ex-Date"]) + if not div_df.empty: + fig = px.scatter( + div_df, x="Ex-Date", y="Amount", color="Action", size="Amount", + title=f"{symbol.upper()} — Corporate Actions Timeline", + labels={"Ex-Date": "Ex-Date", "Amount": "Amount"}, + template="plotly_dark", + ) + fig.update_layout(height=380) + st.plotly_chart(fig, use_container_width=True) + st.caption("Each point represents a corporate action with a declared amount (e.g. dividend).") + + +elif example == "Share Holdings": + client = require_client() + + symbol = st.text_input("Stock Symbol", value="RELIANCE") + + if st.button("▶ Get Share Holdings", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + isin = hits[0].get("isin", "") + if not isin: + st.error("Could not resolve ISIN."); st.stop() + + with st.spinner("Fetching share holdings…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_share_holdings(isin) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + def _flat_hist(hist): + out = [] + for h in (hist or []): + if hasattr(h, "to_dict"): + h = h.to_dict() + if isinstance(h, dict): + out.append((h.get("period"), h.get("value"))) + else: + out.append((None, h)) + return out + + items = data if isinstance(data, list) else ([data] if data else []) + categories = [] + period_labels = [] + for item in items: + if hasattr(item, "to_dict"): + item = item.to_dict() + elif not isinstance(item, dict): + item = vars(item) if hasattr(item, "__dict__") else {} + cat = str(item.get("category") or "—") + flat = _flat_hist(item.get("history")) + if len(flat) > len(period_labels): + period_labels = [p for p, _ in flat] + categories.append((cat, [v for _, v in flat])) + + if not categories: + st.warning("No shareholding data found."); st.stop() + + n_periods = max(len(h) for _, h in categories) + cols = [str(period_labels[i]) if i < len(period_labels) and period_labels[i] else f"Q{i+1}" + for i in range(n_periods)] + + table_rows = [] + for cat, hist in categories: + row = {"Category": cat.replace("_", " ").title()} + for i, col in enumerate(cols): + row[col] = hist[i] if i < len(hist) else None + table_rows.append(row) + + df = pd.DataFrame(table_rows) + for col in cols: + df[col] = pd.to_numeric(df[col], errors="coerce") + + st.dataframe(df, use_container_width=True, hide_index=True) + + # Stacked bar over quarters + chart_cats = [c for c, h in categories if h] + if chart_cats: + fig = go.Figure() + colors = ["#3498db", "#e74c3c", "#27ae60", "#f39c12", "#9b59b6"] + for i, (cat, hist) in enumerate(categories): + y_vals = [float(v) if v is not None else 0 for v in hist] + fig.add_trace(go.Bar( + name=cat.replace("_", " ").title(), + x=cols[:len(y_vals)], y=y_vals, + marker_color=colors[i % len(colors)], + )) + fig.update_layout( + barmode="stack", + title=f"{symbol.upper()} — Shareholding Pattern (Stacked %)", + xaxis_title="Quarter", yaxis_title="Holding (%)", + template="plotly_dark", height=420, + ) + st.plotly_chart(fig, use_container_width=True) + + # Pie of latest quarter + latest_vals = [] + for cat, hist in categories: + v = hist[-1] if hist else None + try: + latest_vals.append((cat.replace("_", " ").title(), float(v))) + except (TypeError, ValueError): + pass + if latest_vals: + pie_cats = [r[0] for r in latest_vals] + pie_vals = [r[1] for r in latest_vals] + pie_fig = px.pie( + names=pie_cats, values=pie_vals, + title=f"{symbol.upper()} — Latest Quarter Shareholding", + template="plotly_dark", + ) + pie_fig.update_layout(height=380) + st.plotly_chart(pie_fig, use_container_width=True) + st.caption("Latest quarter shareholding breakdown by category.") + + +elif example == "Competitors": + client = require_client() + + symbol = st.text_input("Stock Symbol", value="RELIANCE") + + if st.button("▶ Get Competitors", type="primary"): + with st.spinner("Resolving instrument…"): + resp = search_instrument(client, symbol, exchanges="NSE", segments="EQ", records=1) + hits = resp.data or [] + if not hits: + st.error(f"No NSE equity found for '{symbol}'."); st.stop() + instrument_key = hits[0].get("instrument_key", "") + if not instrument_key: + st.error("Could not resolve instrument key."); st.stop() + + with st.spinner("Fetching competitors…"): + try: + api = upstox_client.FundamentalsApi(client) + response = api.get_competitors(instrument_key) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + def _lookup_name(ikey): + if not ikey or "|" not in ikey: + return "—", "—" + isin = ikey.split("|", 1)[-1] + try: + r = search_instrument(client, isin, exchanges="NSE", segments="EQ", records=1) + h = (r.data or [{}])[0] + return (h.get("trading_symbol") or h.get("symbol") or "—", + h.get("name") or "—") + except Exception: + return "—", "—" + + items = data if isinstance(data, list) else ([data] if data else []) + rows = [] + with st.spinner(f"Resolving {len(items)} peer name(s)…"): + for item in items: + item = _as_dict(item) + ikey = str(item.get("instrument_key") or "—") + descr = str(item.get("company_profile") or "") + sector = str(item.get("sector") or "—") + mcap_inr = _as_dict(item.get("sector_market_cap_inr")) + mcap_usd = _as_dict(item.get("sector_market_cap_usd")) + sym, name = _lookup_name(ikey) + rows.append({ + "Symbol": sym, + "Company": name, + "Sector": sector, + "Sector Mkt Cap (INR)": str(mcap_inr.get("formatted") or "—"), + "Sector Mkt Cap (USD)": str(mcap_usd.get("formatted") or "—"), + "Instrument Key": ikey, + "_mcap_value": mcap_inr.get("value"), + "Description": descr, + }) + + if not rows: + st.warning("No competitor data found."); st.stop() + + df = pd.DataFrame(rows) + df["_mcap_value"] = pd.to_numeric(df["_mcap_value"], errors="coerce") + df = df.sort_values("_mcap_value", ascending=False) + + st.metric("Peers Found", len(df)) + st.dataframe( + df.drop(columns=["_mcap_value"]), + use_container_width=True, + hide_index=True, + ) + + chart_df = df.dropna(subset=["_mcap_value"]).copy() + if not chart_df.empty: + # x-axis label: " ()" when known, else instrument key + chart_df["Peer"] = chart_df.apply( + lambda r: f"{r['Company']} ({r['Symbol']})" + if r["Company"] not in ("—", "") and r["Symbol"] not in ("—", "") + else r["Instrument Key"], + axis=1, + ) + fig = px.bar( + chart_df, x="Peer", y="_mcap_value", + title=f"{symbol.upper()} — Peer Sector Market Cap Comparison", + color="Peer", template="plotly_dark", + labels={"_mcap_value": "Sector Market Cap (INR)"}, + hover_data={ + "Sector": True, "Sector Mkt Cap (INR)": True, + "Instrument Key": True, "_mcap_value": False, "Peer": False, + }, + ) + fig.update_layout(height=420, showlegend=False, xaxis_tickangle=-25) + st.plotly_chart(fig, use_container_width=True) + st.caption("Sector market capitalisation for each peer (INR). Note: this is the aggregate for the peer's sector, not the peer's own market cap.") + + +# ── Market Information ─────────────────────────────────────────────────────── + +elif example == "FII Data": + client = require_client() + + DATA_TYPES_FII = [ + "NSE_EQ|CASH", + "NSE_FO|INDEX_FUTURES", + "NSE_FO|STOCK_FUTURES", + "NSE_FO|INDEX_OPTIONS", + "NSE_FO|STOCK_OPTIONS", + ] + c1, c2, c3 = st.columns(3) + data_type = c1.selectbox("Segment", DATA_TYPES_FII) + interval = c2.selectbox("Interval", ["1D", "1M"]) + from_date = c3.date_input("From (optional)", value=None) + + if st.button("▶ Fetch FII Data", type="primary"): + with st.spinner("Fetching FII activity…"): + try: + api = upstox_client.MarketApi(client) + if from_date: + response = api.get_fii_data(data_type, interval, _from=str(from_date)) + else: + response = api.get_fii_data(data_type, interval) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + # The API returns a dict keyed by segment whose values are row lists. + # Flatten into a single rows list with a segment column. + flat_rows = [] + if isinstance(data, dict): + for seg, rows in data.items(): + if not isinstance(rows, list): + rows = [rows] + for r in rows: + d = _as_dict(r); d["segment"] = seg; flat_rows.append(d) + elif isinstance(data, list): + for r in data: + d = _as_dict(r); d.setdefault("segment", data_type); flat_rows.append(d) + else: + d = _as_dict(data); d.setdefault("segment", data_type); flat_rows.append(d) + + if not flat_rows: + st.warning("No FII rows."); st.stop() + + df = pd.DataFrame(flat_rows) + if "time_stamp" in df.columns: + df["date"] = pd.to_datetime(pd.to_numeric(df["time_stamp"], errors="coerce"), + unit="ms", errors="coerce").dt.tz_localize("UTC").dt.tz_convert("Asia/Kolkata").dt.strftime("%Y-%m-%d") + df = df.drop(columns=["time_stamp"]) + + # Drop any column whose values are still nested objects (lists / dicts). + for col in list(df.columns): + if df[col].apply(lambda v: isinstance(v, (list, dict))).any(): + df = df.drop(columns=[col]) + + preferred = ["segment", "date", + "buy_amount", "sell_amount", + "buy_contracts", "sell_contracts", + "oi_contracts", "oi_amount", + "total_long_contracts", "total_short_contracts", + "total_call_long_contracts", "total_put_long_contracts", + "total_call_short_contracts", "total_put_short_contracts"] + ordered = [c for c in preferred if c in df.columns] + [c for c in df.columns if c not in preferred] + df = df[ordered] + if "date" in df.columns: + df = df.sort_values(["segment", "date"]) + + st.dataframe(df, use_container_width=True, hide_index=True) + + if "buy_amount" in df.columns and "sell_amount" in df.columns: + chart_df = df.copy() + chart_df["buy_amount"] = pd.to_numeric(chart_df["buy_amount"], errors="coerce") + chart_df["sell_amount"] = pd.to_numeric(chart_df["sell_amount"], errors="coerce") + chart_df["net"] = chart_df["buy_amount"].fillna(0) - chart_df["sell_amount"].fillna(0) + x_col = "date" if "date" in chart_df.columns else None + + fig = go.Figure() + for seg, sub in chart_df.groupby("segment"): + x = sub[x_col] if x_col else sub.index + fig.add_trace(go.Bar(x=x, y=sub["buy_amount"], name=f"{seg} · Buy", marker_color="#27ae60")) + fig.add_trace(go.Bar(x=x, y=sub["sell_amount"], name=f"{seg} · Sell", marker_color="#e74c3c")) + fig.add_trace(go.Scatter(x=x, y=sub["net"], name=f"{seg} · Net", + mode="lines+markers", line=dict(color="#f1c40f"), yaxis="y2")) + fig.update_layout( + title=f"FII Activity — {data_type} ({interval})", + barmode="group", template="plotly_dark", height=460, + xaxis_title="Period", + yaxis=dict(title="Buy / Sell (₹)"), + yaxis2=dict(title="Net (₹)", overlaying="y", side="right", showgrid=False), + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Buy / sell amounts per period with net flow on the right axis.") + + +elif example == "DII Data": + client = require_client() + + c1, c2 = st.columns(2) + interval = c1.selectbox("Interval", ["1D", "1M"]) + from_date = c2.date_input("From (optional)", value=None) + data_type = "NSE_EQ|CASH" + + if st.button("▶ Fetch DII Data", type="primary"): + with st.spinner("Fetching DII activity…"): + try: + api = upstox_client.MarketApi(client) + if from_date: + response = api.get_dii_data(data_type, interval, _from=str(from_date)) + else: + response = api.get_dii_data(data_type, interval) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + data = response.data + if not data: + st.warning("No data returned."); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + flat_rows = [] + if isinstance(data, dict): + for seg, rows in data.items(): + if not isinstance(rows, list): + rows = [rows] + for r in rows: + d = _as_dict(r); d["segment"] = seg; flat_rows.append(d) + elif isinstance(data, list): + for r in data: + d = _as_dict(r); d.setdefault("segment", data_type); flat_rows.append(d) + else: + d = _as_dict(data); d.setdefault("segment", data_type); flat_rows.append(d) + + if not flat_rows: + st.warning("No DII rows."); st.stop() + + df = pd.DataFrame(flat_rows) + if "time_stamp" in df.columns: + df["date"] = pd.to_datetime(pd.to_numeric(df["time_stamp"], errors="coerce"), + unit="ms", errors="coerce").dt.tz_localize("UTC").dt.tz_convert("Asia/Kolkata").dt.strftime("%Y-%m-%d") + df = df.drop(columns=["time_stamp"]) + + for col in list(df.columns): + if df[col].apply(lambda v: isinstance(v, (list, dict))).any(): + df = df.drop(columns=[col]) + + preferred = ["segment", "date", + "buy_amount", "sell_amount", + "buy_contracts", "sell_contracts"] + ordered = [c for c in preferred if c in df.columns] + [c for c in df.columns if c not in preferred] + df = df[ordered] + if "date" in df.columns: + df = df.sort_values(["segment", "date"]) + + st.dataframe(df, use_container_width=True, hide_index=True) + + if "buy_amount" in df.columns and "sell_amount" in df.columns: + chart_df = df.copy() + chart_df["buy_amount"] = pd.to_numeric(chart_df["buy_amount"], errors="coerce") + chart_df["sell_amount"] = pd.to_numeric(chart_df["sell_amount"], errors="coerce") + chart_df["net"] = chart_df["buy_amount"].fillna(0) - chart_df["sell_amount"].fillna(0) + x_col = "date" if "date" in chart_df.columns else None + + fig = go.Figure() + for seg, sub in chart_df.groupby("segment"): + x = sub[x_col] if x_col else sub.index + fig.add_trace(go.Bar(x=x, y=sub["buy_amount"], name=f"{seg} · Buy", marker_color="#27ae60")) + fig.add_trace(go.Bar(x=x, y=sub["sell_amount"], name=f"{seg} · Sell", marker_color="#e74c3c")) + fig.add_trace(go.Scatter(x=x, y=sub["net"], name=f"{seg} · Net", + mode="lines+markers", line=dict(color="#3498db"), yaxis="y2")) + fig.update_layout( + title=f"DII Activity — {data_type} ({interval})", + barmode="group", template="plotly_dark", height=460, + xaxis_title="Period", + yaxis=dict(title="Buy / Sell (₹)"), + yaxis2=dict(title="Net (₹)", overlaying="y", side="right", showgrid=False), + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Domestic Institutional Investor buy / sell flow over the requested interval.") + + +elif example == "OI": + client = require_client() + + UNDERLYINGS_OI = { + "NIFTY": "NSE_INDEX|Nifty 50", + "BANKNIFTY": "NSE_INDEX|Nifty Bank", + "FINNIFTY": "NSE_INDEX|Nifty Fin Service", + "MIDCPNIFTY":"NSE_INDEX|NIFTY MID SELECT", + } + c1, c2, c3 = st.columns(3) + label = c1.selectbox("Underlying", list(UNDERLYINGS_OI.keys())) + expiry = c2.date_input("Expiry", value=date.today() + timedelta(days=7)) + sel_date = c3.date_input("Date", value=date.today()) + + if st.button("▶ Fetch OI", type="primary"): + with st.spinner("Fetching OI data…"): + try: + api = upstox_client.MarketApi(client) + response = api.get_oi_data(UNDERLYINGS_OI[label], str(expiry), str(sel_date)) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + data = _as_dict(response.data) + if not data: + st.warning("No data returned."); st.stop() + + total_calls = data.get("total_calls") + total_puts = data.get("total_puts") + spot = data.get("spot_closing_price") + strikes = data.get("call_put_oi_data_list") or [] + + c1, c2, c3, c4 = st.columns(4) + c1.metric("Spot Close", f"{float(spot):,.2f}" if spot is not None else "—") + c2.metric("Total Calls OI", f"{int(float(total_calls)):,}" if total_calls is not None else "—") + c3.metric("Total Puts OI", f"{int(float(total_puts)):,}" if total_puts is not None else "—") + try: + pcr = float(total_puts) / float(total_calls) if total_calls else None + except (TypeError, ValueError): + pcr = None + c4.metric("PCR (Puts/Calls)", f"{pcr:.3f}" if pcr is not None else "—") + + rows = [] + for s in strikes: + s = _as_dict(s) + rows.append({ + "Strike": float(s.get("strike") or s.get("strike_price") or 0), + "Call OI": s.get("call_oi"), + "Put OI": s.get("put_oi"), + }) + + if not rows: + st.warning("No per-strike OI data."); st.stop() + + df = pd.DataFrame(rows).sort_values("Strike") + st.dataframe(df, use_container_width=True, hide_index=True) + + df["Call OI"] = pd.to_numeric(df["Call OI"], errors="coerce") + df["Put OI"] = pd.to_numeric(df["Put OI"], errors="coerce") + fig = go.Figure() + fig.add_trace(go.Bar(x=df["Strike"], y=df["Call OI"], name="Call OI", marker_color="#e74c3c")) + fig.add_trace(go.Bar(x=df["Strike"], y=df["Put OI"], name="Put OI", marker_color="#27ae60")) + if spot is not None: + try: + fig.add_vline(x=float(spot), line_dash="dash", line_color="#f1c40f", + annotation_text=f"Spot {float(spot):,.0f}", annotation_position="top") + except (TypeError, ValueError): + pass + fig.update_layout( + title=f"{label} — OI by Strike ({expiry})", + barmode="group", template="plotly_dark", height=460, + xaxis_title="Strike", yaxis_title="Open Interest", + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Call vs put OI across strikes; dashed line is spot close.") + + +elif example == "Change in OI": + client = require_client() + + UNDERLYINGS_COI = { + "NIFTY": "NSE_INDEX|Nifty 50", + "BANKNIFTY": "NSE_INDEX|Nifty Bank", + "FINNIFTY": "NSE_INDEX|Nifty Fin Service", + "MIDCPNIFTY":"NSE_INDEX|NIFTY MID SELECT", + } + c1, c2, c3, c4 = st.columns(4) + label = c1.selectbox("Underlying", list(UNDERLYINGS_COI.keys())) + expiry = c2.date_input("Expiry", value=date.today() + timedelta(days=7)) + sel_date = c3.date_input("Date", value=date.today()) + interval = c4.number_input("Lookback (days)", min_value=1, max_value=30, value=5, step=1) + + if st.button("▶ Fetch Change in OI", type="primary"): + with st.spinner("Fetching change-in-OI…"): + try: + api = upstox_client.MarketApi(client) + response = api.get_change_oi_data( + UNDERLYINGS_COI[label], str(expiry), str(sel_date), str(interval) + ) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + data = _as_dict(response.data) + if not data: + st.warning("No data returned."); st.stop() + + spot = data.get("spot_closing_price") + strikes = data.get("call_put_oi_data_list") or [] + + rows = [] + for s in strikes: + s = _as_dict(s) + rows.append({ + "Strike": float(s.get("strike") or s.get("strike_price") or 0), + "Δ Call OI": s.get("call_oi"), + "Δ Put OI": s.get("put_oi"), + }) + + if not rows: + st.warning("No per-strike delta data."); st.stop() + + df = pd.DataFrame(rows).sort_values("Strike") + df["Δ Call OI"] = pd.to_numeric(df["Δ Call OI"], errors="coerce") + df["Δ Put OI"] = pd.to_numeric(df["Δ Put OI"], errors="coerce") + + st.dataframe(df, use_container_width=True, hide_index=True) + + call_colors = ["#27ae60" if v >= 0 else "#e74c3c" for v in df["Δ Call OI"].fillna(0)] + put_colors = ["#27ae60" if v >= 0 else "#e74c3c" for v in df["Δ Put OI"].fillna(0)] + + fig = go.Figure() + fig.add_trace(go.Bar(x=df["Strike"], y=df["Δ Call OI"], name="Δ Call OI", marker_color=call_colors)) + fig.add_trace(go.Bar(x=df["Strike"], y=df["Δ Put OI"], name="Δ Put OI", marker_color=put_colors, opacity=0.6)) + if spot is not None: + try: + fig.add_vline(x=float(spot), line_dash="dash", line_color="#f1c40f", + annotation_text=f"Spot {float(spot):,.0f}", annotation_position="top") + except (TypeError, ValueError): + pass + fig.update_layout( + title=f"{label} — Δ OI over last {interval}d ({expiry})", + barmode="group", template="plotly_dark", height=460, + xaxis_title="Strike", yaxis_title="Change in OI", + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Green = OI added, Red = OI unwound, over the chosen lookback window.") + + +elif example == "Max Pain": + client = require_client() + + UNDERLYINGS_MP = { + "NIFTY": "NSE_INDEX|Nifty 50", + "BANKNIFTY": "NSE_INDEX|Nifty Bank", + "FINNIFTY": "NSE_INDEX|Nifty Fin Service", + "MIDCPNIFTY":"NSE_INDEX|NIFTY MID SELECT", + } + c1, c2, c3, c4 = st.columns(4) + label = c1.selectbox("Underlying", list(UNDERLYINGS_MP.keys())) + expiry = c2.date_input("Expiry", value=date.today() + timedelta(days=7)) + sel_date = c3.date_input("Date", value=date.today()) + bucket = c4.selectbox("Bucket (mins)", [15, 30, 60], index=2) + + if st.button("▶ Fetch Max Pain", type="primary"): + with st.spinner("Fetching max pain…"): + try: + api = upstox_client.MarketApi(client) + response = api.get_max_pain_data( + UNDERLYINGS_MP[label], str(expiry), str(sel_date), str(bucket) + ) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + data = _as_dict(response.data) + if not data: + st.warning("No data returned."); st.stop() + + max_pain = data.get("max_pain") + spot = data.get("spot_closing_price") + insights = data.get("insights") or [] + + c1, c2 = st.columns(2) + c1.metric("Max Pain", f"{float(max_pain):,.2f}" if max_pain is not None else "—") + c2.metric("Spot Close", f"{float(spot):,.2f}" if spot is not None else "—") + + rows = [] + for ins in insights: + ins = _as_dict(ins) + rows.append({ + "Timestamp": ins.get("timestamp") or ins.get("time"), + "Max Pain": ins.get("max_pain"), + "Spot": ins.get("spot_price"), + }) + + if not rows: + st.warning("No intraday insights."); st.stop() + + df = pd.DataFrame(rows) + df["Max Pain"] = pd.to_numeric(df["Max Pain"], errors="coerce") + df["Spot"] = pd.to_numeric(df["Spot"], errors="coerce") + + st.dataframe(df, use_container_width=True, hide_index=True) + + fig = go.Figure() + fig.add_trace(go.Scatter(x=df["Timestamp"], y=df["Max Pain"], mode="lines+markers", + name="Max Pain", line=dict(color="#f1c40f"))) + fig.add_trace(go.Scatter(x=df["Timestamp"], y=df["Spot"], mode="lines+markers", + name="Spot", line=dict(color="#3498db"))) + fig.update_layout( + title=f"{label} — Intraday Max Pain vs Spot ({expiry})", + template="plotly_dark", height=460, + xaxis_title="Time", yaxis_title="Price", + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Max-pain strike alongside spot price across the trading session.") + + +elif example == "PCR": + client = require_client() + + UNDERLYINGS_PCR = { + "NIFTY": "NSE_INDEX|Nifty 50", + "BANKNIFTY": "NSE_INDEX|Nifty Bank", + "FINNIFTY": "NSE_INDEX|Nifty Fin Service", + "MIDCPNIFTY":"NSE_INDEX|NIFTY MID SELECT", + } + c1, c2, c3, c4 = st.columns(4) + label = c1.selectbox("Underlying", list(UNDERLYINGS_PCR.keys())) + expiry = c2.date_input("Expiry", value=date.today() + timedelta(days=7)) + sel_date = c3.date_input("Date", value=date.today()) + bucket = c4.selectbox("Bucket (mins)", [15, 30, 60], index=2) + + if st.button("▶ Fetch PCR", type="primary"): + with st.spinner("Fetching PCR…"): + try: + api = upstox_client.MarketApi(client) + response = api.get_pcr_data( + UNDERLYINGS_PCR[label], str(expiry), str(sel_date), str(bucket) + ) + except Exception as e: + st.error(f"API error: {e}"); st.stop() + + def _as_dict(o): + if o is None: return {} + if isinstance(o, dict): return o + if hasattr(o, "to_dict"): return o.to_dict() + return vars(o) if hasattr(o, "__dict__") else {} + + data = _as_dict(response.data) + if not data: + st.warning("No data returned."); st.stop() + + overall_pcr = data.get("pcr") or data.get("put_call_ratio") + spot = data.get("spot_closing_price") + insights = data.get("insights") or [] + + c1, c2 = st.columns(2) + c1.metric("Overall PCR", f"{float(overall_pcr):.3f}" if overall_pcr is not None else "—") + c2.metric("Spot Close", f"{float(spot):,.2f}" if spot is not None else "—") + + rows = [] + for ins in insights: + ins = _as_dict(ins) + rows.append({ + "Timestamp": ins.get("timestamp") or ins.get("time"), + "PCR": ins.get("pcr") or ins.get("put_call_ratio"), + "Spot": ins.get("spot_price"), + }) + + if not rows: + st.warning("No intraday insights."); st.stop() + + df = pd.DataFrame(rows) + df["PCR"] = pd.to_numeric(df["PCR"], errors="coerce") + df["Spot"] = pd.to_numeric(df["Spot"], errors="coerce") + + st.dataframe(df, use_container_width=True, hide_index=True) + + fig = go.Figure() + fig.add_trace(go.Scatter(x=df["Timestamp"], y=df["PCR"], mode="lines+markers", + name="PCR", line=dict(color="#9b59b6"))) + fig.add_trace(go.Scatter(x=df["Timestamp"], y=df["Spot"], mode="lines+markers", + name="Spot", line=dict(color="#3498db"), yaxis="y2")) + fig.update_layout( + title=f"{label} — Intraday PCR ({expiry})", + template="plotly_dark", height=460, + xaxis_title="Time", + yaxis=dict(title="PCR"), + yaxis2=dict(title="Spot", overlaying="y", side="right", showgrid=False), + ) + st.plotly_chart(fig, use_container_width=True) + st.caption("Put-call ratio over time; spot is plotted on the right axis for context.") + + else: st.info(f"Example **{example}** — coming soon.") \ No newline at end of file diff --git a/interactive_examples/test_runner.py b/interactive_examples/test_runner.py index bd19118..1228044 100644 --- a/interactive_examples/test_runner.py +++ b/interactive_examples/test_runner.py @@ -9,6 +9,18 @@ import subprocess import sys import os +from datetime import date, timedelta + + +def _next_thursday() -> str: + today = date.today() + delta = (3 - today.weekday()) % 7 + if delta == 0: + delta = 7 + return (today + timedelta(days=delta)).isoformat() + + +NEXT_THU = _next_thursday() def validate_token(token): """Make a lightweight API call to confirm the token works. Returns (ok, message).""" @@ -114,6 +126,22 @@ def _find_python(): ("Historical Analysis", "historical_analysis/vwap.py", ["--query", "RELIANCE"]), ("Historical Analysis", "historical_analysis/beta_calculator.py", ["--query", "RELIANCE"]), ("Historical Analysis", "historical_analysis/stock_correlation.py", ["--queries", "RELIANCE,TCS,INFY"]), + + ("Fundamentals Analysis", "fundamentals/company_profile.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/key_ratios.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/balance_sheet.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/income_statement.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/cash_flow.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/corporate_actions.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/share_holdings.py", ["--symbol", "RELIANCE"]), + ("Fundamentals Analysis", "fundamentals/competitors.py", ["--symbol", "RELIANCE"]), + + ("Market Information", "market_information/fii_data.py", ["--data-type", "NSE_EQ|CASH", "--interval", "1D"]), + ("Market Information", "market_information/dii_data.py", ["--data-type", "NSE_EQ|CASH", "--interval", "1D"]), + ("Market Information", "market_information/oi_data.py", ["--expiry", NEXT_THU]), + ("Market Information", "market_information/change_oi.py", ["--expiry", NEXT_THU, "--interval", "5"]), + ("Market Information", "market_information/max_pain.py", ["--expiry", NEXT_THU, "--bucket-interval", "60"]), + ("Market Information", "market_information/pcr_data.py", ["--expiry", NEXT_THU, "--bucket-interval", "60"]), ] # Scripts that run indefinitely — killed after this many seconds and counted as PASS @@ -155,7 +183,7 @@ def run_example(script, token, extra_args): def main(): hr("═") print(f"{BOLD} Upstox API Examples — Test Runner{RESET}") - print(f" {len(EXAMPLES)} examples across 8 categories (including 7 new analytics scripts)") + print(f" {len(EXAMPLES)} examples across 10 categories") hr("═") print()