Filter non-RUB data at ingest, collapse per-currency collections into single RUB Money, drop currency from TbankPortfolioValuePoint, remove NON_RUB_ACTIVITY and screener currency filter. BREAKING CHANGE: per-currency API fields replaced with single RUB Money; stock screener currency param removed.
101 lines
2.5 KiB
TypeScript
101 lines
2.5 KiB
TypeScript
import type { TbankShare } from '#src/integrations/tbank/tbank.api';
|
|
|
|
export type StockScreenerCatalog = {
|
|
asOf: Date;
|
|
instruments: TbankShare[];
|
|
};
|
|
|
|
export type StockScreenerConditions = {
|
|
countryOfRisk?: string;
|
|
maxLot?: number;
|
|
query?: string;
|
|
realExchange?: string;
|
|
sector?: string;
|
|
shareType?: string;
|
|
};
|
|
|
|
type IdentifiedShare = TbankShare & { uid: string };
|
|
const sortBefore = 0;
|
|
|
|
export function selectStockScreenerInstruments(
|
|
catalog: StockScreenerCatalog,
|
|
conditions: StockScreenerConditions,
|
|
): StockScreenerCatalog {
|
|
const representatives = new Map<string, IdentifiedShare>();
|
|
|
|
for (const instrument of catalog.instruments) {
|
|
if (!matches(instrument, conditions)) continue;
|
|
|
|
const { uid } = instrument;
|
|
const isin = instrument.isin?.trim().toUpperCase();
|
|
const issueKey = isin ? `isin:${isin}` : `uid:${uid}`;
|
|
const existing = representatives.get(issueKey);
|
|
if (!existing || uid.localeCompare(existing.uid) < sortBefore) {
|
|
representatives.set(issueKey, instrument);
|
|
}
|
|
}
|
|
|
|
return {
|
|
asOf: catalog.asOf,
|
|
instruments: [...representatives.values()].sort(
|
|
(left, right) =>
|
|
(left.ticker?.trim() ?? '').localeCompare(right.ticker?.trim() ?? '') ||
|
|
left.uid.localeCompare(right.uid),
|
|
),
|
|
};
|
|
}
|
|
|
|
function matches(
|
|
instrument: TbankShare,
|
|
conditions: StockScreenerConditions,
|
|
): instrument is IdentifiedShare {
|
|
if (
|
|
!instrument.uid?.trim() ||
|
|
instrument.buyAvailableFlag !== true ||
|
|
instrument.apiTradeAvailableFlag !== true
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
conditions.query !== undefined &&
|
|
!`${instrument.name ?? ''}\n${instrument.ticker ?? ''}`
|
|
.toLowerCase()
|
|
.includes(conditions.query.toLowerCase())
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (conditions.sector !== undefined && instrument.sector !== conditions.sector) {
|
|
return false;
|
|
}
|
|
if (
|
|
conditions.shareType !== undefined &&
|
|
instrument.shareType !== conditions.shareType
|
|
) {
|
|
return false;
|
|
}
|
|
if (instrument.currency?.toUpperCase() !== 'RUB') return false;
|
|
if (
|
|
conditions.realExchange !== undefined &&
|
|
instrument.realExchange !== conditions.realExchange
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
conditions.countryOfRisk !== undefined &&
|
|
instrument.countryOfRisk !== conditions.countryOfRisk
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const minimumPositiveLot = 0;
|
|
return (
|
|
conditions.maxLot === undefined ||
|
|
(Number.isInteger(instrument.lot) &&
|
|
instrument.lot !== undefined &&
|
|
instrument.lot > minimumPositiveLot &&
|
|
instrument.lot <= conditions.maxLot)
|
|
);
|
|
}
|