kitecli 0.1.0__py3-none-any.whl
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.
- cli/__init__.py +11 -0
- cli/advisor.py +283 -0
- cli/api_client.py +421 -0
- cli/config.py +81 -0
- cli/display.py +395 -0
- cli/kite_manager.py +1042 -0
- cli/live_session.py +3841 -0
- cli/main.py +351 -0
- cli/nli.py +140 -0
- cli/recorder.py +321 -0
- cli/telegram_bot.py +1074 -0
- kitecli-0.1.0.dist-info/METADATA +259 -0
- kitecli-0.1.0.dist-info/RECORD +16 -0
- kitecli-0.1.0.dist-info/WHEEL +5 -0
- kitecli-0.1.0.dist-info/entry_points.txt +2 -0
- kitecli-0.1.0.dist-info/top_level.txt +1 -0
cli/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# KiteCLI - Kite Connect CLI
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import urllib3.util.connection as connection
|
|
5
|
+
|
|
6
|
+
# Force IPv4 resolution globally to prevent macOS IPv6 lookup timeouts/delays
|
|
7
|
+
# and resolve "IP not allowed" errors where Zerodha registers static IPv4 addresses.
|
|
8
|
+
def allowed_gai_family():
|
|
9
|
+
return socket.AF_INET
|
|
10
|
+
|
|
11
|
+
connection.allowed_gai_family = allowed_gai_family
|
cli/advisor.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
# Cache for NFO instruments to avoid repeated heavy API calls
|
|
9
|
+
_nifty_options_cache = None
|
|
10
|
+
_nifty_options_cache_time = None
|
|
11
|
+
|
|
12
|
+
def get_nifty_options(client, api_key: str) -> List[Dict[str, Any]]:
|
|
13
|
+
"""Fetch NFO NIFTY options and cache them for 1 hour."""
|
|
14
|
+
global _nifty_options_cache, _nifty_options_cache_time
|
|
15
|
+
now = time.time()
|
|
16
|
+
if _nifty_options_cache is not None and _nifty_options_cache_time is not None:
|
|
17
|
+
if now - _nifty_options_cache_time < 3600:
|
|
18
|
+
return _nifty_options_cache
|
|
19
|
+
|
|
20
|
+
# Public NFO instruments API is faster and bypasses account proxy routing
|
|
21
|
+
import requests
|
|
22
|
+
import csv
|
|
23
|
+
import io
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
logger.info("Fetching NFO instruments directly from Zerodha public API...")
|
|
27
|
+
url = "https://api.kite.trade/instruments/NFO"
|
|
28
|
+
# Explicitly bypass proxies to avoid Cloudflare reputational blocks
|
|
29
|
+
resp = requests.get(url, proxies={"http": None, "https": None}, timeout=15)
|
|
30
|
+
if resp.status_code == 200:
|
|
31
|
+
f = io.StringIO(resp.text)
|
|
32
|
+
reader = csv.DictReader(f)
|
|
33
|
+
options = []
|
|
34
|
+
for row in reader:
|
|
35
|
+
if row.get("name") == "NIFTY" and row.get("instrument_type") in ("CE", "PE"):
|
|
36
|
+
try:
|
|
37
|
+
exp_date = datetime.datetime.strptime(row["expiry"], "%Y-%m-%d").date()
|
|
38
|
+
except Exception:
|
|
39
|
+
exp_date = row["expiry"]
|
|
40
|
+
options.append({
|
|
41
|
+
"tradingsymbol": row["tradingsymbol"],
|
|
42
|
+
"name": row["name"],
|
|
43
|
+
"expiry": exp_date,
|
|
44
|
+
"strike": float(row["strike"]) if row.get("strike") else 0.0,
|
|
45
|
+
"instrument_type": row["instrument_type"],
|
|
46
|
+
"lot_size": int(row["lot_size"]) if row.get("lot_size") else 50
|
|
47
|
+
})
|
|
48
|
+
_nifty_options_cache = options
|
|
49
|
+
_nifty_options_cache_time = now
|
|
50
|
+
return options
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
logger.error("Failed to fetch NFO instruments directly: %s. Falling back to client session...", exc)
|
|
53
|
+
|
|
54
|
+
# Fallback to standard client API call if public direct fetch fails
|
|
55
|
+
from cli.api_client import _manager
|
|
56
|
+
kite = _manager._clients.get(api_key)
|
|
57
|
+
if not kite:
|
|
58
|
+
if _nifty_options_cache is not None:
|
|
59
|
+
return _nifty_options_cache
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
instruments = kite.instruments("NFO")
|
|
64
|
+
options = [
|
|
65
|
+
inst for inst in instruments
|
|
66
|
+
if inst.get("name", "").upper() == "NIFTY"
|
|
67
|
+
and inst.get("instrument_type") in ("CE", "PE")
|
|
68
|
+
]
|
|
69
|
+
_nifty_options_cache = options
|
|
70
|
+
_nifty_options_cache_time = now
|
|
71
|
+
return options
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logger.error("Kite fallback instruments fetch failed: %s", exc)
|
|
74
|
+
if _nifty_options_cache is not None:
|
|
75
|
+
return _nifty_options_cache
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
def find_option_symbols(options: List[Dict[str, Any]], expiry: datetime.date, strike: float):
|
|
79
|
+
"""Find CE and PE symbols for a given expiry and strike."""
|
|
80
|
+
ce_symbol = None
|
|
81
|
+
pe_symbol = None
|
|
82
|
+
for inst in options:
|
|
83
|
+
if inst.get("expiry") == expiry and abs(float(inst.get("strike", 0)) - strike) < 0.1:
|
|
84
|
+
inst_type = inst.get("instrument_type")
|
|
85
|
+
if inst_type == "CE":
|
|
86
|
+
ce_symbol = inst.get("tradingsymbol")
|
|
87
|
+
elif inst_type == "PE":
|
|
88
|
+
pe_symbol = inst.get("tradingsymbol")
|
|
89
|
+
return ce_symbol, pe_symbol
|
|
90
|
+
|
|
91
|
+
def generate_tuesday_plan(
|
|
92
|
+
client,
|
|
93
|
+
accounts_positions: List[Dict[str, Any]],
|
|
94
|
+
margins_by_api_key: Dict[str, Dict[str, Any]],
|
|
95
|
+
api_key_to_user_id: Dict[str, str],
|
|
96
|
+
nifty_spot: float | None = None
|
|
97
|
+
) -> Dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
Generate Tuesday strangle advisor plan based on the 50/50 margin split rule.
|
|
100
|
+
"""
|
|
101
|
+
plan = {
|
|
102
|
+
"status": "success",
|
|
103
|
+
"nifty_spot": nifty_spot,
|
|
104
|
+
"expiries": {},
|
|
105
|
+
"accounts": []
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if not accounts_positions:
|
|
109
|
+
return {"status": "error", "message": "No account positions data available."}
|
|
110
|
+
|
|
111
|
+
# Use first authenticated account's api_key to fetch option instruments
|
|
112
|
+
ref_api_key = None
|
|
113
|
+
for acct in accounts_positions:
|
|
114
|
+
if acct.get("status") == "success" and acct.get("api_key"):
|
|
115
|
+
ref_api_key = acct.get("api_key")
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
if not ref_api_key:
|
|
119
|
+
return {"status": "error", "message": "No authenticated accounts available to fetch option database."}
|
|
120
|
+
|
|
121
|
+
options = get_nifty_options(client, ref_api_key)
|
|
122
|
+
if not options:
|
|
123
|
+
return {"status": "error", "message": "Failed to fetch NFO options database."}
|
|
124
|
+
|
|
125
|
+
# Find expiries (E0, E1, E2)
|
|
126
|
+
today = datetime.date.today()
|
|
127
|
+
all_expiries = sorted(
|
|
128
|
+
set(
|
|
129
|
+
inst["expiry"] for inst in options
|
|
130
|
+
if isinstance(inst.get("expiry"), datetime.date) and inst["expiry"] >= today
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if len(all_expiries) < 3:
|
|
135
|
+
return {"status": "error", "message": "Not enough NIFTY expiries found in database (need at least 3)."}
|
|
136
|
+
|
|
137
|
+
E0 = all_expiries[0]
|
|
138
|
+
E1 = all_expiries[1]
|
|
139
|
+
E2 = all_expiries[2]
|
|
140
|
+
|
|
141
|
+
plan["expiries"] = {
|
|
142
|
+
"E0": E0.isoformat(),
|
|
143
|
+
"E1": E1.isoformat(),
|
|
144
|
+
"E2": E2.isoformat()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# If NIFTY spot isn't live yet, try to fetch it
|
|
148
|
+
if nifty_spot is None or nifty_spot <= 0.0:
|
|
149
|
+
try:
|
|
150
|
+
indices = client.get_market_indices()
|
|
151
|
+
nifty_spot = indices.get("nifty")
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
if not nifty_spot or nifty_spot <= 0.0:
|
|
156
|
+
return {
|
|
157
|
+
"status": "error",
|
|
158
|
+
"message": "NIFTY index spot price is not available. Please verify connection/ticker status."
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
plan["nifty_spot"] = nifty_spot
|
|
162
|
+
|
|
163
|
+
# Calculate strikes rounded to nearest 100
|
|
164
|
+
strike_e1_ce = round((nifty_spot * 1.05) / 100) * 100
|
|
165
|
+
strike_e1_pe = round((nifty_spot * 0.95) / 100) * 100
|
|
166
|
+
strike_e2_ce = round((nifty_spot * 1.07) / 100) * 100
|
|
167
|
+
strike_e2_pe = round((nifty_spot * 0.93) / 100) * 100
|
|
168
|
+
|
|
169
|
+
plan["strikes"] = {
|
|
170
|
+
"E1_CE": strike_e1_ce,
|
|
171
|
+
"E1_PE": strike_e1_pe,
|
|
172
|
+
"E2_CE": strike_e2_ce,
|
|
173
|
+
"E2_PE": strike_e2_pe
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Resolve target symbols
|
|
177
|
+
e1_ce_sym, _ = find_option_symbols(options, E1, strike_e1_ce)
|
|
178
|
+
_, e1_pe_sym = find_option_symbols(options, E1, strike_e1_pe)
|
|
179
|
+
e2_ce_sym, _ = find_option_symbols(options, E2, strike_e2_ce)
|
|
180
|
+
_, e2_pe_sym = find_option_symbols(options, E2, strike_e2_pe)
|
|
181
|
+
|
|
182
|
+
plan["symbols"] = {
|
|
183
|
+
"E1_CE": e1_ce_sym,
|
|
184
|
+
"E1_PE": e1_pe_sym,
|
|
185
|
+
"E2_CE": e2_ce_sym,
|
|
186
|
+
"E2_PE": e2_pe_sym
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for acct in accounts_positions:
|
|
190
|
+
api_key = acct.get("api_key")
|
|
191
|
+
user_id = api_key_to_user_id.get(api_key, "UNKNOWN")
|
|
192
|
+
name = acct.get("name", user_id)
|
|
193
|
+
positions = acct.get("positions", [])
|
|
194
|
+
|
|
195
|
+
# Fetch margin details (using live balance and collateral)
|
|
196
|
+
margin_info = margins_by_api_key.get(api_key, {})
|
|
197
|
+
cash = float(margin_info.get("cash") or 0.0)
|
|
198
|
+
collateral = float(margin_info.get("collateral") or 0.0)
|
|
199
|
+
total_capital = cash + collateral
|
|
200
|
+
|
|
201
|
+
# Reserve 3 Lakhs buffer
|
|
202
|
+
trading_capital = total_capital - 300000
|
|
203
|
+
|
|
204
|
+
# Calculate lots under 50/50 split (1.3 Lakhs margin per strangle lot)
|
|
205
|
+
if trading_capital > 0:
|
|
206
|
+
alloc_per_week = 0.5 * trading_capital
|
|
207
|
+
lots_e1 = int(alloc_per_week // 130000)
|
|
208
|
+
lots_e2 = int(alloc_per_week // 130000)
|
|
209
|
+
else:
|
|
210
|
+
lots_e1 = 0
|
|
211
|
+
lots_e2 = 0
|
|
212
|
+
|
|
213
|
+
# Identify existing E0 and E1 positions to exit
|
|
214
|
+
exits_e0 = []
|
|
215
|
+
exits_e1 = []
|
|
216
|
+
for pos in positions:
|
|
217
|
+
symbol = pos.get("tradingsymbol", "")
|
|
218
|
+
# Skip if closed
|
|
219
|
+
if pos.get("quantity", 0) == 0:
|
|
220
|
+
continue
|
|
221
|
+
# Lookup instrument expiry
|
|
222
|
+
inst = next((x for x in options if x.get("tradingsymbol") == symbol), None)
|
|
223
|
+
if inst:
|
|
224
|
+
exp = inst.get("expiry")
|
|
225
|
+
lp = pos.get("last_price")
|
|
226
|
+
try:
|
|
227
|
+
p_val = float(lp) if lp is not None else 0.0
|
|
228
|
+
p_str = f" {p_val:.2f}" if p_val > 0 else ""
|
|
229
|
+
except (ValueError, TypeError):
|
|
230
|
+
p_str = ""
|
|
231
|
+
|
|
232
|
+
if exp == E0:
|
|
233
|
+
exits_e0.append((symbol, p_str))
|
|
234
|
+
elif exp == E1:
|
|
235
|
+
exits_e1.append((symbol, p_str))
|
|
236
|
+
|
|
237
|
+
# Build Stage 1 command
|
|
238
|
+
# Syntax: account <name> && exit <pos1> && exit <pos2> && sell <CE> <lots>L && sell <PE> <lots>L
|
|
239
|
+
stage_1_parts = [f"account {name}"]
|
|
240
|
+
|
|
241
|
+
# Add exit commands
|
|
242
|
+
for sym, p_str in exits_e0:
|
|
243
|
+
stage_1_parts.append(f"exit {sym}{p_str}")
|
|
244
|
+
for sym, p_str in exits_e1:
|
|
245
|
+
stage_1_parts.append(f"exit {sym}{p_str}")
|
|
246
|
+
|
|
247
|
+
# Add new E1 entries if lots > 0 and symbols resolved
|
|
248
|
+
if lots_e1 > 0:
|
|
249
|
+
if e1_ce_sym:
|
|
250
|
+
stage_1_parts.append(f"sell {e1_ce_sym} {lots_e1}L")
|
|
251
|
+
if e1_pe_sym:
|
|
252
|
+
stage_1_parts.append(f"sell {e1_pe_sym} {lots_e1}L")
|
|
253
|
+
|
|
254
|
+
stage_1_cmd = " && ".join(stage_1_parts) if (len(stage_1_parts) > 1 or lots_e1 > 0) else ""
|
|
255
|
+
|
|
256
|
+
# Build Stage 2 command
|
|
257
|
+
stage_2_cmd = ""
|
|
258
|
+
if lots_e2 > 0:
|
|
259
|
+
stage_2_parts = [f"account {name}"]
|
|
260
|
+
if e2_ce_sym:
|
|
261
|
+
stage_2_parts.append(f"sell {e2_ce_sym} {lots_e2}L")
|
|
262
|
+
if e2_pe_sym:
|
|
263
|
+
stage_2_parts.append(f"sell {e2_pe_sym} {lots_e2}L")
|
|
264
|
+
if len(stage_2_parts) > 1:
|
|
265
|
+
stage_2_cmd = " && ".join(stage_2_parts)
|
|
266
|
+
|
|
267
|
+
acct_plan = {
|
|
268
|
+
"name": name,
|
|
269
|
+
"user_id": user_id,
|
|
270
|
+
"cash": cash,
|
|
271
|
+
"collateral": collateral,
|
|
272
|
+
"total_capital": total_capital,
|
|
273
|
+
"trading_capital": trading_capital,
|
|
274
|
+
"lots_e1": lots_e1,
|
|
275
|
+
"lots_e2": lots_e2,
|
|
276
|
+
"exits_e0": [x[0] for x in exits_e0],
|
|
277
|
+
"exits_e1": [x[0] for x in exits_e1],
|
|
278
|
+
"stage_1_cmd": stage_1_cmd,
|
|
279
|
+
"stage_2_cmd": stage_2_cmd
|
|
280
|
+
}
|
|
281
|
+
plan["accounts"].append(acct_plan)
|
|
282
|
+
|
|
283
|
+
return plan
|