kitecli 0.1.0b1__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 +1 -0
- cli/api_client.py +322 -0
- cli/config.py +75 -0
- cli/display.py +367 -0
- cli/kite_manager.py +792 -0
- cli/live_session.py +1908 -0
- cli/main.py +292 -0
- kitecli-0.1.0b1.dist-info/METADATA +158 -0
- kitecli-0.1.0b1.dist-info/RECORD +12 -0
- kitecli-0.1.0b1.dist-info/WHEEL +5 -0
- kitecli-0.1.0b1.dist-info/entry_points.txt +2 -0
- kitecli-0.1.0b1.dist-info/top_level.txt +1 -0
cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# KiteCLI - Kite Connect CLI
|
cli/api_client.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local KiteCLI client.
|
|
3
|
+
|
|
4
|
+
Wraps KiteAccountManager directly — no HTTP server needed.
|
|
5
|
+
Public API is identical to the old HTTP-based KCLIClient so that
|
|
6
|
+
cli/main.py and cli/live_session.py require zero changes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
from cli.kite_manager import KiteAccountManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KCLIClientError(Exception):
|
|
14
|
+
"""Raised when a Kite API call fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Module-level singleton so session state persists across calls within the process.
|
|
18
|
+
_manager = KiteAccountManager()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class KCLIClient:
|
|
22
|
+
"""Local client that delegates directly to KiteAccountManager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
accounts: List of account dicts from config (name, api_key, api_secret,
|
|
26
|
+
user_id, password, totp_secret, proxy). Accounts are
|
|
27
|
+
initialised eagerly on construction so that session tokens are
|
|
28
|
+
restored before the first command runs.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, accounts: list[dict]) -> None:
|
|
32
|
+
self._accounts = accounts
|
|
33
|
+
self._api_keys = [a.get("api_key", "") for a in accounts]
|
|
34
|
+
# Eagerly init all accounts (restores saved sessions if available)
|
|
35
|
+
for acct in accounts:
|
|
36
|
+
_manager.init_account(
|
|
37
|
+
api_key=acct.get("api_key", ""),
|
|
38
|
+
api_secret=acct.get("api_secret", ""),
|
|
39
|
+
name=acct.get("name", ""),
|
|
40
|
+
proxy=acct.get("proxy"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ── compatibility helpers ──────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def health_check(self) -> bool:
|
|
46
|
+
"""Always True — no server to ping in local mode."""
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# ── public API (mirrors old KCLIClient exactly, but runs in parallel) ──
|
|
50
|
+
|
|
51
|
+
def init_accounts(self, accounts: list[dict]) -> dict:
|
|
52
|
+
"""Initialise (or re-initialise) accounts and attempt auto-login in parallel."""
|
|
53
|
+
def init_one(acct):
|
|
54
|
+
api_key = acct.get("api_key", "")
|
|
55
|
+
name = acct.get("name", api_key)
|
|
56
|
+
|
|
57
|
+
login_url = _manager.init_account(
|
|
58
|
+
api_key=api_key,
|
|
59
|
+
api_secret=acct.get("api_secret", ""),
|
|
60
|
+
name=name,
|
|
61
|
+
proxy=acct.get("proxy"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if _manager.is_authenticated(api_key):
|
|
65
|
+
return {
|
|
66
|
+
"name": name,
|
|
67
|
+
"api_key": api_key,
|
|
68
|
+
"login_url": login_url,
|
|
69
|
+
"auto_logged_in": True,
|
|
70
|
+
"message": "Session restored from saved token",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
user_id = acct.get("user_id")
|
|
74
|
+
password = acct.get("password")
|
|
75
|
+
totp_secret = acct.get("totp_secret")
|
|
76
|
+
if user_id and password and totp_secret:
|
|
77
|
+
success = _manager.auto_login(
|
|
78
|
+
api_key=api_key,
|
|
79
|
+
user_id=user_id,
|
|
80
|
+
password=password,
|
|
81
|
+
totp_secret=totp_secret,
|
|
82
|
+
)
|
|
83
|
+
if success:
|
|
84
|
+
return {
|
|
85
|
+
"name": name,
|
|
86
|
+
"api_key": api_key,
|
|
87
|
+
"login_url": login_url,
|
|
88
|
+
"auto_logged_in": True,
|
|
89
|
+
"message": "Auto-login successful",
|
|
90
|
+
}
|
|
91
|
+
else:
|
|
92
|
+
return {
|
|
93
|
+
"name": name,
|
|
94
|
+
"api_key": api_key,
|
|
95
|
+
"login_url": login_url,
|
|
96
|
+
"auto_logged_in": False,
|
|
97
|
+
"message": "Auto-login failed. Use manual login URL.",
|
|
98
|
+
}
|
|
99
|
+
else:
|
|
100
|
+
return {
|
|
101
|
+
"name": name,
|
|
102
|
+
"api_key": api_key,
|
|
103
|
+
"login_url": login_url,
|
|
104
|
+
"auto_logged_in": False,
|
|
105
|
+
"message": "Credentials incomplete — manual login required.",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
with ThreadPoolExecutor(max_workers=max(1, len(accounts))) as executor:
|
|
109
|
+
results = list(executor.map(init_one, accounts))
|
|
110
|
+
|
|
111
|
+
return {"accounts": results}
|
|
112
|
+
|
|
113
|
+
def complete_callback(self, api_key: str, request_token: str) -> dict:
|
|
114
|
+
"""Complete Kite OAuth login with a request token."""
|
|
115
|
+
try:
|
|
116
|
+
success = _manager.complete_login(api_key, request_token.strip())
|
|
117
|
+
if success:
|
|
118
|
+
return {"status": "success", "message": "Login successful"}
|
|
119
|
+
return {"status": "error", "message": "Login failed — check request_token"}
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
raise KCLIClientError(str(exc)) from exc
|
|
122
|
+
|
|
123
|
+
def get_positions(self, api_keys: list[str]) -> dict:
|
|
124
|
+
"""Fetch open positions for the given accounts in parallel."""
|
|
125
|
+
keys = api_keys or _manager.get_all_api_keys()
|
|
126
|
+
|
|
127
|
+
def fetch_one(api_key):
|
|
128
|
+
info = _manager.get_account_info(api_key)
|
|
129
|
+
if not info.get("authenticated"):
|
|
130
|
+
return {
|
|
131
|
+
"name": info.get("name", api_key),
|
|
132
|
+
"api_key": api_key,
|
|
133
|
+
"positions": [],
|
|
134
|
+
"total_pnl": 0.0,
|
|
135
|
+
"status": "unauthenticated",
|
|
136
|
+
}
|
|
137
|
+
try:
|
|
138
|
+
positions = _manager.get_positions(api_key)
|
|
139
|
+
total_pnl = sum(p.get("pnl", 0.0) for p in positions)
|
|
140
|
+
return {
|
|
141
|
+
"name": info.get("name", api_key),
|
|
142
|
+
"api_key": api_key,
|
|
143
|
+
"positions": positions,
|
|
144
|
+
"total_pnl": total_pnl,
|
|
145
|
+
"status": "success",
|
|
146
|
+
}
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
return {
|
|
149
|
+
"name": info.get("name", api_key),
|
|
150
|
+
"api_key": api_key,
|
|
151
|
+
"positions": [],
|
|
152
|
+
"total_pnl": 0.0,
|
|
153
|
+
"status": f"error: {exc}",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
|
|
157
|
+
results = list(executor.map(fetch_one, keys))
|
|
158
|
+
|
|
159
|
+
return {"accounts": results}
|
|
160
|
+
|
|
161
|
+
def get_status(self) -> dict:
|
|
162
|
+
"""Get authentication status for all accounts."""
|
|
163
|
+
accounts = [
|
|
164
|
+
_manager.get_account_info(api_key)
|
|
165
|
+
for api_key in _manager.get_all_api_keys()
|
|
166
|
+
]
|
|
167
|
+
return {"accounts": accounts}
|
|
168
|
+
|
|
169
|
+
def place_order(
|
|
170
|
+
self,
|
|
171
|
+
api_keys: list[str],
|
|
172
|
+
tradingsymbol: str,
|
|
173
|
+
exchange: str,
|
|
174
|
+
transaction_type: str,
|
|
175
|
+
quantity: int,
|
|
176
|
+
order_type: str,
|
|
177
|
+
price: float | None = None,
|
|
178
|
+
trigger_price: float | None = None,
|
|
179
|
+
product: str = "NRML",
|
|
180
|
+
) -> dict:
|
|
181
|
+
"""Place an order across specified accounts in parallel."""
|
|
182
|
+
keys = api_keys or _manager.get_all_api_keys()
|
|
183
|
+
|
|
184
|
+
def place_one(api_key):
|
|
185
|
+
info = _manager.get_account_info(api_key)
|
|
186
|
+
if not info.get("authenticated"):
|
|
187
|
+
return {
|
|
188
|
+
"name": info.get("name", api_key),
|
|
189
|
+
"api_key": api_key,
|
|
190
|
+
"status": "error",
|
|
191
|
+
"order_id": None,
|
|
192
|
+
"message": "Account not authenticated",
|
|
193
|
+
}
|
|
194
|
+
try:
|
|
195
|
+
order_id = _manager.place_order(
|
|
196
|
+
api_key=api_key,
|
|
197
|
+
tradingsymbol=tradingsymbol,
|
|
198
|
+
exchange=exchange,
|
|
199
|
+
transaction_type=transaction_type,
|
|
200
|
+
quantity=quantity,
|
|
201
|
+
order_type=order_type,
|
|
202
|
+
price=price,
|
|
203
|
+
trigger_price=trigger_price,
|
|
204
|
+
product=product,
|
|
205
|
+
)
|
|
206
|
+
return {
|
|
207
|
+
"name": info.get("name", api_key),
|
|
208
|
+
"api_key": api_key,
|
|
209
|
+
"status": "success",
|
|
210
|
+
"order_id": str(order_id),
|
|
211
|
+
"message": f"Order placed: {order_id}",
|
|
212
|
+
}
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
return {
|
|
215
|
+
"name": info.get("name", api_key),
|
|
216
|
+
"api_key": api_key,
|
|
217
|
+
"status": "error",
|
|
218
|
+
"order_id": None,
|
|
219
|
+
"message": str(exc),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
|
|
223
|
+
results = list(executor.map(place_one, keys))
|
|
224
|
+
|
|
225
|
+
return {"results": results}
|
|
226
|
+
|
|
227
|
+
def exit_positions(
|
|
228
|
+
self,
|
|
229
|
+
api_keys: list[str],
|
|
230
|
+
tradingsymbol: str | None = None,
|
|
231
|
+
) -> dict:
|
|
232
|
+
"""Exit positions across specified accounts in parallel."""
|
|
233
|
+
keys = api_keys or _manager.get_all_api_keys()
|
|
234
|
+
|
|
235
|
+
def exit_one(api_key):
|
|
236
|
+
info = _manager.get_account_info(api_key)
|
|
237
|
+
if not info.get("authenticated"):
|
|
238
|
+
return {
|
|
239
|
+
"name": info.get("name", api_key),
|
|
240
|
+
"api_key": api_key,
|
|
241
|
+
"status": "error",
|
|
242
|
+
"message": "Account not authenticated",
|
|
243
|
+
"orders_placed": [],
|
|
244
|
+
}
|
|
245
|
+
try:
|
|
246
|
+
orders = _manager.exit_positions(api_key, tradingsymbol)
|
|
247
|
+
return {
|
|
248
|
+
"name": info.get("name", api_key),
|
|
249
|
+
"api_key": api_key,
|
|
250
|
+
"status": "success",
|
|
251
|
+
"message": f"Exited {len(orders)} position(s)",
|
|
252
|
+
"orders_placed": orders,
|
|
253
|
+
}
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
return {
|
|
256
|
+
"name": info.get("name", api_key),
|
|
257
|
+
"api_key": api_key,
|
|
258
|
+
"status": "error",
|
|
259
|
+
"message": str(exc),
|
|
260
|
+
"orders_placed": [],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
|
|
264
|
+
results = list(executor.map(exit_one, keys))
|
|
265
|
+
|
|
266
|
+
return {"results": results}
|
|
267
|
+
|
|
268
|
+
def get_option_chain(
|
|
269
|
+
self,
|
|
270
|
+
api_key: str,
|
|
271
|
+
underlying: str,
|
|
272
|
+
expiry_week: int = 0,
|
|
273
|
+
expiry_date: str | None = None,
|
|
274
|
+
) -> dict:
|
|
275
|
+
"""Fetch option chain for a specific underlying and expiry."""
|
|
276
|
+
try:
|
|
277
|
+
return _manager.get_option_chain(
|
|
278
|
+
api_key=api_key,
|
|
279
|
+
underlying=underlying,
|
|
280
|
+
expiry_week=expiry_week,
|
|
281
|
+
expiry_date=expiry_date,
|
|
282
|
+
)
|
|
283
|
+
except Exception as exc:
|
|
284
|
+
raise KCLIClientError(str(exc)) from exc
|
|
285
|
+
|
|
286
|
+
def get_orders(self, api_keys: list[str]) -> dict:
|
|
287
|
+
"""Fetch today's order book for specified accounts in parallel."""
|
|
288
|
+
keys = api_keys or _manager.get_all_api_keys()
|
|
289
|
+
|
|
290
|
+
def fetch_one(api_key):
|
|
291
|
+
info = _manager.get_account_info(api_key)
|
|
292
|
+
if not info.get("authenticated"):
|
|
293
|
+
return {
|
|
294
|
+
"name": info.get("name", api_key),
|
|
295
|
+
"api_key": api_key,
|
|
296
|
+
"orders": [],
|
|
297
|
+
"status": "unauthenticated",
|
|
298
|
+
}
|
|
299
|
+
try:
|
|
300
|
+
orders = _manager.get_orders(api_key)
|
|
301
|
+
return {
|
|
302
|
+
"name": info.get("name", api_key),
|
|
303
|
+
"api_key": api_key,
|
|
304
|
+
"orders": orders,
|
|
305
|
+
"status": "success",
|
|
306
|
+
}
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
return {
|
|
309
|
+
"name": info.get("name", api_key),
|
|
310
|
+
"api_key": api_key,
|
|
311
|
+
"orders": [],
|
|
312
|
+
"status": f"error: {exc}",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
with ThreadPoolExecutor(max_workers=max(1, len(keys))) as executor:
|
|
316
|
+
results = list(executor.map(fetch_one, keys))
|
|
317
|
+
|
|
318
|
+
return {"accounts": results}
|
|
319
|
+
|
|
320
|
+
def get_market_indices(self) -> dict:
|
|
321
|
+
"""Fetch live Nifty, Sensex, and India VIX."""
|
|
322
|
+
return _manager.get_market_indices()
|
cli/config.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for KiteCLI.
|
|
3
|
+
|
|
4
|
+
Handles reading, writing, and creating default configuration files
|
|
5
|
+
stored at ~/.kcli/config.yaml.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
CONFIG_DIR = Path.home() / ".kcli"
|
|
14
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
15
|
+
|
|
16
|
+
DEFAULT_CONFIG = {
|
|
17
|
+
"accounts": [
|
|
18
|
+
{
|
|
19
|
+
"name": "Account 1",
|
|
20
|
+
"api_key": "your_api_key",
|
|
21
|
+
"api_secret": "your_api_secret",
|
|
22
|
+
"user_id": "your_zerodha_user_id",
|
|
23
|
+
"password": "your_zerodha_password",
|
|
24
|
+
"totp_secret": "your_totp_secret",
|
|
25
|
+
"proxy": "http://user:pass@host:port",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_config() -> Optional[dict]:
|
|
32
|
+
"""Load configuration from ~/.kcli/config.yaml.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
dict with configuration values, or None if the config file
|
|
36
|
+
does not exist.
|
|
37
|
+
"""
|
|
38
|
+
if not CONFIG_FILE.exists():
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
with open(CONFIG_FILE, "r") as f:
|
|
42
|
+
config = yaml.safe_load(f)
|
|
43
|
+
|
|
44
|
+
# Automatically migrate/remove legacy server config if present
|
|
45
|
+
if config and "server" in config:
|
|
46
|
+
del config["server"]
|
|
47
|
+
save_config(config)
|
|
48
|
+
|
|
49
|
+
return config
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_config(config: dict) -> None:
|
|
53
|
+
"""Write configuration to ~/.kcli/config.yaml.
|
|
54
|
+
|
|
55
|
+
Creates the config directory if it does not already exist.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
config: Configuration dictionary to persist.
|
|
59
|
+
"""
|
|
60
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
with open(CONFIG_FILE, "w") as f:
|
|
62
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_default_config() -> dict:
|
|
66
|
+
"""Create and save a default template configuration.
|
|
67
|
+
|
|
68
|
+
The template contains placeholder values that the user should
|
|
69
|
+
replace with their own credentials.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The default configuration dictionary that was saved.
|
|
73
|
+
"""
|
|
74
|
+
save_config(DEFAULT_CONFIG)
|
|
75
|
+
return DEFAULT_CONFIG
|