kitecli 0.3.3__tar.gz → 0.5.0__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.3.3 → kitecli-0.5.0}/PKG-INFO +2 -1
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/api_client.py +71 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/base_manager.py +8 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/display.py +331 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/executor.py +16 -1
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/greeks.py +212 -38
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/kite_manager.py +243 -40
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/kotak_manager.py +123 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/live_session.py +2210 -175
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/main.py +60 -0
- kitecli-0.5.0/cli/parser.py +571 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/recorder.py +169 -2
- {kitecli-0.3.3 → kitecli-0.5.0}/kitecli.egg-info/PKG-INFO +2 -1
- {kitecli-0.3.3 → kitecli-0.5.0}/kitecli.egg-info/SOURCES.txt +3 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/kitecli.egg-info/requires.txt +1 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/pyproject.toml +2 -1
- kitecli-0.5.0/tests/test_command_matrix.py +401 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/tests/test_multi_broker.py +126 -0
- kitecli-0.5.0/tests/test_option_chain.py +574 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/tests/test_parser.py +109 -0
- kitecli-0.5.0/tests/test_scheduled_orders.py +394 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/tests/test_ui.py +188 -5
- kitecli-0.3.3/cli/parser.py +0 -317
- {kitecli-0.3.3 → kitecli-0.5.0}/README.md +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/__init__.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/advisor.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/charges.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/config.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/indicators.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/nli.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/cli/telegram_bot.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/kitecli.egg-info/dependency_links.txt +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/kitecli.egg-info/entry_points.txt +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/kitecli.egg-info/top_level.txt +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/setup.cfg +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/tests/test_conditional_orders.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/tests/test_nli.py +0 -0
- {kitecli-0.3.3 → kitecli-0.5.0}/tests/test_telegram.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kitecli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: KiteCLI — Multi-account, multi-broker trading positions viewer (Zerodha + Kotak Neo)
|
|
5
5
|
Author: KiteCLI Team
|
|
6
6
|
License: MIT
|
|
@@ -14,6 +14,7 @@ Requires-Dist: kiteconnect>=5.0.0
|
|
|
14
14
|
Requires-Dist: pyotp>=2.9.0
|
|
15
15
|
Requires-Dist: requests>=2.31.0
|
|
16
16
|
Requires-Dist: yfinance>=0.2.0
|
|
17
|
+
Requires-Dist: lark>=1.1.0
|
|
17
18
|
Provides-Extra: server
|
|
18
19
|
Requires-Dist: fastapi>=0.110.0; extra == "server"
|
|
19
20
|
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "server"
|
|
@@ -537,6 +537,73 @@ class KCLIClient:
|
|
|
537
537
|
|
|
538
538
|
return {"accounts": results}
|
|
539
539
|
|
|
540
|
+
def get_holdings(self, api_keys: list[str] | None = None) -> dict:
|
|
541
|
+
"""Fetch stock/equity holdings for specified accounts in parallel."""
|
|
542
|
+
keys = api_keys or [a.get("api_key") for a in self._accounts if a.get("api_key")]
|
|
543
|
+
|
|
544
|
+
def fetch_one(api_key):
|
|
545
|
+
mgr = _manager_for(api_key)
|
|
546
|
+
info = mgr.get_account_info(api_key)
|
|
547
|
+
broker = getattr(mgr, "broker_name", "zerodha")
|
|
548
|
+
if not info.get("authenticated"):
|
|
549
|
+
return {
|
|
550
|
+
"name": info.get("name", api_key),
|
|
551
|
+
"api_key": api_key,
|
|
552
|
+
"broker": broker,
|
|
553
|
+
"holdings": [],
|
|
554
|
+
"total_investment": 0.0,
|
|
555
|
+
"total_current_value": 0.0,
|
|
556
|
+
"total_pnl": 0.0,
|
|
557
|
+
"total_day_change": 0.0,
|
|
558
|
+
"status": "unauthenticated",
|
|
559
|
+
}
|
|
560
|
+
try:
|
|
561
|
+
holdings = mgr.get_holdings(api_key)
|
|
562
|
+
tot_inv = sum(h.get("investment_value", 0.0) for h in holdings)
|
|
563
|
+
tot_cur = sum(h.get("current_value", 0.0) for h in holdings)
|
|
564
|
+
tot_pnl = tot_cur - tot_inv
|
|
565
|
+
tot_day = sum(h.get("day_change", 0.0) for h in holdings)
|
|
566
|
+
return {
|
|
567
|
+
"name": info.get("name", api_key),
|
|
568
|
+
"api_key": api_key,
|
|
569
|
+
"broker": broker,
|
|
570
|
+
"holdings": holdings,
|
|
571
|
+
"total_investment": tot_inv,
|
|
572
|
+
"total_current_value": tot_cur,
|
|
573
|
+
"total_pnl": tot_pnl,
|
|
574
|
+
"total_day_change": tot_day,
|
|
575
|
+
"status": "success",
|
|
576
|
+
}
|
|
577
|
+
except Exception as exc:
|
|
578
|
+
return {
|
|
579
|
+
"name": info.get("name", api_key),
|
|
580
|
+
"api_key": api_key,
|
|
581
|
+
"broker": broker,
|
|
582
|
+
"holdings": [],
|
|
583
|
+
"total_investment": 0.0,
|
|
584
|
+
"total_current_value": 0.0,
|
|
585
|
+
"total_pnl": 0.0,
|
|
586
|
+
"total_day_change": 0.0,
|
|
587
|
+
"status": f"error: {exc}",
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
|
|
591
|
+
results = list(executor.map(fetch_one, keys))
|
|
592
|
+
|
|
593
|
+
grand_inv = sum(a.get("total_investment", 0.0) for a in results)
|
|
594
|
+
grand_cur = sum(a.get("total_current_value", 0.0) for a in results)
|
|
595
|
+
grand_pnl = grand_cur - grand_inv
|
|
596
|
+
grand_day = sum(a.get("total_day_change", 0.0) for a in results)
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
"accounts": results,
|
|
600
|
+
"grand_total_investment": grand_inv,
|
|
601
|
+
"grand_total_current_value": grand_cur,
|
|
602
|
+
"grand_total_pnl": grand_pnl,
|
|
603
|
+
"grand_total_day_change": grand_day,
|
|
604
|
+
"status": "success",
|
|
605
|
+
}
|
|
606
|
+
|
|
540
607
|
def modify_order(
|
|
541
608
|
self,
|
|
542
609
|
api_key: str,
|
|
@@ -578,6 +645,10 @@ class KCLIClient:
|
|
|
578
645
|
"""Fetch NFO tradingsymbol → lot_size map (one-shot, cache at startup)."""
|
|
579
646
|
return _kite_manager.get_nfo_lot_sizes()
|
|
580
647
|
|
|
648
|
+
def get_nfo_symbol_token_map(self) -> dict:
|
|
649
|
+
"""Fetch NFO tradingsymbol → instrument_token map."""
|
|
650
|
+
return _kite_manager.get_nfo_symbol_token_map()
|
|
651
|
+
|
|
581
652
|
def get_market_indices(self) -> dict:
|
|
582
653
|
"""Fetch live Nifty, Sensex, and India VIX."""
|
|
583
654
|
return _kite_manager.get_market_indices()
|
|
@@ -81,6 +81,10 @@ class BaseBrokerManager(ABC):
|
|
|
81
81
|
def get_margins(self, account_key: str) -> dict[str, Any]:
|
|
82
82
|
"""Return {"net": float|None, "cash": float|None, "collateral": float|None}."""
|
|
83
83
|
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def get_holdings(self, account_key: str) -> list[dict[str, Any]]:
|
|
86
|
+
"""Fetch equity/stock holdings using the normalised schema."""
|
|
87
|
+
|
|
84
88
|
# ── order management ──────────────────────────────────────────────────────
|
|
85
89
|
|
|
86
90
|
@abstractmethod
|
|
@@ -140,6 +144,10 @@ class BaseBrokerManager(ABC):
|
|
|
140
144
|
"""
|
|
141
145
|
return {}
|
|
142
146
|
|
|
147
|
+
def get_nfo_symbol_token_map(self) -> dict[str, int]:
|
|
148
|
+
"""Return tradingsymbol → instrument_token map for NFO instruments. Default: empty dict."""
|
|
149
|
+
return {}
|
|
150
|
+
|
|
143
151
|
def get_market_indices(self) -> dict[str, Any]:
|
|
144
152
|
"""Return Nifty / Sensex / India VIX snapshot. Default: not supported."""
|
|
145
153
|
return {"status": "error", "message": "not supported by this broker"}
|
|
@@ -404,6 +404,337 @@ def display_status(accounts: list[dict]) -> None:
|
|
|
404
404
|
)
|
|
405
405
|
|
|
406
406
|
|
|
407
|
+
# ── Holdings Display ───────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
def _clean_instrument_name(name: str) -> str:
|
|
410
|
+
"""Clean up verbose mutual fund or stock names for compact table display."""
|
|
411
|
+
import re
|
|
412
|
+
s = name.strip()
|
|
413
|
+
# Mutual fund scheme name compressions
|
|
414
|
+
s = re.sub(r"\s*-\s*DIRECT\s+PLAN\s*-\s*GROWTH(?:\s+OPTION)?", " (Dir-Gr)", s, flags=re.IGNORECASE)
|
|
415
|
+
s = re.sub(r"\s*-\s*DIRECT\s+PLAN\s*-\s*IDCW(?:\s+OPTION)?", " (Dir-IDCW)", s, flags=re.IGNORECASE)
|
|
416
|
+
s = re.sub(r"\s*-\s*DIRECT\s+PLAN", " (Dir)", s, flags=re.IGNORECASE)
|
|
417
|
+
s = re.sub(r"\s*-\s*REGULAR\s+PLAN\s*-\s*GROWTH(?:\s+OPTION)?", " (Reg-Gr)", s, flags=re.IGNORECASE)
|
|
418
|
+
s = re.sub(r"\s*-\s*REGULAR\s+PLAN", " (Reg)", s, flags=re.IGNORECASE)
|
|
419
|
+
s = re.sub(r"\s*-\s*GROWTH\s+PLAN", " (Gr)", s, flags=re.IGNORECASE)
|
|
420
|
+
s = re.sub(r"\s*-\s*GROWTH\s+OPTION", " (Gr)", s, flags=re.IGNORECASE)
|
|
421
|
+
s = re.sub(r"\s+FUND\s+OF\s+FUND", " FoF", s, flags=re.IGNORECASE)
|
|
422
|
+
s = re.sub(r"\s+INDEX\s+FUND", " Index", s, flags=re.IGNORECASE)
|
|
423
|
+
s = re.sub(r"\s+ETF\s+FUND\s+OF\s+FUND", " ETF FoF", s, flags=re.IGNORECASE)
|
|
424
|
+
return s.strip()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _build_holdings_summary_table(accounts_data: list[dict]) -> Table:
|
|
428
|
+
"""Build a summary Table showing total holdings metrics per account."""
|
|
429
|
+
table = Table(
|
|
430
|
+
show_header=True,
|
|
431
|
+
header_style="bold #58a6ff",
|
|
432
|
+
border_style="#30363d",
|
|
433
|
+
row_styles=["", "dim"],
|
|
434
|
+
pad_edge=True,
|
|
435
|
+
expand=False,
|
|
436
|
+
)
|
|
437
|
+
table.add_column("Account", style="bold #e6edf3", no_wrap=True)
|
|
438
|
+
table.add_column("Invested", justify="right", no_wrap=True)
|
|
439
|
+
table.add_column("Current Value", justify="right", no_wrap=True)
|
|
440
|
+
table.add_column("Total P&L", justify="right", no_wrap=True)
|
|
441
|
+
table.add_column("P&L %", justify="right", no_wrap=True)
|
|
442
|
+
table.add_column("Day P&L", justify="right", no_wrap=True)
|
|
443
|
+
|
|
444
|
+
for acct in accounts_data:
|
|
445
|
+
name = acct.get("name", "Unknown")
|
|
446
|
+
status = acct.get("status", "success")
|
|
447
|
+
holdings = acct.get("holdings", [])
|
|
448
|
+
|
|
449
|
+
if status != "success" and not holdings:
|
|
450
|
+
table.add_row(
|
|
451
|
+
name,
|
|
452
|
+
"₹0.00",
|
|
453
|
+
"₹0.00",
|
|
454
|
+
Text(f"✗ {status}", style="#f85149"),
|
|
455
|
+
"0.00%",
|
|
456
|
+
"₹0.00",
|
|
457
|
+
)
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
tot_inv = float(acct.get("total_investment", 0.0))
|
|
461
|
+
tot_cur = float(acct.get("total_current_value", 0.0))
|
|
462
|
+
tot_pnl = tot_cur - tot_inv
|
|
463
|
+
tot_day = float(acct.get("total_day_change", 0.0))
|
|
464
|
+
pnl_pct = (tot_pnl / tot_inv * 100.0) if tot_inv > 0 else 0.0
|
|
465
|
+
|
|
466
|
+
pnl_color = _pnl_style(tot_pnl)
|
|
467
|
+
day_color = _pnl_style(tot_day)
|
|
468
|
+
|
|
469
|
+
table.add_row(
|
|
470
|
+
name,
|
|
471
|
+
_format_currency(tot_inv),
|
|
472
|
+
_format_currency(tot_cur),
|
|
473
|
+
Text(_format_currency(tot_pnl), style=f"bold {pnl_color}"),
|
|
474
|
+
Text(_format_pnl_pct(pnl_pct), style=f"bold {pnl_color}"),
|
|
475
|
+
Text(_format_currency(tot_day), style=day_color),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Total Row at the bottom of the table
|
|
479
|
+
if accounts_data:
|
|
480
|
+
grand_inv = sum(float(a.get("total_investment", 0.0)) for a in accounts_data)
|
|
481
|
+
grand_cur = sum(float(a.get("total_current_value", 0.0)) for a in accounts_data)
|
|
482
|
+
grand_pnl = grand_cur - grand_inv
|
|
483
|
+
grand_day = sum(float(a.get("total_day_change", 0.0)) for a in accounts_data)
|
|
484
|
+
grand_pnl_pct = (grand_pnl / grand_inv * 100.0) if grand_inv > 0 else 0.0
|
|
485
|
+
|
|
486
|
+
pnl_color = _pnl_style(grand_pnl)
|
|
487
|
+
day_color = _pnl_style(grand_day)
|
|
488
|
+
|
|
489
|
+
table.add_section()
|
|
490
|
+
table.add_row(
|
|
491
|
+
Text("Total", style="bold #e6edf3"),
|
|
492
|
+
Text(_format_currency(grand_inv), style="bold #e6edf3"),
|
|
493
|
+
Text(_format_currency(grand_cur), style="bold #e6edf3"),
|
|
494
|
+
Text(_format_currency(grand_pnl), style=f"bold {pnl_color}"),
|
|
495
|
+
Text(_format_pnl_pct(grand_pnl_pct), style=f"bold {pnl_color}"),
|
|
496
|
+
Text(_format_currency(grand_day), style=f"bold {day_color}"),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return table
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _build_account_deep_holdings_table(holdings: list[dict]) -> Table:
|
|
503
|
+
"""Build a detailed constituents Table for an account's holdings."""
|
|
504
|
+
table = Table(
|
|
505
|
+
show_header=True,
|
|
506
|
+
header_style="bold #58a6ff",
|
|
507
|
+
border_style="#30363d",
|
|
508
|
+
row_styles=["", "dim"],
|
|
509
|
+
pad_edge=True,
|
|
510
|
+
expand=False,
|
|
511
|
+
)
|
|
512
|
+
table.add_column("Symbol", style="bold #e6edf3", max_width=32, no_wrap=True, overflow="ellipsis")
|
|
513
|
+
table.add_column("Qty", justify="right", style="#79c0ff", no_wrap=True)
|
|
514
|
+
table.add_column("Avg", justify="right", no_wrap=True)
|
|
515
|
+
table.add_column("LTP", justify="right", no_wrap=True)
|
|
516
|
+
table.add_column("Invested", justify="right", no_wrap=True)
|
|
517
|
+
table.add_column("Current", justify="right", no_wrap=True)
|
|
518
|
+
table.add_column("Total P&L", justify="right", no_wrap=True)
|
|
519
|
+
table.add_column("P&L %", justify="right", no_wrap=True)
|
|
520
|
+
|
|
521
|
+
# Sort holdings alphabetically by tradingsymbol
|
|
522
|
+
sorted_holdings = sorted(holdings, key=lambda h: str(h.get("tradingsymbol", "")))
|
|
523
|
+
|
|
524
|
+
for h in sorted_holdings:
|
|
525
|
+
raw_sym = str(h.get("tradingsymbol", "?"))
|
|
526
|
+
sym = _clean_instrument_name(raw_sym)
|
|
527
|
+
is_mf = (h.get("exchange") == "MF" or h.get("instrument_type") == "MF")
|
|
528
|
+
sym_style = "italic #79c0ff" if is_mf else "bold #e6edf3"
|
|
529
|
+
|
|
530
|
+
qty_val = float(h.get("quantity", 0))
|
|
531
|
+
qty_fmt = f"{int(qty_val)}" if qty_val.is_integer() else f"{qty_val:,.2f}"
|
|
532
|
+
|
|
533
|
+
avg = float(h.get("average_price", 0.0))
|
|
534
|
+
ltp = float(h.get("last_price", avg))
|
|
535
|
+
inv = float(h.get("investment_value", qty_val * avg))
|
|
536
|
+
cur = float(h.get("current_value", qty_val * ltp))
|
|
537
|
+
pnl = cur - inv
|
|
538
|
+
pnl_pct = (pnl / inv * 100.0) if inv > 0 else 0.0
|
|
539
|
+
|
|
540
|
+
pnl_color = _pnl_style(pnl)
|
|
541
|
+
|
|
542
|
+
table.add_row(
|
|
543
|
+
Text(sym, style=sym_style),
|
|
544
|
+
qty_fmt,
|
|
545
|
+
f"{avg:,.2f}",
|
|
546
|
+
f"{ltp:,.2f}",
|
|
547
|
+
_format_currency(inv),
|
|
548
|
+
_format_currency(cur),
|
|
549
|
+
Text(_format_currency(pnl), style=f"bold {pnl_color}"),
|
|
550
|
+
Text(_format_pnl_pct(pnl_pct), style=f"bold {pnl_color}"),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Total Row for multiple constituents
|
|
554
|
+
if len(sorted_holdings) > 1:
|
|
555
|
+
tot_qty = sum(float(h.get("quantity", 0)) for h in sorted_holdings)
|
|
556
|
+
tot_qty_str = f"{int(tot_qty)}" if tot_qty.is_integer() else f"{tot_qty:,.2f}"
|
|
557
|
+
tot_inv = sum(float(h.get("investment_value", float(h.get("quantity", 0)) * float(h.get("average_price", 0.0)))) for h in sorted_holdings)
|
|
558
|
+
tot_cur = sum(float(h.get("current_value", float(h.get("quantity", 0)) * float(h.get("last_price", 0.0)))) for h in sorted_holdings)
|
|
559
|
+
tot_pnl = tot_cur - tot_inv
|
|
560
|
+
tot_pnl_pct = (tot_pnl / tot_inv * 100.0) if tot_inv > 0 else 0.0
|
|
561
|
+
|
|
562
|
+
pnl_color = _pnl_style(tot_pnl)
|
|
563
|
+
|
|
564
|
+
table.add_section()
|
|
565
|
+
table.add_row(
|
|
566
|
+
Text("Total", style="bold #e6edf3"),
|
|
567
|
+
Text(tot_qty_str, style="bold #79c0ff"),
|
|
568
|
+
Text("-", style="#8b949e", justify="right"),
|
|
569
|
+
Text("-", style="#8b949e", justify="right"),
|
|
570
|
+
Text(_format_currency(tot_inv), style="bold #e6edf3"),
|
|
571
|
+
Text(_format_currency(tot_cur), style="bold #e6edf3"),
|
|
572
|
+
Text(_format_currency(tot_pnl), style=f"bold {pnl_color}"),
|
|
573
|
+
Text(_format_pnl_pct(tot_pnl_pct), style=f"bold {pnl_color}"),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
return table
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def display_holdings(holdings_data: dict, deep: bool = False, target_account: Optional[str] = None) -> None:
|
|
580
|
+
"""Print a Rich table of stock holdings across accounts.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
holdings_data: Dict returned by client.get_holdings().
|
|
584
|
+
deep: If True, show full constituent stock breakdowns.
|
|
585
|
+
target_account: Optional account name or ID to filter single-account deep-dive.
|
|
586
|
+
"""
|
|
587
|
+
accounts = holdings_data.get("accounts", [])
|
|
588
|
+
if not accounts:
|
|
589
|
+
display_info("No holdings data available.")
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
# Filter by account if requested
|
|
593
|
+
if target_account:
|
|
594
|
+
target_norm = target_account.strip().lower()
|
|
595
|
+
if target_norm.isdigit():
|
|
596
|
+
idx = int(target_norm) - 1
|
|
597
|
+
if 0 <= idx < len(accounts):
|
|
598
|
+
accounts = [accounts[idx]]
|
|
599
|
+
else:
|
|
600
|
+
display_error(f"Invalid account index: {target_account}")
|
|
601
|
+
return
|
|
602
|
+
else:
|
|
603
|
+
accounts = [a for a in accounts if a.get("name", "").lower() == target_norm or a.get("api_key", "").lower() == target_norm]
|
|
604
|
+
if not accounts:
|
|
605
|
+
display_error(f"Account '{target_account}' not found.")
|
|
606
|
+
return
|
|
607
|
+
deep = True # Single account drill-down defaults to deep view
|
|
608
|
+
|
|
609
|
+
if not deep:
|
|
610
|
+
# Summary View
|
|
611
|
+
table = _build_holdings_summary_table(accounts)
|
|
612
|
+
console.print(table)
|
|
613
|
+
console.print()
|
|
614
|
+
else:
|
|
615
|
+
# Deep View: Per-account constituent tables
|
|
616
|
+
for acct in accounts:
|
|
617
|
+
name = acct.get("name", "Unknown")
|
|
618
|
+
broker = str(acct.get("broker", "zerodha")).title()
|
|
619
|
+
holdings = acct.get("holdings", [])
|
|
620
|
+
tot_cur = float(acct.get("total_current_value", 0.0))
|
|
621
|
+
tot_inv = float(acct.get("total_investment", 0.0))
|
|
622
|
+
tot_pnl = tot_cur - tot_inv
|
|
623
|
+
pnl_pct = (tot_pnl / tot_inv * 100.0) if tot_inv > 0 else 0.0
|
|
624
|
+
pnl_color = _pnl_style(tot_pnl)
|
|
625
|
+
|
|
626
|
+
header_text = Text.assemble(
|
|
627
|
+
(f" {name} ({broker}) ", "bold #e6edf3"),
|
|
628
|
+
(" │ ", "#8b949e"),
|
|
629
|
+
("Invested: ", "#8b949e"),
|
|
630
|
+
(f"{_format_currency(tot_inv)} ", "bold"),
|
|
631
|
+
(" │ ", "#8b949e"),
|
|
632
|
+
("Current: ", "#8b949e"),
|
|
633
|
+
(f"{_format_currency(tot_cur)} ", "bold"),
|
|
634
|
+
(" │ ", "#8b949e"),
|
|
635
|
+
("P&L: ", "bold"),
|
|
636
|
+
(f"{_format_currency(tot_pnl)} ({_format_pnl_pct(pnl_pct)})", f"bold {pnl_color}"),
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if not holdings:
|
|
640
|
+
panel = Panel(
|
|
641
|
+
Text(" No stock holdings in this account.", style="#8b949e italic"),
|
|
642
|
+
title=header_text,
|
|
643
|
+
border_style="#30363d",
|
|
644
|
+
padding=(1, 2),
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
table = _build_account_deep_holdings_table(holdings)
|
|
648
|
+
panel = Panel(
|
|
649
|
+
table,
|
|
650
|
+
title=header_text,
|
|
651
|
+
border_style=pnl_color,
|
|
652
|
+
padding=(0, 1),
|
|
653
|
+
)
|
|
654
|
+
console.print(panel)
|
|
655
|
+
console.print()
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def render_holdings_to_string(
|
|
659
|
+
holdings_data: dict,
|
|
660
|
+
width: int = 100,
|
|
661
|
+
deep: bool = False,
|
|
662
|
+
target_account: Optional[str] = None
|
|
663
|
+
) -> str:
|
|
664
|
+
"""Render holdings to an ANSI string for the live session TUI."""
|
|
665
|
+
from io import StringIO
|
|
666
|
+
from rich.console import Console
|
|
667
|
+
|
|
668
|
+
string_io = StringIO()
|
|
669
|
+
capture_console = Console(
|
|
670
|
+
file=string_io,
|
|
671
|
+
width=max(width, 100),
|
|
672
|
+
force_terminal=True,
|
|
673
|
+
color_system="truecolor",
|
|
674
|
+
)
|
|
675
|
+
accounts = holdings_data.get("accounts", [])
|
|
676
|
+
if not accounts:
|
|
677
|
+
return "No holdings data available."
|
|
678
|
+
|
|
679
|
+
if target_account:
|
|
680
|
+
target_norm = target_account.strip().lower()
|
|
681
|
+
if target_norm.isdigit():
|
|
682
|
+
idx = int(target_norm) - 1
|
|
683
|
+
if 0 <= idx < len(accounts):
|
|
684
|
+
accounts = [accounts[idx]]
|
|
685
|
+
else:
|
|
686
|
+
filtered = [a for a in accounts if a.get("name", "").lower() == target_norm or a.get("api_key", "").lower() == target_norm]
|
|
687
|
+
if filtered:
|
|
688
|
+
accounts = filtered
|
|
689
|
+
deep = True
|
|
690
|
+
|
|
691
|
+
if not deep:
|
|
692
|
+
table = _build_holdings_summary_table(accounts)
|
|
693
|
+
capture_console.print(table)
|
|
694
|
+
else:
|
|
695
|
+
for acct in accounts:
|
|
696
|
+
name = acct.get("name", "Unknown")
|
|
697
|
+
broker = str(acct.get("broker", "zerodha")).title()
|
|
698
|
+
holdings = acct.get("holdings", [])
|
|
699
|
+
tot_cur = float(acct.get("total_current_value", 0.0))
|
|
700
|
+
tot_inv = float(acct.get("total_investment", 0.0))
|
|
701
|
+
tot_pnl = tot_cur - tot_inv
|
|
702
|
+
pnl_pct = (tot_pnl / tot_inv * 100.0) if tot_inv > 0 else 0.0
|
|
703
|
+
pnl_color = _pnl_style(tot_pnl)
|
|
704
|
+
|
|
705
|
+
header_text = Text.assemble(
|
|
706
|
+
(f" {name} ({broker}) ", "bold #e6edf3"),
|
|
707
|
+
(" │ ", "#8b949e"),
|
|
708
|
+
("Invested: ", "#8b949e"),
|
|
709
|
+
(f"{_format_currency(tot_inv)} ", "bold"),
|
|
710
|
+
(" │ ", "#8b949e"),
|
|
711
|
+
("Current: ", "#8b949e"),
|
|
712
|
+
(f"{_format_currency(tot_cur)} ", "bold"),
|
|
713
|
+
(" │ ", "#8b949e"),
|
|
714
|
+
("P&L: ", "bold"),
|
|
715
|
+
(f"{_format_currency(tot_pnl)} ({_format_pnl_pct(pnl_pct)})", f"bold {pnl_color}"),
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
if not holdings:
|
|
719
|
+
panel = Panel(
|
|
720
|
+
Text(" No stock holdings in this account.", style="#8b949e italic"),
|
|
721
|
+
title=header_text,
|
|
722
|
+
border_style="#30363d",
|
|
723
|
+
padding=(0, 1),
|
|
724
|
+
)
|
|
725
|
+
else:
|
|
726
|
+
table = _build_account_deep_holdings_table(holdings)
|
|
727
|
+
panel = Panel(
|
|
728
|
+
table,
|
|
729
|
+
title=header_text,
|
|
730
|
+
border_style=pnl_color,
|
|
731
|
+
padding=(0, 1),
|
|
732
|
+
)
|
|
733
|
+
capture_console.print(panel)
|
|
734
|
+
|
|
735
|
+
return string_io.getvalue()
|
|
736
|
+
|
|
737
|
+
|
|
407
738
|
# ── Simple messages ────────────────────────────────────────────────
|
|
408
739
|
|
|
409
740
|
def display_error(message: str) -> None:
|
|
@@ -4,7 +4,8 @@ from cli.parser import (
|
|
|
4
4
|
AccountSelectCommand, PlaceOrderCommand, ExitCommand,
|
|
5
5
|
StatusCommand, PositionsCommand, OrdersCommand,
|
|
6
6
|
CancelOrderCommand, ModifyOrderCommand, MarketDepthCommand,
|
|
7
|
-
ConditionalOrderCommand, CancelConditionalOrderCommand, ConditionalOrdersViewCommand
|
|
7
|
+
ConditionalOrderCommand, CancelConditionalOrderCommand, ConditionalOrdersViewCommand,
|
|
8
|
+
CancelScheduledOrderCommand, ScheduledOrdersViewCommand
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
class ExecutionContext:
|
|
@@ -450,4 +451,18 @@ async def execute_command(cmd: Any, context: ExecutionContext) -> dict:
|
|
|
450
451
|
"message": "Conditional orders viewer is active in live TUI dashboard ('kcli live')."
|
|
451
452
|
}
|
|
452
453
|
|
|
454
|
+
# 13. Cancel Scheduled Order Command
|
|
455
|
+
elif isinstance(cmd, CancelScheduledOrderCommand):
|
|
456
|
+
return {
|
|
457
|
+
"status": "executed",
|
|
458
|
+
"message": f"Cancelled scheduled orders targeting '{cmd.target}'."
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# 14. Scheduled Orders View Command
|
|
462
|
+
elif isinstance(cmd, ScheduledOrdersViewCommand):
|
|
463
|
+
return {
|
|
464
|
+
"status": "executed",
|
|
465
|
+
"message": "Scheduled orders viewer is active in live TUI dashboard ('kcli live')."
|
|
466
|
+
}
|
|
467
|
+
|
|
453
468
|
raise TypeError(f"Execution not implemented for command type '{type(cmd)}'.")
|