189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import type { TbankShare } from '#src/integrations/tbank/tbank.api';
|
|
|
|
import { selectStockScreenerInstruments } from './stock-screener-selection.rules';
|
|
|
|
const asOf = new Date('2026-10-10T12:00:00.000Z');
|
|
|
|
function share(overrides: Partial<TbankShare> = {}): TbankShare {
|
|
return {
|
|
apiTradeAvailableFlag: true,
|
|
buyAvailableFlag: true,
|
|
currency: 'RUB',
|
|
lot: 10,
|
|
name: 'North Star',
|
|
ticker: 'NSTR',
|
|
uid: 'uid-1',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('selectStockScreenerInstruments', () => {
|
|
it('keeps only instruments matching every selected condition', () => {
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [
|
|
share({ isin: 'A', sector: 'energy' }),
|
|
share({ currency: 'USD', isin: 'B', sector: 'energy', uid: 'uid-2' }),
|
|
share({ isin: 'C', name: 'Other', sector: 'energy', uid: 'uid-3' }),
|
|
share({ isin: 'D', sector: 'finance', uid: 'uid-4' }),
|
|
],
|
|
},
|
|
{ currency: 'RUB', query: 'star', sector: 'energy' },
|
|
);
|
|
|
|
expect(result).toEqual({ asOf, instruments: [share({ isin: 'A', sector: 'energy' })] });
|
|
});
|
|
|
|
it.each([
|
|
['sector', { sector: 'energy' }, share({ sector: undefined })],
|
|
['share type', { shareType: 'common' }, share({ shareType: 'preferred' })],
|
|
['currency', { currency: 'USD' }, share({ currency: undefined })],
|
|
['exchange', { realExchange: 'MOEX' }, share({ realExchange: undefined })],
|
|
['country of risk', { countryOfRisk: 'RU' }, share({ countryOfRisk: 'US' })],
|
|
['query', { query: 'star' }, share({ name: undefined, ticker: undefined })],
|
|
] as const)('excludes an unknown or unequal selected %s', (_condition, conditions, instrument) => {
|
|
const result = selectStockScreenerInstruments(
|
|
{ asOf, instruments: [instrument] },
|
|
conditions,
|
|
);
|
|
|
|
expect(result.instruments).toEqual([]);
|
|
});
|
|
|
|
it('uses an inclusive maximum lot and ignores lots when it is not selected', () => {
|
|
const instruments = [
|
|
share({ isin: 'equal', lot: 10, uid: 'equal' }),
|
|
share({ isin: 'large', lot: 11, uid: 'large' }),
|
|
share({ isin: 'unknown', lot: undefined, uid: 'unknown' }),
|
|
share({ isin: 'zero', lot: 0, uid: 'zero' }),
|
|
];
|
|
|
|
expect(
|
|
selectStockScreenerInstruments({ asOf, instruments }, { maxLot: 10 }).instruments.map(
|
|
({ uid }) => uid,
|
|
),
|
|
).toEqual(['equal']);
|
|
expect(
|
|
selectStockScreenerInstruments({ asOf, instruments }, {}).instruments.map(({ uid }) => uid),
|
|
).toEqual(['equal', 'large', 'unknown', 'zero']);
|
|
});
|
|
|
|
it('chooses the smallest matching uid as the complete representative of an ISIN', () => {
|
|
const chosen = share({ isin: ' same ', lot: 1, name: 'Chosen', uid: 'a' });
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [share({ isin: 'SAME', lot: 99, name: 'Sibling', uid: 'z' }), chosen],
|
|
},
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments).toEqual([chosen]);
|
|
expect(result.instruments[0]).toMatchObject({ lot: 1, name: 'Chosen', uid: 'a' });
|
|
});
|
|
|
|
it('chooses the sole matching mode of an issue', () => {
|
|
const matching = share({ isin: 'same', sector: 'energy', uid: 'matching' });
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [share({ isin: 'same', sector: 'finance', uid: 'smaller-uid' }), matching],
|
|
},
|
|
{ sector: 'energy' },
|
|
);
|
|
|
|
expect(result.instruments).toEqual([matching]);
|
|
});
|
|
|
|
it('keeps different ISIN issues from one issuer separate', () => {
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [
|
|
share({ isin: 'common', shareType: 'common', uid: 'common' }),
|
|
share({ isin: 'preferred', shareType: 'preferred', uid: 'preferred' }),
|
|
],
|
|
},
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments.map(({ uid }) => uid)).toEqual(['common', 'preferred']);
|
|
});
|
|
|
|
it('treats instruments without an ISIN as separate issues', () => {
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [share({ isin: ' ', uid: 'first' }), share({ isin: undefined, uid: 'second' })],
|
|
},
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments.map(({ uid }) => uid)).toEqual(['first', 'second']);
|
|
});
|
|
|
|
it('preserves exact whitespace-differing UIDs as separate blank-ISIN issues', () => {
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [
|
|
share({ isin: ' ', uid: 'uid' }),
|
|
share({ isin: undefined, uid: ' uid ' }),
|
|
],
|
|
},
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments.map(({ uid }) => uid)).toEqual([' uid ', 'uid']);
|
|
});
|
|
|
|
it('returns the exact chosen uid for the later card link', () => {
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [share({ isin: 'same', uid: 'sibling' }), share({ isin: 'SAME', uid: 'chosen' })],
|
|
},
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments[0]?.uid).toBe('chosen');
|
|
});
|
|
|
|
it.each([
|
|
[true, true, 1],
|
|
[true, false, 0],
|
|
[false, true, 0],
|
|
[false, false, 0],
|
|
[true, undefined, 0],
|
|
[undefined, true, 0],
|
|
] as const)(
|
|
'includes only confirmed buy and API availability: buy=%s api=%s',
|
|
(buyAvailableFlag, apiTradeAvailableFlag, expectedCount) => {
|
|
const result = selectStockScreenerInstruments(
|
|
{ asOf, instruments: [share({ apiTradeAvailableFlag, buyAvailableFlag })] },
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments).toHaveLength(expectedCount);
|
|
},
|
|
);
|
|
|
|
it('orders representatives by ticker and then uid before paging', () => {
|
|
const result = selectStockScreenerInstruments(
|
|
{
|
|
asOf,
|
|
instruments: [
|
|
share({ isin: 'c', ticker: 'B', uid: 'z' }),
|
|
share({ isin: 'a', ticker: 'A', uid: 'z' }),
|
|
share({ isin: 'b', ticker: 'A', uid: 'a' }),
|
|
share({ isin: 'empty', ticker: ' ', uid: 'empty' }),
|
|
],
|
|
},
|
|
{},
|
|
);
|
|
|
|
expect(result.instruments.map(({ uid }) => uid)).toEqual(['empty', 'a', 'z', 'z']);
|
|
});
|
|
});
|