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 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