kitecli 0.2.9__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {kitecli-0.2.9 → kitecli-0.3.3}/PKG-INFO +1 -1
  2. kitecli-0.3.3/cli/__init__.py +2 -0
  3. {kitecli-0.2.9 → kitecli-0.3.3}/cli/api_client.py +39 -0
  4. {kitecli-0.2.9 → kitecli-0.3.3}/cli/base_manager.py +18 -0
  5. kitecli-0.3.3/cli/charges.py +92 -0
  6. {kitecli-0.2.9 → kitecli-0.3.3}/cli/config.py +1 -0
  7. {kitecli-0.2.9 → kitecli-0.3.3}/cli/display.py +122 -69
  8. {kitecli-0.2.9 → kitecli-0.3.3}/cli/executor.py +94 -1
  9. kitecli-0.3.3/cli/greeks.py +334 -0
  10. kitecli-0.3.3/cli/indicators.py +208 -0
  11. {kitecli-0.2.9 → kitecli-0.3.3}/cli/kite_manager.py +138 -0
  12. {kitecli-0.2.9 → kitecli-0.3.3}/cli/kotak_manager.py +54 -2
  13. {kitecli-0.2.9 → kitecli-0.3.3}/cli/live_session.py +1955 -198
  14. {kitecli-0.2.9 → kitecli-0.3.3}/cli/main.py +10 -1
  15. {kitecli-0.2.9 → kitecli-0.3.3}/cli/nli.py +1 -1
  16. kitecli-0.3.3/cli/parser.py +317 -0
  17. kitecli-0.3.3/cli/recorder.py +792 -0
  18. {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/PKG-INFO +1 -1
  19. {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/SOURCES.txt +4 -0
  20. {kitecli-0.2.9 → kitecli-0.3.3}/pyproject.toml +1 -1
  21. kitecli-0.3.3/tests/test_conditional_orders.py +515 -0
  22. {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_multi_broker.py +59 -1
  23. {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_parser.py +12 -0
  24. kitecli-0.3.3/tests/test_ui.py +329 -0
  25. kitecli-0.2.9/cli/__init__.py +0 -11
  26. kitecli-0.2.9/cli/parser.py +0 -181
  27. kitecli-0.2.9/cli/recorder.py +0 -321
  28. kitecli-0.2.9/tests/test_ui.py +0 -120
  29. {kitecli-0.2.9 → kitecli-0.3.3}/README.md +0 -0
  30. {kitecli-0.2.9 → kitecli-0.3.3}/cli/advisor.py +0 -0
  31. {kitecli-0.2.9 → kitecli-0.3.3}/cli/telegram_bot.py +0 -0
  32. {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/dependency_links.txt +0 -0
  33. {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/entry_points.txt +0 -0
  34. {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/requires.txt +0 -0
  35. {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/top_level.txt +0 -0
  36. {kitecli-0.2.9 → kitecli-0.3.3}/setup.cfg +0 -0
  37. {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_nli.py +0 -0
  38. {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_telegram.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kitecli
3
- Version: 0.2.9
3
+ Version: 0.3.3
4
4
  Summary: KiteCLI — Multi-account, multi-broker trading positions viewer (Zerodha + Kotak Neo)
5
5
  Author: KiteCLI Team
6
6
  License: MIT
@@ -0,0 +1,2 @@
1
+ # KiteCLI - Kite Connect CLI
2
+
@@ -261,6 +261,30 @@ class KCLIClient:
261
261
  except Exception as exc:
262
262
  raise KCLIClientError(str(exc)) from exc
263
263
 
264
+ def get_order_margin(
265
+ self,
266
+ api_key: str,
267
+ tradingsymbol: str,
268
+ transaction_type: str,
269
+ quantity: int,
270
+ price: float | None = None,
271
+ product: str = "NRML",
272
+ exchange: str = "NFO",
273
+ order_type: str = "LIMIT",
274
+ ) -> dict:
275
+ """Calculate margin required for a proposed order on a specific account."""
276
+ mgr = _manager_for(api_key)
277
+ return mgr.get_order_margin(
278
+ account_key=api_key,
279
+ tradingsymbol=tradingsymbol,
280
+ transaction_type=transaction_type,
281
+ quantity=quantity,
282
+ price=price,
283
+ product=product,
284
+ exchange=exchange,
285
+ order_type=order_type,
286
+ )
287
+
264
288
  def get_positions(self, api_keys: list[str]) -> dict:
265
289
  """Fetch open positions for the given accounts in parallel."""
266
290
  keys = api_keys or [a.get("api_key") for a in self._accounts if a.get("api_key")]
@@ -612,3 +636,18 @@ class KCLIClient:
612
636
  return mgr.get_ltp_and_tokens(api_key, symbols)
613
637
  return {}
614
638
 
639
+ def get_market_depth(self, symbol: str) -> dict:
640
+ """Fetch 5-depth order book, OHLC, and market stats for a symbol."""
641
+ if hasattr(_kite_manager, "get_market_depth"):
642
+ res = _kite_manager.get_market_depth(symbol)
643
+ if res.get("status") == "success":
644
+ return res
645
+
646
+ for mgr in [_kotak_manager]:
647
+ if hasattr(mgr, "get_market_depth"):
648
+ res = mgr.get_market_depth(symbol)
649
+ if res.get("status") == "success":
650
+ return res
651
+
652
+ return {"status": "error", "message": f"Market depth not available for symbol '{symbol}'"}
653
+
@@ -143,3 +143,21 @@ class BaseBrokerManager(ABC):
143
143
  def get_market_indices(self) -> dict[str, Any]:
144
144
  """Return Nifty / Sensex / India VIX snapshot. Default: not supported."""
145
145
  return {"status": "error", "message": "not supported by this broker"}
146
+
147
+ def get_order_margin(
148
+ self,
149
+ account_key: str,
150
+ tradingsymbol: str,
151
+ transaction_type: str,
152
+ quantity: int,
153
+ price: float | None = None,
154
+ product: str = "NRML",
155
+ exchange: str = "NFO",
156
+ order_type: str = "LIMIT",
157
+ ) -> dict[str, Any]:
158
+ """Calculate margin required for a proposed order. Default: fallback estimation."""
159
+ return {"status": "error", "message": "not supported by this broker"}
160
+
161
+ def get_market_depth(self, symbol: str) -> dict[str, Any]:
162
+ """Fetch 5-depth order book, OHLC, and market stats for a symbol. Default: not supported."""
163
+ return {"status": "error", "message": "not supported by this broker"}
@@ -0,0 +1,92 @@
1
+ """Zerodha & Statutory Charges Calculation Engine for Indian F&O & Equity Trades.
2
+
3
+ Official Rates from https://zerodha.com/charges/#charges-explained:
4
+ - Brokerage: Flat ₹20 per executed order (or 0.03% whichever is lower).
5
+ - STT/CTT: 0.15% on sell side (on premium value).
6
+ - Transaction charges: NSE/NFO Options = 0.03553% (on premium), BSE/BFO Options = 0.0325% (on premium).
7
+ - GST: 18% on (brokerage + SEBI charges + transaction charges).
8
+ - SEBI charges: ₹10 / crore (0.0001%).
9
+ - Stamp charges: 0.003% or ₹300 / crore on buy side.
10
+ - IPFT fee: ₹0.01 / crore (0.0000001%).
11
+ """
12
+
13
+ from typing import Any
14
+
15
+
16
+ def calculate_option_charges(
17
+ buy_price: float,
18
+ sell_price: float,
19
+ qty: int,
20
+ buy_orders_count: int = 1,
21
+ sell_orders_count: int = 1,
22
+ exchange: str = "NSE",
23
+ ) -> dict[str, float]:
24
+ """Calculate exact statutory charges and taxes for an F&O option contract trade per Zerodha official rates.
25
+
26
+ Args:
27
+ buy_price: Average buy price of matched legs.
28
+ sell_price: Average sell price of matched legs.
29
+ qty: Matched quantity.
30
+ buy_orders_count: Number of buy orders executed.
31
+ sell_orders_count: Number of sell orders executed.
32
+ exchange: Exchange name ('NSE', 'NFO', 'BSE', 'BFO').
33
+
34
+ Returns:
35
+ Dictionary containing breakdown of brokerage, STT, exchange fees, SEBI fees,
36
+ GST, stamp duty, IPFT, total charges, and net P&L.
37
+ """
38
+ buy_val = max(0.0, buy_price * qty)
39
+ sell_val = max(0.0, sell_price * qty)
40
+ total_val = buy_val + sell_val
41
+
42
+ exch_str = str(exchange or "NSE").upper()
43
+ is_nse = exch_str in ["NSE", "NFO"]
44
+
45
+ # 1. Brokerage: Flat ₹20 per executed order
46
+ brokerage = float((buy_orders_count + sell_orders_count) * 20.0)
47
+
48
+ # 2. STT: 0.15% on Sell side premium (Zerodha official rate)
49
+ stt = round(sell_val * 0.0015, 2)
50
+
51
+ # 3. Transaction charges: NSE/NFO Options = 0.03553% on premium, BSE/BFO Options = 0.0325% on premium
52
+ exch_rate = 0.0003553 if is_nse else 0.000325
53
+ exch_fee = round(total_val * exch_rate, 2)
54
+
55
+ # 4. SEBI turnover fee: ₹10 / crore (0.0001%)
56
+ sebi_fee = round(total_val * 0.000001, 2)
57
+
58
+ # 5. IPFT fee: ₹0.01 / crore (0.0000001%) for NSE/NFO
59
+ ipft_fee = round(total_val * 1e-9, 2) if is_nse else 0.0
60
+
61
+ # 6. GST: 18% on (Brokerage + Exchange Fee + SEBI Fee)
62
+ gst = round((brokerage + exch_fee + sebi_fee) * 0.18, 2)
63
+
64
+ # 7. Stamp duty: 0.003% (₹300/crore) on Buy side premium
65
+ stamp_duty = round(buy_val * 0.00003, 2)
66
+
67
+ total_charges = round(brokerage + stt + exch_fee + sebi_fee + ipft_fee + gst + stamp_duty, 2)
68
+
69
+ return {
70
+ "brokerage": brokerage,
71
+ "stt": stt,
72
+ "exch_fee": exch_fee,
73
+ "sebi_fee": sebi_fee,
74
+ "ipft_fee": ipft_fee,
75
+ "gst": gst,
76
+ "stamp_duty": stamp_duty,
77
+ "total_charges": total_charges,
78
+ }
79
+
80
+
81
+ def fetch_broker_charges(client: Any, order_params_list: list[dict[str, Any]]) -> float | None:
82
+ """Fetch official charges from Zerodha API if available."""
83
+ try:
84
+ if hasattr(client, "get_order_charges"):
85
+ resp = client.get_order_charges(order_params_list)
86
+ if isinstance(resp, list):
87
+ return sum(float(item.get("total", 0.0)) for item in resp)
88
+ elif isinstance(resp, dict):
89
+ return float(resp.get("total", 0.0))
90
+ except Exception:
91
+ pass
92
+ return None
@@ -21,6 +21,7 @@ sessions_lock = threading.RLock()
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
  DEFAULT_CONFIG = {
24
+ "record_executed_order_context": False,
24
25
  "accounts": [
25
26
  {
26
27
  # --- Zerodha account (default) ---
@@ -6,10 +6,13 @@ through the functions in this module so the CLI has a consistent,
6
6
  polished look.
7
7
  """
8
8
 
9
+ from datetime import date, datetime
10
+ from typing import Optional, Any
9
11
  from rich.console import Console
10
12
  from rich.panel import Panel
11
13
  from rich.table import Table
12
14
  from rich.text import Text
15
+ from cli.greeks import get_position_expiry_info, format_expiry_header, format_strike_display
13
16
 
14
17
  console = Console()
15
18
 
@@ -70,12 +73,121 @@ def _format_pnl_pct(value: float) -> str:
70
73
  return f"{prefix}{value:.2f}%"
71
74
 
72
75
 
76
+ def _build_account_positions_table(
77
+ positions: list[dict],
78
+ show_indices: bool = False,
79
+ start_idx: int = 1,
80
+ today: Optional[date] = None,
81
+ ) -> tuple[Table, int]:
82
+ """Build a grouped-by-expiry Table for an account's positions.
83
+
84
+ Returns:
85
+ (table, next_pos_idx)
86
+ """
87
+ table = Table(
88
+ show_header=True,
89
+ header_style="bold #58a6ff",
90
+ border_style="#30363d",
91
+ row_styles=["", "dim"],
92
+ pad_edge=True,
93
+ expand=True,
94
+ )
95
+ table.add_column("Symbol", style="bold #e6edf3", ratio=1)
96
+ table.add_column("Strike", style="bold #79c0ff", justify="center", no_wrap=True)
97
+ table.add_column("Lots/Qty", justify="right", no_wrap=True)
98
+ table.add_column("Avg Price", justify="right", no_wrap=True)
99
+ table.add_column("LTP", justify="right", no_wrap=True)
100
+ table.add_column("P&L", justify="right", no_wrap=True)
101
+
102
+ if not positions:
103
+ return table, start_idx
104
+
105
+ if today is None:
106
+ today = date.today()
107
+
108
+ # 1. Group positions by expiry
109
+ groups: dict[Optional[date], list[tuple[dict, dict]]] = {}
110
+ for pos in positions:
111
+ info = get_position_expiry_info(pos)
112
+ exp = info.get("expiry_date")
113
+ groups.setdefault(exp, []).append((info, pos))
114
+
115
+ # 2. Sort expiry keys: real dates ascending, None (Equity) at end
116
+ sorted_exp_keys = sorted([k for k in groups.keys() if k is not None])
117
+ if None in groups:
118
+ sorted_exp_keys.append(None)
119
+
120
+ pos_idx = start_idx
121
+ first_group = True
122
+
123
+ for exp_k in sorted_exp_keys:
124
+ items = groups[exp_k]
125
+ # Sort within group: strike ascending, CE before PE, tradingsymbol
126
+ items.sort(
127
+ key=lambda x: (
128
+ x[0].get("strike", 0.0),
129
+ 0 if x[0].get("option_type") == "CE" else (1 if x[0].get("option_type") == "PE" else 2),
130
+ str(x[1].get("tradingsymbol", "")),
131
+ )
132
+ )
133
+
134
+ group_pnl = sum(float(p.get("pnl", 0)) for _, p in items)
135
+ date_str, dte_str = format_expiry_header(exp_k, today=today)
136
+
137
+ # Section header
138
+ if not first_group:
139
+ table.add_section()
140
+
141
+ pnl_col = _pnl_style(group_pnl)
142
+ pnl_sign = "+" if group_pnl > 0 else ""
143
+ header_title = Text.assemble(
144
+ ("📅 " + date_str, "bold #58a6ff"),
145
+ (f" ({dte_str})" if dte_str else "", "#8b949e"),
146
+ )
147
+ subtotal_title = Text(f"{pnl_sign}{_format_currency(group_pnl)}", style=f"bold {pnl_col}")
148
+ table.add_row(header_title, "", "", "", "", subtotal_title, style="bold")
149
+ table.add_section()
150
+
151
+ for info, pos in items:
152
+ pnl = float(pos.get("pnl", 0))
153
+ style = _pnl_style(pnl)
154
+ pos_sign = "+" if pnl > 0 else ""
155
+
156
+ symbol = str(pos.get("tradingsymbol", ""))
157
+ if show_indices:
158
+ symbol = f"[{pos_idx}] {symbol}"
159
+ pos_idx += 1
160
+
161
+ strike_display = format_strike_display(info)
162
+
163
+ lot_size = pos.get("lot_size", 1) or 1
164
+ qty = pos.get("quantity", 0)
165
+ if lot_size > 1:
166
+ lots = qty / lot_size
167
+ qty_display = f"{int(lots)}L" if lots == int(lots) else f"{lots:.1f}L"
168
+ else:
169
+ qty_display = str(qty)
170
+
171
+ table.add_row(
172
+ symbol,
173
+ strike_display,
174
+ qty_display,
175
+ _format_currency(float(pos.get("average_price", 0))),
176
+ _format_currency(float(pos.get("last_price", 0))),
177
+ Text(f"{pos_sign}{_format_currency(pnl)}", style=style),
178
+ )
179
+
180
+ first_group = False
181
+
182
+ return table, pos_idx
183
+
184
+
73
185
  def display_positions(accounts_data: list[dict]) -> None:
74
- """Render positions tables for every account.
186
+ """Print a Rich table of open positions across all accounts.
75
187
 
76
188
  Args:
77
- accounts_data: List of dicts, each with keys ``name``,
78
- ``total_pnl``, and ``positions`` (list of position dicts).
189
+ accounts_data: List of account dicts, each with keys ``name``,
190
+ ``total_pnl``, and ``positions``.
79
191
  """
80
192
  grand_total_pnl = 0.0
81
193
 
@@ -86,6 +198,7 @@ def display_positions(accounts_data: list[dict]) -> None:
86
198
  grand_total_pnl += total_pnl
87
199
 
88
200
  pnl_color = _pnl_style(total_pnl)
201
+
89
202
  header_text = Text.assemble(
90
203
  (f" {name} ", "bold #e6edf3"),
91
204
  (" │ ", "#8b949e"),
@@ -104,31 +217,7 @@ def display_positions(accounts_data: list[dict]) -> None:
104
217
  console.print()
105
218
  continue
106
219
 
107
- table = Table(
108
- show_header=True,
109
- header_style="bold #58a6ff",
110
- border_style="#30363d",
111
- row_styles=["", "dim"],
112
- pad_edge=True,
113
- expand=True,
114
- )
115
- table.add_column("Symbol", style="bold #e6edf3", no_wrap=True)
116
- table.add_column("Qty", justify="right")
117
- table.add_column("Avg Price", justify="right")
118
- table.add_column("LTP", justify="right")
119
- table.add_column("P&L", justify="right")
120
-
121
- for pos in positions:
122
- pnl = float(pos.get("pnl", 0))
123
- style = _pnl_style(pnl)
124
-
125
- table.add_row(
126
- str(pos.get("tradingsymbol", "")),
127
- str(pos.get("quantity", 0)),
128
- _format_currency(float(pos.get("average_price", 0))),
129
- _format_currency(float(pos.get("last_price", 0))),
130
- Text(_format_currency(pnl), style=style),
131
- )
220
+ table, _ = _build_account_positions_table(positions, show_indices=False)
132
221
 
133
222
  panel = Panel(
134
223
  table,
@@ -181,7 +270,7 @@ def render_positions_to_string(accounts_data: list[dict], width: int = 80, show_
181
270
 
182
271
  pnl_color = _pnl_style(total_pnl)
183
272
 
184
- margin_net = account.get("margin_net")
273
+ margin_net = account.get("margin_net")
185
274
  margin_cash = account.get("margin_cash")
186
275
 
187
276
  header_parts: list[tuple[str, str]] = [
@@ -205,7 +294,6 @@ def render_positions_to_string(accounts_data: list[dict], width: int = 80, show_
205
294
 
206
295
  header_text = Text.assemble(*header_parts)
207
296
 
208
-
209
297
  if not positions:
210
298
  panel = Panel(
211
299
  Text(" No open positions", style="#8b949e italic"),
@@ -217,46 +305,11 @@ def render_positions_to_string(accounts_data: list[dict], width: int = 80, show_
217
305
  capture_console.print()
218
306
  continue
219
307
 
220
- table = Table(
221
- show_header=True,
222
- header_style="bold #58a6ff",
223
- border_style="#30363d",
224
- row_styles=["", "dim"],
225
- pad_edge=True,
226
- expand=True,
308
+ table, pos_idx = _build_account_positions_table(
309
+ positions,
310
+ show_indices=show_indices,
311
+ start_idx=pos_idx,
227
312
  )
228
- table.add_column("Symbol", style="bold #e6edf3", no_wrap=True)
229
- table.add_column("Lots/Qty", justify="right")
230
- table.add_column("Avg Price", justify="right")
231
- table.add_column("LTP", justify="right")
232
- table.add_column("P&L", justify="right")
233
-
234
- for pos in positions:
235
- pnl = float(pos.get("pnl", 0))
236
- style = _pnl_style(pnl)
237
-
238
- symbol = str(pos.get("tradingsymbol", ""))
239
- if show_indices:
240
- symbol = f"[{pos_idx}] {symbol}"
241
- pos_idx += 1
242
-
243
- lot_size = pos.get("lot_size", 1) or 1
244
- qty = pos.get("quantity", 0)
245
- if lot_size > 1:
246
- lots = qty / lot_size
247
- # Show as integer lots if whole number, else 1 decimal
248
- qty_display = f"{int(lots)}L" if lots == int(lots) else f"{lots:.1f}L"
249
- else:
250
- qty_display = str(qty)
251
-
252
- table.add_row(
253
- symbol,
254
- qty_display,
255
- _format_currency(float(pos.get("average_price", 0))),
256
- _format_currency(float(pos.get("last_price", 0))),
257
- Text(_format_currency(pnl), style=style),
258
- )
259
-
260
313
 
261
314
  panel = Panel(
262
315
  table,
@@ -3,7 +3,8 @@ from cli.api_client import KCLIClient
3
3
  from cli.parser import (
4
4
  AccountSelectCommand, PlaceOrderCommand, ExitCommand,
5
5
  StatusCommand, PositionsCommand, OrdersCommand,
6
- CancelOrderCommand, ModifyOrderCommand
6
+ CancelOrderCommand, ModifyOrderCommand, MarketDepthCommand,
7
+ ConditionalOrderCommand, CancelConditionalOrderCommand, ConditionalOrdersViewCommand
7
8
  )
8
9
 
9
10
  class ExecutionContext:
@@ -247,6 +248,19 @@ async def execute_command(cmd: Any, context: ExecutionContext) -> dict:
247
248
  "message": "\n".join(output_lines)
248
249
  }
249
250
 
251
+ # 5.5 Status Command
252
+ elif isinstance(cmd, StatusCommand):
253
+ res = context.client.get_status()
254
+ output_lines = ["🔌 Account Connection & Health Status:"]
255
+ for acct in res.get("accounts", []):
256
+ auth_icon = "🟢 Active" if acct.get("authenticated") else "🔴 Inactive"
257
+ broker_name = acct.get("broker", "Zerodha").title()
258
+ output_lines.append(f" • {acct.get('name', 'Account')} ({broker_name}): {auth_icon}")
259
+ return {
260
+ "status": "executed",
261
+ "message": "\n".join(output_lines)
262
+ }
263
+
250
264
  # 6. Orders Command
251
265
  elif isinstance(cmd, OrdersCommand):
252
266
  api_keys = [context.selected_account_key] if context.selected_account_key != "ALL" else None
@@ -357,4 +371,83 @@ async def execute_command(cmd: Any, context: ExecutionContext) -> dict:
357
371
  "message": "\n".join(output_lines)
358
372
  }
359
373
 
374
+ # 9. Market Depth Command
375
+ elif isinstance(cmd, MarketDepthCommand):
376
+ target_sym = cmd.symbol or context.selected_symbol
377
+ if not target_sym:
378
+ raise ValueError("Usage: md <symbol> (e.g. 'md NIFTY2682025000CE' or 'md INFY')")
379
+
380
+ res = context.client.get_market_depth(target_sym)
381
+ if res.get("status") != "success":
382
+ return {
383
+ "status": "executed",
384
+ "message": f"❌ Failed to fetch market depth for {target_sym}: {res.get('message', 'Unknown error')}"
385
+ }
386
+
387
+ sym = res.get("symbol", target_sym)
388
+ ltp = res.get("last_price", 0.0)
389
+ chg = res.get("change", 0.0)
390
+ chg_pct = res.get("change_pct", 0.0)
391
+ vol = res.get("volume", 0)
392
+ oi = res.get("oi", 0)
393
+ tot_b = res.get("total_buy_quantity", 0)
394
+ tot_s = res.get("total_sell_quantity", 0)
395
+
396
+ output_lines = [
397
+ f"📊 Market Depth: {sym} | LTP: ₹{ltp:.2f} ({chg:+.2f} / {chg_pct:+.2f}%) | Vol: {vol:,} | OI: {oi:,}",
398
+ "┌─────────┬─────────┬──────────────┬──────────────┬─────────┬─────────┐",
399
+ "│ Orders │ Qty │ Bid Price │ Ask Price │ Qty │ Orders │",
400
+ "├─────────┼─────────┼──────────────┼──────────────┼─────────┼─────────┤"
401
+ ]
402
+
403
+ bids = res.get("buy_depth", [])
404
+ asks = res.get("sell_depth", [])
405
+ for i in range(5):
406
+ b = bids[i] if i < len(bids) else {}
407
+ a = asks[i] if i < len(asks) else {}
408
+ b_ord = f"{b.get('orders', 0):>7}" if b.get('orders') else " -"
409
+ b_qty = f"{b.get('quantity', 0):>7}" if b.get('quantity') else " -"
410
+ b_pr = f"₹{b.get('price', 0.0):>9.2f}" if b.get('price') else " -"
411
+ a_pr = f"₹{a.get('price', 0.0):>9.2f}" if a.get('price') else " -"
412
+ a_qty = f"{a.get('quantity', 0):>7}" if a.get('quantity') else " -"
413
+ a_ord = f"{a.get('orders', 0):>7}" if a.get('orders') else " -"
414
+ output_lines.append(f"│ {b_ord} │ {b_qty} │ {b_pr} │ {a_pr} │ {a_qty} │ {a_ord} │")
415
+
416
+ output_lines.append("├─────────┼─────────┼──────────────┼──────────────┼─────────┼─────────┤")
417
+ output_lines.append(f"│ Total Buy Qty: {tot_b:<17,} │ Total Sell Qty: {tot_s:<16,} │")
418
+ output_lines.append("└────────────────────────────────┬──────────────────────────────┘")
419
+
420
+ return {
421
+ "status": "executed",
422
+ "message": "\n".join(output_lines)
423
+ }
424
+
425
+ # 10. Conditional Order Command (Headless / CLI)
426
+ elif isinstance(cmd, ConditionalOrderCommand):
427
+ parent_cmd = cmd.parent_cmd
428
+ if not cmd.confirmed:
429
+ parent_res = await execute_command(parent_cmd, context)
430
+ if parent_res.get("status") == "pending_confirmation":
431
+ return {
432
+ "status": "pending_confirmation",
433
+ "message": f"Confirm CONDITIONAL CHAIN:\n 1) {parent_res.get('message')}\n 2) THEN ON FILL: {cmd.child_raw_text}",
434
+ "command": cmd
435
+ }
436
+ parent_cmd.confirmed = True
437
+ return await execute_command(parent_cmd, context)
438
+
439
+ # 11. Cancel Conditional Order Command
440
+ elif isinstance(cmd, CancelConditionalOrderCommand):
441
+ return {
442
+ "status": "executed",
443
+ "message": f"Cancelled conditional orders targeting '{cmd.target}'."
444
+ }
445
+
446
+ # 12. Conditional Orders View Command
447
+ elif isinstance(cmd, ConditionalOrdersViewCommand):
448
+ return {
449
+ "status": "executed",
450
+ "message": "Conditional orders viewer is active in live TUI dashboard ('kcli live')."
451
+ }
452
+
360
453
  raise TypeError(f"Execution not implemented for command type '{type(cmd)}'.")