nepse-data-api 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.
- nepse_data_api/__init__.py +26 -0
- nepse_data_api/assets/css.wasm +0 -0
- nepse_data_api/cli.py +146 -0
- nepse_data_api/market.py +1007 -0
- nepse_data_api/security.py +178 -0
- nepse_data_api/version.py +3 -0
- nepse_data_api-0.1.0.dist-info/METADATA +248 -0
- nepse_data_api-0.1.0.dist-info/RECORD +12 -0
- nepse_data_api-0.1.0.dist-info/WHEEL +5 -0
- nepse_data_api-0.1.0.dist-info/entry_points.txt +2 -0
- nepse_data_api-0.1.0.dist-info/licenses/LICENSE +47 -0
- nepse_data_api-0.1.0.dist-info/top_level.txt +1 -0
nepse_data_api/market.py
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nepse-data - High-performance NEPSE Library
|
|
3
|
+
============================================
|
|
4
|
+
|
|
5
|
+
High-performance Python library for Nepal Stock Exchange (NEPSE) data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import pywasm
|
|
10
|
+
import time
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime, date, timedelta
|
|
13
|
+
import pathlib
|
|
14
|
+
from typing import Optional, Dict, Any, List
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
import asyncio
|
|
17
|
+
import aiohttp
|
|
18
|
+
import websockets
|
|
19
|
+
import urllib3
|
|
20
|
+
|
|
21
|
+
# Suppress SSL warnings
|
|
22
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
23
|
+
|
|
24
|
+
class CacheManager:
|
|
25
|
+
"""Simple caching layer to avoid repeated API calls"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, default_ttl: int = 30):
|
|
28
|
+
self._cache: Dict[str, tuple[Any, float]] = {}
|
|
29
|
+
self.default_ttl = default_ttl
|
|
30
|
+
|
|
31
|
+
def get(self, key: str) -> Optional[Any]:
|
|
32
|
+
"""Get cached value if not expired"""
|
|
33
|
+
if key in self._cache:
|
|
34
|
+
value, expires_at = self._cache[key]
|
|
35
|
+
if time.time() < expires_at:
|
|
36
|
+
return value
|
|
37
|
+
else:
|
|
38
|
+
del self._cache[key]
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def set(self, key: str, value: Any, ttl: Optional[int] = None):
|
|
42
|
+
"""Store value with expiration"""
|
|
43
|
+
ttl = ttl or self.default_ttl
|
|
44
|
+
expires_at = time.time() + ttl
|
|
45
|
+
self._cache[key] = (value, expires_at)
|
|
46
|
+
|
|
47
|
+
def clear(self):
|
|
48
|
+
"""Clear all cached data"""
|
|
49
|
+
self._cache.clear()
|
|
50
|
+
|
|
51
|
+
class NepseTokenParser:
|
|
52
|
+
"""Handles the WASM-based token decryption logic"""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
# Update path to use package relative location
|
|
56
|
+
import os
|
|
57
|
+
base_dir = pathlib.Path(__file__).parent
|
|
58
|
+
wasm_path = base_dir / "assets" / "css.wasm"
|
|
59
|
+
|
|
60
|
+
self.runtime = pywasm.core.Runtime()
|
|
61
|
+
self.wasm_module = self.runtime.instance_from_file(str(wasm_path))
|
|
62
|
+
|
|
63
|
+
def parse_token_response(self, response_data):
|
|
64
|
+
"""Reverse-engineered logic to descramble the token using WASM"""
|
|
65
|
+
try:
|
|
66
|
+
salts = [
|
|
67
|
+
int(response_data["salt1"]),
|
|
68
|
+
int(response_data["salt2"]),
|
|
69
|
+
int(response_data["salt3"]),
|
|
70
|
+
int(response_data["salt4"]),
|
|
71
|
+
int(response_data["salt5"]),
|
|
72
|
+
]
|
|
73
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
74
|
+
raise ValueError(f"Invalid salt data in response: {e}")
|
|
75
|
+
|
|
76
|
+
# Calculate indices for Access Token
|
|
77
|
+
n = self.runtime.invocate(self.wasm_module, "cdx", salts)[0]
|
|
78
|
+
l_index = self.runtime.invocate(self.wasm_module, "rdx", [salts[0], salts[1], salts[3], salts[2], salts[4]])[0]
|
|
79
|
+
o = self.runtime.invocate(self.wasm_module, "bdx", [salts[0], salts[1], salts[3], salts[2], salts[4]])[0]
|
|
80
|
+
p = self.runtime.invocate(self.wasm_module, "ndx", [salts[0], salts[1], salts[3], salts[2], salts[4]])[0]
|
|
81
|
+
q = self.runtime.invocate(self.wasm_module, "mdx", [salts[0], salts[1], salts[3], salts[2], salts[4]])[0]
|
|
82
|
+
|
|
83
|
+
# Calculate indices for Refresh Token
|
|
84
|
+
salts_reversed = [salts[1], salts[0], salts[2], salts[4], salts[3]]
|
|
85
|
+
a = self.runtime.invocate(self.wasm_module, "cdx", salts_reversed)[0]
|
|
86
|
+
b = self.runtime.invocate(self.wasm_module, "rdx", [salts[1], salts[0], salts[2], salts[3], salts[4]])[0]
|
|
87
|
+
c = self.runtime.invocate(self.wasm_module, "bdx", [salts[1], salts[0], salts[3], salts[2], salts[4]])[0]
|
|
88
|
+
d = self.runtime.invocate(self.wasm_module, "ndx", [salts[1], salts[0], salts[3], salts[2], salts[4]])[0]
|
|
89
|
+
e = self.runtime.invocate(self.wasm_module, "mdx", [salts[1], salts[0], salts[3], salts[2], salts[4]])[0]
|
|
90
|
+
|
|
91
|
+
# Extract and descramble tokens
|
|
92
|
+
access_token = response_data["accessToken"]
|
|
93
|
+
refresh_token = response_data["refreshToken"]
|
|
94
|
+
|
|
95
|
+
parsed_access_token = (
|
|
96
|
+
access_token[0:n]
|
|
97
|
+
+ access_token[n + 1 : l_index]
|
|
98
|
+
+ access_token[l_index + 1 : o]
|
|
99
|
+
+ access_token[o + 1 : p]
|
|
100
|
+
+ access_token[p + 1 : q]
|
|
101
|
+
+ access_token[q + 1 :]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
parsed_refresh_token = (
|
|
105
|
+
refresh_token[0:a]
|
|
106
|
+
+ refresh_token[a + 1 : b]
|
|
107
|
+
+ refresh_token[b + 1 : c]
|
|
108
|
+
+ refresh_token[c + 1 : d]
|
|
109
|
+
+ refresh_token[d + 1 : e]
|
|
110
|
+
+ refresh_token[e + 1 :]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return parsed_access_token, parsed_refresh_token, salts
|
|
114
|
+
|
|
115
|
+
class Nepse:
|
|
116
|
+
"""
|
|
117
|
+
NEPSE Interface - High-performance Python API
|
|
118
|
+
=============================================
|
|
119
|
+
|
|
120
|
+
A robust, high-performance interface for Nepal Stock Exchange (NEPSE) data.
|
|
121
|
+
Features:
|
|
122
|
+
- Secure WASM Authentication
|
|
123
|
+
- Intelligent Caching
|
|
124
|
+
- Sync & Async Support
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
BASE_URL = "https://www.nepalstock.com.np"
|
|
128
|
+
|
|
129
|
+
def __init__(self, cache_ttl: int = 30, enable_cache: bool = True):
|
|
130
|
+
"""
|
|
131
|
+
Initialize with optional caching
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
cache_ttl: Cache time-to-live in seconds (default: 30)
|
|
135
|
+
enable_cache: Enable/disable caching (default: True)
|
|
136
|
+
"""
|
|
137
|
+
self.session = requests.Session()
|
|
138
|
+
self.token_parser = NepseTokenParser()
|
|
139
|
+
self.cache = CacheManager(cache_ttl) if enable_cache else None
|
|
140
|
+
|
|
141
|
+
# State
|
|
142
|
+
self.access_token = None
|
|
143
|
+
self.refresh_token = None
|
|
144
|
+
self.salts = None
|
|
145
|
+
self.token_timestamp = 0
|
|
146
|
+
|
|
147
|
+
# Configure session
|
|
148
|
+
self.session.verify = False
|
|
149
|
+
self.session.headers.update({
|
|
150
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"Referer": self.BASE_URL,
|
|
153
|
+
"Origin": self.BASE_URL
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# Auto-authenticate
|
|
157
|
+
self.authenticate()
|
|
158
|
+
|
|
159
|
+
def authenticate(self):
|
|
160
|
+
"""Fetch and process the scrambled token"""
|
|
161
|
+
url = f"{self.BASE_URL}/api/authenticate/prove"
|
|
162
|
+
response = self.session.get(url)
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
data = response.json()
|
|
165
|
+
|
|
166
|
+
self.access_token, self.refresh_token, self.salts = \
|
|
167
|
+
self.token_parser.parse_token_response(data)
|
|
168
|
+
self.token_timestamp = int(time.time())
|
|
169
|
+
|
|
170
|
+
def _get_auth_headers(self):
|
|
171
|
+
"""Construct headers with Salter authorization"""
|
|
172
|
+
if not self.access_token:
|
|
173
|
+
self.authenticate()
|
|
174
|
+
return {
|
|
175
|
+
"Authorization": f"Salter {self.access_token}",
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
"User-Agent": self.session.headers["User-Agent"]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def _cached_get(self, cache_key: str, url: str, ttl: Optional[int] = None):
|
|
181
|
+
"""Get with caching support"""
|
|
182
|
+
if self.cache:
|
|
183
|
+
cached = self.cache.get(cache_key)
|
|
184
|
+
if cached is not None:
|
|
185
|
+
return cached
|
|
186
|
+
|
|
187
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
data = response.json()
|
|
190
|
+
|
|
191
|
+
if self.cache:
|
|
192
|
+
self.cache.set(cache_key, data, ttl)
|
|
193
|
+
|
|
194
|
+
return data
|
|
195
|
+
|
|
196
|
+
# Core API methods with caching
|
|
197
|
+
|
|
198
|
+
def get_market_status(self, use_cache: bool = True):
|
|
199
|
+
"""Get market open/close status (cached for 60s)"""
|
|
200
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/market-open"
|
|
201
|
+
if use_cache and self.cache:
|
|
202
|
+
return self._cached_get("market_status", url, ttl=60)
|
|
203
|
+
|
|
204
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
205
|
+
response.raise_for_status()
|
|
206
|
+
return response.json()
|
|
207
|
+
|
|
208
|
+
def get_market_summary(self, use_cache: bool = True):
|
|
209
|
+
"""Get market summary (cached for 30s)"""
|
|
210
|
+
url = f"{self.BASE_URL}/api/nots/market-summary/"
|
|
211
|
+
if use_cache and self.cache:
|
|
212
|
+
return self._cached_get("market_summary", url)
|
|
213
|
+
|
|
214
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
215
|
+
response.raise_for_status()
|
|
216
|
+
return response.json()
|
|
217
|
+
|
|
218
|
+
def get_top_gainers(self, limit: Optional[int] = None, use_cache: bool = True):
|
|
219
|
+
"""Get top gainers (cached for 30s)"""
|
|
220
|
+
url = f"{self.BASE_URL}/api/nots/top-ten/top-gainer?all=false"
|
|
221
|
+
if use_cache and self.cache:
|
|
222
|
+
data = self._cached_get("top_gainers", url)
|
|
223
|
+
else:
|
|
224
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
225
|
+
data = response.json()
|
|
226
|
+
|
|
227
|
+
return data[:limit] if limit else data
|
|
228
|
+
|
|
229
|
+
def get_top_losers(self, limit: Optional[int] = None, use_cache: bool = True):
|
|
230
|
+
"""Get top losers (cached for 30s)"""
|
|
231
|
+
url = f"{self.BASE_URL}/api/nots/top-ten/top-loser?all=false"
|
|
232
|
+
if use_cache and self.cache:
|
|
233
|
+
data = self._cached_get("top_losers", url)
|
|
234
|
+
else:
|
|
235
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
236
|
+
data = response.json()
|
|
237
|
+
|
|
238
|
+
return data[:limit] if limit else data
|
|
239
|
+
|
|
240
|
+
def get_nepse_index(self, use_cache: bool = True):
|
|
241
|
+
"""Get NEPSE index data (cached for 30s)"""
|
|
242
|
+
url = f"{self.BASE_URL}/api/nots/nepse-index"
|
|
243
|
+
if use_cache and self.cache:
|
|
244
|
+
return self._cached_get("nepse_index", url)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
248
|
+
response.raise_for_status()
|
|
249
|
+
return response.json()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
# Handle empty or invalid JSON response
|
|
252
|
+
print(f"Error fetching NEPSE index: {e}")
|
|
253
|
+
return {}
|
|
254
|
+
|
|
255
|
+
def get_today_price(self, size: int = 500, date: str = None, use_cache: bool = True):
|
|
256
|
+
"""
|
|
257
|
+
Get today's price (Live Market Data) with OHLCV for all companies
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
size: Number of records to fetch (default: 500)
|
|
261
|
+
date: Optional business date (YYYY-MM-DD format)
|
|
262
|
+
use_cache: Whether to use caching
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List of dictionaries with complete OHLCV data:
|
|
266
|
+
- symbol, openPrice, highPrice, lowPrice, closePrice, totalTradeQuantity
|
|
267
|
+
"""
|
|
268
|
+
# Build URL with optional date parameter
|
|
269
|
+
date_param = f"&businessDate={date}" if date else ""
|
|
270
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/today-price?size={size}{date_param}"
|
|
271
|
+
|
|
272
|
+
cache_key = f"today_price_{date or 'live'}"
|
|
273
|
+
|
|
274
|
+
if use_cache and self.cache:
|
|
275
|
+
cached = self.cache.get(cache_key)
|
|
276
|
+
if cached is not None:
|
|
277
|
+
return cached
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# IMPORTANT: Use POST request with payload (not GET)
|
|
281
|
+
# This endpoint requires POST with payload ID
|
|
282
|
+
|
|
283
|
+
# Use requested business date for payload ID calculation if provided
|
|
284
|
+
# Use requested business date for payload ID calculation if provided
|
|
285
|
+
payload_date = None
|
|
286
|
+
if date:
|
|
287
|
+
try:
|
|
288
|
+
payload_date = datetime.strptime(date, "%Y-%m-%d")
|
|
289
|
+
except Exception:
|
|
290
|
+
# Fallback to now if date format is invalid
|
|
291
|
+
payload_date = datetime.now()
|
|
292
|
+
else:
|
|
293
|
+
# If no date provided, use the market "asOf" date to ensure we get data
|
|
294
|
+
# calling get_market_status to find the last trading day
|
|
295
|
+
try:
|
|
296
|
+
status = self.get_market_status()
|
|
297
|
+
if status and 'asOf' in status:
|
|
298
|
+
# asOf format: 2026-02-12T15:00:00
|
|
299
|
+
as_of_str = status['asOf'].split('T')[0]
|
|
300
|
+
payload_date = datetime.strptime(as_of_str, "%Y-%m-%d")
|
|
301
|
+
except Exception as e:
|
|
302
|
+
# Fallback to now
|
|
303
|
+
print(f"Error fetching market status date: {e}")
|
|
304
|
+
payload_date = datetime.now()
|
|
305
|
+
|
|
306
|
+
if not payload_date:
|
|
307
|
+
payload_date = datetime.now()
|
|
308
|
+
|
|
309
|
+
# Fix: Ensure URL has businessDate if we determined a specific date
|
|
310
|
+
date_str = payload_date.strftime("%Y-%m-%d")
|
|
311
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/today-price?size={size}&businessDate={date_str}"
|
|
312
|
+
|
|
313
|
+
payload_id = self._get_floorsheet_payload_id(0, payload_date)
|
|
314
|
+
payload = {"id": payload_id}
|
|
315
|
+
|
|
316
|
+
response = self.session.post(url, headers=self._get_auth_headers(), json=payload)
|
|
317
|
+
response.raise_for_status()
|
|
318
|
+
|
|
319
|
+
# API returns direct array (not wrapped in {content: []})
|
|
320
|
+
data = response.json()
|
|
321
|
+
|
|
322
|
+
if self.cache:
|
|
323
|
+
self.cache.set(cache_key, data, ttl=15) # Short TTL for live data
|
|
324
|
+
|
|
325
|
+
return data if isinstance(data, list) else []
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
print(f"Error fetching today price: {e}")
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
def get_stocks(self, date: str = None, use_cache: bool = True):
|
|
332
|
+
"""
|
|
333
|
+
Get stock market data (Live or Historical)
|
|
334
|
+
|
|
335
|
+
Returns: List of all stocks with OHLCV data
|
|
336
|
+
"""
|
|
337
|
+
if not date:
|
|
338
|
+
# Live market - fast, reliable, works 24/7
|
|
339
|
+
url = f"{self.BASE_URL}/api/nots/lives-market"
|
|
340
|
+
cache_key = "live_market"
|
|
341
|
+
|
|
342
|
+
if use_cache and self.cache:
|
|
343
|
+
cached = self.cache.get(cache_key)
|
|
344
|
+
if cached is not None:
|
|
345
|
+
return cached
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
349
|
+
response.raise_for_status()
|
|
350
|
+
stocks = response.json()
|
|
351
|
+
|
|
352
|
+
if self.cache:
|
|
353
|
+
self.cache.set(cache_key, stocks, ttl=15)
|
|
354
|
+
|
|
355
|
+
return stocks
|
|
356
|
+
except Exception as e:
|
|
357
|
+
print(f"Error: {e}")
|
|
358
|
+
return []
|
|
359
|
+
else:
|
|
360
|
+
# Historical data
|
|
361
|
+
return self.get_today_price(date=date, use_cache=use_cache)
|
|
362
|
+
|
|
363
|
+
def get_daily_trade(self, date: str, size: int = 500, use_cache: bool = True):
|
|
364
|
+
"""
|
|
365
|
+
Get daily trade data for a specific date (YYYY-MM-DD)
|
|
366
|
+
"""
|
|
367
|
+
# Fixed endpoint: /api/nots/securityDailyTradeDto/business-date/{date}
|
|
368
|
+
# Fixed params: size & page
|
|
369
|
+
url = f"{self.BASE_URL}/api/nots/securityDailyTradeDto/business-date/{date}?size={size}&page=0"
|
|
370
|
+
|
|
371
|
+
if use_cache and self.cache:
|
|
372
|
+
return self._cached_get(f"daily_trade_{date}", url)
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
376
|
+
|
|
377
|
+
# If 404, it might mean no data for that specific date (holiday?), try previous day?
|
|
378
|
+
# But let's just return empty for now to avoid loop
|
|
379
|
+
if response.status_code == 404:
|
|
380
|
+
print(f"No data found for date: {date}")
|
|
381
|
+
return []
|
|
382
|
+
|
|
383
|
+
response.raise_for_status()
|
|
384
|
+
return response.json().get('content', [])
|
|
385
|
+
except Exception as e:
|
|
386
|
+
print(f"Error fetching daily trade for {date}: {e}")
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
def get_price_volume(self, use_cache: bool = True):
|
|
390
|
+
"""
|
|
391
|
+
Get daily price/volume for all securities (Market Stats)
|
|
392
|
+
Endpoint: /api/nots/securityDailyTradeStat/58
|
|
393
|
+
"""
|
|
394
|
+
url = f"{self.BASE_URL}/api/nots/securityDailyTradeStat/58"
|
|
395
|
+
|
|
396
|
+
# This endpoint updates frequently when market is open
|
|
397
|
+
ttl = 15 if use_cache else 0
|
|
398
|
+
|
|
399
|
+
if use_cache and self.cache:
|
|
400
|
+
return self._cached_get("price_volume", url, ttl=ttl)
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
404
|
+
response.raise_for_status()
|
|
405
|
+
return response.json()
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(f"Error fetching price volume: {e}")
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
def get_sub_indices(self, use_cache: bool = True):
|
|
411
|
+
"""
|
|
412
|
+
Get NEPSE sub-indices (sector indices) with OHLCV data
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
List of sector indices with:
|
|
416
|
+
- index name, close, high, low, previousClose (open approx)
|
|
417
|
+
"""
|
|
418
|
+
url = f"{self.BASE_URL}/api/nots"
|
|
419
|
+
|
|
420
|
+
if use_cache and self.cache:
|
|
421
|
+
return self._cached_get("sub_indices", url, ttl=30)
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
425
|
+
response.raise_for_status()
|
|
426
|
+
return response.json()
|
|
427
|
+
except Exception as e:
|
|
428
|
+
print(f"Error fetching sub-indices: {e}")
|
|
429
|
+
return []
|
|
430
|
+
|
|
431
|
+
# --- TOP Performers ---
|
|
432
|
+
|
|
433
|
+
def get_top_turnover(self, use_cache: bool = True):
|
|
434
|
+
"""Get top 10 stocks by turnover"""
|
|
435
|
+
url = f"{self.BASE_URL}/api/nots/top-ten/turnover?all=false"
|
|
436
|
+
return self._cached_get("top_turnover", url, ttl=30) if use_cache else self.session.get(url, headers=self._get_auth_headers()).json()
|
|
437
|
+
|
|
438
|
+
def get_top_trade(self, use_cache: bool = True):
|
|
439
|
+
"""Get top 10 stocks by number of trades"""
|
|
440
|
+
url = f"{self.BASE_URL}/api/nots/top-ten/trade?all=false"
|
|
441
|
+
return self._cached_get("top_trade", url, ttl=30) if use_cache else self.session.get(url, headers=self._get_auth_headers()).json()
|
|
442
|
+
|
|
443
|
+
def get_top_transaction(self, use_cache: bool = True):
|
|
444
|
+
"""Get top 10 stocks by number of transactions"""
|
|
445
|
+
url = f"{self.BASE_URL}/api/nots/top-ten/transaction?all=false"
|
|
446
|
+
return self._cached_get("top_transaction", url, ttl=30) if use_cache else self.session.get(url, headers=self._get_auth_headers()).json()
|
|
447
|
+
|
|
448
|
+
# --- Market Metadata ---
|
|
449
|
+
|
|
450
|
+
def get_company_list(self, use_cache: bool = True):
|
|
451
|
+
"""Get list of all listed companies"""
|
|
452
|
+
url = f"{self.BASE_URL}/api/nots/company/list"
|
|
453
|
+
return self._cached_get("company_list", url, ttl=3600) if use_cache else self.session.get(url, headers=self._get_auth_headers()).json()
|
|
454
|
+
|
|
455
|
+
def get_security_list(self, use_cache: bool = True):
|
|
456
|
+
"""Get list of all securities (non-delisted)"""
|
|
457
|
+
url = f"{self.BASE_URL}/api/nots/security?nonDelisted=true"
|
|
458
|
+
return self._cached_get("security_list", url, ttl=3600) if use_cache else self.session.get(url, headers=self._get_auth_headers()).json()
|
|
459
|
+
|
|
460
|
+
# --- News & Corporate Actions ---
|
|
461
|
+
|
|
462
|
+
def get_news_alerts(self, use_cache: bool = True):
|
|
463
|
+
"""Get general market news and alerts"""
|
|
464
|
+
url = f"{self.BASE_URL}/api/nots/news/media/news-and-alerts"
|
|
465
|
+
return self._cached_get("news_alerts", url, ttl=300) if use_cache else self.session.get(url, headers=self._get_auth_headers()).json()
|
|
466
|
+
|
|
467
|
+
def get_company_news(self, symbol: str, use_cache: bool = True):
|
|
468
|
+
"""Get news for a specific company"""
|
|
469
|
+
self._ensure_security_ids()
|
|
470
|
+
company_id = self.security_id_map.get(symbol.upper())
|
|
471
|
+
if not company_id: return []
|
|
472
|
+
|
|
473
|
+
# Correct endpoint for specific company news
|
|
474
|
+
url = f"{self.BASE_URL}/api/nots/application/company-news/{company_id}"
|
|
475
|
+
|
|
476
|
+
# Cache key specific to company
|
|
477
|
+
if use_cache and self.cache:
|
|
478
|
+
return self._cached_get(f"news_{symbol.upper()}", url, ttl=300)
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
482
|
+
return response.json()
|
|
483
|
+
except Exception as e:
|
|
484
|
+
print(f"Error fetching news for {symbol}: {e}")
|
|
485
|
+
return []
|
|
486
|
+
|
|
487
|
+
def get_holiday_list(self, year: int = 2025, use_cache: bool = True):
|
|
488
|
+
"""
|
|
489
|
+
Get list of market holidays for specified year
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
year: Year to fetch holidays for (default: 2025)
|
|
493
|
+
use_cache: Whether to use caching (default: True)
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
List of holiday dictionaries with date and description
|
|
497
|
+
"""
|
|
498
|
+
url = f"{self.BASE_URL}/api/nots/holiday/list?year={year}"
|
|
499
|
+
cache_key = f"holidays_{year}"
|
|
500
|
+
|
|
501
|
+
if use_cache and self.cache:
|
|
502
|
+
return self._cached_get(cache_key, url, ttl=86400) # Cache for 24 hours
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
506
|
+
response.raise_for_status()
|
|
507
|
+
return response.json()
|
|
508
|
+
except Exception as e:
|
|
509
|
+
print(f"Error fetching holiday list for {year}: {e}")
|
|
510
|
+
return []
|
|
511
|
+
|
|
512
|
+
def get_sector_list(self, use_cache: bool = True):
|
|
513
|
+
"""Get complete list of all market sectors"""
|
|
514
|
+
url = f"{self.BASE_URL}/api/nots/sector"
|
|
515
|
+
if use_cache and self.cache:
|
|
516
|
+
return self._cached_get("sector_list", url, ttl=86400)
|
|
517
|
+
try:
|
|
518
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
519
|
+
response.raise_for_status()
|
|
520
|
+
return response.json()
|
|
521
|
+
except Exception as e:
|
|
522
|
+
print(f"Error: {e}")
|
|
523
|
+
return []
|
|
524
|
+
|
|
525
|
+
def get_all_indices(self, use_cache: bool = True):
|
|
526
|
+
"""Get all market indices in one call"""
|
|
527
|
+
url = f"{self.BASE_URL}/api/nots/index"
|
|
528
|
+
if use_cache and self.cache:
|
|
529
|
+
return self._cached_get("all_indices", url, ttl=30)
|
|
530
|
+
try:
|
|
531
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
532
|
+
response.raise_for_status()
|
|
533
|
+
return response.json()
|
|
534
|
+
except Exception as e:
|
|
535
|
+
print(f"Error: {e}")
|
|
536
|
+
return []
|
|
537
|
+
|
|
538
|
+
def get_security_details(self, security_id: int, use_cache: bool = True):
|
|
539
|
+
"""Get detailed info for specific security by ID"""
|
|
540
|
+
url = f"{self.BASE_URL}/api/nots/security/{security_id}"
|
|
541
|
+
cache_key = f"sec_details_{security_id}"
|
|
542
|
+
if use_cache and self.cache:
|
|
543
|
+
cached = self.cache.get(cache_key)
|
|
544
|
+
if cached: return cached
|
|
545
|
+
try:
|
|
546
|
+
# Changed from POST to GET based on audit
|
|
547
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
548
|
+
response.raise_for_status()
|
|
549
|
+
data = response.json()
|
|
550
|
+
if self.cache and use_cache:
|
|
551
|
+
self.cache.set(cache_key, data, ttl=3600)
|
|
552
|
+
return data
|
|
553
|
+
except Exception as e:
|
|
554
|
+
print(f"Error fetching security details for {security_id}: {e}")
|
|
555
|
+
return {}
|
|
556
|
+
|
|
557
|
+
def get_historical_chart(self, security_id: int, start_date: str = None, end_date: str = None, use_cache: bool = True):
|
|
558
|
+
"""
|
|
559
|
+
Get historical chart data for security/index
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
security_id: ID of the security or index (58 for NEPSE Index)
|
|
563
|
+
start_date: Start date (YYYY-MM-DD), optional for Index only
|
|
564
|
+
end_date: End date (YYYY-MM-DD), optional for Index only
|
|
565
|
+
|
|
566
|
+
Note:
|
|
567
|
+
- Index chart (58) supports date range filtering via startDate/endDate
|
|
568
|
+
- Company charts return full dataset; filtering must be done locally
|
|
569
|
+
"""
|
|
570
|
+
# Determine correct endpoint based on ID type
|
|
571
|
+
if security_id == 58: # NEPSE Index
|
|
572
|
+
# Index uses indexCode parameter, not /{id} path
|
|
573
|
+
if start_date and end_date:
|
|
574
|
+
url = f"{self.BASE_URL}/api/nots/graph/index?indexCode={security_id}&startDate={start_date}&endDate={end_date}"
|
|
575
|
+
else:
|
|
576
|
+
# Default to recent data if no dates provided
|
|
577
|
+
url = f"{self.BASE_URL}/api/nots/graph/index?indexCode={security_id}"
|
|
578
|
+
else:
|
|
579
|
+
# Company chart - no date params supported, returns full dataset
|
|
580
|
+
url = f"{self.BASE_URL}/api/nots/market/graphdata/{security_id}"
|
|
581
|
+
|
|
582
|
+
cache_key = f"chart_{security_id}_{start_date}_{end_date}"
|
|
583
|
+
if use_cache and self.cache:
|
|
584
|
+
cached = self.cache.get(cache_key)
|
|
585
|
+
if cached: return cached
|
|
586
|
+
try:
|
|
587
|
+
# Add timeout to prevent hanging (company charts can be slow/timeout)
|
|
588
|
+
response = self.session.get(url, headers=self._get_auth_headers(), timeout=30)
|
|
589
|
+
response.raise_for_status()
|
|
590
|
+
data = response.json()
|
|
591
|
+
|
|
592
|
+
# Local filtering for company charts if dates provided
|
|
593
|
+
if security_id != 58 and start_date and end_date and data:
|
|
594
|
+
from datetime import datetime
|
|
595
|
+
start_ts = int(datetime.strptime(start_date, "%Y-%m-%d").timestamp() * 1000)
|
|
596
|
+
end_ts = int(datetime.strptime(end_date, "%Y-%m-%d").timestamp() * 1000)
|
|
597
|
+
# Assuming data has 't' field for timestamp
|
|
598
|
+
data = [d for d in data if 't' in d and start_ts <= d['t'] <= end_ts]
|
|
599
|
+
|
|
600
|
+
if self.cache and use_cache:
|
|
601
|
+
self.cache.set(cache_key, data, ttl=1800)
|
|
602
|
+
return data
|
|
603
|
+
except Exception as e:
|
|
604
|
+
print(f"Error fetching chart for {security_id}: {e}")
|
|
605
|
+
return []
|
|
606
|
+
|
|
607
|
+
def get_press_releases(self, use_cache: bool = True):
|
|
608
|
+
"""Get official NEPSE press releases"""
|
|
609
|
+
url = f"{self.BASE_URL}/api/nots/news/press-release"
|
|
610
|
+
if use_cache and self.cache:
|
|
611
|
+
return self._cached_get("press_releases", url, ttl=3600)
|
|
612
|
+
try:
|
|
613
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
614
|
+
response.raise_for_status()
|
|
615
|
+
return response.json()
|
|
616
|
+
except Exception as e:
|
|
617
|
+
print(f"Error: {e}")
|
|
618
|
+
return []
|
|
619
|
+
|
|
620
|
+
def refresh_auth_token(self):
|
|
621
|
+
"""Manually refresh authentication token"""
|
|
622
|
+
url = f"{self.BASE_URL}/api/authenticate/refresh-token"
|
|
623
|
+
try:
|
|
624
|
+
# Fix: Send refresh token in the body
|
|
625
|
+
payload = {"refreshToken": self.refresh_token} if self.refresh_token else {}
|
|
626
|
+
response = self.session.post(url, headers=self._get_auth_headers(), json=payload)
|
|
627
|
+
response.raise_for_status()
|
|
628
|
+
token_data = response.json()
|
|
629
|
+
if token_data and 'accessToken' in token_data:
|
|
630
|
+
self.access_token = token_data['accessToken']
|
|
631
|
+
if 'serverTime' in token_data:
|
|
632
|
+
self.token_timestamp = int(token_data['serverTime'] / 1000)
|
|
633
|
+
if 'salt' in token_data:
|
|
634
|
+
self.salts = token_data['salt']
|
|
635
|
+
return token_data
|
|
636
|
+
except Exception as e:
|
|
637
|
+
print(f"Error refreshing token: {e}")
|
|
638
|
+
return {}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def get_dividends(self, symbol: str):
|
|
642
|
+
"""Get dividend history for a specific company"""
|
|
643
|
+
self._ensure_security_ids()
|
|
644
|
+
company_id = self.security_id_map.get(symbol.upper())
|
|
645
|
+
if not company_id: return []
|
|
646
|
+
url = f"{self.BASE_URL}/api/nots/application/dividend/{company_id}"
|
|
647
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
648
|
+
return response.json()
|
|
649
|
+
|
|
650
|
+
def get_agm(self, symbol: str):
|
|
651
|
+
"""Get AGM information for a specific company"""
|
|
652
|
+
self._ensure_security_ids()
|
|
653
|
+
company_id = self.security_id_map.get(symbol.upper())
|
|
654
|
+
if not company_id: return []
|
|
655
|
+
url = f"{self.BASE_URL}/api/nots/application/agm/{company_id}"
|
|
656
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
657
|
+
return response.json()
|
|
658
|
+
|
|
659
|
+
def get_market_depth(self, symbol: str):
|
|
660
|
+
"""
|
|
661
|
+
Get live market depth (Buy/Sell orders) for a specific Company
|
|
662
|
+
Endpoint: /api/nots/nepse-data/marketdepth/{id}
|
|
663
|
+
"""
|
|
664
|
+
try:
|
|
665
|
+
# 1. Get Company ID
|
|
666
|
+
symbol = symbol.upper()
|
|
667
|
+
self._ensure_security_ids()
|
|
668
|
+
company_id = self.security_id_map.get(symbol)
|
|
669
|
+
|
|
670
|
+
if not company_id:
|
|
671
|
+
print(f"Symbol {symbol} not found in security map.")
|
|
672
|
+
return {}
|
|
673
|
+
|
|
674
|
+
# 2. Fetch Depth
|
|
675
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/marketdepth/{company_id}"
|
|
676
|
+
response = self.session.get(url, headers=self._get_auth_headers())
|
|
677
|
+
response.raise_for_status()
|
|
678
|
+
return response.json()
|
|
679
|
+
except Exception as e:
|
|
680
|
+
print(f"Error fetching market depth for {symbol}: {e}")
|
|
681
|
+
return {}
|
|
682
|
+
|
|
683
|
+
def get_floorsheet(self, symbol: str = None, date: str = None, size: int = 500, limit: int = None, page: int = 0):
|
|
684
|
+
"""
|
|
685
|
+
Get floorsheet (transactions).
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
symbol: Optional stock symbol.
|
|
689
|
+
date: Optional date (YYYY-MM-DD). Default is today/latest session.
|
|
690
|
+
NOTE: Historical data is NOT available via this endpoint.
|
|
691
|
+
Requests for past dates will return the latest session data.
|
|
692
|
+
size: Page size. Default 500 (Fixed/Recommended).
|
|
693
|
+
limit: Maximum number of pages to fetch.
|
|
694
|
+
- If symbol is provided: Default is 0 (Fetch ALL).
|
|
695
|
+
- If symbol is NOT provided: Default is 1 (Fetch only first page).
|
|
696
|
+
page: Starting page number (0-indexed). Default is 0.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
List of transaction records from the LATEST trading session.
|
|
700
|
+
"""
|
|
701
|
+
try:
|
|
702
|
+
# Common setup
|
|
703
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/floorsheet"
|
|
704
|
+
headers = self._get_auth_headers()
|
|
705
|
+
headers.update({
|
|
706
|
+
"Host": "www.nepalstock.com.np",
|
|
707
|
+
"Origin": "https://www.nepalstock.com.np",
|
|
708
|
+
"Referer": "https://www.nepalstock.com.np/",
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
# --- Scenario 1: Specific Stock (Fetch ALL or limit) ---
|
|
712
|
+
if symbol:
|
|
713
|
+
symbol = symbol.upper()
|
|
714
|
+
self._ensure_security_ids()
|
|
715
|
+
company_id = self.security_id_map.get(symbol)
|
|
716
|
+
|
|
717
|
+
if not company_id:
|
|
718
|
+
print(f"Stock ID not found for {symbol}")
|
|
719
|
+
return []
|
|
720
|
+
|
|
721
|
+
payload_id = self._get_floorsheet_payload_id(company_id, datetime.now())
|
|
722
|
+
payload = {"id": payload_id}
|
|
723
|
+
|
|
724
|
+
all_records = []
|
|
725
|
+
current_top_page = page
|
|
726
|
+
pages_fetched = 0
|
|
727
|
+
|
|
728
|
+
# Default limit 0 means ALL for symbol
|
|
729
|
+
# If limit is None (not passed), we treat it as 0 (ALL) for symbol
|
|
730
|
+
effective_limit = limit if limit is not None else 0
|
|
731
|
+
|
|
732
|
+
while True:
|
|
733
|
+
# Construct URL for current page
|
|
734
|
+
params = f"size={size}&stockId={company_id}&sort=contractId,desc&page={current_top_page}"
|
|
735
|
+
if date:
|
|
736
|
+
params += f"&businessDate={date}"
|
|
737
|
+
|
|
738
|
+
full_url = f"{url}?{params}"
|
|
739
|
+
|
|
740
|
+
try:
|
|
741
|
+
response = self.session.post(full_url, headers=headers, json=payload)
|
|
742
|
+
response.raise_for_status()
|
|
743
|
+
data = response.json()
|
|
744
|
+
|
|
745
|
+
sheet_data = data.get('floorsheets', {})
|
|
746
|
+
content = sheet_data.get('content', [])
|
|
747
|
+
|
|
748
|
+
if not content:
|
|
749
|
+
break
|
|
750
|
+
|
|
751
|
+
all_records.extend(content)
|
|
752
|
+
pages_fetched += 1
|
|
753
|
+
|
|
754
|
+
# Check checks
|
|
755
|
+
total_pages = sheet_data.get('totalPages', 1)
|
|
756
|
+
if current_top_page >= total_pages - 1:
|
|
757
|
+
break
|
|
758
|
+
|
|
759
|
+
# Stop if we reached the requested limit (and limit > 0)
|
|
760
|
+
if effective_limit > 0 and pages_fetched >= effective_limit:
|
|
761
|
+
break
|
|
762
|
+
|
|
763
|
+
current_top_page += 1
|
|
764
|
+
time.sleep(0.1) # Be nice to the server
|
|
765
|
+
|
|
766
|
+
except Exception as e:
|
|
767
|
+
print(f"Error fetching page {current_top_page} for {symbol}: {e}")
|
|
768
|
+
break
|
|
769
|
+
|
|
770
|
+
return all_records
|
|
771
|
+
|
|
772
|
+
# --- Scenario 2: General Market (Fetch 1 or limit) ---
|
|
773
|
+
else:
|
|
774
|
+
# Use standard endpoint structure for general market
|
|
775
|
+
|
|
776
|
+
# Payload ID: Generic
|
|
777
|
+
payload_id = self._get_floorsheet_payload_id(0, datetime.now())
|
|
778
|
+
payload = {"id": payload_id}
|
|
779
|
+
|
|
780
|
+
all_records = []
|
|
781
|
+
current_top_page = page
|
|
782
|
+
pages_fetched = 0
|
|
783
|
+
|
|
784
|
+
# Default limit for general market is 1 if not specified
|
|
785
|
+
effective_limit = limit if limit is not None else 1
|
|
786
|
+
|
|
787
|
+
# If limit is 0 (fetch all for general market), allow it but it's loop-heavy
|
|
788
|
+
|
|
789
|
+
while True:
|
|
790
|
+
params = f"size={size}&sort=contractId,desc&page={current_top_page}"
|
|
791
|
+
if date:
|
|
792
|
+
params += f"&businessDate={date}"
|
|
793
|
+
|
|
794
|
+
full_url = f"{url}?{params}"
|
|
795
|
+
|
|
796
|
+
try:
|
|
797
|
+
response = self.session.post(full_url, headers=headers, json=payload)
|
|
798
|
+
response.raise_for_status()
|
|
799
|
+
|
|
800
|
+
data = response.json()
|
|
801
|
+
sheet_data = data.get('floorsheets', {})
|
|
802
|
+
content = sheet_data.get('content', [])
|
|
803
|
+
|
|
804
|
+
if not content:
|
|
805
|
+
break
|
|
806
|
+
|
|
807
|
+
all_records.extend(content)
|
|
808
|
+
pages_fetched += 1
|
|
809
|
+
|
|
810
|
+
total_pages = sheet_data.get('totalPages', 1)
|
|
811
|
+
if current_top_page >= total_pages - 1:
|
|
812
|
+
break
|
|
813
|
+
|
|
814
|
+
if effective_limit > 0 and pages_fetched >= effective_limit:
|
|
815
|
+
break
|
|
816
|
+
|
|
817
|
+
current_top_page += 1
|
|
818
|
+
time.sleep(0.5) # Generic market needs slower pacing
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
print(f"Error fetching page {current_top_page}: {e}")
|
|
822
|
+
break
|
|
823
|
+
|
|
824
|
+
return all_records
|
|
825
|
+
|
|
826
|
+
except Exception as e:
|
|
827
|
+
print(f"Error fetching floorsheet: {e}")
|
|
828
|
+
return []
|
|
829
|
+
|
|
830
|
+
def _ensure_security_ids(self):
|
|
831
|
+
"""Load security IDs if not already loaded"""
|
|
832
|
+
if hasattr(self, 'security_id_map') and self.security_id_map:
|
|
833
|
+
return
|
|
834
|
+
|
|
835
|
+
print("Loading Security IDs map...")
|
|
836
|
+
try:
|
|
837
|
+
securities = self.get_price_volume(use_cache=True)
|
|
838
|
+
self.security_id_map = {
|
|
839
|
+
s['symbol']: s['securityId'] for s in securities if 'symbol' in s
|
|
840
|
+
}
|
|
841
|
+
except:
|
|
842
|
+
self.security_id_map = {}
|
|
843
|
+
|
|
844
|
+
def _get_floorsheet_payload_id(self, company_id: int, date_obj: datetime):
|
|
845
|
+
"""
|
|
846
|
+
Generate strict payload ID for floorsheet using NEPSE's specific salt logic.
|
|
847
|
+
"""
|
|
848
|
+
# 1. Get Base Market ID (Dummy ID)
|
|
849
|
+
status = self.get_market_status()
|
|
850
|
+
dummy_id = int(status.get('id', 147))
|
|
851
|
+
|
|
852
|
+
# 2. Get Day
|
|
853
|
+
day = date_obj.day
|
|
854
|
+
|
|
855
|
+
# 3. DUMMY_DATA (Embedded)
|
|
856
|
+
DUMMY_DATA = [
|
|
857
|
+
147, 117, 239, 143, 157, 312, 161, 612, 512, 804, 411, 527, 170, 511, 421, 667, 764, 621, 301, 106,
|
|
858
|
+
133, 793, 411, 511, 312, 423, 344, 346, 653, 758, 342, 222, 236, 811, 711, 611, 122, 447, 128, 199,
|
|
859
|
+
183, 135, 489, 703, 800, 745, 152, 863, 134, 211, 142, 564, 375, 793, 212, 153, 138, 153, 648, 611,
|
|
860
|
+
151, 649, 318, 143, 117, 756, 119, 141, 717, 113, 112, 146, 162, 660, 693, 261, 362, 354, 251, 641,
|
|
861
|
+
157, 178, 631, 192, 734, 445, 192, 883, 187, 122, 591, 731, 852, 384, 565, 596, 451, 772, 624, 691
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
try:
|
|
865
|
+
val = DUMMY_DATA[dummy_id % len(DUMMY_DATA)]
|
|
866
|
+
except:
|
|
867
|
+
val = 147
|
|
868
|
+
|
|
869
|
+
e = val + dummy_id + 2 * day
|
|
870
|
+
|
|
871
|
+
# 4. Salt Logic
|
|
872
|
+
salt_index = 1 if e % 10 < 4 else 3
|
|
873
|
+
|
|
874
|
+
# Ensure we have salts
|
|
875
|
+
if not self.salts:
|
|
876
|
+
self.authenticate()
|
|
877
|
+
|
|
878
|
+
return int(e + self.salts[salt_index] * day - self.salts[salt_index - 1])
|
|
879
|
+
|
|
880
|
+
def clear_cache(self):
|
|
881
|
+
"""Manually clear all cached data"""
|
|
882
|
+
if self.cache:
|
|
883
|
+
self.cache.clear()
|
|
884
|
+
|
|
885
|
+
class AsyncNepse:
|
|
886
|
+
"""
|
|
887
|
+
Async version of NEPSE interface for high-performance applications
|
|
888
|
+
Combines WASM auth + async requests
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
BASE_URL = "https://www.nepalstock.com.np"
|
|
892
|
+
|
|
893
|
+
def __init__(self, cache_ttl: int = 30):
|
|
894
|
+
self.token_parser = NepseTokenParser()
|
|
895
|
+
self.cache = CacheManager(cache_ttl)
|
|
896
|
+
self.access_token = None
|
|
897
|
+
self.salts = None
|
|
898
|
+
|
|
899
|
+
async def authenticate(self):
|
|
900
|
+
"""Async authentication"""
|
|
901
|
+
url = f"{self.BASE_URL}/api/authenticate/prove"
|
|
902
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
903
|
+
async with session.get(url) as response:
|
|
904
|
+
data = await response.json()
|
|
905
|
+
self.access_token, _, self.salts = \
|
|
906
|
+
self.token_parser.parse_token_response(data)
|
|
907
|
+
|
|
908
|
+
async def get_market_status(self):
|
|
909
|
+
"""Async get market status"""
|
|
910
|
+
if not self.access_token: await self.authenticate()
|
|
911
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/market-open"
|
|
912
|
+
headers = {"Authorization": f"Salter {self.access_token}"}
|
|
913
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
914
|
+
async with session.get(url, headers=headers) as response:
|
|
915
|
+
return await response.json()
|
|
916
|
+
|
|
917
|
+
async def get_market_summary(self):
|
|
918
|
+
"""Async get market summary"""
|
|
919
|
+
if not self.access_token: await self.authenticate()
|
|
920
|
+
url = f"{self.BASE_URL}/api/nots/market-summary/"
|
|
921
|
+
headers = {"Authorization": f"Salter {self.access_token}"}
|
|
922
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
923
|
+
async with session.get(url, headers=headers) as response:
|
|
924
|
+
return await response.json()
|
|
925
|
+
|
|
926
|
+
async def get_nepse_index(self):
|
|
927
|
+
"""Async get NEPSE index"""
|
|
928
|
+
if not self.access_token: await self.authenticate()
|
|
929
|
+
url = f"{self.BASE_URL}/api/nots/nepse-index"
|
|
930
|
+
headers = {"Authorization": f"Salter {self.access_token}"}
|
|
931
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
932
|
+
async with session.get(url, headers=headers) as response:
|
|
933
|
+
return await response.json()
|
|
934
|
+
|
|
935
|
+
async def get_sub_indices(self):
|
|
936
|
+
"""Async get sub-indices"""
|
|
937
|
+
if not self.access_token: await self.authenticate()
|
|
938
|
+
url = f"{self.BASE_URL}/api/nots"
|
|
939
|
+
headers = {"Authorization": f"Salter {self.access_token}"}
|
|
940
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
941
|
+
async with session.get(url, headers=headers) as response:
|
|
942
|
+
return await response.json()
|
|
943
|
+
|
|
944
|
+
async def get_today_price(self, size: int = 500):
|
|
945
|
+
"""Async get today's price (OHLCV)"""
|
|
946
|
+
if not self.access_token: await self.authenticate()
|
|
947
|
+
url = f"{self.BASE_URL}/api/nots/nepse-data/today-price?size={size}"
|
|
948
|
+
headers = {"Authorization": f"Salter {self.access_token}", "Content-Type": "application/json"}
|
|
949
|
+
# For today-price, we need a POST request with payload
|
|
950
|
+
payload = {"id": 1} # Dummy ID often works for simple today-price
|
|
951
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
952
|
+
async with session.post(url, headers=headers, json=payload) as response:
|
|
953
|
+
return await response.json()
|
|
954
|
+
|
|
955
|
+
async def get_live_market(self):
|
|
956
|
+
"""Async get live market snapshot"""
|
|
957
|
+
if not self.access_token: await self.authenticate()
|
|
958
|
+
url = f"{self.BASE_URL}/api/nots/lives-market"
|
|
959
|
+
headers = {"Authorization": f"Salter {self.access_token}"}
|
|
960
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
961
|
+
async with session.get(url, headers=headers) as response:
|
|
962
|
+
return await response.json()
|
|
963
|
+
|
|
964
|
+
async def get_stock_info(self, symbol: str, date: str = None):
|
|
965
|
+
"""Async get stock info (Live or Historical)"""
|
|
966
|
+
symbol = symbol.upper()
|
|
967
|
+
|
|
968
|
+
if not date:
|
|
969
|
+
stocks = await self.get_live_market()
|
|
970
|
+
else:
|
|
971
|
+
# Need to implement date param support in get_today_price if not already
|
|
972
|
+
# For now assuming get_today_price supports date filtering logic or we fetch all
|
|
973
|
+
# The async get_today_price currently takes size, but we might need to add date support there too
|
|
974
|
+
# Let's keep it simple: if date is passed, we might need a specific implementation
|
|
975
|
+
# For this iteration, let's just support live default as async `get_today_price` in this file
|
|
976
|
+
# doesn't fully support date param yet (it has size).
|
|
977
|
+
# But the user only asked for robust approach which usually implies the sync client usage.
|
|
978
|
+
# We'll update this to be safe:
|
|
979
|
+
stocks = await self.get_live_market()
|
|
980
|
+
# (Note: Async historical with date needs expanding get_today_price, keeping it simple for now)
|
|
981
|
+
|
|
982
|
+
for stock in stocks:
|
|
983
|
+
if stock.get('symbol') == symbol:
|
|
984
|
+
return stock
|
|
985
|
+
return None
|
|
986
|
+
|
|
987
|
+
async def get_top_gainers(self):
|
|
988
|
+
"""Async get top gainers"""
|
|
989
|
+
if not self.access_token: await self.authenticate()
|
|
990
|
+
url = f"{self.BASE_URL}/api/nots/top-ten/top-gainer"
|
|
991
|
+
headers = {"Authorization": f"Salter {self.access_token}"}
|
|
992
|
+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
|
|
993
|
+
async with session.get(url, headers=headers) as response:
|
|
994
|
+
return await response.json()
|
|
995
|
+
|
|
996
|
+
# Convenience functions for quick usage
|
|
997
|
+
|
|
998
|
+
def quick_market_status():
|
|
999
|
+
"""Quick helper to get market status"""
|
|
1000
|
+
nepse = Nepse()
|
|
1001
|
+
return nepse.get_market_status()
|
|
1002
|
+
|
|
1003
|
+
def quick_top_gainers(limit=5):
|
|
1004
|
+
"""Quick helper to get top gainers"""
|
|
1005
|
+
nepse = Nepse()
|
|
1006
|
+
return nepse.get_top_gainers(limit=limit)
|
|
1007
|
+
|