Spaces:
Running
Running
| 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<string, Record<string, IndexRef[]>>; | |
| interface SearchHit { | |
| symbol: string; | |
| company: string; | |
| indexCode: string; | |
| indexName: string; | |
| exchange: string; | |
| country: string; | |
| } | |
| ({ | |
| 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<string>('', { nonNullable: true }); | |
| filteredCompanies$!: Observable<SearchHit[]>; | |
| 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 <mat-select> (symbols only) */ | |
| indexSelected: string[] = []; | |
| /** Names for chips (search picks only): symbol -> company name */ | |
| private selectedNameMap = new Map<string, string>(); | |
| /** Symbols picked via SEARCH only (used to show chips) */ | |
| private searchSelected = new Set<string>(); | |
| 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<string, string>(); // symbol -> name | |
| private currentIndexSymbols: Set<string> = new Set<string>(); // 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<string>(); | |
| 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<string>(); | |
| 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<string>(); | |
| 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(); | |
| } | |
| } | |
| } | |