kitecli 0.1.0b1__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.1.0b1/PKG-INFO +158 -0
- kitecli-0.1.0b1/README.md +133 -0
- kitecli-0.1.0b1/cli/__init__.py +1 -0
- kitecli-0.1.0b1/cli/api_client.py +322 -0
- kitecli-0.1.0b1/cli/config.py +75 -0
- kitecli-0.1.0b1/cli/display.py +367 -0
- kitecli-0.1.0b1/cli/kite_manager.py +792 -0
- kitecli-0.1.0b1/cli/live_session.py +1908 -0
- kitecli-0.1.0b1/cli/main.py +292 -0
- kitecli-0.1.0b1/kitecli.egg-info/PKG-INFO +158 -0
- kitecli-0.1.0b1/kitecli.egg-info/SOURCES.txt +15 -0
- kitecli-0.1.0b1/kitecli.egg-info/dependency_links.txt +1 -0
- kitecli-0.1.0b1/kitecli.egg-info/entry_points.txt +2 -0
- kitecli-0.1.0b1/kitecli.egg-info/requires.txt +18 -0
- kitecli-0.1.0b1/kitecli.egg-info/top_level.txt +1 -0
- kitecli-0.1.0b1/pyproject.toml +43 -0
- kitecli-0.1.0b1/setup.cfg +4 -0
kitecli-0.1.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kitecli
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Kite Connect CLI — Multi-account Zerodha trading positions viewer
|
|
5
|
+
Author: KiteCLI Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
12
|
+
Requires-Dist: prompt_toolkit>=3.0.36
|
|
13
|
+
Requires-Dist: kiteconnect>=5.0.0
|
|
14
|
+
Requires-Dist: pyotp>=2.9.0
|
|
15
|
+
Requires-Dist: requests>=2.31.0
|
|
16
|
+
Requires-Dist: yfinance>=0.2.0
|
|
17
|
+
Provides-Extra: server
|
|
18
|
+
Requires-Dist: fastapi>=0.110.0; extra == "server"
|
|
19
|
+
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "server"
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0; extra == "server"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
24
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# KCLI — Kite Connect CLI
|
|
27
|
+
|
|
28
|
+
A multi-account Zerodha Kite Connect positions viewer with a beautiful terminal interface.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
╦╔═╔═╗╦ ╦
|
|
32
|
+
╠╩╗║ ║ ║
|
|
33
|
+
╩ ╩╚═╝╩═╝╩
|
|
34
|
+
Kite Connect CLI
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
┌─────────────────┐ HTTPS ┌──────────────────┐ Kite API ┌─────────────────┐
|
|
41
|
+
│ kcli (CLI) │ ──────────────── │ FastAPI Server │ ──────────────────│ Zerodha Kite │
|
|
42
|
+
│ Your Machine │ │ Google Cloud │ │ Connect API │
|
|
43
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
~/.kcli/config.yaml
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **CLI (`kcli`)**: Runs on your machine. Beautiful color-coded terminal UI.
|
|
50
|
+
- **Server**: Runs on Google Cloud (Cloud Run). Proxies Kite API calls.
|
|
51
|
+
- **Config**: Multi-account config stored locally at `~/.kcli/config.yaml`.
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Deploy the Server
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Build and deploy to Cloud Run
|
|
59
|
+
cd server
|
|
60
|
+
gcloud run deploy kcli-server \
|
|
61
|
+
--source . \
|
|
62
|
+
--region asia-south1 \
|
|
63
|
+
--allow-unauthenticated \
|
|
64
|
+
--set-env-vars AUTH_TOKEN=your-secret-token
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Install the CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# From the project root
|
|
71
|
+
pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Initialize Config
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Create the default config file
|
|
78
|
+
kcli config --init
|
|
79
|
+
|
|
80
|
+
# Edit the config with your accounts
|
|
81
|
+
# Open ~/.kcli/config.yaml and add your Kite API credentials
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Config file format (`~/.kcli/config.yaml`):**
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
server:
|
|
88
|
+
url: "https://your-cloud-run-url.run.app"
|
|
89
|
+
auth_token: "your-secret-token"
|
|
90
|
+
|
|
91
|
+
accounts:
|
|
92
|
+
- name: "Trading Account 1"
|
|
93
|
+
api_key: "your_api_key_1"
|
|
94
|
+
api_secret: "your_api_secret_1"
|
|
95
|
+
- name: "Trading Account 2"
|
|
96
|
+
api_key: "your_api_key_2"
|
|
97
|
+
api_secret: "your_api_secret_2"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4. Login to Kite
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Initialize and authenticate all accounts
|
|
104
|
+
kcli init
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This will:
|
|
108
|
+
1. Send your account configs to the server
|
|
109
|
+
2. Display login URLs for each account
|
|
110
|
+
3. Prompt you to paste the `request_token` after logging in via browser
|
|
111
|
+
|
|
112
|
+
> **Note:** Kite access tokens expire daily (~6 AM IST). You need to run `kcli init` once each trading day.
|
|
113
|
+
|
|
114
|
+
### 5. View Positions
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# View positions across all accounts
|
|
118
|
+
kcli positions
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Commands
|
|
122
|
+
|
|
123
|
+
| Command | Description |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `kcli init` | Authenticate all accounts (daily) |
|
|
126
|
+
| `kcli positions` | View positions across all accounts |
|
|
127
|
+
| `kcli status` | Check authentication status |
|
|
128
|
+
| `kcli config --init` | Create default config file |
|
|
129
|
+
| `kcli config --show` | Show current config (secrets masked) |
|
|
130
|
+
| `kcli config --path` | Print config file path |
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
### Run Server Locally
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
cd server
|
|
138
|
+
pip install -r requirements.txt
|
|
139
|
+
AUTH_TOKEN=test-token uvicorn main:app --reload --port 8080
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Run CLI
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install -e .
|
|
146
|
+
kcli --help
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Getting Kite API Credentials
|
|
150
|
+
|
|
151
|
+
1. Go to [Kite Developer Console](https://developers.kite.trade/)
|
|
152
|
+
2. Create a new app
|
|
153
|
+
3. Note your **API Key** and **API Secret**
|
|
154
|
+
4. Set the **Redirect URL** to your server's callback URL
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# KCLI — Kite Connect CLI
|
|
2
|
+
|
|
3
|
+
A multi-account Zerodha Kite Connect positions viewer with a beautiful terminal interface.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
╦╔═╔═╗╦ ╦
|
|
7
|
+
╠╩╗║ ║ ║
|
|
8
|
+
╩ ╩╚═╝╩═╝╩
|
|
9
|
+
Kite Connect CLI
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌─────────────────┐ HTTPS ┌──────────────────┐ Kite API ┌─────────────────┐
|
|
16
|
+
│ kcli (CLI) │ ──────────────── │ FastAPI Server │ ──────────────────│ Zerodha Kite │
|
|
17
|
+
│ Your Machine │ │ Google Cloud │ │ Connect API │
|
|
18
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
19
|
+
│
|
|
20
|
+
▼
|
|
21
|
+
~/.kcli/config.yaml
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **CLI (`kcli`)**: Runs on your machine. Beautiful color-coded terminal UI.
|
|
25
|
+
- **Server**: Runs on Google Cloud (Cloud Run). Proxies Kite API calls.
|
|
26
|
+
- **Config**: Multi-account config stored locally at `~/.kcli/config.yaml`.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### 1. Deploy the Server
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Build and deploy to Cloud Run
|
|
34
|
+
cd server
|
|
35
|
+
gcloud run deploy kcli-server \
|
|
36
|
+
--source . \
|
|
37
|
+
--region asia-south1 \
|
|
38
|
+
--allow-unauthenticated \
|
|
39
|
+
--set-env-vars AUTH_TOKEN=your-secret-token
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Install the CLI
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# From the project root
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Initialize Config
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Create the default config file
|
|
53
|
+
kcli config --init
|
|
54
|
+
|
|
55
|
+
# Edit the config with your accounts
|
|
56
|
+
# Open ~/.kcli/config.yaml and add your Kite API credentials
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Config file format (`~/.kcli/config.yaml`):**
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
server:
|
|
63
|
+
url: "https://your-cloud-run-url.run.app"
|
|
64
|
+
auth_token: "your-secret-token"
|
|
65
|
+
|
|
66
|
+
accounts:
|
|
67
|
+
- name: "Trading Account 1"
|
|
68
|
+
api_key: "your_api_key_1"
|
|
69
|
+
api_secret: "your_api_secret_1"
|
|
70
|
+
- name: "Trading Account 2"
|
|
71
|
+
api_key: "your_api_key_2"
|
|
72
|
+
api_secret: "your_api_secret_2"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 4. Login to Kite
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Initialize and authenticate all accounts
|
|
79
|
+
kcli init
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This will:
|
|
83
|
+
1. Send your account configs to the server
|
|
84
|
+
2. Display login URLs for each account
|
|
85
|
+
3. Prompt you to paste the `request_token` after logging in via browser
|
|
86
|
+
|
|
87
|
+
> **Note:** Kite access tokens expire daily (~6 AM IST). You need to run `kcli init` once each trading day.
|
|
88
|
+
|
|
89
|
+
### 5. View Positions
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# View positions across all accounts
|
|
93
|
+
kcli positions
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Commands
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `kcli init` | Authenticate all accounts (daily) |
|
|
101
|
+
| `kcli positions` | View positions across all accounts |
|
|
102
|
+
| `kcli status` | Check authentication status |
|
|
103
|
+
| `kcli config --init` | Create default config file |
|
|
104
|
+
| `kcli config --show` | Show current config (secrets masked) |
|
|
105
|
+
| `kcli config --path` | Print config file path |
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
### Run Server Locally
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cd server
|
|
113
|
+
pip install -r requirements.txt
|
|
114
|
+
AUTH_TOKEN=test-token uvicorn main:app --reload --port 8080
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Run CLI
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install -e .
|
|
121
|
+
kcli --help
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Getting Kite API Credentials
|
|
125
|
+
|
|
126
|
+
1. Go to [Kite Developer Console](https://developers.kite.trade/)
|
|
127
|
+
2. Create a new app
|
|
128
|
+
3. Note your **API Key** and **API Secret**
|
|
129
|
+
4. Set the **Redirect URL** to your server's callback URL
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# KiteCLI - Kite Connect CLI
|
|
@@ -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()
|
|
@@ -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
|