kitecli 0.4.0__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.4.0 → kitecli-0.5.0}/PKG-INFO +2 -1
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/api_client.py +4 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/base_manager.py +4 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/greeks.py +212 -38
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/kite_manager.py +140 -40
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/live_session.py +1320 -63
- kitecli-0.5.0/cli/parser.py +571 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/kitecli.egg-info/PKG-INFO +2 -1
- {kitecli-0.4.0 → kitecli-0.5.0}/kitecli.egg-info/SOURCES.txt +1 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/kitecli.egg-info/requires.txt +1 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/pyproject.toml +2 -1
- kitecli-0.5.0/tests/test_option_chain.py +574 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_ui.py +120 -5
- kitecli-0.4.0/cli/parser.py +0 -511
- {kitecli-0.4.0 → kitecli-0.5.0}/README.md +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/__init__.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/advisor.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/charges.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/config.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/display.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/executor.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/indicators.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/kotak_manager.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/main.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/nli.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/recorder.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/cli/telegram_bot.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/kitecli.egg-info/dependency_links.txt +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/kitecli.egg-info/entry_points.txt +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/kitecli.egg-info/top_level.txt +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/setup.cfg +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_command_matrix.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_conditional_orders.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_multi_broker.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_nli.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_parser.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.0}/tests/test_scheduled_orders.py +0 -0
- {kitecli-0.4.0 → 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"
|
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
219
|
+
|
|
220
|
+
sigma = 0.20
|
|
221
|
+
df = math.exp(-r * T)
|
|
209
222
|
for _ in range(100):
|
|
210
|
-
d1 = (math.log(
|
|
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 =
|
|
226
|
+
price = df * (F * _norm_cdf(d1) - strike_price * _norm_cdf(d2))
|
|
214
227
|
else:
|
|
215
|
-
price =
|
|
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 =
|
|
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.
|
|
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-
|
|
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.
|
|
261
|
-
strike_price: Strike price of option (e.g.
|
|
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
|
|
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
|
|
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
|
-
|
|
294
|
+
exp_d = datetime.strptime(expiry_date[:10], "%Y-%m-%d").date()
|
|
278
295
|
except ValueError:
|
|
279
|
-
|
|
280
|
-
elif isinstance(expiry_date,
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
302
|
-
d1 = (math.log(
|
|
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) / (
|
|
343
|
+
gamma = (df * _norm_pdf(d1)) / (F * sigma * math.sqrt(T))
|
|
316
344
|
|
|
317
345
|
# 3. Theta (daily decay)
|
|
318
|
-
|
|
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 = (
|
|
349
|
+
vega = (df * F * math.sqrt(T) * _norm_pdf(d1)) / 100.0
|
|
327
350
|
|
|
328
351
|
return {
|
|
329
|
-
"delta": round(delta,
|
|
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
|
+
|
|
@@ -68,6 +68,7 @@ class KiteAccountManager(BaseBrokerManager):
|
|
|
68
68
|
self._authenticated: dict[str, bool] = {}
|
|
69
69
|
self._proxies: dict[str, dict] = {} # per-account proxy dicts for requests.Session
|
|
70
70
|
self._nfo_lot_size_cache: dict[str, int] = {} # fetched once, reused across calls
|
|
71
|
+
self._nfo_symbol_token_cache: dict[str, int] = {} # tradingsymbol -> instrument_token map
|
|
71
72
|
|
|
72
73
|
# ── public init helpers (named with _by_key suffix to avoid ABC clash) ────
|
|
73
74
|
|
|
@@ -858,39 +859,89 @@ class KiteAccountManager(BaseBrokerManager):
|
|
|
858
859
|
"message": "No active expiry dates found.",
|
|
859
860
|
}
|
|
860
861
|
|
|
862
|
+
# Identify monthly expiries (last expiry for each year-month)
|
|
863
|
+
month_last_expiry = {}
|
|
864
|
+
for exp in all_expiries:
|
|
865
|
+
month_key = (exp.year, exp.month)
|
|
866
|
+
month_last_expiry[month_key] = exp
|
|
867
|
+
monthly_expiries = set(month_last_expiry.values())
|
|
868
|
+
sorted_monthly = sorted(monthly_expiries)
|
|
869
|
+
|
|
861
870
|
# Build available_expiries labels
|
|
862
871
|
available_expiries = []
|
|
863
872
|
for i, exp in enumerate(all_expiries):
|
|
873
|
+
is_monthly = exp in monthly_expiries
|
|
874
|
+
month_name = exp.strftime("%b")
|
|
875
|
+
|
|
876
|
+
# Weekly designation
|
|
864
877
|
if i == 0:
|
|
865
|
-
|
|
878
|
+
week_desc = "Current Week"
|
|
866
879
|
elif i == 1:
|
|
867
|
-
|
|
880
|
+
week_desc = "Next Week"
|
|
881
|
+
elif i == 2:
|
|
882
|
+
week_desc = "Far Week"
|
|
868
883
|
else:
|
|
869
|
-
|
|
870
|
-
|
|
884
|
+
week_desc = f"Week +{i}"
|
|
885
|
+
|
|
886
|
+
# Monthly designation
|
|
887
|
+
month_desc = ""
|
|
888
|
+
if is_monthly:
|
|
889
|
+
m_idx = sorted_monthly.index(exp)
|
|
890
|
+
if m_idx == 0:
|
|
891
|
+
month_desc = f"Current Month ({month_name})"
|
|
892
|
+
elif m_idx == 1:
|
|
893
|
+
month_desc = f"Next Month ({month_name})"
|
|
894
|
+
elif m_idx == 2:
|
|
895
|
+
month_desc = f"Far Month ({month_name})"
|
|
896
|
+
else:
|
|
897
|
+
month_desc = f"{month_name} Monthly"
|
|
898
|
+
|
|
899
|
+
label = f"{month_desc}" if is_monthly and i >= 2 else f"{week_desc} ({month_name} Monthly)" if is_monthly else week_desc
|
|
900
|
+
|
|
901
|
+
available_expiries.append({
|
|
902
|
+
"expiry": exp.isoformat(),
|
|
903
|
+
"week_label": label,
|
|
904
|
+
"week_desc": week_desc,
|
|
905
|
+
"month_desc": month_desc,
|
|
906
|
+
"is_monthly": is_monthly,
|
|
907
|
+
})
|
|
871
908
|
|
|
872
909
|
# Resolve target expiry
|
|
873
910
|
if expiry_date:
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
911
|
+
exp_date_lower = expiry_date.strip().lower()
|
|
912
|
+
if exp_date_lower in ("monthly", "month", "m", "current month", "current monthly"):
|
|
913
|
+
target_expiry = sorted_monthly[0] if sorted_monthly else all_expiries[0]
|
|
914
|
+
elif exp_date_lower in ("next monthly", "next month", "next_month", "month 1", "monthly 1"):
|
|
915
|
+
target_expiry = sorted_monthly[1] if len(sorted_monthly) > 1 else (sorted_monthly[0] if sorted_monthly else all_expiries[-1])
|
|
916
|
+
elif exp_date_lower in ("far monthly", "far month", "far_month", "month 2", "monthly 2"):
|
|
917
|
+
target_expiry = sorted_monthly[2] if len(sorted_monthly) > 2 else (sorted_monthly[-1] if sorted_monthly else all_expiries[-1])
|
|
918
|
+
elif exp_date_lower in ("current week", "current"):
|
|
919
|
+
target_expiry = all_expiries[0]
|
|
920
|
+
elif exp_date_lower in ("next week", "next"):
|
|
921
|
+
target_expiry = all_expiries[1] if len(all_expiries) > 1 else all_expiries[0]
|
|
922
|
+
elif exp_date_lower in ("far week", "far"):
|
|
923
|
+
target_expiry = all_expiries[2] if len(all_expiries) > 2 else all_expiries[-1]
|
|
924
|
+
else:
|
|
925
|
+
try:
|
|
926
|
+
target_expiry = datetime.date.fromisoformat(expiry_date)
|
|
927
|
+
except ValueError:
|
|
928
|
+
return {
|
|
929
|
+
"underlying": underlying,
|
|
930
|
+
"expiry": "",
|
|
931
|
+
"strikes": [],
|
|
932
|
+
"available_expiries": available_expiries,
|
|
933
|
+
"status": "error",
|
|
934
|
+
"message": f"Invalid expiry_date format '{expiry_date}'. Use 'monthly', 'next month', 'week <N>', or YYYY-MM-DD.",
|
|
935
|
+
}
|
|
936
|
+
if target_expiry not in all_expiries:
|
|
937
|
+
return {
|
|
938
|
+
"underlying": underlying,
|
|
939
|
+
"expiry": expiry_date,
|
|
940
|
+
"strikes": [],
|
|
941
|
+
"available_expiries": available_expiries,
|
|
942
|
+
"status": "error",
|
|
943
|
+
"message": f"Expiry date '{expiry_date}' not found for {underlying}.",
|
|
944
|
+
}
|
|
894
945
|
else:
|
|
895
946
|
idx = min(expiry_week, len(all_expiries) - 1)
|
|
896
947
|
target_expiry = all_expiries[idx]
|
|
@@ -917,47 +968,84 @@ class KiteAccountManager(BaseBrokerManager):
|
|
|
917
968
|
|
|
918
969
|
strikes = sorted(strike_map.values(), key=lambda x: x["strike"])
|
|
919
970
|
|
|
920
|
-
#
|
|
921
|
-
|
|
971
|
+
# Map spot underlying symbol
|
|
972
|
+
index_spot_map = {
|
|
973
|
+
"NIFTY": "NSE:NIFTY 50",
|
|
974
|
+
"BANKNIFTY": "NSE:NIFTY BANK",
|
|
975
|
+
"FINNIFTY": "NSE:NIFTY FIN SERVICE",
|
|
976
|
+
"MIDCPNIFTY": "NSE:NIFTY MID SELECT",
|
|
977
|
+
"SENSEX": "BSE:SENSEX",
|
|
978
|
+
"BANKEX": "BSE:BANKEX",
|
|
979
|
+
}
|
|
980
|
+
spot_symbol = index_spot_map.get(underlying, f"NSE:{underlying}")
|
|
981
|
+
|
|
982
|
+
# Batch-fetch Quote (LTP, OI, Volume) for all option symbols + Spot Index
|
|
983
|
+
all_symbols = [spot_symbol]
|
|
922
984
|
for s in strikes:
|
|
923
985
|
if s.get("ce_symbol"):
|
|
924
986
|
all_symbols.append(f"NFO:{s['ce_symbol']}")
|
|
925
987
|
if s.get("pe_symbol"):
|
|
926
988
|
all_symbols.append(f"NFO:{s['pe_symbol']}")
|
|
927
989
|
|
|
928
|
-
|
|
990
|
+
quote_data: dict = {}
|
|
929
991
|
if all_symbols:
|
|
930
992
|
try:
|
|
931
|
-
# kite.
|
|
993
|
+
# kite.quote() accepts up to 500 symbols at once
|
|
932
994
|
for i in range(0, len(all_symbols), 500):
|
|
933
995
|
chunk = all_symbols[i : i + 500]
|
|
934
|
-
|
|
996
|
+
quote_data.update(kite.quote(chunk))
|
|
935
997
|
except Exception as exc:
|
|
936
|
-
logger.warning("
|
|
998
|
+
logger.warning("kite.quote failed for option chain, falling back to kite.ltp: %s", exc)
|
|
999
|
+
try:
|
|
1000
|
+
for i in range(0, len(all_symbols), 500):
|
|
1001
|
+
chunk = all_symbols[i : i + 500]
|
|
1002
|
+
quote_data.update(kite.ltp(chunk))
|
|
1003
|
+
except Exception as exc2:
|
|
1004
|
+
logger.warning("kite.ltp also failed for option chain: %s", exc2)
|
|
937
1005
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1006
|
+
spot_price = 0.0
|
|
1007
|
+
if spot_symbol in quote_data:
|
|
1008
|
+
spot_price = float(quote_data[spot_symbol].get("last_price") or 0.0)
|
|
1009
|
+
|
|
1010
|
+
# Enrich each strike entry with live LTP, instrument tokens, OI, and volume
|
|
941
1011
|
for s in strikes:
|
|
942
1012
|
ce_key = f"NFO:{s.get('ce_symbol', '')}"
|
|
943
1013
|
pe_key = f"NFO:{s.get('pe_symbol', '')}"
|
|
944
|
-
if ce_key in
|
|
945
|
-
|
|
946
|
-
s["
|
|
947
|
-
|
|
948
|
-
s["
|
|
949
|
-
s["
|
|
1014
|
+
if ce_key in quote_data:
|
|
1015
|
+
q = quote_data[ce_key]
|
|
1016
|
+
s["ce_ltp"] = q.get("last_price")
|
|
1017
|
+
s["ce_token"] = q.get("instrument_token")
|
|
1018
|
+
s["ce_oi"] = q.get("oi") or 0
|
|
1019
|
+
s["ce_volume"] = q.get("volume") or 0
|
|
1020
|
+
if pe_key in quote_data:
|
|
1021
|
+
q = quote_data[pe_key]
|
|
1022
|
+
s["pe_ltp"] = q.get("last_price")
|
|
1023
|
+
s["pe_token"] = q.get("instrument_token")
|
|
1024
|
+
s["pe_oi"] = q.get("oi") or 0
|
|
1025
|
+
s["pe_volume"] = q.get("volume") or 0
|
|
1026
|
+
|
|
1027
|
+
# Enrich with Greeks (Delta, IV, Theta, ATM Straddle, PCR, Max Pain)
|
|
1028
|
+
from cli.greeks import enrich_option_chain_greeks
|
|
1029
|
+
summary = enrich_option_chain_greeks(
|
|
1030
|
+
strikes=strikes,
|
|
1031
|
+
spot_price=spot_price,
|
|
1032
|
+
expiry_date=target_expiry,
|
|
1033
|
+
)
|
|
950
1034
|
|
|
951
1035
|
logger.info(
|
|
952
|
-
"Option chain: %s, expiry=%s, strikes=%d",
|
|
1036
|
+
"Option chain: %s (Spot: %.2f), expiry=%s, strikes=%d, PCR=%.2f",
|
|
953
1037
|
underlying,
|
|
1038
|
+
spot_price,
|
|
954
1039
|
target_expiry.isoformat(),
|
|
955
1040
|
len(strikes),
|
|
1041
|
+
summary.get("pcr", 0.0),
|
|
956
1042
|
)
|
|
957
1043
|
|
|
958
1044
|
return {
|
|
959
1045
|
"underlying": underlying,
|
|
960
1046
|
"expiry": target_expiry.isoformat(),
|
|
1047
|
+
"spot_price": spot_price,
|
|
1048
|
+
"summary": summary,
|
|
961
1049
|
"strikes": strikes,
|
|
962
1050
|
"available_expiries": available_expiries,
|
|
963
1051
|
"status": "success",
|
|
@@ -1135,12 +1223,24 @@ class KiteAccountManager(BaseBrokerManager):
|
|
|
1135
1223
|
for inst in instruments
|
|
1136
1224
|
if inst.get("tradingsymbol")
|
|
1137
1225
|
}
|
|
1226
|
+
self._nfo_symbol_token_cache = {
|
|
1227
|
+
inst["tradingsymbol"]: int(inst["instrument_token"])
|
|
1228
|
+
for inst in instruments
|
|
1229
|
+
if inst.get("tradingsymbol") and inst.get("instrument_token")
|
|
1230
|
+
}
|
|
1138
1231
|
return self._nfo_lot_size_cache
|
|
1139
1232
|
except Exception as exc:
|
|
1140
1233
|
logger.warning("get_nfo_lot_sizes failed: %s", exc)
|
|
1141
1234
|
return {}
|
|
1142
1235
|
return {}
|
|
1143
1236
|
|
|
1237
|
+
def get_nfo_symbol_token_map(self) -> dict[str, int]:
|
|
1238
|
+
"""Return tradingsymbol → instrument_token map for NFO instruments."""
|
|
1239
|
+
if self._nfo_symbol_token_cache:
|
|
1240
|
+
return self._nfo_symbol_token_cache
|
|
1241
|
+
self.get_nfo_lot_sizes()
|
|
1242
|
+
return self._nfo_symbol_token_cache
|
|
1243
|
+
|
|
1144
1244
|
def get_market_indices(self) -> dict[str, Any]:
|
|
1145
1245
|
# 1. Try Kite ohlc first for last_price and yesterday's close
|
|
1146
1246
|
for api_key in self.get_all_api_keys():
|