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.
- {kitecli-0.4.0 → kitecli-0.5.1}/PKG-INFO +2 -1
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/api_client.py +4 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/base_manager.py +4 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/greeks.py +212 -38
- kitecli-0.5.1/cli/highlighter.py +433 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/kite_manager.py +140 -40
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/live_session.py +1362 -64
- kitecli-0.5.1/cli/parser.py +571 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/PKG-INFO +2 -1
- {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/SOURCES.txt +3 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/requires.txt +1 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/pyproject.toml +2 -1
- kitecli-0.5.1/tests/test_highlighter.py +126 -0
- kitecli-0.5.1/tests/test_option_chain.py +574 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_ui.py +120 -5
- kitecli-0.4.0/cli/parser.py +0 -511
- {kitecli-0.4.0 → kitecli-0.5.1}/README.md +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/__init__.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/advisor.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/charges.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/config.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/display.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/executor.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/indicators.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/kotak_manager.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/main.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/nli.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/recorder.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/cli/telegram_bot.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/dependency_links.txt +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/entry_points.txt +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/kitecli.egg-info/top_level.txt +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/setup.cfg +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_command_matrix.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_conditional_orders.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_multi_broker.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_nli.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_parser.py +0 -0
- {kitecli-0.4.0 → kitecli-0.5.1}/tests/test_scheduled_orders.py +0 -0
- {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.
|
|
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,
|
|
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
|
+
|