import { Component } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MarketService } from '../marketselect/market.service'; import { Router } from '@angular/router'; import { Observable, of } from 'rxjs'; import { debounceTime, finalize, map, startWith, switchMap } from 'rxjs/operators'; // --- Types --- interface Company { symbol: string; company: string; } interface CompanyOption { symbol: string; company: string; } // for chips view interface IndexRef { code: string; name: string; } type MarketMap = Record>; interface SearchHit { symbol: string; company: string; indexCode: string; indexName: string; exchange: string; country: string; } @Component({ selector: 'app-toolspage', standalone: true, imports: [ CommonModule, FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatAutocompleteModule, MatIconModule, MatButtonModule, MatChipsModule, MatProgressSpinnerModule ], templateUrl: './toolspage.component.html', styleUrls: ['./toolspage.component.scss'] }) export class ToolspageComponent { // ---------- Backend / config ---------- private readonly API = location.hostname.endsWith('hf.space') ? 'https://pykara-pytrade-backend.hf.space' : 'http://127.0.0.1:5000'; loading = false; error: string | null = null; // ---------- Global search (backend-powered) ---------- tickerCtrl = new FormControl('', { nonNullable: true }); filteredCompanies$!: Observable; globalLoading = false; // ---------- Filters (country → exchange → index) ---------- markets: MarketMap = { India: { 'NSE (National Stock Exchange)': [ { code: 'NIFTY50', name: 'NIFTY 50' }, { code: 'NIFTY100', name: 'NIFTY 100' }, { code: 'NIFTY200', name: 'NIFTY 200' }, { code: 'NIFTYMID100', name: 'NIFTY Midcap 100' }, { code: 'NIFTY500', name: 'NIFTY 500' } ] }, 'United States': { NASDAQ: [ { code: 'NASDAQ100', name: 'NASDAQ-100' } ] }, Germany: { 'XETRA (Deutsche Börse)': [ { code: 'DAX40', name: 'DAX 40' } ] }, Sweden: { 'OMX Stockholm': [ { code: 'OMXS30', name: 'OMX Stockholm 30' } ] } }; // ---------- Selections ---------- selectedCountry = ''; selectedExchange = ''; selectedIndexName = ''; /** Global selection used for analysis (symbols only) */ selectedCompanies: string[] = []; /** Current index selection bound to (symbols only) */ indexSelected: string[] = []; /** Names for chips (search picks only): symbol -> company name */ private selectedNameMap = new Map(); /** Symbols picked via SEARCH only (used to show chips) */ private searchSelected = new Set(); selectedTradingType = 'swing'; // ---------- Derived lists for the index path ---------- countries = Object.keys(this.markets); exchanges: string[] = []; indices: IndexRef[] = []; companies: Company[] = []; // Fast lookup & membership for current index private companiesBySymbol = new Map(); // symbol -> name private currentIndexSymbols: Set = new Set(); // membership only // ---------- UI flags ---------- allSelected = false; loadingCompanies = false; constructor( private http: HttpClient, private marketService: MarketService, private router: Router ) { } // ---------- Init ---------- ngOnInit(): void { // Wire the search box to /searchcompanies this.filteredCompanies$ = this.tickerCtrl.valueChanges.pipe( startWith(''), debounceTime(200), map((q: any) => (q || '').trim()), switchMap((query: any) => { if (!query) return of([] as SearchHit[]); this.globalLoading = true; const params = new HttpParams().set('q', query).set('limit', '50'); return this.http .get<{ results: SearchHit[] }>(`${this.API}/search_companies`, { params }) .pipe( map((res: any) => (Array.isArray(res?.results) ? res.results : [])), finalize(() => (this.globalLoading = false)) ); }) ); console.log(this.filteredCompanies$); } // ---------- Search selection ---------- onPickTicker(hit: SearchHit): void { // Add to global selection and mark as search-picked this.addToSelected(hit.symbol, hit.company); this.searchSelected.add(hit.symbol); // If this symbol is in the currently loaded index, also reflect in dropdown if (this.currentIndexSymbols.has(hit.symbol) && !this.indexSelected.includes(hit.symbol)) { this.indexSelected = [...this.indexSelected, hit.symbol]; } this.allSelected = this.isAllSelected(); this.tickerCtrl.setValue(''); // clear } clearTickerSearch(): void { this.tickerCtrl.setValue(''); } // ---------- Country / Exchange / Index handlers ---------- onCountryChange(): void { this.error = null; this.selectedExchange = ''; this.selectedIndexName = ''; this.exchanges = this.selectedCountry ? Object.keys(this.markets[this.selectedCountry]) : []; this.indices = []; this.companies = []; this.resetCompanyLookup(); this.indexSelected = []; // reset dropdown model } onExchangeChange(): void { this.error = null; this.selectedIndexName = ''; this.indices = this.markets[this.selectedCountry]?.[this.selectedExchange] ?? []; this.companies = []; this.resetCompanyLookup(); this.indexSelected = []; // reset dropdown model } onIndexChange(): void { this.error = null; const ref = this.indices.find(i => i.name === this.selectedIndexName) || this.indices.find(i => i.code === this.selectedIndexName); const indexCode = (ref?.code || this.deriveIndexCode(this.selectedIndexName)).toUpperCase(); this.companies = []; this.resetCompanyLookup(); if (!indexCode) { this.error = 'Unable to resolve index code from selection.'; return; } this.fetchCompanies(indexCode); } // ---------- Multi-select model change (merge with global, no chips for dropdown) ---------- onCompaniesModelChange(newSymbols: string[]): void { this.indexSelected = [...newSymbols]; // Recompute global: (global - currentIndex) ∪ indexSelected const others = this.selectedCompanies.filter(s => !this.currentIndexSymbols.has(s)); this.selectedCompanies = Array.from(new Set([...others, ...this.indexSelected])); // If something was deselected via dropdown, also remove its chip if it was a search-pick for (const s of [...this.searchSelected]) { if (!this.selectedCompanies.includes(s)) this.searchSelected.delete(s); } this.allSelected = this.isAllSelected(); } // ---------- Select All only for current index ---------- toggleSelectAll(): void { this.allSelected = !this.allSelected; if (this.allSelected) { // Select all in current index (chips are not added here) this.indexSelected = Array.from(this.currentIndexSymbols); } else { // Deselect all current index symbols from global and chips const current = Array.from(this.currentIndexSymbols); this.indexSelected = []; this.selectedCompanies = this.selectedCompanies.filter(s => !this.currentIndexSymbols.has(s)); for (const s of current) this.searchSelected.delete(s); } // Keep any selections from other indices const others = this.selectedCompanies.filter(s => !this.currentIndexSymbols.has(s)); this.selectedCompanies = Array.from(new Set([...others, ...this.indexSelected])); } isAllSelected(): boolean { if (this.companies.length === 0) return false; const selectedSet = new Set(this.indexSelected); return this.companies.every(c => selectedSet.has(c.symbol)); } trackBySymbol(_: number, c: { symbol: string }): string { return c.symbol; } // ---------- Backend: fetch constituents for an index ---------- private fetchCompanies(indexCode: string): void { this.loadingCompanies = true; const params = new HttpParams().set('code', indexCode); this.http.get<{ code: string; count: number; constituents: Company[] }>( `${this.API}/getcompanies`, { params } ).subscribe({ next: (res:any) => { this.error = null; this.companies = res?.constituents ?? []; // Build lookups for this index this.companiesBySymbol.clear(); this.currentIndexSymbols = new Set(); for (const c of this.companies) { this.companiesBySymbol.set(c.symbol, c.company); this.currentIndexSymbols.add(c.symbol); } // Seed dropdown selection with intersection of global selection & current index const selectedSet = new Set(this.selectedCompanies); this.indexSelected = this.companies .map(c => c.symbol) .filter(sym => selectedSet.has(sym)); // Do NOT set names here (chips are for search picks only) this.allSelected = this.isAllSelected(); this.loadingCompanies = false; }, error: (err:any) => { console.error('GET /getcompanies failed', err); this.error = (err?.error?.error as string) || 'Failed to load companies.'; this.companies = []; this.companiesBySymbol.clear(); this.currentIndexSymbols = new Set(); this.indexSelected = []; this.allSelected = false; this.loadingCompanies = false; } }); } // ---------- Helpers ---------- private deriveIndexCode(name: string): string { return (name || '') .toUpperCase() .replace(/S&P/g, 'SP') .replace(/&/g, 'AND') .replace(/\s+/g, '') .replace(/[^A-Z0-9]/g, ''); } getMarket(): string[] { return this.exchanges; } getMarketDivision(): string[] { return this.indices.map(i => i.name); } getCompanies(): Company[] { return this.companies; } private addToSelected(symbol: string, companyName: string): void { if (!this.selectedCompanies.includes(symbol)) { this.selectedCompanies = [...this.selectedCompanies, symbol]; } // Store name only for search picks (chips) if (companyName) this.selectedNameMap.set(symbol, companyName); } private resetCompanyLookup(): void { this.companiesBySymbol.clear(); this.currentIndexSymbols = new Set(); this.allSelected = false; } // Chips show only search-picked symbols get selectedChips(): CompanyOption[] { return Array.from(this.searchSelected).map(s => ({ symbol: s, company: this.selectedNameMap.get(s) || s })); } // ---------- Analysis ---------- canRunAnalysis(): boolean { return !!this.selectedTradingType && this.selectedCompanies.length > 0; } submit(): void { if (this.selectedTradingType == 'swing') { const symbols = [...new Set(this.selectedCompanies)]; if (!this.selectedTradingType) { this.error = 'Please select a trading type.'; return; } if (symbols.length === 0) { this.error = 'Please select at least one company or ticker.'; return; } this.error = null; this.loading = true; this.marketService.getAnalyseResult(symbols) .pipe(finalize(() => (this.loading = false))) .subscribe({ next: (res:any) => this.router.navigate(['/analysepage'], { state: { result: res } }), error: (err:any) => { console.error('API error:', err); this.error = 'Analysis failed. Please try again.'; } }); } else { } } resetAll(): void { this.error = null; // Filters this.selectedCountry = ''; this.selectedExchange = ''; this.selectedIndexName = ''; this.exchanges = []; this.indices = []; this.companies = []; this.resetCompanyLookup(); // Unified selection this.selectedCompanies = []; this.indexSelected = []; this.selectedNameMap.clear(); this.searchSelected.clear(); // Search this.tickerCtrl.setValue(''); } // Chips remove (search picks only) onRemoveChip(symbol: string): void { this.searchSelected.delete(symbol); // hide chip this.selectedCompanies = this.selectedCompanies.filter(s => s !== symbol); // unselect globally // If symbol is part of the current index, reflect in dropdown if (this.currentIndexSymbols.has(symbol)) { this.indexSelected = this.indexSelected.filter(s => s !== symbol); this.allSelected = this.isAllSelected(); } } }