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 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()