kitecli 0.4.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {kitecli-0.4.0 → kitecli-0.5.1}/PKG-INFO +2 -1
  2. {kitecli-0.4.0 → kitecli-0.5.1}/cli/api_client.py +4 -0
  3. {kitecli-0.4.0 → kitecli-0.5.1}/cli/base_manager.py +4 -0
  4. {kitecli-0.4.0 → kitecli-0.5.1}/cli/greeks.py +212 -38
  5. kitecli-0.5.1/cli/highlighter.py +433 -0
  6. {kitecli-0.4.0 → kitecli-0.5.1}/cli/kite_manager.py +140 -40
  7. {kitecli-0.4.0 → kitecli-0.5.1}/cli/live_session.py +1362 -64
  8. kitecli-0.5.1/cli/parser.py +571 -0
  9. {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/PKG-INFO +2 -1
  10. {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/SOURCES.txt +3 -0
  11. {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/requires.txt +1 -0
  12. {kitecli-0.4.0 → kitecli-0.5.1}/pyproject.toml +2 -1
  13. kitecli-0.5.1/tests/test_highlighter.py +126 -0
  14. kitecli-0.5.1/tests/test_option_chain.py +574 -0
  15. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_ui.py +120 -5
  16. kitecli-0.4.0/cli/parser.py +0 -511
  17. {kitecli-0.4.0 → kitecli-0.5.1}/README.md +0 -0
  18. {kitecli-0.4.0 → kitecli-0.5.1}/cli/__init__.py +0 -0
  19. {kitecli-0.4.0 → kitecli-0.5.1}/cli/advisor.py +0 -0
  20. {kitecli-0.4.0 → kitecli-0.5.1}/cli/charges.py +0 -0
  21. {kitecli-0.4.0 → kitecli-0.5.1}/cli/config.py +0 -0
  22. {kitecli-0.4.0 → kitecli-0.5.1}/cli/display.py +0 -0
  23. {kitecli-0.4.0 → kitecli-0.5.1}/cli/executor.py +0 -0
  24. {kitecli-0.4.0 → kitecli-0.5.1}/cli/indicators.py +0 -0
  25. {kitecli-0.4.0 → kitecli-0.5.1}/cli/kotak_manager.py +0 -0
  26. {kitecli-0.4.0 → kitecli-0.5.1}/cli/main.py +0 -0
  27. {kitecli-0.4.0 → kitecli-0.5.1}/cli/nli.py +0 -0
  28. {kitecli-0.4.0 → kitecli-0.5.1}/cli/recorder.py +0 -0
  29. {kitecli-0.4.0 → kitecli-0.5.1}/cli/telegram_bot.py +0 -0
  30. {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/dependency_links.txt +0 -0
  31. {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/entry_points.txt +0 -0
  32. {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/top_level.txt +0 -0
  33. {kitecli-0.4.0 → kitecli-0.5.1}/setup.cfg +0 -0
  34. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_command_matrix.py +0 -0
  35. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_conditional_orders.py +0 -0
  36. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_multi_broker.py +0 -0
  37. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_nli.py +0 -0
  38. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_parser.py +0 -0
  39. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_scheduled_orders.py +0 -0
  40. {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_telegram.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kitecli
3
- Version: 0.4.0
3
+ Version: 0.5.1
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"
@@ -645,6 +645,10 @@ class KCLIClient:
645
645
  """Fetch NFO tradingsymbol → lot_size map (one-shot, cache at startup)."""
646
646
  return _kite_manager.get_nfo_lot_sizes()
647
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
+
648
652
  def get_market_indices(self) -> dict:
649
653
  """Fetch live Nifty, Sensex, and India VIX."""
650
654
  return _kite_manager.get_market_indices()
@@ -144,6 +144,10 @@ class BaseBrokerManager(ABC):
144
144
  """
145
145
  return {}
146
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
+
147
151
  def get_market_indices(self) -> dict[str, Any]:
148
152
  """Return Nifty / Sensex / India VIX snapshot. Default: not supported."""
149
153
  return {"status": "error", "message": "not supported by this broker"}
@@ -200,25 +200,38 @@ def format_strike_display(info: Dict[str, Any]) -> str:
200
200
 
201
201
 
202
202
  def implied_volatility(
203
- spot_price: float, strike_price: float, T: float, option_price: float, is_call: bool, r: float = 0.07
203
+ spot_price: float,
204
+ strike_price: float,
205
+ T: float,
206
+ option_price: float,
207
+ is_call: bool,
208
+ r: float = 0.0,
209
+ forward_price: float | None = None,
204
210
  ) -> float | None:
205
- """Solve for Implied Volatility (IV %) from option price using Newton-Raphson."""
206
- if option_price <= 0 or T <= 0:
211
+ """Solve for Implied Volatility (IV %) from option price using Black-76 Newton-Raphson."""
212
+ F = forward_price if (forward_price and forward_price > 0) else spot_price
213
+ if option_price <= 0 or T <= 0 or F <= 0 or strike_price <= 0:
214
+ return None
215
+
216
+ intrinsic = max(0.0, (F - strike_price) if is_call else (strike_price - F))
217
+ if option_price < intrinsic - 1e-4:
207
218
  return None
208
- sigma = 0.25
219
+
220
+ sigma = 0.20
221
+ df = math.exp(-r * T)
209
222
  for _ in range(100):
210
- d1 = (math.log(spot_price / strike_price) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
223
+ d1 = (math.log(F / strike_price) + (0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
211
224
  d2 = d1 - sigma * math.sqrt(T)
212
225
  if is_call:
213
- price = spot_price * _norm_cdf(d1) - strike_price * math.exp(-r * T) * _norm_cdf(d2)
226
+ price = df * (F * _norm_cdf(d1) - strike_price * _norm_cdf(d2))
214
227
  else:
215
- price = strike_price * math.exp(-r * T) * _norm_cdf(-d2) - spot_price * _norm_cdf(-d1)
228
+ price = df * (strike_price * _norm_cdf(-d2) - F * _norm_cdf(-d1))
216
229
 
217
230
  diff = price - option_price
218
231
  if abs(diff) < 1e-4:
219
232
  return sigma * 100.0
220
233
 
221
- vega = spot_price * math.sqrt(T) * _norm_pdf(d1)
234
+ vega = df * F * math.sqrt(T) * _norm_pdf(d1)
222
235
  if vega < 1e-8:
223
236
  break
224
237
  sigma -= diff / vega
@@ -251,39 +264,48 @@ def calculate_greeks(
251
264
  expiry_date: str | datetime | date,
252
265
  option_type: str,
253
266
  vix_pct: float = 15.0,
254
- risk_free_rate: float = 0.07,
267
+ risk_free_rate: float = 0.0,
255
268
  option_price: float | None = None,
269
+ forward_price: float | None = None,
256
270
  ) -> Dict[str, Optional[float]]:
257
- """Calculate Black-Scholes Option Greeks.
271
+ """Calculate Black-76 Option Greeks using Forward / Synthetic Future price.
258
272
 
259
273
  Args:
260
- spot_price: Current spot price of underlying index/stock (e.g. 24500.0)
261
- strike_price: Strike price of option (e.g. 24500.0)
274
+ spot_price: Current spot price of underlying index/stock (e.g. 24055.80)
275
+ strike_price: Strike price of option (e.g. 24000.0)
262
276
  expiry_date: Expiry date string (YYYY-MM-DD), date, or datetime object
263
277
  option_type: 'CE' / 'CALL' or 'PE' / 'PUT'
264
278
  vix_pct: Baseline Volatility percentage (e.g. 15.2 for 15.2% India VIX)
265
- risk_free_rate: Annual risk-free interest rate (default 7% = 0.07)
279
+ risk_free_rate: Annual risk-free interest rate (default 0.0 for Forward pricing)
266
280
  option_price: Optional option market price (LTP) to solve for exact IV.
281
+ forward_price: Optional Synthetic Forward price (F) from Put-Call Parity.
267
282
 
268
283
  Returns:
269
284
  Dict containing delta, gamma, theta, vega, and iv.
270
285
  """
271
- if spot_price <= 0 or strike_price <= 0:
286
+ F = forward_price if (forward_price and forward_price > 0) else spot_price
287
+ if F <= 0 or strike_price <= 0:
272
288
  return {"delta": None, "gamma": None, "theta": None, "vega": None, "iv": None}
273
289
 
274
- # Calculate time to expiry in years
290
+ # Calculate time to expiry in years targeting 15:30 IST market close
291
+ from datetime import time as _time
275
292
  if isinstance(expiry_date, str):
276
293
  try:
277
- exp_dt = datetime.strptime(expiry_date, "%Y-%m-%d")
294
+ exp_d = datetime.strptime(expiry_date[:10], "%Y-%m-%d").date()
278
295
  except ValueError:
279
- exp_dt = datetime.now() + timedelta(days=7)
280
- elif isinstance(expiry_date, date) and not isinstance(expiry_date, datetime):
281
- exp_dt = datetime.combine(expiry_date, datetime.min.time())
296
+ exp_d = date.today() + timedelta(days=7)
297
+ elif isinstance(expiry_date, datetime):
298
+ exp_d = expiry_date.date()
299
+ elif isinstance(expiry_date, date):
300
+ exp_d = expiry_date
282
301
  else:
283
- exp_dt = expiry_date
302
+ exp_d = date.today() + timedelta(days=7)
284
303
 
304
+ exp_dt = datetime.combine(exp_d, _time(15, 30))
285
305
  now = datetime.now()
286
- days_to_exp = max((exp_dt - now).total_seconds() / 86400.0, 0.25)
306
+ seconds = max((exp_dt - now).total_seconds(), 0.0)
307
+ # Minimum 0.05 days (~1.2 hours) for numerical stability on expiry day close
308
+ days_to_exp = max(seconds / 86400.0, 0.05)
287
309
  T = days_to_exp / 365.0
288
310
 
289
311
  opt_type_upper = option_type.upper()
@@ -291,44 +313,196 @@ def calculate_greeks(
291
313
 
292
314
  eff_vix = vix_pct
293
315
  if option_price and option_price > 0:
294
- solved_iv = implied_volatility(spot_price, strike_price, T, option_price, is_call, risk_free_rate)
316
+ solved_iv = implied_volatility(
317
+ spot_price=spot_price,
318
+ strike_price=strike_price,
319
+ T=T,
320
+ option_price=option_price,
321
+ is_call=is_call,
322
+ r=risk_free_rate,
323
+ forward_price=F,
324
+ )
295
325
  if not solved_iv or solved_iv <= 0:
296
326
  return {"delta": None, "gamma": None, "theta": None, "vega": None, "iv": None}
297
327
  eff_vix = solved_iv
298
328
 
299
329
  sigma = max(eff_vix / 100.0, 0.01)
330
+ df = math.exp(-risk_free_rate * T)
300
331
 
301
- # Black-Scholes d1 and d2 calculations
302
- d1 = (math.log(spot_price / strike_price) + (risk_free_rate + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
332
+ # Black-76 d1 and d2 calculations
333
+ d1 = (math.log(F / strike_price) + 0.5 * sigma**2 * T) / (sigma * math.sqrt(T))
303
334
  d2 = d1 - sigma * math.sqrt(T)
304
335
 
305
- opt_type_upper = option_type.upper()
306
- is_call = "CE" in opt_type_upper or "CALL" in opt_type_upper
307
-
308
336
  # 1. Delta
309
337
  if is_call:
310
- delta = _norm_cdf(d1)
338
+ delta = df * _norm_cdf(d1)
311
339
  else:
312
- delta = _norm_cdf(d1) - 1.0
340
+ delta = df * (_norm_cdf(d1) - 1.0)
313
341
 
314
342
  # 2. Gamma
315
- gamma = _norm_pdf(d1) / (spot_price * sigma * math.sqrt(T))
343
+ gamma = (df * _norm_pdf(d1)) / (F * sigma * math.sqrt(T))
316
344
 
317
345
  # 3. Theta (daily decay)
318
- term1 = -(spot_price * _norm_pdf(d1) * sigma) / (2.0 * math.sqrt(T))
319
- if is_call:
320
- term2 = -risk_free_rate * strike_price * math.exp(-risk_free_rate * T) * _norm_cdf(d2)
321
- else:
322
- term2 = risk_free_rate * strike_price * math.exp(-risk_free_rate * T) * _norm_cdf(-d2)
323
- theta = (term1 + term2) / 365.0
346
+ theta = -(F * _norm_pdf(d1) * sigma) / (2.0 * math.sqrt(T) * 365.0)
324
347
 
325
348
  # 4. Vega (per 1% change in volatility)
326
- vega = (spot_price * math.sqrt(T) * _norm_pdf(d1)) / 100.0
349
+ vega = (df * F * math.sqrt(T) * _norm_pdf(d1)) / 100.0
327
350
 
328
351
  return {
329
- "delta": round(delta, 4),
352
+ "delta": round(delta, 2),
330
353
  "gamma": round(gamma, 6),
331
354
  "theta": round(theta, 2),
332
355
  "vega": round(vega, 2),
333
356
  "iv": round(eff_vix, 2),
334
357
  }
358
+
359
+
360
+ def enrich_option_chain_greeks(
361
+ strikes: list[dict],
362
+ spot_price: float,
363
+ expiry_date: str | datetime | date,
364
+ vix_pct: float = 15.0,
365
+ risk_free_rate: float = 0.0,
366
+ ) -> Dict[str, Any]:
367
+ """Calculate Greeks for every strike in the option chain using Black-76 forward model.
368
+
369
+ Derives the Synthetic Forward Price (F) from Put-Call parity across liquid pairs.
370
+
371
+ Returns a summary dict with:
372
+ - 'atm_strike': float
373
+ - 'atm_straddle': float | None
374
+ - 'pcr': float
375
+ - 'max_pain': float
376
+ - 'total_ce_oi': int
377
+ - 'total_pe_oi': int
378
+ - 'forward_price': float
379
+ """
380
+ if not strikes or spot_price <= 0:
381
+ return {
382
+ "atm_strike": 0.0,
383
+ "atm_straddle": None,
384
+ "pcr": 0.0,
385
+ "max_pain": 0.0,
386
+ "total_ce_oi": 0,
387
+ "total_pe_oi": 0,
388
+ "forward_price": 0.0,
389
+ }
390
+
391
+ # 1. Derive Forward Price (F) from liquid strike pairs via Put-Call Parity:
392
+ # F_i = K_i + CE_ltp_i - PE_ltp_i
393
+ fwd_estimates = []
394
+ for s in strikes:
395
+ k = float(s.get("strike", 0.0))
396
+ ce = s.get("ce_ltp")
397
+ pe = s.get("pe_ltp")
398
+ if ce is not None and pe is not None and ce > 0 and pe > 0:
399
+ if spot_price <= 0 or abs(k - spot_price) / spot_price <= 0.05:
400
+ fwd_estimates.append(k + ce - pe)
401
+
402
+ if fwd_estimates:
403
+ fwd_estimates.sort()
404
+ mid = len(fwd_estimates) // 2
405
+ forward_price = (
406
+ fwd_estimates[mid]
407
+ if len(fwd_estimates) % 2 == 1
408
+ else (fwd_estimates[mid - 1] + fwd_estimates[mid]) / 2.0
409
+ )
410
+ else:
411
+ forward_price = spot_price
412
+
413
+ total_ce_oi = 0
414
+ total_pe_oi = 0
415
+ atm_strike = None
416
+ min_fwd_diff = float("inf")
417
+ atm_straddle = None
418
+
419
+ for s in strikes:
420
+ strike = float(s.get("strike", 0.0))
421
+ ce_ltp = s.get("ce_ltp")
422
+ pe_ltp = s.get("pe_ltp")
423
+ ce_oi = int(s.get("ce_oi") or 0)
424
+ pe_oi = int(s.get("pe_oi") or 0)
425
+ total_ce_oi += ce_oi
426
+ total_pe_oi += pe_oi
427
+
428
+ # Find ATM strike (closest strike to Forward Price)
429
+ diff = abs(strike - forward_price)
430
+ if diff < min_fwd_diff:
431
+ min_fwd_diff = diff
432
+ atm_strike = strike
433
+ if ce_ltp is not None and pe_ltp is not None:
434
+ atm_straddle = round(ce_ltp + pe_ltp, 2)
435
+ else:
436
+ atm_straddle = None
437
+
438
+ # Calculate CE Greeks
439
+ if ce_ltp is not None and ce_ltp > 0:
440
+ ce_g = calculate_greeks(
441
+ spot_price=spot_price,
442
+ strike_price=strike,
443
+ expiry_date=expiry_date,
444
+ option_type="CE",
445
+ vix_pct=vix_pct,
446
+ risk_free_rate=risk_free_rate,
447
+ option_price=ce_ltp,
448
+ forward_price=forward_price,
449
+ )
450
+ s["ce_delta"] = ce_g.get("delta")
451
+ s["ce_iv"] = ce_g.get("iv")
452
+ s["ce_theta"] = ce_g.get("theta")
453
+ else:
454
+ s["ce_delta"] = None
455
+ s["ce_iv"] = None
456
+ s["ce_theta"] = None
457
+
458
+ # Calculate PE Greeks
459
+ if pe_ltp is not None and pe_ltp > 0:
460
+ pe_g = calculate_greeks(
461
+ spot_price=spot_price,
462
+ strike_price=strike,
463
+ expiry_date=expiry_date,
464
+ option_type="PE",
465
+ vix_pct=vix_pct,
466
+ risk_free_rate=risk_free_rate,
467
+ option_price=pe_ltp,
468
+ forward_price=forward_price,
469
+ )
470
+ s["pe_delta"] = pe_g.get("delta")
471
+ s["pe_iv"] = pe_g.get("iv")
472
+ s["pe_theta"] = pe_g.get("theta")
473
+ else:
474
+ s["pe_delta"] = None
475
+ s["pe_iv"] = None
476
+ s["pe_theta"] = None
477
+
478
+ # Calculate PCR
479
+ pcr = round(total_pe_oi / total_ce_oi, 2) if total_ce_oi > 0 else 0.0
480
+
481
+ # Calculate Max Pain
482
+ max_pain = atm_strike or 0.0
483
+ min_total_payout = float("inf")
484
+ for target in strikes:
485
+ k_target = float(target.get("strike", 0.0))
486
+ total_payout = 0.0
487
+ for s in strikes:
488
+ k = float(s.get("strike", 0.0))
489
+ ce_oi = int(s.get("ce_oi") or 0)
490
+ pe_oi = int(s.get("pe_oi") or 0)
491
+ if k_target > k:
492
+ total_payout += (k_target - k) * ce_oi
493
+ elif k_target < k:
494
+ total_payout += (k - k_target) * pe_oi
495
+ if total_payout < min_total_payout:
496
+ min_total_payout = total_payout
497
+ max_pain = k_target
498
+
499
+ return {
500
+ "atm_strike": atm_strike,
501
+ "atm_straddle": atm_straddle,
502
+ "pcr": pcr,
503
+ "max_pain": max_pain,
504
+ "total_ce_oi": total_ce_oi,
505
+ "total_pe_oi": total_pe_oi,
506
+ "forward_price": round(forward_price, 2),
507
+ }
508
+