kitecli 0.2.5__tar.gz → 0.2.9__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.5 → kitecli-0.2.9}/PKG-INFO +1 -1
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/api_client.py +4 -2
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/kite_manager.py +23 -9
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/kotak_manager.py +63 -34
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/live_session.py +234 -23
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/main.py +1 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/kitecli.egg-info/PKG-INFO +1 -1
- {kitecli-0.2.5 → kitecli-0.2.9}/pyproject.toml +1 -1
- {kitecli-0.2.5 → kitecli-0.2.9}/README.md +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/__init__.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/advisor.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/base_manager.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/config.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/display.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/executor.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/nli.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/parser.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/recorder.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/cli/telegram_bot.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/kitecli.egg-info/SOURCES.txt +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/kitecli.egg-info/dependency_links.txt +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/kitecli.egg-info/entry_points.txt +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/kitecli.egg-info/requires.txt +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/kitecli.egg-info/top_level.txt +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/setup.cfg +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/tests/test_multi_broker.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/tests/test_nli.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/tests/test_parser.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/tests/test_telegram.py +0 -0
- {kitecli-0.2.5 → kitecli-0.2.9}/tests/test_ui.py +0 -0
|
@@ -141,11 +141,13 @@ class KCLIClient:
|
|
|
141
141
|
is_valid = False
|
|
142
142
|
if mgr.is_authenticated(account_key):
|
|
143
143
|
try:
|
|
144
|
-
mgr._clients[account_key].limits()
|
|
144
|
+
resp = mgr._clients[account_key].limits()
|
|
145
|
+
from cli.kotak_manager import _check_neo_error
|
|
146
|
+
_check_neo_error(resp)
|
|
145
147
|
is_valid = True
|
|
146
148
|
except Exception as exc:
|
|
147
149
|
msg = str(exc).lower()
|
|
148
|
-
is_auth_error = any(x in msg for x in ["100008", "unauthorized", "invalid token", "session expired", "session has been closed", "session closed"])
|
|
150
|
+
is_auth_error = any(x in msg for x in ["100008", "100022", "1037", "unauthorized", "invalid token", "invalid session", "session expired", "session has been closed", "session closed"])
|
|
149
151
|
if is_auth_error:
|
|
150
152
|
logger.info("Kotak validation failed (expired/invalid) for %s: %s", name, exc)
|
|
151
153
|
mgr._authenticated[account_key] = False
|
|
@@ -1039,22 +1039,36 @@ class KiteAccountManager(BaseBrokerManager):
|
|
|
1039
1039
|
return {}
|
|
1040
1040
|
|
|
1041
1041
|
def get_market_indices(self) -> dict[str, Any]:
|
|
1042
|
-
# 1. Try Kite first
|
|
1042
|
+
# 1. Try Kite ohlc first for last_price and yesterday's close
|
|
1043
1043
|
for api_key in self.get_all_api_keys():
|
|
1044
1044
|
if self.is_authenticated(api_key):
|
|
1045
1045
|
kite = self._clients.get(api_key)
|
|
1046
1046
|
if kite:
|
|
1047
1047
|
try:
|
|
1048
|
-
data = kite.
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1048
|
+
data = kite.ohlc(["NSE:NIFTY 50", "BSE:SENSEX", "NSE:INDIA VIX"])
|
|
1049
|
+
|
|
1050
|
+
def parse_item(item_data):
|
|
1051
|
+
if not isinstance(item_data, dict):
|
|
1052
|
+
return None, None
|
|
1053
|
+
last = item_data.get("last_price")
|
|
1054
|
+
ohlc = item_data.get("ohlc", {})
|
|
1055
|
+
close = ohlc.get("close") if isinstance(ohlc, dict) else None
|
|
1056
|
+
change = (last - close) if (last is not None and close is not None) else None
|
|
1057
|
+
return last, change
|
|
1058
|
+
|
|
1059
|
+
nifty_last, nifty_change = parse_item(data.get("NSE:NIFTY 50"))
|
|
1060
|
+
sensex_last, sensex_change = parse_item(data.get("BSE:SENSEX"))
|
|
1061
|
+
vix_last, vix_change = parse_item(data.get("NSE:INDIA VIX"))
|
|
1062
|
+
|
|
1063
|
+
if nifty_last or sensex_last or vix_last:
|
|
1053
1064
|
return {
|
|
1054
1065
|
"status": "success",
|
|
1055
|
-
"nifty":
|
|
1056
|
-
"
|
|
1057
|
-
"
|
|
1066
|
+
"nifty": nifty_last,
|
|
1067
|
+
"nifty_change": nifty_change,
|
|
1068
|
+
"sensex": sensex_last,
|
|
1069
|
+
"sensex_change": sensex_change,
|
|
1070
|
+
"vix": vix_last,
|
|
1071
|
+
"vix_change": vix_change,
|
|
1058
1072
|
}
|
|
1059
1073
|
except Exception as exc:
|
|
1060
1074
|
logger.warning("Kite indices fetch failed for api_key=%s…: %s", api_key[:8], exc)
|
|
@@ -137,7 +137,9 @@ class KotakAccountManager(BaseBrokerManager):
|
|
|
137
137
|
|
|
138
138
|
def init_account(self, account_key: str, **credentials) -> str:
|
|
139
139
|
"""ABC entry point — delegates to init_account_kotak."""
|
|
140
|
-
|
|
140
|
+
creds = dict(credentials)
|
|
141
|
+
creds.pop("consumer_key", None)
|
|
142
|
+
return self.init_account_kotak(consumer_key=account_key, **creds)
|
|
141
143
|
|
|
142
144
|
def init_account_kotak(
|
|
143
145
|
self,
|
|
@@ -226,70 +228,82 @@ class KotakAccountManager(BaseBrokerManager):
|
|
|
226
228
|
if 'Content-Type' not in headers:
|
|
227
229
|
headers['Content-Type'] = 'application/json'
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
u_with_params += '?' + _urlencode(query_params)
|
|
231
|
+
def _do_req(h, max_proxy_retries: int = 2):
|
|
232
|
+
if query_params and isinstance(query_params, dict):
|
|
233
|
+
base_u = url.split('?')[0]
|
|
234
|
+
u_curr = base_u + '?' + _urlencode(query_params)
|
|
234
235
|
else:
|
|
235
|
-
|
|
236
|
-
else:
|
|
237
|
-
u_with_params = u
|
|
236
|
+
u_curr = url
|
|
238
237
|
|
|
239
|
-
def _do_req(h):
|
|
240
238
|
req_kwargs = {}
|
|
241
239
|
if _proxies:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
url_lower = u_curr.lower()
|
|
241
|
+
is_order_api = "/order/" in url_lower and any(x in url_lower for x in ["/place", "/modify", "/cancel"])
|
|
242
|
+
is_login_api = "/login/" in url_lower or "tradeapilogin" in url_lower or "tradeapivalidate" in url_lower
|
|
243
|
+
if is_order_api or is_login_api:
|
|
245
244
|
req_kwargs["proxies"] = _proxies
|
|
246
245
|
|
|
247
|
-
logger.debug("Kotak REST [%s] requesting: %s", method,
|
|
246
|
+
logger.debug("Kotak REST [%s] requesting: %s", method, u_curr)
|
|
248
247
|
logger.debug("Kotak REST headers: %s", {k: v[:15] + '...' if len(str(v)) > 15 else v for k, v in h.items()})
|
|
249
248
|
|
|
250
|
-
|
|
251
|
-
if
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
249
|
+
try:
|
|
250
|
+
if method in ['POST', 'PUT', 'PATCH', 'DELETE']:
|
|
251
|
+
if _re.search('json', h.get('Content-Type', ''), _re.IGNORECASE):
|
|
252
|
+
request_body = _json.dumps(body) if body is not None else None
|
|
253
|
+
return _req.post(url=u_curr, headers=h, data=request_body, **req_kwargs)
|
|
254
|
+
elif _re.search('x-www-form-urlencoded', h.get('Content-Type', ''), _re.IGNORECASE):
|
|
255
|
+
request_body = {"jData": _json.dumps(body)} if body is not None else {}
|
|
256
|
+
return _req.post(url=u_curr, headers=h, data=request_body, **req_kwargs)
|
|
257
|
+
elif method == 'GET':
|
|
258
|
+
return _req.get(url=u_curr, headers=h, **req_kwargs)
|
|
259
|
+
return _orig_request(method, url, query_params=query_params, headers=h, body=body)
|
|
260
|
+
except Exception as req_exc:
|
|
261
|
+
exc_str = str(req_exc).lower()
|
|
262
|
+
if max_proxy_retries > 0 and _proxies and ("proxy" in exc_str or "502" in exc_str or "tunnel" in exc_str or "connect" in exc_str):
|
|
263
|
+
import time
|
|
264
|
+
logger.warning("Kotak static proxy transient error (%s). Retrying via proxy in 1s...", req_exc)
|
|
265
|
+
time.sleep(1.0)
|
|
266
|
+
return _do_req(h, max_proxy_retries=max_proxy_retries - 1)
|
|
267
|
+
raise
|
|
260
268
|
|
|
261
269
|
resp = _do_req(headers)
|
|
262
|
-
logger.info("Kotak REST %s %s → status %s", method,
|
|
270
|
+
logger.info("Kotak REST %s %s → status %s", method, url, resp.status_code)
|
|
263
271
|
try:
|
|
264
272
|
logger.debug("Kotak REST response body: %s", resp.json())
|
|
265
273
|
except Exception:
|
|
266
274
|
logger.debug("Kotak REST response body (raw): %s", resp.text[:200])
|
|
267
275
|
|
|
268
|
-
# Check for IP mismatch error (stCode 1037)
|
|
276
|
+
# Check for IP mismatch error (stCode 1037), Session expired (stCode 100008, 100022), or HTTP 401
|
|
269
277
|
try:
|
|
270
|
-
resp_json = resp.json()
|
|
278
|
+
resp_json = resp.json() if resp.text else {}
|
|
271
279
|
if isinstance(resp_json, dict):
|
|
272
280
|
st_code = resp_json.get("stCode")
|
|
273
281
|
err_msg = str(resp_json.get("errMsg", "")).lower()
|
|
274
282
|
if (
|
|
275
|
-
|
|
276
|
-
st_code
|
|
277
|
-
"session ip" in err_msg or
|
|
278
|
-
"unauthorized" in err_msg
|
|
283
|
+
resp.status_code == 401 or
|
|
284
|
+
st_code in (1037, 100008, 100022) or
|
|
285
|
+
"session ip" in err_msg or
|
|
286
|
+
"unauthorized" in err_msg or
|
|
287
|
+
"invalid session" in err_msg or
|
|
288
|
+
"invalid token" in err_msg
|
|
279
289
|
):
|
|
280
290
|
logger.info("Kotak session invalid (stCode=%s, msg='%s') for account '%s'. Triggering auto-login...", st_code, resp_json.get("errMsg"), name)
|
|
281
291
|
if self.auto_login(consumer_key):
|
|
282
292
|
# Update headers with new credentials
|
|
293
|
+
new_sid = client.configuration.serverId or client.configuration.edit_sid
|
|
283
294
|
headers["Sid"] = client.configuration.edit_sid
|
|
284
295
|
headers["Auth"] = client.configuration.edit_token
|
|
296
|
+
if new_sid:
|
|
297
|
+
url = _re.sub(r'([?&]sId=)[^&]*', r'\g<1>' + str(new_sid), url)
|
|
298
|
+
url = _re.sub(r'([?&]sid=)[^&]*', r'\g<1>' + str(new_sid), url)
|
|
285
299
|
if isinstance(query_params, dict):
|
|
286
300
|
if "sId" in query_params:
|
|
287
|
-
query_params["sId"] =
|
|
301
|
+
query_params["sId"] = new_sid
|
|
288
302
|
if "sid" in query_params:
|
|
289
|
-
query_params["sid"] =
|
|
303
|
+
query_params["sid"] = new_sid
|
|
290
304
|
logger.info("Retrying request after successful auto-login...")
|
|
291
305
|
resp = _do_req(headers)
|
|
292
|
-
logger.info("Kotak REST retry %s %s → status %s", method,
|
|
306
|
+
logger.info("Kotak REST retry %s %s → status %s", method, url, resp.status_code)
|
|
293
307
|
try:
|
|
294
308
|
logger.debug("Kotak REST retry response body: %s", resp.json())
|
|
295
309
|
except Exception:
|
|
@@ -624,6 +638,18 @@ class KotakAccountManager(BaseBrokerManager):
|
|
|
624
638
|
except ValueError:
|
|
625
639
|
pending_quantity = 0
|
|
626
640
|
|
|
641
|
+
fld_qty_str = str(ord.get("fldQty", ord.get("filled_quantity", "0")))
|
|
642
|
+
try:
|
|
643
|
+
filled_quantity = int(fld_qty_str)
|
|
644
|
+
except ValueError:
|
|
645
|
+
filled_quantity = 0
|
|
646
|
+
|
|
647
|
+
avg_prc_str = str(ord.get("avgPrc", ord.get("average_price", "0")))
|
|
648
|
+
try:
|
|
649
|
+
average_price = float(avg_prc_str)
|
|
650
|
+
except ValueError:
|
|
651
|
+
average_price = 0.0
|
|
652
|
+
|
|
627
653
|
price_str = str(ord.get("prc", ord.get("price", "0")))
|
|
628
654
|
try:
|
|
629
655
|
price = float(price_str)
|
|
@@ -635,8 +661,10 @@ class KotakAccountManager(BaseBrokerManager):
|
|
|
635
661
|
"tradingsymbol": ord.get("trdSym", ord.get("tradingsymbol", "")),
|
|
636
662
|
"transaction_type": str(ord.get("trnsTp", ord.get("transaction_type", ""))).upper(),
|
|
637
663
|
"quantity": quantity,
|
|
664
|
+
"filled_quantity": filled_quantity,
|
|
638
665
|
"pending_quantity": pending_quantity,
|
|
639
666
|
"price": price,
|
|
667
|
+
"average_price": average_price,
|
|
640
668
|
"order_type": str(ord.get("prcTp", ord.get("order_type", "MARKET"))).upper(),
|
|
641
669
|
"status": status,
|
|
642
670
|
"product": str(ord.get("prod", ord.get("product", "NRML"))).upper(),
|
|
@@ -655,6 +683,7 @@ class KotakAccountManager(BaseBrokerManager):
|
|
|
655
683
|
|
|
656
684
|
try:
|
|
657
685
|
resp = client.limits()
|
|
686
|
+
_check_neo_error(resp)
|
|
658
687
|
if isinstance(resp, dict):
|
|
659
688
|
data = resp.get("data", resp)
|
|
660
689
|
net = data.get("Net", data.get("net"))
|
|
@@ -312,6 +312,7 @@ class KCLILiveSession:
|
|
|
312
312
|
# NSE:NIFTY 50 -> 256265, BSE:SENSEX -> 265, NSE:INDIA VIX -> 264969
|
|
313
313
|
self.index_tokens = {256265: "nifty", 265: "sensex", 264969: "vix"}
|
|
314
314
|
self.index_values = {"nifty": None, "sensex": None, "vix": None}
|
|
315
|
+
self.index_close_prices = {}
|
|
315
316
|
|
|
316
317
|
# Designated primary streaming account. Index and option-chain ticks are
|
|
317
318
|
# subscribed only on this account's ticker (chosen in
|
|
@@ -1003,6 +1004,14 @@ class KCLILiveSession:
|
|
|
1003
1004
|
" Advisor ",
|
|
1004
1005
|
make_click_handler("advisor")
|
|
1005
1006
|
))
|
|
1007
|
+
fragments.append(("", " "))
|
|
1008
|
+
|
|
1009
|
+
# Tab 5: Help
|
|
1010
|
+
fragments.append((
|
|
1011
|
+
"class:tab.active" if self.info_mode == "help" else "class:tab.inactive",
|
|
1012
|
+
" Help ",
|
|
1013
|
+
make_click_handler("help")
|
|
1014
|
+
))
|
|
1006
1015
|
|
|
1007
1016
|
return fragments
|
|
1008
1017
|
|
|
@@ -1162,6 +1171,12 @@ class KCLILiveSession:
|
|
|
1162
1171
|
async def _initial_fetch_and_connect(self) -> None:
|
|
1163
1172
|
"""Initial fetch of data and connect all WebSockets."""
|
|
1164
1173
|
self.log_message("Initializing connections and fetching data...")
|
|
1174
|
+
|
|
1175
|
+
# Ensure accounts are validated & auto-logged in
|
|
1176
|
+
try:
|
|
1177
|
+
await self._run_api_call(self.client.init_accounts, self.accounts)
|
|
1178
|
+
except Exception as e:
|
|
1179
|
+
self.log_message(f"[#ff8700]Account init warning:[/#] {e}")
|
|
1165
1180
|
|
|
1166
1181
|
# Fire off REST data fetch in the background so it doesn't block WebSockets
|
|
1167
1182
|
if hasattr(self, "app") and self.app and self.app.loop:
|
|
@@ -1188,7 +1203,7 @@ class KCLILiveSession:
|
|
|
1188
1203
|
)
|
|
1189
1204
|
|
|
1190
1205
|
# Connect WebSockets for accounts that support it (Zerodha and Kotak).
|
|
1191
|
-
for
|
|
1206
|
+
for acct in self.accounts:
|
|
1192
1207
|
api_key = acct.get("api_key")
|
|
1193
1208
|
broker = acct.get("broker", "zerodha").lower()
|
|
1194
1209
|
access_token = self.client.get_access_token(api_key)
|
|
@@ -1201,9 +1216,6 @@ class KCLILiveSession:
|
|
|
1201
1216
|
)
|
|
1202
1217
|
continue
|
|
1203
1218
|
|
|
1204
|
-
if idx > 0:
|
|
1205
|
-
await asyncio.sleep(0.2)
|
|
1206
|
-
|
|
1207
1219
|
if api_key and (access_token or broker == "kotak"):
|
|
1208
1220
|
try:
|
|
1209
1221
|
if broker == "kotak":
|
|
@@ -1318,7 +1330,9 @@ class KCLILiveSession:
|
|
|
1318
1330
|
code == 403
|
|
1319
1331
|
or "403" in reason_str
|
|
1320
1332
|
or "auth_failed" in reason_str
|
|
1321
|
-
or "token" in reason_str
|
|
1333
|
+
or "invalid token" in reason_str
|
|
1334
|
+
or "token expired" in reason_str
|
|
1335
|
+
or "unauthorized" in reason_str
|
|
1322
1336
|
)
|
|
1323
1337
|
if is_auth_failure:
|
|
1324
1338
|
self.log_message(
|
|
@@ -1367,9 +1381,22 @@ class KCLILiveSession:
|
|
|
1367
1381
|
if not token or ltp is None:
|
|
1368
1382
|
continue
|
|
1369
1383
|
|
|
1370
|
-
# Market index tick → update the header values.
|
|
1384
|
+
# Market index tick → update the header values & streaming change.
|
|
1371
1385
|
if token in self.index_tokens:
|
|
1372
|
-
self.
|
|
1386
|
+
key = self.index_tokens[token]
|
|
1387
|
+
self.index_values[key] = ltp
|
|
1388
|
+
|
|
1389
|
+
# Extract close from tick OHLC if available, or use cached REST close price
|
|
1390
|
+
tick_close = None
|
|
1391
|
+
if isinstance(tick.get("ohlc"), dict):
|
|
1392
|
+
tick_close = tick["ohlc"].get("close")
|
|
1393
|
+
if tick_close:
|
|
1394
|
+
self.index_close_prices[key] = tick_close
|
|
1395
|
+
|
|
1396
|
+
close_val = self.index_close_prices.get(key)
|
|
1397
|
+
if close_val is not None:
|
|
1398
|
+
self.index_values[f"{key}_change"] = ltp - close_val
|
|
1399
|
+
|
|
1373
1400
|
indices_updated = True
|
|
1374
1401
|
continue
|
|
1375
1402
|
|
|
@@ -1447,18 +1474,82 @@ class KCLILiveSession:
|
|
|
1447
1474
|
self.app.invalidate()
|
|
1448
1475
|
|
|
1449
1476
|
def _render_indices_html(self) -> HTML:
|
|
1450
|
-
"""Build
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
)
|
|
1477
|
+
"""Build formatted market-index header tailored to the Right Pane width."""
|
|
1478
|
+
total_cols = 100
|
|
1479
|
+
if hasattr(self, "app") and self.app and hasattr(self.app, "output") and self.app.output:
|
|
1480
|
+
try:
|
|
1481
|
+
cols = self.app.output.get_size().columns
|
|
1482
|
+
if isinstance(cols, int):
|
|
1483
|
+
total_cols = cols
|
|
1484
|
+
except Exception:
|
|
1485
|
+
pass
|
|
1486
|
+
|
|
1487
|
+
# The indices bar is rendered inside the Right Pane, so compute right pane width
|
|
1488
|
+
lw = getattr(self, "left_pane_weight", 50)
|
|
1489
|
+
rw = getattr(self, "right_pane_weight", 50)
|
|
1490
|
+
pane_cols = int(total_cols * (rw / max(1, lw + rw)))
|
|
1491
|
+
|
|
1492
|
+
def fmt_item(val, chg, is_vix: bool = False, compact: bool = False):
|
|
1493
|
+
if val is None:
|
|
1494
|
+
return "<style fg='#ffffff'>N/A</style>"
|
|
1495
|
+
val_str = f"{val:,.2f}"
|
|
1496
|
+
if chg is None:
|
|
1497
|
+
return f"<style fg='#ffffff'>{val_str}</style>"
|
|
1498
|
+
|
|
1499
|
+
# For VIX: negative change (volatility drop) is green, positive (volatility spike) is red
|
|
1500
|
+
# For NIFTY/SENSEX: positive is green, negative is red
|
|
1501
|
+
if is_vix:
|
|
1502
|
+
color = "#00ff00" if chg <= 0 else "#ff5f5f"
|
|
1503
|
+
else:
|
|
1504
|
+
color = "#00ff00" if chg >= 0 else "#ff5f5f"
|
|
1505
|
+
|
|
1506
|
+
sign = "+" if chg >= 0 else ""
|
|
1507
|
+
if compact:
|
|
1508
|
+
# In tight right-pane spaces, round large changes to integers e.g. (+80) instead of (+80.50)
|
|
1509
|
+
chg_str = f"{sign}{int(round(chg)):,d}" if not is_vix else f"{sign}{chg:.2f}"
|
|
1510
|
+
else:
|
|
1511
|
+
chg_str = f"{sign}{chg:,.2f}"
|
|
1512
|
+
return f"<style fg='{color}'>{val_str} ({chg_str})</style>"
|
|
1513
|
+
|
|
1514
|
+
if pane_cols >= 75:
|
|
1515
|
+
# Full width: NIFTY 50: 24,234.85 (-99.80) │ SENSEX: 77,708.24 (-441.41) │ IV: 13.45 (+0.25)
|
|
1516
|
+
nifty_html = fmt_item(self.index_values.get("nifty"), self.index_values.get("nifty_change"))
|
|
1517
|
+
sensex_html = fmt_item(self.index_values.get("sensex"), self.index_values.get("sensex_change"))
|
|
1518
|
+
vix_html = fmt_item(self.index_values.get("vix"), self.index_values.get("vix_change"), is_vix=True)
|
|
1519
|
+
return HTML(
|
|
1520
|
+
f" <ansicyan><b>NIFTY 50:</b></ansicyan> {nifty_html} "
|
|
1521
|
+
f"│ <ansiyellow><b>SENSEX:</b></ansiyellow> {sensex_html} "
|
|
1522
|
+
f"│ <ansired><b>IV:</b></ansired> {vix_html}"
|
|
1523
|
+
)
|
|
1524
|
+
elif pane_cols >= 55:
|
|
1525
|
+
# Medium width: NIFTY: 24,234.85 (-100) │ SENSEX: 77,708.24 (-441) │ IV: 13.45 (+0.25)
|
|
1526
|
+
nifty_html = fmt_item(self.index_values.get("nifty"), self.index_values.get("nifty_change"), compact=True)
|
|
1527
|
+
sensex_html = fmt_item(self.index_values.get("sensex"), self.index_values.get("sensex_change"), compact=True)
|
|
1528
|
+
vix_html = fmt_item(self.index_values.get("vix"), self.index_values.get("vix_change"), is_vix=True, compact=True)
|
|
1529
|
+
return HTML(
|
|
1530
|
+
f"<ansicyan><b>NIFTY:</b></ansicyan> {nifty_html} "
|
|
1531
|
+
f"│ <ansiyellow><b>SENSEX:</b></ansiyellow> {sensex_html} "
|
|
1532
|
+
f"│ <ansired><b>IV:</b></ansired> {vix_html}"
|
|
1533
|
+
)
|
|
1534
|
+
else:
|
|
1535
|
+
# Ultra compact for narrow right pane (< 55 cols): N: 24,234.85 │ S: 77,708.24 │ IV: 13.45
|
|
1536
|
+
def fmt_val_only(val, chg, is_vix=False):
|
|
1537
|
+
if val is None:
|
|
1538
|
+
return "<style fg='#ffffff'>N/A</style>"
|
|
1539
|
+
val_str = f"{val:,.2f}"
|
|
1540
|
+
if chg is None:
|
|
1541
|
+
return f"<style fg='#ffffff'>{val_str}</style>"
|
|
1542
|
+
color = ("#00ff00" if chg <= 0 else "#ff5f5f") if is_vix else ("#00ff00" if chg >= 0 else "#ff5f5f")
|
|
1543
|
+
return f"<style fg='{color}'>{val_str}</style>"
|
|
1544
|
+
|
|
1545
|
+
nifty_html = fmt_val_only(self.index_values.get("nifty"), self.index_values.get("nifty_change"))
|
|
1546
|
+
sensex_html = fmt_val_only(self.index_values.get("sensex"), self.index_values.get("sensex_change"))
|
|
1547
|
+
vix_html = fmt_val_only(self.index_values.get("vix"), self.index_values.get("vix_change"), is_vix=True)
|
|
1548
|
+
return HTML(
|
|
1549
|
+
f"<ansicyan><b>N:</b></ansicyan> {nifty_html} "
|
|
1550
|
+
f"│ <ansiyellow><b>S:</b></ansiyellow> {sensex_html} "
|
|
1551
|
+
f"│ <ansired><b>IV:</b></ansired> {vix_html}"
|
|
1552
|
+
)
|
|
1462
1553
|
|
|
1463
1554
|
def _get_active_ticker(self):
|
|
1464
1555
|
"""Get the primary ticker if it is connected, else fallback to any connected ticker."""
|
|
@@ -1719,9 +1810,20 @@ class KCLILiveSession:
|
|
|
1719
1810
|
if self._ws_should_log("rest_err:indices", min_interval=30.0):
|
|
1720
1811
|
self.log_message(f"[#ff8700]Indices fetch failed:[/#] {self._clean_error(str(indices_resp))}")
|
|
1721
1812
|
elif isinstance(indices_resp, dict) and indices_resp.get("status") == "success":
|
|
1722
|
-
self.index_values["nifty"]
|
|
1723
|
-
self.index_values["
|
|
1724
|
-
self.index_values["
|
|
1813
|
+
self.index_values["nifty"] = indices_resp.get("nifty")
|
|
1814
|
+
self.index_values["nifty_change"] = indices_resp.get("nifty_change")
|
|
1815
|
+
self.index_values["sensex"] = indices_resp.get("sensex")
|
|
1816
|
+
self.index_values["sensex_change"] = indices_resp.get("sensex_change")
|
|
1817
|
+
self.index_values["vix"] = indices_resp.get("vix")
|
|
1818
|
+
self.index_values["vix_change"] = indices_resp.get("vix_change")
|
|
1819
|
+
|
|
1820
|
+
# Cache close prices (last - change) so live WebSocket ticks can dynamically recalculate change
|
|
1821
|
+
for key in ["nifty", "sensex", "vix"]:
|
|
1822
|
+
last_val = self.index_values.get(key)
|
|
1823
|
+
chg_val = self.index_values.get(f"{key}_change")
|
|
1824
|
+
if last_val is not None and chg_val is not None:
|
|
1825
|
+
self.index_close_prices[key] = last_val - chg_val
|
|
1826
|
+
|
|
1725
1827
|
self.market_indices_control.text = self._render_indices_html()
|
|
1726
1828
|
elif isinstance(indices_resp, dict):
|
|
1727
1829
|
errors_this_cycle += 1
|
|
@@ -2320,6 +2422,44 @@ class KCLILiveSession:
|
|
|
2320
2422
|
)
|
|
2321
2423
|
return
|
|
2322
2424
|
|
|
2425
|
+
# Direct commands to switch info pane mode
|
|
2426
|
+
cmd_norm = cmd.strip().lower()
|
|
2427
|
+
if cmd_norm in ("pending", "p", "1", "/1", "/pending"):
|
|
2428
|
+
self.info_mode = "orders_pending"
|
|
2429
|
+
self._update_info_buffer()
|
|
2430
|
+
if hasattr(self, "app") and self.app:
|
|
2431
|
+
self.app.invalidate()
|
|
2432
|
+
self.log_message("Switched info pane to [bold]Pending Orders[/bold].")
|
|
2433
|
+
return
|
|
2434
|
+
elif cmd_norm in ("executed", "e", "ex", "2", "/2", "/executed"):
|
|
2435
|
+
self.info_mode = "orders_executed"
|
|
2436
|
+
self._update_info_buffer()
|
|
2437
|
+
if hasattr(self, "app") and self.app:
|
|
2438
|
+
self.app.invalidate()
|
|
2439
|
+
self.log_message("Switched info pane to [bold]Executed Orders[/bold].")
|
|
2440
|
+
return
|
|
2441
|
+
elif cmd_norm in ("oc", "optionchain", "chain", "3", "/3", "/oc"):
|
|
2442
|
+
self.info_mode = "oc"
|
|
2443
|
+
self._update_info_buffer()
|
|
2444
|
+
if hasattr(self, "app") and self.app:
|
|
2445
|
+
self.app.invalidate()
|
|
2446
|
+
self.log_message("Switched info pane to [bold]Option Chain[/bold].")
|
|
2447
|
+
return
|
|
2448
|
+
elif cmd_norm in ("advisor", "adv", "ai", "4", "/4", "/advisor"):
|
|
2449
|
+
self.info_mode = "advisor"
|
|
2450
|
+
self._update_info_buffer()
|
|
2451
|
+
if hasattr(self, "app") and self.app:
|
|
2452
|
+
self.app.invalidate()
|
|
2453
|
+
self.log_message("Switched info pane to [bold]AI Advisor[/bold].")
|
|
2454
|
+
return
|
|
2455
|
+
elif cmd_norm in ("help", "h", "5", "/5", "/help"):
|
|
2456
|
+
self.info_mode = "help"
|
|
2457
|
+
self._update_info_buffer()
|
|
2458
|
+
if hasattr(self, "app") and self.app:
|
|
2459
|
+
self.app.invalidate()
|
|
2460
|
+
self.log_message("Switched info pane to [bold]Help & Shortcuts[/bold].")
|
|
2461
|
+
return
|
|
2462
|
+
|
|
2323
2463
|
# Attempt parsing via the unified parser
|
|
2324
2464
|
try:
|
|
2325
2465
|
parsed_cmds = parse_command_line(cmd)
|
|
@@ -3826,6 +3966,59 @@ class KCLILiveSession:
|
|
|
3826
3966
|
lines.append("No pending orders across all accounts.")
|
|
3827
3967
|
return "\n".join(lines)
|
|
3828
3968
|
|
|
3969
|
+
def _build_help_text(self) -> str:
|
|
3970
|
+
"""Return formatted keyboard shortcuts & command cheat sheet."""
|
|
3971
|
+
return """=== KiteCLI Keyboard & Command Cheat Sheet ===
|
|
3972
|
+
|
|
3973
|
+
1. TAB SWITCHING (RIGHT PANE)
|
|
3974
|
+
Type at prompt:
|
|
3975
|
+
p or pending or 1 -> Switch to Pending Orders tab
|
|
3976
|
+
e or executed or 2 -> Switch to Executed Orders tab
|
|
3977
|
+
oc or chain or 3 -> Switch to Option Chain tab
|
|
3978
|
+
adv or advisor or 4-> Switch to AI Advisor tab
|
|
3979
|
+
h or help or 5 -> Switch to this Help tab
|
|
3980
|
+
|
|
3981
|
+
Universal Hotkeys:
|
|
3982
|
+
Ctrl + P -> Pending Orders
|
|
3983
|
+
Ctrl + E -> Executed Orders
|
|
3984
|
+
Ctrl + O -> Option Chain
|
|
3985
|
+
Ctrl + G -> AI Advisor
|
|
3986
|
+
F5 / Esc+5 -> Help
|
|
3987
|
+
|
|
3988
|
+
Function Keys / Alt:
|
|
3989
|
+
F1 / F2 / F3 / F4 / F5
|
|
3990
|
+
Option+1 / 2 / 3 / 4 / 5 (or Esc then 1..5)
|
|
3991
|
+
Mouse Click: Click directly on any tab title in header bar
|
|
3992
|
+
|
|
3993
|
+
2. ORDER PLACEMENT & TRADING COMMANDS
|
|
3994
|
+
Buy / Sell Orders:
|
|
3995
|
+
b <symbol> <qty> [price] -> Buy limit/market (e.g. b NIFTY26JUL22500PE 100 1.40)
|
|
3996
|
+
s <symbol> <qty> [price] -> Sell limit/market (e.g. s BANKNIFTY26JUL50000CE 15@2.50)
|
|
3997
|
+
b 1 100 -> Buy 100 qty of active position #1
|
|
3998
|
+
s 2 -> Sell/exit all qty of active position #2
|
|
3999
|
+
|
|
4000
|
+
Exiting Positions:
|
|
4001
|
+
exit <symbol|id> [price] -> Exit position by symbol or table ID (e.g. exit 1, exit 2 1.50)
|
|
4002
|
+
exit all [price] -> Exit all active positions (e.g. exit all)
|
|
4003
|
+
|
|
4004
|
+
Modifying & Cancelling:
|
|
4005
|
+
modify <order_id> [qty] [price] -> Modify open order
|
|
4006
|
+
cancel <order_id> -> Cancel pending order
|
|
4007
|
+
cancel all -> Cancel all open/pending orders
|
|
4008
|
+
|
|
4009
|
+
3. SELECTION & SELECTION CLEARING
|
|
4010
|
+
Selection:
|
|
4011
|
+
Click on symbol/account row -> Target specific position/account
|
|
4012
|
+
Esc -> Clear active selection / reset account filter
|
|
4013
|
+
|
|
4014
|
+
4. GLOBAL NAVIGATION & CONTROL
|
|
4015
|
+
Tab -> Cycle focus (Input box -> Positions -> Right Pane)
|
|
4016
|
+
Ctrl + R -> Search command history
|
|
4017
|
+
Ctrl + Left -> Narrow left pane split width
|
|
4018
|
+
Ctrl + Right -> Widen left pane split width
|
|
4019
|
+
Ctrl + C -> Exit KiteCLI
|
|
4020
|
+
"""
|
|
4021
|
+
|
|
3829
4022
|
def _update_info_buffer(self, reset_scroll: bool = True) -> None:
|
|
3830
4023
|
"""Push current info_mode text into the info_buffer.
|
|
3831
4024
|
|
|
@@ -3846,6 +4039,8 @@ class KCLILiveSession:
|
|
|
3846
4039
|
except Exception as exc:
|
|
3847
4040
|
self._last_advisor_text = f"=== Tuesday Expiry Advisor ===\n\nFailed to plan: {exc}"
|
|
3848
4041
|
text = self._last_advisor_text
|
|
4042
|
+
elif self.info_mode == "help":
|
|
4043
|
+
text = self._build_help_text()
|
|
3849
4044
|
else:
|
|
3850
4045
|
text = self._last_oc_text
|
|
3851
4046
|
if reset_scroll:
|
|
@@ -4217,8 +4412,10 @@ class KCLILiveSession:
|
|
|
4217
4412
|
nxt = self.input_field.control
|
|
4218
4413
|
event.app.layout.focus(nxt)
|
|
4219
4414
|
|
|
4220
|
-
# Ctrl+1/2/3/4
|
|
4415
|
+
# Ctrl+1/2/3/4, Ctrl+P/E/O/G, Alt/Esc+1/2/3/4, F1-F4 — switch info pane content
|
|
4221
4416
|
@kb.add("c-1")
|
|
4417
|
+
@kb.add("c-p")
|
|
4418
|
+
@kb.add("escape", "1")
|
|
4222
4419
|
@kb.add("f1")
|
|
4223
4420
|
def _c1(event):
|
|
4224
4421
|
self.info_mode = "orders_pending"
|
|
@@ -4226,6 +4423,8 @@ class KCLILiveSession:
|
|
|
4226
4423
|
event.app.invalidate()
|
|
4227
4424
|
|
|
4228
4425
|
@kb.add("c-2")
|
|
4426
|
+
@kb.add("c-e")
|
|
4427
|
+
@kb.add("escape", "2")
|
|
4229
4428
|
@kb.add("f2")
|
|
4230
4429
|
def _c2(event):
|
|
4231
4430
|
self.info_mode = "orders_executed"
|
|
@@ -4233,6 +4432,8 @@ class KCLILiveSession:
|
|
|
4233
4432
|
event.app.invalidate()
|
|
4234
4433
|
|
|
4235
4434
|
@kb.add("c-3")
|
|
4435
|
+
@kb.add("c-o")
|
|
4436
|
+
@kb.add("escape", "3")
|
|
4236
4437
|
@kb.add("f3")
|
|
4237
4438
|
def _c3(event):
|
|
4238
4439
|
self.info_mode = "oc"
|
|
@@ -4240,12 +4441,22 @@ class KCLILiveSession:
|
|
|
4240
4441
|
event.app.invalidate()
|
|
4241
4442
|
|
|
4242
4443
|
@kb.add("c-4")
|
|
4444
|
+
@kb.add("c-g")
|
|
4445
|
+
@kb.add("escape", "4")
|
|
4243
4446
|
@kb.add("f4")
|
|
4244
4447
|
def _c4(event):
|
|
4245
4448
|
self.info_mode = "advisor"
|
|
4246
4449
|
self._update_info_buffer()
|
|
4247
4450
|
event.app.invalidate()
|
|
4248
4451
|
|
|
4452
|
+
@kb.add("c-5")
|
|
4453
|
+
@kb.add("escape", "5")
|
|
4454
|
+
@kb.add("f5")
|
|
4455
|
+
def _c5(event):
|
|
4456
|
+
self.info_mode = "help"
|
|
4457
|
+
self._update_info_buffer()
|
|
4458
|
+
event.app.invalidate()
|
|
4459
|
+
|
|
4249
4460
|
# Ctrl+Left/Right — adjust left/right pane split
|
|
4250
4461
|
@kb.add("c-left")
|
|
4251
4462
|
def _narrow_left(event):
|
|
@@ -206,6 +206,7 @@ def live() -> None:
|
|
|
206
206
|
display_error("No accounts configured. Edit your config file to add accounts.")
|
|
207
207
|
raise typer.Exit(code=1)
|
|
208
208
|
client = _build_client(config)
|
|
209
|
+
client.init_accounts(accounts)
|
|
209
210
|
|
|
210
211
|
# Launch interactive session
|
|
211
212
|
session = KCLILiveSession(client, accounts)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kitecli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "KiteCLI — Multi-account, multi-broker trading positions viewer (Zerodha + Kotak Neo)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|