feat: add stock screener
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
import type { TbankShare } from '../../../../../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']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user