tradingnexus 0.1.0__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.
- tradingnexus-0.1.0/PKG-INFO +125 -0
- tradingnexus-0.1.0/README.md +99 -0
- tradingnexus-0.1.0/pyproject.toml +39 -0
- tradingnexus-0.1.0/setup.cfg +4 -0
- tradingnexus-0.1.0/tradingnexus/__init__.py +36 -0
- tradingnexus-0.1.0/tradingnexus/client.py +308 -0
- tradingnexus-0.1.0/tradingnexus/errors.py +57 -0
- tradingnexus-0.1.0/tradingnexus/websocket.py +130 -0
- tradingnexus-0.1.0/tradingnexus.egg-info/PKG-INFO +125 -0
- tradingnexus-0.1.0/tradingnexus.egg-info/SOURCES.txt +11 -0
- tradingnexus-0.1.0/tradingnexus.egg-info/dependency_links.txt +1 -0
- tradingnexus-0.1.0/tradingnexus.egg-info/requires.txt +6 -0
- tradingnexus-0.1.0/tradingnexus.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tradingnexus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Trading Nexus Personal Trade API
|
|
5
|
+
Author: Trading Nexus
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://tradingnexus.pro/developers
|
|
8
|
+
Project-URL: Documentation, https://tradingnexus.pro/developers
|
|
9
|
+
Project-URL: Source, https://github.com/tradingnexus
|
|
10
|
+
Keywords: trading,tradingnexus,algo,orders,indian-markets
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.28.0
|
|
22
|
+
Requires-Dist: websocket-client>=1.6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Trading Nexus — Python client (draft)
|
|
28
|
+
|
|
29
|
+
Official-style Python helper for the **Personal Trade API**
|
|
30
|
+
(own-account algo / bot access).
|
|
31
|
+
|
|
32
|
+
> Status: **v0.1.0 draft** — not published to PyPI yet. Install from this folder for testing.
|
|
33
|
+
|
|
34
|
+
## Install (local)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd sdks/python
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from tradingnexus import TradeClient
|
|
45
|
+
|
|
46
|
+
client = TradeClient(api_key="tk_your_secret_key_here")
|
|
47
|
+
|
|
48
|
+
# 1) Find the exact option / equity contract
|
|
49
|
+
found = client.search_instruments(
|
|
50
|
+
underlying="NIFTY",
|
|
51
|
+
exchange_segment="NSE_FNO",
|
|
52
|
+
symbol="24000",
|
|
53
|
+
limit=50,
|
|
54
|
+
)
|
|
55
|
+
row = next(
|
|
56
|
+
(
|
|
57
|
+
i
|
|
58
|
+
for i in found.get("instruments", [])
|
|
59
|
+
if str(i.get("option_type", "")).upper() == "PE"
|
|
60
|
+
and float(i.get("strike_price") or 0) == 24000
|
|
61
|
+
),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
if not row:
|
|
65
|
+
raise SystemExit("Contract not found — refine search / expiry")
|
|
66
|
+
|
|
67
|
+
# 2) Place order using returned fields
|
|
68
|
+
order = client.place_order(
|
|
69
|
+
side="BUY",
|
|
70
|
+
quantity=int(row["lot_size"]),
|
|
71
|
+
order_type="MARKET",
|
|
72
|
+
product_type="MIS",
|
|
73
|
+
symbol=row["symbol"],
|
|
74
|
+
exchange_segment=row["exchange_segment"],
|
|
75
|
+
security_id=row.get("security_id") or row.get("instrument_token"),
|
|
76
|
+
idempotency_key="bot-demo-001",
|
|
77
|
+
)
|
|
78
|
+
print(order)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Live order updates
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from tradingnexus import OrderStream
|
|
85
|
+
|
|
86
|
+
def on_message(msg):
|
|
87
|
+
print(msg)
|
|
88
|
+
|
|
89
|
+
stream = OrderStream(api_key="tk_your_secret_key_here", on_message=on_message)
|
|
90
|
+
stream.start()
|
|
91
|
+
# ... keep process alive ...
|
|
92
|
+
# stream.stop()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## What this covers
|
|
96
|
+
|
|
97
|
+
| Client method | API |
|
|
98
|
+
|---------------|-----|
|
|
99
|
+
| `search_instruments` / `get_instrument` | Instruments |
|
|
100
|
+
| `place_order` / `modify_order` / `cancel_order` | Orders write |
|
|
101
|
+
| `get_order` / `list_orders` | Orders read |
|
|
102
|
+
| `list_trades` / `list_trades_for_order` | Fills |
|
|
103
|
+
| `list_positions` | Positions |
|
|
104
|
+
| `OrderStream` | WebSocket `/personal/ws/orders` |
|
|
105
|
+
|
|
106
|
+
## Auth notes
|
|
107
|
+
|
|
108
|
+
- Header: `X-PERSONAL-KEY` (server also accepts `X-API-KEY`)
|
|
109
|
+
- IP allowlist is **optional per key** (Super Admin → API Access)
|
|
110
|
+
- Keys are issued only by Trading Nexus Super Admin — no self-service
|
|
111
|
+
|
|
112
|
+
## Docs
|
|
113
|
+
|
|
114
|
+
- Web: https://tradingnexus.pro/developers
|
|
115
|
+
- Markdown: `docs/Personal_Trade_API_Reference.md`
|
|
116
|
+
|
|
117
|
+
## PyPI (later)
|
|
118
|
+
|
|
119
|
+
When you approve publishing:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install tradingnexus
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Until then, use `pip install -e .` from `sdks/python`.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Trading Nexus — Python client (draft)
|
|
2
|
+
|
|
3
|
+
Official-style Python helper for the **Personal Trade API**
|
|
4
|
+
(own-account algo / bot access).
|
|
5
|
+
|
|
6
|
+
> Status: **v0.1.0 draft** — not published to PyPI yet. Install from this folder for testing.
|
|
7
|
+
|
|
8
|
+
## Install (local)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd sdks/python
|
|
12
|
+
pip install -e .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from tradingnexus import TradeClient
|
|
19
|
+
|
|
20
|
+
client = TradeClient(api_key="tk_your_secret_key_here")
|
|
21
|
+
|
|
22
|
+
# 1) Find the exact option / equity contract
|
|
23
|
+
found = client.search_instruments(
|
|
24
|
+
underlying="NIFTY",
|
|
25
|
+
exchange_segment="NSE_FNO",
|
|
26
|
+
symbol="24000",
|
|
27
|
+
limit=50,
|
|
28
|
+
)
|
|
29
|
+
row = next(
|
|
30
|
+
(
|
|
31
|
+
i
|
|
32
|
+
for i in found.get("instruments", [])
|
|
33
|
+
if str(i.get("option_type", "")).upper() == "PE"
|
|
34
|
+
and float(i.get("strike_price") or 0) == 24000
|
|
35
|
+
),
|
|
36
|
+
None,
|
|
37
|
+
)
|
|
38
|
+
if not row:
|
|
39
|
+
raise SystemExit("Contract not found — refine search / expiry")
|
|
40
|
+
|
|
41
|
+
# 2) Place order using returned fields
|
|
42
|
+
order = client.place_order(
|
|
43
|
+
side="BUY",
|
|
44
|
+
quantity=int(row["lot_size"]),
|
|
45
|
+
order_type="MARKET",
|
|
46
|
+
product_type="MIS",
|
|
47
|
+
symbol=row["symbol"],
|
|
48
|
+
exchange_segment=row["exchange_segment"],
|
|
49
|
+
security_id=row.get("security_id") or row.get("instrument_token"),
|
|
50
|
+
idempotency_key="bot-demo-001",
|
|
51
|
+
)
|
|
52
|
+
print(order)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Live order updates
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from tradingnexus import OrderStream
|
|
59
|
+
|
|
60
|
+
def on_message(msg):
|
|
61
|
+
print(msg)
|
|
62
|
+
|
|
63
|
+
stream = OrderStream(api_key="tk_your_secret_key_here", on_message=on_message)
|
|
64
|
+
stream.start()
|
|
65
|
+
# ... keep process alive ...
|
|
66
|
+
# stream.stop()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What this covers
|
|
70
|
+
|
|
71
|
+
| Client method | API |
|
|
72
|
+
|---------------|-----|
|
|
73
|
+
| `search_instruments` / `get_instrument` | Instruments |
|
|
74
|
+
| `place_order` / `modify_order` / `cancel_order` | Orders write |
|
|
75
|
+
| `get_order` / `list_orders` | Orders read |
|
|
76
|
+
| `list_trades` / `list_trades_for_order` | Fills |
|
|
77
|
+
| `list_positions` | Positions |
|
|
78
|
+
| `OrderStream` | WebSocket `/personal/ws/orders` |
|
|
79
|
+
|
|
80
|
+
## Auth notes
|
|
81
|
+
|
|
82
|
+
- Header: `X-PERSONAL-KEY` (server also accepts `X-API-KEY`)
|
|
83
|
+
- IP allowlist is **optional per key** (Super Admin → API Access)
|
|
84
|
+
- Keys are issued only by Trading Nexus Super Admin — no self-service
|
|
85
|
+
|
|
86
|
+
## Docs
|
|
87
|
+
|
|
88
|
+
- Web: https://tradingnexus.pro/developers
|
|
89
|
+
- Markdown: `docs/Personal_Trade_API_Reference.md`
|
|
90
|
+
|
|
91
|
+
## PyPI (later)
|
|
92
|
+
|
|
93
|
+
When you approve publishing:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install tradingnexus
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Until then, use `pip install -e .` from `sdks/python`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tradingnexus"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the Trading Nexus Personal Trade API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "Trading Nexus" }]
|
|
13
|
+
keywords = ["trading", "tradingnexus", "algo", "orders", "indian-markets"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"requests>=2.28.0",
|
|
26
|
+
"websocket-client>=1.6.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=7.0", "responses>=0.25"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://tradingnexus.pro/developers"
|
|
34
|
+
Documentation = "https://tradingnexus.pro/developers"
|
|
35
|
+
Source = "https://github.com/tradingnexus"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["tradingnexus*"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trading Nexus Personal Trade API — Python client.
|
|
3
|
+
|
|
4
|
+
Install (from this folder, for local draft use):
|
|
5
|
+
pip install -e .
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from tradingnexus import TradeClient
|
|
9
|
+
|
|
10
|
+
client = TradeClient(api_key="tk_...")
|
|
11
|
+
instruments = client.search_instruments(underlying="NIFTY", exchange_segment="NSE_FNO", symbol="24000")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .client import TradeClient
|
|
15
|
+
from .errors import (
|
|
16
|
+
ApiError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
TradeApiError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
from .websocket import OrderStream
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"TradeClient",
|
|
27
|
+
"OrderStream",
|
|
28
|
+
"TradeApiError",
|
|
29
|
+
"ApiError",
|
|
30
|
+
"AuthenticationError",
|
|
31
|
+
"ValidationError",
|
|
32
|
+
"NotFoundError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Synchronous REST client for Trading Nexus Personal Trade API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional
|
|
6
|
+
from urllib.parse import urljoin
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .errors import (
|
|
11
|
+
ApiError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
TradeApiError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.tradingnexus.pro/api/v2"
|
|
20
|
+
DEFAULT_TIMEOUT = 15.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TradeClient:
|
|
24
|
+
"""
|
|
25
|
+
Personal Trade API client (own account only).
|
|
26
|
+
|
|
27
|
+
Auth:
|
|
28
|
+
- Header X-PERSONAL-KEY (preferred)
|
|
29
|
+
- X-API-KEY also accepted by the server
|
|
30
|
+
|
|
31
|
+
IP restriction is configured by Super Admin per key (ON allowlist / OFF any IP).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: str,
|
|
37
|
+
*,
|
|
38
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
39
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
40
|
+
session: Optional[requests.Session] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
key = (api_key or "").strip()
|
|
43
|
+
if not key:
|
|
44
|
+
raise AuthenticationError("api_key is required")
|
|
45
|
+
|
|
46
|
+
self.api_key = key
|
|
47
|
+
self.base_url = base_url.rstrip("/") + "/"
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self._session = session or requests.Session()
|
|
50
|
+
self._session.headers.update(
|
|
51
|
+
{
|
|
52
|
+
"X-PERSONAL-KEY": self.api_key,
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ── Instruments ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def search_instruments(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
symbol: Optional[str] = None,
|
|
63
|
+
exchange_segment: Optional[str] = None,
|
|
64
|
+
underlying: Optional[str] = None,
|
|
65
|
+
offset: int = 0,
|
|
66
|
+
limit: int = 100,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""GET /personal/instruments — search before placing F&O / equity orders."""
|
|
69
|
+
params: Dict[str, Any] = {"offset": offset, "limit": limit}
|
|
70
|
+
if symbol:
|
|
71
|
+
params["symbol"] = symbol
|
|
72
|
+
if exchange_segment:
|
|
73
|
+
params["exchange_segment"] = exchange_segment
|
|
74
|
+
if underlying:
|
|
75
|
+
params["underlying"] = underlying
|
|
76
|
+
return self._request("GET", "personal/instruments", params=params)
|
|
77
|
+
|
|
78
|
+
def get_instrument(self, symbol: str) -> Dict[str, Any]:
|
|
79
|
+
"""GET /personal/instruments/{symbol} — exact symbol match."""
|
|
80
|
+
return self._request("GET", f"personal/instruments/{symbol}")
|
|
81
|
+
|
|
82
|
+
# ── Orders ───────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def place_order(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
side: str,
|
|
88
|
+
quantity: int,
|
|
89
|
+
order_type: str = "MARKET",
|
|
90
|
+
product_type: str = "NORMAL",
|
|
91
|
+
symbol: Optional[str] = None,
|
|
92
|
+
exchange_segment: Optional[str] = None,
|
|
93
|
+
security_id: Optional[int] = None,
|
|
94
|
+
instrument_token: Optional[int] = None,
|
|
95
|
+
price: Optional[float] = None,
|
|
96
|
+
trigger_price: Optional[float] = None,
|
|
97
|
+
expiry_date: Optional[str] = None,
|
|
98
|
+
idempotency_key: Optional[str] = None,
|
|
99
|
+
transaction_type: Optional[str] = None,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
"""
|
|
102
|
+
POST /personal/orders
|
|
103
|
+
|
|
104
|
+
Prefer security_id from search_instruments(). Always pass a unique
|
|
105
|
+
idempotency_key for each new order intent.
|
|
106
|
+
"""
|
|
107
|
+
body: Dict[str, Any] = {
|
|
108
|
+
"side": side,
|
|
109
|
+
"quantity": quantity,
|
|
110
|
+
"order_type": order_type,
|
|
111
|
+
"product_type": product_type,
|
|
112
|
+
}
|
|
113
|
+
if transaction_type is not None:
|
|
114
|
+
body["transaction_type"] = transaction_type
|
|
115
|
+
if symbol is not None:
|
|
116
|
+
body["symbol"] = symbol
|
|
117
|
+
if exchange_segment is not None:
|
|
118
|
+
body["exchange_segment"] = exchange_segment
|
|
119
|
+
if security_id is not None:
|
|
120
|
+
body["security_id"] = security_id
|
|
121
|
+
if instrument_token is not None:
|
|
122
|
+
body["instrument_token"] = instrument_token
|
|
123
|
+
if price is not None:
|
|
124
|
+
body["price"] = price
|
|
125
|
+
if trigger_price is not None:
|
|
126
|
+
body["trigger_price"] = trigger_price
|
|
127
|
+
if expiry_date is not None:
|
|
128
|
+
body["expiry_date"] = expiry_date
|
|
129
|
+
if idempotency_key is not None:
|
|
130
|
+
body["idempotency_key"] = idempotency_key
|
|
131
|
+
return self._request("POST", "personal/orders", json=body)
|
|
132
|
+
|
|
133
|
+
def get_order(self, order_id: str) -> Dict[str, Any]:
|
|
134
|
+
"""GET /personal/orders/{order_id}"""
|
|
135
|
+
return self._request("GET", f"personal/orders/{order_id}")
|
|
136
|
+
|
|
137
|
+
def list_orders(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
from_date: Optional[str] = None,
|
|
141
|
+
to_date: Optional[str] = None,
|
|
142
|
+
status_filter: Optional[str] = None,
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""GET /personal/orders — max 500 rows."""
|
|
145
|
+
params: Dict[str, Any] = {}
|
|
146
|
+
if from_date:
|
|
147
|
+
params["from_date"] = from_date
|
|
148
|
+
if to_date:
|
|
149
|
+
params["to_date"] = to_date
|
|
150
|
+
if status_filter:
|
|
151
|
+
params["status_filter"] = status_filter
|
|
152
|
+
return self._request("GET", "personal/orders", params=params)
|
|
153
|
+
|
|
154
|
+
def modify_order(
|
|
155
|
+
self,
|
|
156
|
+
order_id: str,
|
|
157
|
+
*,
|
|
158
|
+
quantity: Optional[int] = None,
|
|
159
|
+
price: Optional[float] = None,
|
|
160
|
+
trigger_price: Optional[float] = None,
|
|
161
|
+
order_type: Optional[str] = None,
|
|
162
|
+
product_type: Optional[str] = None,
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
"""PATCH /personal/orders/{order_id}"""
|
|
165
|
+
body: Dict[str, Any] = {}
|
|
166
|
+
if quantity is not None:
|
|
167
|
+
body["quantity"] = quantity
|
|
168
|
+
if price is not None:
|
|
169
|
+
body["price"] = price
|
|
170
|
+
if trigger_price is not None:
|
|
171
|
+
body["trigger_price"] = trigger_price
|
|
172
|
+
if order_type is not None:
|
|
173
|
+
body["order_type"] = order_type
|
|
174
|
+
if product_type is not None:
|
|
175
|
+
body["product_type"] = product_type
|
|
176
|
+
if not body:
|
|
177
|
+
raise ValidationError("Provide at least one field to modify")
|
|
178
|
+
return self._request("PATCH", f"personal/orders/{order_id}", json=body)
|
|
179
|
+
|
|
180
|
+
def cancel_order(self, order_id: str) -> Dict[str, Any]:
|
|
181
|
+
"""DELETE /personal/orders/{order_id}"""
|
|
182
|
+
return self._request("DELETE", f"personal/orders/{order_id}")
|
|
183
|
+
|
|
184
|
+
# ── Trades & positions ───────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def list_trades(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
from_date: Optional[str] = None,
|
|
190
|
+
to_date: Optional[str] = None,
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""GET /personal/trades"""
|
|
193
|
+
params: Dict[str, Any] = {}
|
|
194
|
+
if from_date:
|
|
195
|
+
params["from_date"] = from_date
|
|
196
|
+
if to_date:
|
|
197
|
+
params["to_date"] = to_date
|
|
198
|
+
return self._request("GET", "personal/trades", params=params)
|
|
199
|
+
|
|
200
|
+
def list_trades_for_order(self, order_id: str) -> Dict[str, Any]:
|
|
201
|
+
"""GET /personal/trades/{order_id}"""
|
|
202
|
+
return self._request("GET", f"personal/trades/{order_id}")
|
|
203
|
+
|
|
204
|
+
def list_positions(self) -> Dict[str, Any]:
|
|
205
|
+
"""GET /personal/positions"""
|
|
206
|
+
return self._request("GET", "personal/positions")
|
|
207
|
+
|
|
208
|
+
# ── Internals ────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
def _request(
|
|
211
|
+
self,
|
|
212
|
+
method: str,
|
|
213
|
+
path: str,
|
|
214
|
+
*,
|
|
215
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
216
|
+
json: Optional[Mapping[str, Any]] = None,
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
url = urljoin(self.base_url, path.lstrip("/"))
|
|
219
|
+
headers = {}
|
|
220
|
+
if json is not None:
|
|
221
|
+
headers["Content-Type"] = "application/json"
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
response = self._session.request(
|
|
225
|
+
method=method.upper(),
|
|
226
|
+
url=url,
|
|
227
|
+
params=params,
|
|
228
|
+
json=json,
|
|
229
|
+
headers=headers or None,
|
|
230
|
+
timeout=self.timeout,
|
|
231
|
+
)
|
|
232
|
+
except requests.RequestException as exc:
|
|
233
|
+
raise ApiError(f"Network error: {exc}") from exc
|
|
234
|
+
|
|
235
|
+
return self._parse_response(response)
|
|
236
|
+
|
|
237
|
+
def _parse_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
238
|
+
status = response.status_code
|
|
239
|
+
try:
|
|
240
|
+
payload: Any = response.json() if response.content else {}
|
|
241
|
+
except ValueError:
|
|
242
|
+
payload = {"detail": response.text}
|
|
243
|
+
|
|
244
|
+
if 200 <= status < 300:
|
|
245
|
+
if isinstance(payload, dict):
|
|
246
|
+
return payload
|
|
247
|
+
return {"data": payload}
|
|
248
|
+
|
|
249
|
+
message = _extract_message(payload) or response.reason or "Request failed"
|
|
250
|
+
retry_after = None
|
|
251
|
+
if status == 429:
|
|
252
|
+
raw = response.headers.get("Retry-After")
|
|
253
|
+
if raw and str(raw).isdigit():
|
|
254
|
+
retry_after = int(raw)
|
|
255
|
+
if isinstance(payload, dict):
|
|
256
|
+
detail = payload.get("detail")
|
|
257
|
+
if isinstance(detail, dict) and detail.get("retry_after_seconds") is not None:
|
|
258
|
+
try:
|
|
259
|
+
retry_after = int(detail["retry_after_seconds"])
|
|
260
|
+
except (TypeError, ValueError):
|
|
261
|
+
pass
|
|
262
|
+
raise RateLimitError(
|
|
263
|
+
message,
|
|
264
|
+
status_code=status,
|
|
265
|
+
payload=payload,
|
|
266
|
+
retry_after_seconds=retry_after,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
error_cls = _error_class_for_status(status)
|
|
270
|
+
raise error_cls(message, status_code=status, payload=payload)
|
|
271
|
+
|
|
272
|
+
def close(self) -> None:
|
|
273
|
+
self._session.close()
|
|
274
|
+
|
|
275
|
+
def __enter__(self) -> "TradeClient":
|
|
276
|
+
return self
|
|
277
|
+
|
|
278
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
279
|
+
self.close()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _extract_message(payload: Any) -> str:
|
|
283
|
+
if payload is None:
|
|
284
|
+
return ""
|
|
285
|
+
if isinstance(payload, str):
|
|
286
|
+
return payload
|
|
287
|
+
if isinstance(payload, dict):
|
|
288
|
+
detail = payload.get("detail", payload.get("message", payload.get("error")))
|
|
289
|
+
if isinstance(detail, str):
|
|
290
|
+
return detail
|
|
291
|
+
if isinstance(detail, dict):
|
|
292
|
+
return str(detail.get("message") or detail.get("error") or detail)
|
|
293
|
+
if detail is not None:
|
|
294
|
+
return str(detail)
|
|
295
|
+
return str(payload)
|
|
296
|
+
return str(payload)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _error_class_for_status(status: int) -> type[TradeApiError]:
|
|
300
|
+
if status in (401, 403):
|
|
301
|
+
return AuthenticationError
|
|
302
|
+
if status == 404:
|
|
303
|
+
return NotFoundError
|
|
304
|
+
if status == 400 or status == 409:
|
|
305
|
+
return ValidationError
|
|
306
|
+
if status == 429:
|
|
307
|
+
return RateLimitError
|
|
308
|
+
return ApiError
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Exceptions for the Trading Nexus Personal Trade API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TradeApiError(Exception):
|
|
9
|
+
"""Base error for all Trading Nexus client failures."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status_code: Optional[int] = None,
|
|
16
|
+
payload: Any = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.message = message
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.payload = payload
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
if self.status_code is None:
|
|
25
|
+
return self.message
|
|
26
|
+
return f"[{self.status_code}] {self.message}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthenticationError(TradeApiError):
|
|
30
|
+
"""Missing / invalid / revoked API key, or IP not allowed when restriction is ON."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValidationError(TradeApiError):
|
|
34
|
+
"""Bad request input (lot size, price, missing fields, etc.)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NotFoundError(TradeApiError):
|
|
38
|
+
"""Order / instrument / resource not found for this account."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RateLimitError(TradeApiError):
|
|
42
|
+
"""HTTP 429 — slow down; honour retry_after_seconds when present."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
message: str,
|
|
47
|
+
*,
|
|
48
|
+
status_code: int = 429,
|
|
49
|
+
payload: Any = None,
|
|
50
|
+
retry_after_seconds: Optional[int] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
super().__init__(message, status_code=status_code, payload=payload)
|
|
53
|
+
self.retry_after_seconds = retry_after_seconds
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ApiError(TradeApiError):
|
|
57
|
+
"""Unexpected API / network failure."""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""WebSocket helper for live Personal Trade API order / fill updates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any, Callable, Dict, Optional
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
from .errors import AuthenticationError, TradeApiError
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import websocket
|
|
14
|
+
except ImportError as exc: # pragma: no cover
|
|
15
|
+
raise ImportError(
|
|
16
|
+
"websocket-client is required for OrderStream. Install with: pip install websocket-client"
|
|
17
|
+
) from exc
|
|
18
|
+
|
|
19
|
+
MessageHandler = Callable[[Dict[str, Any]], None]
|
|
20
|
+
ErrorHandler = Callable[[Exception], None]
|
|
21
|
+
|
|
22
|
+
DEFAULT_WS_BASE = "wss://api.tradingnexus.pro/api/v2/personal/ws/orders"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OrderStream:
|
|
26
|
+
"""
|
|
27
|
+
Live order + trade updates for the key owner's account.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
def on_message(msg):
|
|
31
|
+
print(msg)
|
|
32
|
+
|
|
33
|
+
stream = OrderStream(api_key="tk_...", on_message=on_message)
|
|
34
|
+
stream.start()
|
|
35
|
+
...
|
|
36
|
+
stream.stop()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: str,
|
|
42
|
+
*,
|
|
43
|
+
ws_url: str = DEFAULT_WS_BASE,
|
|
44
|
+
on_message: Optional[MessageHandler] = None,
|
|
45
|
+
on_error: Optional[ErrorHandler] = None,
|
|
46
|
+
on_open: Optional[Callable[[], None]] = None,
|
|
47
|
+
on_close: Optional[Callable[[], None]] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
key = (api_key or "").strip()
|
|
50
|
+
if not key:
|
|
51
|
+
raise AuthenticationError("api_key is required")
|
|
52
|
+
|
|
53
|
+
self.api_key = key
|
|
54
|
+
query = urlencode({"api_key": self.api_key})
|
|
55
|
+
sep = "&" if "?" in ws_url else "?"
|
|
56
|
+
self.ws_url = f"{ws_url}{sep}{query}"
|
|
57
|
+
self.on_message = on_message
|
|
58
|
+
self.on_error = on_error
|
|
59
|
+
self.on_open = on_open
|
|
60
|
+
self.on_close = on_close
|
|
61
|
+
|
|
62
|
+
self._ws: Optional[websocket.WebSocketApp] = None
|
|
63
|
+
self._thread: Optional[threading.Thread] = None
|
|
64
|
+
self._running = False
|
|
65
|
+
|
|
66
|
+
def start(self, *, ping_interval: int = 20, ping_timeout: int = 10) -> None:
|
|
67
|
+
if self._running:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
def _on_message(_ws, message: str) -> None:
|
|
71
|
+
try:
|
|
72
|
+
payload = json.loads(message)
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
payload = {"type": "raw", "data": message}
|
|
75
|
+
if self.on_message:
|
|
76
|
+
self.on_message(payload)
|
|
77
|
+
|
|
78
|
+
def _on_error(_ws, error: Any) -> None:
|
|
79
|
+
exc = error if isinstance(error, Exception) else TradeApiError(str(error))
|
|
80
|
+
if self.on_error:
|
|
81
|
+
self.on_error(exc)
|
|
82
|
+
|
|
83
|
+
def _on_open(_ws) -> None:
|
|
84
|
+
if self.on_open:
|
|
85
|
+
self.on_open()
|
|
86
|
+
|
|
87
|
+
def _on_close(_ws, _status_code, _msg) -> None:
|
|
88
|
+
self._running = False
|
|
89
|
+
if self.on_close:
|
|
90
|
+
self.on_close()
|
|
91
|
+
|
|
92
|
+
self._ws = websocket.WebSocketApp(
|
|
93
|
+
self.ws_url,
|
|
94
|
+
header=[f"X-PERSONAL-KEY: {self.api_key}"],
|
|
95
|
+
on_message=_on_message,
|
|
96
|
+
on_error=_on_error,
|
|
97
|
+
on_open=_on_open,
|
|
98
|
+
on_close=_on_close,
|
|
99
|
+
)
|
|
100
|
+
self._running = True
|
|
101
|
+
self._thread = threading.Thread(
|
|
102
|
+
target=self._ws.run_forever,
|
|
103
|
+
kwargs={"ping_interval": ping_interval, "ping_timeout": ping_timeout},
|
|
104
|
+
name="tradingnexus-order-stream",
|
|
105
|
+
daemon=True,
|
|
106
|
+
)
|
|
107
|
+
self._thread.start()
|
|
108
|
+
|
|
109
|
+
def stop(self) -> None:
|
|
110
|
+
self._running = False
|
|
111
|
+
if self._ws is not None:
|
|
112
|
+
try:
|
|
113
|
+
self._ws.close()
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
if self._thread is not None and self._thread.is_alive():
|
|
117
|
+
self._thread.join(timeout=5)
|
|
118
|
+
self._thread = None
|
|
119
|
+
self._ws = None
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def is_running(self) -> bool:
|
|
123
|
+
return self._running
|
|
124
|
+
|
|
125
|
+
def __enter__(self) -> "OrderStream":
|
|
126
|
+
self.start()
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
130
|
+
self.stop()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tradingnexus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Trading Nexus Personal Trade API
|
|
5
|
+
Author: Trading Nexus
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://tradingnexus.pro/developers
|
|
8
|
+
Project-URL: Documentation, https://tradingnexus.pro/developers
|
|
9
|
+
Project-URL: Source, https://github.com/tradingnexus
|
|
10
|
+
Keywords: trading,tradingnexus,algo,orders,indian-markets
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: requests>=2.28.0
|
|
22
|
+
Requires-Dist: websocket-client>=1.6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Trading Nexus — Python client (draft)
|
|
28
|
+
|
|
29
|
+
Official-style Python helper for the **Personal Trade API**
|
|
30
|
+
(own-account algo / bot access).
|
|
31
|
+
|
|
32
|
+
> Status: **v0.1.0 draft** — not published to PyPI yet. Install from this folder for testing.
|
|
33
|
+
|
|
34
|
+
## Install (local)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd sdks/python
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from tradingnexus import TradeClient
|
|
45
|
+
|
|
46
|
+
client = TradeClient(api_key="tk_your_secret_key_here")
|
|
47
|
+
|
|
48
|
+
# 1) Find the exact option / equity contract
|
|
49
|
+
found = client.search_instruments(
|
|
50
|
+
underlying="NIFTY",
|
|
51
|
+
exchange_segment="NSE_FNO",
|
|
52
|
+
symbol="24000",
|
|
53
|
+
limit=50,
|
|
54
|
+
)
|
|
55
|
+
row = next(
|
|
56
|
+
(
|
|
57
|
+
i
|
|
58
|
+
for i in found.get("instruments", [])
|
|
59
|
+
if str(i.get("option_type", "")).upper() == "PE"
|
|
60
|
+
and float(i.get("strike_price") or 0) == 24000
|
|
61
|
+
),
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
if not row:
|
|
65
|
+
raise SystemExit("Contract not found — refine search / expiry")
|
|
66
|
+
|
|
67
|
+
# 2) Place order using returned fields
|
|
68
|
+
order = client.place_order(
|
|
69
|
+
side="BUY",
|
|
70
|
+
quantity=int(row["lot_size"]),
|
|
71
|
+
order_type="MARKET",
|
|
72
|
+
product_type="MIS",
|
|
73
|
+
symbol=row["symbol"],
|
|
74
|
+
exchange_segment=row["exchange_segment"],
|
|
75
|
+
security_id=row.get("security_id") or row.get("instrument_token"),
|
|
76
|
+
idempotency_key="bot-demo-001",
|
|
77
|
+
)
|
|
78
|
+
print(order)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Live order updates
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from tradingnexus import OrderStream
|
|
85
|
+
|
|
86
|
+
def on_message(msg):
|
|
87
|
+
print(msg)
|
|
88
|
+
|
|
89
|
+
stream = OrderStream(api_key="tk_your_secret_key_here", on_message=on_message)
|
|
90
|
+
stream.start()
|
|
91
|
+
# ... keep process alive ...
|
|
92
|
+
# stream.stop()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## What this covers
|
|
96
|
+
|
|
97
|
+
| Client method | API |
|
|
98
|
+
|---------------|-----|
|
|
99
|
+
| `search_instruments` / `get_instrument` | Instruments |
|
|
100
|
+
| `place_order` / `modify_order` / `cancel_order` | Orders write |
|
|
101
|
+
| `get_order` / `list_orders` | Orders read |
|
|
102
|
+
| `list_trades` / `list_trades_for_order` | Fills |
|
|
103
|
+
| `list_positions` | Positions |
|
|
104
|
+
| `OrderStream` | WebSocket `/personal/ws/orders` |
|
|
105
|
+
|
|
106
|
+
## Auth notes
|
|
107
|
+
|
|
108
|
+
- Header: `X-PERSONAL-KEY` (server also accepts `X-API-KEY`)
|
|
109
|
+
- IP allowlist is **optional per key** (Super Admin → API Access)
|
|
110
|
+
- Keys are issued only by Trading Nexus Super Admin — no self-service
|
|
111
|
+
|
|
112
|
+
## Docs
|
|
113
|
+
|
|
114
|
+
- Web: https://tradingnexus.pro/developers
|
|
115
|
+
- Markdown: `docs/Personal_Trade_API_Reference.md`
|
|
116
|
+
|
|
117
|
+
## PyPI (later)
|
|
118
|
+
|
|
119
|
+
When you approve publishing:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install tradingnexus
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Until then, use `pip install -e .` from `sdks/python`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tradingnexus/__init__.py
|
|
4
|
+
tradingnexus/client.py
|
|
5
|
+
tradingnexus/errors.py
|
|
6
|
+
tradingnexus/websocket.py
|
|
7
|
+
tradingnexus.egg-info/PKG-INFO
|
|
8
|
+
tradingnexus.egg-info/SOURCES.txt
|
|
9
|
+
tradingnexus.egg-info/dependency_links.txt
|
|
10
|
+
tradingnexus.egg-info/requires.txt
|
|
11
|
+
tradingnexus.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tradingnexus
|