kitecli 0.3.1__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 (37) hide show
  1. {kitecli-0.3.1 → kitecli-0.3.3}/PKG-INFO +1 -1
  2. {kitecli-0.3.1 → kitecli-0.3.3}/cli/api_client.py +15 -0
  3. {kitecli-0.3.1 → kitecli-0.3.3}/cli/base_manager.py +4 -0
  4. {kitecli-0.3.1 → kitecli-0.3.3}/cli/charges.py +3 -3
  5. {kitecli-0.3.1 → kitecli-0.3.3}/cli/display.py +122 -69
  6. {kitecli-0.3.1 → kitecli-0.3.3}/cli/executor.py +81 -1
  7. kitecli-0.3.3/cli/greeks.py +334 -0
  8. {kitecli-0.3.1 → kitecli-0.3.3}/cli/indicators.py +6 -3
  9. {kitecli-0.3.1 → kitecli-0.3.3}/cli/kite_manager.py +83 -0
  10. {kitecli-0.3.1 → kitecli-0.3.3}/cli/kotak_manager.py +8 -0
  11. {kitecli-0.3.1 → kitecli-0.3.3}/cli/live_session.py +973 -73
  12. {kitecli-0.3.1 → kitecli-0.3.3}/cli/nli.py +1 -1
  13. kitecli-0.3.3/cli/parser.py +317 -0
  14. {kitecli-0.3.1 → kitecli-0.3.3}/cli/recorder.py +281 -6
  15. {kitecli-0.3.1 → kitecli-0.3.3}/kitecli.egg-info/PKG-INFO +1 -1
  16. {kitecli-0.3.1 → kitecli-0.3.3}/kitecli.egg-info/SOURCES.txt +1 -0
  17. {kitecli-0.3.1 → kitecli-0.3.3}/pyproject.toml +1 -1
  18. kitecli-0.3.3/tests/test_conditional_orders.py +515 -0
  19. {kitecli-0.3.1 → kitecli-0.3.3}/tests/test_parser.py +12 -0
  20. kitecli-0.3.3/tests/test_ui.py +329 -0
  21. kitecli-0.3.1/cli/greeks.py +0 -160
  22. kitecli-0.3.1/cli/parser.py +0 -181
  23. kitecli-0.3.1/tests/test_ui.py +0 -153
  24. {kitecli-0.3.1 → kitecli-0.3.3}/README.md +0 -0
  25. {kitecli-0.3.1 → kitecli-0.3.3}/cli/__init__.py +0 -0
  26. {kitecli-0.3.1 → kitecli-0.3.3}/cli/advisor.py +0 -0
  27. {kitecli-0.3.1 → kitecli-0.3.3}/cli/config.py +0 -0
  28. {kitecli-0.3.1 → kitecli-0.3.3}/cli/main.py +0 -0
  29. {kitecli-0.3.1 → kitecli-0.3.3}/cli/telegram_bot.py +0 -0
  30. {kitecli-0.3.1 → kitecli-0.3.3}/kitecli.egg-info/dependency_links.txt +0 -0
  31. {kitecli-0.3.1 → kitecli-0.3.3}/kitecli.egg-info/entry_points.txt +0 -0
  32. {kitecli-0.3.1 → kitecli-0.3.3}/kitecli.egg-info/requires.txt +0 -0
  33. {kitecli-0.3.1 → kitecli-0.3.3}/kitecli.egg-info/top_level.txt +0 -0
  34. {kitecli-0.3.1 → kitecli-0.3.3}/setup.cfg +0 -0
  35. {kitecli-0.3.1 → kitecli-0.3.3}/tests/test_multi_broker.py +0 -0
  36. {kitecli-0.3.1 → kitecli-0.3.3}/tests/test_nli.py +0 -0
  37. {kitecli-0.3.1 → 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.3.1
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
@@ -636,3 +636,18 @@ class KCLIClient:
636
636
  return mgr.get_ltp_and_tokens(api_key, symbols)
637
637
  return {}
638
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
+
@@ -157,3 +157,7 @@ class BaseBrokerManager(ABC):
157
157
  ) -> dict[str, Any]:
158
158
  """Calculate margin required for a proposed order. Default: fallback estimation."""
159
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"}
@@ -7,7 +7,7 @@ Official Rates from https://zerodha.com/charges/#charges-explained:
7
7
  - GST: 18% on (brokerage + SEBI charges + transaction charges).
8
8
  - SEBI charges: ₹10 / crore (0.0001%).
9
9
  - Stamp charges: 0.003% or ₹300 / crore on buy side.
10
- - IPFT fee: ₹50 / crore (0.0005%).
10
+ - IPFT fee: ₹0.01 / crore (0.0000001%).
11
11
  """
12
12
 
13
13
  from typing import Any
@@ -55,8 +55,8 @@ def calculate_option_charges(
55
55
  # 4. SEBI turnover fee: ₹10 / crore (0.0001%)
56
56
  sebi_fee = round(total_val * 0.000001, 2)
57
57
 
58
- # 5. IPFT fee: ₹50 / crore (0.000005%) for NSE/NFO
59
- ipft_fee = round(total_val * 0.000005, 2) if is_nse else 0.0
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
60
 
61
61
  # 6. GST: 18% on (Brokerage + Exchange Fee + SEBI Fee)
62
62
  gst = round((brokerage + exch_fee + sebi_fee) * 0.18, 2)
@@ -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:
@@ -370,4 +371,83 @@ async def execute_command(cmd: Any, context: ExecutionContext) -> dict:
370
371
  "message": "\n".join(output_lines)
371
372
  }
372
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
+
373
453
  raise TypeError(f"Execution not implemented for command type '{type(cmd)}'.")