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.
- {kitecli-0.2.9 → kitecli-0.3.3}/PKG-INFO +1 -1
- kitecli-0.3.3/cli/__init__.py +2 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/api_client.py +39 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/base_manager.py +18 -0
- kitecli-0.3.3/cli/charges.py +92 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/config.py +1 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/display.py +122 -69
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/executor.py +94 -1
- kitecli-0.3.3/cli/greeks.py +334 -0
- kitecli-0.3.3/cli/indicators.py +208 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/kite_manager.py +138 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/kotak_manager.py +54 -2
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/live_session.py +1955 -198
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/main.py +10 -1
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/nli.py +1 -1
- kitecli-0.3.3/cli/parser.py +317 -0
- kitecli-0.3.3/cli/recorder.py +792 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/PKG-INFO +1 -1
- {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/SOURCES.txt +4 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/pyproject.toml +1 -1
- kitecli-0.3.3/tests/test_conditional_orders.py +515 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_multi_broker.py +59 -1
- {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_parser.py +12 -0
- kitecli-0.3.3/tests/test_ui.py +329 -0
- kitecli-0.2.9/cli/__init__.py +0 -11
- kitecli-0.2.9/cli/parser.py +0 -181
- kitecli-0.2.9/cli/recorder.py +0 -321
- kitecli-0.2.9/tests/test_ui.py +0 -120
- {kitecli-0.2.9 → kitecli-0.3.3}/README.md +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/advisor.py +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/cli/telegram_bot.py +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/dependency_links.txt +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/entry_points.txt +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/requires.txt +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/kitecli.egg-info/top_level.txt +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/setup.cfg +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_nli.py +0 -0
- {kitecli-0.2.9 → kitecli-0.3.3}/tests/test_telegram.py +0 -0
|
@@ -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
|
|
@@ -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
|
-
"""
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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)}'.")
|