py-trade / src /app /toolspage /toolspage.component.ts
Oviya
update feedback fixing
1f75759
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;
}
@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<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();
}
}
}