phase-8:server code enhancements.

This commit is contained in:
Sai Kiran Vella
2026-06-05 22:44:04 -04:00
parent a7108b448a
commit 1e2aac7164
15 changed files with 781 additions and 94 deletions
+24 -24
View File
@@ -44,24 +44,24 @@ export class ScreenerEngine {
}
async screenTickers(tickers: string[]): Promise<ScreenerResult> {
return this._screenInternal(tickers, false);
return this.screenInternal(tickers, false);
}
async screenWithProgress(tickers: string[]): Promise<ScreenerResult> {
return this._screenInternal(tickers, true);
return this.screenInternal(tickers, true);
}
private async _screenInternal(tickers: string[], showProgress: boolean): Promise<ScreenerResult> {
const marketContext = await this._fetchMarketContext(showProgress);
const results = this._initializeResults();
private async screenInternal(tickers: string[], showProgress: boolean): Promise<ScreenerResult> {
const marketContext = await this.fetchMarketContext(showProgress);
const results = this.initializeResults();
const chunks = chunkArray(tickers, ScreenerEngine.BATCH_SIZE);
let processed = 0;
for (const chunk of chunks) {
await this._processBatch(chunk, marketContext, results);
await this.processBatch(chunk, marketContext, results);
processed += chunk.length;
this._logProgress(showProgress, processed, tickers.length);
await this._rateLimitDelay();
this.logProgress(showProgress, processed, tickers.length);
await this.rateLimitDelay();
}
if (showProgress) {
@@ -71,7 +71,7 @@ export class ScreenerEngine {
return { ...results, marketContext };
}
private async _fetchMarketContext(showProgress: boolean): Promise<MarketContext> {
private async fetchMarketContext(showProgress: boolean): Promise<MarketContext> {
if (showProgress) {
this.logger.write('⏳ Fetching market context...');
}
@@ -82,30 +82,30 @@ export class ScreenerEngine {
return context;
}
private _initializeResults(): Omit<ScreenerResult, 'marketContext'> {
private initializeResults(): Omit<ScreenerResult, 'marketContext'> {
return { STOCK: [], ETF: [], BOND: [], ERROR: [] };
}
private async _processBatch(
private async processBatch(
tickers: string[],
marketContext: MarketContext,
results: Omit<ScreenerResult, 'marketContext'>,
): Promise<void> {
const batch = await Promise.all(tickers.map((t) => this._fetch(t)));
batch.forEach((data) => this._process(data, marketContext, results));
const batch = await Promise.all(tickers.map((t) => this.fetch(t)));
batch.forEach((data) => this.process(data, marketContext, results));
}
private _logProgress(showProgress: boolean, processed: number, total: number): void {
private logProgress(showProgress: boolean, processed: number, total: number): void {
if (showProgress) {
this.logger.write(`\r⏳ Screening tickers... ${processed}/${total}`);
}
}
private async _rateLimitDelay(): Promise<void> {
private async rateLimitDelay(): Promise<void> {
await new Promise<void>((r) => setTimeout(r, ScreenerEngine.BATCH_DELAY_MS));
}
private async _fetch(ticker: string): Promise<MappedData | ErrorResult> {
private async fetch(ticker: string): Promise<MappedData | ErrorResult> {
try {
const summary = await this.client.fetchSummary(ticker);
if (!summary?.price) throw new Error('Empty response from Yahoo');
@@ -115,7 +115,7 @@ export class ScreenerEngine {
}
}
private _process(
private process(
data: MappedData | ErrorResult,
marketContext: MarketContext,
results: Omit<ScreenerResult, 'marketContext'>,
@@ -127,15 +127,15 @@ export class ScreenerEngine {
}
try {
const asset = this._buildAsset(data as MappedData);
const fundamental = this._score(asset, marketContext, SCORE_MODE.FUNDAMENTAL);
const inflated = this._score(asset, marketContext, SCORE_MODE.INFLATED);
const asset = this.buildAsset(data as MappedData);
const fundamental = this.score(asset, marketContext, SCORE_MODE.FUNDAMENTAL);
const inflated = this.score(asset, marketContext, SCORE_MODE.INFLATED);
(results[asset.type as AssetType] as unknown[]).push({
asset,
fundamental,
inflated,
signal: this._signal(fundamental.label, inflated.label),
signal: this.signal(fundamental.label, inflated.label),
});
} catch (err) {
results.ERROR.push({
@@ -147,7 +147,7 @@ export class ScreenerEngine {
// Typed scorer dispatch — instanceof narrows the asset so each scorer receives
// its exact metrics type. No `as never` or unsafe casts required.
private _score(
private score(
asset: Stock | Etf | Bond,
marketContext: MarketContext,
mode: string,
@@ -165,7 +165,7 @@ export class ScreenerEngine {
throw new Error('No scorer for unknown asset type');
}
private _buildAsset(data: Record<string, unknown>): Stock | Etf | Bond {
private buildAsset(data: Record<string, unknown>): Stock | Etf | Bond {
switch (((data.type as string) || ASSET_TYPE.STOCK).toUpperCase()) {
case ASSET_TYPE.BOND:
return new Bond(data as BondData);
@@ -176,7 +176,7 @@ export class ScreenerEngine {
}
}
private _signal(fundamentalLabel: string, inflatedLabel: string): Signal {
private signal(fundamentalLabel: string, inflatedLabel: string): Signal {
const green = (l: string) => l.startsWith('🟢');
const yellow = (l: string) => l.startsWith('🟡');
if (green(fundamentalLabel)) return SIGNAL.STRONG_BUY;