kitecli 0.1.0b1__py3-none-any.whl
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.
- cli/__init__.py +1 -0
- cli/api_client.py +322 -0
- cli/config.py +75 -0
- cli/display.py +367 -0
- cli/kite_manager.py +792 -0
- cli/live_session.py +1908 -0
- cli/main.py +292 -0
- kitecli-0.1.0b1.dist-info/METADATA +158 -0
- kitecli-0.1.0b1.dist-info/RECORD +12 -0
- kitecli-0.1.0b1.dist-info/WHEEL +5 -0
- kitecli-0.1.0b1.dist-info/entry_points.txt +2 -0
- kitecli-0.1.0b1.dist-info/top_level.txt +1 -0
cli/display.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich-based display helpers for KiteCLI.
|
|
3
|
+
|
|
4
|
+
All terminal output—banners, tables, panels, status messages—goes
|
|
5
|
+
through the functions in this module so the CLI has a consistent,
|
|
6
|
+
polished look.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
# ── Banner ─────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
BANNER = r"""
|
|
19
|
+
╦╔═╔═╗╦ ╦
|
|
20
|
+
╠╩╗║ ║ ║
|
|
21
|
+
╩ ╩╚═╝╩═╝╩
|
|
22
|
+
Kite Connect CLI
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def display_banner() -> None:
|
|
27
|
+
"""Print the KiteCLI banner in bold blue/cyan."""
|
|
28
|
+
console.print(Text(BANNER, style="bold #58a6ff"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Positions ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
def _format_currency(value: float) -> str:
|
|
34
|
+
"""Format a number as ₹ with commas (Indian locale style)."""
|
|
35
|
+
sign = "-" if value < 0 else ""
|
|
36
|
+
abs_val = abs(value)
|
|
37
|
+
# Split into integer and decimal parts
|
|
38
|
+
int_part = int(abs_val)
|
|
39
|
+
dec_part = f"{abs_val - int_part:.2f}"[1:] # ".xx"
|
|
40
|
+
|
|
41
|
+
# Indian grouping: last 3 digits, then groups of 2
|
|
42
|
+
s = str(int_part)
|
|
43
|
+
if len(s) > 3:
|
|
44
|
+
last3 = s[-3:]
|
|
45
|
+
rest = s[:-3]
|
|
46
|
+
groups = []
|
|
47
|
+
while rest:
|
|
48
|
+
groups.append(rest[-2:])
|
|
49
|
+
rest = rest[:-2]
|
|
50
|
+
groups.reverse()
|
|
51
|
+
formatted = ",".join(groups) + "," + last3
|
|
52
|
+
else:
|
|
53
|
+
formatted = s
|
|
54
|
+
|
|
55
|
+
return f"{sign}₹{formatted}{dec_part}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _pnl_style(value: float) -> str:
|
|
59
|
+
"""Return soft green/red/dim colors for positive/negative P&L."""
|
|
60
|
+
if value > 0:
|
|
61
|
+
return "#3fb950"
|
|
62
|
+
elif value < 0:
|
|
63
|
+
return "#f85149"
|
|
64
|
+
return "#8b949e"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _format_pnl_pct(value: float) -> str:
|
|
68
|
+
"""Format P&L percentage with + / - prefix."""
|
|
69
|
+
prefix = "+" if value > 0 else ""
|
|
70
|
+
return f"{prefix}{value:.2f}%"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def display_positions(accounts_data: list[dict]) -> None:
|
|
74
|
+
"""Render positions tables for every account.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
accounts_data: List of dicts, each with keys ``name``,
|
|
78
|
+
``total_pnl``, and ``positions`` (list of position dicts).
|
|
79
|
+
"""
|
|
80
|
+
grand_total_pnl = 0.0
|
|
81
|
+
|
|
82
|
+
for account in accounts_data:
|
|
83
|
+
name = account.get("name", "Unknown")
|
|
84
|
+
total_pnl = float(account.get("total_pnl", 0))
|
|
85
|
+
positions = account.get("positions", [])
|
|
86
|
+
grand_total_pnl += total_pnl
|
|
87
|
+
|
|
88
|
+
pnl_color = _pnl_style(total_pnl)
|
|
89
|
+
header_text = Text.assemble(
|
|
90
|
+
(f" {name} ", "bold #e6edf3"),
|
|
91
|
+
(" │ ", "#8b949e"),
|
|
92
|
+
("P&L: ", "bold"),
|
|
93
|
+
(f"{_format_currency(total_pnl)}", f"bold {pnl_color}"),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not positions:
|
|
97
|
+
panel = Panel(
|
|
98
|
+
Text(" No open positions", style="#8b949e italic"),
|
|
99
|
+
title=header_text,
|
|
100
|
+
border_style=pnl_color,
|
|
101
|
+
padding=(1, 2),
|
|
102
|
+
)
|
|
103
|
+
console.print(panel)
|
|
104
|
+
console.print()
|
|
105
|
+
continue
|
|
106
|
+
|
|
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
|
+
table.add_column("P&L %", justify="right")
|
|
121
|
+
|
|
122
|
+
for pos in positions:
|
|
123
|
+
pnl = float(pos.get("pnl", 0))
|
|
124
|
+
pnl_pct = float(pos.get("pnl_pct", 0))
|
|
125
|
+
style = _pnl_style(pnl)
|
|
126
|
+
|
|
127
|
+
table.add_row(
|
|
128
|
+
str(pos.get("tradingsymbol", "")),
|
|
129
|
+
str(pos.get("quantity", 0)),
|
|
130
|
+
_format_currency(float(pos.get("average_price", 0))),
|
|
131
|
+
_format_currency(float(pos.get("last_price", 0))),
|
|
132
|
+
Text(_format_currency(pnl), style=style),
|
|
133
|
+
Text(_format_pnl_pct(pnl_pct), style=style),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
panel = Panel(
|
|
137
|
+
table,
|
|
138
|
+
title=header_text,
|
|
139
|
+
border_style=pnl_color,
|
|
140
|
+
padding=(0, 1),
|
|
141
|
+
)
|
|
142
|
+
console.print(panel)
|
|
143
|
+
console.print()
|
|
144
|
+
|
|
145
|
+
# Grand total summary
|
|
146
|
+
gt_style = _pnl_style(grand_total_pnl)
|
|
147
|
+
summary = Text.assemble(
|
|
148
|
+
(" Grand Total P&L ", "bold"),
|
|
149
|
+
(f"{_format_currency(grand_total_pnl)}", f"bold {gt_style}"),
|
|
150
|
+
)
|
|
151
|
+
console.print(
|
|
152
|
+
Panel(
|
|
153
|
+
summary,
|
|
154
|
+
border_style=gt_style,
|
|
155
|
+
padding=(1, 2),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def render_positions_to_string(accounts_data: list[dict], width: int = 80, show_indices: bool = False) -> str:
|
|
161
|
+
"""Render positions to a string with ANSI color codes.
|
|
162
|
+
|
|
163
|
+
Identical to display_positions, but returns the string instead of printing.
|
|
164
|
+
"""
|
|
165
|
+
from io import StringIO
|
|
166
|
+
from rich.console import Console
|
|
167
|
+
|
|
168
|
+
# Create an in-memory console capturing output
|
|
169
|
+
capture_console = Console(
|
|
170
|
+
file=StringIO(),
|
|
171
|
+
force_terminal=True,
|
|
172
|
+
color_system="truecolor",
|
|
173
|
+
width=width,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
grand_total_pnl = 0.0
|
|
177
|
+
pos_idx = 1
|
|
178
|
+
|
|
179
|
+
for account in accounts_data:
|
|
180
|
+
name = account.get("name", "Unknown")
|
|
181
|
+
total_pnl = float(account.get("total_pnl", 0))
|
|
182
|
+
positions = account.get("positions", [])
|
|
183
|
+
grand_total_pnl += total_pnl
|
|
184
|
+
|
|
185
|
+
pnl_color = _pnl_style(total_pnl)
|
|
186
|
+
header_text = Text.assemble(
|
|
187
|
+
(f" {name} ", "bold #e6edf3"),
|
|
188
|
+
(" │ ", "#8b949e"),
|
|
189
|
+
("P&L: ", "bold"),
|
|
190
|
+
(f"{_format_currency(total_pnl)}", f"bold {pnl_color}"),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not positions:
|
|
194
|
+
panel = Panel(
|
|
195
|
+
Text(" No open positions", style="#8b949e italic"),
|
|
196
|
+
title=header_text,
|
|
197
|
+
border_style=pnl_color,
|
|
198
|
+
padding=(1, 2),
|
|
199
|
+
)
|
|
200
|
+
capture_console.print(panel)
|
|
201
|
+
capture_console.print()
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
table = Table(
|
|
205
|
+
show_header=True,
|
|
206
|
+
header_style="bold #58a6ff",
|
|
207
|
+
border_style="#30363d",
|
|
208
|
+
row_styles=["", "dim"],
|
|
209
|
+
pad_edge=True,
|
|
210
|
+
expand=True,
|
|
211
|
+
)
|
|
212
|
+
table.add_column("Symbol", style="bold #e6edf3", no_wrap=True)
|
|
213
|
+
table.add_column("Qty", justify="right")
|
|
214
|
+
table.add_column("Avg Price", justify="right")
|
|
215
|
+
table.add_column("LTP", justify="right")
|
|
216
|
+
table.add_column("P&L", justify="right")
|
|
217
|
+
table.add_column("P&L %", justify="right")
|
|
218
|
+
|
|
219
|
+
for pos in positions:
|
|
220
|
+
pnl = float(pos.get("pnl", 0))
|
|
221
|
+
pnl_pct = float(pos.get("pnl_pct", 0))
|
|
222
|
+
style = _pnl_style(pnl)
|
|
223
|
+
|
|
224
|
+
symbol = str(pos.get("tradingsymbol", ""))
|
|
225
|
+
if show_indices:
|
|
226
|
+
symbol = f"[{pos_idx}] {symbol}"
|
|
227
|
+
pos_idx += 1
|
|
228
|
+
|
|
229
|
+
table.add_row(
|
|
230
|
+
symbol,
|
|
231
|
+
str(pos.get("quantity", 0)),
|
|
232
|
+
_format_currency(float(pos.get("average_price", 0))),
|
|
233
|
+
_format_currency(float(pos.get("last_price", 0))),
|
|
234
|
+
Text(_format_currency(pnl), style=style),
|
|
235
|
+
Text(_format_pnl_pct(pnl_pct), style=style),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
panel = Panel(
|
|
240
|
+
table,
|
|
241
|
+
title=header_text,
|
|
242
|
+
border_style=pnl_color,
|
|
243
|
+
padding=(0, 1),
|
|
244
|
+
)
|
|
245
|
+
capture_console.print(panel)
|
|
246
|
+
capture_console.print()
|
|
247
|
+
|
|
248
|
+
# Grand total summary
|
|
249
|
+
gt_style = _pnl_style(grand_total_pnl)
|
|
250
|
+
summary_text = Text.assemble(
|
|
251
|
+
(" Grand Total P&L ", "bold"),
|
|
252
|
+
(f"{_format_currency(grand_total_pnl)}", f"bold {gt_style}"),
|
|
253
|
+
)
|
|
254
|
+
capture_console.print(
|
|
255
|
+
Panel(
|
|
256
|
+
summary_text,
|
|
257
|
+
border_style=gt_style,
|
|
258
|
+
padding=(1, 2),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return capture_console.file.getvalue()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ── Login URLs ─────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
def display_login_urls(accounts: list[dict]) -> None:
|
|
268
|
+
"""Show Kite login URLs for each account.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
accounts: List of dicts with ``name`` and ``login_url`` keys.
|
|
272
|
+
"""
|
|
273
|
+
lines = Text()
|
|
274
|
+
for acct in accounts:
|
|
275
|
+
lines.append(" ✓ ", style="bold #3fb950")
|
|
276
|
+
lines.append(f"{acct.get('name', 'Account')}", style="bold #e6edf3")
|
|
277
|
+
lines.append("\n ", style="")
|
|
278
|
+
lines.append(f"{acct.get('login_url', 'N/A')}", style="underline #58a6ff")
|
|
279
|
+
lines.append("\n\n")
|
|
280
|
+
|
|
281
|
+
lines.append(
|
|
282
|
+
" Open each URL in your browser to authorize the account.\n",
|
|
283
|
+
style="#8b949e italic",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
console.print(
|
|
287
|
+
Panel(
|
|
288
|
+
lines,
|
|
289
|
+
title="[bold #58a6ff]🔗 Login URLs[/bold #58a6ff]",
|
|
290
|
+
border_style="#58a6ff",
|
|
291
|
+
padding=(1, 2),
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ── Status ─────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
def display_status(accounts: list[dict]) -> None:
|
|
299
|
+
"""Show per-account authentication status.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
accounts: List of dicts with ``name`` and ``authenticated`` keys.
|
|
303
|
+
"""
|
|
304
|
+
table = Table(
|
|
305
|
+
show_header=True,
|
|
306
|
+
header_style="bold #58a6ff",
|
|
307
|
+
border_style="#30363d",
|
|
308
|
+
pad_edge=True,
|
|
309
|
+
)
|
|
310
|
+
table.add_column("Account", style="bold #e6edf3")
|
|
311
|
+
table.add_column("Status", justify="center")
|
|
312
|
+
|
|
313
|
+
for acct in accounts:
|
|
314
|
+
name = acct.get("name", "Unknown")
|
|
315
|
+
authed = acct.get("authenticated", False)
|
|
316
|
+
if authed:
|
|
317
|
+
status = Text("✓ Authenticated", style="bold #3fb950")
|
|
318
|
+
else:
|
|
319
|
+
status = Text("✗ Not Authenticated", style="bold #f85149")
|
|
320
|
+
table.add_row(name, status)
|
|
321
|
+
|
|
322
|
+
console.print(
|
|
323
|
+
Panel(
|
|
324
|
+
table,
|
|
325
|
+
title="[bold #58a6ff]📊 Account Status[/bold #58a6ff]",
|
|
326
|
+
border_style="#58a6ff",
|
|
327
|
+
padding=(1, 1),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ── Simple messages ────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
def display_error(message: str) -> None:
|
|
335
|
+
"""Print a styled error message."""
|
|
336
|
+
console.print(
|
|
337
|
+
Panel(
|
|
338
|
+
Text(f" {message}", style="bold #f85149"),
|
|
339
|
+
title="[bold #f85149]✗ Error[/bold #f85149]",
|
|
340
|
+
border_style="#f85149",
|
|
341
|
+
padding=(0, 1),
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def display_success(message: str) -> None:
|
|
347
|
+
"""Print a styled success message."""
|
|
348
|
+
console.print(
|
|
349
|
+
Panel(
|
|
350
|
+
Text(f" {message}", style="bold #3fb950"),
|
|
351
|
+
title="[bold #3fb950]✓ Success[/bold #3fb950]",
|
|
352
|
+
border_style="#3fb950",
|
|
353
|
+
padding=(0, 1),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def display_info(message: str) -> None:
|
|
359
|
+
"""Print a styled informational message."""
|
|
360
|
+
console.print(
|
|
361
|
+
Panel(
|
|
362
|
+
Text(f" {message}", style="bold #58a6ff"),
|
|
363
|
+
title="[bold #58a6ff]ℹ Info[/bold #58a6ff]",
|
|
364
|
+
border_style="#58a6ff",
|
|
365
|
+
padding=(0, 1),
|
|
366
|
+
)
|
|
367
|
+
)
|