nepse-cli 1.0.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.
- main.py +3418 -0
- nepse_cli-1.0.0.dist-info/METADATA +224 -0
- nepse_cli-1.0.0.dist-info/RECORD +8 -0
- nepse_cli-1.0.0.dist-info/WHEEL +5 -0
- nepse_cli-1.0.0.dist-info/entry_points.txt +2 -0
- nepse_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- nepse_cli-1.0.0.dist-info/top_level.txt +2 -0
- nepse_cli.py +100 -0
main.py
ADDED
|
@@ -0,0 +1,3418 @@
|
|
|
1
|
+
from playwright.sync_api import sync_playwright
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import getpass
|
|
6
|
+
import time
|
|
7
|
+
import sys
|
|
8
|
+
import shlex
|
|
9
|
+
import difflib
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import requests
|
|
13
|
+
from bs4 import BeautifulSoup
|
|
14
|
+
|
|
15
|
+
from colorama import init as colorama_init, Fore, Style
|
|
16
|
+
|
|
17
|
+
from prompt_toolkit import PromptSession
|
|
18
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
19
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
20
|
+
from prompt_toolkit.history import FileHistory
|
|
21
|
+
from prompt_toolkit.shortcuts import CompleteStyle
|
|
22
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
23
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
24
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
25
|
+
from prompt_toolkit.application import Application
|
|
26
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
27
|
+
from prompt_toolkit.layout.containers import Window
|
|
28
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
29
|
+
from prompt_toolkit.layout.layout import Layout
|
|
30
|
+
|
|
31
|
+
from rich.console import Console, Group
|
|
32
|
+
from rich.panel import Panel
|
|
33
|
+
from rich.table import Table
|
|
34
|
+
from rich.text import Text
|
|
35
|
+
from rich import box
|
|
36
|
+
from rich.align import Align
|
|
37
|
+
from rich.columns import Columns
|
|
38
|
+
from rich.rule import Rule
|
|
39
|
+
|
|
40
|
+
def ensure_playwright_browsers():
|
|
41
|
+
"""Ensure Playwright browsers are installed, install if missing."""
|
|
42
|
+
try:
|
|
43
|
+
from playwright.sync_api import sync_playwright
|
|
44
|
+
with sync_playwright() as p:
|
|
45
|
+
# Try to launch chromium briefly to check if installed
|
|
46
|
+
browser = p.chromium.launch(headless=True, timeout=5000)
|
|
47
|
+
browser.close()
|
|
48
|
+
except Exception:
|
|
49
|
+
console.print("[yellow]⚠️ Playwright browsers not found. Installing chromium...[/yellow]")
|
|
50
|
+
try:
|
|
51
|
+
import subprocess
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
[sys.executable, "-m", "playwright", "install", "chromium"],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=300 # 5 minutes timeout
|
|
57
|
+
)
|
|
58
|
+
if result.returncode == 0:
|
|
59
|
+
console.print("[green]✓ Browsers installed successfully![/green]")
|
|
60
|
+
else:
|
|
61
|
+
console.print(f"[red]✗ Failed to install browsers: {result.stderr}[/red]")
|
|
62
|
+
console.print("[yellow]You can install manually with: playwright install chromium[/yellow]")
|
|
63
|
+
except subprocess.TimeoutExpired:
|
|
64
|
+
console.print("[red]✗ Browser installation timed out. Please install manually.[/red]")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
console.print(f"[red]✗ Error installing browsers: {e}[/red]")
|
|
67
|
+
|
|
68
|
+
# Dynamic data directory for all credentials
|
|
69
|
+
# Uses user's Documents folder if available, otherwise home directory
|
|
70
|
+
DATA_DIR = Path.home() / "Documents" / "merosharedata"
|
|
71
|
+
if not DATA_DIR.parent.exists():
|
|
72
|
+
DATA_DIR = Path.home() / "merosharedata"
|
|
73
|
+
|
|
74
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
CONFIG_FILE = DATA_DIR / "family_members.json"
|
|
77
|
+
IPO_CONFIG_FILE = DATA_DIR / "ipo_config.json"
|
|
78
|
+
CLI_HISTORY_FILE = DATA_DIR / "nepse_cli_history.txt"
|
|
79
|
+
|
|
80
|
+
console = Console(force_terminal=True, legacy_windows=False)
|
|
81
|
+
CLI_PROMPT_STYLE = PTStyle.from_dict({
|
|
82
|
+
# Gemini-inspired colors
|
|
83
|
+
"prompt": "bold #4da6ff", # Bright Blue prompt
|
|
84
|
+
"input": "#ffffff", # White input text
|
|
85
|
+
|
|
86
|
+
# Completion Menu Styles
|
|
87
|
+
"completion-menu": "bg:#111111 noinherit", # Very dark background
|
|
88
|
+
"completion-menu.completion": "bg:#111111 #bbbbbb", # Default text grey
|
|
89
|
+
"completion-menu.completion.current": "bg:#2d2d2d #ffffff bold", # Highlighted row
|
|
90
|
+
|
|
91
|
+
# Custom classes for formatted completions
|
|
92
|
+
"completion-command": "bold #ffffff", # Command name white/bold
|
|
93
|
+
"completion-description": "italic #ff55ff", # Description pink/magenta
|
|
94
|
+
"completion-builtin": "italic #888888", # Built-in tag grey
|
|
95
|
+
|
|
96
|
+
"scrollbar.background": "bg:#111111",
|
|
97
|
+
"scrollbar.button": "bg:#555555",
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
def print_progress(step, total, message, sub_message=""):
|
|
101
|
+
"""
|
|
102
|
+
Print a progress bar with current step
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
step: Current step number (1-indexed)
|
|
106
|
+
total: Total number of steps
|
|
107
|
+
message: Main message to display
|
|
108
|
+
sub_message: Optional sub-message with arrow prefix
|
|
109
|
+
"""
|
|
110
|
+
# Calculate progress
|
|
111
|
+
percentage = int((step / total) * 100)
|
|
112
|
+
filled = int((step / total) * 30) # 30 character wide bar
|
|
113
|
+
bar = "█" * filled + "░" * (30 - filled)
|
|
114
|
+
|
|
115
|
+
# Print progress bar
|
|
116
|
+
print(f"\r[{bar}] {percentage}% ({step}/{total}) {message}", end="", flush=True)
|
|
117
|
+
|
|
118
|
+
# If this is the last step or there's a sub-message, move to new line
|
|
119
|
+
if sub_message:
|
|
120
|
+
print(f"\n → {sub_message}", flush=True)
|
|
121
|
+
elif step == total:
|
|
122
|
+
print() # New line at the end
|
|
123
|
+
|
|
124
|
+
def load_family_members():
|
|
125
|
+
"""Load all family members from config file"""
|
|
126
|
+
if not CONFIG_FILE.exists():
|
|
127
|
+
return {"members": []}
|
|
128
|
+
|
|
129
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
130
|
+
return json.load(f)
|
|
131
|
+
|
|
132
|
+
def save_family_members(config):
|
|
133
|
+
"""Save family members to config file"""
|
|
134
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
135
|
+
json.dump(config, f, indent=2)
|
|
136
|
+
|
|
137
|
+
if os.name != 'nt':
|
|
138
|
+
os.chmod(CONFIG_FILE, 0o600)
|
|
139
|
+
|
|
140
|
+
def add_family_member():
|
|
141
|
+
"""Add a new family member"""
|
|
142
|
+
print("\n=== Add Family Member ===")
|
|
143
|
+
|
|
144
|
+
config = load_family_members()
|
|
145
|
+
|
|
146
|
+
member_name = input("\nEnter member name (e.g., Dad, Mom, Me, Brother): ").strip()
|
|
147
|
+
|
|
148
|
+
# Check if member already exists
|
|
149
|
+
for member in config.get('members', []):
|
|
150
|
+
if member['name'].lower() == member_name.lower():
|
|
151
|
+
print(f"\n⚠ Member '{member_name}' already exists!")
|
|
152
|
+
update = input("Update this member? (yes/no): ").strip().lower()
|
|
153
|
+
if update != 'yes':
|
|
154
|
+
return
|
|
155
|
+
config['members'].remove(member)
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
print("\n--- Meroshare Credentials ---")
|
|
159
|
+
print("Common DPs (or use option 6 to see all):")
|
|
160
|
+
print(" 139 - CREATIVE SECURITIES PRIVATE LIMITED (13300)")
|
|
161
|
+
print(" 146 - GLOBAL IME CAPITAL LIMITED (11200)")
|
|
162
|
+
print(" 175 - NMB CAPITAL LIMITED (11000)")
|
|
163
|
+
print(" 190 - SIDDHARTHA CAPITAL LIMITED (10900)\n")
|
|
164
|
+
|
|
165
|
+
dp_value = input("Enter DP value (e.g., 139): ").strip()
|
|
166
|
+
username = input("Enter username: ").strip()
|
|
167
|
+
password = getpass.getpass("Enter password: ")
|
|
168
|
+
pin = getpass.getpass("Enter 4-digit transaction PIN: ")
|
|
169
|
+
|
|
170
|
+
print("\n--- IPO Application Settings ---")
|
|
171
|
+
applied_kitta = input("Applied Kitta (default 10): ").strip() or "10"
|
|
172
|
+
crn_number = input("CRN Number: ").strip()
|
|
173
|
+
|
|
174
|
+
member = {
|
|
175
|
+
"name": member_name,
|
|
176
|
+
"dp_value": dp_value,
|
|
177
|
+
"username": username,
|
|
178
|
+
"password": password,
|
|
179
|
+
"transaction_pin": pin,
|
|
180
|
+
"applied_kitta": int(applied_kitta),
|
|
181
|
+
"crn_number": crn_number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if 'members' not in config:
|
|
185
|
+
config['members'] = []
|
|
186
|
+
|
|
187
|
+
config['members'].append(member)
|
|
188
|
+
save_family_members(config)
|
|
189
|
+
|
|
190
|
+
print(f"\n✓ Member '{member_name}' added successfully!")
|
|
191
|
+
print(f"✓ Total members: {len(config['members'])}\n")
|
|
192
|
+
|
|
193
|
+
def list_family_members():
|
|
194
|
+
"""List all family members"""
|
|
195
|
+
config = load_family_members()
|
|
196
|
+
members = config.get('members', [])
|
|
197
|
+
|
|
198
|
+
if not members:
|
|
199
|
+
console.print(Panel("⚠ No family members found. Add members first!", style="bold red", box=box.ROUNDED))
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
table = Table(title="Family Members", box=box.ROUNDED, header_style="bold cyan", expand=True)
|
|
203
|
+
table.add_column("#", style="dim", width=4, justify="center")
|
|
204
|
+
table.add_column("Name", style="bold white")
|
|
205
|
+
table.add_column("Username", style="cyan")
|
|
206
|
+
table.add_column("DP", style="magenta")
|
|
207
|
+
table.add_column("Kitta", justify="right")
|
|
208
|
+
table.add_column("CRN", style="yellow")
|
|
209
|
+
|
|
210
|
+
for idx, member in enumerate(members, 1):
|
|
211
|
+
table.add_row(
|
|
212
|
+
str(idx),
|
|
213
|
+
member['name'],
|
|
214
|
+
member['username'],
|
|
215
|
+
member['dp_value'],
|
|
216
|
+
str(member['applied_kitta']),
|
|
217
|
+
member['crn_number']
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
console.print(table)
|
|
221
|
+
return members
|
|
222
|
+
|
|
223
|
+
def select_family_member():
|
|
224
|
+
"""Select a family member for IPO application using an inline interactive menu"""
|
|
225
|
+
config = load_family_members()
|
|
226
|
+
members = config.get('members', [])
|
|
227
|
+
|
|
228
|
+
if not members:
|
|
229
|
+
console.print(Panel("⚠ No family members found. Add members first!", style="bold red", box=box.ROUNDED))
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
# Inline interactive selection
|
|
233
|
+
selected_index = 0
|
|
234
|
+
|
|
235
|
+
bindings = KeyBindings()
|
|
236
|
+
|
|
237
|
+
@bindings.add('up')
|
|
238
|
+
def _(event):
|
|
239
|
+
nonlocal selected_index
|
|
240
|
+
selected_index = (selected_index - 1) % len(members)
|
|
241
|
+
|
|
242
|
+
@bindings.add('down')
|
|
243
|
+
def _(event):
|
|
244
|
+
nonlocal selected_index
|
|
245
|
+
selected_index = (selected_index + 1) % len(members)
|
|
246
|
+
|
|
247
|
+
@bindings.add('enter')
|
|
248
|
+
def _(event):
|
|
249
|
+
event.app.exit(result=members[selected_index])
|
|
250
|
+
|
|
251
|
+
@bindings.add('c-c')
|
|
252
|
+
def _(event):
|
|
253
|
+
event.app.exit(result=None)
|
|
254
|
+
|
|
255
|
+
def get_formatted_text():
|
|
256
|
+
result = []
|
|
257
|
+
result.append(('class:title', 'Select Family Member (Use ↑/↓ and Enter):\n'))
|
|
258
|
+
for i, member in enumerate(members):
|
|
259
|
+
if i == selected_index:
|
|
260
|
+
# Highlight selected item
|
|
261
|
+
result.append(('class:selected', f' > {member["name"]} (DP: {member["dp_value"]})\n'))
|
|
262
|
+
else:
|
|
263
|
+
result.append(('class:unselected', f' {member["name"]} (DP: {member["dp_value"]})\n'))
|
|
264
|
+
return FormattedText(result)
|
|
265
|
+
|
|
266
|
+
# Define style for the menu
|
|
267
|
+
style = PTStyle.from_dict({
|
|
268
|
+
'selected': 'fg:ansigreen bold',
|
|
269
|
+
'unselected': '',
|
|
270
|
+
'title': 'bold underline'
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
# Create a small application that runs inline
|
|
274
|
+
app = Application(
|
|
275
|
+
layout=Layout(
|
|
276
|
+
Window(content=FormattedTextControl(get_formatted_text), height=len(members) + 2)
|
|
277
|
+
),
|
|
278
|
+
key_bindings=bindings,
|
|
279
|
+
style=style,
|
|
280
|
+
full_screen=False, # Inline mode
|
|
281
|
+
mouse_support=False
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
selected = app.run()
|
|
286
|
+
|
|
287
|
+
if selected:
|
|
288
|
+
console.print(f"[bold green]✓ Selected:[/bold green] {selected['name']} (Kitta: {selected['applied_kitta']} | CRN: {selected['crn_number']})")
|
|
289
|
+
return selected
|
|
290
|
+
else:
|
|
291
|
+
console.print("\n[yellow]✗ Selection cancelled[/yellow]")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
# Fallback to simple input if something goes wrong
|
|
296
|
+
console.print(f"[yellow]Interactive menu failed ({str(e)}). Using standard input.[/yellow]")
|
|
297
|
+
list_family_members()
|
|
298
|
+
while True:
|
|
299
|
+
try:
|
|
300
|
+
choice = input(f"\n👉 Enter member number (1-{len(members)}): ").strip()
|
|
301
|
+
idx = int(choice) - 1
|
|
302
|
+
if 0 <= idx < len(members):
|
|
303
|
+
selected = members[idx]
|
|
304
|
+
print(f"\n✓ Selected: {selected['name']}")
|
|
305
|
+
return selected
|
|
306
|
+
else:
|
|
307
|
+
print(f"❌ Invalid choice. Enter a number between 1 and {len(members)}")
|
|
308
|
+
except ValueError:
|
|
309
|
+
print("❌ Invalid input. Please enter a number.")
|
|
310
|
+
except KeyboardInterrupt:
|
|
311
|
+
print("\n\n✗ Cancelled")
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
def save_credentials():
|
|
315
|
+
"""Legacy function - redirects to add_family_member"""
|
|
316
|
+
return add_family_member()
|
|
317
|
+
|
|
318
|
+
def load_credentials():
|
|
319
|
+
"""Load credentials - for backward compatibility"""
|
|
320
|
+
# Check for old single-member config
|
|
321
|
+
old_config_file = DATA_DIR / "meroshare_config.json"
|
|
322
|
+
if old_config_file.exists() and not CONFIG_FILE.exists():
|
|
323
|
+
print("\n⚠ Old config format detected. Migrating to multi-member format...\n")
|
|
324
|
+
with open(old_config_file, 'r') as f:
|
|
325
|
+
old_config = json.load(f)
|
|
326
|
+
|
|
327
|
+
# Migrate to new format
|
|
328
|
+
member_name = input("Enter name for this member (e.g., Me): ").strip() or "Me"
|
|
329
|
+
|
|
330
|
+
new_config = {
|
|
331
|
+
"members": [{
|
|
332
|
+
"name": member_name,
|
|
333
|
+
"dp_value": old_config.get('dp_value', ''),
|
|
334
|
+
"username": old_config.get('username', ''),
|
|
335
|
+
"password": old_config.get('password', ''),
|
|
336
|
+
"transaction_pin": old_config.get('transaction_pin', ''),
|
|
337
|
+
"applied_kitta": 10,
|
|
338
|
+
"crn_number": ""
|
|
339
|
+
}]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
save_family_members(new_config)
|
|
343
|
+
print(f"✓ Migrated to new format as '{member_name}'\n")
|
|
344
|
+
|
|
345
|
+
# Backup old file
|
|
346
|
+
os.rename(old_config_file, old_config_file + ".backup")
|
|
347
|
+
|
|
348
|
+
config = load_family_members()
|
|
349
|
+
|
|
350
|
+
if not config.get('members'):
|
|
351
|
+
print("\n⚠ No family members found. Let's add one!\n")
|
|
352
|
+
add_family_member()
|
|
353
|
+
config = load_family_members()
|
|
354
|
+
|
|
355
|
+
return config
|
|
356
|
+
|
|
357
|
+
def update_credentials():
|
|
358
|
+
"""Legacy function - redirects to add_family_member"""
|
|
359
|
+
add_family_member()
|
|
360
|
+
|
|
361
|
+
def meroshare_login(auto_load=True, headless=False):
|
|
362
|
+
"""
|
|
363
|
+
Automated login for Meroshare with correct selectors
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
auto_load: Load credentials from config file
|
|
367
|
+
headless: Run browser in headless mode (no GUI)
|
|
368
|
+
"""
|
|
369
|
+
if auto_load:
|
|
370
|
+
print("Loading credentials...")
|
|
371
|
+
config = load_credentials()
|
|
372
|
+
dp_value = config['dp_value']
|
|
373
|
+
username = config['username']
|
|
374
|
+
password = config['password']
|
|
375
|
+
else:
|
|
376
|
+
dp_value = input("Enter DP value: ")
|
|
377
|
+
username = input("Enter username: ")
|
|
378
|
+
password = getpass.getpass("Enter password: ")
|
|
379
|
+
|
|
380
|
+
with sync_playwright() as p:
|
|
381
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
382
|
+
context = browser.new_context()
|
|
383
|
+
page = context.new_page()
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
print_progress(1, 6, "Navigating to Meroshare...")
|
|
387
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
388
|
+
time.sleep(2)
|
|
389
|
+
|
|
390
|
+
# Select2 dropdown - click to open
|
|
391
|
+
print_progress(2, 6, "Opening DP dropdown...")
|
|
392
|
+
page.click("span.select2-selection")
|
|
393
|
+
time.sleep(1)
|
|
394
|
+
|
|
395
|
+
# Select the option - Select2 creates a results list in the DOM
|
|
396
|
+
print_progress(3, 6, f"Selecting DP (value: {dp_value})...")
|
|
397
|
+
# Wait for dropdown results to appear
|
|
398
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
399
|
+
|
|
400
|
+
# Type in search box and select
|
|
401
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
402
|
+
if search_box:
|
|
403
|
+
print(f" → Searching for DP value {dp_value}...")
|
|
404
|
+
search_box.type(dp_value)
|
|
405
|
+
time.sleep(0.5)
|
|
406
|
+
|
|
407
|
+
# Click the first result or press Enter
|
|
408
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
409
|
+
if first_result:
|
|
410
|
+
first_result.click()
|
|
411
|
+
else:
|
|
412
|
+
page.keyboard.press("Enter")
|
|
413
|
+
else:
|
|
414
|
+
# Fallback: click by text
|
|
415
|
+
try:
|
|
416
|
+
results = page.query_selector_all("li.select2-results__option")
|
|
417
|
+
for result in results:
|
|
418
|
+
if dp_value in result.inner_text():
|
|
419
|
+
result.click()
|
|
420
|
+
break
|
|
421
|
+
except:
|
|
422
|
+
print(" ⚠ Using fallback selection method...")
|
|
423
|
+
page.select_option("select.select2-hidden-accessible", dp_value)
|
|
424
|
+
|
|
425
|
+
time.sleep(1)
|
|
426
|
+
|
|
427
|
+
# Fill username - try multiple possible selectors
|
|
428
|
+
print_progress(4, 6, "Filling username...")
|
|
429
|
+
username_selectors = [
|
|
430
|
+
"input[formcontrolname='username']",
|
|
431
|
+
"input#username",
|
|
432
|
+
"input[placeholder*='User']"
|
|
433
|
+
]
|
|
434
|
+
for selector in username_selectors:
|
|
435
|
+
try:
|
|
436
|
+
page.fill(selector, username, timeout=2000)
|
|
437
|
+
break
|
|
438
|
+
except:
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
# Fill password
|
|
442
|
+
print_progress(5, 6, "Filling password...")
|
|
443
|
+
password_selectors = [
|
|
444
|
+
"input[formcontrolname='password']",
|
|
445
|
+
"input[type='password']"
|
|
446
|
+
]
|
|
447
|
+
for selector in password_selectors:
|
|
448
|
+
try:
|
|
449
|
+
page.fill(selector, password, timeout=2000)
|
|
450
|
+
break
|
|
451
|
+
except:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
# Click login button
|
|
455
|
+
print_progress(6, 6, "Clicking login button...")
|
|
456
|
+
login_button_selectors = [
|
|
457
|
+
"button.btn.sign-in",
|
|
458
|
+
"button[type='submit']",
|
|
459
|
+
"button:has-text('Login')"
|
|
460
|
+
]
|
|
461
|
+
for selector in login_button_selectors:
|
|
462
|
+
try:
|
|
463
|
+
page.click(selector, timeout=2000)
|
|
464
|
+
break
|
|
465
|
+
except:
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
# Wait for response - try to detect navigation
|
|
469
|
+
print("\nWaiting for response...")
|
|
470
|
+
try:
|
|
471
|
+
# Wait for navigation or some change (max 8 seconds)
|
|
472
|
+
page.wait_for_load_state("networkidle", timeout=8000)
|
|
473
|
+
except:
|
|
474
|
+
print(" (networkidle timeout - page may still be loading)")
|
|
475
|
+
|
|
476
|
+
time.sleep(2) # extra buffer for any JS to complete
|
|
477
|
+
|
|
478
|
+
# Angular apps: wait for hash route to change from #/login to #/dashboard
|
|
479
|
+
print("Waiting for Angular routing...")
|
|
480
|
+
try:
|
|
481
|
+
# Wait up to 3 seconds for URL to change away from #/login
|
|
482
|
+
page.wait_for_function("window.location.hash !== '#/login'", timeout=3000)
|
|
483
|
+
time.sleep(0.5) # small buffer for final render
|
|
484
|
+
except:
|
|
485
|
+
print(" (route didn't change, but may still be logged in)")
|
|
486
|
+
|
|
487
|
+
# Check result - multiple detection methods
|
|
488
|
+
current_url = page.url
|
|
489
|
+
print(f"\nCurrent URL: {current_url}")
|
|
490
|
+
|
|
491
|
+
# Method 1: Check URL patterns (Angular hash routing)
|
|
492
|
+
url_success = False
|
|
493
|
+
if "#/login" not in current_url.lower():
|
|
494
|
+
url_success = True
|
|
495
|
+
print("✓ URL changed from login page")
|
|
496
|
+
|
|
497
|
+
# Method 2: Check if login form is still visible
|
|
498
|
+
form_gone = False
|
|
499
|
+
try:
|
|
500
|
+
login_form_visible = page.is_visible("input[formcontrolname='username']", timeout=1000)
|
|
501
|
+
if not login_form_visible:
|
|
502
|
+
form_gone = True
|
|
503
|
+
print("✓ Login form disappeared")
|
|
504
|
+
except:
|
|
505
|
+
form_gone = True
|
|
506
|
+
print("✓ Login form not found")
|
|
507
|
+
|
|
508
|
+
# Method 3: Look for success indicators
|
|
509
|
+
success_elements = [
|
|
510
|
+
"a[href*='dashboard']",
|
|
511
|
+
"button:has-text('Logout')",
|
|
512
|
+
".user-info, .user-profile",
|
|
513
|
+
"[class*='dashboard']"
|
|
514
|
+
]
|
|
515
|
+
found_success_element = False
|
|
516
|
+
for selector in success_elements:
|
|
517
|
+
try:
|
|
518
|
+
if page.query_selector(selector):
|
|
519
|
+
found_success_element = True
|
|
520
|
+
print(f"✓ Found success indicator: {selector}")
|
|
521
|
+
break
|
|
522
|
+
except:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
# Method 4: Check for error messages
|
|
526
|
+
error_found = False
|
|
527
|
+
try:
|
|
528
|
+
errors = page.query_selector_all(".error, .alert-danger, .text-danger, [class*='error'], .invalid-feedback")
|
|
529
|
+
for error in errors:
|
|
530
|
+
text = error.inner_text().strip()
|
|
531
|
+
if text and len(text) > 0:
|
|
532
|
+
print(f"⚠ Error message found: {text}")
|
|
533
|
+
error_found = True
|
|
534
|
+
except:
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
# Final verdict
|
|
538
|
+
if error_found:
|
|
539
|
+
print("\n✗ LOGIN FAILED - Error message detected")
|
|
540
|
+
# Take screenshot for debugging
|
|
541
|
+
try:
|
|
542
|
+
screenshot_path = "login_error.png"
|
|
543
|
+
page.screenshot(path=screenshot_path)
|
|
544
|
+
print(f"📸 Screenshot saved to {screenshot_path}")
|
|
545
|
+
except:
|
|
546
|
+
pass
|
|
547
|
+
elif url_success or form_gone or found_success_element:
|
|
548
|
+
print("\n✓✓✓ LOGIN SUCCESSFUL! ✓✓✓")
|
|
549
|
+
print(f"Page URL: {current_url}")
|
|
550
|
+
else:
|
|
551
|
+
print("\n⚠ Login status uncertain - please verify manually")
|
|
552
|
+
print(f"URL contains 'login': {'login' in current_url.lower()}")
|
|
553
|
+
# Take screenshot for debugging
|
|
554
|
+
try:
|
|
555
|
+
screenshot_path = "login_uncertain.png"
|
|
556
|
+
page.screenshot(path=screenshot_path)
|
|
557
|
+
print(f"📸 Screenshot saved to {screenshot_path}")
|
|
558
|
+
except:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
# In headless mode, don't wait
|
|
562
|
+
if not headless:
|
|
563
|
+
print("\nBrowser will stay open for 30 seconds...")
|
|
564
|
+
time.sleep(30)
|
|
565
|
+
else:
|
|
566
|
+
print("\n✓ Script completed in headless mode")
|
|
567
|
+
time.sleep(2)
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"\n✗ Error: {e}")
|
|
571
|
+
if not headless:
|
|
572
|
+
time.sleep(5)
|
|
573
|
+
|
|
574
|
+
finally:
|
|
575
|
+
browser.close()
|
|
576
|
+
|
|
577
|
+
def get_portfolio(auto_load=True, headless=False):
|
|
578
|
+
"""
|
|
579
|
+
Login and fetch portfolio holdings from Meroshare
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
auto_load: Load credentials from config file
|
|
583
|
+
headless: Run browser in headless mode (no GUI)
|
|
584
|
+
"""
|
|
585
|
+
if auto_load:
|
|
586
|
+
print("Loading credentials...")
|
|
587
|
+
config = load_credentials()
|
|
588
|
+
dp_value = config['dp_value']
|
|
589
|
+
username = config['username']
|
|
590
|
+
password = config['password']
|
|
591
|
+
else:
|
|
592
|
+
dp_value = input("Enter DP value: ")
|
|
593
|
+
username = input("Enter username: ")
|
|
594
|
+
password = getpass.getpass("Enter password: ")
|
|
595
|
+
|
|
596
|
+
with sync_playwright() as p:
|
|
597
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
598
|
+
context = browser.new_context()
|
|
599
|
+
page = context.new_page()
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
# Use the EXACT same login logic that works in meroshare_login()
|
|
603
|
+
print(); print_progress(1, 7, "Navigating to Meroshare...")
|
|
604
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
605
|
+
time.sleep(2)
|
|
606
|
+
|
|
607
|
+
# Select2 dropdown - click to open
|
|
608
|
+
print_progress(2, 7, "Opening DP dropdown...")
|
|
609
|
+
page.click("span.select2-selection")
|
|
610
|
+
time.sleep(1)
|
|
611
|
+
|
|
612
|
+
# Select the option - Select2 creates a results list in the DOM
|
|
613
|
+
print_progress(3, 7, f"Selecting DP (value: {dp_value})...")
|
|
614
|
+
# Wait for dropdown results to appear
|
|
615
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
616
|
+
|
|
617
|
+
# Type in search box and select
|
|
618
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
619
|
+
if search_box:
|
|
620
|
+
print(f" → Searching for DP value {dp_value}...")
|
|
621
|
+
search_box.type(dp_value)
|
|
622
|
+
time.sleep(0.5)
|
|
623
|
+
|
|
624
|
+
# Click the first result or press Enter
|
|
625
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
626
|
+
if first_result:
|
|
627
|
+
first_result.click()
|
|
628
|
+
else:
|
|
629
|
+
page.keyboard.press("Enter")
|
|
630
|
+
else:
|
|
631
|
+
# Fallback: click by text
|
|
632
|
+
try:
|
|
633
|
+
results = page.query_selector_all("li.select2-results__option")
|
|
634
|
+
for result in results:
|
|
635
|
+
if dp_value in result.inner_text():
|
|
636
|
+
result.click()
|
|
637
|
+
break
|
|
638
|
+
except:
|
|
639
|
+
print(" ⚠ Using fallback selection method...")
|
|
640
|
+
page.select_option("select.select2-hidden-accessible", dp_value)
|
|
641
|
+
|
|
642
|
+
time.sleep(1)
|
|
643
|
+
|
|
644
|
+
# Fill username - try multiple possible selectors
|
|
645
|
+
print_progress(4, 7, "Filling username...")
|
|
646
|
+
username_selectors = [
|
|
647
|
+
"input[formcontrolname='username']",
|
|
648
|
+
"input#username",
|
|
649
|
+
"input[placeholder*='User']"
|
|
650
|
+
]
|
|
651
|
+
for selector in username_selectors:
|
|
652
|
+
try:
|
|
653
|
+
page.fill(selector, username, timeout=2000)
|
|
654
|
+
break
|
|
655
|
+
except:
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
# Fill password
|
|
659
|
+
print_progress(5, 7, "Filling password...")
|
|
660
|
+
password_selectors = [
|
|
661
|
+
"input[formcontrolname='password']",
|
|
662
|
+
"input[type='password']"
|
|
663
|
+
]
|
|
664
|
+
for selector in password_selectors:
|
|
665
|
+
try:
|
|
666
|
+
page.fill(selector, password, timeout=2000)
|
|
667
|
+
break
|
|
668
|
+
except:
|
|
669
|
+
continue
|
|
670
|
+
|
|
671
|
+
# Click login button
|
|
672
|
+
print_progress(6, 7, "Clicking login button...")
|
|
673
|
+
login_button_selectors = [
|
|
674
|
+
"button.btn.sign-in",
|
|
675
|
+
"button[type='submit']",
|
|
676
|
+
"button:has-text('Login')"
|
|
677
|
+
]
|
|
678
|
+
for selector in login_button_selectors:
|
|
679
|
+
try:
|
|
680
|
+
page.click(selector, timeout=2000)
|
|
681
|
+
break
|
|
682
|
+
except:
|
|
683
|
+
continue
|
|
684
|
+
|
|
685
|
+
# Wait for login to complete - same as working login function
|
|
686
|
+
print(); print_progress(7, 7, "Waiting for login...")
|
|
687
|
+
try:
|
|
688
|
+
page.wait_for_load_state("networkidle", timeout=8000)
|
|
689
|
+
except:
|
|
690
|
+
print(" (networkidle timeout - page may still be loading)")
|
|
691
|
+
|
|
692
|
+
time.sleep(2)
|
|
693
|
+
|
|
694
|
+
# Wait for Angular routing
|
|
695
|
+
print("Waiting for Angular routing...")
|
|
696
|
+
try:
|
|
697
|
+
page.wait_for_function("window.location.hash !== '#/login'", timeout=3000)
|
|
698
|
+
time.sleep(0.5)
|
|
699
|
+
except:
|
|
700
|
+
print(" (route didn't change, but may still be logged in)")
|
|
701
|
+
|
|
702
|
+
# Check if logged in
|
|
703
|
+
current_url = page.url
|
|
704
|
+
print(f"Current URL: {current_url}")
|
|
705
|
+
|
|
706
|
+
if "#/login" not in current_url.lower() or page.query_selector("a[href*='dashboard']"):
|
|
707
|
+
print("✓ Login successful!")
|
|
708
|
+
else:
|
|
709
|
+
print("⚠ Login may have failed, but continuing to portfolio...")
|
|
710
|
+
|
|
711
|
+
# Navigate to Portfolio
|
|
712
|
+
print("\n📊 Navigating to Portfolio page...")
|
|
713
|
+
page.goto("https://meroshare.cdsc.com.np/#/portfolio", wait_until="networkidle")
|
|
714
|
+
time.sleep(3)
|
|
715
|
+
|
|
716
|
+
print("Fetching holdings...\n")
|
|
717
|
+
|
|
718
|
+
# Extract portfolio data with correct selectors
|
|
719
|
+
try:
|
|
720
|
+
# Wait for the table to load (Angular app with _ngcontent attributes)
|
|
721
|
+
print("Waiting for portfolio table to load...")
|
|
722
|
+
page.wait_for_selector("table.table tbody tr", timeout=10000)
|
|
723
|
+
time.sleep(2)
|
|
724
|
+
|
|
725
|
+
# Get all data rows (excluding the total row)
|
|
726
|
+
rows = page.query_selector_all("table.table tbody:first-of-type tr")
|
|
727
|
+
|
|
728
|
+
if rows and len(rows) > 0:
|
|
729
|
+
table = Table(title="YOUR PORTFOLIO HOLDINGS", box=box.ROUNDED, header_style="bold cyan", expand=True)
|
|
730
|
+
table.add_column("#", style="dim", width=4)
|
|
731
|
+
table.add_column("Scrip", style="bold white")
|
|
732
|
+
table.add_column("Balance", justify="right")
|
|
733
|
+
table.add_column("Last Price", justify="right")
|
|
734
|
+
table.add_column("Value (Last)", justify="right")
|
|
735
|
+
table.add_column("LTP", justify="right")
|
|
736
|
+
table.add_column("Value (LTP)", justify="right", style="green")
|
|
737
|
+
|
|
738
|
+
portfolio_data = []
|
|
739
|
+
total_value_last = 0
|
|
740
|
+
total_value_ltp = 0
|
|
741
|
+
|
|
742
|
+
for row in rows:
|
|
743
|
+
cells = row.query_selector_all("td")
|
|
744
|
+
if cells and len(cells) >= 7:
|
|
745
|
+
# Extract each column
|
|
746
|
+
num = cells[0].inner_text().strip()
|
|
747
|
+
scrip = cells[1].inner_text().strip()
|
|
748
|
+
balance = cells[2].inner_text().strip()
|
|
749
|
+
last_price = cells[3].inner_text().strip()
|
|
750
|
+
value_last = cells[4].inner_text().strip()
|
|
751
|
+
ltp = cells[5].inner_text().strip()
|
|
752
|
+
value_ltp = cells[6].inner_text().strip()
|
|
753
|
+
|
|
754
|
+
# Store as structured data
|
|
755
|
+
holding = {
|
|
756
|
+
"number": num,
|
|
757
|
+
"scrip": scrip,
|
|
758
|
+
"current_balance": balance,
|
|
759
|
+
"last_closing_price": last_price,
|
|
760
|
+
"value_as_of_last_price": value_last,
|
|
761
|
+
"last_transaction_price": ltp,
|
|
762
|
+
"value_as_of_ltp": value_ltp
|
|
763
|
+
}
|
|
764
|
+
portfolio_data.append(holding)
|
|
765
|
+
|
|
766
|
+
table.add_row(num, scrip, balance, last_price, value_last, ltp, value_ltp)
|
|
767
|
+
|
|
768
|
+
# Get total row (from second tbody)
|
|
769
|
+
total_rows = page.query_selector_all("table.table tbody:last-of-type tr")
|
|
770
|
+
if total_rows and len(total_rows) > 0:
|
|
771
|
+
total_cells = total_rows[0].query_selector_all("td")
|
|
772
|
+
if total_cells and len(total_cells) >= 5:
|
|
773
|
+
total_last = total_cells[4].inner_text().strip()
|
|
774
|
+
total_ltp = total_cells[6].inner_text().strip() if len(total_cells) > 6 else ""
|
|
775
|
+
|
|
776
|
+
table.add_section()
|
|
777
|
+
table.add_row("TOTAL", "", "", "", total_last, "", total_ltp, style="bold")
|
|
778
|
+
|
|
779
|
+
console.print(table)
|
|
780
|
+
console.print(f"✓ Total holdings: {len(portfolio_data)} scrips\n")
|
|
781
|
+
|
|
782
|
+
# Save to JSON with metadata
|
|
783
|
+
output_file = "portfolio_data.json"
|
|
784
|
+
output = {
|
|
785
|
+
"fetched_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
786
|
+
"total_scrips": len(portfolio_data),
|
|
787
|
+
"holdings": portfolio_data
|
|
788
|
+
}
|
|
789
|
+
with open(output_file, 'w') as f:
|
|
790
|
+
json.dump(output, f, indent=2)
|
|
791
|
+
print(f"✓ Portfolio data saved to {output_file}")
|
|
792
|
+
|
|
793
|
+
else:
|
|
794
|
+
console.print(Panel("⚠ No portfolio data found.", style="bold yellow", box=box.ROUNDED))
|
|
795
|
+
except Exception as e:
|
|
796
|
+
print(f"⚠ Error extracting portfolio: {e}")
|
|
797
|
+
screenshot_path = "portfolio_error.png"
|
|
798
|
+
page.screenshot(path=screenshot_path, full_page=True)
|
|
799
|
+
print(f"📸 Screenshot saved to {screenshot_path}")
|
|
800
|
+
|
|
801
|
+
# Keep browser open in non-headless mode
|
|
802
|
+
if not headless:
|
|
803
|
+
print("\nBrowser will stay open for 30 seconds...")
|
|
804
|
+
time.sleep(30)
|
|
805
|
+
|
|
806
|
+
except Exception as e:
|
|
807
|
+
print(f"\n✗ Error: {e}")
|
|
808
|
+
if not headless:
|
|
809
|
+
time.sleep(5)
|
|
810
|
+
finally:
|
|
811
|
+
browser.close()
|
|
812
|
+
|
|
813
|
+
def load_ipo_config():
|
|
814
|
+
"""Load IPO application configuration"""
|
|
815
|
+
if not IPO_CONFIG_FILE.exists():
|
|
816
|
+
print(f"\n⚠ IPO config file not found. Creating template...")
|
|
817
|
+
default_config = {
|
|
818
|
+
"applied_kitta": 10,
|
|
819
|
+
"crn_number": "YOUR_CRN_NUMBER_HERE"
|
|
820
|
+
}
|
|
821
|
+
with open(IPO_CONFIG_FILE, 'w') as f:
|
|
822
|
+
json.dump(default_config, f, indent=2)
|
|
823
|
+
print(f"✓ Created {IPO_CONFIG_FILE}")
|
|
824
|
+
print(f"⚠ Please edit {IPO_CONFIG_FILE} with your actual CRN number before applying!\n")
|
|
825
|
+
return default_config
|
|
826
|
+
|
|
827
|
+
with open(IPO_CONFIG_FILE, 'r') as f:
|
|
828
|
+
return json.load(f)
|
|
829
|
+
|
|
830
|
+
def apply_ipo(auto_load=True, headless=False):
|
|
831
|
+
"""
|
|
832
|
+
Complete IPO application automation with family member selection
|
|
833
|
+
|
|
834
|
+
Args:
|
|
835
|
+
auto_load: Load credentials from config file
|
|
836
|
+
headless: Run browser in headless mode (no GUI)
|
|
837
|
+
"""
|
|
838
|
+
if auto_load:
|
|
839
|
+
# Select family member
|
|
840
|
+
member = select_family_member()
|
|
841
|
+
if not member:
|
|
842
|
+
print("\n✗ No member selected. Exiting...")
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
dp_value = member['dp_value']
|
|
846
|
+
username = member['username']
|
|
847
|
+
password = member['password']
|
|
848
|
+
transaction_pin = member['transaction_pin']
|
|
849
|
+
applied_kitta = member['applied_kitta']
|
|
850
|
+
crn_number = member['crn_number']
|
|
851
|
+
member_name = member['name']
|
|
852
|
+
else:
|
|
853
|
+
member_name = "Manual Entry"
|
|
854
|
+
dp_value = input("Enter DP value: ")
|
|
855
|
+
username = input("Enter username: ")
|
|
856
|
+
password = getpass.getpass("Enter password: ")
|
|
857
|
+
transaction_pin = getpass.getpass("Enter 4-digit transaction PIN: ")
|
|
858
|
+
applied_kitta = int(input("Applied Kitta: ").strip() or "10")
|
|
859
|
+
crn_number = input("CRN Number: ").strip()
|
|
860
|
+
|
|
861
|
+
if not crn_number:
|
|
862
|
+
print(f"\n✗ CRN number is required!")
|
|
863
|
+
return
|
|
864
|
+
|
|
865
|
+
print(f"\n✓ Applying IPO for: {member_name}")
|
|
866
|
+
print(f"✓ Kitta: {applied_kitta} | CRN: {crn_number}")
|
|
867
|
+
|
|
868
|
+
with sync_playwright() as p:
|
|
869
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
870
|
+
context = browser.new_context()
|
|
871
|
+
page = context.new_page()
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
# ========== PHASE 1: LOGIN ==========
|
|
875
|
+
print("\n" + "="*60)
|
|
876
|
+
print("PHASE 1: LOGIN")
|
|
877
|
+
print("="*60)
|
|
878
|
+
|
|
879
|
+
print(); print_progress(1, 6, "Navigating to Meroshare...")
|
|
880
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
881
|
+
time.sleep(2)
|
|
882
|
+
|
|
883
|
+
print_progress(2, 6, "Opening DP dropdown...")
|
|
884
|
+
page.click("span.select2-selection")
|
|
885
|
+
time.sleep(1)
|
|
886
|
+
|
|
887
|
+
print_progress(3, 6, f"Selecting DP (value: {dp_value})...")
|
|
888
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
889
|
+
|
|
890
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
891
|
+
if search_box:
|
|
892
|
+
print(f" → Searching for DP value {dp_value}...")
|
|
893
|
+
search_box.type(dp_value)
|
|
894
|
+
time.sleep(0.5)
|
|
895
|
+
|
|
896
|
+
# Click the first result or press Enter
|
|
897
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
898
|
+
if first_result:
|
|
899
|
+
first_result.click()
|
|
900
|
+
else:
|
|
901
|
+
page.keyboard.press("Enter")
|
|
902
|
+
else:
|
|
903
|
+
# Fallback: click by text
|
|
904
|
+
try:
|
|
905
|
+
results = page.query_selector_all("li.select2-results__option")
|
|
906
|
+
for result in results:
|
|
907
|
+
if dp_value in result.inner_text():
|
|
908
|
+
result.click()
|
|
909
|
+
break
|
|
910
|
+
except:
|
|
911
|
+
print(" ⚠ Using fallback selection method...")
|
|
912
|
+
page.select_option("select.select2-hidden-accessible", dp_value)
|
|
913
|
+
|
|
914
|
+
time.sleep(1)
|
|
915
|
+
|
|
916
|
+
print_progress(4, 6, "Filling username...")
|
|
917
|
+
username_selectors = [
|
|
918
|
+
"input[formcontrolname='username']",
|
|
919
|
+
"input#username",
|
|
920
|
+
"input[placeholder*='User']"
|
|
921
|
+
]
|
|
922
|
+
for selector in username_selectors:
|
|
923
|
+
try:
|
|
924
|
+
page.fill(selector, username, timeout=2000)
|
|
925
|
+
break
|
|
926
|
+
except:
|
|
927
|
+
continue
|
|
928
|
+
|
|
929
|
+
print_progress(5, 6, "Filling password...")
|
|
930
|
+
password_selectors = [
|
|
931
|
+
"input[formcontrolname='password']",
|
|
932
|
+
"input[type='password']"
|
|
933
|
+
]
|
|
934
|
+
for selector in password_selectors:
|
|
935
|
+
try:
|
|
936
|
+
page.fill(selector, password, timeout=2000)
|
|
937
|
+
break
|
|
938
|
+
except:
|
|
939
|
+
continue
|
|
940
|
+
|
|
941
|
+
print_progress(6, 6, "Clicking login button...")
|
|
942
|
+
login_button_selectors = [
|
|
943
|
+
"button.btn.sign-in",
|
|
944
|
+
"button[type='submit']",
|
|
945
|
+
"button:has-text('Login')"
|
|
946
|
+
]
|
|
947
|
+
for selector in login_button_selectors:
|
|
948
|
+
try:
|
|
949
|
+
page.click(selector, timeout=2000)
|
|
950
|
+
break
|
|
951
|
+
except:
|
|
952
|
+
continue
|
|
953
|
+
|
|
954
|
+
print("\nWaiting for login...")
|
|
955
|
+
try:
|
|
956
|
+
page.wait_for_function("window.location.hash !== '#/login'", timeout=8000)
|
|
957
|
+
time.sleep(2)
|
|
958
|
+
except:
|
|
959
|
+
print(" (timeout, but may still be logged in)")
|
|
960
|
+
|
|
961
|
+
if "#/login" not in page.url.lower():
|
|
962
|
+
print("✓ Login successful!")
|
|
963
|
+
else:
|
|
964
|
+
print("⚠ Login may have failed, but continuing to portfolio...")
|
|
965
|
+
|
|
966
|
+
# Navigate to Portfolio
|
|
967
|
+
print("\n📊 Navigating to Portfolio page...")
|
|
968
|
+
page.goto("https://meroshare.cdsc.com.np/#/portfolio", wait_until="networkidle")
|
|
969
|
+
time.sleep(3)
|
|
970
|
+
|
|
971
|
+
print("Fetching holdings...\n")
|
|
972
|
+
|
|
973
|
+
# Extract portfolio data with correct selectors
|
|
974
|
+
try:
|
|
975
|
+
# Wait for the table to load (Angular app with _ngcontent attributes)
|
|
976
|
+
print("Waiting for portfolio table to load...")
|
|
977
|
+
page.wait_for_selector("table.table tbody tr", timeout=10000)
|
|
978
|
+
time.sleep(2)
|
|
979
|
+
|
|
980
|
+
# Get all data rows (excluding the total row)
|
|
981
|
+
rows = page.query_selector_all("table.table tbody:first-of-type tr")
|
|
982
|
+
|
|
983
|
+
if rows and len(rows) > 0:
|
|
984
|
+
table = Table(title="YOUR PORTFOLIO HOLDINGS", box=box.ROUNDED, header_style="bold cyan", expand=True)
|
|
985
|
+
table.add_column("#", style="dim", width=4)
|
|
986
|
+
table.add_column("Scrip", style="bold white")
|
|
987
|
+
table.add_column("Balance", justify="right")
|
|
988
|
+
table.add_column("Last Price", justify="right")
|
|
989
|
+
table.add_column("Value (Last)", justify="right")
|
|
990
|
+
table.add_column("LTP", justify="right")
|
|
991
|
+
table.add_column("Value (LTP)", justify="right", style="green")
|
|
992
|
+
|
|
993
|
+
portfolio_data = []
|
|
994
|
+
total_value_last = 0
|
|
995
|
+
total_value_ltp = 0
|
|
996
|
+
|
|
997
|
+
for row in rows:
|
|
998
|
+
cells = row.query_selector_all("td")
|
|
999
|
+
if cells and len(cells) >= 7:
|
|
1000
|
+
# Extract each column
|
|
1001
|
+
num = cells[0].inner_text().strip()
|
|
1002
|
+
scrip = cells[1].inner_text().strip()
|
|
1003
|
+
balance = cells[2].inner_text().strip()
|
|
1004
|
+
last_price = cells[3].inner_text().strip()
|
|
1005
|
+
value_last = cells[4].inner_text().strip()
|
|
1006
|
+
ltp = cells[5].inner_text().strip()
|
|
1007
|
+
value_ltp = cells[6].inner_text().strip()
|
|
1008
|
+
|
|
1009
|
+
# Store as structured data
|
|
1010
|
+
holding = {
|
|
1011
|
+
"number": num,
|
|
1012
|
+
"scrip": scrip,
|
|
1013
|
+
"current_balance": balance,
|
|
1014
|
+
"last_closing_price": last_price,
|
|
1015
|
+
"value_as_of_last_price": value_last,
|
|
1016
|
+
"last_transaction_price": ltp,
|
|
1017
|
+
"value_as_of_ltp": value_ltp
|
|
1018
|
+
}
|
|
1019
|
+
portfolio_data.append(holding)
|
|
1020
|
+
|
|
1021
|
+
table.add_row(num, scrip, balance, last_price, value_last, ltp, value_ltp)
|
|
1022
|
+
|
|
1023
|
+
# Get total row (from second tbody)
|
|
1024
|
+
total_rows = page.query_selector_all("table.table tbody:last-of-type tr")
|
|
1025
|
+
if total_rows and len(total_rows) > 0:
|
|
1026
|
+
total_cells = total_rows[0].query_selector_all("td")
|
|
1027
|
+
if total_cells and len(total_cells) >= 5:
|
|
1028
|
+
total_last = total_cells[4].inner_text().strip()
|
|
1029
|
+
total_ltp = total_cells[6].inner_text().strip() if len(total_cells) > 6 else ""
|
|
1030
|
+
|
|
1031
|
+
table.add_section()
|
|
1032
|
+
table.add_row("TOTAL", "", "", "", total_last, "", total_ltp, style="bold")
|
|
1033
|
+
|
|
1034
|
+
console.print(table)
|
|
1035
|
+
console.print(f"✓ Total holdings: {len(portfolio_data)} scrips\n")
|
|
1036
|
+
|
|
1037
|
+
# Save to JSON with metadata
|
|
1038
|
+
output_file = "portfolio_data.json"
|
|
1039
|
+
output = {
|
|
1040
|
+
"fetched_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
1041
|
+
"total_scrips": len(portfolio_data),
|
|
1042
|
+
"holdings": portfolio_data
|
|
1043
|
+
}
|
|
1044
|
+
with open(output_file, 'w') as f:
|
|
1045
|
+
json.dump(output, f, indent=2)
|
|
1046
|
+
print(f"✓ Portfolio data saved to {output_file}")
|
|
1047
|
+
|
|
1048
|
+
else:
|
|
1049
|
+
console.print(Panel("⚠ No portfolio data found.", style="bold yellow", box=box.ROUNDED))
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
print(f"⚠ Error extracting portfolio: {e}")
|
|
1052
|
+
screenshot_path = "portfolio_error.png"
|
|
1053
|
+
page.screenshot(path=screenshot_path, full_page=True)
|
|
1054
|
+
print(f"📸 Screenshot saved to {screenshot_path}")
|
|
1055
|
+
|
|
1056
|
+
# Keep browser open in non-headless mode
|
|
1057
|
+
if not headless:
|
|
1058
|
+
print("\nBrowser will stay open for 30 seconds...")
|
|
1059
|
+
time.sleep(30)
|
|
1060
|
+
|
|
1061
|
+
except Exception as e:
|
|
1062
|
+
print(f"\n✗ Error: {e}")
|
|
1063
|
+
if not headless:
|
|
1064
|
+
time.sleep(5)
|
|
1065
|
+
finally:
|
|
1066
|
+
browser.close()
|
|
1067
|
+
|
|
1068
|
+
def load_ipo_config():
|
|
1069
|
+
"""Load IPO application configuration"""
|
|
1070
|
+
if not IPO_CONFIG_FILE.exists():
|
|
1071
|
+
print(f"\n⚠ IPO config file not found. Creating template...")
|
|
1072
|
+
default_config = {
|
|
1073
|
+
"applied_kitta": 10,
|
|
1074
|
+
"crn_number": "YOUR_CRN_NUMBER_HERE"
|
|
1075
|
+
}
|
|
1076
|
+
with open(IPO_CONFIG_FILE, 'w') as f:
|
|
1077
|
+
json.dump(default_config, f, indent=2)
|
|
1078
|
+
print(f"✓ Created {IPO_CONFIG_FILE}")
|
|
1079
|
+
print(f"⚠ Please edit {IPO_CONFIG_FILE} with your actual CRN number before applying!\n")
|
|
1080
|
+
return default_config
|
|
1081
|
+
|
|
1082
|
+
with open(IPO_CONFIG_FILE, 'r') as f:
|
|
1083
|
+
return json.load(f)
|
|
1084
|
+
|
|
1085
|
+
def apply_ipo(auto_load=True, headless=False):
|
|
1086
|
+
"""
|
|
1087
|
+
Complete IPO application automation with family member selection
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
auto_load: Load credentials from config file
|
|
1091
|
+
headless: Run browser in headless mode (no GUI)
|
|
1092
|
+
"""
|
|
1093
|
+
if auto_load:
|
|
1094
|
+
# Select family member
|
|
1095
|
+
member = select_family_member()
|
|
1096
|
+
if not member:
|
|
1097
|
+
print("\n✗ No member selected. Exiting...")
|
|
1098
|
+
return
|
|
1099
|
+
|
|
1100
|
+
dp_value = member['dp_value']
|
|
1101
|
+
username = member['username']
|
|
1102
|
+
password = member['password']
|
|
1103
|
+
transaction_pin = member['transaction_pin']
|
|
1104
|
+
applied_kitta = member['applied_kitta']
|
|
1105
|
+
crn_number = member['crn_number']
|
|
1106
|
+
member_name = member['name']
|
|
1107
|
+
else:
|
|
1108
|
+
member_name = "Manual Entry"
|
|
1109
|
+
dp_value = input("Enter DP value: ")
|
|
1110
|
+
username = input("Enter username: ")
|
|
1111
|
+
password = getpass.getpass("Enter password: ")
|
|
1112
|
+
transaction_pin = getpass.getpass("Enter 4-digit transaction PIN: ")
|
|
1113
|
+
applied_kitta = int(input("Applied Kitta: ").strip() or "10")
|
|
1114
|
+
crn_number = input("CRN Number: ").strip()
|
|
1115
|
+
|
|
1116
|
+
if not crn_number:
|
|
1117
|
+
print(f"\n✗ CRN number is required!")
|
|
1118
|
+
return
|
|
1119
|
+
|
|
1120
|
+
print(f"\n✓ Applying IPO for: {member_name}")
|
|
1121
|
+
print(f"✓ Kitta: {applied_kitta} | CRN: {crn_number}")
|
|
1122
|
+
|
|
1123
|
+
with sync_playwright() as p:
|
|
1124
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
1125
|
+
context = browser.new_context()
|
|
1126
|
+
page = context.new_page()
|
|
1127
|
+
|
|
1128
|
+
try:
|
|
1129
|
+
# ========== PHASE 1: LOGIN ==========
|
|
1130
|
+
print("\n" + "="*60)
|
|
1131
|
+
print("PHASE 1: LOGIN")
|
|
1132
|
+
print("="*60)
|
|
1133
|
+
|
|
1134
|
+
print(); print_progress(1, 6, "Navigating to Meroshare...")
|
|
1135
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
1136
|
+
time.sleep(2)
|
|
1137
|
+
|
|
1138
|
+
print_progress(2, 6, "Opening DP dropdown...")
|
|
1139
|
+
page.click("span.select2-selection")
|
|
1140
|
+
time.sleep(1)
|
|
1141
|
+
|
|
1142
|
+
print_progress(3, 6, f"Selecting DP (value: {dp_value})...")
|
|
1143
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
1144
|
+
|
|
1145
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
1146
|
+
if search_box:
|
|
1147
|
+
print(f" → Searching for DP value {dp_value}...")
|
|
1148
|
+
search_box.type(dp_value)
|
|
1149
|
+
time.sleep(0.5)
|
|
1150
|
+
|
|
1151
|
+
# Click the first result or press Enter
|
|
1152
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
1153
|
+
if first_result:
|
|
1154
|
+
first_result.click()
|
|
1155
|
+
else:
|
|
1156
|
+
page.keyboard.press("Enter")
|
|
1157
|
+
else:
|
|
1158
|
+
# Fallback: click by text
|
|
1159
|
+
try:
|
|
1160
|
+
results = page.query_selector_all("li.select2-results__option")
|
|
1161
|
+
for result in results:
|
|
1162
|
+
if dp_value in result.inner_text():
|
|
1163
|
+
result.click()
|
|
1164
|
+
break
|
|
1165
|
+
except:
|
|
1166
|
+
print(" ⚠ Using fallback selection method...")
|
|
1167
|
+
page.select_option("select.select2-hidden-accessible", dp_value)
|
|
1168
|
+
|
|
1169
|
+
time.sleep(1)
|
|
1170
|
+
|
|
1171
|
+
print_progress(4, 6, "Filling username...")
|
|
1172
|
+
username_selectors = [
|
|
1173
|
+
"input[formcontrolname='username']",
|
|
1174
|
+
"input#username",
|
|
1175
|
+
"input[placeholder*='User']"
|
|
1176
|
+
]
|
|
1177
|
+
for selector in username_selectors:
|
|
1178
|
+
try:
|
|
1179
|
+
page.fill(selector, username, timeout=2000)
|
|
1180
|
+
break
|
|
1181
|
+
except:
|
|
1182
|
+
continue
|
|
1183
|
+
|
|
1184
|
+
print_progress(5, 6, "Filling password...")
|
|
1185
|
+
password_selectors = [
|
|
1186
|
+
"input[formcontrolname='password']",
|
|
1187
|
+
"input[type='password']"
|
|
1188
|
+
]
|
|
1189
|
+
for selector in password_selectors:
|
|
1190
|
+
try:
|
|
1191
|
+
page.fill(selector, password, timeout=2000)
|
|
1192
|
+
break
|
|
1193
|
+
except:
|
|
1194
|
+
continue
|
|
1195
|
+
|
|
1196
|
+
print_progress(6, 6, "Clicking login button...")
|
|
1197
|
+
login_button_selectors = [
|
|
1198
|
+
"button.btn.sign-in",
|
|
1199
|
+
"button[type='submit']",
|
|
1200
|
+
"button:has-text('Login')"
|
|
1201
|
+
]
|
|
1202
|
+
for selector in login_button_selectors:
|
|
1203
|
+
try:
|
|
1204
|
+
page.click(selector, timeout=2000)
|
|
1205
|
+
break
|
|
1206
|
+
except:
|
|
1207
|
+
continue
|
|
1208
|
+
|
|
1209
|
+
print("\nWaiting for login...")
|
|
1210
|
+
try:
|
|
1211
|
+
page.wait_for_function("window.location.hash !== '#/login'", timeout=8000)
|
|
1212
|
+
time.sleep(2)
|
|
1213
|
+
except:
|
|
1214
|
+
print(" (timeout, but may still be logged in)")
|
|
1215
|
+
|
|
1216
|
+
# ========== PHASE 2: FETCH AVAILABLE IPOs ==========
|
|
1217
|
+
print("\n" + "="*60)
|
|
1218
|
+
print("PHASE 2: FETCH AVAILABLE IPOs")
|
|
1219
|
+
print("="*60)
|
|
1220
|
+
|
|
1221
|
+
print("\nNavigating to ASBA page...")
|
|
1222
|
+
page.goto("https://meroshare.cdsc.com.np/#/asba", wait_until="networkidle")
|
|
1223
|
+
time.sleep(3)
|
|
1224
|
+
|
|
1225
|
+
print("Fetching IPO list...\n")
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
page.wait_for_selector(".company-list", timeout=10000)
|
|
1229
|
+
time.sleep(2)
|
|
1230
|
+
except Exception as e:
|
|
1231
|
+
print("⚠ No IPOs currently available on Meroshare")
|
|
1232
|
+
print("✗ Cannot proceed with IPO application\n")
|
|
1233
|
+
|
|
1234
|
+
try:
|
|
1235
|
+
no_data = page.query_selector("text=No Data Available")
|
|
1236
|
+
if no_data:
|
|
1237
|
+
print("→ Meroshare shows: 'No Data Available'")
|
|
1238
|
+
except:
|
|
1239
|
+
pass
|
|
1240
|
+
|
|
1241
|
+
page.screenshot(path="no_ipos_available.png")
|
|
1242
|
+
print("📸 Screenshot saved: no_ipos_available.png\n")
|
|
1243
|
+
|
|
1244
|
+
if not headless:
|
|
1245
|
+
print("Browser will stay open for 20 seconds...")
|
|
1246
|
+
time.sleep(20)
|
|
1247
|
+
|
|
1248
|
+
return
|
|
1249
|
+
|
|
1250
|
+
company_rows = page.query_selector_all(".company-list")
|
|
1251
|
+
|
|
1252
|
+
available_ipos = []
|
|
1253
|
+
for idx, row in enumerate(company_rows, 1):
|
|
1254
|
+
try:
|
|
1255
|
+
company_name_elem = row.query_selector(".company-name span")
|
|
1256
|
+
share_type_elem = row.query_selector(".share-of-type")
|
|
1257
|
+
share_group_elem = row.query_selector(".isin")
|
|
1258
|
+
|
|
1259
|
+
if company_name_elem and share_type_elem and share_group_elem:
|
|
1260
|
+
company_name = company_name_elem.inner_text().strip()
|
|
1261
|
+
share_type = share_type_elem.inner_text().strip()
|
|
1262
|
+
share_group = share_group_elem.inner_text().strip()
|
|
1263
|
+
|
|
1264
|
+
if "ipo" in share_type.lower() and "ordinary" in share_group.lower():
|
|
1265
|
+
# Check for both Apply and Edit buttons
|
|
1266
|
+
apply_button = row.query_selector("button.btn-issue")
|
|
1267
|
+
|
|
1268
|
+
# Check if IPO is already applied (button shows 'Edit')
|
|
1269
|
+
is_applied = False
|
|
1270
|
+
button_text = ""
|
|
1271
|
+
if apply_button:
|
|
1272
|
+
button_text = apply_button.inner_text().strip().lower()
|
|
1273
|
+
is_applied = "edit" in button_text or "view" in button_text
|
|
1274
|
+
|
|
1275
|
+
if apply_button:
|
|
1276
|
+
available_ipos.append({
|
|
1277
|
+
"index": len(available_ipos) + 1,
|
|
1278
|
+
"company_name": company_name,
|
|
1279
|
+
"share_type": share_type,
|
|
1280
|
+
"share_group": share_group,
|
|
1281
|
+
"element": row,
|
|
1282
|
+
"apply_button": apply_button,
|
|
1283
|
+
"is_applied": is_applied,
|
|
1284
|
+
"button_text": button_text
|
|
1285
|
+
})
|
|
1286
|
+
except Exception as e:
|
|
1287
|
+
print(f" Error parsing row {idx}: {e}")
|
|
1288
|
+
|
|
1289
|
+
if not available_ipos:
|
|
1290
|
+
print("✗ No IPOs (Ordinary Shares) available to apply!")
|
|
1291
|
+
page.screenshot(path="no_ipos_found.png")
|
|
1292
|
+
return
|
|
1293
|
+
|
|
1294
|
+
print("="*60)
|
|
1295
|
+
print("AVAILABLE IPOs (Ordinary Shares)")
|
|
1296
|
+
print("="*60)
|
|
1297
|
+
for ipo in available_ipos:
|
|
1298
|
+
print(f"{ipo['index']}. {ipo['company_name']}")
|
|
1299
|
+
print(f" Type: {ipo['share_type']} | Group: {ipo['share_group']}")
|
|
1300
|
+
print()
|
|
1301
|
+
print("="*60)
|
|
1302
|
+
|
|
1303
|
+
if not headless:
|
|
1304
|
+
selection = input(f"\nEnter IPO number to apply (1-{len(available_ipos)}): ").strip()
|
|
1305
|
+
try:
|
|
1306
|
+
selected_idx = int(selection) - 1
|
|
1307
|
+
if selected_idx < 0 or selected_idx >= len(available_ipos):
|
|
1308
|
+
print("✗ Invalid selection!")
|
|
1309
|
+
return
|
|
1310
|
+
except ValueError:
|
|
1311
|
+
print("✗ Invalid input!")
|
|
1312
|
+
return
|
|
1313
|
+
else:
|
|
1314
|
+
selected_idx = 0
|
|
1315
|
+
print(f"\n→ Auto-selecting IPO #1: {available_ipos[0]['company_name']}")
|
|
1316
|
+
|
|
1317
|
+
selected_ipo = available_ipos[selected_idx]
|
|
1318
|
+
print(f"\n✓ Selected: {selected_ipo['company_name']}\n")
|
|
1319
|
+
|
|
1320
|
+
# Check if IPO is already applied
|
|
1321
|
+
if selected_ipo.get('is_applied', False):
|
|
1322
|
+
print(f"⚠ IPO already applied for this account!")
|
|
1323
|
+
print(f" Button shows: '{selected_ipo.get('button_text', 'N/A').title()}'")
|
|
1324
|
+
print(f" (Edit button indicates IPO was already applied)")
|
|
1325
|
+
page.screenshot(path="ipo_already_applied.png")
|
|
1326
|
+
print("📸 Screenshot saved: ipo_already_applied.png\n")
|
|
1327
|
+
|
|
1328
|
+
if not headless:
|
|
1329
|
+
print("Browser will stay open for 20 seconds...")
|
|
1330
|
+
time.sleep(20)
|
|
1331
|
+
return
|
|
1332
|
+
|
|
1333
|
+
print("Clicking Apply button...")
|
|
1334
|
+
selected_ipo['apply_button'].click()
|
|
1335
|
+
time.sleep(3)
|
|
1336
|
+
|
|
1337
|
+
page.screenshot(path="ipo_form_loaded.png")
|
|
1338
|
+
print("✓ IPO form loaded")
|
|
1339
|
+
|
|
1340
|
+
# ========== PHASE 3: FILL IPO APPLICATION FORM ==========
|
|
1341
|
+
print("\n" + "="*60)
|
|
1342
|
+
print("PHASE 3: FILL APPLICATION FORM")
|
|
1343
|
+
print("="*60)
|
|
1344
|
+
|
|
1345
|
+
page.wait_for_selector("select#selectBank", timeout=10000)
|
|
1346
|
+
time.sleep(2)
|
|
1347
|
+
|
|
1348
|
+
print(); print_progress(1, 5, "Selecting bank...")
|
|
1349
|
+
bank_options = page.query_selector_all("select#selectBank option")
|
|
1350
|
+
valid_banks = [opt for opt in bank_options if opt.get_attribute("value")]
|
|
1351
|
+
|
|
1352
|
+
if len(valid_banks) == 1:
|
|
1353
|
+
bank_value = valid_banks[0].get_attribute("value")
|
|
1354
|
+
bank_name = valid_banks[0].inner_text().strip()
|
|
1355
|
+
print(f" → Auto-selected: {bank_name}")
|
|
1356
|
+
page.select_option("select#selectBank", bank_value)
|
|
1357
|
+
elif len(valid_banks) > 1:
|
|
1358
|
+
print(f" → Found {len(valid_banks)} banks, selecting first one")
|
|
1359
|
+
bank_value = valid_banks[0].get_attribute("value")
|
|
1360
|
+
page.select_option("select#selectBank", bank_value)
|
|
1361
|
+
else:
|
|
1362
|
+
print(" ✗ No banks found!")
|
|
1363
|
+
return
|
|
1364
|
+
|
|
1365
|
+
time.sleep(2)
|
|
1366
|
+
|
|
1367
|
+
print(); print_progress(2, 5, "Selecting account number...")
|
|
1368
|
+
page.wait_for_selector("select#accountNumber", timeout=5000)
|
|
1369
|
+
account_options = page.query_selector_all("select#accountNumber option")
|
|
1370
|
+
valid_accounts = [opt for opt in account_options if opt.get_attribute("value")]
|
|
1371
|
+
|
|
1372
|
+
if len(valid_accounts) == 1:
|
|
1373
|
+
account_value = valid_accounts[0].get_attribute("value")
|
|
1374
|
+
account_text = valid_accounts[0].inner_text().strip()
|
|
1375
|
+
print(f" → Auto-selected: {account_text}")
|
|
1376
|
+
page.select_option("select#accountNumber", account_value)
|
|
1377
|
+
elif len(valid_accounts) > 1:
|
|
1378
|
+
print(f" → Found {len(valid_accounts)} accounts, selecting first one")
|
|
1379
|
+
account_value = valid_accounts[0].get_attribute("value")
|
|
1380
|
+
page.select_option("select#accountNumber", account_value)
|
|
1381
|
+
else:
|
|
1382
|
+
print(" ✗ No accounts found!")
|
|
1383
|
+
return
|
|
1384
|
+
|
|
1385
|
+
time.sleep(2)
|
|
1386
|
+
|
|
1387
|
+
print(); print_progress(3, 5, "Waiting for branch to auto-fill...")
|
|
1388
|
+
time.sleep(1)
|
|
1389
|
+
branch_value = page.input_value("input#selectBranch")
|
|
1390
|
+
if branch_value:
|
|
1391
|
+
print(f" → Branch: {branch_value}")
|
|
1392
|
+
|
|
1393
|
+
print(); print_progress(4, 5, f"Filling applied kitta: {applied_kitta}")
|
|
1394
|
+
page.fill("input#appliedKitta", str(applied_kitta))
|
|
1395
|
+
time.sleep(1)
|
|
1396
|
+
|
|
1397
|
+
amount_value = page.input_value("input#amount")
|
|
1398
|
+
print(f" → Amount: {amount_value}")
|
|
1399
|
+
|
|
1400
|
+
print(); print_progress(5, 5, f"Filling CRN: {crn_number}")
|
|
1401
|
+
page.fill("input#crnNumber", crn_number)
|
|
1402
|
+
time.sleep(1)
|
|
1403
|
+
|
|
1404
|
+
page.screenshot(path="form_filled.png")
|
|
1405
|
+
print("\n✓ Form filled successfully")
|
|
1406
|
+
|
|
1407
|
+
# ========== PHASE 4: ACCEPT DISCLAIMER & PROCEED ==========
|
|
1408
|
+
print("\n" + "="*60)
|
|
1409
|
+
print("PHASE 4: ACCEPT DISCLAIMER & PROCEED")
|
|
1410
|
+
print("="*60)
|
|
1411
|
+
|
|
1412
|
+
print("\nChecking disclaimer checkbox...")
|
|
1413
|
+
disclaimer_checkbox = page.query_selector("input#disclaimer")
|
|
1414
|
+
if disclaimer_checkbox:
|
|
1415
|
+
disclaimer_checkbox.check()
|
|
1416
|
+
print("✓ Disclaimer accepted")
|
|
1417
|
+
else:
|
|
1418
|
+
print("⚠ Disclaimer checkbox not found")
|
|
1419
|
+
|
|
1420
|
+
time.sleep(1)
|
|
1421
|
+
|
|
1422
|
+
print("\nClicking Proceed button...")
|
|
1423
|
+
proceed_button = page.query_selector("button.btn-primary[type='submit']")
|
|
1424
|
+
if proceed_button:
|
|
1425
|
+
proceed_button.click()
|
|
1426
|
+
print("✓ Clicked Proceed")
|
|
1427
|
+
else:
|
|
1428
|
+
print("✗ Proceed button not found!")
|
|
1429
|
+
page.screenshot(path="proceed_error.png")
|
|
1430
|
+
return
|
|
1431
|
+
|
|
1432
|
+
time.sleep(3)
|
|
1433
|
+
|
|
1434
|
+
# ========== PHASE 5: ENTER TRANSACTION PIN ==========
|
|
1435
|
+
print("\n" + "="*60)
|
|
1436
|
+
print("PHASE 5: ENTER TRANSACTION PIN")
|
|
1437
|
+
print("="*60)
|
|
1438
|
+
|
|
1439
|
+
print("\nWaiting for PIN entry screen...")
|
|
1440
|
+
page.wait_for_selector("input#transactionPIN", timeout=10000)
|
|
1441
|
+
time.sleep(2)
|
|
1442
|
+
|
|
1443
|
+
page.screenshot(path="pin_screen.png")
|
|
1444
|
+
print("✓ PIN entry screen loaded")
|
|
1445
|
+
|
|
1446
|
+
print(f"\nEntering transaction PIN...")
|
|
1447
|
+
page.fill("input#transactionPIN", transaction_pin)
|
|
1448
|
+
print("✓ PIN entered")
|
|
1449
|
+
|
|
1450
|
+
time.sleep(1)
|
|
1451
|
+
|
|
1452
|
+
# ========== PHASE 6: FINAL SUBMISSION ==========
|
|
1453
|
+
print("\n" + "="*60)
|
|
1454
|
+
print("PHASE 6: FINAL SUBMISSION")
|
|
1455
|
+
print("="*60)
|
|
1456
|
+
|
|
1457
|
+
if not headless:
|
|
1458
|
+
confirm = input("\n⚠ Ready to submit application? (yes/no): ").strip().lower()
|
|
1459
|
+
if confirm != 'yes':
|
|
1460
|
+
print("✗ Application cancelled by user")
|
|
1461
|
+
return
|
|
1462
|
+
|
|
1463
|
+
print("\nSubmitting application...")
|
|
1464
|
+
|
|
1465
|
+
# Wait a bit more for the button to be fully ready
|
|
1466
|
+
time.sleep(2)
|
|
1467
|
+
|
|
1468
|
+
# Try multiple methods to click the Apply button
|
|
1469
|
+
clicked = False
|
|
1470
|
+
|
|
1471
|
+
# Method 1: Find button with text "Apply" (more reliable)
|
|
1472
|
+
try:
|
|
1473
|
+
apply_buttons = page.query_selector_all("button:has-text('Apply')")
|
|
1474
|
+
for btn in apply_buttons:
|
|
1475
|
+
if btn.is_visible() and not btn.is_disabled():
|
|
1476
|
+
btn.click()
|
|
1477
|
+
print("✓ Submit button clicked (Method 1)")
|
|
1478
|
+
clicked = True
|
|
1479
|
+
break
|
|
1480
|
+
except Exception as e:
|
|
1481
|
+
print(f" Method 1 failed: {e}")
|
|
1482
|
+
|
|
1483
|
+
# Method 2: Find by class and type in the confirm page
|
|
1484
|
+
if not clicked:
|
|
1485
|
+
try:
|
|
1486
|
+
submit_button = page.query_selector("div.confirm-page-btn button.btn-primary[type='submit']")
|
|
1487
|
+
if submit_button and submit_button.is_visible():
|
|
1488
|
+
submit_button.click()
|
|
1489
|
+
print("✓ Submit button clicked (Method 2)")
|
|
1490
|
+
clicked = True
|
|
1491
|
+
except Exception as e:
|
|
1492
|
+
print(f" Method 2 failed: {e}")
|
|
1493
|
+
|
|
1494
|
+
# Method 3: Find any submit button in the confirmation section
|
|
1495
|
+
if not clicked:
|
|
1496
|
+
try:
|
|
1497
|
+
submit_button = page.query_selector("button.btn-gap.btn-primary[type='submit']")
|
|
1498
|
+
if submit_button and submit_button.is_visible():
|
|
1499
|
+
submit_button.click()
|
|
1500
|
+
print("✓ Submit button clicked (Method 3)")
|
|
1501
|
+
clicked = True
|
|
1502
|
+
except Exception as e:
|
|
1503
|
+
print(f" Method 3 failed: {e}")
|
|
1504
|
+
|
|
1505
|
+
# Method 4: Force click using JavaScript
|
|
1506
|
+
if not clicked:
|
|
1507
|
+
try:
|
|
1508
|
+
page.evaluate("""
|
|
1509
|
+
const buttons = document.querySelectorAll('button');
|
|
1510
|
+
for (const btn of buttons) {
|
|
1511
|
+
if (btn.textContent.includes('Apply') && btn.type === 'submit') {
|
|
1512
|
+
btn.click();
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
""")
|
|
1517
|
+
print("✓ Submit button clicked (Method 4 - JavaScript)")
|
|
1518
|
+
clicked = True
|
|
1519
|
+
except Exception as e:
|
|
1520
|
+
print(f" Method 4 failed: {e}")
|
|
1521
|
+
|
|
1522
|
+
if not clicked:
|
|
1523
|
+
print("✗ Failed to click submit button!")
|
|
1524
|
+
page.screenshot(path="submit_error.png")
|
|
1525
|
+
print("📸 Screenshot saved: submit_error.png")
|
|
1526
|
+
print("\nPlease click the Apply button manually.")
|
|
1527
|
+
if not headless:
|
|
1528
|
+
time.sleep(30)
|
|
1529
|
+
return
|
|
1530
|
+
|
|
1531
|
+
time.sleep(5)
|
|
1532
|
+
|
|
1533
|
+
page.screenshot(path="submission_result.png")
|
|
1534
|
+
print("\n✓✓✓ APPLICATION SUBMITTED! ✓✓✓")
|
|
1535
|
+
print(f"📸 Screenshots saved for verification")
|
|
1536
|
+
print(f"Current URL: {page.url}")
|
|
1537
|
+
|
|
1538
|
+
if not headless:
|
|
1539
|
+
print("\nBrowser will stay open for 30 seconds...")
|
|
1540
|
+
time.sleep(30)
|
|
1541
|
+
|
|
1542
|
+
except Exception as e:
|
|
1543
|
+
print(f"\n✗ Error: {e}")
|
|
1544
|
+
page.screenshot(path="error.png")
|
|
1545
|
+
if not headless:
|
|
1546
|
+
time.sleep(10)
|
|
1547
|
+
finally:
|
|
1548
|
+
browser.close()
|
|
1549
|
+
|
|
1550
|
+
def get_portfolio_for_member(member, headless=False):
|
|
1551
|
+
"""Get portfolio for a specific family member"""
|
|
1552
|
+
print(f"\nFetching portfolio for: {member['name']}...")
|
|
1553
|
+
|
|
1554
|
+
# Call existing get_portfolio but with member's credentials passed directly
|
|
1555
|
+
# We'll modify it to accept parameters
|
|
1556
|
+
with sync_playwright() as p:
|
|
1557
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
1558
|
+
context = browser.new_context()
|
|
1559
|
+
page = context.new_page()
|
|
1560
|
+
|
|
1561
|
+
try:
|
|
1562
|
+
dp_value = member['dp_value']
|
|
1563
|
+
username = member['username']
|
|
1564
|
+
password = member['password']
|
|
1565
|
+
|
|
1566
|
+
print(); print_progress(1, 7, "Navigating to Meroshare...")
|
|
1567
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
1568
|
+
time.sleep(2)
|
|
1569
|
+
|
|
1570
|
+
print_progress(2, 7, "Opening DP dropdown...")
|
|
1571
|
+
page.click("span.select2-selection")
|
|
1572
|
+
time.sleep(1)
|
|
1573
|
+
|
|
1574
|
+
print_progress(3, 7, f"Selecting DP...")
|
|
1575
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
1576
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
1577
|
+
if search_box:
|
|
1578
|
+
search_box.type(dp_value)
|
|
1579
|
+
time.sleep(0.5)
|
|
1580
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
1581
|
+
if first_result:
|
|
1582
|
+
first_result.click()
|
|
1583
|
+
else:
|
|
1584
|
+
page.keyboard.press("Enter")
|
|
1585
|
+
time.sleep(1)
|
|
1586
|
+
|
|
1587
|
+
print_progress(4, 7, "Filling username...")
|
|
1588
|
+
username_selectors = [
|
|
1589
|
+
"input[formcontrolname='username']",
|
|
1590
|
+
"input#username",
|
|
1591
|
+
"input[placeholder*='User']"
|
|
1592
|
+
]
|
|
1593
|
+
for selector in username_selectors:
|
|
1594
|
+
try:
|
|
1595
|
+
page.fill(selector, username, timeout=2000)
|
|
1596
|
+
break
|
|
1597
|
+
except:
|
|
1598
|
+
continue
|
|
1599
|
+
|
|
1600
|
+
print_progress(5, 7, "Filling password...")
|
|
1601
|
+
password_selectors = [
|
|
1602
|
+
"input[formcontrolname='password']",
|
|
1603
|
+
"input[type='password']"
|
|
1604
|
+
]
|
|
1605
|
+
for selector in password_selectors:
|
|
1606
|
+
try:
|
|
1607
|
+
page.fill(selector, password, timeout=2000)
|
|
1608
|
+
break
|
|
1609
|
+
except:
|
|
1610
|
+
continue
|
|
1611
|
+
|
|
1612
|
+
print_progress(6, 7, "Clicking login...")
|
|
1613
|
+
login_button_selectors = [
|
|
1614
|
+
"button.btn.sign-in",
|
|
1615
|
+
"button[type='submit']",
|
|
1616
|
+
"button:has-text('Login')"
|
|
1617
|
+
]
|
|
1618
|
+
for selector in login_button_selectors:
|
|
1619
|
+
try:
|
|
1620
|
+
page.click(selector, timeout=2000)
|
|
1621
|
+
break
|
|
1622
|
+
except:
|
|
1623
|
+
continue
|
|
1624
|
+
|
|
1625
|
+
print(); print_progress(7, 7, "Waiting for login...")
|
|
1626
|
+
page.wait_for_load_state("networkidle", timeout=8000)
|
|
1627
|
+
time.sleep(2)
|
|
1628
|
+
|
|
1629
|
+
print(f"✓ Logged in as {member['name']}")
|
|
1630
|
+
|
|
1631
|
+
# Navigate to portfolio
|
|
1632
|
+
print("\n📊 Navigating to Portfolio...")
|
|
1633
|
+
page.goto("https://meroshare.cdsc.com.np/#/portfolio", wait_until="networkidle")
|
|
1634
|
+
time.sleep(3)
|
|
1635
|
+
|
|
1636
|
+
print("Fetching holdings...\n")
|
|
1637
|
+
page.wait_for_selector("table.table tbody tr", timeout=10000)
|
|
1638
|
+
time.sleep(2)
|
|
1639
|
+
|
|
1640
|
+
rows = page.query_selector_all("table.table tbody:first-of-type tr")
|
|
1641
|
+
|
|
1642
|
+
if rows and len(rows) > 0:
|
|
1643
|
+
table = Table(title=f"PORTFOLIO: {member['name'].upper()}", box=box.ROUNDED, header_style="bold cyan", expand=True)
|
|
1644
|
+
table.add_column("#", style="dim", width=4)
|
|
1645
|
+
table.add_column("Scrip", style="bold white")
|
|
1646
|
+
table.add_column("Balance", justify="right")
|
|
1647
|
+
table.add_column("Last Price", justify="right")
|
|
1648
|
+
table.add_column("Value (Last)", justify="right")
|
|
1649
|
+
table.add_column("LTP", justify="right")
|
|
1650
|
+
table.add_column("Value (LTP)", justify="right", style="green")
|
|
1651
|
+
|
|
1652
|
+
total_value_ltp = 0.0
|
|
1653
|
+
|
|
1654
|
+
for row in rows:
|
|
1655
|
+
cells = row.query_selector_all("td")
|
|
1656
|
+
if cells and len(cells) >= 7:
|
|
1657
|
+
num = cells[0].inner_text().strip()
|
|
1658
|
+
scrip = cells[1].inner_text().strip()
|
|
1659
|
+
balance = cells[2].inner_text().strip()
|
|
1660
|
+
last_price = cells[3].inner_text().strip()
|
|
1661
|
+
value_last = cells[4].inner_text().strip()
|
|
1662
|
+
ltp = cells[5].inner_text().strip()
|
|
1663
|
+
value_ltp = cells[6].inner_text().strip()
|
|
1664
|
+
|
|
1665
|
+
# Calculate total
|
|
1666
|
+
try:
|
|
1667
|
+
value_ltp_num = float(value_ltp.replace(',', ''))
|
|
1668
|
+
total_value_ltp += value_ltp_num
|
|
1669
|
+
except:
|
|
1670
|
+
pass
|
|
1671
|
+
|
|
1672
|
+
table.add_row(num, scrip, balance, last_price, value_last, ltp, value_ltp)
|
|
1673
|
+
|
|
1674
|
+
table.add_section()
|
|
1675
|
+
table.add_row("TOTAL", "", "", "", "", "", f"Rs. {total_value_ltp:,.2f}", style="bold")
|
|
1676
|
+
|
|
1677
|
+
console.print(table)
|
|
1678
|
+
|
|
1679
|
+
if not headless:
|
|
1680
|
+
print("\nBrowser will stay open for 20 seconds...")
|
|
1681
|
+
time.sleep(20)
|
|
1682
|
+
|
|
1683
|
+
except Exception as e:
|
|
1684
|
+
print(f"\n✗ Error: {e}")
|
|
1685
|
+
finally:
|
|
1686
|
+
browser.close()
|
|
1687
|
+
|
|
1688
|
+
def test_login_for_member(member, headless=True):
|
|
1689
|
+
"""Test login for a specific family member"""
|
|
1690
|
+
print(f"\nTesting login for: {member['name']}...")
|
|
1691
|
+
|
|
1692
|
+
with sync_playwright() as p:
|
|
1693
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
1694
|
+
context = browser.new_context()
|
|
1695
|
+
page = context.new_page()
|
|
1696
|
+
|
|
1697
|
+
try:
|
|
1698
|
+
dp_value = member['dp_value']
|
|
1699
|
+
username = member['username']
|
|
1700
|
+
password = member['password']
|
|
1701
|
+
|
|
1702
|
+
print(); print_progress(1, 7, "Navigating to Meroshare...")
|
|
1703
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
1704
|
+
time.sleep(2)
|
|
1705
|
+
|
|
1706
|
+
print_progress(2, 7, "Opening DP dropdown...")
|
|
1707
|
+
page.click("span.select2-selection")
|
|
1708
|
+
time.sleep(1)
|
|
1709
|
+
|
|
1710
|
+
print_progress(3, 7, f"Selecting DP (value: {dp_value})...")
|
|
1711
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
1712
|
+
|
|
1713
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
1714
|
+
if search_box:
|
|
1715
|
+
print(f" → Searching for DP value {dp_value}...")
|
|
1716
|
+
search_box.type(dp_value)
|
|
1717
|
+
time.sleep(0.5)
|
|
1718
|
+
|
|
1719
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
1720
|
+
if first_result:
|
|
1721
|
+
first_result.click()
|
|
1722
|
+
else:
|
|
1723
|
+
page.keyboard.press("Enter")
|
|
1724
|
+
else:
|
|
1725
|
+
# Fallback: click by text
|
|
1726
|
+
try:
|
|
1727
|
+
results = page.query_selector_all("li.select2-results__option")
|
|
1728
|
+
for result in results:
|
|
1729
|
+
if dp_value in result.inner_text():
|
|
1730
|
+
result.click()
|
|
1731
|
+
break
|
|
1732
|
+
except:
|
|
1733
|
+
print(" ⚠ Using fallback selection method...")
|
|
1734
|
+
page.select_option("select.select2-hidden-accessible", dp_value)
|
|
1735
|
+
|
|
1736
|
+
time.sleep(1)
|
|
1737
|
+
|
|
1738
|
+
print_progress(4, 7, "Filling username...")
|
|
1739
|
+
username_selectors = [
|
|
1740
|
+
"input[formcontrolname='username']",
|
|
1741
|
+
"input#username",
|
|
1742
|
+
"input[placeholder*='User']"
|
|
1743
|
+
]
|
|
1744
|
+
for selector in username_selectors:
|
|
1745
|
+
try:
|
|
1746
|
+
page.fill(selector, username, timeout=2000)
|
|
1747
|
+
break
|
|
1748
|
+
except:
|
|
1749
|
+
continue
|
|
1750
|
+
|
|
1751
|
+
print_progress(5, 7, "Filling password...")
|
|
1752
|
+
password_selectors = [
|
|
1753
|
+
"input[formcontrolname='password']",
|
|
1754
|
+
"input[type='password']"
|
|
1755
|
+
]
|
|
1756
|
+
for selector in password_selectors:
|
|
1757
|
+
try:
|
|
1758
|
+
page.fill(selector, password, timeout=2000)
|
|
1759
|
+
break
|
|
1760
|
+
except:
|
|
1761
|
+
continue
|
|
1762
|
+
|
|
1763
|
+
print_progress(6, 7, "Clicking login button...")
|
|
1764
|
+
login_button_selectors = [
|
|
1765
|
+
"button.btn.sign-in",
|
|
1766
|
+
"button[type='submit']",
|
|
1767
|
+
"button:has-text('Login')"
|
|
1768
|
+
]
|
|
1769
|
+
for selector in login_button_selectors:
|
|
1770
|
+
try:
|
|
1771
|
+
page.click(selector, timeout=2000)
|
|
1772
|
+
break
|
|
1773
|
+
except:
|
|
1774
|
+
continue
|
|
1775
|
+
|
|
1776
|
+
print(); print_progress(7, 7, "Waiting for login...")
|
|
1777
|
+
try:
|
|
1778
|
+
page.wait_for_load_state("networkidle", timeout=8000)
|
|
1779
|
+
except:
|
|
1780
|
+
print(" (networkidle timeout - page may still be loading)")
|
|
1781
|
+
|
|
1782
|
+
time.sleep(2)
|
|
1783
|
+
|
|
1784
|
+
try:
|
|
1785
|
+
page.wait_for_function("window.location.hash !== '#/login'", timeout=3000)
|
|
1786
|
+
time.sleep(0.5)
|
|
1787
|
+
except:
|
|
1788
|
+
print(" (route didn't change, but may still be logged in)")
|
|
1789
|
+
|
|
1790
|
+
current_url = page.url
|
|
1791
|
+
print(f"\nCurrent URL: {current_url}")
|
|
1792
|
+
|
|
1793
|
+
if "#/login" not in current_url.lower():
|
|
1794
|
+
print(f"\n✓✓✓ LOGIN SUCCESSFUL for {member['name']}! ✓✓✓")
|
|
1795
|
+
else:
|
|
1796
|
+
print(f"\n⚠ Login may have failed for {member['name']}")
|
|
1797
|
+
page.screenshot(path=f"login_test_{member['name']}.png")
|
|
1798
|
+
|
|
1799
|
+
if not headless:
|
|
1800
|
+
print("\nBrowser will stay open for 20 seconds...")
|
|
1801
|
+
time.sleep(20)
|
|
1802
|
+
|
|
1803
|
+
except Exception as e:
|
|
1804
|
+
print(f"\n✗ Error: {e}")
|
|
1805
|
+
import traceback
|
|
1806
|
+
traceback.print_exc()
|
|
1807
|
+
finally:
|
|
1808
|
+
browser.close()
|
|
1809
|
+
|
|
1810
|
+
def apply_ipo_for_all_members(headless=True):
|
|
1811
|
+
"""Apply IPO for all family members - Sequential Login + Sequential Application"""
|
|
1812
|
+
|
|
1813
|
+
# Load family members
|
|
1814
|
+
config = load_family_members()
|
|
1815
|
+
members = config.get('members', [])
|
|
1816
|
+
|
|
1817
|
+
if not members:
|
|
1818
|
+
console.print(Panel("[bold red]⚠ No family members found. Add members first![/bold red]", box=box.ROUNDED, border_style="red"))
|
|
1819
|
+
return
|
|
1820
|
+
|
|
1821
|
+
# Display members FIRST
|
|
1822
|
+
table = Table(title="Family Members to Apply IPO", box=box.ROUNDED, header_style="bold cyan")
|
|
1823
|
+
table.add_column("No.", justify="right", style="cyan")
|
|
1824
|
+
table.add_column("Name", style="bold white")
|
|
1825
|
+
table.add_column("Kitta", justify="right", style="yellow")
|
|
1826
|
+
table.add_column("CRN", style="dim")
|
|
1827
|
+
|
|
1828
|
+
for idx, member in enumerate(members, 1):
|
|
1829
|
+
table.add_row(str(idx), member['name'], str(member['applied_kitta']), member['crn_number'])
|
|
1830
|
+
|
|
1831
|
+
console.print(table)
|
|
1832
|
+
|
|
1833
|
+
# Confirmation AFTER showing the list
|
|
1834
|
+
confirm = input(f"\n⚠️ Apply IPO for ALL {len(members)} members shown above? (yes/no): ").strip().lower()
|
|
1835
|
+
if confirm != 'yes':
|
|
1836
|
+
console.print("[bold red]✗ Operation cancelled[/bold red]\n")
|
|
1837
|
+
return
|
|
1838
|
+
|
|
1839
|
+
with sync_playwright() as p:
|
|
1840
|
+
browser = p.chromium.launch(headless=headless, slow_mo=100 if not headless else 0)
|
|
1841
|
+
context = browser.new_context()
|
|
1842
|
+
|
|
1843
|
+
try:
|
|
1844
|
+
# ========== PHASE 1: CREATE TABS & LOGIN ALL MEMBERS ==========
|
|
1845
|
+
console.print()
|
|
1846
|
+
console.print(Rule("[bold cyan]PHASE 1: MULTI-TAB LOGIN (ALL MEMBERS)[/bold cyan]"))
|
|
1847
|
+
console.print()
|
|
1848
|
+
|
|
1849
|
+
pages_data = []
|
|
1850
|
+
|
|
1851
|
+
# Create tabs and login sequentially (but keep all tabs open)
|
|
1852
|
+
console.print(f"[bold]🚀 Opening {len(members)} tabs and logging in...[/bold]\n")
|
|
1853
|
+
|
|
1854
|
+
for idx, member in enumerate(members, 1):
|
|
1855
|
+
member_name = member['name']
|
|
1856
|
+
page = context.new_page()
|
|
1857
|
+
|
|
1858
|
+
try:
|
|
1859
|
+
console.print(f"[cyan][Tab {idx}][/cyan] Starting login for: [bold]{member_name}[/bold]")
|
|
1860
|
+
|
|
1861
|
+
# Navigate
|
|
1862
|
+
page.goto("https://meroshare.cdsc.com.np/#/login", wait_until="networkidle")
|
|
1863
|
+
time.sleep(2)
|
|
1864
|
+
|
|
1865
|
+
# Select DP
|
|
1866
|
+
page.click("span.select2-selection")
|
|
1867
|
+
time.sleep(1)
|
|
1868
|
+
page.wait_for_selector(".select2-results", timeout=5000)
|
|
1869
|
+
|
|
1870
|
+
search_box = page.query_selector("input.select2-search__field")
|
|
1871
|
+
if search_box:
|
|
1872
|
+
search_box.type(member['dp_value'])
|
|
1873
|
+
time.sleep(0.5)
|
|
1874
|
+
first_result = page.query_selector("li.select2-results__option--highlighted, li.select2-results__option[aria-selected='true']")
|
|
1875
|
+
if first_result:
|
|
1876
|
+
first_result.click()
|
|
1877
|
+
else:
|
|
1878
|
+
page.keyboard.press("Enter")
|
|
1879
|
+
time.sleep(1)
|
|
1880
|
+
|
|
1881
|
+
# Fill username
|
|
1882
|
+
username_selectors = [
|
|
1883
|
+
"input[formcontrolname='username']",
|
|
1884
|
+
"input#username",
|
|
1885
|
+
"input[placeholder*='User']"
|
|
1886
|
+
]
|
|
1887
|
+
for selector in username_selectors:
|
|
1888
|
+
try:
|
|
1889
|
+
page.fill(selector, member['username'], timeout=2000)
|
|
1890
|
+
break
|
|
1891
|
+
except:
|
|
1892
|
+
continue
|
|
1893
|
+
|
|
1894
|
+
# Fill password
|
|
1895
|
+
password_selectors = [
|
|
1896
|
+
"input[formcontrolname='password']",
|
|
1897
|
+
"input[type='password']"
|
|
1898
|
+
]
|
|
1899
|
+
for selector in password_selectors:
|
|
1900
|
+
try:
|
|
1901
|
+
page.fill(selector, member['password'], timeout=2000)
|
|
1902
|
+
break
|
|
1903
|
+
except:
|
|
1904
|
+
continue
|
|
1905
|
+
|
|
1906
|
+
# Click login
|
|
1907
|
+
login_button_selectors = [
|
|
1908
|
+
"button.btn.sign-in",
|
|
1909
|
+
"button[type='submit']",
|
|
1910
|
+
"button:has-text('Login')"
|
|
1911
|
+
]
|
|
1912
|
+
for selector in login_button_selectors:
|
|
1913
|
+
try:
|
|
1914
|
+
page.click(selector, timeout=2000)
|
|
1915
|
+
break
|
|
1916
|
+
except:
|
|
1917
|
+
continue
|
|
1918
|
+
|
|
1919
|
+
# Wait for login
|
|
1920
|
+
try:
|
|
1921
|
+
page.wait_for_function("window.location.hash !== '#/login'", timeout=8000)
|
|
1922
|
+
time.sleep(2)
|
|
1923
|
+
except:
|
|
1924
|
+
time.sleep(2)
|
|
1925
|
+
|
|
1926
|
+
# Check if logged in
|
|
1927
|
+
if "#/login" not in page.url.lower():
|
|
1928
|
+
console.print(f"[green]✓ [Tab {idx}] Login successful: {member_name}[/green]")
|
|
1929
|
+
pages_data.append({"success": True, "member": member, "page": page, "tab_index": idx})
|
|
1930
|
+
else:
|
|
1931
|
+
console.print(f"[red]✗ [Tab {idx}] Login failed: {member_name}[/red]")
|
|
1932
|
+
pages_data.append({"success": False, "member": member, "page": page, "tab_index": idx, "error": "Login failed"})
|
|
1933
|
+
|
|
1934
|
+
except Exception as e:
|
|
1935
|
+
console.print(f"[red]✗ [Tab {idx}] Error logging in {member_name}: {e}[/red]")
|
|
1936
|
+
pages_data.append({"success": False, "member": member, "page": page, "tab_index": idx, "error": str(e)})
|
|
1937
|
+
|
|
1938
|
+
# Summary of login phase
|
|
1939
|
+
successful_logins = [p for p in pages_data if p['success']]
|
|
1940
|
+
failed_logins = [p for p in pages_data if not p['success']]
|
|
1941
|
+
|
|
1942
|
+
console.print()
|
|
1943
|
+
summary_table = Table(title=f"Login Summary ({len(successful_logins)}/{len(members)} successful)", box=box.ROUNDED)
|
|
1944
|
+
summary_table.add_column("Member", style="white")
|
|
1945
|
+
summary_table.add_column("Status", style="bold")
|
|
1946
|
+
summary_table.add_column("Message", style="dim")
|
|
1947
|
+
|
|
1948
|
+
for p in successful_logins:
|
|
1949
|
+
summary_table.add_row(p['member']['name'], "[green]Success[/green]", "-")
|
|
1950
|
+
for p in failed_logins:
|
|
1951
|
+
summary_table.add_row(p['member']['name'], "[red]Failed[/red]", p.get('error', 'Unknown error'))
|
|
1952
|
+
|
|
1953
|
+
console.print(summary_table)
|
|
1954
|
+
|
|
1955
|
+
if not successful_logins:
|
|
1956
|
+
console.print("[bold red]\n✗ No successful logins. Exiting...[/bold red]")
|
|
1957
|
+
return
|
|
1958
|
+
|
|
1959
|
+
# Continue with successful logins only
|
|
1960
|
+
if failed_logins:
|
|
1961
|
+
proceed = input(f"\n⚠ {len(failed_logins)} login(s) failed. Continue with {len(successful_logins)} member(s)? (yes/no): ").strip().lower()
|
|
1962
|
+
if proceed != 'yes':
|
|
1963
|
+
console.print("[red]✗ Operation cancelled[/red]")
|
|
1964
|
+
return
|
|
1965
|
+
|
|
1966
|
+
# ========== PHASE 2: SEQUENTIAL IPO APPLICATION ==========
|
|
1967
|
+
console.print()
|
|
1968
|
+
console.print(Rule("[bold cyan]PHASE 2: IPO APPLICATION (SEQUENTIAL)[/bold cyan]"))
|
|
1969
|
+
console.print()
|
|
1970
|
+
|
|
1971
|
+
# Use first successful login to select IPO
|
|
1972
|
+
first_page = successful_logins[0]['page']
|
|
1973
|
+
|
|
1974
|
+
console.print("Navigating to IPO page to select IPO...")
|
|
1975
|
+
first_page.goto("https://meroshare.cdsc.com.np/#/asba", wait_until="networkidle")
|
|
1976
|
+
time.sleep(3)
|
|
1977
|
+
|
|
1978
|
+
console.print("Fetching available IPOs...\n")
|
|
1979
|
+
|
|
1980
|
+
# Check if there are any IPOs available
|
|
1981
|
+
try:
|
|
1982
|
+
first_page.wait_for_selector(".company-list", timeout=10000)
|
|
1983
|
+
time.sleep(2)
|
|
1984
|
+
except Exception as e:
|
|
1985
|
+
console.print("[bold yellow]⚠ No IPOs currently available on Meroshare[/bold yellow]")
|
|
1986
|
+
console.print("[red]✗ Cannot proceed with IPO application[/red]\n")
|
|
1987
|
+
|
|
1988
|
+
# Check if there's a "no data" message
|
|
1989
|
+
try:
|
|
1990
|
+
no_data = first_page.query_selector("text=No Data Available")
|
|
1991
|
+
if no_data:
|
|
1992
|
+
console.print("→ Meroshare shows: 'No Data Available'")
|
|
1993
|
+
except:
|
|
1994
|
+
pass
|
|
1995
|
+
|
|
1996
|
+
first_page.screenshot(path="no_ipos_available.png")
|
|
1997
|
+
console.print("[dim]📸 Screenshot saved: no_ipos_available.png[/dim]\n")
|
|
1998
|
+
|
|
1999
|
+
if not headless:
|
|
2000
|
+
console.print("Browser will stay open for 20 seconds...")
|
|
2001
|
+
time.sleep(20)
|
|
2002
|
+
|
|
2003
|
+
return
|
|
2004
|
+
|
|
2005
|
+
company_rows = first_page.query_selector_all(".company-list")
|
|
2006
|
+
|
|
2007
|
+
available_ipos = []
|
|
2008
|
+
for idx, row in enumerate(company_rows, 1):
|
|
2009
|
+
try:
|
|
2010
|
+
company_name_elem = row.query_selector(".company-name span")
|
|
2011
|
+
share_type_elem = row.query_selector(".share-of-type")
|
|
2012
|
+
share_group_elem = row.query_selector(".isin")
|
|
2013
|
+
|
|
2014
|
+
if company_name_elem and share_type_elem and share_group_elem:
|
|
2015
|
+
company_name = company_name_elem.inner_text().strip()
|
|
2016
|
+
share_type = share_type_elem.inner_text().strip()
|
|
2017
|
+
share_group = share_group_elem.inner_text().strip()
|
|
2018
|
+
|
|
2019
|
+
if "ipo" in share_type.lower() and "ordinary" in share_group.lower():
|
|
2020
|
+
available_ipos.append({
|
|
2021
|
+
"index": len(available_ipos) + 1,
|
|
2022
|
+
"company_name": company_name,
|
|
2023
|
+
"share_type": share_type,
|
|
2024
|
+
"share_group": share_group
|
|
2025
|
+
})
|
|
2026
|
+
except Exception as e:
|
|
2027
|
+
pass
|
|
2028
|
+
|
|
2029
|
+
if not available_ipos:
|
|
2030
|
+
console.print("[bold red]✗ No IPOs available to apply![/bold red]")
|
|
2031
|
+
return
|
|
2032
|
+
|
|
2033
|
+
ipo_table = Table(title="Available IPOs (Ordinary Shares)", box=box.ROUNDED)
|
|
2034
|
+
ipo_table.add_column("No.", justify="right", style="cyan")
|
|
2035
|
+
ipo_table.add_column("Company", style="bold white")
|
|
2036
|
+
ipo_table.add_column("Type", style="yellow")
|
|
2037
|
+
ipo_table.add_column("Group", style="dim")
|
|
2038
|
+
|
|
2039
|
+
for ipo in available_ipos:
|
|
2040
|
+
ipo_table.add_row(str(ipo['index']), ipo['company_name'], ipo['share_type'], ipo['share_group'])
|
|
2041
|
+
|
|
2042
|
+
console.print(ipo_table)
|
|
2043
|
+
|
|
2044
|
+
if not headless:
|
|
2045
|
+
selection = input(f"\nSelect IPO to apply for all members (1-{len(available_ipos)}): ").strip()
|
|
2046
|
+
try:
|
|
2047
|
+
selected_idx = int(selection) - 1
|
|
2048
|
+
if selected_idx < 0 or selected_idx >= len(available_ipos):
|
|
2049
|
+
console.print("[red]✗ Invalid selection![/red]")
|
|
2050
|
+
return
|
|
2051
|
+
except ValueError:
|
|
2052
|
+
console.print("[red]✗ Invalid input![/red]")
|
|
2053
|
+
return
|
|
2054
|
+
else:
|
|
2055
|
+
selected_idx = 0
|
|
2056
|
+
|
|
2057
|
+
selected_ipo = available_ipos[selected_idx]
|
|
2058
|
+
console.print(Panel(f"[bold green]✓ Selected IPO: {selected_ipo['company_name']}[/bold green]\n[yellow]⚠ Will apply this IPO for {len(successful_logins)} member(s)[/yellow]", box=box.ROUNDED))
|
|
2059
|
+
|
|
2060
|
+
# Apply IPO for each member sequentially
|
|
2061
|
+
application_results = []
|
|
2062
|
+
|
|
2063
|
+
for page_data in successful_logins:
|
|
2064
|
+
member = page_data['member']
|
|
2065
|
+
page = page_data['page']
|
|
2066
|
+
tab_index = page_data['tab_index']
|
|
2067
|
+
member_name = member['name']
|
|
2068
|
+
|
|
2069
|
+
console.print()
|
|
2070
|
+
console.print(Rule(f"[Tab {tab_index}] APPLYING FOR: {member_name}"))
|
|
2071
|
+
|
|
2072
|
+
try:
|
|
2073
|
+
# Navigate to ASBA
|
|
2074
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Navigating to IPO page...")
|
|
2075
|
+
page.goto("https://meroshare.cdsc.com.np/#/asba", wait_until="networkidle")
|
|
2076
|
+
time.sleep(3)
|
|
2077
|
+
|
|
2078
|
+
# Find and click the IPO
|
|
2079
|
+
page.wait_for_selector(".company-list", timeout=10000)
|
|
2080
|
+
time.sleep(2)
|
|
2081
|
+
|
|
2082
|
+
company_rows = page.query_selector_all(".company-list")
|
|
2083
|
+
ipo_found = False
|
|
2084
|
+
already_applied = False
|
|
2085
|
+
|
|
2086
|
+
for row in company_rows:
|
|
2087
|
+
try:
|
|
2088
|
+
company_name_elem = row.query_selector(".company-name span")
|
|
2089
|
+
if company_name_elem and selected_ipo['company_name'] in company_name_elem.inner_text():
|
|
2090
|
+
apply_button = row.query_selector("button.btn-issue")
|
|
2091
|
+
if apply_button:
|
|
2092
|
+
# Check button text to see if already applied (shows 'Edit')
|
|
2093
|
+
button_text = apply_button.inner_text().strip().lower()
|
|
2094
|
+
|
|
2095
|
+
if "edit" in button_text or "view" in button_text:
|
|
2096
|
+
console.print(f"[yellow][Tab {tab_index}] ⚠ IPO already applied (button shows: '{button_text.title()}')[/yellow]")
|
|
2097
|
+
already_applied = True
|
|
2098
|
+
ipo_found = True
|
|
2099
|
+
break
|
|
2100
|
+
else:
|
|
2101
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Clicking Apply button (button shows: '{button_text.title()}')...")
|
|
2102
|
+
apply_button.click()
|
|
2103
|
+
ipo_found = True
|
|
2104
|
+
break
|
|
2105
|
+
except:
|
|
2106
|
+
continue
|
|
2107
|
+
|
|
2108
|
+
if not ipo_found:
|
|
2109
|
+
raise Exception("IPO not found in the list")
|
|
2110
|
+
|
|
2111
|
+
if already_applied:
|
|
2112
|
+
console.print(f"[green]✓ [Tab {tab_index}] Skipping - IPO already applied for {member_name}[/green]")
|
|
2113
|
+
application_results.append({"member": member_name, "success": True, "status": "already_applied"})
|
|
2114
|
+
continue
|
|
2115
|
+
|
|
2116
|
+
time.sleep(3)
|
|
2117
|
+
|
|
2118
|
+
# Fill form
|
|
2119
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Filling application form...")
|
|
2120
|
+
page.wait_for_selector("select#selectBank", timeout=10000)
|
|
2121
|
+
time.sleep(2)
|
|
2122
|
+
|
|
2123
|
+
# Select bank
|
|
2124
|
+
bank_options = page.query_selector_all("select#selectBank option")
|
|
2125
|
+
valid_banks = [opt for opt in bank_options if opt.get_attribute("value")]
|
|
2126
|
+
if valid_banks:
|
|
2127
|
+
page.select_option("select#selectBank", valid_banks[0].get_attribute("value"))
|
|
2128
|
+
time.sleep(2)
|
|
2129
|
+
|
|
2130
|
+
# Select account
|
|
2131
|
+
page.wait_for_selector("select#accountNumber", timeout=5000)
|
|
2132
|
+
account_options = page.query_selector_all("select#accountNumber option")
|
|
2133
|
+
valid_accounts = [opt for opt in account_options if opt.get_attribute("value")]
|
|
2134
|
+
if valid_accounts:
|
|
2135
|
+
page.select_option("select#accountNumber", valid_accounts[0].get_attribute("value"))
|
|
2136
|
+
time.sleep(2)
|
|
2137
|
+
|
|
2138
|
+
# Fill kitta
|
|
2139
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Kitta: [bold]{member['applied_kitta']}[/bold]")
|
|
2140
|
+
page.fill("input#appliedKitta", str(member['applied_kitta']))
|
|
2141
|
+
time.sleep(1)
|
|
2142
|
+
|
|
2143
|
+
# Fill CRN
|
|
2144
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] CRN: [dim]{member['crn_number']}[/dim]")
|
|
2145
|
+
page.fill("input#crnNumber", member['crn_number'])
|
|
2146
|
+
time.sleep(1)
|
|
2147
|
+
|
|
2148
|
+
# Accept disclaimer
|
|
2149
|
+
disclaimer_checkbox = page.query_selector("input#disclaimer")
|
|
2150
|
+
if disclaimer_checkbox:
|
|
2151
|
+
disclaimer_checkbox.check()
|
|
2152
|
+
time.sleep(1)
|
|
2153
|
+
|
|
2154
|
+
# Click proceed
|
|
2155
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Clicking Proceed...")
|
|
2156
|
+
proceed_button = page.query_selector("button.btn-primary[type='submit']")
|
|
2157
|
+
if proceed_button:
|
|
2158
|
+
proceed_button.click()
|
|
2159
|
+
time.sleep(3)
|
|
2160
|
+
|
|
2161
|
+
# Enter PIN
|
|
2162
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Entering transaction PIN...")
|
|
2163
|
+
page.wait_for_selector("input#transactionPIN", timeout=10000)
|
|
2164
|
+
time.sleep(2)
|
|
2165
|
+
page.fill("input#transactionPIN", member['transaction_pin'])
|
|
2166
|
+
time.sleep(2)
|
|
2167
|
+
|
|
2168
|
+
# Submit
|
|
2169
|
+
console.print(f"[cyan][Tab {tab_index}][/cyan] Submitting application...")
|
|
2170
|
+
clicked = False
|
|
2171
|
+
|
|
2172
|
+
# Try multiple methods to click Apply button
|
|
2173
|
+
try:
|
|
2174
|
+
apply_buttons = page.query_selector_all("button:has-text('Apply')")
|
|
2175
|
+
for btn in apply_buttons:
|
|
2176
|
+
if btn.is_visible() and not btn.is_disabled():
|
|
2177
|
+
btn.click()
|
|
2178
|
+
clicked = True
|
|
2179
|
+
break
|
|
2180
|
+
except:
|
|
2181
|
+
pass
|
|
2182
|
+
|
|
2183
|
+
if not clicked:
|
|
2184
|
+
try:
|
|
2185
|
+
submit_button = page.query_selector("div.confirm-page-btn button.btn-primary[type='submit']")
|
|
2186
|
+
if submit_button and submit_button.is_visible():
|
|
2187
|
+
submit_button.click()
|
|
2188
|
+
clicked = True
|
|
2189
|
+
except:
|
|
2190
|
+
pass
|
|
2191
|
+
|
|
2192
|
+
if not clicked:
|
|
2193
|
+
try:
|
|
2194
|
+
page.evaluate("""
|
|
2195
|
+
const buttons = document.querySelectorAll('button');
|
|
2196
|
+
for (const btn of buttons) {
|
|
2197
|
+
if (btn.textContent.includes('Apply') && btn.type === 'submit') {
|
|
2198
|
+
btn.click();
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
""")
|
|
2203
|
+
clicked = True
|
|
2204
|
+
except:
|
|
2205
|
+
pass
|
|
2206
|
+
|
|
2207
|
+
if not clicked:
|
|
2208
|
+
raise Exception("Failed to click submit button")
|
|
2209
|
+
|
|
2210
|
+
time.sleep(5)
|
|
2211
|
+
|
|
2212
|
+
console.print(f"[bold green]✓ [Tab {tab_index}] Application submitted for {member_name}![/bold green]")
|
|
2213
|
+
application_results.append({"member": member_name, "success": True})
|
|
2214
|
+
|
|
2215
|
+
except Exception as e:
|
|
2216
|
+
console.print(f"[bold red]✗ [Tab {tab_index}] Failed for {member_name}: {e}[/bold red]")
|
|
2217
|
+
application_results.append({"member": member_name, "success": False, "error": str(e)})
|
|
2218
|
+
page.screenshot(path=f"error_{member_name}.png")
|
|
2219
|
+
|
|
2220
|
+
# ========== FINAL SUMMARY ==========
|
|
2221
|
+
console.print()
|
|
2222
|
+
|
|
2223
|
+
successful_apps = [r for r in application_results if r['success']]
|
|
2224
|
+
failed_apps = [r for r in application_results if not r['success']]
|
|
2225
|
+
|
|
2226
|
+
final_table = Table(title=f"Final Application Summary: {selected_ipo['company_name']}", box=box.ROUNDED)
|
|
2227
|
+
final_table.add_column("Member", style="white")
|
|
2228
|
+
final_table.add_column("Status", style="bold")
|
|
2229
|
+
final_table.add_column("Details", style="dim")
|
|
2230
|
+
|
|
2231
|
+
for r in successful_apps:
|
|
2232
|
+
status = "[yellow]Already Applied[/yellow]" if r.get('status') == 'already_applied' else "[green]Success[/green]"
|
|
2233
|
+
details = "Skipped" if r.get('status') == 'already_applied' else "Applied Successfully"
|
|
2234
|
+
final_table.add_row(r['member'], status, details)
|
|
2235
|
+
|
|
2236
|
+
for r in failed_apps:
|
|
2237
|
+
final_table.add_row(r['member'], "[red]Failed[/red]", r.get('error', 'Unknown error'))
|
|
2238
|
+
|
|
2239
|
+
console.print(final_table)
|
|
2240
|
+
|
|
2241
|
+
if not headless:
|
|
2242
|
+
console.print("\n[dim]Browser will stay open for 60 seconds for verification...[/dim]")
|
|
2243
|
+
time.sleep(60)
|
|
2244
|
+
|
|
2245
|
+
except Exception as e:
|
|
2246
|
+
console.print(f"\n[bold red]✗ Critical error: {e}[/bold red]")
|
|
2247
|
+
import traceback
|
|
2248
|
+
traceback.print_exc()
|
|
2249
|
+
finally:
|
|
2250
|
+
browser.close()
|
|
2251
|
+
|
|
2252
|
+
def get_dp_list():
|
|
2253
|
+
"""Fetch and display available DP list with values from API"""
|
|
2254
|
+
import requests
|
|
2255
|
+
|
|
2256
|
+
try:
|
|
2257
|
+
with console.status("[bold green]Fetching DP list from Meroshare API...", spinner="dots"):
|
|
2258
|
+
# Fetch data from API
|
|
2259
|
+
response = requests.get("https://webbackend.cdsc.com.np/api/meroShare/capital/")
|
|
2260
|
+
response.raise_for_status()
|
|
2261
|
+
|
|
2262
|
+
dp_data = response.json()
|
|
2263
|
+
|
|
2264
|
+
# Sort by name for better readability
|
|
2265
|
+
dp_data.sort(key=lambda x: x['name'])
|
|
2266
|
+
|
|
2267
|
+
table = Table(title=f"Available Depository Participants (Total: {len(dp_data)})", box=box.ROUNDED, header_style="bold cyan")
|
|
2268
|
+
table.add_column("ID", style="bold yellow", justify="right")
|
|
2269
|
+
table.add_column("Code", style="dim")
|
|
2270
|
+
table.add_column("Name", style="white")
|
|
2271
|
+
|
|
2272
|
+
for dp in dp_data:
|
|
2273
|
+
table.add_row(str(dp['id']), str(dp['code']), dp['name'])
|
|
2274
|
+
|
|
2275
|
+
console.print(table)
|
|
2276
|
+
console.print(Panel("Note: Use the [bold yellow]ID[/] (first column) when setting up credentials\n(e.g., 139 for CREATIVE SECURITIES)", box=box.ROUNDED, style="dim"))
|
|
2277
|
+
|
|
2278
|
+
except requests.RequestException as e:
|
|
2279
|
+
console.print(f"[bold red]✗ Error fetching DP list from API:[/bold red] {e}")
|
|
2280
|
+
console.print(" Please check your internet connection.\n")
|
|
2281
|
+
except Exception as e:
|
|
2282
|
+
console.print(f"[bold red]✗ Unexpected error:[/bold red] {e}\n")
|
|
2283
|
+
|
|
2284
|
+
# ============================================
|
|
2285
|
+
# Market Data Command Functions
|
|
2286
|
+
# ============================================
|
|
2287
|
+
|
|
2288
|
+
def format_number(num):
|
|
2289
|
+
"""Format large numbers with K, M, B suffixes"""
|
|
2290
|
+
try:
|
|
2291
|
+
num = float(str(num).replace(',', ''))
|
|
2292
|
+
if num >= 1_000_000_000:
|
|
2293
|
+
return f"{num / 1_000_000_000:.2f}B"
|
|
2294
|
+
elif num >= 1_000_000:
|
|
2295
|
+
return f"{num / 1_000_000:.2f}M"
|
|
2296
|
+
elif num >= 1_000:
|
|
2297
|
+
return f"{num / 1_000:.2f}K"
|
|
2298
|
+
else:
|
|
2299
|
+
return f"{num:.2f}"
|
|
2300
|
+
except (ValueError, AttributeError):
|
|
2301
|
+
return str(num)
|
|
2302
|
+
|
|
2303
|
+
def format_rupees(amount):
|
|
2304
|
+
"""Format amount as rupees with proper comma placement"""
|
|
2305
|
+
try:
|
|
2306
|
+
amount = float(str(amount).replace(',', ''))
|
|
2307
|
+
return f"Rs. {amount:,.2f}"
|
|
2308
|
+
except (ValueError, AttributeError):
|
|
2309
|
+
return f"Rs. {amount}"
|
|
2310
|
+
|
|
2311
|
+
def get_ss_time():
|
|
2312
|
+
"""Get timestamp from ShareSansar market summary"""
|
|
2313
|
+
try:
|
|
2314
|
+
response = requests.get("https://www.sharesansar.com/market-summary", timeout=10)
|
|
2315
|
+
soup = BeautifulSoup(response.text, "lxml")
|
|
2316
|
+
summary_cont = soup.find("div", id="market_symmary_data")
|
|
2317
|
+
if summary_cont is not None:
|
|
2318
|
+
msdate = summary_cont.find("h5").find("span")
|
|
2319
|
+
if msdate is not None:
|
|
2320
|
+
return msdate.text
|
|
2321
|
+
except:
|
|
2322
|
+
pass
|
|
2323
|
+
return "N/A"
|
|
2324
|
+
|
|
2325
|
+
def cmd_ipo():
|
|
2326
|
+
"""Display all open IPOs/public offerings"""
|
|
2327
|
+
try:
|
|
2328
|
+
with console.status("[bold green]Fetching open IPOs...", spinner="dots"):
|
|
2329
|
+
response = requests.get(
|
|
2330
|
+
"https://sharehubnepal.com/data/api/v1/public-offering",
|
|
2331
|
+
timeout=10
|
|
2332
|
+
)
|
|
2333
|
+
response.raise_for_status()
|
|
2334
|
+
data = response.json()
|
|
2335
|
+
|
|
2336
|
+
if not data.get('success'):
|
|
2337
|
+
console.print(Panel("⚠️ Unable to fetch IPO data. API request failed.", style="bold red", box=box.ROUNDED))
|
|
2338
|
+
return
|
|
2339
|
+
|
|
2340
|
+
all_ipos = data.get('data', {}).get('content', [])
|
|
2341
|
+
|
|
2342
|
+
def _is_general_public(ipo_item):
|
|
2343
|
+
"""Return True if the IPO is for the general public."""
|
|
2344
|
+
try:
|
|
2345
|
+
f = str(ipo_item.get('for', '')).lower()
|
|
2346
|
+
except Exception:
|
|
2347
|
+
return False
|
|
2348
|
+
return 'general' in f and 'public' in f
|
|
2349
|
+
|
|
2350
|
+
# Filter to only open IPOs that are for the general public
|
|
2351
|
+
open_ipos = [ipo for ipo in all_ipos if ipo.get('status') == 'Open' and _is_general_public(ipo)]
|
|
2352
|
+
|
|
2353
|
+
if not open_ipos:
|
|
2354
|
+
console.print(Panel("💤 No IPOs are currently open for subscription.", style="bold yellow", box=box.ROUNDED))
|
|
2355
|
+
return
|
|
2356
|
+
|
|
2357
|
+
table = Table(title=f"📈 Open IPOs ({len(open_ipos)})", box=box.ROUNDED, header_style="bold cyan", expand=True)
|
|
2358
|
+
table.add_column("#", style="dim", width=4)
|
|
2359
|
+
table.add_column("Company", style="bold white")
|
|
2360
|
+
table.add_column("Type", style="cyan")
|
|
2361
|
+
table.add_column("Units", justify="right")
|
|
2362
|
+
table.add_column("Price", justify="right")
|
|
2363
|
+
table.add_column("Closing", style="yellow")
|
|
2364
|
+
table.add_column("Status", justify="center")
|
|
2365
|
+
|
|
2366
|
+
for index, ipo in enumerate(open_ipos, 1):
|
|
2367
|
+
symbol = ipo.get('symbol', 'N/A')
|
|
2368
|
+
name = ipo.get('name', 'N/A')
|
|
2369
|
+
units = ipo.get('units', 0)
|
|
2370
|
+
price = ipo.get('price', 0)
|
|
2371
|
+
closing_date = ipo.get('closingDate', 'N/A')
|
|
2372
|
+
extended_closing = ipo.get('extendedClosingDate', None)
|
|
2373
|
+
ipo_type = ipo.get('type', 'N/A')
|
|
2374
|
+
|
|
2375
|
+
try:
|
|
2376
|
+
closing_date_obj = datetime.fromisoformat(closing_date.replace('T', ' '))
|
|
2377
|
+
closing_date_str = closing_date_obj.strftime('%d %b')
|
|
2378
|
+
except:
|
|
2379
|
+
closing_date_str = closing_date
|
|
2380
|
+
|
|
2381
|
+
days_left = None
|
|
2382
|
+
urgency_text = ""
|
|
2383
|
+
urgency_style = "white"
|
|
2384
|
+
|
|
2385
|
+
try:
|
|
2386
|
+
target_date = extended_closing if extended_closing else closing_date
|
|
2387
|
+
target_date_obj = datetime.fromisoformat(target_date.replace('T', ' '))
|
|
2388
|
+
days_left = (target_date_obj - datetime.now()).days
|
|
2389
|
+
|
|
2390
|
+
if days_left >= 0:
|
|
2391
|
+
if days_left <= 2:
|
|
2392
|
+
urgency_text = f"⚠️ {days_left}d left"
|
|
2393
|
+
urgency_style = "bold red"
|
|
2394
|
+
elif days_left <= 5:
|
|
2395
|
+
urgency_text = f"⏰ {days_left}d left"
|
|
2396
|
+
urgency_style = "yellow"
|
|
2397
|
+
else:
|
|
2398
|
+
urgency_text = f"📅 {days_left}d"
|
|
2399
|
+
urgency_style = "green"
|
|
2400
|
+
except:
|
|
2401
|
+
urgency_text = "Check dates"
|
|
2402
|
+
|
|
2403
|
+
type_emojis = {
|
|
2404
|
+
'Ipo': '🆕 IPO',
|
|
2405
|
+
'Right': '🔄 Right',
|
|
2406
|
+
'MutualFund': '💼 MF',
|
|
2407
|
+
'BondOrDebenture': '💰 Bond'
|
|
2408
|
+
}
|
|
2409
|
+
type_display = type_emojis.get(ipo_type, ipo_type)
|
|
2410
|
+
|
|
2411
|
+
table.add_row(
|
|
2412
|
+
str(index),
|
|
2413
|
+
f"{symbol}\n[dim]{name}[/dim]",
|
|
2414
|
+
type_display,
|
|
2415
|
+
f"{units:,}",
|
|
2416
|
+
format_rupees(price),
|
|
2417
|
+
closing_date_str,
|
|
2418
|
+
f"[{urgency_style}]{urgency_text}[/{urgency_style}]"
|
|
2419
|
+
)
|
|
2420
|
+
|
|
2421
|
+
console.print(table)
|
|
2422
|
+
console.print(Panel("💡 Tip: Use [bold cyan]nepse apply[/] to apply for IPO via Meroshare", box=box.ROUNDED, style="dim"))
|
|
2423
|
+
|
|
2424
|
+
except requests.exceptions.RequestException as e:
|
|
2425
|
+
console.print(f"[bold red]🔌 Connection Error:[/bold red] Unable to connect to API.\n{str(e)[:100]}\n")
|
|
2426
|
+
except Exception as e:
|
|
2427
|
+
console.print(f"[bold red]⚠️ Error:[/bold red] {str(e)[:200]}\n")
|
|
2428
|
+
|
|
2429
|
+
def cmd_nepse():
|
|
2430
|
+
"""Display NEPSE indices data"""
|
|
2431
|
+
try:
|
|
2432
|
+
with console.status("[bold green]Fetching NEPSE indices...", spinner="dots"):
|
|
2433
|
+
import cloudscraper
|
|
2434
|
+
scraper = cloudscraper.create_scraper()
|
|
2435
|
+
url = "https://nepsealpha.com/live/stocks"
|
|
2436
|
+
response = scraper.get(url, timeout=10)
|
|
2437
|
+
response.raise_for_status()
|
|
2438
|
+
data = response.json()
|
|
2439
|
+
|
|
2440
|
+
# Get all indices from stock_live.prices where type is 'index'
|
|
2441
|
+
prices = data.get('stock_live', {}).get('prices', [])
|
|
2442
|
+
indices = [item for item in prices if item.get('stockinfo', {}).get('type') == 'index']
|
|
2443
|
+
|
|
2444
|
+
if not indices:
|
|
2445
|
+
console.print(Panel("⚠️ No index data available.", style="bold yellow", box=box.ROUNDED))
|
|
2446
|
+
return
|
|
2447
|
+
|
|
2448
|
+
# Get timestamp
|
|
2449
|
+
timestamp = data.get('stock_live', {}).get('asOf', 'N/A')
|
|
2450
|
+
|
|
2451
|
+
table = Table(title=f"NEPSE Index Data (Live) - {timestamp}", box=box.ROUNDED, header_style="bold cyan")
|
|
2452
|
+
table.add_column("Index", style="bold white")
|
|
2453
|
+
table.add_column("Close", justify="right")
|
|
2454
|
+
table.add_column("Change", justify="right")
|
|
2455
|
+
table.add_column("% Change", justify="right")
|
|
2456
|
+
table.add_column("Trend", justify="center")
|
|
2457
|
+
table.add_column("Range", justify="center", style="dim")
|
|
2458
|
+
table.add_column("Turnover", justify="right")
|
|
2459
|
+
|
|
2460
|
+
for item in indices:
|
|
2461
|
+
index_name = item.get('symbol', 'N/A')
|
|
2462
|
+
close_val = item.get('close', 0)
|
|
2463
|
+
pct_change = item.get('percent_change', 0)
|
|
2464
|
+
low_val = item.get('low', 0)
|
|
2465
|
+
high_val = item.get('high', 0)
|
|
2466
|
+
turnover = item.get('volume', 0) # Using volume as turnover
|
|
2467
|
+
|
|
2468
|
+
# Calculate point change
|
|
2469
|
+
try:
|
|
2470
|
+
if pct_change != 0 and close_val != 0:
|
|
2471
|
+
prev_close = close_val / (1 + pct_change / 100)
|
|
2472
|
+
point_change = close_val - prev_close
|
|
2473
|
+
else:
|
|
2474
|
+
point_change = 0
|
|
2475
|
+
except:
|
|
2476
|
+
point_change = 0
|
|
2477
|
+
|
|
2478
|
+
color = "green" if pct_change > 0 else "red" if pct_change < 0 else "yellow"
|
|
2479
|
+
trend_icon = "▲" if pct_change > 0 else "▼" if pct_change < 0 else "•"
|
|
2480
|
+
|
|
2481
|
+
range_str = f"{low_val:,.2f} - {high_val:,.2f}"
|
|
2482
|
+
|
|
2483
|
+
table.add_row(
|
|
2484
|
+
index_name,
|
|
2485
|
+
f"{close_val:,.2f}",
|
|
2486
|
+
f"[{color}]{point_change:+,.2f}[/{color}]",
|
|
2487
|
+
f"[{color}]{pct_change:+.2f}%[/{color}]",
|
|
2488
|
+
f"[{color}]{trend_icon}[/{color}]",
|
|
2489
|
+
range_str,
|
|
2490
|
+
format_number(turnover)
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2493
|
+
console.print(table)
|
|
2494
|
+
|
|
2495
|
+
except Exception as e:
|
|
2496
|
+
console.print(f"[bold red]⚠️ Error fetching NEPSE data:[/bold red] {str(e)}\n")
|
|
2497
|
+
|
|
2498
|
+
def cmd_subidx(subindex_name):
|
|
2499
|
+
"""Display sub-index details"""
|
|
2500
|
+
try:
|
|
2501
|
+
subindex_name = subindex_name.upper()
|
|
2502
|
+
|
|
2503
|
+
# Mapping user-friendly names to API symbols
|
|
2504
|
+
sub_index_mapping = {
|
|
2505
|
+
"BANKING": "BANKING",
|
|
2506
|
+
"DEVBANK": "DEVBANK",
|
|
2507
|
+
"FINANCE": "FINANCE",
|
|
2508
|
+
"HOTELS AND TOURISM": "HOTELS",
|
|
2509
|
+
"HOTELS": "HOTELS",
|
|
2510
|
+
"HYDROPOWER": "HYDROPOWER",
|
|
2511
|
+
"INVESTMENT": "INVESTMENT",
|
|
2512
|
+
"LIFE INSURANCE": "LIFEINSU",
|
|
2513
|
+
"LIFEINSU": "LIFEINSU",
|
|
2514
|
+
"MANUFACTURING AND PROCESSING": "MANUFACTURE",
|
|
2515
|
+
"MANUFACTURE": "MANUFACTURE",
|
|
2516
|
+
"MICROFINANCE": "MICROFINANCE",
|
|
2517
|
+
"MUTUAL FUND": "MUTUAL",
|
|
2518
|
+
"MUTUAL": "MUTUAL",
|
|
2519
|
+
"NONLIFE INSURANCE": "NONLIFEINSU",
|
|
2520
|
+
"NONLIFEINSU": "NONLIFEINSU",
|
|
2521
|
+
"OTHERS": "OTHERS",
|
|
2522
|
+
"TRADING": "TRADING",
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
with console.status(f"[bold green]Fetching {subindex_name} sub-index data...", spinner="dots"):
|
|
2526
|
+
import cloudscraper
|
|
2527
|
+
scraper = cloudscraper.create_scraper()
|
|
2528
|
+
response = scraper.get("https://nepsealpha.com/live/stocks", timeout=10)
|
|
2529
|
+
response.raise_for_status()
|
|
2530
|
+
data = response.json()
|
|
2531
|
+
|
|
2532
|
+
# Get the mapped symbol
|
|
2533
|
+
search_symbol = sub_index_mapping.get(subindex_name, subindex_name)
|
|
2534
|
+
|
|
2535
|
+
# Get all indices from stock_live.prices
|
|
2536
|
+
prices = data.get('stock_live', {}).get('prices', [])
|
|
2537
|
+
indices = [item for item in prices if item.get('stockinfo', {}).get('type') == 'index']
|
|
2538
|
+
|
|
2539
|
+
# Find the specific sub-index
|
|
2540
|
+
sub_index_data = None
|
|
2541
|
+
for item in indices:
|
|
2542
|
+
if item.get('symbol', '').upper() == search_symbol.upper():
|
|
2543
|
+
sub_index_data = item
|
|
2544
|
+
break
|
|
2545
|
+
|
|
2546
|
+
if not sub_index_data:
|
|
2547
|
+
console.print(Panel(f"⚠️ Sub-index '{subindex_name}' not found.", style="bold red", box=box.ROUNDED))
|
|
2548
|
+
|
|
2549
|
+
available = set()
|
|
2550
|
+
for item in indices:
|
|
2551
|
+
symbol = item.get('symbol', '')
|
|
2552
|
+
if symbol not in ['NEPSE', 'SENSITIVE', 'FLOAT']:
|
|
2553
|
+
available.add(symbol)
|
|
2554
|
+
|
|
2555
|
+
table = Table(title="Available Sub-Indices", box=box.ROUNDED)
|
|
2556
|
+
table.add_column("Symbol", style="cyan")
|
|
2557
|
+
for sym in sorted(available):
|
|
2558
|
+
table.add_row(sym)
|
|
2559
|
+
console.print(table)
|
|
2560
|
+
return
|
|
2561
|
+
|
|
2562
|
+
# Get sector full name from sectors mapping
|
|
2563
|
+
sectors = data.get('sectors', {})
|
|
2564
|
+
sector_full_name = sectors.get(search_symbol, search_symbol)
|
|
2565
|
+
|
|
2566
|
+
close_val = sub_index_data.get('close', 0)
|
|
2567
|
+
pct_change = sub_index_data.get('percent_change', 0)
|
|
2568
|
+
low_val = sub_index_data.get('low', 0)
|
|
2569
|
+
high_val = sub_index_data.get('high', 0)
|
|
2570
|
+
open_val = sub_index_data.get('open', 0)
|
|
2571
|
+
turnover = sub_index_data.get('volume', 0)
|
|
2572
|
+
|
|
2573
|
+
# Calculate point change
|
|
2574
|
+
try:
|
|
2575
|
+
if pct_change != 0 and close_val != 0:
|
|
2576
|
+
prev_close = close_val / (1 + pct_change / 100)
|
|
2577
|
+
point_change = close_val - prev_close
|
|
2578
|
+
else:
|
|
2579
|
+
point_change = 0
|
|
2580
|
+
except:
|
|
2581
|
+
point_change = 0
|
|
2582
|
+
|
|
2583
|
+
color = "green" if pct_change > 0 else "red" if pct_change < 0 else "yellow"
|
|
2584
|
+
trend_icon = "▲" if pct_change > 0 else "▼" if pct_change < 0 else "•"
|
|
2585
|
+
|
|
2586
|
+
timestamp = data.get('stock_live', {}).get('asOf', 'N/A')
|
|
2587
|
+
|
|
2588
|
+
# Create a grid layout for the details
|
|
2589
|
+
grid = Table.grid(expand=True, padding=(0, 2))
|
|
2590
|
+
grid.add_column(style="bold white")
|
|
2591
|
+
grid.add_column(justify="right")
|
|
2592
|
+
|
|
2593
|
+
grid.add_row("Close Price", f"{close_val:,.2f}")
|
|
2594
|
+
grid.add_row("Change", f"[{color}]{point_change:+,.2f} ({pct_change:+.2f}%)[/{color}]")
|
|
2595
|
+
grid.add_row("Trend", f"[{color}]{trend_icon} {color.upper()}[/{color}]")
|
|
2596
|
+
grid.add_row("Range (Low-High)", f"{low_val:,.2f} - {high_val:,.2f}")
|
|
2597
|
+
grid.add_row("Open Price", f"{open_val:,.2f}")
|
|
2598
|
+
grid.add_row("Turnover", format_number(turnover))
|
|
2599
|
+
|
|
2600
|
+
panel = Panel(
|
|
2601
|
+
grid,
|
|
2602
|
+
title=f"[bold {color}]{sector_full_name} ({search_symbol})[/]",
|
|
2603
|
+
subtitle=f"As of: {timestamp}",
|
|
2604
|
+
box=box.ROUNDED,
|
|
2605
|
+
border_style=color
|
|
2606
|
+
)
|
|
2607
|
+
console.print(panel)
|
|
2608
|
+
|
|
2609
|
+
except Exception as e:
|
|
2610
|
+
console.print(f"[bold red]⚠️ Error fetching sub-index data:[/bold red] {str(e)}\n")
|
|
2611
|
+
|
|
2612
|
+
def cmd_mktsum():
|
|
2613
|
+
"""Display market summary"""
|
|
2614
|
+
try:
|
|
2615
|
+
with console.status("[bold green]Fetching market summary...", spinner="dots"):
|
|
2616
|
+
# Get current date for API call
|
|
2617
|
+
current_date = datetime.now().strftime("%d")
|
|
2618
|
+
|
|
2619
|
+
import cloudscraper
|
|
2620
|
+
scraper = cloudscraper.create_scraper()
|
|
2621
|
+
url = f"https://nepsealpha.com/daily-market-summary?type=ajax&date={current_date}&fs=&fsk=zxYtAGo9T6BxxJMI"
|
|
2622
|
+
response = scraper.get(url, timeout=10)
|
|
2623
|
+
response.raise_for_status()
|
|
2624
|
+
data = response.json()
|
|
2625
|
+
|
|
2626
|
+
all_records = data.get('allRecords', [])
|
|
2627
|
+
|
|
2628
|
+
if not all_records:
|
|
2629
|
+
console.print(Panel("⚠️ No market summary data available.", style="bold yellow", box=box.ROUNDED))
|
|
2630
|
+
return
|
|
2631
|
+
|
|
2632
|
+
# Find the "Top Sectors" section for NEPSE overall data
|
|
2633
|
+
top_sectors = None
|
|
2634
|
+
for record in all_records:
|
|
2635
|
+
if record.get('type') == 'top-sector':
|
|
2636
|
+
top_sectors = record
|
|
2637
|
+
break
|
|
2638
|
+
|
|
2639
|
+
if not top_sectors or not top_sectors.get('data'):
|
|
2640
|
+
console.print(Panel("⚠️ No sector data available.", style="bold yellow", box=box.ROUNDED))
|
|
2641
|
+
return
|
|
2642
|
+
|
|
2643
|
+
# Get NEPSE main data (first item is always NEPSE index)
|
|
2644
|
+
nepse_data = None
|
|
2645
|
+
for item in top_sectors['data']:
|
|
2646
|
+
if item.get('sector') == 'NEPSE':
|
|
2647
|
+
nepse_data = item
|
|
2648
|
+
break
|
|
2649
|
+
|
|
2650
|
+
if not nepse_data:
|
|
2651
|
+
console.print(Panel("⚠️ NEPSE data not found.", style="bold red", box=box.ROUNDED))
|
|
2652
|
+
return
|
|
2653
|
+
|
|
2654
|
+
timestamp = top_sectors.get('as_of', 'N/A')
|
|
2655
|
+
|
|
2656
|
+
# Display NEPSE Index Info
|
|
2657
|
+
current_price = float(nepse_data.get('current', 0))
|
|
2658
|
+
daily_gain = float(nepse_data.get('daily_gain', 0))
|
|
2659
|
+
turnover = float(nepse_data.get('turn_over', 0))
|
|
2660
|
+
weeks_52_change = float(nepse_data.get('_52_weeks_change', 0))
|
|
2661
|
+
positive_stocks = nepse_data.get('positive_stocks', 0)
|
|
2662
|
+
negative_stocks = nepse_data.get('negative_stocks', 0)
|
|
2663
|
+
|
|
2664
|
+
color = "green" if daily_gain > 0 else "red" if daily_gain < 0 else "yellow"
|
|
2665
|
+
trend_icon = "▲" if daily_gain > 0 else "▼" if daily_gain < 0 else "•"
|
|
2666
|
+
|
|
2667
|
+
# Main NEPSE Panel
|
|
2668
|
+
nepse_grid = Table.grid(expand=True, padding=(0, 2))
|
|
2669
|
+
nepse_grid.add_column(style="bold white")
|
|
2670
|
+
nepse_grid.add_column(justify="right")
|
|
2671
|
+
|
|
2672
|
+
nepse_grid.add_row("Current Index", f"{current_price:,.2f}")
|
|
2673
|
+
nepse_grid.add_row("Daily Gain", f"[{color}]{daily_gain:+.2f}% {trend_icon}[/{color}]")
|
|
2674
|
+
nepse_grid.add_row("52 Week Change", f"{weeks_52_change:+.2f}%")
|
|
2675
|
+
nepse_grid.add_row("Turnover", format_number(turnover))
|
|
2676
|
+
|
|
2677
|
+
nepse_panel = Panel(
|
|
2678
|
+
nepse_grid,
|
|
2679
|
+
title=f"[bold {color}]NEPSE INDEX[/]",
|
|
2680
|
+
box=box.ROUNDED,
|
|
2681
|
+
border_style=color
|
|
2682
|
+
)
|
|
2683
|
+
|
|
2684
|
+
# Trading Activity Panel
|
|
2685
|
+
activity_grid = Table.grid(expand=True, padding=(0, 2))
|
|
2686
|
+
activity_grid.add_column(style="bold white")
|
|
2687
|
+
activity_grid.add_column(justify="right")
|
|
2688
|
+
|
|
2689
|
+
activity_grid.add_row("Positive Stocks", f"[green]{positive_stocks}[/green]")
|
|
2690
|
+
activity_grid.add_row("Negative Stocks", f"[red]{negative_stocks}[/red]")
|
|
2691
|
+
activity_grid.add_row("Total Traded", f"{positive_stocks + negative_stocks}")
|
|
2692
|
+
|
|
2693
|
+
activity_panel = Panel(
|
|
2694
|
+
activity_grid,
|
|
2695
|
+
title="[bold cyan]TRADING ACTIVITY[/]",
|
|
2696
|
+
box=box.ROUNDED,
|
|
2697
|
+
border_style="cyan"
|
|
2698
|
+
)
|
|
2699
|
+
|
|
2700
|
+
console.print(Columns([nepse_panel, activity_panel]))
|
|
2701
|
+
|
|
2702
|
+
# Sector Performance Table
|
|
2703
|
+
table = Table(title="Sector Performance", box=box.ROUNDED, header_style="bold cyan", expand=True)
|
|
2704
|
+
table.add_column("Sector", style="white")
|
|
2705
|
+
table.add_column("Current", justify="right")
|
|
2706
|
+
table.add_column("Daily Gain", justify="right")
|
|
2707
|
+
table.add_column("Turnover", justify="right")
|
|
2708
|
+
table.add_column("52w Change", justify="right")
|
|
2709
|
+
|
|
2710
|
+
for item in top_sectors['data'][1:]: # Skip NEPSE (first item)
|
|
2711
|
+
sector = item.get('sector', 'N/A')
|
|
2712
|
+
current = float(item.get('current', 0))
|
|
2713
|
+
daily = float(item.get('daily_gain', 0))
|
|
2714
|
+
turn = float(item.get('turn_over', 0))
|
|
2715
|
+
weeks_52 = float(item.get('_52_weeks_change', 0))
|
|
2716
|
+
|
|
2717
|
+
s_color = "green" if daily > 0 else "red" if daily < 0 else "yellow"
|
|
2718
|
+
|
|
2719
|
+
table.add_row(
|
|
2720
|
+
sector,
|
|
2721
|
+
f"{current:,.2f}",
|
|
2722
|
+
f"[{s_color}]{daily:+.2f}%[/{s_color}]",
|
|
2723
|
+
format_number(turn),
|
|
2724
|
+
f"{weeks_52:+.2f}%"
|
|
2725
|
+
)
|
|
2726
|
+
|
|
2727
|
+
console.print(table)
|
|
2728
|
+
console.print(f"[dim]As of: {timestamp} | Note: Updates after 3:00 PM[/dim]\n", justify="center")
|
|
2729
|
+
|
|
2730
|
+
except Exception as e:
|
|
2731
|
+
console.print(f"[bold red]⚠️ Error fetching market summary:[/bold red] {str(e)}\n")
|
|
2732
|
+
|
|
2733
|
+
def cmd_topgl():
|
|
2734
|
+
"""Display top 10 gainers and losers"""
|
|
2735
|
+
try:
|
|
2736
|
+
with console.status("[bold green]Fetching top gainers and losers...", spinner="dots"):
|
|
2737
|
+
response = requests.get("https://merolagani.com/LatestMarket.aspx", timeout=10)
|
|
2738
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
2739
|
+
|
|
2740
|
+
tgtl_col = soup.find('div', class_="col-md-4 hidden-xs hidden-sm")
|
|
2741
|
+
tgtl_tables = tgtl_col.find_all('table')
|
|
2742
|
+
|
|
2743
|
+
gainers = tgtl_tables[0]
|
|
2744
|
+
gainers_row = gainers.find_all('tr')
|
|
2745
|
+
|
|
2746
|
+
losers = tgtl_tables[1]
|
|
2747
|
+
losers_row = losers.find_all('tr')
|
|
2748
|
+
|
|
2749
|
+
# Gainers Table
|
|
2750
|
+
g_table = Table(title="📈 TOP 10 GAINERS", box=box.ROUNDED, header_style="bold green", expand=True)
|
|
2751
|
+
g_table.add_column("#", style="dim", width=4)
|
|
2752
|
+
g_table.add_column("Symbol", style="bold white")
|
|
2753
|
+
g_table.add_column("LTP", justify="right")
|
|
2754
|
+
g_table.add_column("%Chg", justify="right", style="green")
|
|
2755
|
+
g_table.add_column("High", justify="right", style="dim")
|
|
2756
|
+
g_table.add_column("Low", justify="right", style="dim")
|
|
2757
|
+
g_table.add_column("Volume", justify="right")
|
|
2758
|
+
|
|
2759
|
+
for idx, tr in enumerate(gainers_row[1:], 1):
|
|
2760
|
+
tds = tr.find_all('td')
|
|
2761
|
+
if tds and len(tds) >= 8:
|
|
2762
|
+
medal = ["🥇", "🥈", "🥉"] + [""] * 7
|
|
2763
|
+
g_table.add_row(
|
|
2764
|
+
f"{idx} {medal[idx-1]}",
|
|
2765
|
+
tds[0].text,
|
|
2766
|
+
tds[1].text,
|
|
2767
|
+
f"+{tds[2].text}%",
|
|
2768
|
+
tds[3].text,
|
|
2769
|
+
tds[4].text,
|
|
2770
|
+
format_number(tds[6].text)
|
|
2771
|
+
)
|
|
2772
|
+
|
|
2773
|
+
# Losers Table
|
|
2774
|
+
l_table = Table(title="📉 TOP 10 LOSERS", box=box.ROUNDED, header_style="bold red", expand=True)
|
|
2775
|
+
l_table.add_column("#", style="dim", width=4)
|
|
2776
|
+
l_table.add_column("Symbol", style="bold white")
|
|
2777
|
+
l_table.add_column("LTP", justify="right")
|
|
2778
|
+
l_table.add_column("%Chg", justify="right", style="red")
|
|
2779
|
+
l_table.add_column("High", justify="right", style="dim")
|
|
2780
|
+
l_table.add_column("Low", justify="right", style="dim")
|
|
2781
|
+
l_table.add_column("Volume", justify="right")
|
|
2782
|
+
|
|
2783
|
+
for idx, tr in enumerate(losers_row[1:], 1):
|
|
2784
|
+
tds = tr.find_all('td')
|
|
2785
|
+
if tds and len(tds) >= 8:
|
|
2786
|
+
l_table.add_row(
|
|
2787
|
+
str(idx),
|
|
2788
|
+
tds[0].text,
|
|
2789
|
+
tds[1].text,
|
|
2790
|
+
f"-{tds[2].text}%",
|
|
2791
|
+
tds[3].text,
|
|
2792
|
+
tds[4].text,
|
|
2793
|
+
format_number(tds[6].text)
|
|
2794
|
+
)
|
|
2795
|
+
|
|
2796
|
+
console.print(g_table)
|
|
2797
|
+
console.print(l_table)
|
|
2798
|
+
|
|
2799
|
+
timestamp = get_ss_time()
|
|
2800
|
+
console.print(f"[dim]As of: {timestamp}[/dim]\n", justify="center")
|
|
2801
|
+
|
|
2802
|
+
except Exception as e:
|
|
2803
|
+
console.print(f"[bold red]⚠️ Error fetching top gainers/losers:[/bold red] {str(e)}\n")
|
|
2804
|
+
|
|
2805
|
+
def cmd_stonk(stock_name):
|
|
2806
|
+
"""Display stock details (information only - no charts/alerts)"""
|
|
2807
|
+
try:
|
|
2808
|
+
stock_name = stock_name.upper()
|
|
2809
|
+
with console.status(f"[bold green]Fetching details for {stock_name}...", spinner="dots"):
|
|
2810
|
+
import cloudscraper
|
|
2811
|
+
scraper = cloudscraper.create_scraper()
|
|
2812
|
+
|
|
2813
|
+
# Try NepseAlpha API first
|
|
2814
|
+
stock_price_data = None
|
|
2815
|
+
try:
|
|
2816
|
+
response = scraper.get('https://nepsealpha.com/live/stocks', timeout=10)
|
|
2817
|
+
if response.status_code == 200:
|
|
2818
|
+
data = response.json()
|
|
2819
|
+
prices = data.get('stock_live', {}).get('prices', [])
|
|
2820
|
+
|
|
2821
|
+
for item in prices:
|
|
2822
|
+
if item.get('symbol', '').upper() == stock_name:
|
|
2823
|
+
stock_price_data = item
|
|
2824
|
+
break
|
|
2825
|
+
except:
|
|
2826
|
+
pass
|
|
2827
|
+
|
|
2828
|
+
# Fetch company details from ShareSansar
|
|
2829
|
+
company_details = {
|
|
2830
|
+
"sector": "N/A",
|
|
2831
|
+
"share_registrar": "N/A",
|
|
2832
|
+
"company_fullform": stock_name,
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
try:
|
|
2836
|
+
response2 = requests.get(
|
|
2837
|
+
f"https://www.sharesansar.com/company/{stock_name}", timeout=10)
|
|
2838
|
+
|
|
2839
|
+
if response2.status_code == 200:
|
|
2840
|
+
soup2 = BeautifulSoup(response2.text, "lxml")
|
|
2841
|
+
all_rows = soup2.find_all("div", class_="row")
|
|
2842
|
+
|
|
2843
|
+
if len(all_rows) >= 6:
|
|
2844
|
+
info_row = all_rows[5]
|
|
2845
|
+
second_row = info_row.find_all("div", class_="col-md-12")
|
|
2846
|
+
if len(second_row) > 1:
|
|
2847
|
+
shareinfo = second_row[1]
|
|
2848
|
+
heading_list = shareinfo.find_all("h4")
|
|
2849
|
+
|
|
2850
|
+
if len(heading_list) > 2:
|
|
2851
|
+
company_details["sector"] = heading_list[1].find("span", class_="text-org").text
|
|
2852
|
+
company_details["share_registrar"] = heading_list[2].find("span", class_="text-org").text
|
|
2853
|
+
|
|
2854
|
+
company_full_form_tag = soup2.find(
|
|
2855
|
+
"h1", style="color: #333;font-size: 20px;font-weight: 600;"
|
|
2856
|
+
)
|
|
2857
|
+
if company_full_form_tag is not None:
|
|
2858
|
+
company_details["company_fullform"] = company_full_form_tag.text
|
|
2859
|
+
except:
|
|
2860
|
+
pass
|
|
2861
|
+
|
|
2862
|
+
# Fallback to ShareSansar if NepseAlpha failed
|
|
2863
|
+
if not stock_price_data:
|
|
2864
|
+
try:
|
|
2865
|
+
response_live = requests.get(
|
|
2866
|
+
"https://www.sharesansar.com/live-trading", timeout=10)
|
|
2867
|
+
|
|
2868
|
+
if response_live.status_code == 200:
|
|
2869
|
+
soup = BeautifulSoup(response_live.text, "lxml")
|
|
2870
|
+
stock_rows = soup.find_all("tr")
|
|
2871
|
+
|
|
2872
|
+
for row in stock_rows[1:]:
|
|
2873
|
+
row_data = row.find_all("td")
|
|
2874
|
+
|
|
2875
|
+
if len(row_data) > 9 and row_data[1].text.strip() == stock_name:
|
|
2876
|
+
close_price = float(row_data[2].text.strip().replace(',', ''))
|
|
2877
|
+
pt_change = float(row_data[3].text.strip().replace(',', ''))
|
|
2878
|
+
pct_change = row_data[4].text.strip()
|
|
2879
|
+
|
|
2880
|
+
color = "green" if pt_change > 0 else "red" if pt_change < 0 else "yellow"
|
|
2881
|
+
trend_icon = "▲" if pt_change > 0 else "▼" if pt_change < 0 else "•"
|
|
2882
|
+
|
|
2883
|
+
grid = Table.grid(expand=True, padding=(0, 2))
|
|
2884
|
+
grid.add_column(style="bold white")
|
|
2885
|
+
grid.add_column(justify="right")
|
|
2886
|
+
|
|
2887
|
+
grid.add_row("Last Traded Price", f"Rs. {row_data[2].text.strip()}")
|
|
2888
|
+
grid.add_row("Change", f"[{color}]{row_data[3].text.strip()} ({pct_change}) {trend_icon}[/{color}]")
|
|
2889
|
+
grid.add_row("Open", row_data[5].text.strip())
|
|
2890
|
+
grid.add_row("High", row_data[6].text.strip())
|
|
2891
|
+
grid.add_row("Low", row_data[7].text.strip())
|
|
2892
|
+
grid.add_row("Volume", row_data[8].text.strip())
|
|
2893
|
+
grid.add_row("Prev. Closing", row_data[9].text.strip())
|
|
2894
|
+
grid.add_row("Sector", company_details['sector'])
|
|
2895
|
+
grid.add_row("Share Registrar", company_details['share_registrar'])
|
|
2896
|
+
|
|
2897
|
+
panel = Panel(
|
|
2898
|
+
grid,
|
|
2899
|
+
title=f"[bold {color}]{stock_name} — {company_details['company_fullform']}[/]",
|
|
2900
|
+
subtitle="Source: ShareSansar",
|
|
2901
|
+
box=box.ROUNDED,
|
|
2902
|
+
border_style=color
|
|
2903
|
+
)
|
|
2904
|
+
console.print(panel)
|
|
2905
|
+
return
|
|
2906
|
+
except:
|
|
2907
|
+
pass
|
|
2908
|
+
|
|
2909
|
+
console.print(Panel(f"⚠️ Stock '{stock_name}' not found.", style="bold red", box=box.ROUNDED))
|
|
2910
|
+
return
|
|
2911
|
+
|
|
2912
|
+
# Use NepseAlpha data
|
|
2913
|
+
close_price = stock_price_data.get("close", 0)
|
|
2914
|
+
percent_change = stock_price_data.get("percent_change", 0)
|
|
2915
|
+
|
|
2916
|
+
try:
|
|
2917
|
+
if percent_change != 0 and close_price != 0:
|
|
2918
|
+
prev_close = close_price / (1 + percent_change / 100)
|
|
2919
|
+
pt_change = close_price - prev_close
|
|
2920
|
+
else:
|
|
2921
|
+
prev_close = close_price
|
|
2922
|
+
pt_change = 0
|
|
2923
|
+
except:
|
|
2924
|
+
prev_close = close_price
|
|
2925
|
+
pt_change = 0
|
|
2926
|
+
|
|
2927
|
+
color = "green" if pt_change > 0 else "red" if pt_change < 0 else "yellow"
|
|
2928
|
+
trend_icon = "▲" if pt_change > 0 else "▼" if pt_change < 0 else "•"
|
|
2929
|
+
|
|
2930
|
+
grid = Table.grid(expand=True, padding=(0, 2))
|
|
2931
|
+
grid.add_column(style="bold white")
|
|
2932
|
+
grid.add_column(justify="right")
|
|
2933
|
+
|
|
2934
|
+
grid.add_row("Last Traded Price", f"Rs. {close_price:,.2f}")
|
|
2935
|
+
grid.add_row("Change", f"[{color}]{pt_change:+,.2f} ({percent_change:+.2f}%) {trend_icon}[/{color}]")
|
|
2936
|
+
grid.add_row("Open", f"Rs. {stock_price_data.get('open', 0):,.2f}")
|
|
2937
|
+
grid.add_row("High", f"Rs. {stock_price_data.get('high', 0):,.2f}")
|
|
2938
|
+
grid.add_row("Low", f"Rs. {stock_price_data.get('low', 0):,.2f}")
|
|
2939
|
+
grid.add_row("Volume", f"{int(stock_price_data.get('volume', 0)):,}")
|
|
2940
|
+
grid.add_row("Prev. Closing", f"Rs. {prev_close:,.2f}")
|
|
2941
|
+
grid.add_row("Sector", company_details['sector'])
|
|
2942
|
+
grid.add_row("Share Registrar", company_details['share_registrar'])
|
|
2943
|
+
|
|
2944
|
+
panel = Panel(
|
|
2945
|
+
grid,
|
|
2946
|
+
title=f"[bold {color}]{stock_name} — {company_details['company_fullform']}[/]",
|
|
2947
|
+
subtitle=f"As of: {data.get('stock_live', {}).get('asOf', 'N/A')}",
|
|
2948
|
+
box=box.ROUNDED,
|
|
2949
|
+
border_style=color
|
|
2950
|
+
)
|
|
2951
|
+
console.print(panel)
|
|
2952
|
+
|
|
2953
|
+
except Exception as e:
|
|
2954
|
+
console.print(f"[bold red]⚠️ Error fetching stock data:[/bold red] {str(e)}\n")
|
|
2955
|
+
|
|
2956
|
+
# ============================================
|
|
2957
|
+
# Argument Parser and Command Metadata
|
|
2958
|
+
# ============================================
|
|
2959
|
+
|
|
2960
|
+
# Category ordering for command palette
|
|
2961
|
+
COMMAND_CATEGORY_ORDER = [
|
|
2962
|
+
"Market Data",
|
|
2963
|
+
"IPO Management",
|
|
2964
|
+
"Interactive Tools",
|
|
2965
|
+
"Configuration"
|
|
2966
|
+
]
|
|
2967
|
+
|
|
2968
|
+
# Map commands to their categories
|
|
2969
|
+
COMMAND_CATEGORY_MAP = {
|
|
2970
|
+
# Market Data
|
|
2971
|
+
"nepse": "Market Data",
|
|
2972
|
+
"subidx": "Market Data",
|
|
2973
|
+
"mktsum": "Market Data",
|
|
2974
|
+
"topgl": "Market Data",
|
|
2975
|
+
"stonk": "Market Data",
|
|
2976
|
+
|
|
2977
|
+
# IPO Management
|
|
2978
|
+
"ipo": "IPO Management",
|
|
2979
|
+
"apply": "IPO Management",
|
|
2980
|
+
"status": "IPO Management",
|
|
2981
|
+
|
|
2982
|
+
# Configuration
|
|
2983
|
+
"add-member": "Configuration",
|
|
2984
|
+
"list-members": "Configuration",
|
|
2985
|
+
"test-login": "Configuration",
|
|
2986
|
+
"get-portfolio": "Configuration",
|
|
2987
|
+
"dplist": "Configuration"
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
def build_parser():
|
|
2991
|
+
"""Build the argument parser for the CLI"""
|
|
2992
|
+
import argparse
|
|
2993
|
+
|
|
2994
|
+
parser = argparse.ArgumentParser(
|
|
2995
|
+
prog="nepse",
|
|
2996
|
+
description="NEPSE CLI - Stock Market & IPO Automation Tool",
|
|
2997
|
+
epilog="Use 'nepse interactive' for an interactive command palette."
|
|
2998
|
+
)
|
|
2999
|
+
|
|
3000
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
3001
|
+
|
|
3002
|
+
# Market Data Commands
|
|
3003
|
+
subparsers.add_parser("nepse", help="Display NEPSE indices")
|
|
3004
|
+
subparsers.add_parser("ipo", help="Show open IPOs")
|
|
3005
|
+
subparsers.add_parser("mktsum", help="Market summary")
|
|
3006
|
+
subparsers.add_parser("topgl", help="Top gainers and losers")
|
|
3007
|
+
|
|
3008
|
+
subidx_parser = subparsers.add_parser("subidx", help="Sub-index details")
|
|
3009
|
+
subidx_parser.add_argument("name", help="Sub-index name (e.g., BANKING, HOTELS)")
|
|
3010
|
+
|
|
3011
|
+
stonk_parser = subparsers.add_parser("stonk", help="Stock details")
|
|
3012
|
+
stonk_parser.add_argument("symbol", help="Stock symbol (e.g., NABIL)")
|
|
3013
|
+
|
|
3014
|
+
# IPO Commands
|
|
3015
|
+
subparsers.add_parser("apply", help="Apply for IPO interactively")
|
|
3016
|
+
subparsers.add_parser("status", help="Check IPO application status")
|
|
3017
|
+
|
|
3018
|
+
# Configuration Commands
|
|
3019
|
+
subparsers.add_parser("add-member", help="Add family member for IPO applications")
|
|
3020
|
+
subparsers.add_parser("list-members", help="List all family members")
|
|
3021
|
+
|
|
3022
|
+
test_login_parser = subparsers.add_parser("test-login", help="Test login credentials")
|
|
3023
|
+
test_login_parser.add_argument("member_id", nargs="?", help="Member ID to test (optional)")
|
|
3024
|
+
|
|
3025
|
+
portfolio_parser = subparsers.add_parser("get-portfolio", help="Get portfolio details")
|
|
3026
|
+
portfolio_parser.add_argument("member_id", nargs="?", help="Member ID (optional)")
|
|
3027
|
+
|
|
3028
|
+
subparsers.add_parser("dplist", help="List available DPs")
|
|
3029
|
+
|
|
3030
|
+
# Interactive Mode
|
|
3031
|
+
subparsers.add_parser("interactive", help="Launch interactive command palette")
|
|
3032
|
+
|
|
3033
|
+
return parser
|
|
3034
|
+
|
|
3035
|
+
def get_command_metadata():
|
|
3036
|
+
"""Return metadata for all commands (for interactive mode)"""
|
|
3037
|
+
return [
|
|
3038
|
+
# Market Data
|
|
3039
|
+
{"name": "nepse", "description": "Display NEPSE indices", "category": "Market Data"},
|
|
3040
|
+
{"name": "subidx <name>", "description": "Show sub-index details", "category": "Market Data"},
|
|
3041
|
+
{"name": "mktsum", "description": "Display market summary", "category": "Market Data"},
|
|
3042
|
+
{"name": "topgl", "description": "Show top gainers and losers", "category": "Market Data"},
|
|
3043
|
+
{"name": "stonk <symbol>", "description": "Show stock details", "category": "Market Data"},
|
|
3044
|
+
{"name": "ipo", "description": "List open IPOs", "category": "IPO Management"},
|
|
3045
|
+
|
|
3046
|
+
# IPO Management
|
|
3047
|
+
{"name": "apply", "description": "Apply for IPO (use --gui for browser window)", "category": "IPO Management"},
|
|
3048
|
+
{"name": "apply-all", "description": "Apply IPO for all family members", "category": "IPO Management"},
|
|
3049
|
+
|
|
3050
|
+
# Configuration
|
|
3051
|
+
{"name": "add", "description": "Add/update family member", "category": "Configuration"},
|
|
3052
|
+
{"name": "list", "description": "List all family members", "category": "Configuration"},
|
|
3053
|
+
{"name": "login [name]", "description": "Test login for member", "category": "Configuration"},
|
|
3054
|
+
{"name": "portfolio [name]", "description": "Get portfolio for member", "category": "Configuration"},
|
|
3055
|
+
{"name": "dp-list", "description": "List available DPs", "category": "Configuration"},
|
|
3056
|
+
|
|
3057
|
+
# Interactive
|
|
3058
|
+
{"name": "help", "description": "Show help information", "category": "Interactive Tools"},
|
|
3059
|
+
{"name": "exit", "description": "Exit the CLI", "category": "Interactive Tools"},
|
|
3060
|
+
]
|
|
3061
|
+
|
|
3062
|
+
def print_logo():
|
|
3063
|
+
"""Render the gradient welcome logo when interactive mode launches."""
|
|
3064
|
+
# Initialize colorama for Windows ANSI support
|
|
3065
|
+
colorama_init(autoreset=True)
|
|
3066
|
+
|
|
3067
|
+
# Add some top margin/gap before the logo
|
|
3068
|
+
print("\n")
|
|
3069
|
+
|
|
3070
|
+
logo_lines = [
|
|
3071
|
+
" ███╗ ██╗███████╗██████╗ ███████╗███████╗ ██████╗██╗ ██╗",
|
|
3072
|
+
" ████╗ ██║██╔════╝██╔══██╗██╔════╝██╔════╝ ██╔════╝██║ ██║",
|
|
3073
|
+
" ██╔██╗ ██║█████╗ ██████╔╝███████╗█████╗ █████╗██║ ██║ ██║",
|
|
3074
|
+
" ██║╚██╗██║██╔══╝ ██╔═══╝ ╚════██║██╔══╝ ╚════╝██║ ██║ ██║",
|
|
3075
|
+
" ██║ ╚████║███████╗██║ ███████║███████╗ ╚██████╗███████╗██║",
|
|
3076
|
+
" ╚═╝ ╚═══╝╚══════╝╚═╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝",
|
|
3077
|
+
]
|
|
3078
|
+
|
|
3079
|
+
# Gradient colors from bright green to dark green (RGB values)
|
|
3080
|
+
colors = [
|
|
3081
|
+
(0, 255, 0), # Bright green
|
|
3082
|
+
(0, 230, 0),
|
|
3083
|
+
(0, 204, 0),
|
|
3084
|
+
(0, 179, 0),
|
|
3085
|
+
(0, 153, 0),
|
|
3086
|
+
(0, 128, 0), # Dark green
|
|
3087
|
+
]
|
|
3088
|
+
|
|
3089
|
+
for idx, line in enumerate(logo_lines):
|
|
3090
|
+
r, g, b = colors[idx]
|
|
3091
|
+
# Use ANSI 24-bit true color escape codes
|
|
3092
|
+
print(f"\033[38;2;{r};{g};{b}m{line}\033[0m")
|
|
3093
|
+
|
|
3094
|
+
|
|
3095
|
+
def fuzzy_filter_commands(commands: List[Dict[str, str]], query: str) -> List[Dict[str, str]]:
|
|
3096
|
+
if not query:
|
|
3097
|
+
return commands
|
|
3098
|
+
query_lower = query.lower()
|
|
3099
|
+
filtered: List[Dict[str, str]] = []
|
|
3100
|
+
for command in commands:
|
|
3101
|
+
haystack = f"{command['name']} {command['description']}".lower()
|
|
3102
|
+
if query_lower in haystack:
|
|
3103
|
+
filtered.append(command)
|
|
3104
|
+
continue
|
|
3105
|
+
ratio = difflib.SequenceMatcher(None, query_lower, command['name'].lower()).ratio()
|
|
3106
|
+
if ratio >= 0.6:
|
|
3107
|
+
filtered.append(command)
|
|
3108
|
+
return filtered
|
|
3109
|
+
|
|
3110
|
+
|
|
3111
|
+
def display_command_palette(commands: List[Dict[str, str]], category_order: List[str], query: str = "") -> None:
|
|
3112
|
+
# Hack to fix Rich output when running inside prompt_toolkit's patch_stdout
|
|
3113
|
+
# We must temporarily restore the original stdout so Rich can detect the terminal properly
|
|
3114
|
+
from prompt_toolkit.patch_stdout import StdoutProxy
|
|
3115
|
+
original_stdout = sys.stdout
|
|
3116
|
+
if isinstance(sys.stdout, StdoutProxy):
|
|
3117
|
+
sys.stdout = sys.stdout.original_stdout
|
|
3118
|
+
|
|
3119
|
+
try:
|
|
3120
|
+
filtered_commands = fuzzy_filter_commands(commands, query)
|
|
3121
|
+
if not filtered_commands:
|
|
3122
|
+
message = f"No commands match '{query}'" if query else "No commands available"
|
|
3123
|
+
console.print(Panel(Text(message, justify="center"), title="Available Commands", border_style="red"))
|
|
3124
|
+
return
|
|
3125
|
+
|
|
3126
|
+
grouped: Dict[str, List[Dict[str, str]]] = {category: [] for category in category_order}
|
|
3127
|
+
for command in filtered_commands:
|
|
3128
|
+
grouped.setdefault(command['category'], []).append(command)
|
|
3129
|
+
|
|
3130
|
+
sections = []
|
|
3131
|
+
for category in category_order:
|
|
3132
|
+
items = grouped.get(category) or []
|
|
3133
|
+
if not items:
|
|
3134
|
+
continue
|
|
3135
|
+
header = Text(category, style="bold green")
|
|
3136
|
+
table = Table.grid(expand=True)
|
|
3137
|
+
table.add_column(style="bold cyan", width=20)
|
|
3138
|
+
table.add_column()
|
|
3139
|
+
for item in items:
|
|
3140
|
+
table.add_row(item['name'], item['description'])
|
|
3141
|
+
sections.extend([header, table, Text("")])
|
|
3142
|
+
|
|
3143
|
+
sections.append(Text("Type to search commands...", style="dim"))
|
|
3144
|
+
console.print(Panel(Group(*sections), title="Available Commands", border_style="green"))
|
|
3145
|
+
finally:
|
|
3146
|
+
# Restore the proxy
|
|
3147
|
+
sys.stdout = original_stdout
|
|
3148
|
+
|
|
3149
|
+
|
|
3150
|
+
class NepseCommandCompleter(Completer):
|
|
3151
|
+
"""
|
|
3152
|
+
Completer that supports:
|
|
3153
|
+
1. Normal command completion
|
|
3154
|
+
2. 'Gemini-style' search when starting with '/'
|
|
3155
|
+
- Shows command + description
|
|
3156
|
+
- Filters by both name and description
|
|
3157
|
+
"""
|
|
3158
|
+
|
|
3159
|
+
def __init__(self, metadata: List[Dict[str, str]]):
|
|
3160
|
+
self.metadata = metadata
|
|
3161
|
+
# Quick lookup for normal completion
|
|
3162
|
+
self.names = [m['name'] for m in metadata] + ["exit", "quit", "help", "?"]
|
|
3163
|
+
|
|
3164
|
+
def get_completions(self, document, complete_event): # type: ignore[override]
|
|
3165
|
+
text = document.text_before_cursor
|
|
3166
|
+
|
|
3167
|
+
# Check if we are in "Search Mode" (starts with /)
|
|
3168
|
+
if text.startswith('/'):
|
|
3169
|
+
query = text[1:].lower() # Strip the leading /
|
|
3170
|
+
|
|
3171
|
+
# Filter commands
|
|
3172
|
+
for cmd in self.metadata:
|
|
3173
|
+
name = cmd['name']
|
|
3174
|
+
desc = cmd.get('description', '')
|
|
3175
|
+
|
|
3176
|
+
# Fuzzy-ish match: query in name OR query in description
|
|
3177
|
+
if query in name.lower() or query in desc.lower():
|
|
3178
|
+
yield Completion(
|
|
3179
|
+
name,
|
|
3180
|
+
start_position=-len(query),
|
|
3181
|
+
display=FormattedText([
|
|
3182
|
+
("class:completion-command", f"{name:<15}"),
|
|
3183
|
+
("class:completion-description", f" {desc}")
|
|
3184
|
+
]),
|
|
3185
|
+
)
|
|
3186
|
+
|
|
3187
|
+
# Filter built-ins
|
|
3188
|
+
for builtin in ["exit", "quit", "help", "?"]:
|
|
3189
|
+
if query in builtin:
|
|
3190
|
+
yield Completion(
|
|
3191
|
+
builtin,
|
|
3192
|
+
start_position=-len(query),
|
|
3193
|
+
display=FormattedText([
|
|
3194
|
+
("class:completion-command", f"{builtin:<15}"),
|
|
3195
|
+
("class:completion-builtin", " Built-in command")
|
|
3196
|
+
]),
|
|
3197
|
+
)
|
|
3198
|
+
else:
|
|
3199
|
+
# Normal completion (first word)
|
|
3200
|
+
word = text.split(' ')[-1]
|
|
3201
|
+
for name in self.names:
|
|
3202
|
+
if name.startswith(word):
|
|
3203
|
+
yield Completion(name, start_position=-len(word))
|
|
3204
|
+
|
|
3205
|
+
|
|
3206
|
+
LEGACY_SHORTCUTS = {
|
|
3207
|
+
"1": "apply",
|
|
3208
|
+
"2": "add",
|
|
3209
|
+
"3": "list",
|
|
3210
|
+
"4": "portfolio",
|
|
3211
|
+
"5": "login",
|
|
3212
|
+
"6": "dp-list",
|
|
3213
|
+
"7": "apply-all",
|
|
3214
|
+
"8": "ipo",
|
|
3215
|
+
"9": "nepse",
|
|
3216
|
+
"10": "subidx",
|
|
3217
|
+
"11": "mktsum",
|
|
3218
|
+
"12": "topgl",
|
|
3219
|
+
"13": "stonk",
|
|
3220
|
+
"0": "exit",
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
|
|
3224
|
+
def ensure_history_file() -> None:
|
|
3225
|
+
if not CLI_HISTORY_FILE.exists():
|
|
3226
|
+
CLI_HISTORY_FILE.touch()
|
|
3227
|
+
|
|
3228
|
+
|
|
3229
|
+
def create_prompt_session(command_metadata: List[Dict[str, str]]) -> PromptSession:
|
|
3230
|
+
ensure_history_file()
|
|
3231
|
+
completer = NepseCommandCompleter(command_metadata)
|
|
3232
|
+
return PromptSession(
|
|
3233
|
+
history=FileHistory(str(CLI_HISTORY_FILE)),
|
|
3234
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
3235
|
+
completer=completer,
|
|
3236
|
+
complete_style=CompleteStyle.COLUMN,
|
|
3237
|
+
style=CLI_PROMPT_STYLE,
|
|
3238
|
+
)
|
|
3239
|
+
|
|
3240
|
+
|
|
3241
|
+
def _resolve_member_by_name(member_name: str):
|
|
3242
|
+
config = load_family_members()
|
|
3243
|
+
for member in config.get('members', []):
|
|
3244
|
+
if member['name'].lower() == member_name.lower():
|
|
3245
|
+
return member
|
|
3246
|
+
return None
|
|
3247
|
+
|
|
3248
|
+
|
|
3249
|
+
def execute_interactive_command(command: str, args: List[str], context: Dict[str, object]) -> bool:
|
|
3250
|
+
command = command.lower()
|
|
3251
|
+
if command in {"help", "?"}:
|
|
3252
|
+
display_command_palette(context['metadata'], context['category_order'])
|
|
3253
|
+
return True
|
|
3254
|
+
|
|
3255
|
+
flag_args = [arg for arg in args if arg.startswith("--")]
|
|
3256
|
+
positional_args = [arg for arg in args if not arg.startswith("--")]
|
|
3257
|
+
gui_requested = "--gui" in flag_args
|
|
3258
|
+
headless = not gui_requested
|
|
3259
|
+
|
|
3260
|
+
if command == "apply":
|
|
3261
|
+
context['apply_ipo'](auto_load=True, headless=headless)
|
|
3262
|
+
return True
|
|
3263
|
+
if command == "apply-all":
|
|
3264
|
+
context['apply_all'](headless=headless)
|
|
3265
|
+
return True
|
|
3266
|
+
if command == "add":
|
|
3267
|
+
context['add_member']()
|
|
3268
|
+
return True
|
|
3269
|
+
if command == "list":
|
|
3270
|
+
context['list_members']()
|
|
3271
|
+
return True
|
|
3272
|
+
if command == "portfolio":
|
|
3273
|
+
member = None
|
|
3274
|
+
if positional_args:
|
|
3275
|
+
member = _resolve_member_by_name(positional_args[0])
|
|
3276
|
+
if not member:
|
|
3277
|
+
print(f"\n✗ Member '{positional_args[0]}' not found.")
|
|
3278
|
+
if not member:
|
|
3279
|
+
member = context['select_member']()
|
|
3280
|
+
if member:
|
|
3281
|
+
context['portfolio'](member, headless=headless)
|
|
3282
|
+
return True
|
|
3283
|
+
if command == "login":
|
|
3284
|
+
member = context['select_member']()
|
|
3285
|
+
if member:
|
|
3286
|
+
context['login'](member, headless=headless)
|
|
3287
|
+
return True
|
|
3288
|
+
if command == "dp-list":
|
|
3289
|
+
context['dp_list']()
|
|
3290
|
+
return True
|
|
3291
|
+
if command == "ipo":
|
|
3292
|
+
context['cmd_ipo']()
|
|
3293
|
+
return True
|
|
3294
|
+
if command == "nepse":
|
|
3295
|
+
context['cmd_nepse']()
|
|
3296
|
+
return True
|
|
3297
|
+
if command == "subidx":
|
|
3298
|
+
if positional_args:
|
|
3299
|
+
subindex_name = " ".join(positional_args)
|
|
3300
|
+
else:
|
|
3301
|
+
print("\nAvailable sub-indices: banking, development-bank, finance, hotels-and-tourism,")
|
|
3302
|
+
print("hydropower, investment, life-insurance, manufacturing-and-processing,")
|
|
3303
|
+
print("microfinance, non-life-insurance, others, trading")
|
|
3304
|
+
subindex_name = input("\nEnter sub-index name: ").strip()
|
|
3305
|
+
if subindex_name:
|
|
3306
|
+
context['cmd_subidx'](subindex_name)
|
|
3307
|
+
else:
|
|
3308
|
+
print("✗ Sub-index name is required.")
|
|
3309
|
+
return True
|
|
3310
|
+
if command == "mktsum":
|
|
3311
|
+
context['cmd_mktsum']()
|
|
3312
|
+
return True
|
|
3313
|
+
if command == "topgl":
|
|
3314
|
+
context['cmd_topgl']()
|
|
3315
|
+
return True
|
|
3316
|
+
if command == "stonk":
|
|
3317
|
+
symbol = positional_args[0] if positional_args else input("\nEnter stock symbol (e.g., NABIL): ").strip()
|
|
3318
|
+
if symbol:
|
|
3319
|
+
context['cmd_stonk'](symbol.upper())
|
|
3320
|
+
else:
|
|
3321
|
+
print("✗ Stock symbol is required.")
|
|
3322
|
+
return True
|
|
3323
|
+
if command == "dplist":
|
|
3324
|
+
context['dp_list']()
|
|
3325
|
+
return True
|
|
3326
|
+
if command in {"exit", "quit"}:
|
|
3327
|
+
return False
|
|
3328
|
+
return False
|
|
3329
|
+
|
|
3330
|
+
|
|
3331
|
+
def main():
|
|
3332
|
+
"""Modern interactive CLI entry point."""
|
|
3333
|
+
# Ensure Playwright browsers are available
|
|
3334
|
+
ensure_playwright_browsers()
|
|
3335
|
+
|
|
3336
|
+
# All functions are now in this file - no imports needed from nepse_cli
|
|
3337
|
+
|
|
3338
|
+
command_metadata = get_command_metadata()
|
|
3339
|
+
session = create_prompt_session(command_metadata)
|
|
3340
|
+
|
|
3341
|
+
# Print logo BEFORE entering patch_stdout context so Rich colors work
|
|
3342
|
+
print_logo()
|
|
3343
|
+
print("\nType '/' to search commands, 'help' for hints, and 'exit' to quit.\n")
|
|
3344
|
+
|
|
3345
|
+
context = {
|
|
3346
|
+
'apply_ipo': apply_ipo,
|
|
3347
|
+
'apply_all': apply_ipo_for_all_members,
|
|
3348
|
+
'add_member': add_family_member,
|
|
3349
|
+
'list_members': list_family_members,
|
|
3350
|
+
'portfolio': get_portfolio_for_member,
|
|
3351
|
+
'login': test_login_for_member,
|
|
3352
|
+
'dp_list': get_dp_list,
|
|
3353
|
+
'cmd_ipo': cmd_ipo,
|
|
3354
|
+
'cmd_nepse': cmd_nepse,
|
|
3355
|
+
'cmd_subidx': cmd_subidx,
|
|
3356
|
+
'cmd_mktsum': cmd_mktsum,
|
|
3357
|
+
'cmd_topgl': cmd_topgl,
|
|
3358
|
+
'cmd_stonk': cmd_stonk,
|
|
3359
|
+
'select_member': select_family_member,
|
|
3360
|
+
'metadata': command_metadata,
|
|
3361
|
+
'category_order': COMMAND_CATEGORY_ORDER,
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
prompt_tokens = FormattedText([("class:prompt", "> ")])
|
|
3365
|
+
|
|
3366
|
+
while True:
|
|
3367
|
+
try:
|
|
3368
|
+
with patch_stdout():
|
|
3369
|
+
user_input = session.prompt(prompt_tokens)
|
|
3370
|
+
except KeyboardInterrupt:
|
|
3371
|
+
print("\n(Press Ctrl+D or type 'exit' to quit, Enter to continue)")
|
|
3372
|
+
continue
|
|
3373
|
+
except EOFError:
|
|
3374
|
+
print("\nGoodbye!")
|
|
3375
|
+
break
|
|
3376
|
+
|
|
3377
|
+
user_input = user_input.strip()
|
|
3378
|
+
if not user_input:
|
|
3379
|
+
continue
|
|
3380
|
+
|
|
3381
|
+
# Handle "/command" syntax
|
|
3382
|
+
if user_input.startswith('/'):
|
|
3383
|
+
# Strip the slash
|
|
3384
|
+
user_input = user_input[1:].strip()
|
|
3385
|
+
|
|
3386
|
+
# If user just typed '/' and hit enter, show palette (legacy behavior fallback)
|
|
3387
|
+
if not user_input:
|
|
3388
|
+
display_command_palette(command_metadata, COMMAND_CATEGORY_ORDER, "")
|
|
3389
|
+
continue
|
|
3390
|
+
|
|
3391
|
+
try:
|
|
3392
|
+
tokens = shlex.split(user_input)
|
|
3393
|
+
except ValueError as exc:
|
|
3394
|
+
print(f"✗ Unable to parse input: {exc}")
|
|
3395
|
+
continue
|
|
3396
|
+
if not tokens:
|
|
3397
|
+
continue
|
|
3398
|
+
|
|
3399
|
+
command = LEGACY_SHORTCUTS.get(tokens[0], tokens[0])
|
|
3400
|
+
args = tokens[1:]
|
|
3401
|
+
|
|
3402
|
+
if command in {"exit", "quit"}:
|
|
3403
|
+
print("Goodbye!")
|
|
3404
|
+
break
|
|
3405
|
+
|
|
3406
|
+
try:
|
|
3407
|
+
handled = execute_interactive_command(command, args, context)
|
|
3408
|
+
if not handled:
|
|
3409
|
+
print(f"Unknown command: '{user_input}'. Type '/' to explore commands.")
|
|
3410
|
+
except KeyboardInterrupt:
|
|
3411
|
+
print("\n\n✗ Command cancelled")
|
|
3412
|
+
continue
|
|
3413
|
+
except Exception as e:
|
|
3414
|
+
print(f"\n✗ Error executing command: {e}")
|
|
3415
|
+
continue
|
|
3416
|
+
|
|
3417
|
+
if __name__ == "__main__":
|
|
3418
|
+
main()
|